This is a static, read-only snapshot — sliders and other controls will not respond.
» Back to notebook list
·
» Run it yourself
%23%20%2F%2F%2F%20script%0A%23%20%5Btool.marimo.opengraph%5D%0A%23%20title%20%3D%20%22MoselPy%3A%20Timetabling%22%0A%23%20description%20%3D%20%22Nurses%2C%20schools%2C%20exams%2C%20staffing%22%0A%23%20image%20%3D%20%22__marimo__%2Fthumbnail-moselpy-timetabling.svg%22%0A%23%20%2F%2F%2F%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.25.2%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20**FICO%26reg%3B%20Xpress%20MoselPy%20-%20Chapter%20I%3A%20Timetabling%20and%20Personnel**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20***moselpy_timetabling_personnel.py***%20-%20Timetabling%20and%20personnel%20scheduling%20problems%20from%20the%20Xpress%20Applications%20Book%20-%20worker%20assignment%2C%20nurse%20scheduling%2C%20school%20timetabling%2C%20exam%20scheduling%2C%20production%20planning%20with%20transfers%2C%20and%20construction%20site%20staffing%2C%20solved%20with%20MoselPy.%0A%0A%20%20%20%20%26copy%3B%20Copyright%202026%20Fair%20Isaac%20Corporation.%20The%20use%20of%20this%20example%20is%20subject%20to%20%5Blegal%20and%20license%20requirements%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fxpress-community%23legal-and-license-requirements).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20cell%20below%20imports%20the%20necessary%20libraries%20and%20set%20%60MODELS%60%20and%20%60DATA%60%2C%20the%20path%20variables%20pointing%20to%20the%20Mosel%20model%20files%20and%20data%20files%20used%20in%20this%20notebook.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20%23%20On%20Linux%2C%20preload%20the%20Xpress%20solver%20library%20so%20the%20Mosel%20optimizer%20module%0A%20%20%20%20%23%20(mmxprs)%20can%20resolve%20it%20at%20runtime.%0A%20%20%20%20import%20sys%2C%20os%2C%20glob%2C%20ctypes%2C%20xpresslibs%0A%20%20%20%20if%20sys.platform%20%3D%3D%20%22linux%22%3A%0A%20%20%20%20%20%20%20%20for%20_lib%20in%20glob.glob(os.path.join(xpresslibs.solver_libs_dir%2C%20%22libxprs.so*%22))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20ctypes.CDLL(_lib%2C%20mode%3Dctypes.RTLD_GLOBAL)%0A%0A%20%20%20%20import%20moselpy%20as%20mp%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20matplotlib.patches%20as%20mpatches%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20pandas%20as%20pd%0A%0A%20%20%20%20_root%20%3D%20mo.notebook_dir()%20%2F%20%22public%22%20%2F%20%22moselpy%22%0A%20%20%20%20MODELS%20%3D%20str(_root%20%2F%20%22models%22%20%2F%20%22timetabling_personnel%22)%0A%20%20%20%20DATA%20%20%20%3D%20str(_root%20%2F%20%22data%22%20%2F%20%22timetabling_personnel%22)%0A%0A%20%20%20%20print(f%22MoselPy%20%7Bmp.version%7D%20%20%7C%20%20Mosel%20%7Bmp.mosel_version%7D%20%20%7C%20%20Xpress%20%7Bmp.xpress_version%7D%22)%0A%20%20%20%20mp.config.set_default_stream(mp.StreamType.OUTPUT%2C%20%22null%3A%22)%0A%20%20%20%20return%20MODELS%2C%20mp%2C%20np%2C%20pd%2C%20plt%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Timetabling%20and%20personnel%20scheduling%20problems%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20This%20notebook%20covers%20six%20IP%20examples%20from%20Chapter%20I%20of%20the%20Xpress%20Applications%20Book%2C%20solved%20via%20%5BMoselPy%5D(https%3A%2F%2Fpypi.org%2Fproject%2Fmoselpy%2F).%20Examples%20include%20worker-to-machine%20assignment%2C%20nurse%20scheduling%20with%20cyclic%20shifts%2C%20school%20timetabling%2C%20exam%20scheduling%2C%20production%20planning%20with%20personnel%20transfers%2C%20and%20construction%20site%20staffing.%0A%0A%20%20%20%20%3E%20**Mosel%20source%20files%3A**%20%5B%60models%2Ftimetabling_personnel%2F%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Ftree%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel)%20%26nbsp%3B%7C%26nbsp%3B%20**Data%20files%3A**%20%5B%60data%2Ftimetabling_personnel%2F%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Ftree%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fdata%2Ftimetabling_personnel)%0A%0A%20%20%20%20%3E%20**Requirements%3A**%20%60pip%20install%20moselpy%20mosellibs%20xpresslibs%60%20-%20%60mosellibs%60%20provides%20the%20Mosel%20runtime%20and%20%60xpresslibs%60%20the%20Xpress%20solver%20libraries.%20No%20local%20Xpress%20installation%20is%20needed%20to%20run%20these%20notebooks%2C%20as%20long%20as%20the%20packages%20with%20libraries%20have%20been%20installed.%0A%0A%20%20%20%20%3E%20A%20local%20FICO%26reg%3B%20Xpress%20installation%20is%20recommended%20if%20you%20want%20access%20to%20the%20full%20set%20of%20examples%2C%20user%20guides%20and%20documentation%2C%20solver%20libraries%2C%20and%20Xpress%20Workbench.%20Obtain%20one%20via%20the%20%5BFICO%26reg%3B%20Xpress%20community%20license%5D(https%3A%2F%2Fwww.fico.com%2Fen%2Ffico-xpress-community-license)%20page.%20If%20a%20local%20installation%20is%20present%2C%20set%20%60XPRESSDIR%60%20before%20starting%20marimo%20to%20use%20it%20instead%20of%20the%20%60mosellibs%60%20and%20%60xpresslibs%60%20packages.%0A%0A%20%20%20%20%3E%20**MoselPy%20documentation%3A**%20%5BMoselPy%20User%20Guide%20and%20Reference%20Manual%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fmosel%2FMoselPy%2Fdhtml)%0A%0A%20%20%20%20%3E%20**Mosel%20examples%20on%20the%20web%3A**%20%5Bexamples.xpress.fico.com%5D(https%3A%2F%2Fexamples.xpress.fico.com%2Fexample.pl%3Fid%3Dmosel_app_9)%0A%0A%20%20%20%20%23%23%23%20MoselPy%20features%20demonstrated%20in%20this%20chapter%0A%0A%20%20%20%20-%20**Two%20sequential%20solves%20in%20one%20call**%3A%20the%20model%20solves%20LP%20relaxation%20then%20MIP%20(I-1)%2C%20or%20solves%20with%20then%20without%20a%20constraint%20(I-2%2C%20I-5)%2C%20returning%20all%20results%20from%20a%20single%20%60execute_model%60.%0A%20%20%20%20-%20**Named%20%60linctr%60%20constraints%20reassigned%20between%20solves**%3A%20%60Load%60%20in%20I-5%20and%20%60Period%60%20in%20I-2%20are%20redefined%20in-place%20without%20rebuilding%20the%20model.%0A%20%20%20%20-%20**Dynamic%20arrays%20with%20%60exists()%60%20and%20%60create()%60**%3A%20transfer%20variables%20created%20only%20for%20permitted%20line%20pairs%20(I-5).%0A%20%20%20%20-%20**Feasibility%20solve**%3A%20%60minimize(0)%60%20finds%20any%20feasible%20timetable%3B%20%60is_solution_available%60%20confirms%20feasibility%20(I-4).%0A%20%20%20%20-%20**3D%20array%20result%20from%20Python**%3A%20%60sol_teach%60%20indexed%20by%20(teacher%2C%20class%2C%20slot)%20reconstructed%20as%20a%20weekly%20grid%20(I-3).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-1%20Personnel%20assignment%20(IP)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%201*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i1assign_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi1assign_p.mos)%0A%0A%20%20%20%20Six%20operators%20must%20each%20be%20assigned%20to%20one%20of%20six%20machines%20in%20a%20workshop.%20Two%20objectives%20are%20considered%3A%20(1)%20**parallel%20machines**%20-%20maximize%20total%20productivity%20across%20all%20assignments%3B%20(2)%20**serial%20machines**%20-%20maximize%20the%20bottleneck%20(minimum)%20productivity%2C%20since%20overall%20throughput%20is%20limited%20by%20the%20slowest%20assignment.%0A%0A%20%20%20%20The%20model%20solves%20both%20problems%20in%20sequence%3A%20the%20LP%20relaxation%20of%20the%20parallel%20problem%20is%20solved%20first%20(which%20happens%20to%20produce%20integer%20values%20due%20to%20total%20unimodularity)%2C%20then%20binary%20constraints%20and%20the%20bottleneck%20variable%20are%20added%20for%20the%20serial%20solve.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPERS%7D%20%3D%201..6%24%2C%20%24%5Ctext%7BMACH%7D%20%3D%201..6%24%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BOUTP%7D_%7Bp%2Cm%7D%24%20-%20productivity%20of%20person%20%24p%24%20on%20machine%20%24m%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bassign%7D_%7Bp%2Cm%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20person%20%24p%24%20assigned%20to%20machine%20%24m%24%3B%20%24%5Cmathit%7Bpmin%7D%24%20-%20minimum%20productivity%20(serial%20problem%20only)%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Cbegin%7Balign*%7D%0A%20%20%20%20%5Ctext%7B(parallel)%7D%20%5Cquad%20%5Ctext%7Bmaximize%7D%20%5Cquad%20%26%20%5Csum_%7Bp%2Cm%7D%20%5Ctext%7BOUTP%7D_%7Bp%2Cm%7D%20%5Ccdot%20%5Cmathit%7Bassign%7D_%7Bp%2Cm%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7B(serial)%7D%20%20%20%5Cquad%20%5Ctext%7Bmaximize%7D%20%5Cquad%20%26%20%5Cmathit%7Bpmin%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Csum_m%20%5Cmathit%7Bassign%7D_%7Bp%2Cm%7D%20%3D%201%20%5Cquad%20%5Cforall%5C%2C%20p%20%5C%5C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%26%20%5Csum_p%20%5Cmathit%7Bassign%7D_%7Bp%2Cm%7D%20%3D%201%20%5Cquad%20%5Cforall%5C%2C%20m%20%5C%5C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%26%20%5Csum_m%20%5Ctext%7BOUTP%7D_%7Bp%2Cm%7D%20%5Ccdot%20%5Cmathit%7Bassign%7D_%7Bp%2Cm%7D%20%5Cgeq%20%5Cmathit%7Bpmin%7D%20%5Cquad%20%5Cforall%5C%2C%20p%20%5Cquad%20%5Ctext%7B(serial%20only)%7D%0A%20%20%20%20%5Cend%7Balign*%7D%0A%20%20%20%20%24%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20the%206x6%20productivity%20matrix%20as%20a%20dict%20with%20%60(person%2C%20machine)%60%20tuple%20keys.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(pd)%3A%0A%20%20%20%20OUTP%20%3D%20%7B%0A%20%20%20%20%20%20%20%20(1%2C1)%3A13%2C(1%2C2)%3A24%2C(1%2C3)%3A31%2C(1%2C4)%3A19%2C(1%2C5)%3A40%2C(1%2C6)%3A29%2C%0A%20%20%20%20%20%20%20%20(2%2C1)%3A18%2C(2%2C2)%3A25%2C(2%2C3)%3A30%2C(2%2C4)%3A15%2C(2%2C5)%3A43%2C(2%2C6)%3A22%2C%0A%20%20%20%20%20%20%20%20(3%2C1)%3A20%2C(3%2C2)%3A20%2C(3%2C3)%3A27%2C(3%2C4)%3A25%2C(3%2C5)%3A34%2C(3%2C6)%3A33%2C%0A%20%20%20%20%20%20%20%20(4%2C1)%3A23%2C(4%2C2)%3A26%2C(4%2C3)%3A28%2C(4%2C4)%3A18%2C(4%2C5)%3A37%2C(4%2C6)%3A30%2C%0A%20%20%20%20%20%20%20%20(5%2C1)%3A28%2C(5%2C2)%3A33%2C(5%2C3)%3A34%2C(5%2C4)%3A17%2C(5%2C5)%3A38%2C(5%2C6)%3A20%2C%0A%20%20%20%20%20%20%20%20(6%2C1)%3A19%2C(6%2C2)%3A36%2C(6%2C3)%3A25%2C(6%2C4)%3A27%2C(6%2C5)%3A45%2C(6%2C6)%3A24%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20outp_df%20%3D%20pd.DataFrame(%0A%20%20%20%20%20%20%20%20%7Bf%22M%7Bm%7D%22%3A%20%5BOUTP%5B(p%2Cm)%5D%20for%20p%20in%20range(1%2C7)%5D%20for%20m%20in%20range(1%2C7)%7D%2C%0A%20%20%20%20%20%20%20%20index%3D%5Bf%22P%7Bp%7D%22%20for%20p%20in%20range(1%2C7)%5D%2C%0A%20%20%20%20)%0A%20%20%20%20outp_df.index.name%20%3D%20%22Person%22%0A%20%20%20%20print(%22Productivity%20matrix%20(person%20x%20machine)%3A%22)%0A%20%20%20%20print(outp_df.to_string())%0A%20%20%20%20return%20(OUTP%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20single%20execution%20command.%20The%20model%20solves%20both%20the%20parallel%20and%20serial%20problems%20in%20a%20single%20call%2C%20returning%20%60obj_parallel%60%2C%20%60obj_serial%60%2C%20%60par_sol%60%2C%20and%20%60ser_sol%60%20(each%20a%20dict%20with%20%60(person%2C%20machine)%60%20keys).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MODELS%2C%20OUTP%2C%20mp)%3A%0A%20%20%20%20model%2C%20_output%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Fi1assign_p.mos'%2C%20exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'OUTP'%3A%20OUTP%7D)%0A%20%20%20%20if%20model.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_par%20%3D%20_output%5B'obj_parallel'%5D%0A%20%20%20%20%20%20%20%20obj_ser%20%3D%20_output%5B'obj_serial'%5D%0A%20%20%20%20%20%20%20%20par_sol%20%3D%20_output%5B'par_sol'%5D%0A%20%20%20%20%20%20%20%20ser_sol%20%3D%20_output%5B'ser_sol'%5D%0A%20%20%20%20%20%20%20%20outp_out%20%3D%20_output%5B'OUTP'%5D%0A%20%20%20%20%20%20%20%20print(f'Parallel%20machines%20-%20total%20productivity%3A%20%7Bobj_par%3A.0f%7D')%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%207)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_m%20%3D%20next((m%20for%20m%20in%20range(1%2C%207)%20if%20par_sol.get((_p%2C%20m)%2C%200)%20%3E%200.5))%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Person%20%7B_p%7D%20-%3E%20Machine%20%7B_m%7D%20%20(%7BOUTP%5B_p%2C%20_m%5D%7D)')%0A%20%20%20%20%20%20%20%20print(f'%5CnSerial%20machines%20-%20bottleneck%20productivity%3A%20%7Bobj_ser%3A.0f%7D')%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%207)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_m%20%3D%20next((m%20for%20m%20in%20range(1%2C%207)%20if%20ser_sol.get((_p%2C%20m)%2C%200)%20%3E%200.5))%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Person%20%7B_p%7D%20-%3E%20Machine%20%7B_m%7D%20%20(%7BOUTP%5B_p%2C%20_m%5D%7D)')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel.prob_status%7D)')%0A%20%20%20%20return%20obj_par%2C%20obj_ser%2C%20par_sol%2C%20ser_sol%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Side-by-side%20assignment%20matrices%20showing%20which%20person%20operates%20which%20machine%20for%20the%20parallel%20and%20serial%20solutions.%20Cell%20colour%20reflects%20productivity.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(OUTP%2C%20mo%2C%20np%2C%20obj_par%2C%20obj_ser%2C%20par_sol%2C%20plt%2C%20ser_sol)%3A%0A%20%20%20%20if%20'par_sol'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(12%2C%204))%0A%20%20%20%20%20%20%20%20for%20_ax%2C%20sol%2C%20_title%20in%20%5B(_axes%5B0%5D%2C%20par_sol%2C%20f'I-1%20Parallel%20(total%3D%7Bobj_par%3A.0f%7D)')%2C%20(_axes%5B1%5D%2C%20ser_sol%2C%20f'I-1%20Serial%20(bottleneck%3D%7Bobj_ser%3A.0f%7D)')%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20matrix%20%3D%20np.array(%5B%5BOUTP%5Bp%2C%20m%5D%20*%20(sol.get((p%2C%20m)%2C%200)%20%3E%200.5)%20for%20m%20in%20range(1%2C%207)%5D%20for%20p%20in%20range(1%2C%207)%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20_im%20%3D%20_ax.imshow(matrix%2C%20cmap%3D'Blues'%2C%20aspect%3D'auto')%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.colorbar(_im%2C%20ax%3D_ax%2C%20label%3D'Productivity')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticks(range(6))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticklabels(%5Bf'M%7Bm%7D'%20for%20m%20in%20range(1%2C%207)%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_yticks(range(6))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_yticklabels(%5Bf'P%7Bp%7D'%20for%20p%20in%20range(1%2C%207)%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_p%20in%20range(6)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_m%20in%20range(6)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20matrix%5B_p%2C%20_m%5D%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(_m%2C%20_p%2C%20str(matrix%5B_p%2C%20_m%5D)%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D9%2C%20color%3D'white')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_title(_title)%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20assignment%20chart')%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-2%20Nurse%20scheduling%20(IP)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%202*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i2nurse_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi2nurse_p.mos)%0A%0A%20%20%20%20A%20hospital%20schedules%20nurses%20across%2012%20two-hour%20periods%20in%20a%20day.%20Nurses%20work%208%20consecutive%20hours%20(4%20periods)%20with%20a%202-hour%20break%20after%206%20hours%3B%20they%20may%20start%20in%20any%20period.%20The%20model%20first%20minimizes%20total%20nurse%20headcount.%20A%20second%20variant%20caps%20the%20total%20at%2080%20nurses%20and%20minimizes%20overtime%20(nurses%20who%20extend%20shifts%20into%20an%20adjacent%20period).%0A%0A%20%20%20%20The%20demand%20constraints%20are%20modeled%20as%20named%20%60linctr%60%20objects%20(%60Period%60)%20which%20are%20reassigned%20in-place%20when%20extending%20the%20model%20for%20overtime%2C%20without%20removing%20and%20recreating%20constraints.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPERIODS%7D%20%3D%20%5C%7B0..11%5C%7D%24%2C%20%24%5Ctext%7BWORK%7D%20%3D%20%5C%7B0%2C-1%2C-3%2C-4%5C%7D%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bstart%7D_t%20%5Cin%20%5Cmathbb%7BZ%7D_%7B%5Cgeq%200%7D%24%20-%20nurses%20starting%20in%20period%20%24t%24%3B%20%24%5Cmathit%7Bovert%7D_t%24%20-%20nurses%20working%20overtime%20in%20period%20%24t%24%20(variant%202)%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Cbegin%7Balign*%7D%0A%20%20%20%20%5Ctext%7B(v1)%7D%20%5Cquad%20%5Ctext%7Bminimize%7D%20%5Cquad%20%26%20%5Csum_t%20%5Cmathit%7Bstart%7D_t%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Csum_%7Bi%20%5Cin%20%5Ctext%7BWORK%7D%7D%20%5Cmathit%7Bstart%7D_%7B(t%2Bi)%20%5Cbmod%2012%7D%20%5Cgeq%20%5Ctext%7BREQ%7D_t%20%5Cquad%20%5Cforall%5C%2C%20t%0A%20%20%20%20%5Cend%7Balign*%7D%0A%20%20%20%20%24%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20hourly%20nurse%20requirements%20as%20a%200-indexed%20dict.%20The%20second%20variant's%20staff%20cap%20is%20set%20via%20%60exec_params%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20REQ%20%3D%20%7B0%3A%2015%2C%201%3A%2015%2C%202%3A%2015%2C%203%3A%2035%2C%204%3A%2040%2C%205%3A%2040%2C%206%3A%2035%2C%207%3A%2030%2C%208%3A%2030%2C%209%3A%2035%2C%2010%3A%2030%2C%2011%3A%2020%7D%0A%20%20%20%20NUM%20%3D%2080%20%20%23%20staff%20cap%20for%20variant%202%0A%20%20%20%20period_labels%20%3D%20%5Bf'%7Bt%20*%202%3A02d%7D%3A00'%20for%20t%20in%20range(12)%5D%0A%20%20%20%20print('Nurse%20requirements%20per%202-hour%20period%3A')%0A%20%20%20%20for%20_t%20in%20range(12)%3A%0A%20%20%20%20%20%20%20%20print(f'%20%20%7Bperiod_labels%5B_t%5D%7D-%7B(_t%20%2B%201)%20*%202%3A02d%7D%3A00%3A%20%7BREQ%5B_t%5D%3A%3E3%7D%20nurses%20required')%0A%20%20%20%20return%20NUM%2C%20REQ%2C%20period_labels%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20compile-load-run%20sequence%20to%20allow%20rerunning%20with%20different%20%60NUM%60%20values.%20The%20model%20solves%20both%20variants%20in%20a%20single%20%60run()%60%20call%2C%20returning%20results%20for%20minimum%20headcount%20(%60obj_total%60%2C%20%60sol_start1%60)%20and%20minimum%20overtime%20(%60obj_overt%60%2C%20%60sol_start2%60%2C%20%60sol_overt%60).%0A%0A%20%20%20%20Named%20%60linctr%60%20constraints%20(%60Period%60)%20are%20redefined%20inside%20the%20model%20between%20the%20two%20solves%20-%20this%20is%20a%20key%20Mosel%20capability%20for%20progressively%20tightening%20or%20modifying%20a%20formulation%20without%20starting%20over.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MODELS%2C%20NUM%2C%20REQ%2C%20mp%2C%20period_labels)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Fi2nurse_p.mos'%2C%20f'%7BMODELS%7D%2Fi2nurse_p.bim')%0A%20%20%20%20model_1%20%3D%20mp.load_model(f'%7BMODELS%7D%2Fi2nurse_p.bim')%0A%20%20%20%20model_1.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20_output%20%3D%20model_1.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%2C%20'NUM'%3A%20NUM%7D%2C%20input_data%3D%7B'REQ'%3A%20REQ%7D)%0A%20%20%20%20if%20model_1.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_total%20%3D%20_output%5B'obj_total'%5D%0A%20%20%20%20%20%20%20%20obj_overt%20%3D%20_output%5B'obj_overt'%5D%0A%20%20%20%20%20%20%20%20sol_start1%20%3D%20_output%5B'sol_start1'%5D%0A%20%20%20%20%20%20%20%20sol_start2%20%3D%20_output%5B'sol_start2'%5D%0A%20%20%20%20%20%20%20%20sol_overt%20%3D%20_output%5B'sol_overt'%5D%0A%20%20%20%20%20%20%20%20print(f'Variant%201%20-%20minimum%20total%20nurses%3A%20%7Bobj_total%3A.0f%7D')%0A%20%20%20%20%20%20%20%20print(f'Variant%202%20-%20overtime%20nurses%20(cap%3D%7BNUM%7D)%3A%20%7Bobj_overt%3A.0f%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'%7B'Period'%3A%3C12%7D%20%7B'Req'%3A%3E4%7D%20%7B'Start%20v1'%3A%3E9%7D%20%7B'Start%20v2'%3A%3E9%7D%20%7B'Overtime'%3A%3E9%7D')%0A%20%20%20%20%20%20%20%20for%20_t%20in%20range(12)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_s1%20%3D%20round(sol_start1.get(_t%2C%200))%0A%20%20%20%20%20%20%20%20%20%20%20%20_s2%20%3D%20round(sol_start2.get(_t%2C%200))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ov%20%3D%20round(sol_overt.get(_t%2C%200))%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20%7Bperiod_labels%5B_t%5D%3A%3C10%7D%20%7BREQ%5B_t%5D%3A%3E4%7D%20%7B_s1%3A%3E9%7D%20%7B_s2%3A%3E9%7D%20%7B_ov%3A%3E9%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel_1.prob_status%7D)')%0A%20%20%20%20return%20obj_overt%2C%20obj_total%2C%20sol_overt%2C%20sol_start1%2C%20sol_start2%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Two-panel%20chart%20for%20both%20variants.%20Bars%20show%20nurses%20*starting*%20their%20shift%20in%20each%20period%3B%20these%20are%20always%20lower%20than%20the%20requirement%20because%20a%20single%20nurse%20covers%204%20consecutive%20periods%20(at%20offsets%200%2C%201%2C%203%2C%204%20from%20start%20time).%20The%20orange%20line%20shows%20total%20*active*%20nurses%20per%20period%20(summing%20starters%20at%20those%20offsets)%2C%20which%20meets%20or%20exceeds%20the%20red%20requirement%20line%20at%20every%20period.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20NUM%2C%0A%20%20%20%20REQ%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20obj_overt%2C%0A%20%20%20%20obj_total%2C%0A%20%20%20%20period_labels%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20sol_overt%2C%0A%20%20%20%20sol_start1%2C%0A%20%20%20%20sol_start2%2C%0A)%3A%0A%20%20%20%20if%20'sol_start1'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_s1%20%3D%20%5Bround(sol_start1.get(t%2C%200))%20for%20t%20in%20range(12)%5D%0A%20%20%20%20%20%20%20%20_s2%20%3D%20%5Bround(sol_start2.get(t%2C%200))%20for%20t%20in%20range(12)%5D%0A%20%20%20%20%20%20%20%20_ov%20%3D%20%5Bround(sol_overt.get(t%2C%200))%20for%20t%20in%20range(12)%5D%0A%20%20%20%20%20%20%20%20req%20%3D%20%5BREQ%5Bt%5D%20for%20t%20in%20range(12)%5D%0A%20%20%20%20%20%20%20%20offsets%20%3D%20%5B0%2C%20-1%2C%20-3%2C%20-4%5D%0A%20%20%20%20%20%20%20%20active1%20%3D%20%5Bsum((_s1%5B(t%20%2B%20o)%20%25%2012%5D%20for%20o%20in%20offsets))%20for%20t%20in%20range(12)%5D%20%20%23%20Active%20nurses%20per%20period%3A%20sum%20of%20starters%20at%20offsets%20%7B0%2C-1%2C-3%2C-4%7D%0A%20%20%20%20%20%20%20%20active2%20%3D%20%5Bsum((_s2%5B(t%20%2B%20o)%20%25%2012%5D%20%2B%20_ov%5B(t%20%2B%20o)%20%25%2012%5D%20for%20o%20in%20offsets))%20for%20t%20in%20range(12)%5D%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(13%2C%204))%0A%20%20%20%20%20%20%20%20for%20_ax%2C%20starts%2C%20overt_vals%2C%20active%2C%20_title%20in%20%5B(_axes%5B0%5D%2C%20_s1%2C%20%5B0%5D%20*%2012%2C%20active1%2C%20f'Variant%201%20(total%3D%7Bobj_total%3A.0f%7D)')%2C%20(_axes%5B1%5D%2C%20_s2%2C%20_ov%2C%20active2%2C%20f'Variant%202%20(overtime%3D%7Bobj_overt%3A.0f%7D%2C%20cap%3D%7BNUM%7D)')%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.bar(range(12)%2C%20starts%2C%20color%3D'%231E88E5'%2C%20label%3D'Nurses%20starting'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.bar(range(12)%2C%20overt_vals%2C%20bottom%3Dstarts%2C%20color%3D'%23FB8C00'%2C%20label%3D'Overtime'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.step(range(12)%2C%20req%2C%20where%3D'mid'%2C%20color%3D'%23E53935'%2C%20linewidth%3D2%2C%20label%3D'Requirement')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.step(range(12)%2C%20active%2C%20where%3D'mid'%2C%20color%3D'%23FB8C00'%2C%20linewidth%3D2%2C%20linestyle%3D'--'%2C%20label%3D'Active%20nurses')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticks(range(12))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticklabels(period_labels%2C%20rotation%3D45%2C%20ha%3D'right'%2C%20fontsize%3D7)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_ylabel('Nurses')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_title(f'I-2%20Nurse%20scheduling%20-%20%7B_title%7D')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.legend(fontsize%3D8)%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20nurse%20schedule%20chart')%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-3%20School%20timetable%20(IP)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%203*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i3school_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi3school_p.mos)%0A%0A%20%20%20%20A%20weekly%20timetable%20must%20be%20determined%20for%20two%20classes%20sharing%20nine%20teachers.%20Teachers%20have%20specific%20lesson%20counts%20per%20class%20and%20subject.%20The%20objective%20minimizes%20'holes'%20-%20courses%20scheduled%20at%20the%20start%20or%20end%20of%20a%20day%2C%20which%20leave%20gaps%20in%20the%20students'%20timetable.%20Several%20hard%20constraints%20encode%20specific%20scheduling%20requirements%20(sport%20on%20Thursday%20afternoon%2C%20no%20Monday%20morning%20lessons%2C%20etc.).%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BTEACHERS%7D%24%20(9%20teachers%2C%20auto-populated)%2C%20%24%5Ctext%7BCLASS%7D%20%3D%20%5C%7B1%2C2%5C%7D%24%2C%20%24%5Ctext%7BSLOTS%7D%20%3D%201..20%24%20(5%20days%20x%204%20periods)%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BCOURSE%7D_%7Bt%2Cc%7D%24%20-%20lessons%20teacher%20%24t%24%20must%20give%20to%20class%20%24c%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bteach%7D_%7Bt%2Cc%2Cl%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20teacher%20%24t%24%20gives%20class%20%24c%24%20in%20slot%20%24l%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20the%20lesson%20counts%20as%20a%20dict%20with%20%60(teacher_name%2C%20class)%60%20tuple%20keys.%20The%20%60TEACHERS%60%20set%20of%20strings%20auto-populates%20from%20the%20dict%20keys.%20The%20slot%20structure%20uses%204%20periods%20per%20day%20across%205%20days%20(slots%201-4%20%3D%20Monday%2C%205-8%20%3D%20Tuesday%2C%20...%2C%2017-20%20%3D%20Friday).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20COURSE%20%3D%20%7B%0A%20%20%20%20%20%20%20%20(%22Mr%20Cheese%22%2C%20%20%20%201)%3A%201%2C%20(%22Mr%20Cheese%22%2C%20%20%20%202)%3A%201%2C%0A%20%20%20%20%20%20%20%20(%22Mrs%20Insulin%22%2C%20%201)%3A%203%2C%20(%22Mrs%20Insulin%22%2C%20%202)%3A%203%2C%0A%20%20%20%20%20%20%20%20(%22Mr%20Map%22%2C%20%20%20%20%20%20%201)%3A%202%2C%20(%22Mr%20Map%22%2C%20%20%20%20%20%20%202)%3A%202%2C%0A%20%20%20%20%20%20%20%20(%22Mr%20Effofecks%22%2C%201)%3A%200%2C%20(%22Mr%20Effofecks%22%2C%202)%3A%204%2C%0A%20%20%20%20%20%20%20%20(%22Mrs%20Derivate%22%2C%201)%3A%204%2C%20(%22Mrs%20Derivate%22%2C%202)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22Mrs%20Electron%22%2C%201)%3A%203%2C%20(%22Mrs%20Electron%22%2C%202)%3A%203%2C%0A%20%20%20%20%20%20%20%20(%22Mr%20Wise%22%2C%20%20%20%20%20%201)%3A%201%2C%20(%22Mr%20Wise%22%2C%20%20%20%20%20%202)%3A%201%2C%0A%20%20%20%20%20%20%20%20(%22Mr%20Muscle%22%2C%20%20%20%201)%3A%201%2C%20(%22Mr%20Muscle%22%2C%20%20%20%202)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22Mrs%20Biceps%22%2C%20%20%201)%3A%200%2C%20(%22Mrs%20Biceps%22%2C%20%20%202)%3A%201%2C%0A%20%20%20%20%7D%0A%20%20%20%20TEACHERS%20%3D%20sorted(%7Bt%20for%20(t%2C%20_)%20in%20COURSE%7D)%0A%20%20%20%20NP%2C%20ND%20%3D%204%2C%205%20%20%23%20periods%20per%20day%2C%20days%20per%20week%0A%20%20%20%20DAY_NAMES%20%3D%20%5B%22Mon%22%2C%20%22Tue%22%2C%20%22Wed%22%2C%20%22Thu%22%2C%20%22Fri%22%5D%0A%0A%20%20%20%20total_lessons%20%3D%20%7Bc%3A%20sum(COURSE.get((t%2Cc)%2C0)%20for%20t%20in%20TEACHERS)%20for%20c%20in%20%5B1%2C2%5D%7D%0A%20%20%20%20print(f%22Total%20lessons%3A%20Class%201%20%3D%20%7Btotal_lessons%5B1%5D%7D%2C%20Class%202%20%3D%20%7Btotal_lessons%5B2%5D%7D%22)%0A%20%20%20%20print(f%22Slots%20%3D%20%7BNP%7D%20periods%2Fday%20x%20%7BND%7D%20days%20%3D%20%7BNP*ND%7D%20total%22)%0A%20%20%20%20return%20COURSE%2C%20DAY_NAMES%2C%20ND%2C%20NP%2C%20TEACHERS%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20single%20execution%20command.%20%60COURSE%60%20is%20the%20only%20required%20input%3B%20the%20%60TEACHERS%60%20set%20auto-populates%20from%20the%20dict%20keys.%20The%20result%20%60sol_teach%60%20is%20a%20dict%20with%20%60(teacher%2C%20class%2C%20slot)%60%20keys.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(COURSE%2C%20DAY_NAMES%2C%20MODELS%2C%20ND%2C%20NP%2C%20TEACHERS%2C%20mp)%3A%0A%20%20%20%20model_2%2C%20_output%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Fi3school_p.mos'%2C%20exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'COURSE'%3A%20COURSE%7D)%0A%20%20%20%20if%20model_2.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_val_i3%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20sol_teach%20%3D%20_output%5B'sol_teach'%5D%0A%20%20%20%20%20%20%20%20print(f'Courses%20at%20start%2Fend%20of%20day%20(holes)%3A%20%7Bobj_val_i3%3A.0f%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20for%20_cls%20in%20%5B1%2C%202%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'Class%20%7B_cls%7D%20timetable%3A')%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_d%20in%20range(ND)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20day_slots%20%3D%20range(_d%20*%20NP%20%2B%201%2C%20(_d%20%2B%201)%20*%20NP%20%2B%201)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20assigned%20%3D%20%7B%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_l%20in%20day_slots%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_t%20in%20TEACHERS%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20sol_teach.get((_t%2C%20_cls%2C%20_l)%2C%200)%20%3E%200.5%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20assigned%5B_l%5D%20%3D%20_t%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20slot_strs%20%3D%20%5Bf'%20%20%7Bassigned.get(l%2C%20'(free)')%3A%3C18%7D'%20for%20l%20in%20day_slots%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20%7BDAY_NAMES%5B_d%5D%7D%3A%20%7B''.join(slot_strs)%7D')%0A%20%20%20%20%20%20%20%20%20%20%20%20print()%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel_2.prob_status%7D)')%0A%20%20%20%20return%20obj_val_i3%2C%20sol_teach%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Timetable%20grid%20for%20each%20class%3A%20rows%20%3D%20days%2C%20columns%20%3D%20periods.%20Colored%20cells%20show%20which%20teacher%20is%20assigned%3B%20empty%20cells%20are%20free%20periods.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DAY_NAMES%2C%20ND%2C%20NP%2C%20TEACHERS%2C%20mo%2C%20np%2C%20obj_val_i3%2C%20plt%2C%20sol_teach)%3A%0A%20%20%20%20if%20'sol_teach'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20teacher_colors%20%3D%20%7Bt%3A%20plt.cm.tab20(i%20%2F%20len(TEACHERS))%20for%20i%2C%20t%20in%20enumerate(sorted(TEACHERS))%7D%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(14%2C%205))%0A%20%20%20%20%20%20%20%20for%20_cls%2C%20_ax%20in%20enumerate(%5B_axes%5B0%5D%2C%20_axes%5B1%5D%5D%2C%20start%3D1)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20grid%20%3D%20np.zeros((ND%2C%20NP))%0A%20%20%20%20%20%20%20%20%20%20%20%20annotations%20%3D%20%5B%5B''%20for%20_%20in%20range(NP)%5D%20for%20_%20in%20range(ND)%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20cell_colors%20%3D%20%5B%5B'white'%20for%20_%20in%20range(NP)%5D%20for%20_%20in%20range(ND)%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_d%20in%20range(ND)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_p%20in%20range(NP)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_l%20%3D%20_d%20*%20NP%20%2B%20_p%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_t%20in%20TEACHERS%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20sol_teach.get((_t%2C%20_cls%2C%20_l)%2C%200)%20%3E%200.5%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20annotations%5B_d%5D%5B_p%5D%20%3D%20_t.split()%5B-1%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cell_colors%5B_d%5D%5B_p%5D%20%3D%20teacher_colors%5B_t%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xlim(0%2C%20NP)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_ylim(0%2C%20ND)%20%20%23%20last%20name%20only%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticks(range(NP))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xticklabels(%5Bf'P%7Bp%20%2B%201%7D'%20for%20p%20in%20range(NP)%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_yticks(range(ND))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_yticklabels(DAY_NAMES)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.invert_yaxis()%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_d%20in%20range(ND)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20_p%20in%20range(NP)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%20%3D%20cell_colors%5B_d%5D%5B_p%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.add_patch(plt.Rectangle((_p%2C%20_d)%2C%201%2C%201%2C%20color%3Dcolor%2C%20alpha%3D0.8))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20annotations%5B_d%5D%5B_p%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(_p%20%2B%200.5%2C%20_d%20%2B%200.5%2C%20annotations%5B_d%5D%5B_p%5D%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D7%2C%20fontweight%3D'bold')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_title(f'I-3%20Class%20%7B_cls%7D%20timetable%20(holes%3D%7Bobj_val_i3%3A.0f%7D)')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.grid(True%2C%20color%3D'gray'%2C%20linewidth%3D0.5)%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20timetable%20chart')%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-4%20Exam%20scheduling%20(IP%2C%20feasibility)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%204*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i4exam_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi4exam_p.mos)%0A%0A%20%20%20%20Eleven%20exams%20must%20be%20scheduled%20across%208%20time%20slots%20for%20fourth-year%20students.%20Students%20taking%20certain%20pairs%20of%20exams%20cannot%20have%20them%20in%20the%20same%20slot%20(incompatibility%20constraints).%20Since%20any%20valid%20schedule%20is%20acceptable%2C%20there%20is%20no%20objective%20-%20the%20model%20finds%20a%20feasible%20assignment%20by%20minimizing%200.%0A%0A%20%20%20%20This%20demonstrates%20how%20to%20use%20MoselPy%20for%20**feasibility%20problems**%3A%20%60model.prob_status.is_solution_available%60%20is%20checked%20(not%20%60is_solution_optimal%60)%20since%20there%20is%20no%20meaningful%20objective%20value%20to%20verify.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BEXAM%7D%24%20(11%20exams%2C%20auto-populated%20from%20%60INCOMP%60)%2C%20%24%5Ctext%7BTIME%7D%20%3D%201..8%24%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BINCOMP%7D_%7Bd%2Ce%7D%24%20-%201%20if%20exams%20%24d%24%20and%20%24e%24%20cannot%20be%20in%20the%20same%20slot%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bplan%7D_%7Be%2Ct%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20exam%20%24e%24%20scheduled%20in%20slot%20%24t%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20the%20incompatibility%20matrix%20as%20a%20dict%20with%20%60(exam1%2C%20exam2)%60%20string%20tuple%20keys.%20The%20%60EXAM%60%20set%20auto-populates%20from%20the%20dict%20keys.%20Only%20entries%20equal%20to%201%20need%20to%20be%20listed%3B%20missing%20entries%20default%20to%200.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20EXAM_NAMES%20%3D%20%5B%22DA%22%2C%22NA%22%2C%22C%2B%2B%22%2C%22SE%22%2C%22PM%22%2C%22J%22%2C%22GMA%22%2C%22LP%22%2C%22MP%22%2C%22S%22%2C%22DSE%22%5D%0A%0A%20%20%20%20incomp_pairs%20%3D%20%5B%0A%20%20%20%20%20%20%20%20(%22DA%22%2C%22NA%22)%2C(%22DA%22%2C%22PM%22)%2C(%22DA%22%2C%22GMA%22)%2C(%22DA%22%2C%22S%22)%2C(%22DA%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22NA%22%2C%22DA%22)%2C(%22NA%22%2C%22PM%22)%2C(%22NA%22%2C%22GMA%22)%2C(%22NA%22%2C%22S%22)%2C(%22NA%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22C%2B%2B%22%2C%22SE%22)%2C(%22C%2B%2B%22%2C%22PM%22)%2C(%22C%2B%2B%22%2C%22J%22)%2C(%22C%2B%2B%22%2C%22GMA%22)%2C(%22C%2B%2B%22%2C%22MP%22)%2C(%22C%2B%2B%22%2C%22S%22)%2C(%22C%2B%2B%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22SE%22%2C%22C%2B%2B%22)%2C(%22SE%22%2C%22PM%22)%2C(%22SE%22%2C%22J%22)%2C(%22SE%22%2C%22GMA%22)%2C(%22SE%22%2C%22S%22)%2C(%22SE%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22PM%22%2C%22DA%22)%2C(%22PM%22%2C%22NA%22)%2C(%22PM%22%2C%22C%2B%2B%22)%2C(%22PM%22%2C%22SE%22)%2C(%22PM%22%2C%22J%22)%2C%0A%20%20%20%20%20%20%20%20(%22PM%22%2C%22GMA%22)%2C(%22PM%22%2C%22LP%22)%2C(%22PM%22%2C%22MP%22)%2C(%22PM%22%2C%22S%22)%2C(%22PM%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22J%22%2C%22C%2B%2B%22)%2C(%22J%22%2C%22SE%22)%2C(%22J%22%2C%22PM%22)%2C(%22J%22%2C%22GMA%22)%2C(%22J%22%2C%22MP%22)%2C(%22J%22%2C%22S%22)%2C(%22J%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22GMA%22%2C%22DA%22)%2C(%22GMA%22%2C%22NA%22)%2C(%22GMA%22%2C%22C%2B%2B%22)%2C(%22GMA%22%2C%22SE%22)%2C(%22GMA%22%2C%22PM%22)%2C%0A%20%20%20%20%20%20%20%20(%22GMA%22%2C%22J%22)%2C(%22GMA%22%2C%22LP%22)%2C(%22GMA%22%2C%22MP%22)%2C(%22GMA%22%2C%22S%22)%2C(%22GMA%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22LP%22%2C%22PM%22)%2C(%22LP%22%2C%22GMA%22)%2C(%22LP%22%2C%22S%22)%2C(%22LP%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22MP%22%2C%22C%2B%2B%22)%2C(%22MP%22%2C%22PM%22)%2C(%22MP%22%2C%22J%22)%2C(%22MP%22%2C%22GMA%22)%2C(%22MP%22%2C%22S%22)%2C(%22MP%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22S%22%2C%22DA%22)%2C(%22S%22%2C%22NA%22)%2C(%22S%22%2C%22C%2B%2B%22)%2C(%22S%22%2C%22SE%22)%2C(%22S%22%2C%22PM%22)%2C%0A%20%20%20%20%20%20%20%20(%22S%22%2C%22J%22)%2C(%22S%22%2C%22GMA%22)%2C(%22S%22%2C%22LP%22)%2C(%22S%22%2C%22MP%22)%2C(%22S%22%2C%22DSE%22)%2C%0A%20%20%20%20%20%20%20%20(%22DSE%22%2C%22DA%22)%2C(%22DSE%22%2C%22NA%22)%2C(%22DSE%22%2C%22C%2B%2B%22)%2C(%22DSE%22%2C%22SE%22)%2C(%22DSE%22%2C%22PM%22)%2C%0A%20%20%20%20%20%20%20%20(%22DSE%22%2C%22J%22)%2C(%22DSE%22%2C%22GMA%22)%2C(%22DSE%22%2C%22LP%22)%2C(%22DSE%22%2C%22MP%22)%2C(%22DSE%22%2C%22S%22)%2C%0A%20%20%20%20%5D%0A%20%20%20%20INCOMP%20%3D%20%7Bpair%3A%201%20for%20pair%20in%20incomp_pairs%7D%0A%0A%20%20%20%20print(f%22Exams%20to%20schedule%3A%20%7BEXAM_NAMES%7D%22)%0A%20%20%20%20print(f%22Incompatibility%20pairs%3A%20%7Blen(incomp_pairs)%7D%22)%0A%20%20%20%20print(f%22Available%20time%20slots%3A%208%22)%0A%20%20%20%20return%20(INCOMP%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20single%20execution%20command.%20Since%20the%20objective%20is%200%20(feasibility)%2C%20%60is_solution_available%60%20is%20the%20relevant%20check%20-%20%60is_solution_optimal%60%20would%20also%20be%20true%20since%20minimizing%200%20trivially%20achieves%20optimality.%20%60sol_plan%60%20maps%20%60(exam%2C%20slot)%60%20pairs%20to%20binary%20values.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(INCOMP%2C%20MODELS%2C%20mp)%3A%0A%20%20%20%20model_3%2C%20_output%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Fi4exam_p.mos'%2C%20exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'INCOMP'%3A%20INCOMP%7D)%0A%20%20%20%20if%20model_3.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20sol_plan%20%3D%20_output%5B'sol_plan'%5D%0A%20%20%20%20%20%20%20%20_slot_of%20%3D%20%7Be%3A%20t%20for%20(e%2C%20t)%2C%20v%20in%20sol_plan.items()%20if%20v%20%3E%200.5%7D%0A%20%20%20%20%20%20%20%20print(f'Feasible%20exam%20schedule%20found%3A')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20for%20_t%20in%20range(1%2C%209)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20exams_in_slot%20%3D%20sorted((e%20for%20e%2C%20s%20in%20_slot_of.items()%20if%20s%20%3D%3D%20_t))%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Slot%20%7B_t%7D%3A%20%7B('%2C%20'.join(exams_in_slot)%20if%20exams_in_slot%20else%20'(empty)')%7D')%0A%20%20%20%20%20%20%20%20violations%20%3D%20%5B(d%2C%20e)%20for%20(d%2C%20e)%2C%20v%20in%20INCOMP.items()%20if%20v%20%3D%3D%201%20and%20_slot_of.get(d)%20%3D%3D%20_slot_of.get(e)%5D%0A%20%20%20%20%20%20%20%20print(f'%5CnIncompatibility%20violations%3A%20%7Blen(violations)%7D%20(should%20be%200)')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20feasible%20schedule%20found%20(%7Bmodel_3.prob_status%7D)')%0A%20%20%20%20return%20model_3%2C%20sol_plan%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Slot%20assignment%20chart%20showing%20which%20exams%20are%20scheduled%20in%20each%20slot%2C%20with%20the%20incompatibility%20graph%20summarized%20as%20a%20heatmap.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(INCOMP%2C%20mo%2C%20model_3%2C%20np%2C%20plt%2C%20sol_plan)%3A%0A%20%20%20%20if%20'sol_plan'%20in%20locals()%20and%20model_3.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20_slot_of%20%3D%20%7Be%3A%20t%20for%20(e%2C%20t)%2C%20v%20in%20sol_plan.items()%20if%20v%20%3E%200.5%7D%0A%20%20%20%20%20%20%20%20exams_sorted%20%3D%20sorted(_slot_of.keys())%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(13%2C%204))%0A%20%20%20%20%20%20%20%20_ax%20%3D%20_axes%5B0%5D%0A%20%20%20%20%20%20%20%20colors_slot%20%3D%20plt.cm.Set3(np.linspace(0%2C%201%2C%208))%0A%20%20%20%20%20%20%20%20for%20e%20in%20exams_sorted%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_t%20%3D%20_slot_of.get(e%2C%200)%20-%201%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.barh(e%2C%201%2C%20left%3D_t%2C%20color%3Dcolors_slot%5B_t%5D%2C%20edgecolor%3D'white'%2C%20height%3D0.6)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(_t%20%2B%200.5%2C%20e%2C%20str(_t%20%2B%201)%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D8)%0A%20%20%20%20%20%20%20%20_ax.set_xlim(0%2C%208)%0A%20%20%20%20%20%20%20%20_ax.set_xticks(range(8))%0A%20%20%20%20%20%20%20%20_ax.set_xticklabels(%5Bf'Slot%20%7Bt%7D'%20for%20t%20in%20range(1%2C%209)%5D%2C%20fontsize%3D8)%0A%20%20%20%20%20%20%20%20_ax.set_title('I-4%20Exam%20schedule')%0A%20%20%20%20%20%20%20%20_ax2%20%3D%20_axes%5B1%5D%0A%20%20%20%20%20%20%20%20n%20%3D%20len(exams_sorted)%0A%20%20%20%20%20%20%20%20incomp_matrix%20%3D%20np.array(%5B%5BINCOMP.get((d%2C%20e)%2C%200)%20for%20e%20in%20exams_sorted%5D%20for%20d%20in%20exams_sorted%5D)%0A%20%20%20%20%20%20%20%20_im%20%3D%20_ax2.imshow(incomp_matrix%2C%20cmap%3D'Reds'%2C%20aspect%3D'auto')%0A%20%20%20%20%20%20%20%20_ax2.set_xticks(range(n))%0A%20%20%20%20%20%20%20%20_ax2.set_xticklabels(exams_sorted%2C%20rotation%3D45%2C%20ha%3D'right'%2C%20fontsize%3D7)%0A%20%20%20%20%20%20%20%20_ax2.set_yticks(range(n))%0A%20%20%20%20%20%20%20%20_ax2.set_yticklabels(exams_sorted%2C%20fontsize%3D7)%0A%20%20%20%20%20%20%20%20_ax2.set_title('Incompatibility%20matrix')%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20exam%20chart')%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-5%20Production%20planning%20with%20personnel%20(LP)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%205*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i5pplan_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi5pplan_p.mos)%0A%0A%20%20%20%20A%20company%20produces%20four%20products%20on%20five%20production%20lines.%20Processing%20times%20and%20line%20capacities%20are%20known.%20In%20a%20first%20solve%2C%20no%20transfers%20are%20allowed.%20In%20a%20second%20solve%2C%20working%20hours%20can%20be%20transferred%20between%20certain%20line%20pairs%20(up%20to%20a%20maximum%20per%20line).%20Maximize%20total%20profit%20in%20both%20cases.%0A%0A%20%20%20%20Transfer%20variables%20are%20created%20via%20%60create()%60%20only%20for%20permitted%20line%20pairs%20(dynamic%20array).%20Named%20%60linctr%60%20constraints%20(%60Load%60)%20are%20reassigned%20in-place%20to%20replace%20fixed-capacity%20limits%20with%20effective-hours%20limits%2C%20without%20removing%20the%20existing%20constraints.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPRODS%7D%20%3D%201..4%24%2C%20%24%5Ctext%7BLINES%7D%20%3D%201..5%24%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BDUR%7D_%7Bp%2Cl%7D%24%2C%20%24%5Ctext%7BCAP%7D_l%24%2C%20%24%5Ctext%7BPROFIT%7D_p%24%2C%20%24%5Ctext%7BTRANSF%7D_%7Bk%2Cl%7D%24%2C%20%24%5Ctext%7BTMAX%7D_l%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bproduce%7D_p%20%5Cgeq%200%24%3B%20%24%5Cmathit%7Bhours%7D_l%20%5Cgeq%200%24%3B%20%24%5Cmathit%7Btransfer%7D_%7Bk%2Cl%7D%20%5Cgeq%200%24%20(if%20permitted)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20the%20duration%20matrix%2C%20capacities%2C%20and%20profits%20as%201-indexed%20dicts.%20%60TRANSF%60%20is%20a%20sparse%20dict%20indicating%20which%20transfers%20are%20allowed%20(only%20entries%20with%20value%201%20are%20needed).%20%60TMAX%60%20caps%20outgoing%20transfers%20per%20line.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20dur_raw%20%3D%20%5B%5B1.3%2C%200.9%2C%202.0%2C%200.3%2C%200.9%5D%2C%20%5B1.8%2C%201.7%2C%201.4%2C%200.6%2C%201.1%5D%2C%20%5B1.3%2C%201.2%2C%201.3%2C%201.0%2C%201.4%5D%2C%20%5B0.9%2C%201.1%2C%201.0%2C%200.9%2C%201.0%5D%5D%0A%20%20%20%20DUR%20%3D%20%7B(p%20%2B%201%2C%20l%20%2B%201)%3A%20dur_raw%5Bp%5D%5Bl%5D%20for%20p%20in%20range(4)%20for%20l%20in%20range(5)%7D%0A%20%20%20%20CAP%20%3D%20%7B1%3A%204500%2C%202%3A%205000%2C%203%3A%204500%2C%204%3A%201500%2C%205%3A%202500%7D%0A%20%20%20%20PROFIT%20%3D%20%7B1%3A%207%2C%202%3A%208%2C%203%3A%209%2C%204%3A%207%7D%0A%20%20%20%20TRANSF%20%3D%20%7B(1%2C%202)%3A%201%2C%20(1%2C%203)%3A%201%2C%20(1%2C%204)%3A%201%2C%20(2%2C%203)%3A%201%2C%20(2%2C%205)%3A%201%2C%20(3%2C%201)%3A%201%2C%20(3%2C%202)%3A%201%2C%20(3%2C%204)%3A%201%2C%20(4%2C%205)%3A%201%2C%20(5%2C%201)%3A%201%2C%20(5%2C%202)%3A%201%2C%20(5%2C%203)%3A%201%7D%0A%20%20%20%20TMAX%20%3D%20%7B1%3A%20400%2C%202%3A%20800%2C%203%3A%20200%2C%204%3A%20500%2C%205%3A%20300%7D%0A%20%20%20%20print('Permitted%20transfer%20routes%3A')%0A%20%20%20%20for%20_k%2C%20_l%20in%20sorted(TRANSF)%3A%0A%20%20%20%20%20%20%20%20print(f'%20%20Line%20%7B_k%7D%20-%3E%20Line%20%7B_l%7D%20%20(max%20from%20line%20%7B_k%7D%3A%20%7BTMAX%5B_k%5D%7D%20hrs)')%0A%20%20%20%20return%20CAP%2C%20DUR%2C%20PROFIT%2C%20TMAX%2C%20TRANSF%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20compile-load-run%20sequence%20to%20allow%20running%20with%20different%20capacities.%20The%20model%20solves%20both%20variants%20in%20a%20single%20call.%20%60Load%60%20constraints%20are%20redefined%20in-place%20inside%20the%20Mosel%20model%20between%20the%20two%20solves%3A%20this%20is%20a%20key%20feature%20for%20extending%20a%20model%20without%20re-specifying%20all%20constraints.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(CAP%2C%20DUR%2C%20MODELS%2C%20PROFIT%2C%20TMAX%2C%20TRANSF%2C%20mp)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Fi5pplan_p.mos'%2C%20f'%7BMODELS%7D%2Fi5pplan_p.bim')%0A%20%20%20%20model_4%20%3D%20mp.load_model(f'%7BMODELS%7D%2Fi5pplan_p.bim')%0A%20%20%20%20model_4.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20_output%20%3D%20model_4.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'PROFIT'%3A%20PROFIT%2C%20'DUR'%3A%20DUR%2C%20'CAP'%3A%20CAP%2C%20'TRANSF'%3A%20TRANSF%2C%20'TMAX'%3A%20TMAX%7D)%0A%20%20%20%20if%20model_4.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_notrans%20%3D%20_output%5B'obj_notrans'%5D%0A%20%20%20%20%20%20%20%20obj_trans%20%3D%20_output%5B'obj_trans'%5D%0A%20%20%20%20%20%20%20%20sol_produce1%20%3D%20_output%5B'sol_produce1'%5D%0A%20%20%20%20%20%20%20%20sol_produce2%20%3D%20_output%5B'sol_produce2'%5D%0A%20%20%20%20%20%20%20%20sol_transfer%20%3D%20_output%5B'sol_transfer'%5D%0A%20%20%20%20%20%20%20%20print(f'Without%20transfers%3A%20total%20profit%20%3D%20%7Bobj_notrans%3A%2C.1f%7D')%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%205)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Product%20%7B_p%7D%3A%20%7Bsol_produce1.get(_p%2C%200)%3A%2C.2f%7D%20units')%0A%20%20%20%20%20%20%20%20print(f'%5CnWith%20transfers%3A%20total%20profit%20%3D%20%7Bobj_trans%3A%2C.1f%7D%20%20(%2B%7Bobj_trans%20-%20obj_notrans%3A%2C.1f%7D)')%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%205)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Product%20%7B_p%7D%3A%20%7Bsol_produce2.get(_p%2C%200)%3A%2C.2f%7D%20units')%0A%20%20%20%20%20%20%20%20active_transfers%20%3D%20%7B(k%2C%20l)%3A%20v%20for%20(k%2C%20l)%2C%20v%20in%20sol_transfer.items()%20if%20v%20%3E%200.01%7D%0A%20%20%20%20%20%20%20%20if%20active_transfers%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print('%5CnHours%20transferred%3A')%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(_k%2C%20_l)%2C%20v%20in%20sorted(active_transfers.items())%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Line%20%7B_k%7D%20-%3E%20Line%20%7B_l%7D%3A%20%7Bv%3A%2C.1f%7D%20hrs')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel_4.prob_status%7D)')%0A%20%20%20%20return%20obj_notrans%2C%20obj_trans%2C%20sol_produce1%2C%20sol_produce2%2C%20sol_transfer%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Side-by-side%20comparison%20of%20production%20volumes%20without%20and%20with%20personnel%20transfers%2C%20plus%20a%20transfer%20flow%20diagram.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20mo%2C%0A%20%20%20%20np%2C%0A%20%20%20%20obj_notrans%2C%0A%20%20%20%20obj_trans%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20sol_produce1%2C%0A%20%20%20%20sol_produce2%2C%0A%20%20%20%20sol_transfer%2C%0A)%3A%0A%20%20%20%20if%20'sol_produce1'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(12%2C%204))%0A%20%20%20%20%20%20%20%20_ax%20%3D%20_axes%5B0%5D%0A%20%20%20%20%20%20%20%20x%20%3D%20np.arange(4)%20%20%23%20Left%3A%20production%20comparison%0A%20%20%20%20%20%20%20%20p1%20%3D%20%5Bsol_produce1.get(p%2C%200)%20for%20p%20in%20range(1%2C%205)%5D%0A%20%20%20%20%20%20%20%20p2%20%3D%20%5Bsol_produce2.get(p%2C%200)%20for%20p%20in%20range(1%2C%205)%5D%0A%20%20%20%20%20%20%20%20_ax.bar(x%20-%200.2%2C%20p1%2C%200.35%2C%20label%3Df'No%20transfer%20(%7Bobj_notrans%3A%2C.0f%7D)'%2C%20color%3D'%2390CAF9'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20_ax.bar(x%20%2B%200.2%2C%20p2%2C%200.35%2C%20label%3Df'With%20transfer%20(%7Bobj_trans%3A%2C.0f%7D)'%2C%20color%3D'%231E88E5'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20_ax.set_xticks(x)%0A%20%20%20%20%20%20%20%20_ax.set_xticklabels(%5Bf'Product%20%7Bp%7D'%20for%20p%20in%20range(1%2C%205)%5D)%0A%20%20%20%20%20%20%20%20_ax.set_ylabel('Units%20produced')%0A%20%20%20%20%20%20%20%20_ax.set_title('I-5%20Production%20planning')%0A%20%20%20%20%20%20%20%20_ax.legend(fontsize%3D8)%0A%20%20%20%20%20%20%20%20_ax2%20%3D%20_axes%5B1%5D%0A%20%20%20%20%20%20%20%20active_t%20%3D%20%7B(k%2C%20l)%3A%20v%20for%20(k%2C%20l)%2C%20v%20in%20sol_transfer.items()%20if%20v%20%3E%200.01%7D%0A%20%20%20%20%20%20%20%20lines_with_flow%20%3D%20sorted(set((k%20for%20k%2C%20l%20in%20active_t))%20%7C%20set((l%20for%20k%2C%20l%20in%20active_t)))%0A%20%20%20%20%20%20%20%20if%20active_t%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20labels_t%20%3D%20%5Bf'L%7Bk%7D-%3EL%7Bl%7D'%20for%20k%2C%20l%20in%20sorted(active_t)%5D%20%20%23%20Right%3A%20transfer%20flows%0A%20%20%20%20%20%20%20%20%20%20%20%20values_t%20%3D%20%5Bactive_t%5Bk%2C%20l%5D%20for%20k%2C%20l%20in%20sorted(active_t)%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax2.barh(labels_t%2C%20values_t%2C%20color%3D'%2343A047'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax2.set_xlabel('Hours%20transferred')%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax2.set_title('I-5%20Working%20hours%20transferred')%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax2.text(0.5%2C%200.5%2C%20'No%20transfers%20active'%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20transform%3D_ax2.transAxes)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax2.set_title('I-5%20Working%20hours%20transferred')%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20production%20chart')%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20I-6%20Construction%20site%20personnel%20planning%20(IP)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20*Applications%20Book%20Chapter%20I%2C%20Example%206*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60i6build_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Ftimetabling_personnel%2Fi6build_p.mos)%0A%0A%20%20%20%20A%20company%20plans%20its%20workforce%20at%20a%20construction%20site%20over%206%20months%20(March%20to%20August).%20Workers%20may%20arrive%20from%20other%20sites%20(up%20to%203%20per%20month)%20or%20depart%20(up%20to%201%2F3%20of%20current%20staff).%20Overstaffing%20and%20understaffing%20each%20carry%20a%20penalty%20cost%20per%20worker%20per%20month.%20The%20target%20is%20to%20end%20at%20the%20same%20staff%20level%20as%20the%20start.%20Minimize%20total%20cost.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BMONTHS%7D%20%3D%20%5C%7B3..8%5C%7D%24%20(March%20to%20August)%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BREQ%7D_m%24%2C%20%24%5Ctext%7BCARR%7D%24%2C%20%24%5Ctext%7BCLEAVE%7D%24%2C%20%24%5Ctext%7BCOVER%7D%24%2C%20%24%5Ctext%7BCUNDER%7D%24%2C%20%24%5Ctext%7BNSTART%7D%24%2C%20%24%5Ctext%7BNFINAL%7D%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bonsite%7D_m%24%2C%20%24%5Cmathit%7Barrive%7D_m%24%2C%20%24%5Cmathit%7Bleave%7D_m%24%2C%20%24%5Cmathit%7Bover%7D_m%24%2C%20%24%5Cmathit%7Bunder%7D_m%20%5Cin%20%5Cmathbb%7BZ%7D_%7B%5Cgeq%200%7D%24%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Cbegin%7Balign*%7D%0A%20%20%20%20%5Ctext%7Bminimize%7D%20%5Cquad%20%26%20%5Csum_m%20(%5Ctext%7BCARR%7D%20%5Ccdot%20%5Cmathit%7Barrive%7D_m%20%2B%20%5Ctext%7BCLEAVE%7D%20%5Ccdot%20%5Cmathit%7Bleave%7D_m%20%2B%20%5Ctext%7BCOVER%7D%20%5Ccdot%20%5Cmathit%7Bover%7D_m%20%2B%20%5Ctext%7BCUNDER%7D%20%5Ccdot%20%5Cmathit%7Bunder%7D_m)%0A%20%20%20%20%5Cend%7Balign*%7D%0A%20%20%20%20%24%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Define%20the%20monthly%20worker%20requirements%20and%20all%20cost%2Fstaffing%20parameters%20as%20Python%20scalars%20and%20dicts.%20The%20planning%20horizon%20is%20months%203-8%20(March%20to%20August).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20MONTH_NAMES%20%3D%20%7B3%3A%20'Mar'%2C%204%3A%20'Apr'%2C%205%3A%20'May'%2C%206%3A%20'Jun'%2C%207%3A%20'Jul'%2C%208%3A%20'Aug'%7D%0A%20%20%20%20REQ_1%20%3D%20%7B3%3A%204%2C%204%3A%206%2C%205%3A%207%2C%206%3A%204%2C%207%3A%206%2C%208%3A%202%7D%0A%20%20%20%20CARR%20%3D%20100%0A%20%20%20%20CLEAVE%20%3D%20160%0A%20%20%20%20COVER%20%3D%20200%0A%20%20%20%20CUNDER%20%3D%20200%0A%20%20%20%20NSTART%20%3D%203%0A%20%20%20%20NFINAL%20%3D%203%0A%20%20%20%20print('Monthly%20requirements%3A')%0A%20%20%20%20for%20_m%2C%20name%20in%20MONTH_NAMES.items()%3A%0A%20%20%20%20%20%20%20%20print(f'%20%20%7Bname%7D%3A%20%7BREQ_1%5B_m%5D%7D%20workers%20required')%0A%20%20%20%20print(f'%5CnStart%2Fend%3A%20%7BNSTART%7D%2F%7BNFINAL%7D%20workers')%0A%20%20%20%20print(f'Costs%3A%20arrive%3D%7BCARR%7D%2C%20leave%3D%7BCLEAVE%7D%2C%20over%3D%7BCOVER%7D%2C%20under%3D%7BCUNDER%7D')%0A%20%20%20%20return%20CARR%2C%20CLEAVE%2C%20COVER%2C%20CUNDER%2C%20MONTH_NAMES%2C%20NFINAL%2C%20NSTART%2C%20REQ_1%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Use%20the%20single%20execution%20command.%20All%20staffing%20data%20is%20passed%20via%20%60input_data%60.%20The%20planning%20horizon%20endpoints%20(%60FIRST%3D3%60%2C%20%60LAST%3D8%60)%20use%20the%20model%20defaults%3B%20they%20can%20be%20overridden%20via%20%60exec_params%60%20for%20different%20scenarios.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20CARR%2C%0A%20%20%20%20CLEAVE%2C%0A%20%20%20%20COVER%2C%0A%20%20%20%20CUNDER%2C%0A%20%20%20%20MODELS%2C%0A%20%20%20%20MONTH_NAMES%2C%0A%20%20%20%20NFINAL%2C%0A%20%20%20%20NSTART%2C%0A%20%20%20%20REQ_1%2C%0A%20%20%20%20mp%2C%0A)%3A%0A%20%20%20%20model_5%2C%20_output%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Fi6build_p.mos'%2C%20exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'REQ'%3A%20REQ_1%2C%20'CARR'%3A%20CARR%2C%20'CLEAVE'%3A%20CLEAVE%2C%20'COVER'%3A%20COVER%2C%20'CUNDER'%3A%20CUNDER%2C%20'NSTART'%3A%20NSTART%2C%20'NFINAL'%3A%20NFINAL%7D)%0A%20%20%20%20if%20model_5.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_val_i6%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20sol_onsite%20%3D%20_output%5B'sol_onsite'%5D%0A%20%20%20%20%20%20%20%20sol_arrive%20%3D%20_output%5B'sol_arrive'%5D%0A%20%20%20%20%20%20%20%20sol_leave%20%3D%20_output%5B'sol_leave'%5D%0A%20%20%20%20%20%20%20%20sol_over%20%3D%20_output%5B'sol_over'%5D%0A%20%20%20%20%20%20%20%20sol_under%20%3D%20_output%5B'sol_under'%5D%0A%20%20%20%20%20%20%20%20print(f'Total%20staffing%20cost%3A%20%7Bobj_val_i6%3A%2C.0f%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'%7B'Month'%3A%3C6%7D%20%7B'Req'%3A%3E4%7D%20%7B'OnSite'%3A%3E7%7D%20%7B'Arrive'%3A%3E7%7D%20%7B'Leave'%3A%3E7%7D%20%7B'Over'%3A%3E5%7D%20%7B'Under'%3A%3E6%7D')%0A%20%20%20%20%20%20%20%20for%20_m%20in%20range(3%2C%209)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20%7BMONTH_NAMES%5B_m%5D%3A%3C4%7D%20%7BREQ_1%5B_m%5D%3A%3E4%7D%20%7Bround(sol_onsite.get(_m%2C%200))%3A%3E7%7D%20%7Bround(sol_arrive.get(_m%2C%200))%3A%3E7%7D%20%7Bround(sol_leave.get(_m%2C%200))%3A%3E7%7D%20%7Bround(sol_over.get(_m%2C%200))%3A%3E5%7D%20%7Bround(sol_under.get(_m%2C%200))%3A%3E6%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel_5.prob_status%7D)')%0A%20%20%20%20return%20obj_val_i6%2C%20sol_arrive%2C%20sol_leave%2C%20sol_onsite%2C%20sol_over%2C%20sol_under%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Staffing%20timeline%20showing%20workers%20on%20site%20each%20month%20(blue)%20with%20overstaffing%20(orange)%20stacked%20on%20top%20and%20the%20monthly%20requirement%20(red%20line).%20Note%20that%20the%20requirement%20is%20a%20*soft*%20target%3A%20understaffing%20is%20permitted%20by%20the%20model%20but%20penalized%20(cost%20%60CUNDER%60%20per%20worker%20per%20month).%20Months%20where%20%60onsite%20%3C%20REQ%60%20are%20shown%20with%20a%20hatched%20gap%3B%20this%20is%20the%20deliberate%20optimizer%20trade-off%20when%20the%20cost%20of%20extra%20arrivals%20or%20delayed%20departures%20exceeds%20the%20understaffing%20penalty.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20MONTH_NAMES%2C%0A%20%20%20%20REQ_1%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20obj_val_i6%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20sol_arrive%2C%0A%20%20%20%20sol_leave%2C%0A%20%20%20%20sol_onsite%2C%0A%20%20%20%20sol_over%2C%0A%20%20%20%20sol_under%2C%0A)%3A%0A%20%20%20%20if%20'sol_onsite'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20months%20%3D%20list(range(3%2C%209))%0A%20%20%20%20%20%20%20%20names%20%3D%20%5BMONTH_NAMES%5Bm%5D%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20on_site%20%3D%20%5Bround(sol_onsite.get(m%2C%200))%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20arrives%20%3D%20%5Bround(sol_arrive.get(m%2C%200))%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20leaves%20%3D%20%5Bround(sol_leave.get(m%2C%200))%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20overs%20%3D%20%5Bround(sol_over.get(m%2C%200))%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20unders%20%3D%20%5Bround(sol_under.get(m%2C%200))%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20reqs%20%3D%20%5BREQ_1%5Bm%5D%20for%20m%20in%20months%5D%0A%20%20%20%20%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(9%2C%204))%0A%20%20%20%20%20%20%20%20_ax.bar(names%2C%20on_site%2C%20color%3D'%231E88E5'%2C%20label%3D'On%20site'%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20_ax.bar(names%2C%20overs%2C%20bottom%3Don_site%2C%20color%3D'%23FB8C00'%2C%20label%3D'Overstaffing%20(%2Bpenalty)'%2C%20edgecolor%3D'white'%2C%20alpha%3D0.8)%0A%20%20%20%20%20%20%20%20for%20i%2C%20(s%2C%20r%2C%20u)%20in%20enumerate(zip(on_site%2C%20reqs%2C%20unders))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20u%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.bar(names%5Bi%5D%2C%20u%2C%20bottom%3Ds%2C%20color%3D'%23E53935'%2C%20alpha%3D0.4%2C%20hatch%3D'%2F%2F%2F'%2C%20edgecolor%3D'%23E53935'%2C%20linewidth%3D0.5%2C%20label%3D'Understaffing%20(%2Bpenalty)'%20if%20i%20%3D%3D%200%20else%20'')%0A%20%20%20%20%20%20%20%20_ax.step(range(6)%2C%20reqs%2C%20where%3D'mid'%2C%20color%3D'%23E53935'%2C%20linewidth%3D2%2C%20label%3D'Requirement%20(soft)')%0A%20%20%20%20%20%20%20%20for%20i%2C%20_m%20in%20enumerate(months)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20a%2C%20_l%20%3D%20(arrives%5Bi%5D%2C%20leaves%5Bi%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20a%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.annotate(f'%2B%7Ba%7D'%2C%20(i%2C%20on_site%5Bi%5D%20%2F%202)%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D8%2C%20color%3D'white'%2C%20fontweight%3D'bold')%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20_l%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.annotate(f'-%7B_l%7D'%2C%20(i%2C%20on_site%5Bi%5D%20%2B%200.2)%2C%20ha%3D'center'%2C%20va%3D'bottom'%2C%20fontsize%3D8%2C%20color%3D'%23555')%0A%20%20%20%20%20%20%20%20_ax.set_xticks(range(6))%0A%20%20%20%20%20%20%20%20_ax.set_xticklabels(names)%0A%20%20%20%20%20%20%20%20_ax.set_ylabel('Workers')%0A%20%20%20%20%20%20%20%20_ax.set_title(f'I-6%20Construction%20site%20staffing%20(cost%3D%7Bobj_val_i6%3A%2C.0f%7D)')%0A%20%20%20%20%20%20%20%20_ax.legend(fontsize%3D8)%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20mo.output.append(plt.gcf())%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20available%20-%20skipping%20staffing%20chart')%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
a0abad11d0eb978930417bb93cf4f70b