Skip to content

●  How to use · Routing & Logistics

Facility location

Choose which sites to open and which customers each serves, trading setup cost against distance under capacity limits.

What it decides

For every consumer, the solver picks the facility that serves it. A facility is open when at least one consumer uses it.

Minimal example

from optimaru import facility_location as fl
from optimaru.facility_location.models import (
    ConsumerModel, FacilityLocationProblemModel, FacilityModel, LocationModel,
)

problem = FacilityLocationProblemModel(
    facilities=[
        FacilityModel(id="F1", location=LocationModel(latitude=51.50, longitude=-0.12),
                      setup_cost=50_000, capacity=300),
        FacilityModel(id="F2", location=LocationModel(latitude=51.52, longitude=-0.08),
                      setup_cost=40_000, capacity=300),
    ],
    consumers=[
        ConsumerModel(id=f"C{i}", demand=30,
                      location=LocationModel(latitude=51.49 + i * 0.005, longitude=-0.10))
        for i in range(6)
    ],
)

core = problem.to_core()
fl.solve(core, time_limit_seconds=2.0, random_seed=42)  # solves in place
solved = FacilityLocationProblemModel.from_core(core)
print(solved.score_hard, solved.score_soft)
print({c.id: problem.facilities[c.facility].id for c in solved.consumers})

The same use case as a Jupyter notebook, with charts and saved outputs: examples/facility-location.ipynb ↗

Input parameters

LocationModel

Field Meaning
latitude required Latitude in degrees. longitude required Longitude in degrees.

FacilityModel

Field Meaning
id required Facility id. location required Where it is. setup_cost required Cost of opening it. capacity required Most demand it can serve.

ConsumerModel

Field Meaning
id required Consumer id. location required Where it is. demand required How much it needs.

FacilityLocationProblemModel

Field Meaning
facilities required Candidate facilities. consumers required Consumers to serve.

Solve parameters

What you pass to the solve call. Time limit, seed and parallel count are here.

Parameter Default Meaning
time_limit_seconds None Search budget in seconds, from 0 to 1e9. Left unset, it is the config's spent_limit_seconds, 30 by default.
random_seed None Seed for the search. Left unset, it is the config's value, 0 by default.
parallel_solver_count None Independent parallel starts. Each gets its own seed and the full budget, and the best result wins. Left unset, it is the config's value, 1 by default.
stop_at_first_feasible False Stop as soon as hard 0 is reached.
on_new_best None Callback run on each new best solution. If it raises, the solve stops and raises the same exception.
config None A SolverConfig as the base configuration. Keyword arguments here override it for this solve only; the object you pass is not changed.
constraints None Which constraints to score, and how. Left unset, the defaults apply.
All search options (31)

Set these on a SolverConfig and pass it as config=.

Parameter Default Meaning
best_score_limit — Stop early once the best score reaches this value. By default it never fires.
change_move_weight 0.75 Relative share of change moves. 0 disables them.
close_facility_keep 8 How far down a consumer's nearest facilities that move looks.
close_facility_migrate_probability 0.25 Probability that the emptied facility's consumers move together to one alternative site.
close_facility_moves True Enable the move that empties one used facility at once.
custom_move_probability 1.0 Cap on how often a step may draw a use-case move. 1 means no cap.
descent_weight 0.0 Share of best-improvement descent. 0 turns it off.
full_assert False Check every incremental score against a full recompute. Slow; for debugging.
late_acceptance_size 0 Late-acceptance history length. 0 picks it from the instance size.
nearby_probability 0.0 Probability of drawing move partners from nearby entities. 0 disables it.
nearby_size 20 How many neighbours each entity keeps. 0 picks it automatically or disables it, depending on the use case.
on_new_best None Callback run on each new best solution. If it raises, the solve stops and raises the same exception.
parallel_solver_count 1 Independent parallel starts, each with its own seed and the full budget. The best one wins.
random_seed 0 Seed for the search.
ruin_recreate True Run adaptive ruin-and-recreate kicks when the search stagnates.
ruin_recreate_as_move_max_ruin 12 Most entities that small ruin removes.
ruin_recreate_as_move_weight 0.0 Probability that a step is a small ruin and repair, judged like any other move. Negative picks it automatically where the use case supports that; 0 turns it off.
ruin_recreate_max_ruin_count 60 Absolute cap on entities removed per kick.
ruin_recreate_max_ruin_fraction 0.3 Largest share of the solution one kick removes.
ruin_recreate_min_ruin_fraction 0.1 Smallest share of the solution one kick removes.
ruin_recreate_operator_decay 0.8 How fast operator weights adapt.
ruin_recreate_polish_steps 5000 Hill-climbing moves that polish each kick. Not counted against step_count_limit.
ruin_recreate_rewards [33.0, 9.0, 3.0, 0.0] Operator rewards for a new best, a better, an accepted and a rejected result.
ruin_recreate_unimproved_step_trigger 5000 Steps without a new best before a kick.
spent_limit_seconds 30.0 Time budget in seconds, from 0 to 1e9.
step_count_limit 0 Stop after this many steps. 0 means no limit.
swap_move_weight 0.25 Relative share of swap moves. 0 disables them.
unimproved_spent_limit_seconds 0.0 Stop after this many seconds without a new best. 0 means no limit.
unimproved_step_count_limit 0 Stop after this many steps without a new best. 0 means no limit.
value_recommender_keep 8 How many nearest facilities that draw considers.
value_recommender_probability 0.0 Probability that a change move draws from the nearest facilities or feasible teams. 0 means off.

