-
Notifications
You must be signed in to change notification settings - Fork 0
Code walkthrough settings
In one sentence: settings.jl is the 317-line file every Julia script includes first; it holds no optimisation logic but decides where the Arrow files are, which areas run, how a trip and a location are priced, which regions are out of scope, and how an area's OD matrix becomes the in-memory CountryData struct — so most "why did my run do that?" questions end here.
Where this sits: GeoDMS exports
<area>_od/_i/_j.arrow(GeoDMS OD matrix and choice set) → settings.jl reads them and prices them →lp_run.jlbuilds the LP (Code walkthrough lp_run) →lambda_sweep_simplex.jlsweeps λ (Code walkthrough lambda_sweep_simplex) → rounding, scenarios, deck. Everything here is as of September 2026, commit e25416b.
settings.jl came out of a refactor on 13 May 2026 (8e87523): lp.jl and greedy.jl each carried their own country list, paths, cost function and Arrow loader, and overwrote each other's traveltime.arrow. Eleven days later (40a3d55) every value became an environment variable with a default, so a batch runner can launch the same script per area and per travel-cost function without editing code — which is how the PowerShell orchestrators still drive julia lambda_sweep_simplex.jl (Running a sweep end to end).
Seven scripts include it, and they are not equals. lambda_sweep_simplex.jl is the production sweep; lambda_sweep.jl is its superseded predecessor; lp.jl and greedy.jl are runners from the school era — the project's first phase, which optimised primary-school locations with the same LP before the pharmacy question arrived in spring 2026 (Glossary); cap_scenario.jl with s1_cap.jl/s2_cap.jl is the dropped catchment-cap ladder (Alternatives not pursued).
graph LR
S[settings.jl] --> L[lp_run.jl]
S --> G[greedy.jl<br/>school era]
L ==> P[lambda_sweep_simplex.jl<br/>PRODUCTION]
L --> O[lambda_sweep.jl<br/>superseded]
L --> Q[lp.jl<br/>school era]
L --> C[cap_scenario.jl<br/>cap ladder] --> D[s1_cap.jl / s2_cap.jl]
Every script sees the same constants, but not every script uses the same loader — the trap under "The four loaders" below. Sections follow file order.
The data root is hard-coded: LOCAL_DATA_DIR = C:\LocalData, PROJ_NAME = networkmodel_eu, so LOCAL_DATA_PROJ_DIR = C:\LocalData\networkmodel_eu — the GeoDMS %localDataProjDir% that ClientExport/FacilityExport write into (cfg/main/Analyses.dms around lines 416 and 437). Not env-overridable; on a machine whose GeoDMS LocalDataDir differs you edit line 3.
ANALYSIS (default ExistingPharmacies; also NewPharmacies, ExistingSchools, NewSchools) selects ANALYSIS_DIR = <root>\<ANALYSIS>, which feeds input_path(country, suffix) = <ANALYSIS_DIR>\<country>_<suffix>.arrow (suffix od, i, j). It matters only for the school-era runners (lp.jl, greedy.jl, load_country): the sweep hard-codes EXISTING_PATH and NEW_PATH (lambda_sweep_simplex.jl lines 10–11) because it needs the Existing side (baseline) and the New side (candidates) in one process.
COUNTRIES (default France Italy Netherlands Sweden) is a space-separated list of study areas — the names GeoDMS understands as STUDY_AREA: country names and NUTS-1 codes (FRI, ITG, PL8, SE2, …); line 9 lists the 32-country full set. Why those four is not recorded; they are also DEFAULT_COUNTRIES in run_pharmacy_pipeline.bat line 91, so probably the pilot set (inference). The orchestrators pass one area per Julia process so each gets its own log.
LOCATION_SELECTION_FACTOR (lines 20–24, default 1) keeps every K-th candidate for very large areas — candidate subsampling; mechanism and the PROTECT_BASELINE guard under load_country step 2.
CLIENT_WEIGHT (default total_pop) names the column of <area>_i.arrow that multiplies every client's travel cost — "how many residents make this trip". client_weight_col(loc) returns it, or falls back to pop with a warning.
Why two columns: in GeoDMS, pop = float32(Org/<ModelParameters/Client>) and total_pop = float32(Org/population) (Analyses.dms lines 423–424). When total_pop was added (51157a0, 26 May 2026) the configured Client was still pop_primaryschool — the school era's 6–12-year-olds — and the pharmacy LP needed whole-population weights without regenerating every OD. Since Client := 'population' (ModelParameters.dms line 35) the columns coincide on pharmacy exports.
FACILITY_MIN_COSTS = 100000 is the placeholder fixed cost of one location in euro. It enters the sweep in exactly one way — λ = w · FACILITY_MIN_COSTS (lp_run.jl around lines 447 and 464), w being the swept knob — and it labels the fac_€ log column (lambda_sweep_simplex.jl line 222). Changing it relabels every λ in euros and shifts which frontier points the fixed w-grid visits, but creates or destroys no frontier point, so frontier, S1/S2 and rankings are invariant to it; why the value is a placeholder and what depends on it is on The lambda sweep and the Pareto frontier. The toy load under load_country shows the mechanism: at λ = 1 000 the open sets {1} and {0, 1} tie at 3 600; at λ = 100 the full set {0, 1, 2} wins at 1 550 — w decides how many locations the travel savings can pay for.
FACILITY_CLIENT_COSTS = 3333, FACILITY_FUNC (default LINEAR) and facility_cost(q) implement the per-location cost families a + b·q (LINEAR), max(a, b·q) (FLOOR) and the concave power law 51712·q^0.465 (CONCAVE) of issue #38, q being a location's load. No live script calls facility_cost (the one in greedy-merge.jl is an unrelated local): with every served resident assigned to exactly one open pharmacy the b·q term is a constant, so a linear location cost reduces to λ·(number open) (doc/todo.md item 12; The facility location model explained). The concave branch is the school era's rejected variant (Alternatives not pursued). Treat both as dead code until a real pharmacy cost is calibrated.
The six FUNC_* codes and the FUNC_NAMES dictionary map a string to an integer; parse_func(envname, default) does FUNC_NAMES[get(ENV, envname, default)], so a typo in TRAVEL_FUNC fails immediately with a KeyError — good. travel_func_name (line 64) maps back to the canonical string and becomes the <TRAVEL_FUNC> folder segment of every sweep output.
The pitfall: TRAVEL_FUNC defaults to QUADRATIC (line 44). No pharmacy result uses it: a bare julia lambda_sweep_simplex.jl runs a quadratic sweep with BIG = 780 under …\lambda_sweep\QUADRATIC\…, which the deck scripts cannot parse. The default is a school-era leftover — Chris's 0.05·t² + 0.5·t (issue #27, c01b8b3/26e39c7) was the working function then; why it was never changed to LINEAR is not recorded (inertia — inference). Every production runner sets TRAVEL_FUNC; run_resweep_batch.ps1 accepts only LINEAR, LOGISTIC, QUADRATIC, PIECEWISE.
c(t) (lines 73–89) takes t in minutes — the loaders divide the Arrow t_ij (seconds) by 60 — and returns:
| name | code | c(t) | c(120) = BIG | status |
|---|---|---|---|---|
LINEAR |
1 | t | 120 | production; person-minutes |
QUADRATIC |
2 | 0.05·t² + 0.5·t | 780 | code default; school era; unused for pharmacies |
PIECEWISE |
3 | t (t ≤ 15), 2t (≤ 30), 4t (> 30) | 480 | unused; discontinuous at 15 and 30 min |
LOGISTIC |
4 | 1 / (1 + e^{−(t − m)/s}), m = 25, s = 10 | 1.0 (fixed) | production; dimensionless in (0, 1); c(0) = 0.076 |
FLOOR, CONCAVE
|
5, 6 | — | facility-cost codes; c(t) returns nothing
|
What each curve rewards and ignores, the value tables, and the logistic retune (25/10 replaced 30/15 on 2 July 2026, 7c6768f) are on Travel cost functions. Code-level facts: LOGISTIC_MIDPOINT/LOGISTIC_SCALE (lines 70–71, minutes) are env-overridable so the 30/15 runs stay reproducible; travel_func and the logistic parameters are plain globals, not const, so the environment can override them; and because a logistic trip costs at most 1 against 120 for LINEAR, the sweep's w_max is 0.5 for LOGISTIC against 5.0 for LINEAR (lambda_sweep_simplex.jl lines 304–305).
A stranded client — one with no open pharmacy among its OD rows (Glossary) — must still cost something, otherwise abandoning a remote cluster looks free. big_cost() is that price: 1.0 under LOGISTIC (the curve's supremum; c(120) is already 0.9999), otherwise c(MAX_TRAVELTIME_MIN) with MAX_TRAVELTIME_MIN from BIG_TRAVELTIME_MIN (default 120), so BIG = c(120) = 120 person-minutes per resident under LINEAR. 120 is the GeoDMS OD cutoff max_traveltime_to_facility := 120[min_f] (cfg/main/ModelParameters.dms line 16), so "stranded" is priced as "served at the far edge of the OD"; the two copies of 120 are unlinked, and lowering BIG_TRAVELTIME_MIN below the cutoff would make stranding cheaper than some served trips. Why BIG is a fixed 120 rather than each area's own t_max (4ec47a9, 16 July 2026), and what the cheap logistic stranding does, is on Travel cost functions.
Where it is consumed: the LP objective writes every OD row's coefficient as (c(t_k) − BIG)·pop_k (lp_run.jl around line 420) — every resident starts out charged BIG as if stranded and earns back BIG − c(t) when assigned, which is how the LP may leave a client unserved (soft coverage, The facility location model explained). BIG also enters the LP lower bound travel_relax; the coverage-honest scoring of every rounded set (travel_of, From LP fractions to real pharmacies); the baseline ★ (baseline_metrics, lambda_sweep_simplex.jl line 178, where a stranded client also enters the mean travel time at MAX_TRAVELTIME_MIN minutes); and cap_scenario.jl.
Rounding turns the LP's fractional openness values into a concrete set of p open pharmacies; the three recipes — top-p, greedy, multistart — are explained on From LP fractions to real pharmacies. ROUNDING (default multistart; also topp, greedy) selects which becomes the reported point — all three are always computed and logged; an unknown value silently falls through to top-p (lp_run.jl lines 543–546). Multistart is the default because top-p and greedy are two of its seeds, so its result is never worse than either (comment at lines 52–54, e1c426b). MS_RESTARTS = 10 seeds, MS_ROUNDS = 12 swap rounds per seed and MS_SEED = 20240601 (the random-number seed — a different thing from a starting set) are consumed only by multistart_round (lp_run.jl line 211); why these numbers is not recorded.
Stale comment: lines 48–52 speak of p = round(sum_x) and "largest x_relaxed". In today's lp_run.jl the openness variable is y[j], x[k] the per-OD-row assignment, and the log columns sum_y/frac_y; the comment predates the rename in 4cee238 (24 July 2026).
The rule (issue #49), its rationale — a data gap such as Portugal's Azores and Madeira must not be priced at BIG as if it were policy headroom — and its measured outcome (only PT200 Açores and PT300 Madeira trigger, both 100 % absent) live on Scope rules and data gaps. In one line: if more than EXCLUDE_SHARE (env NUTS_EXCLUDE_SHARE, default 0.5) of a NUTS region's inhabited cells appear in no Existing-side OD row, drop the whole region — residents and candidates — from baseline and sweep alike. What follows is the code reading.
excluded_nuts_regions(country) returns (excluded::Set{String}, level::Int, report):
-
NUTS_EXCLUSION≠"1"→ rule off, return empty (line 130). - Read the Existing side only:
<root>\ExistingPharmacies\<country>_i.arrowand_od.arrow; missing files → empty. The path derives fromLOCAL_DATA_PROJ_DIR, not the sweep'sEXISTING_PATH(a standaloneUndefVarErrorfixed indb7084d). Judging on the existing OD makes the verdict independent of the candidate set, so baseline and sweep exclude the same regions by construction. - No
NUTScolumn in the client table → warn and return empty (old exports). - Mark
inod[client_rel + 1] = truefor every client in the existing OD (client_rel0-based, Julia 1-based). - For
k in (5, 4, 3)— the NUTS3, NUTS2 and NUTS1 prefix lengths — group cells bynuts_prefix(code, k), skipping empty codes. If no group exists at this k, fall to the next coarser level; otherwise judge here and return at the end of this iteration whether or not anything was excluded. The returnedlevelis k − 2;in_excluded_regionconverts it back (line 186). - Per group,
share = count(!, inod[rs]) / length(rs)— the fraction of the region's cells reaching no existing pharmacy within 120 minutes;share > EXCLUDE_SHARE(strict) reports the region with its summed weight and cell count. - If anything is reported: write
<repo>\scratch\excluded_regions_<country>.csv(country,nuts_level,nuts_code,share_not_in_od,population,cells) and log one@infoline.
in_excluded_region(tbl, excluded, level) turns the verdict into a Boolean mask over any table with a NUTS column (both _i.arrow and _j.arrow carry one since 872b388; upper case because the DMS token registry is case-insensitive and had already seen that spelling). The mask bites only in lambda_sweep_simplex.jl load_from: excluded candidate ids are dropped before the OD is masked (lines 85–90), and excluded clients get weight 0 (lines 121–125) — their rows stay so indices remain comparable, but they contribute nothing to cost, BIG or the reported population, and baseline_metrics skips zero-weight cells so they are not counted as unreachable either (Code walkthrough lambda_sweep_simplex).
CountryData is one area's OD matrix in memory. Its central convention: N counts OD rows (client–candidate pairs), not clients, because the LP has one assignment variable x[k] per row. Ids from Arrow are 0-based, Julia vectors 1-based; hence the +1 wherever a client id indexes a population vector (units and id conventions in full on Log lines and output files).
| field | type | holds | units / notes |
|---|---|---|---|
N |
Int |
number of OD rows kept | after subsampling / exclusion masks |
M |
Int |
number of candidates kept | = length(facilities)
|
facilities |
Vector{Int} |
the surviving candidate ids | 0-based, per-file local (Existing and New files number cells independently) |
wpop |
Vector{Float32} |
the client weight repeated on each of its rows | per OD row k = 1..N; residents (CLIENT_WEIGHT) |
clients_col |
Vector{Int} |
client_rel of each row |
0-based client id |
t_ij_col |
Vector{Float32} |
road travel time of each row | minutes (Arrow seconds ÷ 60) |
facilities_col |
Vector{Int} |
facility_rel of each row |
0-based candidate id |
locations |
Dict{Int, Vector{Int}} |
client id → its OD rows: its choice set, every candidate within the road time to its 5th-nearest existing pharmacy, at most 120 min (GeoDMS OD matrix and choice set) | rows in file order |
facility_rows |
Dict{Int, Vector{Int}} |
candidate id → the rows it appears in | empty vector for a candidate no client can reach |
client_pop |
Dict{Int, Float32} |
client id → weight | one entry per client with at least one row |
nearest_facility |
Dict{Int, Int} |
client id → candidate with the smallest c(t) among its rows |
read by greedy.jl (run_scenario, around line 277); the sweep does not read it |
All four loaders build this struct identically, so everything downstream (lp_run.jl, the sweep, the cap ladder) is loader-agnostic.
load_country(country; apply_factor=true) reads the three tables through input_path (so it is bound to ANALYSIS_DIR) and:
-
Candidate ids.
facilities_all = Int.(fac[:id]). -
Subsampling with baseline protection.
factor = apply_factor ? LOCATION_SELECTION_FACTOR : 1; the keyword lets a caller load the Existing side full-size (apply_factor=false) while striding the New side, because the baseline must never be subsampled. Protection applies when all four hold:factor > 1;PROTECT_BASELINEis exactly"1"(its default — the test is== "1", line 236, so any other value,trueoryesincluded, silently disables it);<root>\ExistingPharmacies\<country>_j.arrowexists;fachas anxcolumn. Then: round both files' coordinates to whole metres (EPSG:3035) so float noise cannot break equality; mark candidates whose(x, y)matches an existing pharmacy cell as baseline (isb) — by coordinates, because ids are per-file local; keep all of those plus everyfactor-th of the rest (rest[1:factor:end]), sort, and logprotected … baseline candidates; kept … of … (factor=…). Otherwise the blind stridefacilities_all[1:factor:end]. -
OD mask. Keep rows whose
facility_relsurvived — with the shortcutfactor == 1 ? trues(…) : …(line 252), which assumes that with factor 1 the candidate set is complete (see "The four loaders"). -
Columns.
clients_col,facilities_col,t_ij_col = t_ij / 60(seconds → minutes),population = client_weight_col(loc); thenN,M,wpop[k] = population[clients_col[k] + 1]. -
locations,facility_rows(empty vector for a candidate in no row),client_pop,nearest_facility(argmin ofc(t)over the client's rows).
Why the guard exists: the blind stride (afef439, 27 May 2026, "for scale-up testing") threw away most of today's pharmacy cells, so the frontier could not reproduce today's network; f167da2 (21 July) added the guard. The FRI numbers, why even a protected stride only gives an upper bound of the full frontier, and which production areas were strided are on Scope rules and data gaps and Branches data and environment.
A toy stride: candidates 0..9, factor 3, cells 2 and 5 hold existing pharmacies. Protected: rest = [0,1,3,4,6,7,8,9], rest[1:3:end] = [0,4,8], kept [0,2,4,5,8]. Blind: positions 1, 4, 7, 10 → [0,3,6,9], both pharmacy cells gone.
A toy load (factor 1): clients 0, 1, 2 with total_pop = [100, 50, 10], candidates 0, 1, 2, five OD rows (client, facility, seconds) = (0,0,300), (0,1,900), (1,1,600), (2,1,3600), (2,2,1500). Then N = 5, M = 3, clients_col = [0,0,1,2,2], facilities_col = [0,1,1,1,2], t_ij_col = [5,15,10,60,25], wpop = [100,100,50,10,10], locations = {0→[1,2], 1→[3], 2→[4,5]}, facility_rows = {0→[1], 1→[2,3,4], 2→[5]}, client_pop = {0→100, 1→50, 2→10}, nearest_facility = {0→0, 1→1, 2→2}. Pricing open sets under LINEAR: {1} = 100·15 + 50·10 + 10·60 = 2 600 person-minutes; {0, 1} = 500 + 500 + 600 = 1 600; {0, 1, 2} = 500 + 500 + 250 = 1 250; {0, 2} strands client 1: 500 + 50·120 + 250 = 6 750. Add λ = 1 000 per location (w = 0.01): 3 600, 3 600, 4 250; at λ = 100: 2 700, 1 800, 1 550 — the invariance example from the Prices section; the general tangency picture is on The lambda sweep and the Pareto frontier.
try_load_country(country) (lines 300–317) wraps load_country so a loop over the 32-country list can continue: a SystemError (missing file) or N == 0 prints <country> — skipped (…) and returns nothing; any other exception is rethrown and aborts the loop.
There is not one loader but four near-copies; only one is used in production, and their differences explain three separate bugs.
| loader | file / lines | subsampling | PROTECT_BASELINE |
region exclusion (#49) | OD-mask rule | called by |
|---|---|---|---|---|---|---|
load_country |
settings.jl 222–295 |
yes | yes (f167da2) |
no | shortcut when factor == 1
|
try_load_country → lp.jl (16, 72), greedy.jl (336, 452; line 396 calls load_country directly) — school era, run by hand |
load_from |
lambda_sweep_simplex.jl 71–157 |
yes | yes (f6edc57) |
yes (872b388) |
filter whenever the set shrank by any means (d834a7c) |
analyze_country 252 (Existing side, apply_factor=false), 256 (New side) — the production sweep
|
load_from |
lambda_sweep.jl 60–120 |
yes | yes (f6edc57) |
no | shortcut when factor == 1
|
its own analyze_country (175, 179); no runner calls this superseded driver |
load_dir |
cap_scenario.jl 79–136 |
blind stride | no | no | shortcut when factor == 1
|
analyze_country 417–418 via s1_cap.jl/s2_cap.jl (run_cap_scenarios.bat) |
| bug | when · fix | what happened | still present in |
|---|---|---|---|
| guard on the wrong loader | 21 Jul · f6edc57
|
f167da2 patched PROTECT_BASELINE into load_country, which the sweep never calls, so it never fired (FRI still loaded M = 5 714, not 6 765); ported into both load_from copies the same day (Scope rules and data gaps) |
load_dir |
KeyError(0) while building y[j]
|
2 Sep · d834a7c
|
the factor == 1 shortcut assumed a complete candidate set, but region exclusion also removes candidates, so OD rows referenced a facility absent from the LP's index set; load_from now sets reduced whenever factor != 1 or the candidate list shrank (Code walkthrough lambda_sweep_simplex) |
the other three — harmless only because they never exclude |
| cap ladder learned neither rule | open |
load_dir strides blindly and keeps the Azores (Alternatives not pursued) |
load_dir |
Anything loaded through load_country today still includes the Azores and Madeira; whether the three non-production loaders should be retired in favour of load_from is open.
output_path(country, script, assignment, kind) creates and returns <ANALYSIS_DIR>\<country>\<script>\<assignment>\<kind>.arrow with script ∈ {lp, greedy}, assignment ∈ {nearest, central}, kind ∈ {assignment, traveltime}; only lp.jl (lines 105, 110) and greedy.jl (432, 438) call it. The layout is what the GeoDMS reader ReadResults_T expects (Analyses.dms around lines 179 and 196).
The sweep does not use output_path; its own sweep_dir (lambda_sweep_simplex.jl lines 23–27) keys folders by w and by travel_func_name so LINEAR and LOGISTIC never overwrite each other — layout and columns on Log lines and output files. The only file settings.jl writes itself is scratch\excluded_regions_<country>.csv.
Open settings.jl once, top to bottom. Pinned permalinks (commit e25416b; line numbers drift, read them as "around"):
Grouped as in the file; consumers outside settings.jl are named above and in the four-loaders table.
| name | default | what it changes | why you would change it | interactions |
|---|---|---|---|---|
LOCAL_DATA_DIR, PROJ_NAME (consts) |
C:\LocalData, networkmodel_eu
|
the data root | another GeoDMS LocalDataDir
|
not env-overridable |
ANALYSIS |
ExistingPharmacies |
ANALYSIS_DIR for input_path/output_path
|
school-era runs | ignored by the sweep |
COUNTRIES |
France Italy Netherlands Sweden |
the study areas looped over | always — one area per process | must match the STUDY_AREA used at export |
CLIENT_WEIGHT |
total_pop |
which _i.arrow column weights clients |
school runs (pop = the cohort) |
falls back to pop with a warning |
FACILITY_MIN_COSTS |
100000 |
λ = w·this; € labels; fac_€ column |
when a real fixed cost is calibrated | frontier as a set unchanged |
FACILITY_CLIENT_COSTS, FACILITY_FUNC
|
3333, LINEAR
|
nothing live (unused facility_cost) |
— |
greedy.jl has its own use_power_law flag |
LOCATION_SELECTION_FACTOR |
1 |
keep every K-th non-baseline candidate and its OD rows | very large areas only |
apply_factor=false bypasses it; upper-bound frontier |
PROTECT_BASELINE |
1 |
with factor > 1, keep every candidate coinciding with an existing pharmacy cell |
0 only to reproduce the pre-July blind stride |
must be exactly "1" — any other value disables it; needs _j.arrow x,y and the ExistingPharmacies file; not in load_dir
|
TRAVEL_FUNC |
QUADRATIC |
c(t), big_cost(), output folder, sweep w_max
|
every production run: LINEAR or LOGISTIC
|
forgetting it gives a quadratic sweep with BIG 780 |
LOGISTIC_MIDPOINT, LOGISTIC_SCALE
|
25, 10 (minutes) |
position and steepness of the S-curve | sensitivity tests; reproduce the 30/15 results | LOGISTIC only; BIG stays 1.0 |
BIG_TRAVELTIME_MIN |
120 |
BIG for non-logistic functions; minutes a stranded client adds to the baseline mean time | experiments only | keep equal to GeoDMS max_traveltime_to_facility
|
ROUNDING |
multistart |
which rounded set is selected (all three always computed) | comparisons | unknown value → top-p silently |
MS_RESTARTS, MS_ROUNDS, MS_SEED
|
10, 12, 20240601
|
multistart effort and determinism | quality vs time | multistart only |
NUTS_EXCLUSION |
1 |
rule on/off |
0 to see pre-exclusion Portugal |
off → Azores/Madeira priced at BIG again |
NUTS_EXCLUDE_SHARE (const EXCLUDE_SHARE) |
0.5 |
threshold on the share of a region's cells absent from the Existing-side OD | sensitivity; no borderline case today | strict >; needs re-exported files with NUTS
|
Knobs Maarten's Pharmacy service access page lists alongside these but that live elsewhere: SWEEP_WMAX, SWEEP_MULTS (lambda_sweep_simplex.jl), SOLVER, LP_TIME_LIMIT (lp_run.jl). Two more outside settings.jl are not on that list: SWEEP_STOP_AFTER_REFINE (lambda_sweep_simplex.jl) and MAX_PARALLEL (lambda_sweep.jl only; the production driver is sequential).
-
Stale
scratch\excluded_regions_<country>.csv. Written only when something is excluded and never deleted, so a file from an earlier export can outlive a later clean verdict;scratch\must exist (theopendoes notmkpath). -
NUTS level selection returns at the first level with any group; cells with an empty NUTS code are in no group and are never excluded nor counted;
nuts_prefixon a code shorter than k returns"", so a table carrying only 4-character codes is invisible at the NUTS3 pass. -
Both sides must be re-exported with the
NUTScolumn; without it the rule warns and does nothing. -
PROTECT_BASELINEis a string compare against"1";PROTECT_BASELINE=trueturns protection off without a warning. -
c(t)returnsnothingforFLOOR/CONCAVEinTRAVEL_FUNC; the failure is aMethodErrorin the loader (nearest_facility) or inbaseline_metrics(big_cost()), not at start-up. -
try_load_countrycatches onlySystemError; a corrupt Arrow file aborts the whole loop. BIG_TRAVELTIME_MINand GeoDMSmax_traveltime_to_facilityare two unlinked copies of 120.-
The GeoDMS read-back
SweepResults/baseline(Analyses.dmsline 381) still points at aQUADRATICfolder from the 2 June sweep — stale wiring, not a current result. - A strided candidate set is an upper bound even with protection; which production frontiers were strided is unsettled — Branches data and environment.
- Open: the origin of
EXCLUDE_SHARE = 0.5, of the first logistic 30/15, ofMS_SEED = 20240601and of the four default countries; BN's proposal to rescale the logistic (doc/deck_changes_from_review.md§6, a DECIDE item — Travel cost functions); whetherFACILITY_MIN_COSTSwill be calibrated (andfacility_costwired in or deleted); whether the three non-production loaders should be retired.
- Travel cost functions · The facility location model explained · The lambda sweep and the Pareto frontier
- Scope rules and data gaps · From LP fractions to real pharmacies · Scenarios S1 S2 S3
- Code walkthrough lp_run · Code walkthrough lambda_sweep_simplex · Running a sweep end to end · Log lines and output files · Deck and charts pipeline
- GeoDMS OD matrix and choice set · Branches data and environment · Alternatives not pursued · Decision log
- Maarten's formal pages: Lambda sweep method · Pharmacy service access
- Glossary — BIG, λ / w, choice set, stranded, soft coverage, coverage-honest, rounding, stride, region exclusion, CountryData
Start here
Understanding the method
- The facility location model explained
- Travel cost functions
- The lambda sweep and the Pareto frontier
- From LP fractions to real pharmacies
- Scenarios S1 S2 S3
- Scope rules and data gaps
- Metrics aggregation and ranking
- Background theory
Working with the code
- GeoDMS OD matrix and choice set
- Code walkthrough settings
- Code walkthrough lp_run
- Code walkthrough lambda_sweep_simplex
- Running a sweep end to end
- Log lines and output files
- Deck and charts pipeline
- Installation of Julia
Reference
- Lambda sweep method
- Pharmacy service access
- Pharmacy results September 2026
- Decision log
- Alternatives not pursued
- FAQ
- Glossary
- Service allocation procedure
External