Welcome back to OR-Path!
Field Notes is about applying Operations Research inside organizations, where software, data and people shape how decisions get made.
In this issue
A run that does not depend on its author’s memory.
A small project structure, with reasons for each boundary.
Results and failures that a colleague can act on.
In the Jupyter edition, João used a notebook to plan routes for a small fleet. Let’s extend that example: a colleague needs to prepare Monday’s plan while João is away. The optimizer works, but the colleague still needs to know which file to replace, which parameter to change and which cells to run again.
I would use that handover to guide the next step. Start by making one complete run understandable and repeatable, then decide how much infrastructure it needs.
So let’s get straight to it.
1. Define what one run means
For João’s routing problem, one run might consume orders, available vehicles and a travel-time matrix. Configuration would specify the planning date, solver time limit and other scenario choices. The output would contain routes, unassigned orders where the model allows them, and a summary of the solve.
Those choices should be explicit. If Monday’s vehicle availability is still hard-coded halfway through the notebook, the colleague must understand the implementation just to supply the day’s inputs.
A useful first goal is to execute the workflow from a fresh process, without manually editing cells or relying on variables from an earlier session. Keep a copy or version reference for the inputs and configuration used, alongside the code version, so a surprising plan can be investigated later.
2. Give each part a reason to exist
For a small Python optimizer, I would consider a structure like this:
route-planner/
README.md
pyproject.toml
config.yaml
data/examples/monday.json
runs/
tests/
src/optimizer/
__init__.py
input.py
model.py
solve.py
output.py
run.pyThis is a suggested structure, not a ready-to-run repository. The boundaries matter more than the filenames:
input.pyreads and validates the input, converts units and prepares the problem data. Missing vehicle capacities or inconsistent identifiers should be caught here, with a message that identifies the problem.model.pybuilds variables, constraints and the objective from that prepared data. It should not need to know which spreadsheet column originally held an order’s weight.solve.pyapplies solver settings, runs the optimization and returns its termination status and solution availability. Separating this step makes it easier to distinguish model-building time from solve time.output.pyextracts decisions only when a solution is available, maps internal indices back to business identifiers and formats the results and diagnostics.run.pycoordinates the steps, passes their results along and records progress. Reading it should reveal the workflow without requiring someone to inspect every constraint.
Related functions can stay together while the project is small. I would split them when it makes a change easier to locate or a behavior easier to test.
The surrounding files also have a job. data/examples/ holds a small, non-sensitive instance; tests/ holds meaningful checks; runs/ can hold local run outputs and logs. pyproject.toml describes the package and dependencies, while README.md explains setup and execution, including solver access. With a src layout, installing the package is part of that setup.
3. Test the boundaries with a realistic change
Suppose the orders previously arrived as CSV files and now come from another application as JSON. If the business meaning is unchanged, most of that adjustment should stay in input.py. The model should still receive the same prepared orders, capacities and travel times.
If the application also introduces split deliveries, that is a different change: the decision itself has changed, so the model may need to change too. Module boundaries help locate work; they do not make every business change independent.
Likewise, switching the exported plan from a spreadsheet to JSON should mainly affect output.py. João can still use a notebook to explore scenarios by importing the same project functions, avoiding a second copy of the model that gradually drifts apart.
4. Make the outcome useful, including when it fails
A file called routes.csv is not enough to tell the colleague whether the run succeeded. Different outcomes need different explanations:
Invalid input: a vehicle has no capacity. Stop before solving and identify the offending record.
Proven infeasibility: no plan satisfies the model’s constraints for these inputs. Explain the outcome and support investigation; do not silently relax constraints.
Time limit with a feasible solution: a candidate plan exists, but optimality may be unproven. Report the status and available quality measures, and apply agreed acceptance criteria.
Time limit without a feasible solution: no plan is available yet. That does not prove infeasibility.
Execution error: a file cannot be read or the solver cannot start. Record the failing step and make recovery ownership clear.
Check termination status and solution availability before reading decision-variable values; the exact fields depend on the solver. Gurobi’s status reference illustrates why a time-limit status alone is insufficient.
For Operations Research systems, returning a feasible plan and approving it for operational use are separate responsibilities. The team needs to decide what quality is acceptable, who reviews exceptions and what happens when no usable plan arrives.
So… what now?
Give a colleague the README and a representative instance. Ask them to run it without your guidance, find the resulting plan, and explain the outcome. Then try an invalid input and a known infeasible case. Each question they ask reveals something still dependent on the author’s memory.
That exercise gives the project a clearer execution and maintenance path. Scheduling, access control, integration and recovery still need decisions. With the workflow explicit, the next question becomes easier to discuss: should it run on a schedule, inside another application, or behind an API?
Until next time 👋
Ricardo
OR-Path


