abmfact 0.1.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.
- abmfact/__init__.py +37 -0
- abmfact/__main__.py +3 -0
- abmfact/adapters.py +364 -0
- abmfact/audit.py +442 -0
- abmfact/checks.py +88 -0
- abmfact/claims.py +195 -0
- abmfact/cli.py +169 -0
- abmfact/config.py +194 -0
- abmfact/demo.py +68 -0
- abmfact/diagnose.py +88 -0
- abmfact/knobs.py +118 -0
- abmfact/py.typed +0 -0
- abmfact/report.py +325 -0
- abmfact/stats.py +189 -0
- abmfact/zoo.py +304 -0
- abmfact-0.1.0.dist-info/METADATA +244 -0
- abmfact-0.1.0.dist-info/RECORD +21 -0
- abmfact-0.1.0.dist-info/WHEEL +4 -0
- abmfact-0.1.0.dist-info/entry_points.txt +2 -0
- abmfact-0.1.0.dist-info/licenses/LICENSE +202 -0
- abmfact-0.1.0.dist-info/licenses/NOTICE +4 -0
abmfact/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""ABMfact: is it a finding or an artefact?
|
|
2
|
+
|
|
3
|
+
Audit the conclusions of an agent-based model by rerunning it under changes that a correct
|
|
4
|
+
implementation must not care about, and report which conclusions survive.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .adapters import Adapter, AdapterError, CommandAdapter, MesaAdapter, NetLogoAdapter, PythonAdapter
|
|
12
|
+
from .audit import AuditResult, run_audit
|
|
13
|
+
from .claims import Claim, ClaimError, Metric
|
|
14
|
+
from .config import Audit, Budget, ConfigError, load_audit, parse_audit
|
|
15
|
+
from .knobs import Knobs, Support
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Adapter",
|
|
19
|
+
"AdapterError",
|
|
20
|
+
"Audit",
|
|
21
|
+
"AuditResult",
|
|
22
|
+
"Budget",
|
|
23
|
+
"Claim",
|
|
24
|
+
"ClaimError",
|
|
25
|
+
"CommandAdapter",
|
|
26
|
+
"ConfigError",
|
|
27
|
+
"Knobs",
|
|
28
|
+
"MesaAdapter",
|
|
29
|
+
"Metric",
|
|
30
|
+
"NetLogoAdapter",
|
|
31
|
+
"PythonAdapter",
|
|
32
|
+
"Support",
|
|
33
|
+
"__version__",
|
|
34
|
+
"load_audit",
|
|
35
|
+
"parse_audit",
|
|
36
|
+
"run_audit",
|
|
37
|
+
]
|
abmfact/__main__.py
ADDED
abmfact/adapters.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Adapters turn a model, in whatever framework it is written, into ``run(params, seed, knobs)``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .knobs import Knobs, Support, derived_seed
|
|
16
|
+
|
|
17
|
+
Outputs = dict[str, float | list[float]]
|
|
18
|
+
RunFn = Callable[[Mapping[str, Any], int, Knobs], Mapping[str, Any]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdapterError(RuntimeError):
|
|
22
|
+
"""A model could not be built, run, or returned outputs ABMfact cannot read."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalise_outputs(raw: Mapping[str, Any]) -> Outputs:
|
|
26
|
+
"""Coerce model outputs to floats and lists of floats, rejecting anything else."""
|
|
27
|
+
if not isinstance(raw, Mapping):
|
|
28
|
+
raise AdapterError(f"model must return a mapping of outputs, got {type(raw).__name__}")
|
|
29
|
+
out: Outputs = {}
|
|
30
|
+
for key, value in raw.items():
|
|
31
|
+
if isinstance(value, int | float):
|
|
32
|
+
out[str(key)] = float(value)
|
|
33
|
+
elif hasattr(value, "tolist") and not isinstance(value, str):
|
|
34
|
+
v = value.tolist()
|
|
35
|
+
out[str(key)] = [float(x) for x in v] if isinstance(v, list) else float(v)
|
|
36
|
+
elif isinstance(value, Sequence) and not isinstance(value, str):
|
|
37
|
+
out[str(key)] = [float(x) for x in value]
|
|
38
|
+
else:
|
|
39
|
+
raise AdapterError(f"output {key!r} must be a number or a sequence of numbers")
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Adapter:
|
|
44
|
+
"""Base class. Subclasses implement :meth:`_run`.
|
|
45
|
+
|
|
46
|
+
The base class implements ``rng_streams="independent"`` for models that do not manage their own
|
|
47
|
+
streams, by remapping the seed through an independent derivation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str = "model"
|
|
51
|
+
support: Support = Support()
|
|
52
|
+
|
|
53
|
+
def run(self, params: Mapping[str, Any], seed: int, knobs: Knobs) -> Outputs:
|
|
54
|
+
if knobs.rng_streams == "independent" and not self.support.rng_streams:
|
|
55
|
+
seed = derived_seed(seed)
|
|
56
|
+
knobs = knobs.with_(rng_streams="default")
|
|
57
|
+
return normalise_outputs(self._run(dict(params), int(seed), knobs))
|
|
58
|
+
|
|
59
|
+
def _run(self, params: dict[str, Any], seed: int, knobs: Knobs) -> Mapping[str, Any]:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
def describe(self) -> dict[str, Any]:
|
|
63
|
+
return {"adapter": type(self).__name__, "name": self.name, "support": self.support.to_dict()}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PythonAdapter(Adapter):
|
|
67
|
+
"""Wrap a plain Python callable ``fn(params, seed, knobs) -> dict``."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, fn: RunFn, supports: Mapping[str, Any] | Support | None = None, name: str | None = None):
|
|
70
|
+
self.fn = fn
|
|
71
|
+
self.support = supports if isinstance(supports, Support) else Support.from_mapping(supports)
|
|
72
|
+
self.name = str(name or getattr(fn, "__qualname__", "python-model"))
|
|
73
|
+
|
|
74
|
+
def _run(self, params: dict[str, Any], seed: int, knobs: Knobs) -> Mapping[str, Any]:
|
|
75
|
+
return self.fn(params, seed, knobs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MesaAdapter(Adapter):
|
|
79
|
+
"""Run a Mesa 3 model class.
|
|
80
|
+
|
|
81
|
+
``outputs`` maps output names to either a callable taking the model, or the string
|
|
82
|
+
``"datacollector:<column>"`` for a model-level reporter series. ``knob_kwargs`` maps knob names
|
|
83
|
+
(``update_order``, ``agent_order``, ``rng_streams``, ``grid_offset``, ``grid_rotation``,
|
|
84
|
+
``dt_scale``, ``pop_scale``) to constructor keyword names. A knob is supported exactly when it is
|
|
85
|
+
mapped.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
model_cls: Any,
|
|
91
|
+
steps: int,
|
|
92
|
+
outputs: Mapping[str, Any],
|
|
93
|
+
knob_kwargs: Mapping[str, str] | None = None,
|
|
94
|
+
update_orders: Sequence[str] = (),
|
|
95
|
+
update_order_default: str | None = None,
|
|
96
|
+
seed_kwarg: str = "rng",
|
|
97
|
+
name: str | None = None,
|
|
98
|
+
):
|
|
99
|
+
self.model_cls = model_cls
|
|
100
|
+
self.steps = int(steps)
|
|
101
|
+
self.outputs = dict(outputs)
|
|
102
|
+
self.knob_kwargs = dict(knob_kwargs or {})
|
|
103
|
+
self.seed_kwarg = seed_kwarg
|
|
104
|
+
self.name = str(name or getattr(model_cls, "__name__", "mesa-model"))
|
|
105
|
+
kk = self.knob_kwargs
|
|
106
|
+
self.support = Support(
|
|
107
|
+
update_order=tuple(update_orders) if "update_order" in kk else (),
|
|
108
|
+
update_order_default=update_order_default or (update_orders[0] if update_orders else None),
|
|
109
|
+
agent_order="agent_order" in kk,
|
|
110
|
+
rng_streams="rng_streams" in kk,
|
|
111
|
+
grid="grid_offset" in kk and "grid_rotation" in kk,
|
|
112
|
+
dt_scale="dt_scale" in kk,
|
|
113
|
+
pop_scale="pop_scale" in kk,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _run(self, params: dict[str, Any], seed: int, knobs: Knobs) -> Mapping[str, Any]:
|
|
117
|
+
kwargs = dict(params)
|
|
118
|
+
kwargs[self.seed_kwarg] = seed
|
|
119
|
+
kd = knobs.to_dict()
|
|
120
|
+
for knob, kw in self.knob_kwargs.items():
|
|
121
|
+
if knob == "agent_order":
|
|
122
|
+
kwargs[kw] = knobs.agent_perm_seed if knobs.agent_order == "shuffled" else None
|
|
123
|
+
elif knob == "update_order":
|
|
124
|
+
kwargs[kw] = knobs.update_order or self.support.update_order_default
|
|
125
|
+
else:
|
|
126
|
+
kwargs[kw] = kd[knob]
|
|
127
|
+
try:
|
|
128
|
+
model = self.model_cls(**kwargs)
|
|
129
|
+
except TypeError as exc:
|
|
130
|
+
raise AdapterError(f"could not build {self.name}: {exc}") from exc
|
|
131
|
+
n = max(1, round(self.steps / knobs.dt_scale)) if self.support.dt_scale else self.steps
|
|
132
|
+
for _ in range(n):
|
|
133
|
+
model.step()
|
|
134
|
+
result: dict[str, Any] = {}
|
|
135
|
+
for out_name, spec in self.outputs.items():
|
|
136
|
+
if callable(spec):
|
|
137
|
+
result[out_name] = spec(model)
|
|
138
|
+
elif isinstance(spec, str) and spec.startswith("datacollector:"):
|
|
139
|
+
col = spec.split(":", 1)[1]
|
|
140
|
+
df = model.datacollector.get_model_vars_dataframe()
|
|
141
|
+
result[out_name] = [float(x) for x in df[col].tolist()]
|
|
142
|
+
else:
|
|
143
|
+
raise AdapterError(f"output spec for {out_name!r} must be callable or 'datacollector:<col>'")
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
_NETLOGO_LINKS: dict[tuple[str, str | None], Any] = {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _default_netlogo_link(model_path: str, netlogo_home: str | None) -> Any:
|
|
151
|
+
try:
|
|
152
|
+
import pynetlogo
|
|
153
|
+
except ImportError as exc: # pragma: no cover - depends on optional install
|
|
154
|
+
raise AdapterError("NetLogo support needs: pip install 'abmfact[netlogo]' and a NetLogo install") from exc
|
|
155
|
+
link = pynetlogo.NetLogoLink(gui=False, netlogo_home=netlogo_home) # pragma: no cover
|
|
156
|
+
link.load_model(model_path) # pragma: no cover
|
|
157
|
+
return link # pragma: no cover
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _nl_literal(value: Any) -> str:
|
|
161
|
+
if isinstance(value, bool):
|
|
162
|
+
return "true" if value else "false"
|
|
163
|
+
if isinstance(value, int | float):
|
|
164
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
165
|
+
raise AdapterError("NetLogo globals must be finite numbers")
|
|
166
|
+
return repr(value)
|
|
167
|
+
if isinstance(value, list | tuple):
|
|
168
|
+
return "[" + " ".join(_nl_literal(v) for v in value) + "]"
|
|
169
|
+
return json.dumps(str(value))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class NetLogoAdapter(Adapter):
|
|
173
|
+
"""Run a NetLogo model through pynetlogo.
|
|
174
|
+
|
|
175
|
+
``reporters`` maps output names to NetLogo reporter strings. A reporter whose name ends with
|
|
176
|
+
``[]`` is collected every tick as a series, otherwise once at the end. ``knob_globals`` maps knob
|
|
177
|
+
names to NetLogo global variable names. One workspace is kept per process and model file.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
model_path: str | os.PathLike[str],
|
|
183
|
+
reporters: Mapping[str, str],
|
|
184
|
+
ticks: int,
|
|
185
|
+
knob_globals: Mapping[str, str] | None = None,
|
|
186
|
+
update_orders: Sequence[str] = (),
|
|
187
|
+
update_order_default: str | None = None,
|
|
188
|
+
setup: str = "setup",
|
|
189
|
+
go: str = "go",
|
|
190
|
+
netlogo_home: str | None = None,
|
|
191
|
+
link_factory: Callable[[str, str | None], Any] | None = None,
|
|
192
|
+
name: str | None = None,
|
|
193
|
+
):
|
|
194
|
+
self.model_path = str(model_path)
|
|
195
|
+
self.reporters = dict(reporters)
|
|
196
|
+
self.ticks = int(ticks)
|
|
197
|
+
self.knob_globals = dict(knob_globals or {})
|
|
198
|
+
self.setup_cmd, self.go_cmd = setup, go
|
|
199
|
+
self.netlogo_home = netlogo_home
|
|
200
|
+
self.link_factory = link_factory or _default_netlogo_link
|
|
201
|
+
self.name = name or Path(self.model_path).stem
|
|
202
|
+
kg = self.knob_globals
|
|
203
|
+
self.support = Support(
|
|
204
|
+
update_order=tuple(update_orders) if "update_order" in kg else (),
|
|
205
|
+
update_order_default=update_order_default or (update_orders[0] if update_orders else None),
|
|
206
|
+
agent_order="agent_order" in kg,
|
|
207
|
+
rng_streams="rng_streams" in kg,
|
|
208
|
+
grid="grid_offset" in kg and "grid_rotation" in kg,
|
|
209
|
+
dt_scale="dt_scale" in kg,
|
|
210
|
+
pop_scale="pop_scale" in kg,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _link(self) -> Any:
|
|
214
|
+
key = (self.model_path, self.netlogo_home)
|
|
215
|
+
if key not in _NETLOGO_LINKS:
|
|
216
|
+
_NETLOGO_LINKS[key] = self.link_factory(self.model_path, self.netlogo_home)
|
|
217
|
+
return _NETLOGO_LINKS[key]
|
|
218
|
+
|
|
219
|
+
def _run(self, params: dict[str, Any], seed: int, knobs: Knobs) -> Mapping[str, Any]:
|
|
220
|
+
link = self._link()
|
|
221
|
+
for name, param in params.items():
|
|
222
|
+
link.command(f"set {name} {_nl_literal(param)}")
|
|
223
|
+
kd = knobs.to_dict()
|
|
224
|
+
for knob, glob in self.knob_globals.items():
|
|
225
|
+
if knob == "update_order":
|
|
226
|
+
value: Any = knobs.update_order or self.support.update_order_default
|
|
227
|
+
elif knob == "agent_order":
|
|
228
|
+
value = knobs.agent_perm_seed if knobs.agent_order == "shuffled" else -1
|
|
229
|
+
else:
|
|
230
|
+
value = kd[knob]
|
|
231
|
+
link.command(f"set {glob} {_nl_literal(value)}")
|
|
232
|
+
link.command(f"random-seed {int(seed) % 2147483647}")
|
|
233
|
+
link.command(self.setup_cmd)
|
|
234
|
+
ticks = max(1, round(self.ticks / knobs.dt_scale)) if self.support.dt_scale else self.ticks
|
|
235
|
+
series = {k: v for k, v in self.reporters.items() if k.endswith("[]")}
|
|
236
|
+
finals = {k: v for k, v in self.reporters.items() if not k.endswith("[]")}
|
|
237
|
+
result: dict[str, Any] = {}
|
|
238
|
+
if series:
|
|
239
|
+
collected: dict[str, list[float]] = {k: [] for k in series}
|
|
240
|
+
for _ in range(ticks):
|
|
241
|
+
link.command(self.go_cmd)
|
|
242
|
+
for k, rep in series.items():
|
|
243
|
+
collected[k].append(float(link.report(rep)))
|
|
244
|
+
for k, vals in collected.items():
|
|
245
|
+
result[k[:-2]] = vals
|
|
246
|
+
else:
|
|
247
|
+
link.command(f"repeat {ticks} [ {self.go_cmd} ]")
|
|
248
|
+
for k, rep in finals.items():
|
|
249
|
+
result[k] = float(link.report(rep))
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class CommandAdapter(Adapter):
|
|
254
|
+
"""Run any program. It reads ``{"params", "seed", "knobs"}`` as JSON on stdin and prints a JSON object."""
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
argv: Sequence[str],
|
|
259
|
+
supports: Mapping[str, Any] | None = None,
|
|
260
|
+
timeout: float = 600.0,
|
|
261
|
+
cwd: str | None = None,
|
|
262
|
+
name: str | None = None,
|
|
263
|
+
):
|
|
264
|
+
if not argv:
|
|
265
|
+
raise AdapterError("command adapter needs a non-empty argv")
|
|
266
|
+
self.argv = [sys.executable if a == "{python}" else str(a) for a in argv]
|
|
267
|
+
self.support = Support.from_mapping(supports)
|
|
268
|
+
self.timeout = float(timeout)
|
|
269
|
+
self.cwd = cwd
|
|
270
|
+
self.name = name or Path(self.argv[-1]).name
|
|
271
|
+
|
|
272
|
+
def _run(self, params: dict[str, Any], seed: int, knobs: Knobs) -> Mapping[str, Any]:
|
|
273
|
+
payload = json.dumps({"params": params, "seed": seed, "knobs": knobs.to_dict()})
|
|
274
|
+
try:
|
|
275
|
+
proc = subprocess.run( # noqa: S603 - argv comes from the user's own audit file
|
|
276
|
+
self.argv,
|
|
277
|
+
input=payload,
|
|
278
|
+
capture_output=True,
|
|
279
|
+
text=True,
|
|
280
|
+
timeout=self.timeout,
|
|
281
|
+
cwd=self.cwd,
|
|
282
|
+
check=False,
|
|
283
|
+
)
|
|
284
|
+
except subprocess.TimeoutExpired as exc:
|
|
285
|
+
raise AdapterError(f"{self.name} timed out after {self.timeout:g}s (seed {seed})") from exc
|
|
286
|
+
if proc.returncode != 0:
|
|
287
|
+
tail = proc.stderr.strip().splitlines()[-5:]
|
|
288
|
+
raise AdapterError(f"{self.name} exited with {proc.returncode} (seed {seed}): " + " | ".join(tail))
|
|
289
|
+
try:
|
|
290
|
+
data = json.loads(proc.stdout)
|
|
291
|
+
except json.JSONDecodeError as exc:
|
|
292
|
+
raise AdapterError(f"{self.name} did not print a JSON object (seed {seed})") from exc
|
|
293
|
+
if not isinstance(data, dict):
|
|
294
|
+
raise AdapterError(f"{self.name} must print a JSON object, got {type(data).__name__}")
|
|
295
|
+
return data
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def import_target(target: str) -> Any:
|
|
299
|
+
"""Import ``package.module:attribute``."""
|
|
300
|
+
if ":" not in target:
|
|
301
|
+
raise AdapterError(f"target must look like 'package.module:attribute', got {target!r}")
|
|
302
|
+
mod_name, _, attr = target.partition(":")
|
|
303
|
+
try:
|
|
304
|
+
module = importlib.import_module(mod_name)
|
|
305
|
+
except ImportError as exc:
|
|
306
|
+
raise AdapterError(f"cannot import {mod_name!r}: {exc}") from exc
|
|
307
|
+
obj: Any = module
|
|
308
|
+
for part in attr.split("."):
|
|
309
|
+
try:
|
|
310
|
+
obj = getattr(obj, part)
|
|
311
|
+
except AttributeError as exc:
|
|
312
|
+
raise AdapterError(f"{mod_name!r} has no attribute {attr!r}") from exc
|
|
313
|
+
return obj
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def load_adapter(cfg: Mapping[str, Any], base_dir: Path | None = None) -> Adapter:
|
|
317
|
+
"""Build an adapter from the ``model:`` section of an audit file."""
|
|
318
|
+
kind = str(cfg.get("adapter", "python"))
|
|
319
|
+
base = base_dir or Path.cwd()
|
|
320
|
+
if base_dir is not None and str(base_dir) not in sys.path:
|
|
321
|
+
sys.path.insert(0, str(base_dir))
|
|
322
|
+
if kind == "python":
|
|
323
|
+
obj = import_target(str(cfg["target"]))
|
|
324
|
+
if isinstance(obj, Adapter):
|
|
325
|
+
return obj
|
|
326
|
+
if callable(obj):
|
|
327
|
+
return PythonAdapter(obj, supports=cfg.get("supports"), name=str(cfg["target"]))
|
|
328
|
+
raise AdapterError(f"{cfg['target']} is neither an Adapter nor a callable")
|
|
329
|
+
if kind == "mesa":
|
|
330
|
+
uo = cfg.get("update_orders", [])
|
|
331
|
+
return MesaAdapter(
|
|
332
|
+
import_target(str(cfg["target"])),
|
|
333
|
+
steps=int(cfg["steps"]),
|
|
334
|
+
outputs={
|
|
335
|
+
k: (import_target(v) if ":" in v and not v.startswith("datacollector:") else v)
|
|
336
|
+
for k, v in dict(cfg["outputs"]).items()
|
|
337
|
+
},
|
|
338
|
+
knob_kwargs=cfg.get("knob_kwargs"),
|
|
339
|
+
update_orders=uo,
|
|
340
|
+
update_order_default=cfg.get("update_order_default"),
|
|
341
|
+
seed_kwarg=str(cfg.get("seed_kwarg", "rng")),
|
|
342
|
+
name=str(cfg["target"]),
|
|
343
|
+
)
|
|
344
|
+
if kind == "netlogo":
|
|
345
|
+
path = Path(str(cfg["path"]))
|
|
346
|
+
return NetLogoAdapter(
|
|
347
|
+
path if path.is_absolute() else base / path,
|
|
348
|
+
reporters=dict(cfg["reporters"]),
|
|
349
|
+
ticks=int(cfg["ticks"]),
|
|
350
|
+
knob_globals=cfg.get("knob_globals"),
|
|
351
|
+
update_orders=cfg.get("update_orders", []),
|
|
352
|
+
update_order_default=cfg.get("update_order_default"),
|
|
353
|
+
setup=str(cfg.get("setup", "setup")),
|
|
354
|
+
go=str(cfg.get("go", "go")),
|
|
355
|
+
netlogo_home=cfg.get("netlogo_home"),
|
|
356
|
+
)
|
|
357
|
+
if kind == "command":
|
|
358
|
+
return CommandAdapter(
|
|
359
|
+
list(cfg["argv"]),
|
|
360
|
+
supports=cfg.get("supports"),
|
|
361
|
+
timeout=float(cfg.get("timeout", 600)),
|
|
362
|
+
cwd=str(base),
|
|
363
|
+
)
|
|
364
|
+
raise AdapterError(f"unknown adapter {kind!r}; expected python, mesa, netlogo or command")
|