maritime-choke 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fathimath S. Vemmarath and Vipin P. Veetil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Note: This licence covers the source code only. The underlying economic data
24
+ (GTAP, OECD ICIO) are governed by their own licences and are not distributed
25
+ with this software.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: maritime-choke
3
+ Version: 0.1.0
4
+ Summary: Interactive model of macroeconomic risk from maritime-chokepoint trade disruptions
5
+ Author: Fathimath S. Vemmarath
6
+ Author-email: "Vipin P. Veetil" <vipin.veetil@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://bitbucket.org/VipinVeetil/chokepoints
9
+ Project-URL: Source, https://bitbucket.org/VipinVeetil/chokepoints
10
+ Keywords: production networks,input-output,maritime,chokepoints,trade disruption,systemic risk,economics,supply chains
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Framework :: Flask
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.23
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: matplotlib>=3.6
23
+ Requires-Dist: flask>=2.2
24
+ Requires-Dist: platformdirs>=2.6
25
+ Requires-Dist: certifi>=2022.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pyshp>=2.3; extra == "dev"
28
+ Requires-Dist: pandas>=1.5; extra == "dev"
29
+ Requires-Dist: build; extra == "dev"
30
+ Requires-Dist: twine; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # maritime-choke
34
+
35
+ An interactive model of the macroeconomic risk from **maritime-chokepoint trade
36
+ disruptions**. Close any set of the world's 12 major shipping chokepoints, pick a
37
+ `(τ, ρ, δ)` calibration, solve a short-run production-network model forward, and
38
+ explore where the value-added loss falls — on an interactive world heat map, by
39
+ country, and by sector — all in a local browser GUI.
40
+
41
+ Based on *Macroeconomic Risks from Maritime Trade Disruptions* (Vemmarath &
42
+ Veetil). Source & data: https://bitbucket.org/VipinVeetil/chokepoints
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install maritime-choke
48
+ ```
49
+
50
+ ## Get the data (one-time)
51
+
52
+ The solver needs a model cache (~180 MB) that isn't shipped in the wheel:
53
+
54
+ ```bash
55
+ maritime-choke fetch-data # downloads and installs it
56
+ ```
57
+
58
+ (You can also point at a local copy: `maritime-choke fetch-data --from-local /path/to/model_cache_2019`.)
59
+
60
+ ## Run
61
+
62
+ ```bash
63
+ maritime-choke run # opens the GUI at http://127.0.0.1:5057
64
+ ```
65
+
66
+ Pick chokepoints, set the calibration, hit **Run scenario**. Results are written
67
+ to a per-user folder (see `maritime-choke where`) and shown in the page:
68
+ world value-added loss, a relatable "scale of the loss" comparison, the world
69
+ heat map (recolour by any of 50 sectors), and per-country / per-sector drill-downs.
70
+
71
+ ## Commands
72
+
73
+ | command | what it does |
74
+ |---|---|
75
+ | `maritime-choke run` | launch the GUI (`--port`, `--no-browser`) |
76
+ | `maritime-choke fetch-data` | download/install the model cache (`--url`, `--from-local`, `--force`) |
77
+ | `maritime-choke pack-data <dir>` | pack a local cache into a `.tar.xz` for hosting |
78
+ | `maritime-choke where` | show data / cache / run-output locations |
79
+ | `maritime-choke build-data` | how to rebuild the cache from GTAP + OECD ICIO |
80
+
81
+ Data locations can be overridden with `MARITIME_CHOKE_DATA` (cache root) and
82
+ `MARITIME_CHOKE_RUNS` (run outputs). The download URL can be set with
83
+ `MARITIME_CHOKE_DATA_URL`.
84
+
85
+ ## Data & licensing
86
+
87
+ The model cache is derived from OECD ICIO (freely downloadable) and the licensed
88
+ **GTAP 10** data base. The raw GTAP data is *not* redistributed; if you need to
89
+ rebuild the cache from scratch you must supply your own GTAP licence — see
90
+ `maritime-choke build-data`. The shipped map/comparison assets are built from
91
+ public Natural Earth and World Bank data.
92
+
93
+ ## License
94
+
95
+ MIT (code). Underlying GTAP and OECD ICIO data are governed by their own terms.
@@ -0,0 +1,63 @@
1
+ # maritime-choke
2
+
3
+ An interactive model of the macroeconomic risk from **maritime-chokepoint trade
4
+ disruptions**. Close any set of the world's 12 major shipping chokepoints, pick a
5
+ `(τ, ρ, δ)` calibration, solve a short-run production-network model forward, and
6
+ explore where the value-added loss falls — on an interactive world heat map, by
7
+ country, and by sector — all in a local browser GUI.
8
+
9
+ Based on *Macroeconomic Risks from Maritime Trade Disruptions* (Vemmarath &
10
+ Veetil). Source & data: https://bitbucket.org/VipinVeetil/chokepoints
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install maritime-choke
16
+ ```
17
+
18
+ ## Get the data (one-time)
19
+
20
+ The solver needs a model cache (~180 MB) that isn't shipped in the wheel:
21
+
22
+ ```bash
23
+ maritime-choke fetch-data # downloads and installs it
24
+ ```
25
+
26
+ (You can also point at a local copy: `maritime-choke fetch-data --from-local /path/to/model_cache_2019`.)
27
+
28
+ ## Run
29
+
30
+ ```bash
31
+ maritime-choke run # opens the GUI at http://127.0.0.1:5057
32
+ ```
33
+
34
+ Pick chokepoints, set the calibration, hit **Run scenario**. Results are written
35
+ to a per-user folder (see `maritime-choke where`) and shown in the page:
36
+ world value-added loss, a relatable "scale of the loss" comparison, the world
37
+ heat map (recolour by any of 50 sectors), and per-country / per-sector drill-downs.
38
+
39
+ ## Commands
40
+
41
+ | command | what it does |
42
+ |---|---|
43
+ | `maritime-choke run` | launch the GUI (`--port`, `--no-browser`) |
44
+ | `maritime-choke fetch-data` | download/install the model cache (`--url`, `--from-local`, `--force`) |
45
+ | `maritime-choke pack-data <dir>` | pack a local cache into a `.tar.xz` for hosting |
46
+ | `maritime-choke where` | show data / cache / run-output locations |
47
+ | `maritime-choke build-data` | how to rebuild the cache from GTAP + OECD ICIO |
48
+
49
+ Data locations can be overridden with `MARITIME_CHOKE_DATA` (cache root) and
50
+ `MARITIME_CHOKE_RUNS` (run outputs). The download URL can be set with
51
+ `MARITIME_CHOKE_DATA_URL`.
52
+
53
+ ## Data & licensing
54
+
55
+ The model cache is derived from OECD ICIO (freely downloadable) and the licensed
56
+ **GTAP 10** data base. The raw GTAP data is *not* redistributed; if you need to
57
+ rebuild the cache from scratch you must supply your own GTAP licence — see
58
+ `maritime-choke build-data`. The shipped map/comparison assets are built from
59
+ public Natural Earth and World Bank data.
60
+
61
+ ## License
62
+
63
+ MIT (code). Underlying GTAP and OECD ICIO data are governed by their own terms.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "maritime-choke"
7
+ version = "0.1.0"
8
+ description = "Interactive model of macroeconomic risk from maritime-chokepoint trade disruptions"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Fathimath S. Vemmarath" },
14
+ { name = "Vipin P. Veetil", email = "vipin.veetil@gmail.com" },
15
+ ]
16
+ keywords = [
17
+ "production networks", "input-output", "maritime", "chokepoints",
18
+ "trade disruption", "systemic risk", "economics", "supply chains",
19
+ ]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Intended Audience :: Science/Research",
25
+ "Topic :: Scientific/Engineering",
26
+ "Framework :: Flask",
27
+ ]
28
+ dependencies = [
29
+ "numpy>=1.23",
30
+ "scipy>=1.10",
31
+ "matplotlib>=3.6",
32
+ "flask>=2.2",
33
+ "platformdirs>=2.6",
34
+ "certifi>=2022.0.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ # tools to regenerate the shipped static assets / build & publish the package
39
+ dev = ["pyshp>=2.3", "pandas>=1.5", "build", "twine"]
40
+
41
+ [project.scripts]
42
+ maritime-choke = "maritime_choke.cli:main"
43
+
44
+ [project.urls]
45
+ Homepage = "https://bitbucket.org/VipinVeetil/chokepoints"
46
+ Source = "https://bitbucket.org/VipinVeetil/chokepoints"
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.setuptools.package-data]
52
+ maritime_choke = ["templates/*.html", "static/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """
2
+ maritime-choke
3
+ ==============
4
+ A short-run production-network model of maritime-chokepoint trade disruption,
5
+ with an interactive local web GUI. Close any set of the world's major shipping
6
+ chokepoints, pick a (tau, rho, delta) calibration, solve the model forward, and
7
+ explore where the value-added loss falls on a world heat map and by sector.
8
+
9
+ Quickstart:
10
+ maritime-choke fetch-data # one-time: install the model cache (~180 MB)
11
+ maritime-choke run # launch the GUI in your browser
12
+ """
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.1.0"
16
+
17
+
18
+ def run(port: int | None = None, open_browser: bool = True) -> None:
19
+ """Launch the GUI (same as `maritime-choke run`)."""
20
+ from .app import serve
21
+ serve(port=port, open_browser=open_browser)
@@ -0,0 +1,187 @@
1
+ """
2
+ app.py -- local web GUI for the maritime-chokepoint disruption model.
3
+
4
+ Run:
5
+ cd 02_scripts/gui
6
+ ../../.venv_gui/bin/python app.py # then open http://127.0.0.1:5000
7
+
8
+ Pick which chokepoints to close and the (tau, rho, delta) calibration, press
9
+ Run, and the model solves forward. Each run writes a self-contained folder
10
+ under 04_outputs/gui_runs/<run_id>/ (config, summary, per-country CSV, two
11
+ PNG plots) and the headline numbers are shown in the page.
12
+
13
+ Solves run in a background thread; the page polls /api/progress for the live
14
+ outer-iteration count and h-residual, then renders the results.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import threading
19
+ import time
20
+ import traceback
21
+ from pathlib import Path
22
+
23
+ from flask import (Flask, jsonify, render_template, request,
24
+ send_from_directory, abort)
25
+
26
+ from . import solver_backend as sb
27
+
28
+ app = Flask(__name__)
29
+
30
+ # In-memory registry of runs started this session: run_id -> state dict.
31
+ RUNS: dict[str, dict] = {}
32
+ _LOCK = threading.Lock()
33
+
34
+
35
+ def _worker(run_id: str, cfg: dict) -> None:
36
+ state = RUNS[run_id]
37
+ try:
38
+ summary = sb.run_scenario(cfg, progress=state["progress"])
39
+ state["status"] = "done"
40
+ state["summary"] = summary
41
+ except Exception as e: # noqa: BLE001
42
+ state["status"] = "error"
43
+ state["error"] = f"{e}"
44
+ state["traceback"] = traceback.format_exc()
45
+ state["progress"]["phase"] = "error"
46
+
47
+
48
+ @app.route("/")
49
+ def index():
50
+ return render_template(
51
+ "index.html",
52
+ chokepoints=sb.chokepoint_labels(),
53
+ caches=sb.list_caches(),
54
+ presets=sb.PRESETS,
55
+ mapmeta=sb.map_metadata(),
56
+ )
57
+
58
+
59
+ @app.route("/api/run", methods=["POST"])
60
+ def api_run():
61
+ body = request.get_json(force=True)
62
+ Q = [int(k) for k in body.get("Q", [])]
63
+ if not Q:
64
+ return jsonify(error="Select at least one chokepoint to close."), 400
65
+
66
+ energy_specific = bool(body.get("energy_specific", True))
67
+ base_delta = float(body.get("base_delta", 0.10))
68
+ energy_delta = (float(body.get("energy_delta", 0.45))
69
+ if energy_specific else None)
70
+
71
+ run_id = body.get("run_id") or f"run_{int(time.time())}"
72
+ # keep run_id filesystem-safe
73
+ run_id = "".join(ch for ch in run_id if ch.isalnum() or ch in "-_")[:60]
74
+
75
+ cfg = dict(
76
+ Q=Q,
77
+ tau=float(body.get("tau", 0.3)),
78
+ rho=float(body.get("rho", -0.5)),
79
+ base_delta=base_delta,
80
+ energy_delta=energy_delta,
81
+ cache=body.get("cache") or "model_cache_2019",
82
+ max_outer=int(body.get("max_outer", 600)),
83
+ run_id=run_id,
84
+ )
85
+
86
+ with _LOCK:
87
+ # one heavy solve at a time keeps the CPU sane on a laptop
88
+ busy = any(s["status"] == "running" for s in RUNS.values())
89
+ if busy:
90
+ return jsonify(error="A solve is already running; wait for it "
91
+ "to finish."), 409
92
+ RUNS[run_id] = {
93
+ "status": "running",
94
+ "cfg": cfg,
95
+ "progress": {"phase": "queued", "iter": 0,
96
+ "max_iter": cfg["max_outer"], "residual": None,
97
+ "elapsed": 0.0},
98
+ "summary": None,
99
+ "error": None,
100
+ }
101
+ t = threading.Thread(target=_worker, args=(run_id, cfg), daemon=True)
102
+ t.start()
103
+ return jsonify(run_id=run_id)
104
+
105
+
106
+ @app.route("/api/progress/<run_id>")
107
+ def api_progress(run_id: str):
108
+ state = RUNS.get(run_id)
109
+ if state is None:
110
+ return jsonify(error="unknown run_id"), 404
111
+ return jsonify(
112
+ status=state["status"],
113
+ progress=state["progress"],
114
+ summary=state["summary"],
115
+ error=state["error"],
116
+ )
117
+
118
+
119
+ @app.route("/api/runs")
120
+ def api_runs():
121
+ """List past runs on disk (most recent first)."""
122
+ import json
123
+ out = []
124
+ if sb._runs_dir().exists():
125
+ for d in sb._runs_dir().iterdir():
126
+ sfile = d / "summary.json"
127
+ if sfile.exists():
128
+ try:
129
+ s = json.loads(sfile.read_text())
130
+ out.append({
131
+ "run_id": s.get("run_id", d.name),
132
+ "closed": s.get("closed_chokepoints", []),
133
+ "world_lambda_pct": s.get("world_lambda_pct"),
134
+ "params": s.get("params", {}),
135
+ "mtime": sfile.stat().st_mtime,
136
+ })
137
+ except Exception: # noqa: BLE001
138
+ pass
139
+ out.sort(key=lambda r: r["mtime"], reverse=True)
140
+ return jsonify(runs=out)
141
+
142
+
143
+ @app.route("/runs/<run_id>/<path:filename>")
144
+ def serve_run_file(run_id: str, filename: str):
145
+ run_id = "".join(ch for ch in run_id if ch.isalnum() or ch in "-_")
146
+ d = sb._runs_dir() / run_id
147
+ if not d.exists():
148
+ abort(404)
149
+ return send_from_directory(d, filename)
150
+
151
+
152
+ def _free_port(preferred: int) -> int:
153
+ """Return `preferred` if free, else an OS-assigned free port."""
154
+ import socket
155
+ for cand in (preferred, 0):
156
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
157
+ try:
158
+ s.bind(("127.0.0.1", cand))
159
+ return s.getsockname()[1]
160
+ except OSError:
161
+ continue
162
+ finally:
163
+ s.close()
164
+ return preferred
165
+
166
+
167
+ def serve(port: int | None = None, open_browser: bool = True) -> None:
168
+ """Launch the local GUI server (used by the `maritime-choke run` command)."""
169
+ import os
170
+ if port is None:
171
+ port = int(os.environ.get("PORT", "5057"))
172
+ port = _free_port(port)
173
+ url = f"http://127.0.0.1:{port}"
174
+ print(f"maritime-choke GUI -> {url}")
175
+ print("Run outputs are written to:", sb._runs_dir())
176
+ if not sb.list_caches():
177
+ print("\n[!] No model cache found. Run `maritime-choke fetch-data` first "
178
+ "(or set MARITIME_CHOKE_DATA to a folder that holds model_cache_2019/).\n")
179
+ if open_browser:
180
+ import threading
181
+ import webbrowser
182
+ threading.Timer(1.0, lambda: webbrowser.open(url)).start()
183
+ app.run(host="127.0.0.1", port=port, debug=False, threaded=True)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ serve()
@@ -0,0 +1,43 @@
1
+ """
2
+ chokepoint_names.py
3
+ ===================
4
+ Single source of truth for the three chokepoint name forms used across the
5
+ pipeline, in the canonical meta.json order (k = 0..11):
6
+
7
+ FULL : meta["chokepoints"] -- table row labels, captions
8
+ CSV : short keys used as the master-matrix CSV columns and by
9
+ verification/verify_ras_prose.py (e.g. "Hormuz", "Bab-el-Mandeb")
10
+ NICE : compact display labels for figure axes / joint constituents
11
+ (e.g. "BAM/Aden", "Malacca-Sing.")
12
+
13
+ Before this module the committed outputs used CSV columns while the committed
14
+ 05_figures.py used FULL columns + a FULL->NICE map, so the script could not
15
+ regenerate the committed CSV/figures. All scripts now import these maps so the
16
+ naming is consistent end to end.
17
+ """
18
+
19
+ FULL = [
20
+ "Strait of Hormuz", "Turkish Straits", "Danish Straits", "Suez Canal",
21
+ "Bab-el-Mandeb", "Strait of Gibraltar", "Strait of Malacca", "Panama Canal",
22
+ "Taiwan-Luzon Corridor", "Strait of Dover", "Korea Strait", "Kerch Strait",
23
+ ]
24
+ CSV = [
25
+ "Hormuz", "Turkish", "Danish", "Suez", "Bab-el-Mandeb", "Gibraltar",
26
+ "Malacca", "Panama", "Taiwan-Luzon", "Dover", "Korea", "Kerch",
27
+ ]
28
+ NICE = [
29
+ "Hormuz", "Turkish", "Danish", "Suez", "BAM/Aden", "Gibraltar",
30
+ "Malacca-Sing.", "Panama", "Taiwan-Luzon", "Dover", "Korea", "Kerch",
31
+ ]
32
+
33
+ assert len(FULL) == len(CSV) == len(NICE) == 12
34
+
35
+ full_to_csv = dict(zip(FULL, CSV))
36
+ full_to_nice = dict(zip(FULL, NICE))
37
+ csv_to_full = dict(zip(CSV, FULL))
38
+ csv_to_nice = dict(zip(CSV, NICE))
39
+
40
+
41
+ def to_csv_columns(full_names):
42
+ """Map an ordered list of FULL chokepoint names to CSV-short columns."""
43
+ return [full_to_csv[c] for c in full_names]
@@ -0,0 +1,101 @@
1
+ """
2
+ cli.py — the `maritime-choke` command.
3
+
4
+ maritime-choke run launch the interactive GUI (opens a browser)
5
+ maritime-choke fetch-data download/install the model cache
6
+ maritime-choke pack-data <dir> pack a local cache into an uploadable archive
7
+ maritime-choke where show data / cache / run-output locations
8
+ maritime-choke build-data how to rebuild the cache from GTAP + OECD ICIO
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+
14
+ from . import __version__, data
15
+
16
+ BUILD_HELP = """\
17
+ Rebuild the model cache from raw inputs (for users with their own GTAP licence)
18
+ ------------------------------------------------------------------------------
19
+ The cache is derived from OECD ICIO (freely downloadable) and the licensed
20
+ GTAP 10 data base. To rebuild it, use the source repository's pipeline:
21
+
22
+ git clone https://bitbucket.org/VipinVeetil/chokepoints
23
+ # place your GTAP .har files and the OECD ICIO tables under 01_raw_data/
24
+ cd chokepoints/02_scripts
25
+ python3 pipeline/00_extract_gtap_har.py
26
+ python3 pipeline/01_build_extended_table_fast.py --year 2019
27
+ python3 pipeline/02_build_inputs_k12.py
28
+
29
+ That writes 03_intermediate/model_cache_2019/. Then install it into
30
+ maritime-choke's data dir:
31
+
32
+ maritime-choke fetch-data --from-local <path>/03_intermediate/model_cache_2019
33
+
34
+ See DATA_AVAILABILITY.md in that repo for the licence notes.
35
+ """
36
+
37
+
38
+ def main(argv=None) -> None:
39
+ p = argparse.ArgumentParser(
40
+ prog="maritime-choke",
41
+ description="Interactive model of macroeconomic risk from "
42
+ "maritime-chokepoint trade disruptions.")
43
+ p.add_argument("--version", action="version",
44
+ version=f"maritime-choke {__version__}")
45
+ sub = p.add_subparsers(dest="cmd")
46
+
47
+ pr = sub.add_parser("run", help="launch the interactive GUI")
48
+ pr.add_argument("--port", type=int, default=None, help="port (default 5057)")
49
+ pr.add_argument("--no-browser", action="store_true",
50
+ help="do not open a browser window")
51
+
52
+ pf = sub.add_parser("fetch-data", help="download/install the model cache")
53
+ pf.add_argument("--url", default=None, help="explicit archive URL")
54
+ pf.add_argument("--from-local", default=None,
55
+ help="a local cache directory or .tar.xz to install instead")
56
+ pf.add_argument("--name", default=data.DEFAULT_CACHE, help="cache name")
57
+ pf.add_argument("--force", action="store_true", help="overwrite if present")
58
+
59
+ pp = sub.add_parser("pack-data",
60
+ help="pack a local cache dir into an uploadable .tar.xz")
61
+ pp.add_argument("cache_dir", help="path to a model_cache_* directory")
62
+ pp.add_argument("-o", "--out", default=None, help="output archive path")
63
+
64
+ sub.add_parser("where", help="show data / cache / run-output locations")
65
+ sub.add_parser("build-data", help="how to rebuild the cache from GTAP + ICIO")
66
+
67
+ args = p.parse_args(argv)
68
+
69
+ if args.cmd == "run":
70
+ from . import app
71
+ app.serve(port=args.port, open_browser=not args.no_browser)
72
+
73
+ elif args.cmd == "fetch-data":
74
+ path = data.fetch(url=args.url, name=args.name,
75
+ from_local=args.from_local, force=args.force)
76
+ print("cache ready at:", path)
77
+
78
+ elif args.cmd == "pack-data":
79
+ out = data.pack(args.cache_dir, args.out)
80
+ mb = out.stat().st_size / (1 << 20)
81
+ print(f"wrote {out} ({mb:.0f} MB)")
82
+ print("Upload this to Zenodo/Release, then set MARITIME_CHOKE_DATA_URL "
83
+ "(or DATA_URL in data.py) to its download URL.")
84
+
85
+ elif args.cmd == "where":
86
+ print("data dir :", data.data_dir())
87
+ print("caches :", data.caches_dir())
88
+ print("run outputs:", data.runs_dir())
89
+ caches = [p.name for p in data.caches_dir().glob("model_cache*")
90
+ if data.is_valid_cache(p)] if data.caches_dir().exists() else []
91
+ print("installed caches:", caches or "(none — run `maritime-choke fetch-data`)")
92
+
93
+ elif args.cmd == "build-data":
94
+ print(BUILD_HELP)
95
+
96
+ else:
97
+ p.print_help()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()