Skip to content

Code walkthrough settings

Jip Claassens edited this page Sep 15, 2026 · 1 revision

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.jl builds the LP (Code walkthrough lp_run) → lambda_sweep_simplex.jl sweeps λ (Code walkthrough lambda_sweep_simplex) → rounding, scenarios, deck. Everything here is as of September 2026, commit e25416b.

What the file is for, and why it is one file

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]
Loading

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.

Paths and study areas (lines 3–10, 20–24)

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 (lines 12–15, 191–199)

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.

Prices: FACILITY_MIN_COSTS, FACILITY_CLIENT_COSTS, facility_cost (lines 17–18, 99–107)

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 travel-cost family and parse_func (lines 26–89)

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).

big_cost() — the stranded-client price (lines 91–97)

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 knobs (lines 47–60)

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 region-exclusion rule: excluded_nuts_regions and in_excluded_region (lines 112–189)

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):

  1. NUTS_EXCLUSION ≠ "1" → rule off, return empty (line 130).
  2. Read the Existing side only: <root>\ExistingPharmacies\<country>_i.arrow and _od.arrow; missing files → empty. The path derives from LOCAL_DATA_PROJ_DIR, not the sweep's EXISTING_PATH (a standalone UndefVarError fixed in db7084d). Judging on the existing OD makes the verdict independent of the candidate set, so baseline and sweep exclude the same regions by construction.
  3. No NUTS column in the client table → warn and return empty (old exports).
  4. Mark inod[client_rel + 1] = true for every client in the existing OD (client_rel 0-based, Julia 1-based).
  5. For k in (5, 4, 3) — the NUTS3, NUTS2 and NUTS1 prefix lengths — group cells by nuts_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 returned level is k − 2; in_excluded_region converts it back (line 186).
  6. 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.
  7. 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 @info line.

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 field by field (lines 208–220)

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 step by step (lines 222–295), and try_load_country

load_country(country; apply_factor=true) reads the three tables through input_path (so it is bound to ANALYSIS_DIR) and:

  1. Candidate ids. facilities_all = Int.(fac[:id]).
  2. 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_BASELINE is exactly "1" (its default — the test is == "1", line 236, so any other value, true or yes included, silently disables it); <root>\ExistingPharmacies\<country>_j.arrow exists; fac has an x column. 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 every factor-th of the rest (rest[1:factor:end]), sort, and log protected … baseline candidates; kept … of … (factor=…). Otherwise the blind stride facilities_all[1:factor:end].
  3. OD mask. Keep rows whose facility_rel survived — with the shortcut factor == 1 ? trues(…) : … (line 252), which assumes that with factor 1 the candidate set is complete (see "The four loaders").
  4. Columns. clients_col, facilities_col, t_ij_col = t_ij / 60 (seconds → minutes), population = client_weight_col(loc); then N, M, wpop[k] = population[clients_col[k] + 1].
  5. locations, facility_rows (empty vector for a candidate in no row), client_pop, nearest_facility (argmin of c(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.

The four loaders

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 and the folder layout (lines 201–206)

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.

In the code

Open settings.jl once, top to bottom. Pinned permalinks (commit e25416b; line numbers drift, read them as "around"):

what permalink
paths, ANALYSIS, COUNTRIES, CLIENT_WEIGHT, prices, LOCATION_SELECTION_FACTOR https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L3-L24
FUNC_*, parse_func, ROUNDING, MS_*, logistic parameters https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L26-L71
c(t), big_cost(), facility_cost(q) (unused), input_path https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L73-L110
EXCLUDE_SHARE, nuts_prefix, excluded_nuts_regions, in_excluded_region https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L123-L189
client_weight_col, output_path, CountryData https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L191-L220
load_country, try_load_country https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/settings.jl#L222-L317
the production loader load_from https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/lambda_sweep_simplex.jl#L71-L157
legacy load_from; cap-ladder load_dir https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/lambda_sweep.jl#L60 · https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/cap_scenario.jl#L79
where λ is formed; where BIG enters the objective https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/lp_run.jl#L464 · https://github.com/ObjectVision/NetworkModel_EU/blob/e25416b/lp_run.jl#L419-L420

Knobs

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).

Pitfalls and open questions

  1. 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 (the open does not mkpath).
  2. 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_prefix on a code shorter than k returns "", so a table carrying only 4-character codes is invisible at the NUTS3 pass.
  3. Both sides must be re-exported with the NUTS column; without it the rule warns and does nothing.
  4. PROTECT_BASELINE is a string compare against "1"; PROTECT_BASELINE=true turns protection off without a warning.
  5. c(t) returns nothing for FLOOR/CONCAVE in TRAVEL_FUNC; the failure is a MethodError in the loader (nearest_facility) or in baseline_metrics (big_cost()), not at start-up.
  6. try_load_country catches only SystemError; a corrupt Arrow file aborts the whole loop.
  7. BIG_TRAVELTIME_MIN and GeoDMS max_traveltime_to_facility are two unlinked copies of 120.
  8. The GeoDMS read-back SweepResults/baseline (Analyses.dms line 381) still points at a QUADRATIC folder from the 2 June sweep — stale wiring, not a current result.
  9. A strided candidate set is an upper bound even with protection; which production frontiers were strided is unsettled — Branches data and environment.
  10. Open: the origin of EXCLUDE_SHARE = 0.5, of the first logistic 30/15, of MS_SEED = 20240601 and 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); whether FACILITY_MIN_COSTS will be calibrated (and facility_cost wired in or deleted); whether the three non-production loaders should be retired.

See also

Clone this wiki locally