What comes back

solve takes the engine object and solves it in place. Convert back with from_core to read the result.

ConsumerModel

Field Meaning
facility Position in facilities of the serving facility. -1 means unassigned.

FacilityLocationProblemModel

Field Meaning
score_hard Hard score. 0 means feasible. score_soft Soft score: weighted setup cost and distance, negated.

Constraints

Pick them by calling their classes from optimaru.facility_location.constraints, such as FacilityCapacityCost(). Only what you pass is scored; pass nothing to keep the defaults. A list may name a constraint more than once, and each pick is scored on its own. Each one takes these parameters:

Parameter Default Meaning
weight None Multiplier on the constraint's cost, in units of the level in effect. Left unset, the default weight below applies. A constraint whose default weight is 0 is off until you give it a weight.
level None "hard" makes the constraint a must and "soft" a preference, as do Level.HARD and Level.SOFT. Left unset, it keeps the level below. This use case has no medium level, so "medium" raises ValueError. A reward refuses "hard", and a fixed level refuses any change; both raise ValueError.
exponent 1.0 Sign-preserving power on the cost, above 0, taken after offset. 2.0 squares it, so concentrated violations cost much more. It applies per unit, such as a vehicle or a day, or to the total of a constraint scored as one sum.
offset 0.0 Subtracted from the cost x before the exponent, per unit or total like the exponent. It is in the cost's own measure: seconds, minutes, a count and so on. Any finite number of either sign, keyword only. clip decides what happens below it.
clip True True makes offset a threshold: only the part beyond it counts, max(0, x - offset) ** exponent, so a cost at or below it adds nothing. False takes the literal (x - offset) ** exponent, sign kept: a unit below the offset counts negative, as a reward, even one whose cost is 0. On a level that counts toward hard, clip=False with a non-zero offset raises ValueError, since hard 0 must mean no violation. Keyword only.

This continues the minimal example above.

from optimaru.facility_location.constraints import (
    DistanceFromFacilityCost, FacilityCapacityCost, FacilitySetupCost, get_constraints,
)

picked = get_constraints([FacilityCapacityCost(), FacilitySetupCost(), DistanceFromFacilityCost(weight=10)])
custom = problem.to_core()
print(fl.solve(custom, time_limit_seconds=2.0, random_seed=42, constraints=picked))
Name Level Default weight What it does
facility_capacity_cost Hard 1 Demand above capacity at a facility in use.
facility_setup_cost Soft 2 Setup cost of every facility serving at least one consumer.
distance_from_facility_cost Soft 6 Distance in meters from each assigned consumer to its facility.

How constraints work →

Starting from an existing solution

This needs the latest release: pip install -U optimaru.

with_initial_solution(problem, solution) returns a copy of problem whose search starts from solution, a solved FacilityLocationProblemModel or that model as a JSON dict. solve takes the engine object, so convert the result with to_core().

This continues the minimal example above.

restart = fl.with_initial_solution(problem, solved).to_core()
fl.solve(restart, time_limit_seconds=2.0, random_seed=43)
print(restart.score, restart.total_cost())
  • Consumers are matched by id.
  • A complete solution skips construction: the search starts exactly from it. A partial one keeps what it gives, and only the consumers it leaves out are constructed.
  • With time_limit_seconds=0 the solve stops at once, so a complete solution comes back as it went in. Use the time limit for this: step_count_limit = 0 means no limit.
  • facility is a position in facilities, so a solution that lists different facilities, or the same ones in another order, raises ValueError. So do unknown ids and out-of-range values.

Exceptions and limits

  • Distance is a straight-line approximation of 1 degree = 111 km, not a road distance.
  • Each consumer is served by exactly one facility. Demand is not split.