solve

site.SiteProblem.solve(
    p,
    objectives='p_median',
    weights=None,
    rank_on=None,
    capacitated=False,
    search_strategy='brute-force',
    brute_force_ignore_limit=False,
    show_progress=True,
    brute_force_keep_best_n=None,
    brute_force_keep_worst_n=None,
    n_jobs=1,
    max_value_cutoff=None,
    threshold_for_coverage=None,
    grasp_num_solutions=5,
    grasp_alpha=0.2,
    grasp_max_attempts='default',
    grasp_min_sites_different=1,
    grasp_local_search_chance=0.8,
    grasp_max_swap_count_local_search=10,
    random_seed=42,
    full_secondary_metrics=False,
    baseline=None,
    meaningful_change_threshold=0.0,
    beyond_thresholds=None,
    unreachable_cost=None,
)

Solve the site location problem using the specified objective and strategy.

This method validates the problem configuration, handles automatic setup of missing demand or site data, and dispatches the optimization task to the appropriate internal solver.

Parameters

Name Type Description Default
p int The number of facilities to be located. required
objectives str or list of str The optimization objective(s). Currently, only single-objective optimization is supported; if a list is provided, only the first element is used. Supported: “p_median”, “p_center”, “mclp”, etc. The objective sets both which metric is ranked by default and which constraints apply (“mclp” requires threshold_for_coverage; the “hybrid_*” models require max_value_cutoff). rank_on below overrides only the first of those, so the two compose: objectives="hybrid_p_median", max_value_cutoff=60, rank_on="inter_tertile_ratio" means “cap the worst journey at 60 minutes, then pick the most equitable option that qualifies”. Pass “custom” for no model constraints at all – it requires rank_on and rejects max_value_cutoff. Use it when you’d rather the returned SiteSolutionSet not name a textbook model the run didn’t actually perform. "p_median"
weights dict Only used with p_median. A dictionary of weights. Recognized keys are “demand”, “equity”, “cost” (requires add_sites(cost_col=...) to have been called), and any label registered via add_additional_data(). Not supported for “p_center”, which ranks solely by worst-case travel time. These are row-level weights over demand regions: they change how weighted_average is computed, not which metric is ranked. To rank on a different metric, use rank_on, not weights. (“cost” is the exception – a per-combination value blended in after the fact, which is why it forces brute-force pruning to materialise every combination first.) None
rank_on str or lokigi.multiobjective.Metric Rank and prune on this metric instead of the one implied by objectives. Any scalar column solve() computes is valid – "inter_tertile_ratio", "90th_percentile", "demand_beyond_threshold_45", "proportion_demand_improved", a "<metric>__<label>" secondary-matrix column, and so on. Call describe_solution_columns() on any previous result to list them. Unlike re-sorting a finished SiteSolutionSet, this drives the search itself, so it also decides which combinations survive brute_force_keep_best_n or GRASP’s pool – re-sorting can only reorder candidates that were already kept for a different metric. A bare string takes its direction from the usual convention (coverage/improvement metrics are higher-is-better, travel costs lower-is-better). Pass a Metric to state it explicitly, which is the only way to express a metric that is best at neither extreme:: from lokigi.multiobjective import Metric problem.solve( p=3, rank_on=Metric( “inter_tertile_ratio”, direction=“closest_to_target”, target=1.0, # 1.0 = equal travel across equity bands ), ) Validated against a representative combination before the search starts, so naming a column that doesn’t exist, holds one value per equity band, or is NaN because its precondition wasn’t met (e.g. inter_tertile_ratio without add_equity_data()) fails immediately rather than after a full solve. With search_strategy="greedy", prefer ranking on a metric that responds smoothly as sites are added one at a time – weighted_average/unweighted_average (sums that improve with almost every added site) or a coverage proportion behave well. A metric defined by a single worst-case region (max, max_increase) can plateau across many candidate sites at each greedy step – e.g. on a 105-combination closure problem, max_increase took only 14 distinct values – so greedy has little gradient to follow and effectively breaks ties arbitrarily among sites that look identical at that step. This is worse the larger the candidate pool relative to p, since more candidates are competing for the same few distinct scores. search_strategy="brute-force" or "grasp" don’t have this problem, since brute-force checks every combination exactly and GRASP’s diversity mechanism doesn’t rely on a smooth per-step gradient the way greedy’s single best-next-site choice does. None
capacitated bool Whether to enforce site capacity constraints. Note: Currently not implemented. False
search_strategy (brute - force, greedy, grasp) The algorithm used to find the solution: - “brute-force”: Exhaustively checks all combinations (if p is small). - “greedy”: Iteratively adds the best performing site. - “grasp”: Greedy Randomized Adaptive Search Procedure. "brute-force"
brute_force_ignore_limit bool (Brute Force only) If True, allows brute-force searching even if the number of combinations is extremely high. False
show_progress bool If True, displays a progress bar during the optimization process. True
brute_force_keep_best_n (Brute Force only) The number of top or bottom results to retain during a brute-force search. Normally this prunes combinations on the fly to bound memory use. If weights includes a positive “cost” weight, that streaming prune is skipped: every combination is evaluated and held in memory so cost can be blended in over the full batch before pruning to N, otherwise a combination that only looks good once cost is considered could be discarded before cost is ever factored in. A UserWarning is raised when this fallback is triggered. None
n_jobs int (Brute Force only) Number of worker processes to evaluate combinations in parallel. 1 (the default) evaluates every combination in the current process and is always byte-for-byte identical to prior (pre-parallel) behaviour, including which combination brute_force_keep_best_n / brute_force_keep_worst_n keeps on an exact score tie. -1 uses all available CPU cores. With n_jobs != 1, results are always correctly ranked and bounded to the requested count, but if more combinations tie exactly on score than keep_best_n / keep_worst_n allows, which specific tied combination is kept can differ from a serial run (their scores are still identical either way). Exact ties are rare with real-valued travel costs. The first call with a given n_jobs value in a process (or any call after switching to a different n_jobs value) pays a one-time worker-pool startup cost – each worker process has to import pandas/numpy/etc. from scratch, which on Windows can take several seconds regardless of workload size. Subsequent calls with the same n_jobs reuse the already-running pool and are fast. For small combination counts that finish in a second or two serially, that startup cost can make a single one-off parallel call look slower than n_jobs=1 – this is a fixed process-spawn cost, not a sign that parallelism itself is ineffective (repeated calls, or larger workloads, amortize it away). 1
max_value_cutoff float The maximum allowable travel cost. Only applicable for hybrid objective models. All search strategies honour it: brute-force discards every combination whose worst-case travel exceeds it, greedy applies it when choosing the final site (raising a ValueError if no feasible completion exists), and GRASP rejects candidate solutions that violate it. None
threshold_for_coverage float The distance or time threshold. Used as a hard filter for MCLP objectives or as a scoring metric for others. Coverage is measured as the proportion of demand within the threshold, weighted by the demand registered via add_demand() (all regions weigh equally if it was never called). The mclp objective therefore maximises covered demand, matching the textbook Maximal Covering Location Problem. The unweighted share of regions is still reported, as proportion_regions_within_coverage_threshold. None
grasp_num_solutions int (GRASP only) The number of high-quality solutions to generate. 5
grasp_alpha float (GRASP only) The selection restriction parameter (0 is fully greedy, 1 is fully random). 0.2
grasp_max_attempts int or default (GRASP only) Maximum iterations to find a valid solution. "default"
grasp_min_sites_different int (GRASP only) Minimum number of sites that must differ between generated solutions. Useful for generating a more diverse solution pool, though you may need to increase the max_attempts at the same time. 1
grasp_local_search_chance float (GRASP only) The probability (0.0 to 1.0) of performing a local search to improve a found solution. 0.8
grasp_max_swap_count_local_search int (GRASP only) Maximum number of site swaps allowed during the local search phase. 10
random_seed int (GRASP only) Seed for reproducibility in randomized strategies like GRASP. 42
full_secondary_metrics bool If False (the default), each registered secondary travel matrix (see add_secondary_travel_matrix()) contributes only its core five metrics plus the float-valued equity aggregations to solution_df. If True, every registered secondary matrix also contributes its dict-valued equity breakdowns (e.g. weighted_by_equity_group__<label>) and description strings, matching what the primary matrix already always returns unsuffixed. This has no effect if no secondary matrices are registered, and costs nothing extra to compute – the values are already computed either way, this only controls which of them are included in the returned table. False
baseline None, True, or SiteSolutionSet Compares every solution against a baseline “do-nothing” network, adding demand_improved/demand_worsened/ demand_unchanged, regions_improved/regions_worsened/ regions_unchanged, mean_reduction_among_improved, mean_increase_among_worsened, max_reduction and max_increase to every row of solution_df – how many people’s journey actually changed relative to the baseline, and by how much, rather than only the region-wide weighted_average shift (which dilutes a large local effect across everyone unaffected by it). See EvaluatedCombination.return_solution_metrics’s point 7, and SolutionComparator.population_impact_summary() for the equivalent baseline-vs-candidate comparison outside solve(). - None (the default): off. solution_df’s column set is byte-for-byte identical to before this parameter existed. - True: build the baseline from the sites flagged via add_sites(required_sites_col=...), inheriting this call’s objectives/weights/threshold_for_coverage (see evaluate_baseline()). Raises ValueError if no required_sites_col is configured. - A SiteSolutionSet containing exactly one solution (typically from evaluate_baseline()): used directly, so a baseline built with different objective/weights/threshold settings than this solve() call can be supplied explicitly. The baseline itself is evaluated once per solve() call, not once per enumerated combination – negligible added cost regardless of search strategy. None
meaningful_change_threshold float Only used when baseline is given. A region’s travel cost must move by strictly more than max(meaningful_change_threshold, 1e-9) to count as improved or worsened; anything smaller (including floating-point noise at the default 0.0) is unchanged. 0.0
beyond_thresholds float or sequence of float One or more “left behind” travel-cost thresholds, added to every row of solution_df as demand_beyond_threshold_<t> / regions_beyond_threshold_<t> – how many people/regions have a travel cost beyond t, for each t. Distinct from threshold_for_coverage: “covered” (good) and “beyond” (bad) cross the threshold in opposite directions, and this parameter accepts more than one value at once (threshold_for_coverage does not). None (the default): off, solution_df’s column set is unchanged. See EvaluatedCombination.return_solution_metrics’s point 8. None
unreachable_cost float Required whenever the primary travel matrix was registered with add_travel_matrix(allow_missing=True) and actually contains a missing (NaN) travel cost, for every objective except "mclp" – see the NotImplementedError this raises when it’s needed but missing for the full explanation. A finite cost substituted for every unreachable pair, used ONLY to rank/prune combinations during search (and to enforce max_value_cutoff) – never in solution_df’s reported weighted_average/unweighted_average/90th_percentile/ max, and never in any plot, both of which stay honest, reachable-only figures throughout. The substituted view is still available for inspection as weighted_average_for_ranking/unweighted_average_for_ranking/ max_for_ranking. Choose a value clearly worse than any real travel cost you’d consider acceptable (e.g. several times your longest reasonable journey) – too small a value under- penalises stranding demand relative to a genuinely long but reachable journey; the two are not the same failure. "mclp" never needs this: its coverage-proportion ranking already treats an unreachable pair as “not covered” – correctly bad, with no equivalent silent-reward failure mode – so it accepts a matrix with missing values regardless of whether unreachable_cost is set. None

Returns

Name Type Description
SiteSolutionSet An object containing the optimal sites, objective score, and detailed assignment data for each provided solution.

Raises

Name Type Description
ValueError If capacitated is True, if the travel matrix is missing, if an unsupported objective/strategy is provided, or if max_value_cutoff is used with an incompatible objective.

Raises

Name Type Description
UserWarning If multi-objective lists are provided (only the first is taken). If demand or site data is missing and must be auto-generated.

Notes

If demand_data or candidate_sites have not been explicitly added prior to calling .solve(), the method will automatically initialize them based on the travel matrix.

Back to top