sqllocks-shape-behavior 0.9.1__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.
Files changed (45) hide show
  1. sqllocks_shape_behavior-0.9.1/LICENSE +21 -0
  2. sqllocks_shape_behavior-0.9.1/PKG-INFO +48 -0
  3. sqllocks_shape_behavior-0.9.1/README.md +25 -0
  4. sqllocks_shape_behavior-0.9.1/pyproject.toml +50 -0
  5. sqllocks_shape_behavior-0.9.1/setup.cfg +4 -0
  6. sqllocks_shape_behavior-0.9.1/src/shape_behavior/__init__.py +40 -0
  7. sqllocks_shape_behavior-0.9.1/src/shape_behavior/behaviors.py +150 -0
  8. sqllocks_shape_behavior-0.9.1/src/shape_behavior/cli.py +240 -0
  9. sqllocks_shape_behavior-0.9.1/src/shape_behavior/conditions.py +221 -0
  10. sqllocks_shape_behavior-0.9.1/src/shape_behavior/dist.py +89 -0
  11. sqllocks_shape_behavior-0.9.1/src/shape_behavior/events.py +108 -0
  12. sqllocks_shape_behavior-0.9.1/src/shape_behavior/examples/equipment_maintenance.json +169 -0
  13. sqllocks_shape_behavior-0.9.1/src/shape_behavior/examples/healthcare_screening.json +255 -0
  14. sqllocks_shape_behavior-0.9.1/src/shape_behavior/examples/subscription.json +261 -0
  15. sqllocks_shape_behavior-0.9.1/src/shape_behavior/extension.py +109 -0
  16. sqllocks_shape_behavior-0.9.1/src/shape_behavior/gmf.py +417 -0
  17. sqllocks_shape_behavior-0.9.1/src/shape_behavior/model.py +495 -0
  18. sqllocks_shape_behavior-0.9.1/src/shape_behavior/params.py +85 -0
  19. sqllocks_shape_behavior-0.9.1/src/shape_behavior/population.py +224 -0
  20. sqllocks_shape_behavior-0.9.1/src/shape_behavior/primitives.py +734 -0
  21. sqllocks_shape_behavior-0.9.1/src/shape_behavior/rng.py +47 -0
  22. sqllocks_shape_behavior-0.9.1/src/shape_behavior/simulator.py +551 -0
  23. sqllocks_shape_behavior-0.9.1/src/shape_behavior/timeutil.py +81 -0
  24. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/PKG-INFO +48 -0
  25. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/SOURCES.txt +43 -0
  26. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/dependency_links.txt +1 -0
  27. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/entry_points.txt +12 -0
  28. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/requires.txt +1 -0
  29. sqllocks_shape_behavior-0.9.1/src/sqllocks_shape_behavior.egg-info/top_level.txt +1 -0
  30. sqllocks_shape_behavior-0.9.1/tests/test_behaviors_group.py +97 -0
  31. sqllocks_shape_behavior-0.9.1/tests/test_cli.py +131 -0
  32. sqllocks_shape_behavior-0.9.1/tests/test_clock.py +123 -0
  33. sqllocks_shape_behavior-0.9.1/tests/test_determinism.py +92 -0
  34. sqllocks_shape_behavior-0.9.1/tests/test_examples.py +144 -0
  35. sqllocks_shape_behavior-0.9.1/tests/test_extension.py +92 -0
  36. sqllocks_shape_behavior-0.9.1/tests/test_gmf.py +138 -0
  37. sqllocks_shape_behavior-0.9.1/tests/test_guards.py +155 -0
  38. sqllocks_shape_behavior-0.9.1/tests/test_primitives.py +627 -0
  39. sqllocks_shape_behavior-0.9.1/tests/test_primitives_cli.py +280 -0
  40. sqllocks_shape_behavior-0.9.1/tests/test_primitives_scale.py +71 -0
  41. sqllocks_shape_behavior-0.9.1/tests/test_resume.py +77 -0
  42. sqllocks_shape_behavior-0.9.1/tests/test_rng.py +36 -0
  43. sqllocks_shape_behavior-0.9.1/tests/test_scale.py +36 -0
  44. sqllocks_shape_behavior-0.9.1/tests/test_skeleton.py +27 -0
  45. sqllocks_shape_behavior-0.9.1/tests/test_transitions.py +137 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 SQLLocks (Jonathan Stewart)
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.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqllocks-shape-behavior
3
+ Version: 0.9.1
4
+ Summary: Shape plugin: behavior models (state machines on a virtual clock)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/sqllocks/shape
7
+ Project-URL: Repository, https://github.com/sqllocks/shape
8
+ Project-URL: Issues, https://github.com/sqllocks/shape/issues
9
+ Project-URL: Documentation, https://github.com/sqllocks/shape/tree/main/plugins/shape-behavior
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Database
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: sqllocks-shape==0.9.1
22
+ Dynamic: license-file
23
+
24
+ # sqllocks-shape-behavior
25
+
26
+ Shape plugin: **behavior models**. Declarative state-machine modules (states with delays, guards,
27
+ weighted and conditional transitions, attributes, clinical-style events and an extension point for
28
+ domain events) run for a population on a virtual clock, deterministic per seed and resumable, and
29
+ return an Arrow table of timestamped events that a domain pack turns into tables. It also imports
30
+ Generic Module Framework JSON modules that you download yourself.
31
+
32
+ ```bash
33
+ pip install sqllocks-shape-behavior
34
+ shape behave run subscription --population 10000 --years 3 --seed 7 -o out/
35
+ ```
36
+
37
+ ```python
38
+ from shape_behavior import Population, Simulator, load_module
39
+
40
+ sim = Simulator([load_module("subscription")], Population(size=10_000, start="2024-01-01"))
41
+ events = sim.run_until("2027-01-01") # a pyarrow.Table
42
+ ```
43
+
44
+ Behavior modules are plugins of the entry-point group `shape.behaviors`: `shape plugins list`
45
+ shows them, and `python -m shape.plugins.kit sqllocks-shape-behavior` checks them.
46
+
47
+ Full reference: `docs/plugins/behavior.md` in the repository. Its version always equals core's
48
+ (`sqllocks-shape`) and it is released together with core. It depends only on numpy and pyarrow.
@@ -0,0 +1,25 @@
1
+ # sqllocks-shape-behavior
2
+
3
+ Shape plugin: **behavior models**. Declarative state-machine modules (states with delays, guards,
4
+ weighted and conditional transitions, attributes, clinical-style events and an extension point for
5
+ domain events) run for a population on a virtual clock, deterministic per seed and resumable, and
6
+ return an Arrow table of timestamped events that a domain pack turns into tables. It also imports
7
+ Generic Module Framework JSON modules that you download yourself.
8
+
9
+ ```bash
10
+ pip install sqllocks-shape-behavior
11
+ shape behave run subscription --population 10000 --years 3 --seed 7 -o out/
12
+ ```
13
+
14
+ ```python
15
+ from shape_behavior import Population, Simulator, load_module
16
+
17
+ sim = Simulator([load_module("subscription")], Population(size=10_000, start="2024-01-01"))
18
+ events = sim.run_until("2027-01-01") # a pyarrow.Table
19
+ ```
20
+
21
+ Behavior modules are plugins of the entry-point group `shape.behaviors`: `shape plugins list`
22
+ shows them, and `python -m shape.plugins.kit sqllocks-shape-behavior` checks them.
23
+
24
+ Full reference: `docs/plugins/behavior.md` in the repository. Its version always equals core's
25
+ (`sqllocks-shape`) and it is released together with core. It depends only on numpy and pyarrow.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqllocks-shape-behavior"
7
+ # Lockstep with core (T-09): scripts/check_plugin_skeletons.py fails when this drifts.
8
+ version = "0.9.1"
9
+ description = "Shape plugin: behavior models (state machines on a virtual clock)"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Database",
23
+ ]
24
+ dependencies = ["sqllocks-shape==0.9.1"]
25
+
26
+ # Entry points (shape.* groups, plugin API v1); see docs/plugins/authoring.md.
27
+ [project.urls]
28
+ Homepage = "https://github.com/sqllocks/shape"
29
+ Repository = "https://github.com/sqllocks/shape"
30
+ Issues = "https://github.com/sqllocks/shape/issues"
31
+ Documentation = "https://github.com/sqllocks/shape/tree/main/plugins/shape-behavior"
32
+
33
+ [project.entry-points."shape.commands"]
34
+ behave = "shape_behavior.cli:BehaveCommand"
35
+
36
+ [project.entry-points."shape.behaviors"]
37
+ subscription = "shape_behavior.behaviors:Subscription"
38
+ equipment_maintenance = "shape_behavior.behaviors:EquipmentMaintenance"
39
+ healthcare_screening = "shape_behavior.behaviors:HealthcareScreening"
40
+ event_sequence = "shape_behavior.behaviors:EventSequence"
41
+ telemetry_series = "shape_behavior.behaviors:TelemetrySeries"
42
+ transaction_stream = "shape_behavior.behaviors:TransactionStream"
43
+ file_arrival = "shape_behavior.behaviors:FileArrival"
44
+ entity_lifecycle = "shape_behavior.behaviors:EntityLifecycle"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ shape_behavior = ["examples/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ """Shape plugin: behavior models (state machines on a virtual clock).
2
+
3
+ The public names are importable from here, lazily: importing the package loads no module of
4
+ this plugin beyond this file, and none of NumPy or Arrow. See ``docs/plugins/behavior.md``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from typing import Any
11
+
12
+ SHAPE_API = "1.0"
13
+
14
+ _EXPORTS = {
15
+ "Module": "shape_behavior.model",
16
+ "ModuleError": "shape_behavior.model",
17
+ "load_module": "shape_behavior.model",
18
+ "Population": "shape_behavior.population",
19
+ "SimConfig": "shape_behavior.simulator",
20
+ "Simulator": "shape_behavior.simulator",
21
+ "Checkpoint": "shape_behavior.simulator",
22
+ "EVENT_SCHEMA": "shape_behavior.events",
23
+ "import_gmf": "shape_behavior.gmf",
24
+ "ImportResult": "shape_behavior.gmf",
25
+ "Unsupported": "shape_behavior.gmf",
26
+ "UnsupportedGmfError": "shape_behavior.gmf",
27
+ "register_state_type": "shape_behavior.extension",
28
+ "StateHandler": "shape_behavior.extension",
29
+ "StateContext": "shape_behavior.extension",
30
+ "Emission": "shape_behavior.extension",
31
+ }
32
+
33
+ __all__ = sorted(_EXPORTS)
34
+
35
+
36
+ def __getattr__(name: str) -> Any:
37
+ module = _EXPORTS.get(name)
38
+ if module is None:
39
+ raise AttributeError(f"module 'shape_behavior' has no attribute {name!r}")
40
+ return getattr(importlib.import_module(module), name)
@@ -0,0 +1,150 @@
1
+ """Behavior modules as ``shape.behaviors`` plugins (plugin API v1, ``Behavior`` protocol).
2
+
3
+ A :class:`ModuleBehavior` wraps one :class:`~shape_behavior.model.Module` so the engine, the
4
+ plugin host, ``shape plugins list`` and the conformance kit can all reach it by name::
5
+
6
+ from shape_behavior.behaviors import behavior
7
+
8
+ b = behavior({"format": "shape-behavior/1", "name": "my_process", "states": {...}})
9
+ events = b.simulate(population=1000, seed=7, years=3) # a pyarrow.Table
10
+
11
+ The built-in examples register under ``shape.behaviors`` through the classes at the bottom of
12
+ this file; a third-party package registers its own module the same way
13
+ (``docs/plugins/behavior.md``, section 10).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from shape_behavior.extension import get_handler
21
+ from shape_behavior.model import EVENT_KINDS, Module, load_module
22
+
23
+ SHAPE_API = "1.0"
24
+ VERSION = "1.0"
25
+
26
+ DEFAULT_POPULATION: dict[str, Any] = {
27
+ "age_at_start": {"kind": "uniform", "low": 0, "high": 90},
28
+ "attributes": {"gender": {"kind": "categorical", "values": {"F": 0.5, "M": 0.5}}},
29
+ }
30
+
31
+
32
+ def population_settings(modules: list[Module]) -> dict[str, Any]:
33
+ """The population settings the modules ask for (``population_defaults``; the first module
34
+ wins on a clash), or :data:`DEFAULT_POPULATION` when none asks."""
35
+ import copy
36
+
37
+ spec: dict[str, Any] = {}
38
+ for m in reversed(modules):
39
+ spec.update(m.doc.get("population_defaults", {}))
40
+ return spec or copy.deepcopy(DEFAULT_POPULATION)
41
+
42
+
43
+ class ModuleBehavior:
44
+ """One module as a ``shape.behaviors`` plugin: name, version, states, attributes, events."""
45
+
46
+ def __init__(self, module: Module | dict[str, Any] | str, version: str = VERSION) -> None:
47
+ self.module = load_module(module)
48
+ self.name: str = self.module.name
49
+ self.version: str = str(self.module.doc.get("version", version))
50
+ self.states: list[str] = list(self.module.states)
51
+ self.attributes: list[str] = sorted(self.module.attribute_uses())
52
+ self.events: list[str] = _event_kinds(self.module)
53
+
54
+ def simulate(self, population: int, seed: int, years: float) -> Any:
55
+ """Events of ``population`` entities over ``years`` from 2020-01-01, deterministic per
56
+ ``seed`` (an Arrow table in :data:`shape_behavior.EVENT_SCHEMA`)."""
57
+ from shape_behavior.population import Population
58
+ from shape_behavior.simulator import SimConfig, Simulator
59
+ from shape_behavior.timeutil import add_years, to_us
60
+
61
+ start = "2020-01-01"
62
+ pop = Population.from_dict(population_settings([self.module]), size=population, start=start)
63
+ sim = Simulator([self.module], pop, SimConfig(seed=seed))
64
+ return sim.run_until(add_years(to_us(start), years))
65
+
66
+
67
+ def behavior(module: Module | dict[str, Any] | str, version: str = VERSION) -> ModuleBehavior:
68
+ """A :class:`ModuleBehavior` for a module, a module document or a module file path."""
69
+ return ModuleBehavior(module, version)
70
+
71
+
72
+ def _event_kinds(module: Module) -> list[str]:
73
+ kinds: set[str] = set()
74
+ for state in module.states.values():
75
+ kind = state["type"]
76
+ if kind == "event":
77
+ kinds.add(str(state["event"]))
78
+ elif kind in EVENT_KINDS:
79
+ kinds.add(EVENT_KINDS[kind])
80
+ else: # a registered state type may declare the kinds it emits (optional `kinds`)
81
+ declared = getattr(get_handler(kind), "kinds", None)
82
+ if declared is not None:
83
+ kinds.update(declared(state))
84
+ return sorted(kinds)
85
+
86
+
87
+ class Subscription(ModuleBehavior):
88
+ """Subscription lifecycle: free trial, conversion, monthly payments, pause and cancel."""
89
+
90
+ def __init__(self) -> None:
91
+ super().__init__("subscription")
92
+
93
+
94
+ class EquipmentMaintenance(ModuleBehavior):
95
+ """Equipment maintenance: run, degrade, fail, repair, with scheduled inspections."""
96
+
97
+ def __init__(self) -> None:
98
+ super().__init__("equipment_maintenance")
99
+
100
+
101
+ class HealthcareScreening(ModuleBehavior):
102
+ """A tiny healthcare example (screening, diagnosis, treatment), written from scratch."""
103
+
104
+ def __init__(self) -> None:
105
+ super().__init__("healthcare_screening")
106
+
107
+
108
+ class EventSequence(ModuleBehavior):
109
+ """An ordered funnel of named events with a continue probability per step (defaults)."""
110
+
111
+ def __init__(self) -> None:
112
+ from shape_behavior.primitives import event_sequence
113
+
114
+ super().__init__(event_sequence())
115
+
116
+
117
+ class TelemetrySeries(ModuleBehavior):
118
+ """A regular reading per device with drift, noise, skipped and stuck readings (defaults)."""
119
+
120
+ def __init__(self) -> None:
121
+ from shape_behavior.primitives import telemetry_series
122
+
123
+ super().__init__(telemetry_series())
124
+
125
+
126
+ class TransactionStream(ModuleBehavior):
127
+ """Transactions as a Poisson process per entity, with refunds and reversals (defaults)."""
128
+
129
+ def __init__(self) -> None:
130
+ from shape_behavior.primitives import transaction_stream
131
+
132
+ super().__init__(transaction_stream())
133
+
134
+
135
+ class FileArrival(ModuleBehavior):
136
+ """A feed with an expected arrival per schedule slot: on time, late, missing, duplicated."""
137
+
138
+ def __init__(self) -> None:
139
+ from shape_behavior.primitives import file_arrival
140
+
141
+ super().__init__(file_arrival())
142
+
143
+
144
+ class EntityLifecycle(ModuleBehavior):
145
+ """Create, update and delete events per entity (change-data-capture style)."""
146
+
147
+ def __init__(self) -> None:
148
+ from shape_behavior.primitives import entity_lifecycle
149
+
150
+ super().__init__(entity_lifecycle())
@@ -0,0 +1,240 @@
1
+ """``shape behave``: run, check and import behavior modules.
2
+
3
+ Reference: ``docs/plugins/behavior.md``, section 7.
4
+
5
+ ::
6
+
7
+ shape behave run subscription --population 10000 --years 3 --seed 7 -o out/
8
+ shape behave check my_module.json
9
+ shape behave import-gmf downloaded_module.json -o converted.json
10
+ shape behave examples -o modules/
11
+
12
+ Nothing heavy loads at import time: NumPy, Arrow and the simulator load when a command runs.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from shape_behavior.params import (
24
+ PRIMITIVE_NAMES,
25
+ PrimitiveParamsError,
26
+ read_params,
27
+ unknown_primitive,
28
+ )
29
+
30
+ SHAPE_API = "1.0"
31
+
32
+
33
+ class BehaveCommand:
34
+ """``shape behave COMMAND ...``."""
35
+
36
+ name = "behave"
37
+ help = "run state-machine behavior modules on a virtual clock (events as Parquet)"
38
+
39
+ def configure(self, parser: Any) -> None:
40
+ sub = parser.add_subparsers(dest="behave_cmd", required=True, metavar="COMMAND")
41
+ run = sub.add_parser("run", help="simulate a population and write the event stream")
42
+ run.add_argument(
43
+ "modules",
44
+ nargs="+",
45
+ metavar="MODULES",
46
+ help="module files (native or GMF JSON) or example names",
47
+ )
48
+ run.add_argument(
49
+ "--population", type=int, required=True, metavar="N", help="number of entities"
50
+ )
51
+ run.add_argument(
52
+ "--years", type=float, required=True, metavar="Y", help="years to simulate"
53
+ )
54
+ run.add_argument("--seed", type=int, default=0, help="random seed (default 0)")
55
+ run.add_argument("-o", "--out", required=True, metavar="OUT", help="output directory")
56
+ run.add_argument(
57
+ "--start", default="2020-01-01", help="virtual clock start (default 2020-01-01)"
58
+ )
59
+ run.add_argument(
60
+ "--population-spec",
61
+ metavar="FILE",
62
+ help="JSON population settings (attributes, age_at_start, arrival, lifetime)",
63
+ )
64
+ run.add_argument(
65
+ "--window-years", type=float, default=1.0, help="years per output file (default 1)"
66
+ )
67
+ run.add_argument(
68
+ "--poll", default="7 days", help="guard polling interval (default '7 days')"
69
+ )
70
+ run.add_argument("--strict", action="store_true", help="fail on unsupported GMF elements")
71
+ run.add_argument(
72
+ "--params",
73
+ metavar="FILE.json",
74
+ help="parameters of a primitive named in MODULES "
75
+ '({"format": "shape-behavior-params", "version": 1, "primitive": NAME, "params": {}})',
76
+ )
77
+ run.set_defaults(fn=_run)
78
+ check = sub.add_parser("check", help="validate modules; exit 1 when one is invalid")
79
+ check.add_argument("modules", nargs="+", metavar="MODULES")
80
+ check.add_argument("--strict", action="store_true", help="fail on unsupported GMF elements")
81
+ check.set_defaults(fn=_check)
82
+ imp = sub.add_parser(
83
+ "import-gmf", help="convert a GMF module and report what is unsupported"
84
+ )
85
+ imp.add_argument("file", help="the GMF JSON file you downloaded")
86
+ imp.add_argument("-o", "--out", metavar="OUT.json", help="write the native module here")
87
+ imp.add_argument("--strict", action="store_true", help="fail on unsupported elements")
88
+ imp.set_defaults(fn=_import)
89
+ ex = sub.add_parser("examples", help="list the built-in example modules, or write them")
90
+ ex.add_argument("-o", "--out", metavar="DIR", help="write the examples into DIR")
91
+ ex.set_defaults(fn=_examples)
92
+
93
+ def run(self, args: Any) -> int:
94
+ try:
95
+ return int(args.fn(args))
96
+ except (FileNotFoundError, json.JSONDecodeError, PrimitiveParamsError) as exc:
97
+ print(f"shape behave: {exc}", file=sys.stderr)
98
+ return 2
99
+ except ValueError as exc: # includes ModuleError, UnsupportedGmfError
100
+ print(f"shape behave: {exc}", file=sys.stderr)
101
+ return 1
102
+
103
+
104
+ def _load(sources: list[str], strict: bool, params: str | None = None) -> list[Any]:
105
+ from shape_behavior.model import load_module
106
+
107
+ primitive, values = read_params(Path(params)) if params else ("", {})
108
+ if params and primitive not in sources:
109
+ if primitive not in PRIMITIVE_NAMES:
110
+ raise unknown_primitive(primitive)
111
+ raise PrimitiveParamsError(
112
+ f"--params is for primitive {primitive!r}, which is not among the modules to run "
113
+ f"({', '.join(sources)})",
114
+ "primitive",
115
+ )
116
+ if params:
117
+ from shape_behavior.primitives import build
118
+ modules = []
119
+ for src in sources:
120
+ module = (
121
+ build(primitive, values)
122
+ if params and src == primitive
123
+ else load_module(src, strict=strict)
124
+ )
125
+ report = module.import_report
126
+ if report is not None and (report.unsupported or report.warnings):
127
+ print(f"{src}:\n{report.report()}", file=sys.stderr)
128
+ modules.append(module)
129
+ return modules
130
+
131
+
132
+ def _run(args: Any) -> int:
133
+ import pyarrow.parquet as pq # type: ignore[import-untyped]
134
+
135
+ from shape_behavior.behaviors import population_settings
136
+ from shape_behavior.population import Population
137
+ from shape_behavior.simulator import SimConfig, Simulator
138
+ from shape_behavior.timeutil import add_years, to_us
139
+
140
+ if args.population < 0 or args.years <= 0 or args.window_years <= 0:
141
+ print(
142
+ "shape behave: --population must be >= 0 and --years and --window-years > 0",
143
+ file=sys.stderr,
144
+ )
145
+ return 2
146
+ modules = _load(args.modules, args.strict, args.params)
147
+ if args.population_spec:
148
+ spec = json.loads(Path(args.population_spec).read_text(encoding="utf-8"))
149
+ else:
150
+ spec = population_settings(modules)
151
+ population = Population.from_dict(spec, size=args.population, start=args.start)
152
+ sim = Simulator(modules, population, SimConfig(seed=args.seed, poll=args.poll))
153
+ out = Path(args.out)
154
+ (out / "events").mkdir(parents=True, exist_ok=True)
155
+ start = to_us(args.start)
156
+ end = add_years(start, args.years)
157
+ started = time.perf_counter()
158
+ windows: list[dict[str, Any]] = []
159
+ cursor, part = start, 0
160
+ while cursor < end:
161
+ nxt = min(add_years(start, args.window_years * (part + 1)), end)
162
+ events = sim.run_until(nxt)
163
+ pq.write_table(events, out / "events" / f"part-{part:04d}.parquet", compression="snappy")
164
+ windows.append(
165
+ {
166
+ "file": f"events/part-{part:04d}.parquet",
167
+ "until": str(sim.now),
168
+ "rows": events.num_rows,
169
+ }
170
+ )
171
+ cursor, part = nxt, part + 1
172
+ pq.write_table(sim.entities(), out / "entities.parquet", compression="snappy")
173
+ elapsed = time.perf_counter() - started
174
+ total = sum(w["rows"] for w in windows)
175
+ manifest = {
176
+ "seed": args.seed,
177
+ "years": args.years,
178
+ "start": args.start,
179
+ "population": population.to_dict(),
180
+ "modules": [_module_entry(m, s) for m, s in zip(modules, args.modules, strict=True)],
181
+ "events": total,
182
+ "windows": windows,
183
+ "seconds": round(elapsed, 3),
184
+ "unsupported": [
185
+ str(u) for m in modules if m.import_report for u in m.import_report.unsupported
186
+ ],
187
+ }
188
+ (out / "run.json").write_text(
189
+ json.dumps(manifest, indent=2) + "\n", encoding="utf-8", newline="\n"
190
+ )
191
+ print(f"{total:,} events for {args.population:,} entities in {elapsed:.1f}s -> {out}")
192
+ return 0
193
+
194
+
195
+ def _module_entry(module: Any, source: str) -> dict[str, Any]:
196
+ entry: dict[str, Any] = {"name": module.name, "digest": module.digest(), "source": source}
197
+ if "primitive" in module.doc: # a primitive: record the parameters it ran with
198
+ entry["primitive"] = module.doc["primitive"]
199
+ entry["parameters"] = module.doc["parameters"]
200
+ return entry
201
+
202
+
203
+ def _check(args: Any) -> int:
204
+ modules = _load(args.modules, args.strict)
205
+ for m in modules:
206
+ print(f"ok {m.name} ({len(m.states)} states)")
207
+ return 0
208
+
209
+
210
+ def _import(args: Any) -> int:
211
+ from shape_behavior.gmf import import_gmf
212
+
213
+ result = import_gmf(Path(args.file), strict=args.strict)
214
+ print(result.report())
215
+ if args.out:
216
+ Path(args.out).write_text(
217
+ json.dumps(result.module.to_dict(), indent=2) + "\n", encoding="utf-8", newline="\n"
218
+ )
219
+ print(f"wrote {args.out}")
220
+ return 0
221
+
222
+
223
+ def _examples(args: Any) -> int:
224
+ import importlib.resources
225
+
226
+ from shape_behavior.model import examples
227
+
228
+ names = examples()
229
+ if not args.out:
230
+ print("\n".join(names))
231
+ return 0
232
+ out = Path(args.out)
233
+ out.mkdir(parents=True, exist_ok=True)
234
+ for name in names:
235
+ text = (
236
+ importlib.resources.files("shape_behavior") / "examples" / f"{name}.json"
237
+ ).read_text(encoding="utf-8")
238
+ (out / f"{name}.json").write_text(text, encoding="utf-8", newline="\n")
239
+ print(f"wrote {len(names)} modules to {out}")
240
+ return 0