Skip to content

Commit 2d8e914

Browse files
committed
explorer: existing-state panel, per-run reproduction with downloadable parameters and data
Every scenario now opens on an Existing (before growth) panel, so the before-picture anchors the comparisons and Baseline reads unambiguously as a run. Each run panel gains a Reproduce section: numbered steps, the exact settings used (collapsible), a downloadable params.json in the plugin's schema that loads straight into the dialog, and a per-scenario ZIP of extents, layers and presets (also linked to GitHub). The gallery script emits the merged full-resolution parameters per preset and builds the ZIPs.
1 parent b8e0aed commit 2d8e914

59 files changed

Lines changed: 3135 additions & 68 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/render_scenario_gallery.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _hex(rgb):
5959
# params.json. "clustering" picks the centre-spacing multiple in post-processing.
6060
def presets_for(name: str, params: dict) -> list[dict]:
6161
base = [
62-
{"id": "baseline", "label": "Baseline", "note": "The scenario's own params.json, as shipped."},
62+
{"id": "baseline", "label": "Baseline run", "note": "The scenario's own params.json, as shipped."},
6363
{"id": "walk800", "label": "Longer walk (800 m)",
6464
"note": "Centres serve an 800 m walk; growth reaches further from each centre.",
6565
"overrides": {"centre_walk_m": 800.0, "green_walk_m": max(400.0, params.get("green_walk_m", 400.0))}},
@@ -215,6 +215,43 @@ def render_png(codes, layers, sub, gran, path):
215215
im.save(path, optimize=True)
216216

217217

218+
_SCHEMA_KEYS = (
219+
"crs", "grid_size_m", "max_iterations", "target_population", "build_prob", "dispersal",
220+
"random_seed", "centre_walk_m", "green_walk_m", "optimise_centres", "centre_m2_per_person",
221+
"min_settlement_ha", "min_green_span_m", "densities_km2", "shares", "ensemble", "ensemble_runs",
222+
)
223+
224+
225+
def merged_formal_params(params: dict, preset: dict, entry_name: str) -> dict:
226+
"""The FULL-resolution parameter set for a preset, in the plugin's params schema, so the file
227+
downloads straight into the dialog's Load parameters button. Post-processing choices that are
228+
not dialog parameters (the clustering option) are noted rather than encoded."""
229+
p = {k: params[k] for k in _SCHEMA_KEYS if k in params}
230+
for k, v in preset.get("overrides", {}).items():
231+
if k in ("shares", "densities_km2"):
232+
p[k] = v
233+
elif k != "clustering":
234+
p[k] = v
235+
p["schema"] = "isobenefit-params/1"
236+
p["name"] = f"{entry_name}_{preset['id']}"
237+
note = preset["note"]
238+
if preset.get("overrides", {}).get("clustering"):
239+
note += " In QGIS, both clustering options are always written; open the tightly-clustered output layer."
240+
p["notes"] = note
241+
return p
242+
243+
244+
def existing_panel(sub):
245+
"""The place as downloaded, before any simulated growth."""
246+
plan = np.zeros_like(sub["state"], np.uint8)
247+
plan[sub["state"] == 0] = G.PLAN_GREEN
248+
plan[sub["origin"] == 0] = G.PLAN_GREEN
249+
plan[sub["origin"] == 1] = G.PLAN_EXIST_BUILT
250+
for r, c in sub["seeds"]:
251+
plan[r, c] = G.PLAN_EXIST_CENTRE
252+
return plan
253+
254+
218255
def entry_for(folder: str, extent_key: str, extent, params, layers, title, subtitle):
219256
name = os.path.basename(folder) + ("" if extent_key == "main" else f"_{extent_key}")
220257
span = max(extent.bounds[2] - extent.bounds[0], extent.bounds[3] - extent.bounds[1])
@@ -223,19 +260,40 @@ def entry_for(folder: str, extent_key: str, extent, params, layers, title, subti
223260
print(f"{name}: grid {sub['cols']}x{sub['rows']} at {gran:.0f} m, {len(sub['seeds'])} centre seeds")
224261
p = dict(params, _gran=gran)
225262
presets_out = []
263+
264+
# panel 0: the existing situation, so every comparison starts from the before-picture
265+
rel = f"{name}/existing.png"
266+
render_png(existing_panel(sub), layers, sub, gran, os.path.join(OUT, rel))
267+
presets_out.append({
268+
"id": "existing", "label": "Existing (before growth)",
269+
"note": "The place as downloaded: existing fabric muted, its mixed-use centres magenta. "
270+
"No simulation has run.",
271+
"image": rel, "metrics": None, "settings": None, "params_file": None,
272+
})
273+
226274
for preset in presets_for(name, params):
227275
disp, metrics = run_preset(sub, p, preset)
228276
rel = f"{name}/{preset['id']}.png"
229277
render_png(disp, layers, sub, gran, os.path.join(OUT, rel))
230278
keep = {k: round(float(metrics.get(k, 0)), 3) for k in
231279
("served_coverage", "centre_access", "green_access", "population",
232280
"centre_m2_per_person", "green_m2_per_person", "built_cells")}
281+
formal = merged_formal_params(params, preset, name)
282+
params_rel = f"{name}/{preset['id']}_params.json"
283+
with open(os.path.join(OUT, params_rel), "w", encoding="utf-8") as fh:
284+
json.dump(formal, fh, indent=2, ensure_ascii=False)
233285
presets_out.append({"id": preset["id"], "label": preset["label"], "note": preset["note"],
234-
"overrides": preset.get("overrides", {}), "image": rel, "metrics": keep})
286+
"overrides": preset.get("overrides", {}), "image": rel, "metrics": keep,
287+
"settings": {k: v for k, v in formal.items() if k not in ("schema", "notes")},
288+
"params_file": params_rel})
235289
print(f" {preset['id']}: served {keep['served_coverage']:.0%}, pop {keep['population']:,.0f}")
236290
return {"id": name, "title": title, "subtitle": subtitle,
237291
"grid": f"{sub['cols']}x{sub['rows']} cells at {gran:.0f} m (preview resolution)",
238-
"seed": int(params.get("random_seed", 42)), "presets": presets_out}
292+
"seed": int(params.get("random_seed", 42)),
293+
"folder": os.path.basename(folder),
294+
"github": f"https://github.com/UCL/BSP-isobenefit-qgis-plugin/tree/main/scenarios/{os.path.basename(folder)}",
295+
"zip": f"scenarios/{os.path.basename(folder)}.zip",
296+
"presets": presets_out}
239297

