qellaris 0.5.0__py3-none-any.whl
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.
- qellaris/__init__.py +40 -0
- qellaris/__main__.py +111 -0
- qellaris/_version.py +3 -0
- qellaris/cli.py +230 -0
- qellaris/control_env.py +504 -0
- qellaris/data/kensington_buildings.json +356793 -0
- qellaris/data/kensington_landuse.json +51216 -0
- qellaris/data/kensington_streets.json +55913 -0
- qellaris/data/kensington_towers.json +1 -0
- qellaris/decision_metrics.py +70 -0
- qellaris/demographics.py +316 -0
- qellaris/domain.py +171 -0
- qellaris/equity.py +627 -0
- qellaris/equity_scenarios.py +230 -0
- qellaris/evaluation.py +260 -0
- qellaris/experiment.py +518 -0
- qellaris/forecasting.py +217 -0
- qellaris/geography.py +409 -0
- qellaris/grid.py +420 -0
- qellaris/hardware_job.py +113 -0
- qellaris/inventory.py +103 -0
- qellaris/menu.py +210 -0
- qellaris/milp_reference.py +56 -0
- qellaris/netbravo.py +442 -0
- qellaris/ofcom.py +231 -0
- qellaris/optimization.py +332 -0
- qellaris/paths.py +98 -0
- qellaris/placement.py +245 -0
- qellaris/power.py +171 -0
- qellaris/quantum.py +514 -0
- qellaris/quantum_zoo.py +594 -0
- qellaris/qubo.py +152 -0
- qellaris/radio.py +189 -0
- qellaris/real_data.py +98 -0
- qellaris/region_packages.py +202 -0
- qellaris/regions.py +519 -0
- qellaris/rl.py +237 -0
- qellaris/road_network.py +197 -0
- qellaris/scenario_generation.py +102 -0
- qellaris/sdk.py +195 -0
- qellaris/simulation.py +928 -0
- qellaris/solara_app.py +2255 -0
- qellaris/solvers.py +375 -0
- qellaris/template.py +736 -0
- qellaris/tests/__init__.py +1 -0
- qellaris/tests/conftest.py +8 -0
- qellaris/tests/test_benchmark.py +182 -0
- qellaris/tests/test_demographics.py +261 -0
- qellaris/tests/test_demographics_page.py +114 -0
- qellaris/tests/test_entry_points.py +194 -0
- qellaris/tests/test_equity.py +67 -0
- qellaris/tests/test_equity_scenarios.py +307 -0
- qellaris/tests/test_equity_state.py +152 -0
- qellaris/tests/test_evaluation.py +144 -0
- qellaris/tests/test_geometry.py +121 -0
- qellaris/tests/test_hardware_job.py +38 -0
- qellaris/tests/test_inventory.py +114 -0
- qellaris/tests/test_land_use.py +222 -0
- qellaris/tests/test_launcher.py +88 -0
- qellaris/tests/test_layout.py +103 -0
- qellaris/tests/test_layout_factorial_archive.py +43 -0
- qellaris/tests/test_menu.py +73 -0
- qellaris/tests/test_milp_reference.py +28 -0
- qellaris/tests/test_netbravo.py +73 -0
- qellaris/tests/test_netbravo_ingest.py +156 -0
- qellaris/tests/test_paths.py +139 -0
- qellaris/tests/test_placement.py +110 -0
- qellaris/tests/test_power.py +213 -0
- qellaris/tests/test_quantum.py +219 -0
- qellaris/tests/test_quantum_zoo.py +101 -0
- qellaris/tests/test_radio.py +57 -0
- qellaris/tests/test_region_packages.py +130 -0
- qellaris/tests/test_regions.py +231 -0
- qellaris/tests/test_rl_env.py +159 -0
- qellaris/tests/test_scenario_generation.py +38 -0
- qellaris/tests/test_sdk.py +60 -0
- qellaris/tests/test_study_area_page.py +149 -0
- qellaris/tests/test_template.py +218 -0
- qellaris/tests/test_ui_model.py +157 -0
- qellaris/ui.py +198 -0
- qellaris-0.5.0.dist-info/METADATA +505 -0
- qellaris-0.5.0.dist-info/RECORD +86 -0
- qellaris-0.5.0.dist-info/WHEEL +5 -0
- qellaris-0.5.0.dist-info/entry_points.txt +13 -0
- qellaris-0.5.0.dist-info/licenses/LICENSE +25 -0
- qellaris-0.5.0.dist-info/top_level.txt +1 -0
qellaris/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""QAIO cellular demand forecasting and sector-scheduling benchmark.
|
|
2
|
+
|
|
3
|
+
Three things come out of a run, and they are kept apart on purpose:
|
|
4
|
+
|
|
5
|
+
* the **schedule and its cost** - energy, unmet demand, switching, the scalar
|
|
6
|
+
every solver is ranked on (:mod:`~qellaris.experiment`);
|
|
7
|
+
* the **demographic state** - who the population is and what it is doing,
|
|
8
|
+
never what it received (:mod:`~qellaris.demographics`);
|
|
9
|
+
* the **equity output** - synthetic group diagnostics under named scenario
|
|
10
|
+
levers (:mod:`~qellaris.equity`, :mod:`~qellaris.equity_scenarios`),
|
|
11
|
+
not a census fairness claim.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .control_env import TransmitterControlEnv
|
|
15
|
+
from .demographics import (ACTIVITIES, DEMOGRAPHIC_AXES, DemographicTracker,
|
|
16
|
+
format_state)
|
|
17
|
+
from .equity import (EQUITY_SCENARIOS, EquityScenario, equity_output,
|
|
18
|
+
get_equity_scenario)
|
|
19
|
+
from .equity_scenarios import (ScenarioConfig, format_scenarios,
|
|
20
|
+
run_equity_scenarios, run_scenario)
|
|
21
|
+
from .experiment import ExperimentConfig, run_experiment
|
|
22
|
+
from .regions import RegionSpec, download_osm_region, find_region
|
|
23
|
+
from .inventory import load_fixed_inventory
|
|
24
|
+
from .rl import SectorPolicy, train_policy
|
|
25
|
+
from .simulation import GridDemandModel
|
|
26
|
+
from .sdk import (BenchmarkState, JobBroker, register_agent,
|
|
27
|
+
register_forecaster, score_action, state_from_model)
|
|
28
|
+
from .solvers import SOLVER_NAMES, get_solver
|
|
29
|
+
from ._version import __version__
|
|
30
|
+
|
|
31
|
+
__all__ = ["ACTIVITIES", "DEMOGRAPHIC_AXES", "EQUITY_SCENARIOS",
|
|
32
|
+
"BenchmarkState", "ExperimentConfig", "EquityScenario", "GridDemandModel",
|
|
33
|
+
"DemographicTracker", "RegionSpec", "SOLVER_NAMES", "ScenarioConfig",
|
|
34
|
+
"JobBroker", "SectorPolicy", "TransmitterControlEnv", "__version__",
|
|
35
|
+
"download_osm_region", "load_fixed_inventory",
|
|
36
|
+
"equity_output", "find_region", "format_scenarios", "format_state",
|
|
37
|
+
"get_equity_scenario", "get_solver", "run_equity_scenarios",
|
|
38
|
+
"register_agent", "register_forecaster",
|
|
39
|
+
"run_experiment", "run_scenario",
|
|
40
|
+
"score_action", "state_from_model", "train_policy"]
|
qellaris/__main__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""One command for the whole package: ``python -m qellaris <subcommand>``.
|
|
2
|
+
|
|
3
|
+
Everything the project can do used to be reached three different ways -- a
|
|
4
|
+
console script, a module, or a loose file in the repository root that fixed up
|
|
5
|
+
``sys.path`` and then imported the real code. The root scripts existed only
|
|
6
|
+
because the package was not importable without help, and each carried its own
|
|
7
|
+
copy of the bootstrap.
|
|
8
|
+
|
|
9
|
+
That bootstrap now lives in :mod:`qellaris.ui` and runs here, so this module
|
|
10
|
+
is the single door in. The root ``run_ui.py`` and friends stay as three-line
|
|
11
|
+
shims that call straight into it, because PyCharm run configurations point at
|
|
12
|
+
them by path.
|
|
13
|
+
|
|
14
|
+
python -m qellaris ui
|
|
15
|
+
python -m qellaris benchmark --geography kensington
|
|
16
|
+
python -m qellaris equity --scenarios protective neutral
|
|
17
|
+
python -m qellaris region camden --place "Camden, London" --radius-km 1
|
|
18
|
+
python -m qellaris template --explain
|
|
19
|
+
|
|
20
|
+
Each subcommand is the same function its console script points at, so there is
|
|
21
|
+
one implementation and no drift between the two ways of calling it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ui(argv: list[str]) -> None:
|
|
30
|
+
import argparse
|
|
31
|
+
|
|
32
|
+
from .ui import main
|
|
33
|
+
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
prog="python -m qellaris ui",
|
|
36
|
+
description="Launch the dashboard in this process, so Stop stops it "
|
|
37
|
+
"and breakpoints in the model bind.")
|
|
38
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
39
|
+
parser.add_argument("--port", type=int, default=8765)
|
|
40
|
+
parser.add_argument("--no-browser", action="store_true",
|
|
41
|
+
help="do not open a browser window")
|
|
42
|
+
args = parser.parse_args(argv)
|
|
43
|
+
main(args.host, args.port, not args.no_browser)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#: ``subcommand -> (loader, one-line help)``. Loaders import lazily: starting
|
|
47
|
+
#: the dashboard should not pay for importing the quantum backends, and the
|
|
48
|
+
#: benchmark should not pay for importing Solara.
|
|
49
|
+
def _commands():
|
|
50
|
+
from . import (cli, hardware_job, menu, netbravo, quantum_zoo, real_data,
|
|
51
|
+
region_packages, regions, template)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"menu": (menu.main, "guided menu: answer a few questions, it runs the command"),
|
|
55
|
+
"ui": (_ui, "launch the interactive dashboard"),
|
|
56
|
+
"benchmark": (cli.main, "run the forecasting and scheduling benchmark"),
|
|
57
|
+
"equity": (cli.equity_main, "compare equity scenarios on one population"),
|
|
58
|
+
"train": (cli.train_main, "train the factored sector-control policy"),
|
|
59
|
+
"region": (regions.main, "download a study area by bounding box or place"),
|
|
60
|
+
"region-pack": (region_packages.main, "list, export or import study areas as .zip packages"),
|
|
61
|
+
"opencellid": (real_data.main, "fetch OpenCellID observations"),
|
|
62
|
+
"netbravo": (netbravo.main, "fetch the NetBravo observed-signal layer"),
|
|
63
|
+
"hardware-job": (hardware_job.main, "decode a returned QAOA hardware job"),
|
|
64
|
+
"template": (template.main, "worked example: register an agent, run it, compare it"),
|
|
65
|
+
"zoo": (quantum_zoo.main, "Quantum Algorithm Zoo entries, read against this benchmark"),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def usage(commands) -> str:
|
|
70
|
+
width = max(len(name) for name in commands)
|
|
71
|
+
lines = ["usage: python -m qellaris <subcommand> [options]", "",
|
|
72
|
+
"subcommands:"]
|
|
73
|
+
lines += [f" {name:<{width}} {help_text}"
|
|
74
|
+
for name, (_, help_text) in commands.items()]
|
|
75
|
+
lines += ["", "Add --help after a subcommand for its own options.",
|
|
76
|
+
"New here? Run `qellaris menu` and it will ask what you want."]
|
|
77
|
+
return "\n".join(lines)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def bootstrap() -> None:
|
|
81
|
+
"""Make the package importable and drop a foreign environment's packages.
|
|
82
|
+
|
|
83
|
+
Each root script used to carry its own copy of this. It runs for every
|
|
84
|
+
subcommand now, not just the dashboard: a compiled extension built for a
|
|
85
|
+
different CPython fails with a thoroughly misleading "No module named ..."
|
|
86
|
+
deep inside an unrelated import, and that is just as confusing from the
|
|
87
|
+
benchmark as from the UI.
|
|
88
|
+
"""
|
|
89
|
+
from .ui import drop_foreign_site_packages, ensure_importable
|
|
90
|
+
|
|
91
|
+
ensure_importable()
|
|
92
|
+
for entry in drop_foreign_site_packages():
|
|
93
|
+
print(f"note: ignoring another environment's packages: {entry}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: list[str] | None = None) -> None:
|
|
97
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
98
|
+
bootstrap()
|
|
99
|
+
commands = _commands()
|
|
100
|
+
if not argv or argv[0] in {"-h", "--help", "help"}:
|
|
101
|
+
print(usage(commands))
|
|
102
|
+
return
|
|
103
|
+
name, rest = argv[0], argv[1:]
|
|
104
|
+
if name not in commands:
|
|
105
|
+
raise SystemExit(f"unknown subcommand {name!r}\n\n{usage(commands)}")
|
|
106
|
+
run, _ = commands[name]
|
|
107
|
+
run(rest)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
qellaris/_version.py
ADDED
qellaris/cli.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Command-line entry points for the benchmark and the RL trainer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import paths
|
|
10
|
+
from .experiment import ExperimentConfig, format_leaderboard, run_experiment
|
|
11
|
+
from .forecasting import FORECASTER_NAMES
|
|
12
|
+
from .simulation import SCENARIO_NAMES
|
|
13
|
+
from .solvers import installed_solver_names, solver_names
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description="Run the demand-forecast / sector-scheduling benchmark.")
|
|
19
|
+
parser.add_argument("--output", default=paths.runs("baseline"))
|
|
20
|
+
parser.add_argument("--seed", type=int, default=17)
|
|
21
|
+
parser.add_argument("--agents", type=int, default=240)
|
|
22
|
+
parser.add_argument("--train-days", type=int, default=7)
|
|
23
|
+
parser.add_argument("--horizon", type=int, default=16)
|
|
24
|
+
parser.add_argument("--intervals-per-day", type=int, default=96)
|
|
25
|
+
parser.add_argument("--point-forecast", action="store_true",
|
|
26
|
+
help="schedule on the point forecast instead of its upper bound")
|
|
27
|
+
parser.add_argument("--scenario", choices=SCENARIO_NAMES,
|
|
28
|
+
default="entertainment_event",
|
|
29
|
+
help="'entertainment_event' stages one demand event "
|
|
30
|
+
"at the best venue the study area actually has "
|
|
31
|
+
"(a stadium if there is one, otherwise a "
|
|
32
|
+
"transport hub, high street, campus or park, at "
|
|
33
|
+
"a magnitude that fits); an area with none gets "
|
|
34
|
+
"no event. A specific venue name (e.g. "
|
|
35
|
+
"'park_event') forces that family instead of "
|
|
36
|
+
"auto-picking. 'normal' asks for none.")
|
|
37
|
+
parser.add_argument("--geography", default="synthetic",
|
|
38
|
+
help="'synthetic', 'kensington', or a folder from "
|
|
39
|
+
"`qellaris region`")
|
|
40
|
+
parser.add_argument("--inventory", default=None,
|
|
41
|
+
help="optional fixed tower/sector JSON "
|
|
42
|
+
"(positions + directions; no placement search)")
|
|
43
|
+
parser.add_argument("--solvers", nargs="+", default=None,
|
|
44
|
+
choices=list(solver_names()),
|
|
45
|
+
help="default: every solver whose backend is installed")
|
|
46
|
+
parser.add_argument("--forecasters", nargs="+", default=None,
|
|
47
|
+
choices=list(FORECASTER_NAMES),
|
|
48
|
+
help="default: seasonal_naive only; pass more than one "
|
|
49
|
+
"to run the forecaster x solver factorial "
|
|
50
|
+
"(every solver replayed once per forecaster; "
|
|
51
|
+
"interface support, not a claimed primary result)")
|
|
52
|
+
parser.add_argument("--rl-weights", default=None,
|
|
53
|
+
help="policy JSON written by `qellaris train`")
|
|
54
|
+
parser.add_argument("--decision-reference", default="greedy",
|
|
55
|
+
choices=list(solver_names()),
|
|
56
|
+
help="reference used to attribute forecast and solver decision gaps")
|
|
57
|
+
parser.add_argument("--no-counterfactual", action="store_true",
|
|
58
|
+
help="skip the seed-paired no-event control replay")
|
|
59
|
+
parser.add_argument("--json", action="store_true",
|
|
60
|
+
help="print the full summary instead of the ranked table")
|
|
61
|
+
parser.add_argument("--list-backends", action="store_true",
|
|
62
|
+
help="report which optional quantum packages are installed")
|
|
63
|
+
parser.add_argument("--seeds", type=int, nargs="+", default=None,
|
|
64
|
+
help="replicate over these seeds and report paired "
|
|
65
|
+
"bootstrap intervals instead of a single run")
|
|
66
|
+
parser.add_argument("--metric", default="objective",
|
|
67
|
+
help="metric the replication table ranks and pairs on")
|
|
68
|
+
args = parser.parse_args(argv)
|
|
69
|
+
|
|
70
|
+
if args.list_backends:
|
|
71
|
+
from .quantum import backends
|
|
72
|
+
|
|
73
|
+
for backend in backends():
|
|
74
|
+
state = "installed" if backend.available else f"missing ({backend.detail})"
|
|
75
|
+
print(f"{backend.name:<16} {backend.package:<22} {state}")
|
|
76
|
+
print("\nsolvers available:", ", ".join(installed_solver_names()))
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
chosen = tuple(args.solvers) if args.solvers else installed_solver_names()
|
|
80
|
+
chosen_forecasters = tuple(args.forecasters) if args.forecasters else ("seasonal_naive",)
|
|
81
|
+
|
|
82
|
+
if args.seeds:
|
|
83
|
+
from .evaluation import format_report, run_replications, write_report
|
|
84
|
+
|
|
85
|
+
report = run_replications(
|
|
86
|
+
args.output,
|
|
87
|
+
ExperimentConfig(
|
|
88
|
+
agents=args.agents, train_days=args.train_days,
|
|
89
|
+
horizon=args.horizon, intervals_per_day=args.intervals_per_day,
|
|
90
|
+
uncertainty_aware=not args.point_forecast, scenario=args.scenario,
|
|
91
|
+
geography=args.geography, inventory_path=args.inventory,
|
|
92
|
+
solvers=chosen,
|
|
93
|
+
forecasters=chosen_forecasters,
|
|
94
|
+
rl_weights=args.rl_weights,
|
|
95
|
+
decision_reference=args.decision_reference,
|
|
96
|
+
paired_counterfactual=not args.no_counterfactual),
|
|
97
|
+
seeds=tuple(args.seeds),
|
|
98
|
+
reference=args.decision_reference)
|
|
99
|
+
print(format_report(report, args.metric))
|
|
100
|
+
print(f"\nartefacts written to "
|
|
101
|
+
f"{write_report(args.output, report, args.metric)}")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
summary = run_experiment(args.output, ExperimentConfig(
|
|
105
|
+
seed=args.seed, agents=args.agents, train_days=args.train_days,
|
|
106
|
+
horizon=args.horizon, intervals_per_day=args.intervals_per_day,
|
|
107
|
+
uncertainty_aware=not args.point_forecast, scenario=args.scenario,
|
|
108
|
+
geography=args.geography, inventory_path=args.inventory,
|
|
109
|
+
solvers=chosen,
|
|
110
|
+
forecasters=chosen_forecasters,
|
|
111
|
+
rl_weights=args.rl_weights, decision_reference=args.decision_reference,
|
|
112
|
+
paired_counterfactual=not args.no_counterfactual))
|
|
113
|
+
if args.json:
|
|
114
|
+
print(json.dumps(summary, indent=2))
|
|
115
|
+
return
|
|
116
|
+
area = summary["study_area"]
|
|
117
|
+
print(f"{area['name']}: {summary['zones']} demand bins, "
|
|
118
|
+
f"{summary['transmitters']} sectors, "
|
|
119
|
+
f"{area['width_km']} x {area['height_km']} km")
|
|
120
|
+
for name, metrics in summary["forecasts"].items():
|
|
121
|
+
print(f"forecast[{name}] MAE {metrics['mae']:.3f}, "
|
|
122
|
+
f"interval coverage {metrics['interval_coverage']:.3f}")
|
|
123
|
+
print()
|
|
124
|
+
print(format_leaderboard(summary["leaderboard"]))
|
|
125
|
+
print(f"\nartefacts written to {args.output}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def equity_main(argv: list[str] | None = None) -> None:
|
|
129
|
+
"""Run the same population under several equity scenarios and compare.
|
|
130
|
+
|
|
131
|
+
Writes the equity output per scenario and the demographic state they all
|
|
132
|
+
shared, as two files, because they are two different things: one says who
|
|
133
|
+
was out there, the other says what the network did about it.
|
|
134
|
+
"""
|
|
135
|
+
from .demographics import DEMOGRAPHIC_AXES, format_state
|
|
136
|
+
from .equity import EQUITY_SCENARIOS
|
|
137
|
+
from .equity_scenarios import (ScenarioConfig, format_scenarios,
|
|
138
|
+
run_equity_scenarios, write_scenarios)
|
|
139
|
+
|
|
140
|
+
parser = argparse.ArgumentParser(
|
|
141
|
+
description="Compare equity scenarios on one population.")
|
|
142
|
+
parser.add_argument("--output", default=paths.runs("equity"))
|
|
143
|
+
parser.add_argument("--scenarios", nargs="+", default=None,
|
|
144
|
+
choices=list(EQUITY_SCENARIOS),
|
|
145
|
+
help="default: every scenario")
|
|
146
|
+
parser.add_argument("--reference", default="protective",
|
|
147
|
+
choices=list(EQUITY_SCENARIOS),
|
|
148
|
+
help="scenario the others are reported against")
|
|
149
|
+
parser.add_argument("--agents", type=int, default=240)
|
|
150
|
+
parser.add_argument("--intervals-per-day", type=int, default=96)
|
|
151
|
+
parser.add_argument("--steps", type=int, default=96,
|
|
152
|
+
help="intervals scored, after the warm-up")
|
|
153
|
+
parser.add_argument("--warmup", type=int, default=96,
|
|
154
|
+
help="intervals run before the audit window opens")
|
|
155
|
+
parser.add_argument("--seed", type=int, default=17)
|
|
156
|
+
parser.add_argument("--geography", default="kensington",
|
|
157
|
+
help="'synthetic', 'kensington', or a folder from "
|
|
158
|
+
"`qellaris region`")
|
|
159
|
+
parser.add_argument("--demand-scenario", choices=SCENARIO_NAMES,
|
|
160
|
+
default="entertainment_event")
|
|
161
|
+
parser.add_argument("--solver", default="greedy", choices=list(solver_names()))
|
|
162
|
+
parser.add_argument("--capacity-headroom", type=float, default=1.2,
|
|
163
|
+
help="site sizing vs. pilot peak demand; below 1.0 forces "
|
|
164
|
+
"a genuinely capacity-bound network so bin-weighting "
|
|
165
|
+
"scenarios have demand to actually ration")
|
|
166
|
+
parser.add_argument("--point-forecast", action="store_true")
|
|
167
|
+
parser.add_argument("--show-demographics", nargs="*", default=None,
|
|
168
|
+
metavar="AXIS",
|
|
169
|
+
help="also print the demographic state; name axes to "
|
|
170
|
+
f"narrow it ({', '.join(DEMOGRAPHIC_AXES)})")
|
|
171
|
+
parser.add_argument("--json", action="store_true")
|
|
172
|
+
args = parser.parse_args(argv)
|
|
173
|
+
|
|
174
|
+
report = run_equity_scenarios(args.scenarios, ScenarioConfig(
|
|
175
|
+
agents=args.agents, intervals_per_day=args.intervals_per_day,
|
|
176
|
+
steps=args.steps, warmup=args.warmup, seed=args.seed,
|
|
177
|
+
geography=args.geography, demand_scenario=args.demand_scenario,
|
|
178
|
+
solver=args.solver, uncertainty_aware=not args.point_forecast,
|
|
179
|
+
capacity_headroom=args.capacity_headroom),
|
|
180
|
+
reference=args.reference)
|
|
181
|
+
|
|
182
|
+
if args.json:
|
|
183
|
+
print(json.dumps(report, indent=2))
|
|
184
|
+
else:
|
|
185
|
+
if args.show_demographics is not None:
|
|
186
|
+
axes = tuple(args.show_demographics) or None
|
|
187
|
+
print(format_state(report["demographics"], axes))
|
|
188
|
+
print(format_scenarios(report))
|
|
189
|
+
print(f"artefacts written to {write_scenarios(args.output, report)}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def train_main(argv: list[str] | None = None) -> None:
|
|
193
|
+
"""Train the factored sector policy against the Gymnasium environment."""
|
|
194
|
+
from .control_env import TransmitterControlEnv
|
|
195
|
+
from .rl import SectorPolicy, evaluate_policy, train_policy
|
|
196
|
+
|
|
197
|
+
parser = argparse.ArgumentParser(description="Train the RL sector-control policy.")
|
|
198
|
+
parser.add_argument("--episodes", type=int, default=80)
|
|
199
|
+
parser.add_argument("--episode-length", type=int, default=48)
|
|
200
|
+
parser.add_argument("--agents", type=int, default=240)
|
|
201
|
+
parser.add_argument("--intervals-per-day", type=int, default=96)
|
|
202
|
+
parser.add_argument("--geography", default="kensington")
|
|
203
|
+
parser.add_argument("--seed", type=int, default=17)
|
|
204
|
+
parser.add_argument("--learning-rate", type=float, default=0.05)
|
|
205
|
+
parser.add_argument("--output", default=paths.runs("rl", "policy.json"))
|
|
206
|
+
parser.add_argument("--resume", default=None, help="continue from an existing policy JSON")
|
|
207
|
+
args = parser.parse_args(argv)
|
|
208
|
+
|
|
209
|
+
env = TransmitterControlEnv(
|
|
210
|
+
n_agents=args.agents, intervals_per_day=args.intervals_per_day,
|
|
211
|
+
episode_length=args.episode_length,
|
|
212
|
+
warmup_intervals=args.intervals_per_day, seed=args.seed,
|
|
213
|
+
geography=args.geography)
|
|
214
|
+
start = SectorPolicy.load(args.resume) if args.resume and Path(args.resume).exists() else None
|
|
215
|
+
|
|
216
|
+
def show(episode: int, total: float) -> None:
|
|
217
|
+
if episode % max(1, args.episodes // 20) == 0:
|
|
218
|
+
print(f"episode {episode:4d} return {total:12.1f}")
|
|
219
|
+
|
|
220
|
+
policy, report = train_policy(env, episodes=args.episodes, seed=args.seed,
|
|
221
|
+
learning_rate=args.learning_rate, policy=start,
|
|
222
|
+
progress=show)
|
|
223
|
+
path = policy.save(args.output)
|
|
224
|
+
print(json.dumps({"weights": path.as_posix(), **report.as_dict(),
|
|
225
|
+
"episode_rewards": report.episode_rewards[-5:],
|
|
226
|
+
"evaluation": evaluate_policy(policy, env, episodes=3)}, indent=2))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|