process-bigraph 1.9.0__tar.gz → 1.10.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.9.0/process_bigraph.egg-info → process_bigraph-1.10.0}/PKG-INFO +36 -20
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/README.md +35 -19
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/__init__.py +39 -1
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/composite.py +136 -122
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/composite_discovery.py +10 -2
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/composite_generator.py +37 -51
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/composite_spec.py +24 -1
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/draft_process.py +52 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/experiments/minimal_gillespie.py +1 -54
- process_bigraph-1.10.0/process_bigraph/holes.py +168 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/nextflow.py +0 -37
- process_bigraph-1.10.0/process_bigraph/orchestration.py +127 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/examples.py +0 -28
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/ray.py +20 -1
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/scheduling.py +221 -20
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/templates.py +14 -8
- {process_bigraph-1.9.0 → process_bigraph-1.10.0/process_bigraph.egg-info}/PKG-INFO +36 -20
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph.egg-info/SOURCES.txt +2 -1
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/pyproject.toml +1 -1
- process_bigraph-1.9.0/process_bigraph/run.py +0 -29
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/AUTHORS.md +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/LICENSE +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/artifacts.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/audit_contracts.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/composite_audit.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/config_helpers.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/contract_drift.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/contract_strict.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/core_introspection.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/emitter.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/events.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/nextflow_deploy.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/bigraphical_reactive_system.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/cached.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/processes/simulation.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/_git_worker.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/clusters/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/clusters/ec2_ssm.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/git.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/pool.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/protocols/session.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/run_composite.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/server/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/server/rest.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/server/start.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualization.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/distribution.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/heatmap.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/param_vs_observable.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/phase_space.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/time_series.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/visualizations/timeseries_from_observables.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/wiring_translator.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/workflow/__init__.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/workflow/backend.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/workflow/provision.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/workflow/recipe.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph/workflow/tasks.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph.egg-info/requires.txt +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/setup.cfg +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/tests/test_composite_generator_analyses.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/tests/test_multirate_scheduling.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/tests/test_process_decorator.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/tests/test_reaction_step.py +0 -0
- {process_bigraph-1.9.0 → process_bigraph-1.10.0}/tests/test_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -128,28 +128,44 @@ print(gather_emitter_results(composite))
|
|
|
128
128
|
# {('emitter',): [{'level': 1.0, 'time': 0.0}, {'level': 1.5, ...}, ... {'level': 7.59, 'time': 5.0}]}
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
**2. A draft
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
**2. A draft — an open hole whose sort is a contract.** A not-yet-implemented
|
|
132
|
+
process-slot is a *hole*: an open **site** (step 3) whose `_sort` names a
|
|
133
|
+
`ProcessContract` (its typed face + conditions). You design and review the topology
|
|
134
|
+
with the hole in place, then fill it with any process the contract *admits*.
|
|
135
|
+
`Composite` refuses to build while a required hole is open, and reports **which** hole,
|
|
136
|
+
**what** contract it requires, and **which** registered processes would fill it.
|
|
135
137
|
|
|
136
138
|
```python
|
|
137
|
-
from
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
from bigraph_schema.contract import ProcessContract
|
|
140
|
+
from process_bigraph import find_candidates, UnfilledHoleError
|
|
141
|
+
|
|
142
|
+
# register the intended interface as a named contract …
|
|
143
|
+
core.register_contract('PTHSecretion', ProcessContract(
|
|
144
|
+
summary='senses serum Ca, secretes PTH',
|
|
145
|
+
face={'inputs': {'ca_sense': 'float'}, 'outputs': {'pth_out': 'float'}}))
|
|
146
|
+
|
|
147
|
+
# … and leave the slot as a hole sorted by it
|
|
148
|
+
template = core.access({'parathyroid': {'ca': 2.5, 'pth': 0.0,
|
|
149
|
+
'secretion': {'_type': 'site', '_sort': 'PTHSecretion'}}})
|
|
150
|
+
|
|
151
|
+
# Composite(..., unfilled) → UnfilledHoleError, not an opaque KeyError:
|
|
152
|
+
# not ground — required site(s) left unfilled: 'parathyroid/secretion'
|
|
153
|
+
# • parathyroid/secretion
|
|
154
|
+
# requires:
|
|
155
|
+
# inputs: ca_sense: float
|
|
156
|
+
# outputs: pth_out: float
|
|
157
|
+
# candidates:
|
|
158
|
+
# ✓ <any registered process whose contract admits the hole>
|
|
148
159
|
```
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
Fill it (step 3) with a process `find_candidates(core, site)` reports as admissible,
|
|
162
|
+
and build. The full design → inspect → fill → build lifecycle — and the rejection
|
|
163
|
+
report in full — is in
|
|
164
|
+
[docs/concepts/designing-with-holes.md](docs/concepts/designing-with-holes.md).
|
|
165
|
+
|
|
166
|
+
> **Deprecated:** `@draft_process` / `DraftProcess` (an inert placeholder *node*) still
|
|
167
|
+
> work but now emit `DraftProcessDeprecationWarning` — model a draft as a hole instead.
|
|
168
|
+
> `draft_to_contract(cls)` bridges an existing draft's interface to a `ProcessContract`.
|
|
153
169
|
|
|
154
170
|
**3. A template — a composite with an open site (hole).** A template is a document that
|
|
155
171
|
isn't *ground*: it has open **sites** (`{"_type": "site"}`). `Composite` refuses to run
|
|
@@ -184,7 +200,7 @@ def model(rate): # any conforming composite
|
|
|
184
200
|
sim = Composite({'state': template_document(core, template, {'study/model': model(0.5)})}, core=core)
|
|
185
201
|
sim.run(4.0)
|
|
186
202
|
print(sim.state['study']['verdict']) # pass (a slower model → fail)
|
|
187
|
-
# template_document(core, template, {}) →
|
|
203
|
+
# template_document(core, template, {}) → UnfilledHoleError: not ground — required site 'study/model'
|
|
188
204
|
```
|
|
189
205
|
|
|
190
206
|
**4. An investigation — one site per member study; gating is *filling*.** Fill a
|
|
@@ -101,28 +101,44 @@ print(gather_emitter_results(composite))
|
|
|
101
101
|
# {('emitter',): [{'level': 1.0, 'time': 0.0}, {'level': 1.5, ...}, ... {'level': 7.59, 'time': 5.0}]}
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
**2. A draft
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
**2. A draft — an open hole whose sort is a contract.** A not-yet-implemented
|
|
105
|
+
process-slot is a *hole*: an open **site** (step 3) whose `_sort` names a
|
|
106
|
+
`ProcessContract` (its typed face + conditions). You design and review the topology
|
|
107
|
+
with the hole in place, then fill it with any process the contract *admits*.
|
|
108
|
+
`Composite` refuses to build while a required hole is open, and reports **which** hole,
|
|
109
|
+
**what** contract it requires, and **which** registered processes would fill it.
|
|
108
110
|
|
|
109
111
|
```python
|
|
110
|
-
from
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
from bigraph_schema.contract import ProcessContract
|
|
113
|
+
from process_bigraph import find_candidates, UnfilledHoleError
|
|
114
|
+
|
|
115
|
+
# register the intended interface as a named contract …
|
|
116
|
+
core.register_contract('PTHSecretion', ProcessContract(
|
|
117
|
+
summary='senses serum Ca, secretes PTH',
|
|
118
|
+
face={'inputs': {'ca_sense': 'float'}, 'outputs': {'pth_out': 'float'}}))
|
|
119
|
+
|
|
120
|
+
# … and leave the slot as a hole sorted by it
|
|
121
|
+
template = core.access({'parathyroid': {'ca': 2.5, 'pth': 0.0,
|
|
122
|
+
'secretion': {'_type': 'site', '_sort': 'PTHSecretion'}}})
|
|
123
|
+
|
|
124
|
+
# Composite(..., unfilled) → UnfilledHoleError, not an opaque KeyError:
|
|
125
|
+
# not ground — required site(s) left unfilled: 'parathyroid/secretion'
|
|
126
|
+
# • parathyroid/secretion
|
|
127
|
+
# requires:
|
|
128
|
+
# inputs: ca_sense: float
|
|
129
|
+
# outputs: pth_out: float
|
|
130
|
+
# candidates:
|
|
131
|
+
# ✓ <any registered process whose contract admits the hole>
|
|
121
132
|
```
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
Fill it (step 3) with a process `find_candidates(core, site)` reports as admissible,
|
|
135
|
+
and build. The full design → inspect → fill → build lifecycle — and the rejection
|
|
136
|
+
report in full — is in
|
|
137
|
+
[docs/concepts/designing-with-holes.md](docs/concepts/designing-with-holes.md).
|
|
138
|
+
|
|
139
|
+
> **Deprecated:** `@draft_process` / `DraftProcess` (an inert placeholder *node*) still
|
|
140
|
+
> work but now emit `DraftProcessDeprecationWarning` — model a draft as a hole instead.
|
|
141
|
+
> `draft_to_contract(cls)` bridges an existing draft's interface to a `ProcessContract`.
|
|
126
142
|
|
|
127
143
|
**3. A template — a composite with an open site (hole).** A template is a document that
|
|
128
144
|
isn't *ground*: it has open **sites** (`{"_type": "site"}`). `Composite` refuses to run
|
|
@@ -157,7 +173,7 @@ def model(rate): # any conforming composite
|
|
|
157
173
|
sim = Composite({'state': template_document(core, template, {'study/model': model(0.5)})}, core=core)
|
|
158
174
|
sim.run(4.0)
|
|
159
175
|
print(sim.state['study']['verdict']) # pass (a slower model → fail)
|
|
160
|
-
# template_document(core, template, {}) →
|
|
176
|
+
# template_document(core, template, {}) → UnfilledHoleError: not ground — required site 'study/model'
|
|
161
177
|
```
|
|
162
178
|
|
|
163
179
|
**4. An investigation — one site per member study; gating is *filling*.** Fill a
|
|
@@ -14,6 +14,35 @@ def allocate_core(*args, strict=False, **kwargs):
|
|
|
14
14
|
"""
|
|
15
15
|
core = _allocate_core(*args, **kwargs)
|
|
16
16
|
core.strict_config = bool(strict)
|
|
17
|
+
_register_builtin_emitters(core)
|
|
18
|
+
return core
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _register_builtin_emitters(core):
|
|
22
|
+
"""Ensure process-bigraph's built-in emitters are registered under their
|
|
23
|
+
SHORT names (``RAMEmitter`` etc.), not only their fully-qualified names.
|
|
24
|
+
|
|
25
|
+
Discovery normally registers both, but when several emitter packages
|
|
26
|
+
co-register in an unlucky import order the short alias is silently dropped
|
|
27
|
+
(see ``process_bigraph.emitter._VIVA_REEXPORTS``) — leaving only the FQN
|
|
28
|
+
``process_bigraph.emitter.RAMEmitter`` and breaking the framework's own
|
|
29
|
+
default sink ``local:RAMEmitter``. Registering the built-ins explicitly here
|
|
30
|
+
is order-independent and idempotent (we only fill a missing short alias).
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
from process_bigraph.emitter import (
|
|
34
|
+
Emitter, ConsoleEmitter, RAMEmitter, JSONEmitter)
|
|
35
|
+
except Exception:
|
|
36
|
+
return
|
|
37
|
+
registry = getattr(core, 'link_registry', None)
|
|
38
|
+
for cls in (Emitter, ConsoleEmitter, RAMEmitter, JSONEmitter):
|
|
39
|
+
name = cls.__name__
|
|
40
|
+
if registry is not None and name in registry:
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
core.register_link(name, cls)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
17
46
|
return core
|
|
18
47
|
|
|
19
48
|
# The installed distribution's version, exposed so a run manifest can record
|
|
@@ -41,7 +70,16 @@ from process_bigraph.composite_spec import ( # noqa: F401
|
|
|
41
70
|
normalize_type, CANONICAL_TYPES,
|
|
42
71
|
)
|
|
43
72
|
from process_bigraph.bundle import load_bundle # noqa: F401
|
|
44
|
-
from process_bigraph.draft_process import
|
|
73
|
+
from process_bigraph.draft_process import ( # noqa: F401
|
|
74
|
+
DraftProcess, draft_process, draft_to_contract, DraftProcessDeprecationWarning,
|
|
75
|
+
)
|
|
76
|
+
# Holes-in-place: the public lifecycle for a process-slot left as a hole whose
|
|
77
|
+
# sort is a ProcessContract — design, inspect, see what fits, fill, build.
|
|
78
|
+
from process_bigraph.holes import UnfilledHoleError # noqa: F401
|
|
79
|
+
from process_bigraph.templates import ( # noqa: F401
|
|
80
|
+
open_sites, required_open_sites, is_ground_document, fill_template, template_document,
|
|
81
|
+
)
|
|
82
|
+
from bigraph_schema.matching import find_candidates # noqa: F401
|
|
45
83
|
from process_bigraph.emitter import Emitter, gather_emitter_results, generate_emitter_state # noqa: F401
|
|
46
84
|
from process_bigraph.types import StepLink, ProcessLink, CompositeLink # noqa: F401
|
|
47
85
|
from process_bigraph import core_introspection # noqa: F401
|
|
@@ -1264,6 +1264,7 @@ class Composite(Process):
|
|
|
1264
1264
|
'parallel_steps': 'boolean{false}',
|
|
1265
1265
|
# Runtime contract checking: off (default, zero-cost) | record | raise.
|
|
1266
1266
|
'contract_strict': 'string{off}',
|
|
1267
|
+
'orchestration': 'string{temporal}',
|
|
1267
1268
|
# Time-scheduled processes: when True, the per-tick `run` loop
|
|
1268
1269
|
# fans out invocations across a thread pool. Right tool when
|
|
1269
1270
|
# processes' update() releases the GIL (network I/O, numpy /
|
|
@@ -2384,13 +2385,16 @@ class Composite(Process):
|
|
|
2384
2385
|
# Build the step execution dependency graph
|
|
2385
2386
|
self.step_dependencies, self.node_dependencies = build_step_network(self.step_paths)
|
|
2386
2387
|
|
|
2387
|
-
# Reject dependency cycles among zero-time
|
|
2388
|
-
# (Supplement 1, 3.8.4) guarantees the step network
|
|
2389
|
-
#
|
|
2390
|
-
#
|
|
2391
|
-
#
|
|
2392
|
-
#
|
|
2393
|
-
#
|
|
2388
|
+
# Reject only NON-DETERMINISTIC dependency cycles among zero-time
|
|
2389
|
+
# steps. The paper (Supplement 1, 3.8.4) guarantees the step network
|
|
2390
|
+
# is a DAG, but in practice ``determine_steps`` resolves a cycle by
|
|
2391
|
+
# firing the highest-priority runnable step first: that is
|
|
2392
|
+
# deterministic when the cyclic steps carry distinct priorities, and
|
|
2393
|
+
# ``find_step_cycle`` allows those (with a warning). It returns a
|
|
2394
|
+
# cycle here only when two of its members share a priority, so the
|
|
2395
|
+
# tie would fall through to a hash-seed-dependent order and the same
|
|
2396
|
+
# document would give different results in different processes
|
|
2397
|
+
# (issue #233).
|
|
2394
2398
|
cycle = find_step_cycle(self.step_dependencies, self.node_dependencies)
|
|
2395
2399
|
if cycle is not None:
|
|
2396
2400
|
def _name(step_key):
|
|
@@ -2399,13 +2403,16 @@ class Composite(Process):
|
|
|
2399
2403
|
return str(step_key)
|
|
2400
2404
|
rendered = ' -> '.join(_name(step_key) for step_key in cycle)
|
|
2401
2405
|
raise ValueError(
|
|
2402
|
-
f'step dependency cycle detected:
|
|
2403
|
-
f'
|
|
2404
|
-
f'
|
|
2405
|
-
f'
|
|
2406
|
-
f'
|
|
2407
|
-
f'
|
|
2408
|
-
f'
|
|
2406
|
+
f'non-deterministic step dependency cycle detected: '
|
|
2407
|
+
f'{rendered}. These steps form a zero-time feedback cycle AND '
|
|
2408
|
+
f'share a priority, so their evaluation order would depend on '
|
|
2409
|
+
f'PYTHONHASHSEED-driven set-iteration order and differ between '
|
|
2410
|
+
f'processes (process-bigraph paper, Supplement 1, 3.8.4; '
|
|
2411
|
+
f'issue #233). Resolve it by giving the cyclic steps '
|
|
2412
|
+
f'distinct priorities (the scheduler then orders them '
|
|
2413
|
+
f'deterministically), removing one of the mutual input/output '
|
|
2414
|
+
f'wirings, or modelling the feedback as a Process with a '
|
|
2415
|
+
f'non-zero time interval.')
|
|
2409
2416
|
|
|
2410
2417
|
# Initialize trigger fulfillment state and steps remaining
|
|
2411
2418
|
self.reset_step_state(self.step_paths)
|
|
@@ -2728,7 +2735,7 @@ class Composite(Process):
|
|
|
2728
2735
|
# control flows on the same thread.
|
|
2729
2736
|
_cc_token = current_composite_var.set(self)
|
|
2730
2737
|
try:
|
|
2731
|
-
return self.
|
|
2738
|
+
return self._orchestration_strategy().advance(interval, force_complete, run_start)
|
|
2732
2739
|
except BaseException as exc:
|
|
2733
2740
|
status, error = 'error', f'{type(exc).__name__}: {exc}'
|
|
2734
2741
|
raise
|
|
@@ -2775,101 +2782,101 @@ class Composite(Process):
|
|
|
2775
2782
|
if span is not None:
|
|
2776
2783
|
span.end(status, error)
|
|
2777
2784
|
|
|
2778
|
-
def
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2785
|
+
def _orchestration_strategy(self):
|
|
2786
|
+
"""Resolve (and cache) this Composite's orchestration strategy from the
|
|
2787
|
+
registry. Defaults to ``temporal`` — the event-scheduling loop in
|
|
2788
|
+
:meth:`advance_tick` — so a Composite that declares no ``orchestration``
|
|
2789
|
+
runs exactly as before."""
|
|
2790
|
+
strat = getattr(self, "_orchestration", None)
|
|
2791
|
+
if strat is None:
|
|
2792
|
+
from process_bigraph.orchestration import get_orchestration
|
|
2793
|
+
name = self.config.get("orchestration") or "temporal"
|
|
2794
|
+
strat = self._orchestration = get_orchestration(name)(self)
|
|
2795
|
+
return strat
|
|
2796
|
+
|
|
2797
|
+
def run_step_network(self) -> None:
|
|
2798
|
+
"""Drain the ready-step DAG to completion: ``run_steps`` self-cascades
|
|
2799
|
+
through ``cycle_step_state``/``trigger_steps``, so this runs every
|
|
2800
|
+
currently-ready step in dependency order. The step-DAG primitive shared
|
|
2801
|
+
by the temporal and workflow orchestration strategies."""
|
|
2783
2802
|
if self.to_run:
|
|
2784
2803
|
self.run_steps(self.to_run)
|
|
2785
2804
|
self.to_run = []
|
|
2786
2805
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
process
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
#
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
self.steps_run = set() # Reset for new timestep
|
|
2849
|
-
# Opt-in halt after structural change. EcoliSim sets
|
|
2850
|
-
# ``_halt_after_structural=True`` to mirror v1's halt
|
|
2851
|
-
# via DivisionDetected. Dynamic-structure tests leave
|
|
2852
|
-
# the flag unset for natural cascading.
|
|
2853
|
-
if (getattr(self, '_halt_after_structural', False)
|
|
2854
|
-
and getattr(self, '_last_apply_structural', False)):
|
|
2855
|
-
self.framework_time += _time.monotonic() - fw_start
|
|
2856
|
-
return
|
|
2857
|
-
self.trigger_steps(update_paths)
|
|
2806
|
+
def advance_tick(self, end_time: float, force_complete: bool):
|
|
2807
|
+
"""Execute ONE temporal tick toward ``end_time`` — the body of the
|
|
2808
|
+
event-scheduling loop. Returns ``(halted, force_complete)``: ``halted``
|
|
2809
|
+
is True when an opt-in structural halt fired and the run must stop now;
|
|
2810
|
+
``force_complete`` is returned because the tick may clear it. The
|
|
2811
|
+
``temporal`` strategy drives this in a loop while ``Composite`` owns the
|
|
2812
|
+
tick mechanics (front, invoke, apply), keeping strategies thin."""
|
|
2813
|
+
full_step = math.inf
|
|
2814
|
+
self._run_ticks = getattr(self, '_run_ticks', 0) + 1
|
|
2815
|
+
self._em.heartbeat(global_time=self.state['global_time'])
|
|
2816
|
+
|
|
2817
|
+
# Partition processes: runtime-batched (tick_lifecycle) vs per-process.
|
|
2818
|
+
managed_paths, runtime_groups = self._partition_processes_by_runtime()
|
|
2819
|
+
|
|
2820
|
+
if self._parallel_processes and len(self.process_paths) > 1:
|
|
2821
|
+
full_step = self._run_processes_layer_parallel(
|
|
2822
|
+
end_time, full_step, force_complete,
|
|
2823
|
+
skip_paths=managed_paths)
|
|
2824
|
+
else:
|
|
2825
|
+
for path in self.process_paths:
|
|
2826
|
+
if path in managed_paths:
|
|
2827
|
+
continue
|
|
2828
|
+
process = get_path(self.state, path)
|
|
2829
|
+
full_step = self.run_process(
|
|
2830
|
+
path, process, end_time, full_step, force_complete)
|
|
2831
|
+
|
|
2832
|
+
for rt, group in runtime_groups:
|
|
2833
|
+
full_step = self._run_tick_lifecycle(
|
|
2834
|
+
rt, group, end_time, full_step, force_complete)
|
|
2835
|
+
|
|
2836
|
+
if full_step == math.inf:
|
|
2837
|
+
# No process ran — jump to the next scheduled process time
|
|
2838
|
+
next_event = end_time
|
|
2839
|
+
for path in self.front.keys():
|
|
2840
|
+
if self.front[path]['time'] < next_event:
|
|
2841
|
+
next_event = self.front[path]['time']
|
|
2842
|
+
self.state['global_time'] = next_event
|
|
2843
|
+
|
|
2844
|
+
elif self.state['global_time'] + full_step <= end_time:
|
|
2845
|
+
# At least one process ran — advance time and apply its update
|
|
2846
|
+
self.state['global_time'] += full_step
|
|
2847
|
+
updates = []
|
|
2848
|
+
paths = []
|
|
2849
|
+
|
|
2850
|
+
for path, advance in self.front.items():
|
|
2851
|
+
if advance['time'] <= self.state['global_time'] and advance['update']:
|
|
2852
|
+
updates.append(advance['update'])
|
|
2853
|
+
advance['update'] = {}
|
|
2854
|
+
paths.append(path)
|
|
2855
|
+
|
|
2856
|
+
fw_start = _time.monotonic()
|
|
2857
|
+
self._flush_protocol_runtimes()
|
|
2858
|
+
update_paths = self.apply_updates(updates)
|
|
2859
|
+
update_paths.append(('global_time',)) # updated global time can trigger steps
|
|
2860
|
+
self.expire_process_paths(update_paths)
|
|
2861
|
+
self.steps_run = set() # Reset for new timestep
|
|
2862
|
+
# Opt-in halt after structural change (EcoliSim parity). On halt the
|
|
2863
|
+
# run stops immediately, skipping the post-loop bookkeeping — the
|
|
2864
|
+
# driving loop returns without finalizing framework_time.
|
|
2865
|
+
if (getattr(self, '_halt_after_structural', False)
|
|
2866
|
+
and getattr(self, '_last_apply_structural', False)):
|
|
2858
2867
|
self.framework_time += _time.monotonic() - fw_start
|
|
2868
|
+
return True, force_complete
|
|
2869
|
+
self.trigger_steps(update_paths)
|
|
2870
|
+
self.framework_time += _time.monotonic() - fw_start
|
|
2859
2871
|
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2872
|
+
else:
|
|
2873
|
+
# All remaining process events are beyond end_time
|
|
2874
|
+
self.state['global_time'] = end_time
|
|
2863
2875
|
|
|
2864
|
-
|
|
2865
|
-
|
|
2876
|
+
if force_complete and self.state['global_time'] == end_time:
|
|
2877
|
+
force_complete = False
|
|
2866
2878
|
|
|
2867
|
-
|
|
2868
|
-
# Framework time = total minus process update time
|
|
2869
|
-
# (the process_update_time was accumulated in process_update and,
|
|
2870
|
-
# for batched-runtime processes, in _run_tick_lifecycle)
|
|
2871
|
-
self.framework_time = total - self.process_update_time
|
|
2872
|
-
self._last_run_total_time = total
|
|
2879
|
+
return False, force_complete
|
|
2873
2880
|
|
|
2874
2881
|
def timing_summary(self) -> TimingSummary:
|
|
2875
2882
|
"""Return a ``TimingSummary`` describing the most recent ``run()`` call.
|
|
@@ -2899,15 +2906,20 @@ class Composite(Process):
|
|
|
2899
2906
|
per_process=dict(self._per_process_time),
|
|
2900
2907
|
)
|
|
2901
2908
|
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2909
|
+
def _raise_unfilled(self, unfilled_pairs) -> None:
|
|
2910
|
+
"""Raise a contract-aware :class:`~process_bigraph.holes.UnfilledHoleError`
|
|
2911
|
+
for the open ``(path, site)`` pairs.
|
|
2912
|
+
|
|
2913
|
+
Names each hole's required contract (typed face + conditions) and the
|
|
2914
|
+
registered processes that would fill it, in place of the opaque
|
|
2915
|
+
``KeyError: 'data'`` an unfilled address site would otherwise raise deep
|
|
2916
|
+
in realization. The error subclasses ``ValueError``, so existing
|
|
2917
|
+
``except ValueError`` callers keep catching it, and its message keeps the
|
|
2918
|
+
"not ground — required site(s) left unfilled" lineage.
|
|
2919
|
+
"""
|
|
2920
|
+
from process_bigraph.holes import build_unfilled_hole_error
|
|
2921
|
+
ordered = sorted(unfilled_pairs, key=lambda pair: pair[0])
|
|
2922
|
+
raise build_unfilled_hole_error(self.core, ordered)
|
|
2911
2923
|
|
|
2912
2924
|
def _require_ground_input(
|
|
2913
2925
|
self,
|
|
@@ -2944,10 +2956,12 @@ class Composite(Process):
|
|
|
2944
2956
|
# A site is a state-level hole: inspect the document as written,
|
|
2945
2957
|
# so a declared `schema` type cannot hide a `state` site.
|
|
2946
2958
|
accessed = self.core.access(initial_state)
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2959
|
+
# Keep one (path, site) per unique path; collect_sites/interfaces may
|
|
2960
|
+
# address a site more than once.
|
|
2961
|
+
unfilled = {}
|
|
2962
|
+
for path, site in interfaces(accessed)[0]._places:
|
|
2963
|
+
if getattr(site, '_default', None) is None:
|
|
2964
|
+
unfilled.setdefault(tuple(path), site)
|
|
2951
2965
|
except Exception:
|
|
2952
2966
|
# If the raw document cannot be inspected for any reason, do not
|
|
2953
2967
|
# mask that behind this check — fall through to realization and
|
|
@@ -2955,7 +2969,7 @@ class Composite(Process):
|
|
|
2955
2969
|
return
|
|
2956
2970
|
|
|
2957
2971
|
if unfilled:
|
|
2958
|
-
self.
|
|
2972
|
+
self._raise_unfilled(list(unfilled.items()))
|
|
2959
2973
|
|
|
2960
2974
|
def _require_ground_document(self) -> None:
|
|
2961
2975
|
"""Reject a document that still has unfilled template holes.
|
|
@@ -2979,14 +2993,14 @@ class Composite(Process):
|
|
|
2979
2993
|
from bigraph_schema.assembly import collect_sites
|
|
2980
2994
|
|
|
2981
2995
|
# ``collect_sites`` addresses each site twice (bare key and path);
|
|
2982
|
-
# collapse to unique
|
|
2983
|
-
unfilled =
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2996
|
+
# collapse to one (path, site) per unique path.
|
|
2997
|
+
unfilled = {}
|
|
2998
|
+
for path, site in collect_sites(self.schema).values():
|
|
2999
|
+
if getattr(site, '_default', None) is None:
|
|
3000
|
+
unfilled.setdefault(tuple(path), site)
|
|
2987
3001
|
|
|
2988
3002
|
if unfilled:
|
|
2989
|
-
self.
|
|
3003
|
+
self._raise_unfilled(list(unfilled.items()))
|
|
2990
3004
|
|
|
2991
3005
|
def _require_advancing_interval(
|
|
2992
3006
|
self,
|
|
@@ -83,8 +83,10 @@ def discover_all(
|
|
|
83
83
|
|
|
84
84
|
Spec entries get ``kind: spec`` and pass through the existing spec
|
|
85
85
|
payload. Generator entries get ``kind: generator`` and a compact dict
|
|
86
|
-
{name, description, parameters,
|
|
87
|
-
|
|
86
|
+
{name, description, parameters, visualizations, emitters, module,
|
|
87
|
+
default_n_steps, methods, fidelity} so dashboards can render them (and
|
|
88
|
+
report their modeling provenance) without holding a reference to the live
|
|
89
|
+
function object.
|
|
88
90
|
|
|
89
91
|
Discovery imports the host packages of every generator (see
|
|
90
92
|
:func:`process_bigraph.composite_generator.discover_generators`). If
|
|
@@ -118,6 +120,12 @@ def discover_all(
|
|
|
118
120
|
"emitters": list(entry.emitters),
|
|
119
121
|
"module": entry.module,
|
|
120
122
|
"default_n_steps": entry.default_n_steps,
|
|
123
|
+
# Modeling-provenance metadata — so a consumer that only sees this
|
|
124
|
+
# serialized dict (e.g. a dashboard across a worker boundary) can
|
|
125
|
+
# still report how the composite is built and whether it wraps a
|
|
126
|
+
# genuine simulator. Always present (empty list / "" when undeclared).
|
|
127
|
+
"methods": list(getattr(entry, "methods", []) or []),
|
|
128
|
+
"fidelity": getattr(entry, "fidelity", "") or "",
|
|
121
129
|
}
|
|
122
130
|
return out
|
|
123
131
|
|