240298

241299
TITLES = {
@@ -268,6 +326,21 @@ def main():
268326
with open(bpath, encoding="utf-8") as fh:
269327
pp = json.load(fh)
270328
entries.append(entry_for(folder, key, extent, pp, layers, title, subtitle))
329+
# one downloadable ZIP per scenario folder (layers + params presets), served by the site
330+
import zipfile
331+
332+
zip_dir = os.path.join(REPO, "website", "public", "scenarios")
333+
os.makedirs(zip_dir, exist_ok=True)
334+
for folder in folders:
335+
base = os.path.basename(folder)
336+
zpath = os.path.join(zip_dir, f"{base}.zip")
337+
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as zf:
338+
for fname in sorted(os.listdir(folder)):
339+
if fname.startswith("_") or fname.endswith(".qgz"):
340+
continue # caches and personal QGIS projects stay out
341+
zf.write(os.path.join(folder, fname), arcname=f"{base}/{fname}")
342+
print(f"{zpath}: {os.path.getsize(zpath) // 1024} kB")
343+
271344
os.makedirs(OUT, exist_ok=True)
272345
with open(os.path.join(OUT, "gallery.json"), "w", encoding="utf-8") as fh:
273346
json.dump({"entries": entries}, fh, indent=1)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "moderate",
8+
"random_seed": 42,
9+
"centre_walk_m": 400,
10+
"green_walk_m": 400,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.2,
22+
"medium": 0.3,
23+
"low": 0.5
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_baseline",
29+
"notes": "The scenario's own params.json, as shipped."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "off",
8+
"random_seed": 42,
9+
"centre_walk_m": 400,
10+
"green_walk_m": 400,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.2,
22+
"medium": 0.3,
23+
"low": 0.5
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_compact",
29+
"notes": "No leapfrogging: one contiguous settlement."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "moderate",
8+
"random_seed": 42,
9+
"centre_walk_m": 400,
10+
"green_walk_m": 400,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.5,
22+
"medium": 0.3,
23+
"low": 0.2
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_denser",
29+
"notes": "Shares shifted one step toward the high tier; the same target houses on less land."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "aggressive",
8+
"random_seed": 42,
9+
"centre_walk_m": 400,
10+
"green_walk_m": 400,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.2,
22+
"medium": 0.3,
23+
"low": 0.5
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_dispersed",
29+
"notes": "Satellites leapfrog readily across the window."
30+
}
196 KB
Loading
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "moderate",
8+
"random_seed": 42,
9+
"centre_walk_m": 400,
10+
"green_walk_m": 400,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.2,
22+
"medium": 0.3,
23+
"low": 0.5
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_tight",
29+
"notes": "The same growth, post-processed to fewer, larger mixed-use centres. In QGIS, both clustering options are always written; open the tightly-clustered output layer."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:27700",
3+
"grid_size_m": 50,
4+
"max_iterations": 400,
5+
"target_population": 12000,
6+
"build_prob": 0.3,
7+
"dispersal": "moderate",
8+
"random_seed": 42,
9+
"centre_walk_m": 800.0,
10+
"green_walk_m": 400.0,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 6000,
17+
"medium": 3000,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.2,
22+
"medium": 0.3,
23+
"low": 0.5
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "cambourne_walk800",
29+
"notes": "Centres serve an 800 m walk; growth reaches further from each centre."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:32614",
3+
"grid_size_m": 30,
4+
"max_iterations": 400,
5+
"target_population": 50000,
6+
"build_prob": 0.3,
7+
"dispersal": "moderate",
8+
"random_seed": 42,
9+
"centre_walk_m": 800,
10+
"green_walk_m": 800,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 18500,
17+
"medium": 7500,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.1,
22+
"medium": 0.3,
23+
"low": 0.6
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "celina_tx_baseline",
29+
"notes": "The scenario's own params.json, as shipped."
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"crs": "EPSG:32614",
3+
"grid_size_m": 30,
4+
"max_iterations": 400,
5+
"target_population": 50000,
6+
"build_prob": 0.3,
7+
"dispersal": "off",
8+
"random_seed": 42,
9+
"centre_walk_m": 800,
10+
"green_walk_m": 800,
11+
"optimise_centres": true,
12+
"centre_m2_per_person": 20,
13+
"min_settlement_ha": 2,
14+
"min_green_span_m": 400,
15+
"densities_km2": {
16+
"high": 18500,
17+
"medium": 7500,
18+
"low": 1500
19+
},
20+
"shares": {
21+
"high": 0.1,
22+
"medium": 0.3,
23+
"low": 0.6
24+
},
25+
"ensemble": true,
26+
"ensemble_runs": 50,
27+
"schema": "isobenefit-params/1",
28+
"name": "celina_tx_compact",
29+
"notes": "No leapfrogging: one contiguous settlement."
30+
}

0 commit comments

Comments
 (0)