process-bigraph 1.8.5__tar.gz → 1.9.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.
- {process_bigraph-1.8.5/process_bigraph.egg-info → process_bigraph-1.9.0}/PKG-INFO +6 -4
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/README.md +4 -2
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/__init__.py +17 -1
- process_bigraph-1.9.0/process_bigraph/audit_contracts.py +128 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/composite.py +204 -6
- process_bigraph-1.9.0/process_bigraph/composite_audit.py +193 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/composite_spec.py +77 -0
- process_bigraph-1.9.0/process_bigraph/contract_drift.py +253 -0
- process_bigraph-1.9.0/process_bigraph/contract_strict.py +176 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/events.py +40 -5
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/scheduling.py +85 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/types/process.py +32 -4
- {process_bigraph-1.8.5 → process_bigraph-1.9.0/process_bigraph.egg-info}/PKG-INFO +6 -4
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph.egg-info/SOURCES.txt +5 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph.egg-info/requires.txt +1 -1
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/pyproject.toml +3 -2
- process_bigraph-1.9.0/tests/test_multirate_scheduling.py +70 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/AUTHORS.md +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/LICENSE +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/artifacts.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/composite_discovery.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/composite_generator.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/config_helpers.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/core_introspection.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/draft_process.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/emitter.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/nextflow.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/nextflow_deploy.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/bigraphical_reactive_system.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/cached.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/processes/simulation.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/_git_worker.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/clusters/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/clusters/ec2_ssm.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/git.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/pool.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/ray.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/protocols/session.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/run.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/run_composite.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/server/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/server/rest.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/server/start.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/templates.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualization.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/distribution.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/heatmap.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/param_vs_observable.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/phase_space.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/time_series.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/visualizations/timeseries_from_observables.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/wiring_translator.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/workflow/__init__.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/workflow/backend.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/workflow/provision.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/workflow/recipe.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph/workflow/tasks.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/setup.cfg +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/tests/test_composite_generator_analyses.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/tests/test_process_decorator.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/tests/test_reaction_step.py +0 -0
- {process_bigraph-1.8.5 → process_bigraph-1.9.0}/tests/test_units.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
License-File: AUTHORS.md
|
|
9
|
-
Requires-Dist: bigraph-schema>=1.
|
|
9
|
+
Requires-Dist: bigraph-schema>=1.8.1
|
|
10
10
|
Requires-Dist: requests>=2.31
|
|
11
11
|
Requires-Dist: matplotlib
|
|
12
12
|
Requires-Dist: pint>=0.24.4
|
|
@@ -119,7 +119,8 @@ core.register_link('Grow', Grow) # register it in the local
|
|
|
119
119
|
composite = Composite({'state': {
|
|
120
120
|
'level': 1.0, # a shared store
|
|
121
121
|
'grow': {'_type': 'process', 'address': 'local:Grow', 'config': {'rate': 0.5},
|
|
122
|
-
'interval': 1.0,
|
|
122
|
+
'interval': 1.0, # seconds; a unit string like '1 min' also works
|
|
123
|
+
'inputs': {'level': ['level']}, 'outputs': {'level': ['level']}},
|
|
123
124
|
'emitter': emitter_from_wires({'level': ['level'], 'time': ['global_time']}),
|
|
124
125
|
}}, core=core)
|
|
125
126
|
composite.run(5.0)
|
|
@@ -234,7 +235,8 @@ rendered to HTML and published automatically on GitHub Pages.
|
|
|
234
235
|
|
|
235
236
|
- **Tutorial 3 — Declarative Math**
|
|
236
237
|
*Defining mathematical relationships, signal pipelines, and events using `MathExpressionStep`*
|
|
237
|
-
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
238
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
239
|
+
Requires the companion package [`viva-basic-processes`](https://github.com/vivarium-collective/viva-basic-processes), published to PyPI as `pbg-basic-processes` (`pip install "pbg-basic-processes[plotting]>=0.3.0"`), which provides `MathExpressionStep`, the `Tick` process, and the plotting helpers.
|
|
238
240
|
|
|
239
241
|
- **Tutorial 4 — Composing a Biological Model**
|
|
240
242
|
*The central dogma from four small processes: composition over shared molecular state, biological units in the port types, and adding regulation by adding a wire*
|
|
@@ -92,7 +92,8 @@ core.register_link('Grow', Grow) # register it in the local
|
|
|
92
92
|
composite = Composite({'state': {
|
|
93
93
|
'level': 1.0, # a shared store
|
|
94
94
|
'grow': {'_type': 'process', 'address': 'local:Grow', 'config': {'rate': 0.5},
|
|
95
|
-
'interval': 1.0,
|
|
95
|
+
'interval': 1.0, # seconds; a unit string like '1 min' also works
|
|
96
|
+
'inputs': {'level': ['level']}, 'outputs': {'level': ['level']}},
|
|
96
97
|
'emitter': emitter_from_wires({'level': ['level'], 'time': ['global_time']}),
|
|
97
98
|
}}, core=core)
|
|
98
99
|
composite.run(5.0)
|
|
@@ -207,7 +208,8 @@ rendered to HTML and published automatically on GitHub Pages.
|
|
|
207
208
|
|
|
208
209
|
- **Tutorial 3 — Declarative Math**
|
|
209
210
|
*Defining mathematical relationships, signal pipelines, and events using `MathExpressionStep`*
|
|
210
|
-
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
211
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
212
|
+
Requires the companion package [`viva-basic-processes`](https://github.com/vivarium-collective/viva-basic-processes), published to PyPI as `pbg-basic-processes` (`pip install "pbg-basic-processes[plotting]>=0.3.0"`), which provides `MathExpressionStep`, the `Tick` process, and the plotting helpers.
|
|
211
213
|
|
|
212
214
|
- **Tutorial 4 — Composing a Biological Model**
|
|
213
215
|
*The central dogma from four small processes: composition over shared molecular state, biological units in the port types, and adding regulation by adding a wire*
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
from bigraph_schema import allocate_core
|
|
1
|
+
from bigraph_schema import allocate_core as _allocate_core
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def allocate_core(*args, strict=False, **kwargs):
|
|
5
|
+
"""Allocate a core (see :func:`bigraph_schema.allocate_core`).
|
|
6
|
+
|
|
7
|
+
Adds process-bigraph's opt-in strict-config mode (issue #232): with
|
|
8
|
+
``strict=True`` the returned core carries ``strict_config = True``, so
|
|
9
|
+
instantiating a Process/Step with a config key that is not declared in
|
|
10
|
+
its ``config_schema`` raises a clear error instead of silently ignoring
|
|
11
|
+
the key. The default (``strict=False``) preserves existing behavior
|
|
12
|
+
exactly. You can also toggle it on an existing core with
|
|
13
|
+
``core.strict_config = True``.
|
|
14
|
+
"""
|
|
15
|
+
core = _allocate_core(*args, **kwargs)
|
|
16
|
+
core.strict_config = bool(strict)
|
|
17
|
+
return core
|
|
2
18
|
|
|
3
19
|
# The installed distribution's version, exposed so a run manifest can record
|
|
4
20
|
# *which* engine produced an artifact. Read from installed metadata rather
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""CI audit gate for declared process contracts (spec section 5).
|
|
2
|
+
|
|
3
|
+
Audits every registered process's DECLARED contract (static — no instantiation)
|
|
4
|
+
and fails (exit 1) on any error-severity finding, or on a declared-coverage
|
|
5
|
+
floor. Read the class `.contract` directly (contract_of drops amendments). In
|
|
6
|
+
process-bigraph's own CI few processes declare contracts, so the error-gate is
|
|
7
|
+
largely a regression guard; the real value is downstream workspaces running
|
|
8
|
+
`python -m process_bigraph.audit_contracts` against their registries, and the
|
|
9
|
+
--require-declared floor makes coverage visible.
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import json as _json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from bigraph_schema.contract_audit import audit_contract, completeness
|
|
17
|
+
from bigraph_schema.assembly import contract_of
|
|
18
|
+
from bigraph_schema.contract import resolve_contract
|
|
19
|
+
AUDIT_AVAILABLE = True
|
|
20
|
+
except Exception: # noqa: BLE001
|
|
21
|
+
AUDIT_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
from process_bigraph import allocate_core
|
|
24
|
+
|
|
25
|
+
_INCOMPLETE_BELOW = 0.5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _class_key(cls):
|
|
29
|
+
return f'{getattr(cls, "__module__", "?")}.{getattr(cls, "__qualname__", getattr(cls, "__name__", "?"))}'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _contract_for(core, address, cls):
|
|
33
|
+
"""Declared contract of a class, amendments preserved. Reads the class
|
|
34
|
+
`.contract` first (contract_of drops amendments), then falls back."""
|
|
35
|
+
declared = getattr(cls, 'contract', None)
|
|
36
|
+
if declared is not None:
|
|
37
|
+
try:
|
|
38
|
+
return resolve_contract(cls) # normalizes ProcessContract/dict; never raises
|
|
39
|
+
except Exception: # noqa: BLE001
|
|
40
|
+
pass
|
|
41
|
+
try:
|
|
42
|
+
return contract_of(core, address)
|
|
43
|
+
except Exception: # noqa: BLE001
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def audit_all(core, *, require_declared=0, strict_resolve=False):
|
|
48
|
+
from process_bigraph.core_introspection import list_processes
|
|
49
|
+
reports = {}
|
|
50
|
+
for address in list_processes(core):
|
|
51
|
+
try:
|
|
52
|
+
cls = core.link_registry.get(address)
|
|
53
|
+
except Exception: # noqa: BLE001 - per-entry import failure
|
|
54
|
+
cls = None
|
|
55
|
+
if cls is None:
|
|
56
|
+
reports.setdefault(f'unresolvable:{address}',
|
|
57
|
+
{'address': address, 'cls': address, 'status': 'unresolvable',
|
|
58
|
+
'grade': None, 'findings': [], 'declared': False})
|
|
59
|
+
continue
|
|
60
|
+
key = _class_key(cls)
|
|
61
|
+
if key in reports:
|
|
62
|
+
continue # alias dedup
|
|
63
|
+
contract = _contract_for(core, address, cls)
|
|
64
|
+
if contract is None:
|
|
65
|
+
reports[key] = {'address': address, 'cls': key, 'status': 'not-declared', 'grade': None, 'findings': [], 'declared': False}
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
report = audit_contract(core, contract)
|
|
69
|
+
grade = completeness(contract)
|
|
70
|
+
findings = [{'severity': f.severity, 'code': f.code, 'where': f.where, 'message': f.message}
|
|
71
|
+
for f in report.findings]
|
|
72
|
+
except Exception as error: # noqa: BLE001
|
|
73
|
+
reports[key] = {'address': address, 'cls': key, 'status': 'unresolvable', 'grade': None,
|
|
74
|
+
'findings': [{'severity': 'error', 'code': 'audit_raised', 'where': key, 'message': str(error)}], 'declared': False}
|
|
75
|
+
continue
|
|
76
|
+
has_contract_attr = getattr(cls, 'contract', None) is not None
|
|
77
|
+
is_declared = has_contract_attr or bool(contract.conditions())
|
|
78
|
+
if any(f['severity'] == 'error' for f in findings):
|
|
79
|
+
status = 'fail'
|
|
80
|
+
elif not is_declared:
|
|
81
|
+
status = 'not-declared'
|
|
82
|
+
elif grade < _INCOMPLETE_BELOW:
|
|
83
|
+
status = 'incomplete'
|
|
84
|
+
else:
|
|
85
|
+
status = 'pass'
|
|
86
|
+
reports[key] = {'address': address, 'cls': key, 'status': status, 'grade': round(grade, 3), 'findings': findings, 'declared': is_declared}
|
|
87
|
+
|
|
88
|
+
rows = list(reports.values())
|
|
89
|
+
errors = sum(1 for r in rows for f in r['findings'] if f['severity'] == 'error')
|
|
90
|
+
if strict_resolve:
|
|
91
|
+
errors += sum(1 for r in rows if r['status'] == 'unresolvable')
|
|
92
|
+
declared_count = sum(1 for r in rows if r.get('declared'))
|
|
93
|
+
exit_code = 1 if (errors > 0 or (require_declared > 0 and declared_count < require_declared)) else 0
|
|
94
|
+
return {'reports': rows, 'declared': declared_count, 'errors': errors, 'exit_code': exit_code}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_parser():
|
|
98
|
+
p = argparse.ArgumentParser(prog='python -m process_bigraph.audit_contracts',
|
|
99
|
+
description='Audit registered process contracts; exit 1 on any error finding.')
|
|
100
|
+
p.add_argument('--require-declared', type=int, default=0, metavar='N',
|
|
101
|
+
help='fail if fewer than N processes declare a real contract (default 0 = off).')
|
|
102
|
+
p.add_argument('--strict-resolve', action='store_true', help='treat unresolvable processes as failures.')
|
|
103
|
+
p.add_argument('--json', action='store_true', help='emit the full report as JSON.')
|
|
104
|
+
return p
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv=None):
|
|
108
|
+
args = _build_parser().parse_args(argv)
|
|
109
|
+
if not AUDIT_AVAILABLE:
|
|
110
|
+
print('contract audit unavailable: bigraph-schema >= 1.7.0 required', file=sys.stderr)
|
|
111
|
+
return 2
|
|
112
|
+
core = allocate_core()
|
|
113
|
+
result = audit_all(core, require_declared=args.require_declared, strict_resolve=args.strict_resolve)
|
|
114
|
+
if args.json:
|
|
115
|
+
print(_json.dumps(result, indent=2))
|
|
116
|
+
return result['exit_code']
|
|
117
|
+
for r in sorted(result['reports'], key=lambda x: (x['status'] != 'fail', x['cls'])):
|
|
118
|
+
grade = '' if r['grade'] is None else f" {int(r['grade'] * 100)}%"
|
|
119
|
+
print(f"{r['status']:13} {r['cls']}{grade}")
|
|
120
|
+
for f in r['findings']:
|
|
121
|
+
print(f" {f['severity']} {f['code']} {f['where']}: {f['message']}")
|
|
122
|
+
print(f"\n{result['declared']} declared, {result['errors']} error finding(s) "
|
|
123
|
+
f"-> {'FAIL' if result['exit_code'] else 'OK'}")
|
|
124
|
+
return result['exit_code']
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
sys.exit(main())
|
|
@@ -76,6 +76,7 @@ from process_bigraph.scheduling import ( # noqa: F401
|
|
|
76
76
|
build_trigger_state,
|
|
77
77
|
find_downstream,
|
|
78
78
|
determine_steps,
|
|
79
|
+
find_step_cycle,
|
|
79
80
|
)
|
|
80
81
|
|
|
81
82
|
|
|
@@ -312,6 +313,36 @@ def interval_time_precision(timestep: float) -> int:
|
|
|
312
313
|
# Process Classes
|
|
313
314
|
# ===============
|
|
314
315
|
|
|
316
|
+
def validate_config_keys(instance, config, core):
|
|
317
|
+
"""Reject config keys not declared in ``config_schema`` when strict mode is on.
|
|
318
|
+
|
|
319
|
+
Opt-in "strict config" mode (issue #232). By default a Process/Step
|
|
320
|
+
silently ignores config keys that aren't part of its ``config_schema``,
|
|
321
|
+
so a typo'd or unsupported key passes unnoticed. When the core carries
|
|
322
|
+
``strict_config`` truthy (set it directly or via
|
|
323
|
+
:func:`process_bigraph.allocate_core(strict=True)`), any such undeclared
|
|
324
|
+
key raises a clear :class:`ValueError` naming the offending key(s) and the
|
|
325
|
+
process type.
|
|
326
|
+
|
|
327
|
+
This is a no-op when ``strict_config`` is falsy/absent on the core, so the
|
|
328
|
+
default behavior of every existing workspace is unchanged.
|
|
329
|
+
"""
|
|
330
|
+
if config is None or core is None:
|
|
331
|
+
return
|
|
332
|
+
if not getattr(core, 'strict_config', False):
|
|
333
|
+
return
|
|
334
|
+
declared = set((getattr(instance, 'config_schema', None) or {}).keys())
|
|
335
|
+
unknown = [key for key in config if key not in declared]
|
|
336
|
+
if unknown:
|
|
337
|
+
type_name = type(instance).__name__
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"{type_name} received unknown config key(s) {sorted(unknown)!r} "
|
|
340
|
+
f"that are not declared in its config_schema "
|
|
341
|
+
f"(declared keys: {sorted(declared)!r}). Strict-config mode is on "
|
|
342
|
+
f"(core.strict_config is set / allocate_core(strict=True)); pass "
|
|
343
|
+
f"only declared keys, fix the typo, or disable strict mode.")
|
|
344
|
+
|
|
345
|
+
|
|
315
346
|
class SyncUpdate:
|
|
316
347
|
"""
|
|
317
348
|
Wrapper for synchronous process updates.
|
|
@@ -348,6 +379,11 @@ class Open(Edge):
|
|
|
348
379
|
self._pending_command: Optional[
|
|
349
380
|
Tuple[str, Optional[tuple], Optional[dict]]] = None
|
|
350
381
|
|
|
382
|
+
# Opt-in strict-config check (issue #232): reject undeclared config
|
|
383
|
+
# keys before the edge fills/initializes. No-op unless the core has
|
|
384
|
+
# strict_config set, so default behavior is unchanged.
|
|
385
|
+
validate_config_keys(self, config, core)
|
|
386
|
+
|
|
351
387
|
super().__init__(config, core=core)
|
|
352
388
|
|
|
353
389
|
def pre_send_command(
|
|
@@ -1148,6 +1184,17 @@ def _project_process_update(update_results: Any, args: Tuple[Any, str, Any]) ->
|
|
|
1148
1184
|
to the previous per-call closure.
|
|
1149
1185
|
"""
|
|
1150
1186
|
composite, ports_key, process_path = args
|
|
1187
|
+
strict = getattr(composite, '_contract_strict', 'off')
|
|
1188
|
+
if strict != 'off':
|
|
1189
|
+
# v1: only a dict delta is post-checked; list-form (multi-update) is left unchecked.
|
|
1190
|
+
compiled = composite._compiled_contracts.get(process_path)
|
|
1191
|
+
if compiled is not None and isinstance(update_results, dict):
|
|
1192
|
+
from process_bigraph.contract_strict import check_post, handle_violations
|
|
1193
|
+
inputs = composite._cached_view(process_path)
|
|
1194
|
+
fails = check_post(compiled, inputs, update_results)
|
|
1195
|
+
handle_violations(fails, mode=strict, phase='post', path=process_path,
|
|
1196
|
+
cls='process', emitter=composite._em,
|
|
1197
|
+
global_time=composite.state.get('global_time', 0.0) if isinstance(composite.state, dict) else 0.0)
|
|
1151
1198
|
if not isinstance(update_results, list):
|
|
1152
1199
|
update_results = [update_results]
|
|
1153
1200
|
return [
|
|
@@ -1215,6 +1262,8 @@ class Composite(Process):
|
|
|
1215
1262
|
# step hot path releases the GIL (numpy / scipy / numba / C
|
|
1216
1263
|
# extensions: yes; pure-Python loops: no).
|
|
1217
1264
|
'parallel_steps': 'boolean{false}',
|
|
1265
|
+
# Runtime contract checking: off (default, zero-cost) | record | raise.
|
|
1266
|
+
'contract_strict': 'string{off}',
|
|
1218
1267
|
# Time-scheduled processes: when True, the per-tick `run` loop
|
|
1219
1268
|
# fans out invocations across a thread pool. Right tool when
|
|
1220
1269
|
# processes' update() releases the GIL (network I/O, numpy /
|
|
@@ -1279,6 +1328,16 @@ class Composite(Process):
|
|
|
1279
1328
|
if 'global_time' not in initial_state:
|
|
1280
1329
|
initial_state['global_time'] = 0.0
|
|
1281
1330
|
|
|
1331
|
+
# Reject open template holes BEFORE realizing, over the document as
|
|
1332
|
+
# given. Realization cannot cope with a site where it expects a type
|
|
1333
|
+
# or an address: a declared `schema` entry silently swallows a `state`
|
|
1334
|
+
# site (it realizes the declared type and drops the hole, so the
|
|
1335
|
+
# composite runs on the type default with no error — case A), and an
|
|
1336
|
+
# open `address` site crashes deep in `realize_link` with an opaque
|
|
1337
|
+
# `KeyError: 'data'` (case B). Checking the raw document first turns
|
|
1338
|
+
# both into the same clear "not ground" error that names the site.
|
|
1339
|
+
self._require_ground_input(initial_schema, initial_state)
|
|
1340
|
+
|
|
1282
1341
|
# Generate internal schema and state structures using the core engine.
|
|
1283
1342
|
# Top-level realize: realize_root defaults to () so no merges escape.
|
|
1284
1343
|
self.schema, self.state, _ = self.core.realize(
|
|
@@ -1423,6 +1482,8 @@ class Composite(Process):
|
|
|
1423
1482
|
# field in interface_schema for the rationale (threading vs.
|
|
1424
1483
|
# multiprocessing for inner parallelism).
|
|
1425
1484
|
self._parallel_steps = bool(self.config.get('parallel_steps', False))
|
|
1485
|
+
self._contract_strict = self.config.get('contract_strict') or getattr(self.core, 'contract_strict', 'off')
|
|
1486
|
+
self._compiled_contracts = {}
|
|
1426
1487
|
self._parallel_processes = bool(self.config.get('parallel_processes', False))
|
|
1427
1488
|
self._parallel_workers = self.config.get('parallel_workers')
|
|
1428
1489
|
self._step_executor = None # lazy: created on first run_steps need
|
|
@@ -1916,16 +1977,27 @@ class Composite(Process):
|
|
|
1916
1977
|
path for any path not present.
|
|
1917
1978
|
"""
|
|
1918
1979
|
self._compiled_links = {}
|
|
1980
|
+
self._compiled_contracts = {}
|
|
1919
1981
|
|
|
1920
1982
|
for path in list(self.process_paths) + list(self.step_paths):
|
|
1921
1983
|
compiled = self.core.precompile_link(
|
|
1922
1984
|
self.schema, self.state, path)
|
|
1923
1985
|
if compiled is not None:
|
|
1924
1986
|
self._compiled_links[path] = compiled
|
|
1987
|
+
if self._contract_strict != 'off':
|
|
1988
|
+
try:
|
|
1989
|
+
from process_bigraph.contract_strict import compile_contract
|
|
1990
|
+
node = self.state
|
|
1991
|
+
for key in path:
|
|
1992
|
+
node = node[key]
|
|
1993
|
+
self._compiled_contracts[path] = compile_contract(node['instance'].describe_contract())
|
|
1994
|
+
except Exception: # noqa: BLE001 - a process without a readable contract is simply unchecked
|
|
1995
|
+
self._compiled_contracts[path] = None
|
|
1925
1996
|
|
|
1926
1997
|
def _invalidate_caches(self) -> None:
|
|
1927
1998
|
"""Invalidate precompiled link caches, forcing rebuild on next use."""
|
|
1928
1999
|
self._compiled_links = {}
|
|
2000
|
+
self._compiled_contracts = {}
|
|
1929
2001
|
self._runtime_partition_cache = None
|
|
1930
2002
|
|
|
1931
2003
|
def _cached_view(self, path: Tuple[str, ...]) -> Dict[str, Any]:
|
|
@@ -2312,6 +2384,29 @@ class Composite(Process):
|
|
|
2312
2384
|
# Build the step execution dependency graph
|
|
2313
2385
|
self.step_dependencies, self.node_dependencies = build_step_network(self.step_paths)
|
|
2314
2386
|
|
|
2387
|
+
# Reject dependency cycles among zero-time steps. The paper
|
|
2388
|
+
# (Supplement 1, 3.8.4) guarantees the step network is a DAG:
|
|
2389
|
+
# "Cycles are disallowed, ensuring deterministic zero-time DAG
|
|
2390
|
+
# evaluation." A cycle has no valid evaluation order, so the
|
|
2391
|
+
# scheduler would otherwise fall back to a hash-seed-dependent
|
|
2392
|
+
# tie-break and the same document would give different results in
|
|
2393
|
+
# different processes (issue #233).
|
|
2394
|
+
cycle = find_step_cycle(self.step_dependencies, self.node_dependencies)
|
|
2395
|
+
if cycle is not None:
|
|
2396
|
+
def _name(step_key):
|
|
2397
|
+
if isinstance(step_key, (tuple, list)):
|
|
2398
|
+
return '/'.join(str(part) for part in step_key)
|
|
2399
|
+
return str(step_key)
|
|
2400
|
+
rendered = ' -> '.join(_name(step_key) for step_key in cycle)
|
|
2401
|
+
raise ValueError(
|
|
2402
|
+
f'step dependency cycle detected: {rendered}. '
|
|
2403
|
+
f'Steps evaluate in zero time as a DAG, so a cycle has no '
|
|
2404
|
+
f'deterministic evaluation order (process-bigraph paper, '
|
|
2405
|
+
f'Supplement 1, 3.8.4). Break the cycle by removing one of '
|
|
2406
|
+
f'the mutual input/output wirings between these steps, or '
|
|
2407
|
+
f'model the feedback as a Process with a non-zero time '
|
|
2408
|
+
f'interval.')
|
|
2409
|
+
|
|
2315
2410
|
# Initialize trigger fulfillment state and steps remaining
|
|
2316
2411
|
self.reset_step_state(self.step_paths)
|
|
2317
2412
|
|
|
@@ -2569,6 +2664,27 @@ class Composite(Process):
|
|
|
2569
2664
|
incrementally based on their configured interval. Updates are applied and
|
|
2570
2665
|
steps are triggered accordingly.
|
|
2571
2666
|
|
|
2667
|
+
Update-timing contract (matters only when intervals are unequal)
|
|
2668
|
+
----------------------------------------------------------------
|
|
2669
|
+
A process reads its input ports from the shared state **at the start of
|
|
2670
|
+
its interval** (the moment it becomes due), computes its update right
|
|
2671
|
+
then, and the delta is applied when ``global_time`` reaches
|
|
2672
|
+
``due_time + interval`` — the "stash" model (Supplement 1, §3.8.6).
|
|
2673
|
+
This is **not** the idealized read-at-event-time ``t*`` rule of
|
|
2674
|
+
Supplement 1, §3.8.2: the two agree when every process shares one
|
|
2675
|
+
interval and diverge otherwise, because a slow process applies a delta
|
|
2676
|
+
computed from inputs as they stood one interval earlier. See
|
|
2677
|
+
:meth:`run_process` and ``docs/concepts/composites-and-templates.md``
|
|
2678
|
+
("Update timing with unequal intervals").
|
|
2679
|
+
|
|
2680
|
+
Chunking caveat: the read-at-start guarantee holds only for a process
|
|
2681
|
+
whose whole next interval fits inside this ``run`` call. A process whose
|
|
2682
|
+
next due time falls past ``global_time + interval`` is deferred to the
|
|
2683
|
+
next ``run`` call and re-reads its inputs there, so ``run(T)`` once can
|
|
2684
|
+
differ from several shorter ``run`` calls over the same span. For
|
|
2685
|
+
chunking-invariant results, pass an ``interval`` at least as large as the
|
|
2686
|
+
largest process interval, or keep intervals equal.
|
|
2687
|
+
|
|
2572
2688
|
After completion, ``self.process_update_time`` holds the cumulative time
|
|
2573
2689
|
spent inside process ``invoke()`` calls, and ``self.framework_time`` holds
|
|
2574
2690
|
the time spent in framework operations (view, project, apply, realize).
|
|
@@ -2783,6 +2899,64 @@ class Composite(Process):
|
|
|
2783
2899
|
per_process=dict(self._per_process_time),
|
|
2784
2900
|
)
|
|
2785
2901
|
|
|
2902
|
+
@staticmethod
|
|
2903
|
+
def _raise_not_ground(unfilled) -> None:
|
|
2904
|
+
"""Raise the shared "not ground" error naming the open site paths."""
|
|
2905
|
+
named = ', '.join(repr('/'.join(path)) for path in unfilled)
|
|
2906
|
+
raise ValueError(
|
|
2907
|
+
f'composite document is not ground — required site(s) left '
|
|
2908
|
+
f'unfilled: {named}. An open site is a hole where a process '
|
|
2909
|
+
f'or value should be; fill it before constructing the '
|
|
2910
|
+
f'composite (bigraph_schema `fill_sites` / `build`).')
|
|
2911
|
+
|
|
2912
|
+
def _require_ground_input(
|
|
2913
|
+
self,
|
|
2914
|
+
initial_schema: Dict[str, Any],
|
|
2915
|
+
initial_state: Dict[str, Any],
|
|
2916
|
+
) -> None:
|
|
2917
|
+
"""Reject unfilled template holes in the raw document, before realizing.
|
|
2918
|
+
|
|
2919
|
+
The post-realize :meth:`_require_ground_document` check sees only the
|
|
2920
|
+
sites realization *kept*, which is too late for two cases:
|
|
2921
|
+
|
|
2922
|
+
- **A — an open site silently swallowed.** When ``schema`` declares a
|
|
2923
|
+
store's type, ``realize`` takes the type from ``schema`` and drops
|
|
2924
|
+
the ``site`` in ``state``; the realized schema no longer contains a
|
|
2925
|
+
hole, so the post-check passes and the composite runs on the type's
|
|
2926
|
+
default value with no error — a template that was never filled runs
|
|
2927
|
+
as if it had been.
|
|
2928
|
+
- **B — an open ``address`` site.** A process/step node whose
|
|
2929
|
+
``address`` is still a site cannot be realized at all: ``realize``
|
|
2930
|
+
fails inside ``realize_link`` with an opaque ``KeyError: 'data'``
|
|
2931
|
+
before the groundness check can report the real problem.
|
|
2932
|
+
|
|
2933
|
+
Checking the document *as given* — the ``state`` as the caller wrote
|
|
2934
|
+
it, where sites still live — catches both and names the offending
|
|
2935
|
+
site, exactly as the baseline (state-only) case already does. The
|
|
2936
|
+
post-realize check stays as a backstop.
|
|
2937
|
+
|
|
2938
|
+
Checked over **sites** (a ``_default``-carrying site is optional and
|
|
2939
|
+
does not block), consistent with :meth:`_require_ground_document`; see
|
|
2940
|
+
:func:`process_bigraph.templates.required_open_sites`.
|
|
2941
|
+
"""
|
|
2942
|
+
try:
|
|
2943
|
+
from bigraph_schema.assembly import interfaces
|
|
2944
|
+
# A site is a state-level hole: inspect the document as written,
|
|
2945
|
+
# so a declared `schema` type cannot hide a `state` site.
|
|
2946
|
+
accessed = self.core.access(initial_state)
|
|
2947
|
+
unfilled = sorted({
|
|
2948
|
+
tuple(path)
|
|
2949
|
+
for path, site in interfaces(accessed)[0]._places
|
|
2950
|
+
if getattr(site, '_default', None) is None})
|
|
2951
|
+
except Exception:
|
|
2952
|
+
# If the raw document cannot be inspected for any reason, do not
|
|
2953
|
+
# mask that behind this check — fall through to realization and
|
|
2954
|
+
# the post-realize backstop, preserving prior behaviour.
|
|
2955
|
+
return
|
|
2956
|
+
|
|
2957
|
+
if unfilled:
|
|
2958
|
+
self._raise_not_ground(unfilled)
|
|
2959
|
+
|
|
2786
2960
|
def _require_ground_document(self) -> None:
|
|
2787
2961
|
"""Reject a document that still has unfilled template holes.
|
|
2788
2962
|
|
|
@@ -2792,6 +2966,10 @@ class Composite(Process):
|
|
|
2792
2966
|
Checking here turns what would be a confusing failure deep in the run
|
|
2793
2967
|
into a clear one at construction.
|
|
2794
2968
|
|
|
2969
|
+
This runs *after* realization as a backstop; the primary check is
|
|
2970
|
+
:meth:`_require_ground_input`, which inspects the raw document before
|
|
2971
|
+
realization can swallow or crash on an open site.
|
|
2972
|
+
|
|
2795
2973
|
Deliberately checked over **sites**, not ``is_ground``: any composite
|
|
2796
2974
|
containing a process has unwired ports by construction, so
|
|
2797
2975
|
``is_ground`` is never true for a real composite and would reject
|
|
@@ -2808,12 +2986,7 @@ class Composite(Process):
|
|
|
2808
2986
|
if getattr(site, '_default', None) is None})
|
|
2809
2987
|
|
|
2810
2988
|
if unfilled:
|
|
2811
|
-
|
|
2812
|
-
raise ValueError(
|
|
2813
|
-
f'composite document is not ground — required site(s) left '
|
|
2814
|
-
f'unfilled: {named}. An open site is a hole where a process '
|
|
2815
|
-
f'or value should be; fill it before constructing the '
|
|
2816
|
-
f'composite (bigraph_schema `fill_sites` / `build`).')
|
|
2989
|
+
self._raise_not_ground(unfilled)
|
|
2817
2990
|
|
|
2818
2991
|
def _require_advancing_interval(
|
|
2819
2992
|
self,
|
|
@@ -2870,6 +3043,23 @@ class Composite(Process):
|
|
|
2870
3043
|
This updates the `self.front` to store when the process is due next,
|
|
2871
3044
|
and captures its update as a deferred computation.
|
|
2872
3045
|
|
|
3046
|
+
Read-at-start contract: when the process is due
|
|
3047
|
+
(``process_time <= global_time``), its input state is sliced **now**
|
|
3048
|
+
via ``_cached_view(path)`` and ``process_update`` is called immediately,
|
|
3049
|
+
but the resulting ``Defer`` is stashed in ``self.front[path]['update']``
|
|
3050
|
+
and only applied once ``global_time`` reaches ``future`` (= due time +
|
|
3051
|
+
interval). So inputs are read at the **start** of the interval and the
|
|
3052
|
+
delta lands at the **end**. With unequal intervals this differs from the
|
|
3053
|
+
read-at-event-time ``t*`` rule of Supplement 1 §3.8.2; it is the §3.8.6
|
|
3054
|
+
stash model. See :meth:`run` and the concepts doc for the full contract.
|
|
3055
|
+
|
|
3056
|
+
Note: the ``'future'`` branch below pops a per-process state stash that
|
|
3057
|
+
§3.8.6 uses to keep a long interval chunking-invariant across successive
|
|
3058
|
+
``run`` calls. Nothing currently writes that key, so a process whose next
|
|
3059
|
+
interval overruns the current ``run`` window is simply deferred and
|
|
3060
|
+
re-reads its inputs on the next ``run`` call (see the chunking caveat on
|
|
3061
|
+
:meth:`run`).
|
|
3062
|
+
|
|
2873
3063
|
Args:
|
|
2874
3064
|
path: The path to the process in the state/schema tree.
|
|
2875
3065
|
process: The dictionary representing the process (must contain 'interval').
|
|
@@ -3077,6 +3267,14 @@ class Composite(Process):
|
|
|
3077
3267
|
# summary of the state it was handed (process_bigraph.events), then
|
|
3078
3268
|
# re-raise the ORIGINAL exception unchanged -- callers may classify
|
|
3079
3269
|
# control-flow exceptions by type, so the type must propagate as is.
|
|
3270
|
+
if self._contract_strict != 'off':
|
|
3271
|
+
compiled = self._compiled_contracts.get(path)
|
|
3272
|
+
if compiled is not None:
|
|
3273
|
+
from process_bigraph.contract_strict import check_pre, handle_violations
|
|
3274
|
+
fails = check_pre(compiled, clean_state)
|
|
3275
|
+
handle_violations(fails, mode=self._contract_strict, phase='pre', path=path,
|
|
3276
|
+
cls=type(process['instance']).__name__, emitter=self._em,
|
|
3277
|
+
global_time=self.state.get('global_time', 0.0) if isinstance(self.state, dict) else 0.0)
|
|
3080
3278
|
t0 = _time.monotonic()
|
|
3081
3279
|
try:
|
|
3082
3280
|
update = process['instance'].invoke(clean_state, interval)
|