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%20Air%20Transport%22%0A%23%20description%20%3D%20%22Connections%2C%20crews%2C%20landing%2C%20hubs%2C%20tours%22%0A%23%20image%20%3D%20%22__marimo__%2Fthumbnail-moselpy-airtransport.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%20F%3A%20Air%20Transport**%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_air_transport.py***%20-%20Air%20transport%20and%20logistics%20problems%20from%20the%20Xpress%20Applications%20Book%20-%20flight%20connections%2C%20crew%20assignment%2C%20landing%20scheduling%2C%20hub%20network%20design%2C%20and%20tour%20planning%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%20from%20moselpy%20import%20ExecStatus%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%22air_transport%22)%0A%20%20%20%20DATA%20%20%20%3D%20str(_root%20%2F%20%22data%22%20%2F%20%22air_transport%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%20mpatches%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%20Air%20transport%20and%20logistics%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%20five%20LP%20and%20MIP%20examples%20from%20Chapter%20F%20of%20the%20Xpress%20Applications%20Book%2C%20solved%20via%20%5BMoselPy%5D(https%3A%2F%2Fpypi.org%2Fproject%2Fmoselpy%2F).%20Examples%20range%20from%20flight%20connection%20planning%20and%20crew%20assignment%20to%20landing%20scheduling%2C%20hub-and-spoke%20network%20design%2C%20and%20tour%20planning%20via%20subtour%20elimination.%0A%0A%20%20%20%20%3E%20**Mosel%20source%20files%3A**%20%5B%60models%2Fair_transport%2F%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Ftree%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport)%20%26nbsp%3B%7C%26nbsp%3B%20**Data%20files%3A**%20%5B%60data%2Fair_transport%2F%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Ftree%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fdata%2Fair_transport)%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_6)%0A%0A%20%20%20%20%23%23%23%20MoselPy%20features%20demonstrated%20in%20this%20chapter%0A%0A%20%20%20%20-%20**Single%20execution%20command**%20(%60mp.execute_model()%60)%3A%20run%20F-1%20in%20a%20single%20call%2C%20inspect%20the%20binary%20connection%20matrix.%0A%20%20%20%20-%20**Compile-load-run%20sequence%20with%20dynamic%20sets**%3A%20F-2%20uses%20a%20computed%20%60ARCS%60%20range%20whose%20size%20depends%20on%20the%20data%3B%20the%20adapted%20model%20preprocesses%20crew%20compatibility%20before%20finalizing%20the%20set.%0A%20%20%20%20-%20**Sequential%20multi-objective%20MIP**%3A%20F-2%20demonstrates%20two%20consecutive%20%60maximize()%60%20calls%20within%20a%20single%20model%20run%2C%20with%20intermediate%20results%20captured%20between%20objectives.%0A%20%20%20%20-%20**%60model.find_identifier()%60**%3A%20retrieve%20the%20%60sol_land%60%20array%20after%20solving%20F-3%20directly%20from%20the%20model%20object%2C%20as%20an%20alternative%20to%20the%20%60initializations%20to%20OUTFILE%60%20dict.%0A%20%20%20%20-%20**Model%20introspection**%3A%20%60model.name%60%2C%20%60model.version%60%2C%20%60model.user_comment%60%2C%20and%20%60model.size%60%20are%20demonstrated%20on%20F-4.%0A%20%20%20%20-%20**Context%20manager**%20(%60with%20mp.load_model(...)%20as%20model%3A%60)%3A%20F-5%20uses%20the%20context%20manager%20form%20to%20ensure%20model%20resources%20are%20released%20automatically%20after%20the%20tour%20planning%20run.%0A%20%20%20%20-%20**%60setcallback(XPRS_CB_PREINTSOL%2C%20...)%60**%3A%20F-5%20(variant)%20registers%20a%20pre-integer-solution%20callback%20in%20Mosel%20that%20adds%20lazy%20subtour-elimination%20cuts%20during%20B%26B%2C%20so%20Python%20calls%20%60execute_model%60%20once%20and%20the%20solver%20handles%20all%20constraint%20generation%20internally.%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%20F-1%20Flight%20connection%20planning%20(MIP)%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%20F%2C%20Example%201*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60f1connect_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff1connect_p.mos)%0A%0A%20%20%20%20An%20airline%20has%206%20planes%20landing%20at%20a%20hub%20within%2090%20minutes.%20During%20the%20following%20hour%2C%20these%20planes%20must%20travel%20to%206%20new%20destinations.%20For%20each%20incoming%2Foutgoing%20pair%2C%20the%20matrix%20%60PASS%5Bi%2Cj%5D%60%20gives%20the%20number%20of%20passengers%20who%20can%20stay%20on%20board%20if%20plane%20%24i%24%20continues%20to%20destination%20%24j%24.%20Infeasible%20connections%20are%20represented%20by%20a%20large%20negative%20value%20%24(-1000)%24.%20The%20model%20assigns%20each%20incoming%20plane%20to%20one%20outgoing%20flight%20to%20maximize%20the%20total%20number%20of%20passengers%20who%20remain%20on%20their%20plane.%0A%0A%20%20%20%20This%20is%20an%20assignment%20problem%3A%20each%20incoming%20flight%20is%20matched%20exactly%20once%20to%20an%20outgoing%20flight%20and%20vice%20versa.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPLANES%7D%20%3D%20%5C%7B1%2C%20%5Cldots%2C%206%5C%7D%24%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BPASS%7D_%7Bi%2Cj%7D%24%20-%20passengers%20staying%20on%20board%20if%20plane%20%24i%24%20continues%20to%20%24j%24%20(%24-1000%24%20for%20infeasible%20connections)%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bcont%7D_%7Bi%2Cj%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20incoming%20flight%20%24i%24%20continues%20to%20destination%20%24j%24%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Cbegin%7Balign*%7D%0A%20%20%20%20%5Ctext%7Bmaximize%7D%20%20%20%5Cquad%20%26%20%5Csum_%7Bi%2Cj%20%5Cin%20%5Ctext%7BPLANES%7D%7D%20%5Ctext%7BPASS%7D_%7Bi%2Cj%7D%20%5Ccdot%20%5Cmathit%7Bcont%7D_%7Bi%2Cj%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Csum_%7Bj%7D%20%5Cmathit%7Bcont%7D_%7Bi%2Cj%7D%20%3D%201%20%5Cquad%20%5Cforall%5C%2C%20i%20%5Cin%20%5Ctext%7BPLANES%7D%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_%7Bi%7D%20%5Cmathit%7Bcont%7D_%7Bi%2Cj%7D%20%3D%201%20%5Cquad%20%5Cforall%5C%2C%20j%20%5Cin%20%5Ctext%7BPLANES%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%20passenger%20connection%20matrix%20%60PASS%60%20as%20a%20Python%20dict%20with%201-indexed%20%60(i%2Cj)%60%20tuple%20keys.%20Entries%20of%20%24-1000%24%20represent%20infeasible%20connections%20(e.g.%20a%20narrow-body%20plane%20cannot%20continue%20on%20a%20wide-body%20route).%20Passing%20the%20matrix%20as%20a%20flat%20dict%20is%20the%20standard%20pattern%20for%202D%20integer%20arrays.%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%20%23%20Passenger%20matrix%3A%20PASS%5Bi%2Cj%5D%20%3D%20passengers%20staying%20on%20board%20if%20plane%20i%20continues%20to%20j%0A%20%20%20%20%23%20-1000%20encodes%20an%20infeasible%20connection%20(too%20few%20seats)%0A%20%20%20%20pass_raw%20%3D%20%5B%5B35%2C%2012%2C%2016%2C%2038%2C%205%2C%202%5D%2C%20%5B25%2C%208%2C%209%2C%2024%2C%206%2C%208%5D%2C%20%5B12%2C%208%2C%2011%2C%2027%2C%203%2C%202%5D%2C%20%5B38%2C%2015%2C%2014%2C%2030%2C%202%2C%209%5D%2C%20%5B-1000%2C%209%2C%208%2C%2025%2C%2010%2C%205%5D%2C%20%5B-1000%2C%20-1000%2C%20-1000%2C%2014%2C%206%2C%207%5D%5D%0A%20%20%20%20PASS%20%3D%20%7B(i%20%2B%201%2C%20j%20%2B%201)%3A%20v%20for%20i%2C%20row%20in%20enumerate(pass_raw)%20for%20j%2C%20v%20in%20enumerate(row)%7D%0A%20%20%20%20pass_df%20%3D%20pd.DataFrame(pass_raw%2C%20index%3Drange(1%2C%207)%2C%20columns%3Drange(1%2C%207))%0A%20%20%20%20pass_df.index.name%20%3D%20'Incoming'%0A%20%20%20%20pass_df.columns.name%20%3D%20'Outgoing'%0A%20%20%20%20print('Passenger%20connection%20matrix%20(PASS)%3A')%0A%20%20%20%20print(pass_df.to_string())%0A%20%20%20%20return%20PASS%2C%20pass_raw%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%60PASS%60%20is%20passed%20via%20%60input_data%60%20as%20a%20dict%20with%20%60(i%2Cj)%60%20tuple%20keys%2C%20matching%20the%20Mosel%202D%20array%20declaration%20%60cont%3A%20array(PLANES%2CPLANES)%20of%20mpvar%60.%20After%20solving%2C%20%60sol_cont%60%20gives%20the%20binary%20assignment%20matrix.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MODELS%2C%20PASS%2C%20mp%2C%20pass_raw)%3A%0A%20%20%20%20_model%2C%20_output%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Ff1connect_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'PASS'%3A%20PASS%7D)%0A%20%20%20%20if%20_model.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_val_f1%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20sol_cont%20%3D%20_output%5B'sol_cont'%5D%0A%20%20%20%20%20%20%20%20assignment%20%3D%20%7Bi%3A%20next((j%20for%20j%20in%20range(1%2C%207)%20if%20sol_cont.get((i%2C%20j)%2C%200)%20%3E%200.5))%20for%20i%20in%20range(1%2C%207)%7D%0A%20%20%20%20%20%20%20%20passengers%20%3D%20%7Bi%3A%20pass_raw%5Bi%20-%201%5D%5Bassignment%5Bi%5D%20-%201%5D%20for%20i%20in%20range(1%2C%207)%7D%0A%20%20%20%20%20%20%20%20print(f'Total%20passengers%20staying%20on%20board%3A%20%7Bobj_val_f1%3A.0f%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'%7B'Incoming'%3A%3E9%7D%20%20%7B'-%3E%20Outgoing'%3A%3E12%7D%20%20%7B'Passengers'%3A%3E12%7D')%0A%20%20%20%20%20%20%20%20for%20_i%20in%20range(1%2C%207)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_j%20%3D%20assignment%5B_i%5D%20%20%23%20Reconstruct%20assignment%3A%20for%20each%20incoming%20plane%20i%2C%20find%20which%20outgoing%20j%20it%20connects%20to%0A%20%20%20%20%20%20%20%20%20%20%20%20pax%20%3D%20passengers%5B_i%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20feasible%20%3D%20''%20if%20pax%20%3E%3D%200%20else%20'%20%20(infeasible%20-%20would%20have%20been%200)'%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Plane%20%7B_i%3A%3E2%7D%20%20-%3E%20%20%20Plane%20%7B_j%3A%3E2%7D%20%20%20%20%20%20%7Bpax%3A%3E8%7D%7Bfeasible%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7B_model.prob_status%7D)')%0A%20%20%20%20return%20assignment%2C%20obj_val_f1%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%20Heatmap%20of%20the%20connection%20matrix%20%60PASS%60.%20The%20optimal%20assignment%20is%20marked%20with%20circles%3B%20infeasible%20connections%20are%20shown%20in%20red.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(assignment%2C%20mo%2C%20np%2C%20obj_val_f1%2C%20pass_raw%2C%20plt)%3A%0A%20%20%20%20if%20'assignment'%20in%20locals()%20and%20'sol_cont'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(7%2C%205))%0A%20%20%20%20%20%20%20%20disp%20%3D%20np.array(%5B%5Bmax(pass_raw%5Bi%5D%5Bj%5D%2C%20-20)%20for%20j%20in%20range(6)%5D%20for%20i%20in%20range(6)%5D%2C%20dtype%3Dfloat)%0A%20%20%20%20%20%20%20%20im%20%3D%20_ax.imshow(disp%2C%20cmap%3D'RdYlGn'%2C%20aspect%3D'equal'%2C%20vmin%3D-20%2C%20vmax%3D40)%20%20%23%20Build%20display%20matrix%3A%20clip%20infeasible%20to%20-20%20for%20color%20contrast%0A%20%20%20%20%20%20%20%20plt.colorbar(im%2C%20ax%3D_ax%2C%20label%3D'Passengers%20staying%20on%20board')%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(%5Bf'Out%20%7Bj%7D'%20for%20j%20in%20range(1%2C%207)%5D)%0A%20%20%20%20%20%20%20%20_ax.set_yticks(range(6))%0A%20%20%20%20%20%20%20%20_ax.set_yticklabels(%5Bf'In%20%7Bi%7D'%20for%20i%20in%20range(1%2C%207)%5D)%0A%20%20%20%20%20%20%20%20_ax.set_title(f'F-1%20Flight%20connections%20-%20PASS%20matrix%20(total%3A%20%7Bobj_val_f1%3A.0f%7D%20passengers)')%0A%20%20%20%20%20%20%20%20for%20_i%20in%20range(1%2C%207)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_j%20%3D%20assignment%5B_i%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.add_patch(plt.Circle((_j%20-%201%2C%20_i%20-%201)%2C%200.35%2C%20color%3D'white'%2C%20fill%3DFalse%2C%20lw%3D2.5))%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.add_patch(plt.Circle((_j%20-%201%2C%20_i%20-%201)%2C%200.35%2C%20color%3D'black'%2C%20fill%3DFalse%2C%20lw%3D1.5))%20%20%23%20Mark%20optimal%20assignment%20with%20circles%0A%20%20%20%20%20%20%20%20for%20_i%20in%20range(6)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_j%20in%20range(6)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20val%20%3D%20pass_raw%5B_i%5D%5B_j%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20txt%20%3D%20str(val)%20if%20val%20%3E%20-100%20else%20'X'%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(_j%2C%20_i%2C%20txt%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D8%2C%20color%3D'white'%20if%20val%20%3C%20-10%20else%20'black')%0A%20%20%20%20%20%20%20%20plt.tight_layout()%20%20%23%20Annotate%20all%20cells%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%20connection%20heatmap')%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%20F-2%20Military%20crew%20composition%20(MIP)%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%20F%2C%20Example%202*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60f2crew_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff2crew_p.mos)%0A%0A%20%20%20%20The%20Royal%20Air%20Force%20(RAF)%20has%208%20pilots%20who%20speak%20different%20languages%20and%20are%20trained%20on%20different%20aircraft%20types.%20Two%20pilots%20can%20form%20a%20crew%20only%20if%20they%20share%20at%20least%20one%20language%20with%20a%20rating%20of%2010%20or%20more%2C%20and%20share%20at%20least%20one%20aircraft%20type%20with%20a%20rating%20of%2010%20or%20more.%0A%0A%20%20%20%20The%20model%20is%20solved%20in%20two%20phases%3A%0A%0A%20%20%20%201.%20**Phase%201**%20-%20maximize%20the%20number%20of%20crews%20flying%20(i.e.%20find%20the%20maximum%20matching).%0A%20%20%20%202.%20**Phase%202**%20-%20keeping%20the%20same%20crew%20count%2C%20maximize%20the%20total%20combined%20aircraft%20type%20rating%20(quality%20score).%0A%0A%20%20%20%20The%20model%20preprocesses%20the%20data%20to%20build%20the%20set%20%60ARCS%60%20of%20all%20feasible%20pilot%20pairs%2C%20which%20is%20finalized%20dynamically%20before%20the%20decision%20variables%20are%20declared.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPILOTS%7D%20%3D%20%5C%7B1%2C%20%5Cldots%2C%208%5C%7D%24%2C%20%24%5Ctext%7BRL%7D%24%20(languages)%2C%20%24%5Ctext%7BRT%7D%24%20(aircraft%20types)%2C%20%24%5Ctext%7BARCS%7D%24%20(feasible%20crew%20pairs%2C%20computed%20from%20data)%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BLANG%7D_%7Bl%2Cp%7D%24%20-%20language%20rating%20of%20pilot%20%24p%24%3B%20%24%5Ctext%7BPTYPE%7D_%7Bt%2Cp%7D%24%20-%20aircraft%20type%20rating%3B%20%24%5Ctext%7BSCORE%7D_a%24%20-%20combined%20type%20score%20of%20crew%20pair%20%24a%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bfly%7D_a%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20crew%20%24a%24%20is%20assigned%20to%20a%20flight%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Cbegin%7Balign*%7D%0A%20%20%20%20%5Ctext%7Bmaximize%7D%20%20%20%5Cquad%20%26%20%5Csum_%7Ba%20%5Cin%20%5Ctext%7BARCS%7D%7D%20%5Cmathit%7Bfly%7D_a%20%5Cqquad%20%5Ctext%7B(Phase%201%3A%20max%20crews)%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bmaximize%7D%20%20%20%5Cquad%20%26%20%5Csum_%7Ba%20%5Cin%20%5Ctext%7BARCS%7D%7D%20%5Ctext%7BSCORE%7D_a%20%5Ccdot%20%5Cmathit%7Bfly%7D_a%20%5Cqquad%20%5Ctext%7B(Phase%202%3A%20max%20score)%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Csum_%7Ba%3A%5C%2C%20p%20%5Cin%20%5Ctext%7BCREW%7D(a)%7D%20%5Cmathit%7Bfly%7D_a%20%5Cleq%201%20%5Cquad%20%5Cforall%5C%2C%20p%20%5Cin%20%5Ctext%7BPILOTS%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%20%60LANG%60%20and%20%60PTYPE%60%20as%20dicts%20with%20%60(language%2Ftype%2C%20pilot_index)%60%20tuple%20keys.%20When%20MoselPy%20reads%20these%20dicts%2C%20the%20string%20dimensions%20(%60RL%60%20and%20%60RT%60%20in%20the%20Mosel%20model)%20are%20auto-populated%20from%20the%20first%20elements%20of%20the%20tuple%20keys.%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%20%23%20Language%20ratings%3A%20(language%2C%20pilot)%20-%3E%20rating%20(0%20%3D%20not%20proficient)%0A%20%20%20%20LANG%20%3D%20%7B%0A%20%20%20%20%20%20%20%20(%22English%22%2C%201)%3A20%2C%20(%22English%22%2C%202)%3A14%2C%20(%22English%22%2C%203)%3A%200%2C%20(%22English%22%2C%204)%3A13%2C%0A%20%20%20%20%20%20%20%20(%22English%22%2C%205)%3A%200%2C%20(%22English%22%2C%206)%3A%200%2C%20(%22English%22%2C%207)%3A%208%2C%20(%22English%22%2C%208)%3A%208%2C%0A%20%20%20%20%20%20%20%20(%22French%22%2C%20%201)%3A12%2C%20(%22French%22%2C%20%202)%3A%200%2C%20(%22French%22%2C%20%203)%3A%200%2C%20(%22French%22%2C%20%204)%3A10%2C%0A%20%20%20%20%20%20%20%20(%22French%22%2C%20%205)%3A15%2C%20(%22French%22%2C%20%206)%3A20%2C%20(%22French%22%2C%20%207)%3A%208%2C%20(%22French%22%2C%20%208)%3A%209%2C%0A%20%20%20%20%20%20%20%20(%22Dutch%22%2C%20%20%201)%3A%200%2C%20(%22Dutch%22%2C%20%20%202)%3A20%2C%20(%22Dutch%22%2C%20%20%203)%3A12%2C%20(%22Dutch%22%2C%20%20%204)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22Dutch%22%2C%20%20%205)%3A%208%2C%20(%22Dutch%22%2C%20%20%206)%3A11%2C%20(%22Dutch%22%2C%20%20%207)%3A14%2C%20(%22Dutch%22%2C%20%20%208)%3A12%2C%0A%20%20%20%20%20%20%20%20(%22Norwegian%22%2C1)%3A%200%2C(%22Norwegian%22%2C2)%3A%200%2C(%22Norwegian%22%2C3)%3A%200%2C(%22Norwegian%22%2C4)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22Norwegian%22%2C5)%3A17%2C(%22Norwegian%22%2C6)%3A%200%2C(%22Norwegian%22%2C7)%3A%200%2C(%22Norwegian%22%2C8)%3A16%2C%0A%20%20%20%20%7D%0A%20%20%20%20%23%20Aircraft%20type%20ratings%3A%20(type%2C%20pilot)%20-%3E%20rating%20(0%20%3D%20not%20qualified)%0A%20%20%20%20PTYPE%20%3D%20%7B%0A%20%20%20%20%20%20%20%20(%22reconnaissance%22%2C%201)%3A18%2C%20(%22reconnaissance%22%2C%202)%3A12%2C%20(%22reconnaissance%22%2C%203)%3A15%2C%0A%20%20%20%20%20%20%20%20(%22reconnaissance%22%2C%204)%3A%200%2C%20(%22reconnaissance%22%2C%205)%3A%200%2C%20(%22reconnaissance%22%2C%206)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22reconnaissance%22%2C%207)%3A%208%2C%20(%22reconnaissance%22%2C%208)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22transport%22%2C%20%201)%3A10%2C%20(%22transport%22%2C%20%202)%3A%200%2C%20(%22transport%22%2C%20%203)%3A%209%2C%20(%22transport%22%2C%20%204)%3A14%2C%0A%20%20%20%20%20%20%20%20(%22transport%22%2C%20%205)%3A15%2C%20(%22transport%22%2C%20%206)%3A%208%2C%20(%22transport%22%2C%20%207)%3A12%2C%20(%22transport%22%2C%20%208)%3A13%2C%0A%20%20%20%20%20%20%20%20(%22bomber%22%2C%20%20%20%20%201)%3A%200%2C%20(%22bomber%22%2C%20%20%20%20%202)%3A17%2C%20(%22bomber%22%2C%20%20%20%20%203)%3A%200%2C%20(%22bomber%22%2C%20%20%20%20%204)%3A11%2C%0A%20%20%20%20%20%20%20%20(%22bomber%22%2C%20%20%20%20%205)%3A13%2C%20(%22bomber%22%2C%20%20%20%20%206)%3A10%2C%20(%22bomber%22%2C%20%20%20%20%207)%3A%200%2C%20(%22bomber%22%2C%20%20%20%20%208)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22fighter-bomber%22%2C1)%3A%200%2C%20(%22fighter-bomber%22%2C2)%3A%200%2C%20(%22fighter-bomber%22%2C3)%3A14%2C%0A%20%20%20%20%20%20%20%20(%22fighter-bomber%22%2C4)%3A%200%2C%20(%22fighter-bomber%22%2C5)%3A%200%2C%20(%22fighter-bomber%22%2C6)%3A12%2C%0A%20%20%20%20%20%20%20%20(%22fighter-bomber%22%2C7)%3A16%2C%20(%22fighter-bomber%22%2C8)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22supply%20plane%22%2C1)%3A%200%2C%20(%22supply%20plane%22%2C2)%3A%200%2C%20(%22supply%20plane%22%2C3)%3A%200%2C%0A%20%20%20%20%20%20%20%20(%22supply%20plane%22%2C4)%3A%200%2C%20(%22supply%20plane%22%2C5)%3A12%2C%20(%22supply%20plane%22%2C6)%3A18%2C%0A%20%20%20%20%20%20%20%20(%22supply%20plane%22%2C7)%3A%200%2C%20(%22supply%20plane%22%2C8)%3A18%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20n_pilots%20%20%20%20%3D%20max(p%20for%20(_%2C%20p)%20in%20LANG)%0A%20%20%20%20n_languages%20%3D%20len(%7Bl%20for%20(l%2C%20_)%20in%20LANG%7D)%0A%20%20%20%20n_aircraft%20%20%3D%20len(%7Bt%20for%20(t%2C%20_)%20in%20PTYPE%7D)%0A%20%20%20%20print(f%22Pilots%3A%20%7Bn_pilots%7D%20%20%7C%20%20Languages%3A%20%7Bn_languages%7D%20%20%7C%20%20Aircraft%20types%3A%20%7Bn_aircraft%7D%22)%0A%20%20%20%20return%20LANG%2C%20PTYPE%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.%20The%20model%20builds%20the%20feasible%20crew%20set%20%60ARCS%60%20internally%20during%20initialization%2C%20then%20solves%20two%20consecutive%20optimization%20problems.%20The%20output%20dict%20contains%20results%20from%20both%20phases%3A%20%60obj_nflying%60%20(Phase%201)%2C%20%60obj_score%60%20(Phase%202)%2C%20%60sol_fly1%60%2C%20%60sol_fly2%60%2C%20%60CREW%60%20(pilot%20pairs)%2C%20and%20%60SCORE%60%20(quality%20scores).%0A%0A%20%20%20%20The%20size%20of%20%60ARCS%60%20is%20determined%20at%20runtime%20from%20the%20data%2C%20making%20this%20model%20an%20example%20of%20dynamic%20set%20computation%20in%20Mosel.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(LANG%2C%20MODELS%2C%20PTYPE%2C%20mp)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Ff2crew_p.mos'%2C%20f'%7BMODELS%7D%2Ff2crew_p.bim')%0A%20%20%20%20_model%20%3D%20mp.load_model(f'%7BMODELS%7D%2Ff2crew_p.bim')%0A%20%20%20%20_model.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20_output%20%3D%20_model.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'LANG'%3A%20LANG%2C%20'PTYPE'%3A%20PTYPE%7D)%0A%20%20%20%20if%20_model.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_nflying%20%3D%20_output%5B'obj_nflying'%5D%0A%20%20%20%20%20%20%20%20obj_score%20%3D%20_output%5B'obj_score'%5D%0A%20%20%20%20%20%20%20%20sol_fly1%20%3D%20_output%5B'sol_fly1'%5D%0A%20%20%20%20%20%20%20%20sol_fly2%20%3D%20_output%5B'sol_fly2'%5D%0A%20%20%20%20%20%20%20%20crew_raw%20%3D%20_output%5B'CREW'%5D%0A%20%20%20%20%20%20%20%20score_raw%20%3D%20_output%5B'SCORE'%5D%0A%20%20%20%20%20%20%20%20num_arcs%20%3D%20max((a%20for%20a%2C%20_%20in%20crew_raw.keys()))%0A%20%20%20%20%20%20%20%20crews%20%3D%20%5B(int(crew_raw%5Ba%2C%201%5D)%2C%20int(crew_raw%5Ba%2C%202%5D))%20for%20a%20in%20range(1%2C%20num_arcs%20%2B%201)%5D%0A%20%20%20%20%20%20%20%20scores%20%3D%20%7Ba%3A%20int(score_raw%5Ba%5D)%20for%20a%20in%20range(1%2C%20num_arcs%20%2B%201)%7D%0A%20%20%20%20%20%20%20%20flying1%20%3D%20%5Ba%20for%20a%20in%20range(1%2C%20num_arcs%20%2B%201)%20if%20sol_fly1.get(a%2C%200)%20%3E%200.5%5D%0A%20%20%20%20%20%20%20%20flying2%20%3D%20%5Ba%20for%20a%20in%20range(1%2C%20num_arcs%20%2B%201)%20if%20sol_fly2.get(a%2C%200)%20%3E%200.5%5D%0A%20%20%20%20%20%20%20%20print(f'Phase%201%3A%20maximum%20crews%20flying%20%3D%20%7Bobj_nflying%3A.0f%7D')%0A%20%20%20%20%20%20%20%20for%20_a%20in%20flying1%3A%20%20%23%20Reconstruct%20pilot%20pairs%3A%20CREW%5B(a%2C1)%5D%20%3D%20pilot%20p%2C%20CREW%5B(a%2C2)%5D%20%3D%20pilot%20q%0A%20%20%20%20%20%20%20%20%20%20%20%20_p%2C%20q%20%3D%20crews%5B_a%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Pilots%20%7B_p%7D%20and%20%7Bq%7D%20%20(score%3A%20%7Bscores%5B_a%5D%7D)')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'Phase%202%3A%20maximum%20total%20score%20%3D%20%7Bobj_score%3A.0f%7D%20%20(crews%3A%20%7Blen(flying2)%7D)')%0A%20%20%20%20%20%20%20%20for%20_a%20in%20flying2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_p%2C%20q%20%3D%20crews%5B_a%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20Pilots%20%7B_p%7D%20and%20%7Bq%7D%20%20(score%3A%20%7Bscores%5B_a%5D%7D)')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7B_model.prob_status%7D)')%0A%20%20%20%20return%20crews%2C%20flying1%2C%20flying2%2C%20obj_nflying%2C%20obj_score%2C%20scores%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%20the%20two%20solutions%3A%20pilot%20assignments%20for%20Phase%201%20(maximum%20crews)%20and%20Phase%202%20(maximum%20quality%20score).%20Each%20pilot%20is%20shown%20as%20a%20node%3B%20assigned%20crew%20pairs%20are%20connected%20by%20edges.%20In%20this%20instance%2C%20Phase%201%20finds%204%20crews%20that%20cover%20all%208%20pilots%20exactly%2C%20so%20no%20pilot%20is%20left%20unassigned.%20The%20grey%20'Unassigned'%20marker%20is%20included%20for%20generality%2C%20but%20does%20not%20appear%20in%20this%20solution.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20crews%2C%0A%20%20%20%20flying1%2C%0A%20%20%20%20flying2%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20mpatches%2C%0A%20%20%20%20obj_nflying%2C%0A%20%20%20%20obj_score%2C%0A%20%20%20%20plt%2C%0A%20%20%20%20scores%2C%0A)%3A%0A%20%20%20%20if%20'crews'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20axes%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(12%2C%205))%0A%20%20%20%20%20%20%20%20import%20math%0A%0A%20%20%20%20%20%20%20%20def%20draw_crew_graph(ax%2C%20flying%2C%20title)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20n%20%3D%208%0A%20%20%20%20%20%20%20%20%20%20%20%20angles%20%3D%20%5B2%20*%20math.pi%20*%20i%20%2F%20n%20for%20i%20in%20range(n)%5D%20%20%23%20Circular%20layout%0A%20%20%20%20%20%20%20%20%20%20%20%20pos%20%3D%20%7Bp%20%2B%201%3A%20(math.cos(a)%2C%20math.sin(a))%20for%20p%2C%20a%20in%20enumerate(angles)%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20assigned%20%3D%20set()%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20a%20in%20flying%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20assigned.update(crews%5Ba%20-%201%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20p%20in%20range(1%2C%209)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%20%3D%20'%231E88E5'%20if%20p%20in%20assigned%20else%20'%23BDBDBD'%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.scatter(*pos%5Bp%5D%2C%20s%3D400%2C%20color%3Dcolor%2C%20zorder%3D3)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.annotate(f'P%7Bp%7D'%2C%20pos%5Bp%5D%2C%20textcoords%3D'offset%20points'%2C%20xytext%3D(0%2C%208)%2C%20ha%3D'center'%2C%20fontsize%3D9%2C%20fontweight%3D'bold')%20%20%23%20Draw%20all%20pilots%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20a%20in%20flying%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%2C%20q%20%3D%20crews%5Ba%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x0%2C%20y0%20%3D%20pos%5Bp%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x1%2C%20y1%20%3D%20pos%5Bq%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.plot(%5Bx0%2C%20x1%5D%2C%20%5By0%2C%20y1%5D%2C%20color%3D'%2343A047'%2C%20lw%3D2.5%2C%20zorder%3D2)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ax.annotate(f'%7Bscores%5Ba%5D%7D'%2C%20((x0%20%2B%20x1)%20%2F%202%2C%20(y0%20%2B%20y1)%20%2F%202)%2C%20fontsize%3D8%2C%20color%3D'%232E7D32'%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20bbox%3Ddict(boxstyle%3D'round%2Cpad%3D0.2'%2C%20fc%3D'white'%2C%20ec%3D'none'))%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.set_xlim(-1.4%2C%201.4)%20%20%23%20Draw%20crew%20edges%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.set_ylim(-1.4%2C%201.4)%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.set_aspect('equal')%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.axis('off')%0A%20%20%20%20%20%20%20%20%20%20%20%20ax.set_title(title)%0A%20%20%20%20%20%20%20%20draw_crew_graph(axes%5B0%5D%2C%20flying1%2C%20f'Phase%201%3A%20max%20crews%20(%7Bobj_nflying%3A.0f%7D%20crews)')%0A%20%20%20%20%20%20%20%20draw_crew_graph(axes%5B1%5D%2C%20flying2%2C%20f'Phase%202%3A%20max%20score%20(score%3D%7Bobj_score%3A.0f%7D%2C%20%7Blen(flying2)%7D%20crews)')%0A%20%20%20%20%20%20%20%20blue_p%20%3D%20mpatches.Patch(color%3D'%231E88E5'%2C%20label%3D'Assigned%20pilot')%0A%20%20%20%20%20%20%20%20grey_p%20%3D%20mpatches.Patch(color%3D'%23BDBDBD'%2C%20label%3D'Unassigned%20pilot')%0A%20%20%20%20%20%20%20%20_fig.legend(handles%3D%5Bblue_p%2C%20grey_p%5D%2C%20loc%3D'lower%20center'%2C%20ncol%3D2%2C%20fontsize%3D9)%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%20crew%20graph')%0A%20%20%20%20return%20(math%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%23%20F-3%20Landing%20schedule%20(MIP)%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%20F%2C%20Example%203*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60f3landing_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff3landing_p.mos)%0A%0A%20%20%20%20Ten%20planes%20are%20scheduled%20to%20arrive%20at%20a%20large%20airport.%20Each%20plane%20has%20an%20earliest%20arrival%20time%20(%60START%60)%20and%20a%20latest%20arrival%20time%20(%60STOP%60)%2C%20with%20a%20preferred%20target%20arrival%20time%20(%60TARGET%60)%20in%20between.%20Penalties%20are%20charged%20for%20each%20minute%20a%20plane%20lands%20early%20or%20late.%20Between%20any%20two%20consecutive%20landings%2C%20a%20minimum%20safety%20separation%20interval%20must%20be%20respected.%0A%0A%20%20%20%20The%20disjunctive%20constraint%20(plane%20%24p%24%20lands%20before%20or%20after%20plane%20%24q%24)%20is%20linearized%20using%20plane-specific%20BigM%20values%20%24M_%7Bp%2Cq%7D%20%3D%20%5Ctext%7BSTOP%7D_p%20%2B%20%5Ctext%7BDIST%7D_%7Bp%2Cq%7D%20-%20%5Ctext%7BSTART%7D_q%24.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BPLANES%7D%20%3D%20%5C%7B1%2C%20%5Cldots%2C%2010%5C%7D%24%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BSTART%7D_p%2C%20%5Ctext%7BSTOP%7D_p%2C%20%5Ctext%7BTARGET%7D_p%24%20-%20time%20window%20and%20target%20(minutes)%3B%20%24%5Ctext%7BCEARLY%7D_p%2C%20%5Ctext%7BCLATE%7D_p%24%20-%20penalty%20per%20minute%3B%20%24%5Ctext%7BDIST%7D_%7Bp%2Cq%7D%24%20-%20minimum%20separation%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bland%7D_p%20%5Cgeq%200%24%20(actual%20landing%20time)%3B%20%24%5Cmathit%7Bearly%7D_p%2C%20%5Cmathit%7Blate%7D_p%20%5Cgeq%200%24%20(deviation)%3B%20%24%5Cmathit%7Bprec%7D_%7Bp%2Cq%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20(1%20if%20%24p%24%20lands%20before%20%24q%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%20%20%5Cquad%20%26%20%5Csum_%7Bp%7D%20(%5Ctext%7BCEARLY%7D_p%20%5Ccdot%20%5Cmathit%7Bearly%7D_p%20%2B%20%5Ctext%7BCLATE%7D_p%20%5Ccdot%20%5Cmathit%7Blate%7D_p)%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Cmathit%7Bland%7D_p%20%2B%20%5Ctext%7BDIST%7D_%7Bp%2Cq%7D%20%5Cleq%20%5Cmathit%7Bland%7D_q%20%2B%20M_%7Bp%2Cq%7D%20%5Ccdot%20%5Cmathit%7Bprec%7D_%7Bq%2Cp%7D%20%5Cquad%20%5Cforall%5C%2C%20p%20%3E%20q%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%5Cmathit%7Bland%7D_p%20%2B%20%5Ctext%7BDIST%7D_%7Bp%2Cq%7D%20%5Cleq%20%5Cmathit%7Bland%7D_q%20%2B%20M_%7Bp%2Cq%7D%20%5Ccdot%20(1%20-%20%5Cmathit%7Bprec%7D_%7Bp%2Cq%7D)%20%5Cquad%20%5Cforall%5C%2C%20p%20%3C%20q%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%5Cmathit%7Bearly%7D_p%20%5Cgeq%20%5Ctext%7BTARGET%7D_p%20-%20%5Cmathit%7Bland%7D_p%2C%20%5Cquad%20%5Cmathit%7Blate%7D_p%20%5Cgeq%20%5Cmathit%7Bland%7D_p%20-%20%5Ctext%7BTARGET%7D_p%20%5Cquad%20%5Cforall%5C%2C%20p%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%20all%20input%20data%20as%201-indexed%20dicts.%20%60DIST%60%20is%20the%2010x10%20separation%20matrix%3A%20planes%201%20and%202%20need%203%20minutes%20of%20separation%20(same%20aircraft%20type)%2C%20all%20other%20pairs%20need%208%20or%2015%20minutes.%20Pass%20%60DIST%60%20as%20a%20flat%20dict%20with%20%60(p%2Cq)%60%20tuple%20keys.%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%20START%20%3D%20%7Bi%3A%20v%20for%20i%2C%20v%20in%20enumerate(%5B129%2C%20195%2C%2089%2C%2096%2C%20110%2C%20120%2C%20124%2C%20126%2C%20135%2C%20160%5D%2C%201)%7D%0A%20%20%20%20TARGET%20%3D%20%7Bi%3A%20v%20for%20i%2C%20v%20in%20enumerate(%5B155%2C%20258%2C%2098%2C%20106%2C%20123%2C%20135%2C%20138%2C%20140%2C%20150%2C%20180%5D%2C%201)%7D%0A%20%20%20%20STOP%20%3D%20%7Bi%3A%20v%20for%20i%2C%20v%20in%20enumerate(%5B559%2C%20744%2C%20510%2C%20521%2C%20555%2C%20576%2C%20577%2C%20573%2C%20591%2C%20657%5D%2C%201)%7D%0A%20%20%20%20CEARLY%20%3D%20%7Bi%3A%20v%20for%20i%2C%20v%20in%20enumerate(%5B10%2C%2010%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%5D%2C%201)%7D%0A%20%20%20%20CLATE%20%3D%20%7Bi%3A%20v%20for%20i%2C%20v%20in%20enumerate(%5B10%2C%2010%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%2C%2030%5D%2C%201)%7D%0A%20%20%20%20_dist_rows%20%3D%20%5B%5B0%2C%203%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%5D%2C%20%5B3%2C%200%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%2C%2015%5D%2C%20%5B15%2C%2015%2C%200%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%200%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%200%2C%208%2C%208%2C%208%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%208%2C%200%2C%208%2C%208%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%208%2C%208%2C%200%2C%208%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%208%2C%208%2C%208%2C%200%2C%208%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%2C%200%2C%208%5D%2C%20%5B15%2C%2015%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%2C%208%2C%200%5D%5D%0A%20%20%20%20DIST_F3%20%3D%20%7B(i%20%2B%201%2C%20j%20%2B%201)%3A%20_dist_rows%5Bi%5D%5Bj%5D%20for%20i%20in%20range(10)%20for%20j%20in%20range(10)%7D%0A%20%20%20%20print(f'%7B'Plane'%3A%3E6%7D%20%20%7B'Earliest'%3A%3E9%7D%20%20%7B'Target'%3A%3E7%7D%20%20%7B'Latest'%3A%3E7%7D%20%20%7B'EarlyPen'%3A%3E9%7D%20%20%7B'LatePen'%3A%3E8%7D')%0A%20%20%20%20for%20_p%20in%20range(1%2C%2011)%3A%0A%20%20%20%20%20%20%20%20print(f'%20%20%7B_p%3A%3E3%7D%20%20%20%20%7BSTART%5B_p%5D%3A%3E7%7D%20%20%20%20%7BTARGET%5B_p%5D%3A%3E6%7D%20%20%20%7BSTOP%5B_p%5D%3A%3E6%7D%20%20%20%20%20%7BCEARLY%5B_p%5D%3A%3E6%7D%2Fmin%20%20%20%20%7BCLATE%5B_p%5D%3A%3E5%7D%2Fmin')%0A%20%20%20%20return%20CEARLY%2C%20CLATE%2C%20DIST_F3%2C%20START%2C%20STOP%2C%20TARGET%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.%20After%20solving%2C%20%60sol_land%60%20gives%20the%20actual%20landing%20time%20for%20each%20plane.%20The%20output%20dict%20also%20includes%20%60TARGET%60%2C%20%60START%60%2C%20and%20%60STOP%60%20so%20the%20results%20can%20be%20displayed%20alongside%20the%20input%20time%20windows%20without%20re-referencing%20the%20Python%20dicts.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(CEARLY%2C%20CLATE%2C%20DIST_F3%2C%20MODELS%2C%20START%2C%20STOP%2C%20TARGET%2C%20mp)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Ff3landing_p.mos'%2C%20f'%7BMODELS%7D%2Ff3landing_p.bim')%0A%20%20%20%20_model%20%3D%20mp.load_model(f'%7BMODELS%7D%2Ff3landing_p.bim')%0A%20%20%20%20_model.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20_output%20%3D%20_model.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'START'%3A%20START%2C%20'STOP'%3A%20STOP%2C%20'TARGET'%3A%20TARGET%2C%20'CEARLY'%3A%20CEARLY%2C%20'CLATE'%3A%20CLATE%2C%20'DIST'%3A%20DIST_F3%7D)%0A%20%20%20%20if%20_model.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_val_f3%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20sol_land%20%3D%20%7Bint(k)%3A%20v%20for%20k%2C%20v%20in%20_output%5B'sol_land'%5D.items()%7D%0A%20%20%20%20%20%20%20%20print(f'Total%20deviation%20cost%3A%20%7Bobj_val_f3%3A.2f%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'%7B'Plane'%3A%3E6%7D%20%20%7B'Target'%3A%3E7%7D%20%20%7B'Actual'%3A%3E7%7D%20%20%7B'Dev%20(min)'%3A%3E10%7D%20%20%7B'Penalty'%3A%3E8%7D')%0A%20%20%20%20%20%20%20%20total_check%20%3D%200%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%2011)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_t%20%3D%20TARGET%5B_p%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_a%20%3D%20sol_land%5B_p%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_dev%20%3D%20_a%20-%20_t%0A%20%20%20%20%20%20%20%20%20%20%20%20pen%20%3D%20CEARLY%5B_p%5D%20*%20max(0%2C%20-_dev)%20%2B%20CLATE%5B_p%5D%20*%20max(0%2C%20_dev)%0A%20%20%20%20%20%20%20%20%20%20%20%20total_check%20%2B%3D%20pen%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20%7B_p%3A%3E3%7D%20%20%20%20%7B_t%3A%3E6%7D%20%20%20%20%7B_a%3A%3E6%7D%20%20%20%20%7B_dev%3A%3E%2B8.0f%7D%20%20%20%7Bpen%3A%3E7.0f%7D')%0A%20%20%20%20%20%20%20%20print(f'%7B'Total'%3A%3E43%7D%20%20%20%7Btotal_check%3A%3E7.0f%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7B_model.prob_status%7D)')%0A%20%20%20%20return%20obj_val_f3%2C%20sol_land%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%20Gantt-style%20chart%20of%20the%20landing%20schedule.%20For%20each%20plane%2C%20the%20grey%20bar%20spans%20the%20full%20**arrival%20window**%20-%20the%20range%20%60%5BSTART%2C%20STOP%5D%60%20within%20which%20the%20plane%20is%20permitted%20to%20land.%20The%20black%20tick%20marks%20the%20**target**%20(preferred)%20time%20within%20that%20window.%20The%20colored%20dot%20shows%20the%20**actual%20scheduled**%20landing%20time.%0A%0A%20%20%20%20Planes%20landing%20before%20their%20target%20are%20shown%20in%20blue%20(early)%3B%20after%20their%20target%20in%20orange%20(late).%20All%20actual%20landings%20remain%20inside%20the%20grey%20window%20-%20no%20plane%20lands%20outside%20its%20permitted%20range.%20Penalties%20arise%20purely%20from%20deviation%20from%20the%20target%3A%20even%20a%20landing%20a%20few%20minutes%20early%20or%20late%20but%20well%20inside%20the%20window%20incurs%20a%20cost.%20This%20deviation%20is%20often%20forced%20by%20the%20minimum%20separation%20constraints%20between%20consecutive%20landings.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(START%2C%20STOP%2C%20TARGET%2C%20mo%2C%20mpatches%2C%20obj_val_f3%2C%20plt%2C%20sol_land)%3A%0A%20%20%20%20if%20'sol_land'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(12%2C%205))%0A%20%20%20%20%20%20%20%20for%20_p%20in%20range(1%2C%2011)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_y%20%3D%2011%20-%20_p%0A%20%20%20%20%20%20%20%20%20%20%20%20s%20%3D%20START%5B_p%5D%20%20%23%20top%20to%20bottom%0A%20%20%20%20%20%20%20%20%20%20%20%20st%20%3D%20STOP%5B_p%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_t%20%3D%20TARGET%5B_p%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_a%20%3D%20sol_land%5B_p%5D%20%20%23%20Time%20window%20bar%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.barh(_y%2C%20st%20-%20s%2C%20left%3Ds%2C%20height%3D0.4%2C%20color%3D'%23B0BEC5'%2C%20alpha%3D0.7)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.plot(%5B_t%2C%20_t%5D%2C%20%5B_y%20-%200.3%2C%20_y%20%2B%200.3%5D%2C%20color%3D'%23212121'%2C%20lw%3D2)%0A%20%20%20%20%20%20%20%20%20%20%20%20_color%20%3D%20'%231E88E5'%20if%20_a%20%3C%20_t%20else%20'%23FB8C00'%20if%20_a%20%3E%20_t%20else%20'%2343A047'%20%20%23%20Target%20tick%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.scatter(_a%2C%20_y%2C%20color%3D_color%2C%20s%3D100%2C%20zorder%3D5)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.plot(%5B_t%2C%20_a%5D%2C%20%5B_y%2C%20_y%5D%2C%20color%3D_color%2C%20lw%3D1.5%2C%20linestyle%3D'--'%2C%20alpha%3D0.7)%0A%20%20%20%20%20%20%20%20%20%20%20%20_dev%20%3D%20_a%20-%20_t%20%20%23%20Actual%20landing%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.annotate(f'P%7B_p%7D%20(%7B_dev%3A%2B.0f%7Dm)'%2C%20(max(_a%2C%20s)%20%2B%202%2C%20_y)%2C%20va%3D'center'%2C%20fontsize%3D8%2C%20color%3D_color)%0A%20%20%20%20%20%20%20%20_ax.set_yticks(range(1%2C%2011))%0A%20%20%20%20%20%20%20%20_ax.set_yticklabels(%5Bf'Plane%20%7B11%20-%20p%7D'%20for%20p%20in%20range(1%2C%2011)%5D)%0A%20%20%20%20%20%20%20%20_ax.set_xlabel('Time%20(minutes)')%0A%20%20%20%20%20%20%20%20_ax.set_title(f'F-3%20Landing%20schedule%20-%20total%20penalty%20cost%20%3D%20%7Bobj_val_f3%3A.0f%7D')%0A%20%20%20%20%20%20%20%20early_p%20%3D%20mpatches.Patch(color%3D'%231E88E5'%2C%20label%3D'Landing%20early')%0A%20%20%20%20%20%20%20%20late_p%20%3D%20mpatches.Patch(color%3D'%23FB8C00'%2C%20label%3D'Landing%20late')%0A%20%20%20%20%20%20%20%20ontime_p%20%3D%20mpatches.Patch(color%3D'%2343A047'%2C%20label%3D'On%20time')%0A%20%20%20%20%20%20%20%20win_p%20%3D%20mpatches.Patch(color%3D'%23B0BEC5'%2C%20label%3D'Arrival%20window')%0A%20%20%20%20%20%20%20%20_ax.legend(handles%3D%5Bearly_p%2C%20late_p%2C%20ontime_p%2C%20win_p%5D%2C%20fontsize%3D8%2C%20loc%3D'upper%20right')%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%20landing%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%20F-4%20Transatlantic%20freight%20hubs%20(MIP)%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%20F%2C%20Example%204*%0A%0A%20%20%20%20%3E%20**Mosel%20models%3A**%20%5B%60f4hub_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff4hub_p.mos)%20(original)%2C%20%5B%60f4hub2_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff4hub2_p.mos)%20(improved)%0A%0A%20%20%20%20An%20airline%20transports%20freight%20between%203%20French%20cities%20(Marseille%2C%20Nice%2C%20Paris)%20and%203%20American%20cities%20(Atlanta%2C%20Boston%2C%20Chicago).%20The%20airline%20wants%20to%20designate%202%20cities%20as%20hubs%3A%20all%20freight%20between%20cities%20assigned%20to%20different%20hubs%20is%20routed%20through%20the%20hub-to-hub%20link%2C%20which%20benefits%20from%20a%2020%25%20distance%20discount.%20Which%20pair%20of%20hubs%20minimizes%20total%20transport%20cost%3F%0A%0A%20%20%20%20The%20original%20formulation%20(%60f4hub_p.mos%60)%20creates%20%246%5E4%20%3D%201296%24%20flow%20variables.%20The%20improved%20formulation%20(%60f4hub2_p.mos%60)%20exploits%20the%20geography%3A%20one%20hub%20must%20be%20in%20the%20US%20(cities%201-3)%2C%20one%20in%20Europe%20(cities%204-6).%20Using%20a%20**dynamic%20array**%2C%20only%20valid%20routes%20are%20created%20--%20same-continent%20pairs%20share%20a%20hub%2C%20cross-continent%20pairs%20use%20one%20hub%20each%20side%20--%20reducing%20the%20number%20of%20variables%20significantly.%20This%20also%20makes%20the%20model%20tractable%20within%20the%20Xpress%20Community%20(free)%20variable%20limit.%20Transport%20costs%20are%20computed%20by%20an%20inline%20%60calccost%60%20function.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BCITIES%7D%20%3D%20%5C%7B1%2C%20%5Cldots%2C%206%5C%7D%24%20(Atlanta%2C%20Boston%2C%20Chicago%2C%20Marseille%2C%20Nice%2C%20Paris)%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BQUANT%7D_%7Bi%2Cj%7D%24%20-%20freight%20volume%3B%20%24%5Ctext%7BDIST%7D_%7Bi%2Cj%7D%24%20-%20distance%3B%20%24%5Ctext%7BFACTOR%7D%3D0.8%24%20(hub%20discount)%3B%20%24%5Ctext%7BCOST%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%3D%20%5Ctext%7BDIST%7D_%7Bi%2Ck%7D%20%2B%200.8%20%5Ccdot%20%5Ctext%7BDIST%7D_%7Bk%2Cl%7D%20%2B%20%5Ctext%7BDIST%7D_%7Bl%2Cj%7D%24%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bhub%7D_i%20%5Cin%20%5C%7B0%2C1%5C%7D%24%3B%20%24%5Cmathit%7Bflow%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%5Cin%20%5C%7B0%2C1%5C%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%20%20%5Cquad%20%26%20%5Csum_%7Bi%2Cj%2Ck%2Cl%7D%20%5Ctext%7BQUANT%7D_%7Bi%2Cj%7D%20%5Ccdot%20%5Ctext%7BCOST%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%5Ccdot%20%5Cmathit%7Bflow%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Ctextstyle%5Csum_i%20%5Cmathit%7Bhub%7D_i%20%3D%202%3B%20%5Cquad%20%5Csum_%7Bk%2Cl%7D%20%5Cmathit%7Bflow%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%3D%201%20%5C%3B%20%5Cforall%5C%2C%20i%2Cj%3B%20%5Cquad%20%5Cmathit%7Bflow%7D_%7Bi%2Cj%2Ck%2Cl%7D%20%5Cleq%20%5Cmathit%7Bhub%7D_k%2C%5C%2C%20%5Cmathit%7Bhub%7D_l%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%20%60DIST%60%20and%20%60QUANT%60%20as%20dicts%20with%20%60(i%2Cj)%60%20tuple%20keys%20(1-indexed).%20City%20names%20are%20provided%20as%20a%201-indexed%20dict%20and%20passed%20to%20the%20model%20so%20they%20can%20be%20included%20in%20the%20output.%20The%20transport%20cost%20reduction%20%60FACTOR%60%20is%20a%20scalar.%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%20NAMES%20%3D%20%7B1%3A%20'Atlanta'%2C%202%3A%20'Boston'%2C%203%3A%20'Chicago'%2C%204%3A%20'Marseille'%2C%205%3A%20'Nice'%2C%206%3A%20'Paris'%7D%0A%20%20%20%20_dist_rows%20%3D%20%5B%5B0%2C%20945%2C%20605%2C%204667%2C%204749%2C%204394%5D%2C%20%5B945%2C%200%2C%20866%2C%203726%2C%203806%2C%203448%5D%2C%20%5B605%2C%20866%2C%200%2C%204471%2C%204541%2C%204152%5D%2C%20%5B4667%2C%203726%2C%204471%2C%200%2C%20109%2C%20415%5D%2C%20%5B4749%2C%203806%2C%204541%2C%20109%2C%200%2C%20431%5D%2C%20%5B4394%2C%203448%2C%204152%2C%20415%2C%20431%2C%200%5D%5D%0A%20%20%20%20quant_rows%20%3D%20%5B%5B0%2C%20500%2C%201000%2C%20300%2C%20400%2C%201500%5D%2C%20%5B1500%2C%200%2C%20250%2C%20630%2C%20360%2C%201140%5D%2C%20%5B400%2C%20510%2C%200%2C%20460%2C%20320%2C%20490%5D%2C%20%5B300%2C%20600%2C%20810%2C%200%2C%20820%2C%20310%5D%2C%20%5B400%2C%20100%2C%20420%2C%20730%2C%200%2C%20970%5D%2C%20%5B350%2C%201020%2C%20260%2C%20580%2C%20380%2C%200%5D%5D%0A%20%20%20%20DIST_F4%20%3D%20%7B(i%20%2B%201%2C%20j%20%2B%201)%3A%20_dist_rows%5Bi%5D%5Bj%5D%20for%20i%20in%20range(6)%20for%20j%20in%20range(6)%7D%0A%20%20%20%20QUANT_F4%20%3D%20%7B(i%20%2B%201%2C%20j%20%2B%201)%3A%20quant_rows%5Bi%5D%5Bj%5D%20for%20i%20in%20range(6)%20for%20j%20in%20range(6)%7D%0A%20%20%20%20FACTOR%20%3D%200.8%0A%20%20%20%20total_vol%20%3D%20sum((quant_rows%5Bi%5D%5Bj%5D%20for%20i%20in%20range(6)%20for%20j%20in%20range(6)))%0A%20%20%20%20print(f'Cities%3A%20%7Blist(NAMES.values())%7D')%0A%20%20%20%20print(f'Total%20freight%20volume%3A%20%7Btotal_vol%3A%2C%7D%20units%20across%20%7B6%20*%206%20-%206%7D%20city%20pairs')%0A%20%20%20%20return%20DIST_F4%2C%20FACTOR%2C%20NAMES%2C%20QUANT_F4%2C%20quant_rows%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.%20After%20loading%20the%20model%2C%20inspect%20its%20metadata%20using%20%60model.name%60%2C%20%60model.version%60%2C%20%60model.user_comment%60%2C%20and%20%60model.size%60%20before%20running%20the%20solve.%20These%20introspection%20properties%20are%20available%20on%20any%20loaded%20model%20without%20executing%20it.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DIST_F4%2C%20FACTOR%2C%20MODELS%2C%20NAMES%2C%20QUANT_F4%2C%20mp)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Ff4hub2_p.mos'%2C%20f'%7BMODELS%7D%2Ff4hub2_p.bim')%0A%20%20%20%20_model%20%3D%20mp.load_model(f'%7BMODELS%7D%2Ff4hub2_p.bim')%0A%20%20%20%20_model.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20print('Model%20metadata%3A')%0A%20%20%20%20%23%20Model%20introspection%20-%20available%20after%20load%2C%20before%20run%0A%20%20%20%20print(f'%20%20name%3A%20%20%20%20%7B_model.name%7D')%0A%20%20%20%20print(f'%20%20version%3A%20%7B_model.version%7D')%0A%20%20%20%20print(f'%20%20size%3A%20%20%20%20%7B_model.size%3A%2C%7D%20bytes')%0A%20%20%20%20if%20_model.user_comment%3A%0A%20%20%20%20%20%20%20%20print(f'%20%20comment%3A%20%7B_model.user_comment%7D')%0A%20%20%20%20print()%0A%20%20%20%20_output%20%3D%20_model.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'QUANT'%3A%20QUANT_F4%2C%20'DIST'%3A%20DIST_F4%2C%20'FACTOR'%3A%20FACTOR%2C%20'NAMES'%3A%20NAMES%7D)%0A%20%20%20%20if%20_model.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_val_f4%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20sol_hub%20%3D%20_output%5B'sol_hub'%5D%0A%20%20%20%20%20%20%20%20hub_indices%20%3D%20%5Bi%20for%20i%20in%20range(1%2C%207)%20if%20sol_hub.get(i%2C%200)%20%3E%200.5%5D%0A%20%20%20%20%20%20%20%20hub_names%20%3D%20%5BNAMES%5Bi%5D%20for%20i%20in%20hub_indices%5D%0A%20%20%20%20%20%20%20%20print(f'Minimum%20total%20transport%20cost%3A%20%7Bobj_val_f4%3A%2C.0f%7D')%0A%20%20%20%20%20%20%20%20print(f'Selected%20hubs%3A%20%7Bhub_names%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7B_model.prob_status%7D)')%0A%20%20%20%20return%20hub_names%2C%20obj_val_f4%2C%20sol_hub%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%20Bar%20chart%20comparing%20total%20freight%20volume%20handled%20through%20each%20city%20pair's%20hub-and-spoke%20routing.%20The%20selected%20hubs%20are%20highlighted.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(NAMES%2C%20hub_names%2C%20mo%2C%20mpatches%2C%20obj_val_f4%2C%20plt%2C%20quant_rows%2C%20sol_hub)%3A%0A%20%20%20%20if%20'sol_hub'%20in%20locals()%3A%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%20cities%20%3D%20list(NAMES.values())%0A%20%20%20%20%20%20%20%20colors%20%3D%20%5B'%231E88E5'%20if%20sol_hub.get(i%2C%200)%20%3E%200.5%20else%20'%23B0BEC5'%20for%20i%20in%20range(1%2C%207)%5D%0A%20%20%20%20%20%20%20%20outbound%20%3D%20%5Bsum((quant_rows%5Bi%5D%5Bj%5D%20for%20j%20in%20range(6)))%20for%20i%20in%20range(6)%5D%0A%20%20%20%20%20%20%20%20bars%20%3D%20_ax.bar(cities%2C%20outbound%2C%20color%3Dcolors%2C%20edgecolor%3D'white')%0A%20%20%20%20%20%20%20%20for%20bar%2C%20vol%20in%20zip(bars%2C%20outbound)%3A%20%20%23%20Show%20total%20outbound%20freight%20volume%20per%20city%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(bar.get_x()%20%2B%20bar.get_width()%20%2F%202%2C%20bar.get_height()%20%2B%2020%2C%20f'%7Bvol%3A%2C%7D'%2C%20ha%3D'center'%2C%20va%3D'bottom'%2C%20fontsize%3D9)%0A%20%20%20%20%20%20%20%20_ax.set_ylabel('Total%20outbound%20freight%20volume%20(units)')%0A%20%20%20%20%20%20%20%20_ax.set_title(f'F-4%20Freight%20hubs%20-%20total%20cost%20%3D%20%7Bobj_val_f4%3A%2C.0f%7D%20%20%7C%20%20Hubs%3A%20%7Bhub_names%7D')%0A%20%20%20%20%20%20%20%20hub_p%20%3D%20mpatches.Patch(color%3D'%231E88E5'%2C%20label%3D'Selected%20hub')%0A%20%20%20%20%20%20%20%20_city_p%20%3D%20mpatches.Patch(color%3D'%23B0BEC5'%2C%20label%3D'Non-hub%20city')%0A%20%20%20%20%20%20%20%20_ax.legend(handles%3D%5Bhub_p%2C%20_city_p%5D)%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%20hub%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%20F-5%20Flight%20tour%20planning%20(MIP)%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%20F%2C%20Example%205*%0A%0A%20%20%20%20%3E%20**Mosel%20model%3A**%20%5B%60f5tour_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff5tour_p.mos)%0A%0A%20%20%20%20A%20government%20in%20south-east%20Asia%20needs%20to%20establish%20an%20air%20supply%20route%20from%20the%20capital%20(city%201)%20visiting%206%20other%20airports%20and%20returning.%20Only%207%20runways%20are%20usable.%20The%20model%20finds%20the%20minimum-distance%20Hamiltonian%20tour.%20Since%20the%20distance%20matrix%20is%20symmetric%2C%20the%20%60.dat%60%20file%20stores%20only%20the%20upper%20triangle%3B%20the%20model%20fills%20the%20lower%20triangle%20automatically.%0A%0A%20%20%20%20The%20initial%20LP%20relaxation%20typically%20produces%20subtours.%20The%20model%20uses%20a%20recursive%20procedure%20%60breaksubtour%60%20that%20identifies%20the%20smallest%20subtour%2C%20adds%20an%20elimination%20constraint%2C%20and%20re-solves%20until%20a%20valid%20Hamiltonian%20tour%20is%20found.%0A%0A%20%20%20%20**Sets%3A**%20%24%5Ctext%7BCITIES%7D%20%3D%20%5C%7B1%2C%20%5Cldots%2C%207%5C%7D%24%20(city%201%20%3D%20capital)%0A%0A%20%20%20%20**Parameters%3A**%20%24%5Ctext%7BDIST%7D_%7Bi%2Cj%7D%24%20-%20distance%20between%20airports%20%24i%24%20and%20%24j%24%20(symmetric)%0A%0A%20%20%20%20**Decision%20variables%3A**%20%24%5Cmathit%7Bfly%7D_%7Bi%2Cj%7D%20%5Cin%20%5C%7B0%2C1%5C%7D%24%20-%201%20if%20the%20tour%20goes%20directly%20from%20city%20%24i%24%20to%20%24j%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%20%20%5Cquad%20%26%20%5Csum_%7Bi%20%5Cneq%20j%7D%20%5Ctext%7BDIST%7D_%7Bi%2Cj%7D%20%5Ccdot%20%5Cmathit%7Bfly%7D_%7Bi%2Cj%7D%20%5C%5C%0A%20%20%20%20%5Ctext%7Bsubject%20to%7D%20%5Cquad%20%26%20%5Csum_%7Bj%20%5Cneq%20i%7D%20%5Cmathit%7Bfly%7D_%7Bi%2Cj%7D%20%3D%201%20%5C%3B%20%5Cforall%5C%2C%20i%2C%20%5Cquad%20%5Csum_%7Bi%20%5Cneq%20j%7D%20%5Cmathit%7Bfly%7D_%7Bi%2Cj%7D%20%3D%201%20%5C%3B%20%5Cforall%5C%2C%20j%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_%7Bi%20%5Cin%20S%7D%20%5Cmathit%7Bfly%7D_%7Bi%2C%20%5Ctext%7BNEXT%7D(i)%7D%20%5Cleq%20%7CS%7C%20-%201%20%5Cquad%20%5Ctext%7Bfor%20each%20subtour%20%7D%20S%20%5Csubsetneq%20%5Ctext%7BCITIES%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%20The%20symmetric%20distance%20matrix%20is%20given%20as%20the%20upper%20triangle%20only.%20Pass%20the%20full%20symmetric%20matrix%20to%20MoselPy%20as%20a%20flat%20dict%20with%20%60(i%2Cj)%60%20tuple%20keys%3B%20the%20Mosel%20model%20calls%20%60DIST(j%2Ci)%20%3A%3D%20DIST(i%2Cj)%60%20after%20reading%2C%20which%20is%20harmless%20when%20the%20full%20matrix%20is%20already%20present.%20For%20standalone%20execution%20from%20the%20%60.dat%60%20file%2C%20the%20upper%20triangle%20format%20is%20preserved.%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%20%23%20Upper%20triangle%20values%20from%20the%20.dat%20file%0A%20%20%20%20upper_vals%20%3D%20%5B786%2C%20549%2C%20657%2C%20331%2C%20559%2C%20250%2C%20668%2C%20979%2C%20593%2C%20224%2C%20905%2C%20316%2C%20607%2C%20472%2C%20467%2C%20890%2C%20769%2C%20400%2C%20386%2C%20559%2C%20681%5D%0A%20%20%20%20pairs%20%3D%20%5B(i%2C%20j)%20for%20i%20in%20range(1%2C%208)%20for%20j%20in%20range(i%20%2B%201%2C%208)%5D%0A%20%20%20%20dist_upper%20%3D%20%7Bpairs%5Bk%5D%3A%20upper_vals%5Bk%5D%20for%20k%20in%20range(len(pairs))%7D%0A%20%20%20%20DIST_F5%20%3D%20%7B%7D%0A%20%20%20%20%23%20Build%20full%20symmetric%20matrix%20for%20Python%20input%0A%20%20%20%20for%20(_i%2C%20_j)%2C%20v%20in%20dist_upper.items()%3A%0A%20%20%20%20%20%20%20%20DIST_F5%5B_i%2C%20_j%5D%20%3D%20v%0A%20%20%20%20%20%20%20%20DIST_F5%5B_j%2C%20_i%5D%20%3D%20v%0A%20%20%20%20city_names%20%3D%20%5B'Capital'%2C%20'City%202'%2C%20'City%203'%2C%20'City%204'%2C%20'City%205'%2C%20'City%206'%2C%20'City%207'%5D%0A%20%20%20%20print(f'Airports%3A%20%7B7%7D%20%20%7C%20%20Distance%20range%3A%20%7Bmin(dist_upper.values())%7D%20-%20%7Bmax(dist_upper.values())%7D%20km')%0A%20%20%20%20dm%20%3D%20pd.DataFrame(%5B%5BDIST_F5.get((i%2C%20j)%2C%200)%20for%20j%20in%20range(1%2C%208)%5D%20for%20i%20in%20range(1%2C%208)%5D%2C%20index%3D%5Bf'C%7Bi%7D'%20for%20i%20in%20range(1%2C%208)%5D%2C%20columns%3D%5Bf'C%7Bj%7D'%20for%20j%20in%20range(1%2C%208)%5D)%0A%20%20%20%20print('Distance%20matrix%20(km)%3A')%0A%20%20%20%20%23%20Show%20distance%20matrix%0A%20%20%20%20print(dm.to_string())%0A%20%20%20%20return%20DIST_F5%2C%20city_names%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%20context%20manager%20form%20of%20%60mp.load_model()%60.%20The%20%60with%60%20statement%20ensures%20model%20resources%20are%20released%20automatically%20when%20the%20block%20exits%2C%20even%20if%20an%20exception%20occurs.%20This%20is%20the%20recommended%20pattern%20when%20the%20model%20is%20used%20once%20and%20will%20not%20be%20reused.%20The%20%60breaksubtour%60%20recursive%20procedure%20runs%20entirely%20inside%20Mosel%2C%20adding%20subtour%20elimination%20cuts%20as%20needed%20and%20re-solving%20until%20a%20valid%20Hamiltonian%20tour%20is%20found.%0A%0A%20%20%20%20**Note%20on%20alternative%20optimal%20solutions%3A**%20The%20tour%20produced%20here%20may%20visit%20the%20airports%20in%20a%20different%20order%20than%20the%20standalone%20%60mosel%20exec%60%20run%2C%20but%20both%20achieve%20the%20same%20minimum%20distance%20of%202575%20km.%20TSP%20problems%20typically%20have%20multiple%20optimal%20tours%20-%20any%20cyclic%20permutation%20or%20reversal%20of%20a%20tour%20yields%20an%20equally%20valid%20solution%20with%20the%20same%20total%20distance.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DIST_F5%2C%20MODELS%2C%20city_names%2C%20mp)%3A%0A%20%20%20%20mp.compile_model(f'%7BMODELS%7D%2Ff5tour_p.mos'%2C%20f'%7BMODELS%7D%2Ff5tour_p.bim')%0A%20%20%20%20with%20mp.load_model(f'%7BMODELS%7D%2Ff5tour_p.bim')%20as%20_model%3A%0A%20%20%20%20%20%20%20%20_model.set_default_stream(mp.StreamType.OUTPUT%2C%20'null%3A')%0A%20%20%20%20%20%20%20%20_output%20%3D%20_model.run(exec_params%3D%7B'DATASOURCE'%3A%20'moselpy%3A'%2C%20'OUTFILE'%3A%20'moselpy%3A'%7D%2C%20input_data%3D%7B'DIST'%3A%20DIST_F5%7D)%0A%20%20%20%20%20%20%20%20prob_ok%20%3D%20_model.prob_status.is_solution_available%0A%20%20%20%20if%20prob_ok%3A%0A%20%20%20%20%20%20%20%20obj_val_f5%20%3D%20_output%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20nextc_raw%20%3D%20_output%5B'NEXTC'%5D%0A%20%20%20%20%20%20%20%20NEXTC%20%3D%20%7Bint(i)%3A%20int(v)%20for%20i%2C%20v%20in%20nextc_raw.items()%7D%0A%20%20%20%20%20%20%20%20tour%20%3D%20%5B1%5D%0A%20%20%20%20%23%20model%20resources%20freed%20here%0A%20%20%20%20%20%20%20%20_current%20%3D%201%0A%20%20%20%20%20%20%20%20for%20_%20in%20range(6)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_current%20%3D%20NEXTC%5B_current%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20tour.append(_current)%0A%20%20%20%20%20%20%20%20tour.append(1)%0A%20%20%20%20%20%20%20%20print(f'Minimum%20tour%20distance%3A%20%7Bobj_val_f5%3A.0f%7D%20km')%0A%20%20%20%20%20%20%20%20print(f'Tour%3A%20%7B'%20-%3E%20'.join((city_names%5Bc%20-%201%5D%20for%20c%20in%20tour))%7D')%20%20%23%20Reconstruct%20tour%20starting%20from%20city%201%0A%20%20%20%20%20%20%20%20total%20%3D%20sum((DIST_F5%5Btour%5Bk%5D%2C%20tour%5Bk%20%2B%201%5D%5D%20for%20k%20in%20range(7)))%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'%7B'Leg'%3A%3C6%7D%20%20%7B'From'%3A%3C10%7D%20%20%7B'To'%3A%3C10%7D%20%20%7B'Distance'%3A%3E10%7D')%0A%20%20%20%20%20%20%20%20for%20_k%20in%20range(7)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_a%2C%20_b%20%3D%20(tour%5B_k%5D%2C%20tour%5B_k%20%2B%201%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20print(f'%20%20%7B_k%20%2B%201%3A%3C4%7D%20%20%7Bcity_names%5B_a%20-%201%5D%3A%3C10%7D%20%20%7Bcity_names%5B_b%20-%201%5D%3A%3C10%7D%20%20%7BDIST_F5%5B_a%2C%20_b%5D%3A%3E8%7D%20km')%20%20%23%20return%20to%20capital%0A%20%20%20%20%20%20%20%20print(f'%7B'Total'%3A%3E30%7D%20%20%20%7Btotal%3A%3E8%7D%20km')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print('No%20solution%20found')%0A%20%20%20%20return%20obj_val_f5%2C%20tour%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%20Tour%20visualization%20using%20a%20circular%20layout%20for%20the%207%20airports.%20Arrows%20show%20the%20flight%20sequence%3B%20the%20capital%20(city%201)%20is%20highlighted%20in%20red.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DIST_F5%2C%20math%2C%20mo%2C%20mpatches%2C%20obj_val_f5%2C%20plt%2C%20tour)%3A%0A%20%20%20%20if%20'tour'%20in%20locals()%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(figsize%3D(7%2C%207))%0A%20%20%20%20%20%20%20%20n%20%3D%207%0A%20%20%20%20%20%20%20%20angles%20%3D%20%5Bmath.pi%20%2F%202%20-%202%20*%20math.pi%20*%20i%20%2F%20n%20for%20i%20in%20range(n)%5D%0A%20%20%20%20%20%20%20%20pos%20%3D%20%7Bi%20%2B%201%3A%20(math.cos(angles%5Bi%5D)%2C%20math.sin(angles%5Bi%5D))%20for%20i%20in%20range(n)%7D%0A%20%20%20%20%20%20%20%20for%20_k%20in%20range(7)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_a%2C%20_b%20%3D%20(tour%5B_k%5D%2C%20tour%5B_k%20%2B%201%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20x0%2C%20y0%20%3D%20pos%5B_a%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20x1%2C%20y1%20%3D%20pos%5B_b%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.annotate(''%2C%20xy%3D(x1%20*%200.88%2C%20y1%20*%200.88)%2C%20xytext%3D(x0%20*%200.88%2C%20y0%20*%200.88)%2C%20arrowprops%3Ddict(arrowstyle%3D'-%3E'%2C%20color%3D'%231E88E5'%2C%20lw%3D2%2C%20connectionstyle%3D'arc3%2Crad%3D0.08'))%0A%20%20%20%20%20%20%20%20%20%20%20%20mx%2C%20my%20%3D%20((x0%20%2B%20x1)%20%2F%202%20*%200.88%2C%20(y0%20%2B%20y1)%20%2F%202%20*%200.88)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.text(mx%2C%20my%2C%20f'%7BDIST_F5%5B_a%2C%20_b%5D%7D%20km'%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D7%2C%20color%3D'%231565C0'%2C%20bbox%3Ddict(boxstyle%3D'round%2Cpad%3D0.15'%2C%20fc%3D'white'%2C%20ec%3D'none'%2C%20alpha%3D0.8))%0A%20%20%20%20%20%20%20%20for%20c%20in%20range(1%2C%208)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20x%2C%20_y%20%3D%20pos%5Bc%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_color%20%3D%20'%23E53935'%20if%20c%20%3D%3D%201%20else%20'%2343A047'%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.scatter(x%2C%20_y%2C%20s%3D350%2C%20color%3D_color%2C%20zorder%3D5)%0A%20%20%20%20%20%20%20%20%20%20%20%20offset_x%20%3D%20x%20*%200.12%0A%20%20%20%20%20%20%20%20%20%20%20%20offset_y%20%3D%20_y%20*%200.12%0A%20%20%20%20%20%20%20%20%20%20%20%20order%20%3D%20tour.index(c)%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.annotate(f'C%7Bc%7D%5Cn(%7Border%7D)'%2C%20(x%20%2B%20offset_x%2C%20_y%20%2B%20offset_y)%2C%20ha%3D'center'%2C%20va%3D'center'%2C%20fontsize%3D8%2C%20fontweight%3D'bold'%2C%20color%3D'white'%20if%20c%20%3D%3D%201%20else%20'black')%0A%20%20%20%20%20%20%20%20_ax.set_xlim(-1.3%2C%201.3)%0A%20%20%20%20%20%20%20%20_ax.set_ylim(-1.3%2C%201.3)%0A%20%20%20%20%20%20%20%20_ax.set_aspect('equal')%0A%20%20%20%20%20%20%20%20_ax.axis('off')%0A%20%20%20%20%20%20%20%20_ax.set_title(f'F-5%20Flight%20tour%20-%20total%20distance%20%3D%20%7Bobj_val_f5%3A.0f%7D%20km%20%20(numbers%20%3D%20visit%20order)')%0A%20%20%20%20%20%20%20%20cap_p%20%3D%20mpatches.Patch(color%3D'%23E53935'%2C%20label%3D'Capital%20(start%2Fend)')%0A%20%20%20%20%20%20%20%20_city_p%20%3D%20mpatches.Patch(color%3D'%2343A047'%2C%20label%3D'Airport')%0A%20%20%20%20%20%20%20%20_ax.legend(handles%3D%5Bcap_p%2C%20_city_p%5D%2C%20loc%3D'lower%20center')%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%20tour%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%23%20F-5%20Variant%3A%20callback-based%20subtour%20elimination%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%3E%20**Mosel%20model%3A**%20%5B%60f5tour_cb_p.mos%60%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmarimo%2Fpublic%2Fmoselpy%2Fmodels%2Fair_transport%2Ff5tour_cb_p.mos)%0A%0A%20%20%20%20The%20sequential%20re-solve%20approach%20in%20%60f5tour_p.mos%60%20works%20but%20restarts%20the%20MIP%20each%20time%20a%20subtour%20is%20found.%20A%20cleaner%20alternative%20registers%20a%20**%60XPRS_CB_PREINTSOL%60%20callback**%20inside%20Mosel%3A%20every%20time%20the%20solver%20finds%20an%20integer%20solution%2C%20the%20callback%20fires%20and%20checks%20for%20subtours%20before%20the%20solution%20is%20accepted.%0A%0A%20%20%20%20-%20**Heuristic%20solutions**%20(%60isheur%3Dtrue%60)%3A%20subtours%20are%20rejected%20immediately%20with%20%60rejectintsol%60%20--%20no%20cuts%20can%20be%20added%20for%20heuristic%20solutions.%0A%20%20%20%20-%20**B%26B%20node%20solutions**%20(%60isheur%3Dfalse%60)%3A%20for%20each%20subtour%20%24S%24%20found%2C%20two%20lazy%20cuts%20are%20injected%20via%20%60addcuts%60%20and%20the%20node%20LP%20is%20re-solved%20with%20the%20new%20constraints%20active.%20The%20solver%20never%20needs%20to%20restart.%0A%0A%20%20%20%20Setting%20%60XPRS_MIPDUALREDUCTIONS%3D0%60%20is%20required%20so%20the%20solver%20does%20not%20apply%20dual%20reductions%20based%20on%20an%20incomplete%20constraint%20set.%0A%0A%20%20%20%20From%20Python's%20perspective%2C%20%60execute_model%60%20is%20called%20**once**%20and%20the%20full%20B%26B%20with%20lazy%20constraint%20generation%20happens%20automatically%20inside%20Mosel.%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%20Use%20the%20single%20execution%20command.%20The%20model%20registers%20the%20callback%2C%20sets%20the%20required%20parameters%2C%20and%20solves.%20All%20subtour%20elimination%20happens%20inside%20Mosel%20during%20B%26B%3B%20Python%20receives%20only%20the%20final%20tour.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DIST_F5%2C%20MODELS%2C%20mp%2C%20obj_val_f5)%3A%0A%20%20%20%20model_cb%2C%20output_cb%20%3D%20mp.execute_model(f'%7BMODELS%7D%2Ff5tour_cb_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'DIST'%3A%20DIST_F5%7D)%0A%20%20%20%20if%20model_cb.prob_status.is_solution_available%3A%0A%20%20%20%20%20%20%20%20obj_cb%20%3D%20output_cb%5B'obj_val'%5D%0A%20%20%20%20%20%20%20%20nextc_cb%20%3D%20output_cb%5B'NEXTC'%5D%0A%20%20%20%20%20%20%20%20tour_cb%20%3D%20%5B1%5D%0A%20%20%20%20%20%20%20%20_current%20%3D%201%0A%20%20%20%20%20%20%20%20for%20_%20in%20range(len(nextc_cb)%20-%201)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_current%20%3D%20round(nextc_cb.get(_current%2C%200))%0A%20%20%20%20%20%20%20%20%20%20%20%20tour_cb.append(_current)%0A%20%20%20%20%20%20%20%20tour_cb.append(1)%0A%20%20%20%20%20%20%20%20print(f'Callback%20version%20-%20minimum%20tour%20distance%3A%20%7Bobj_cb%3A.0f%7D')%0A%20%20%20%20%20%20%20%20print(f'Tour%3A%20%7B'%20-%3E%20'.join((str(c)%20for%20c%20in%20tour_cb))%7D')%0A%20%20%20%20%20%20%20%20print()%0A%20%20%20%20%20%20%20%20print(f'Both%20approaches%20give%20same%20result%3A%20%7Babs(obj_cb%20-%20obj_val_f5)%20%3C%201%7D')%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print(f'No%20solution%20found%20(%7Bmodel_cb.prob_status%7D)')%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
474cd5a578a28a587e0f419613379e87