process-bigraph 1.1.6__tar.gz → 1.2.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.
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/.gitignore +1 -0
- {process_bigraph-1.1.6/process_bigraph.egg-info → process_bigraph-1.2.1}/PKG-INFO +7 -1
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/README.md +6 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/composite.py +197 -90
- process_bigraph-1.2.1/process_bigraph/emitter.py +610 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1/process_bigraph.egg-info}/PKG-INFO +7 -1
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/pyproject.toml +1 -1
- process_bigraph-1.1.6/process_bigraph/emitter.py +0 -248
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/.github/workflows/notebook_to_html.yml +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/.github/workflows/pytest.yml +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/AUTHORS.md +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/CLA.md +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/CODE_OF_CONDUCT.md +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/CONTRIBUTING.md +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/LICENSE +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/doc/_static/process-bigraph.png +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/notebooks/process-bigraphs.ipynb +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/notebooks/visualize_processes.ipynb +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/__init__.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/nextflow.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/math_expression.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/protocols/socket.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/run.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph/units.py +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph.egg-info/SOURCES.txt +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph.egg-info/requires.txt +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/pytest.ini +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/release.sh +0 -0
- {process_bigraph-1.1.6 → process_bigraph-1.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -110,6 +110,12 @@ rendered to HTML and published automatically on GitHub Pages.
|
|
|
110
110
|
|
|
111
111
|
More tutorials are added continuously and appear automatically in the index.
|
|
112
112
|
|
|
113
|
+
### Topic Guides
|
|
114
|
+
|
|
115
|
+
- **Emitters — Recording Simulation Results**
|
|
116
|
+
*Built-in emitters (RAM, console, JSON, SQLite), how to wire them, retrieve results, store runs long-term, and write your own.*
|
|
117
|
+
[doc/emitters.md](doc/emitters.md)
|
|
118
|
+
|
|
113
119
|
---
|
|
114
120
|
|
|
115
121
|
## 🧪 Reference Implementation: spatio-flux
|
|
@@ -90,6 +90,12 @@ rendered to HTML and published automatically on GitHub Pages.
|
|
|
90
90
|
|
|
91
91
|
More tutorials are added continuously and appear automatically in the index.
|
|
92
92
|
|
|
93
|
+
### Topic Guides
|
|
94
|
+
|
|
95
|
+
- **Emitters — Recording Simulation Results**
|
|
96
|
+
*Built-in emitters (RAM, console, JSON, SQLite), how to wire them, retrieve results, store runs long-term, and write your own.*
|
|
97
|
+
[doc/emitters.md](doc/emitters.md)
|
|
98
|
+
|
|
93
99
|
---
|
|
94
100
|
|
|
95
101
|
## 🧪 Reference Implementation: spatio-flux
|
|
@@ -32,6 +32,7 @@ from bigraph_schema import (
|
|
|
32
32
|
is_schema_key, strip_schema_keys)
|
|
33
33
|
|
|
34
34
|
from bigraph_schema.protocols import local_lookup_module
|
|
35
|
+
from bigraph_schema.methods.events import NodeAdded, NodeRemoved, Divided
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
# =========================
|
|
@@ -1281,6 +1282,14 @@ class Composite(Process):
|
|
|
1281
1282
|
# extensions: yes; pure-Python loops: no).
|
|
1282
1283
|
'parallel_steps': 'boolean{false}',
|
|
1283
1284
|
'parallel_workers': 'maybe[integer]',
|
|
1285
|
+
# When True, ``initialize`` trusts ``state`` as-is and skips the
|
|
1286
|
+
# per-process ``link_state`` + ``combine`` pass that normally
|
|
1287
|
+
# seeds initial values. Set this when loading from a saved
|
|
1288
|
+
# bundle (e.g. daughter_state from a workflow generation):
|
|
1289
|
+
# the saved state already contains everything processes would
|
|
1290
|
+
# have contributed, and re-combining their ``initial_state()``
|
|
1291
|
+
# overwrites correctly-divided values with mother-shaped ones.
|
|
1292
|
+
'skip_process_state': 'boolean{false}',
|
|
1284
1293
|
}
|
|
1285
1294
|
|
|
1286
1295
|
|
|
@@ -1336,37 +1345,43 @@ class Composite(Process):
|
|
|
1336
1345
|
self.edge_paths = {**self.process_paths, **self.step_paths}
|
|
1337
1346
|
|
|
1338
1347
|
# Initialize each process/step's state and accumulate it into a unified state tree.
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1348
|
+
# ``skip_process_state`` short-circuits this when the caller supplied
|
|
1349
|
+
# state that already contains everything processes would have seeded
|
|
1350
|
+
# (e.g. a daughter_state bundle from a previous workflow generation).
|
|
1351
|
+
# Without the skip, ``combine`` overwrites correctly-divided values
|
|
1352
|
+
# with full mother-shaped ones from each process's ``initial_state()``.
|
|
1353
|
+
if not self.config.get('skip_process_state', False):
|
|
1354
|
+
edge_schema = {}
|
|
1355
|
+
edge_state = {}
|
|
1356
|
+
for path, edge in self.edge_paths.items():
|
|
1357
|
+
# Generate the initial state for this specific edge (process or step).
|
|
1358
|
+
initial_schema, initial_state = self.core.link_state(
|
|
1359
|
+
edge,
|
|
1360
|
+
path)
|
|
1361
|
+
|
|
1362
|
+
# Merge the new edge state with the global state tree, checking for conflicts.
|
|
1363
|
+
try:
|
|
1364
|
+
edge_schema, edge_state = self.core.combine(
|
|
1365
|
+
edge_schema, edge_state,
|
|
1366
|
+
initial_schema, initial_state)
|
|
1367
|
+
|
|
1368
|
+
except Exception as e:
|
|
1369
|
+
import sys as _sys
|
|
1370
|
+
_sys.stderr.write(f'[INIT_COMBINE_FAIL] edge={path}\n')
|
|
1371
|
+
_sys.stderr.write(f'[INIT_COMBINE_FAIL] new_schema={initial_schema}\n')
|
|
1372
|
+
_sys.stderr.write(f'[INIT_COMBINE_FAIL] err={e}\n')
|
|
1373
|
+
_sys.stderr.flush()
|
|
1374
|
+
raise Exception(
|
|
1375
|
+
f'initial state from edge does not match initial state from other edges:\n'
|
|
1376
|
+
f'{path}\n{edge}\n{edge_state}\n'
|
|
1377
|
+
f'{e}'
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1380
|
+
# Apply the merged edge_state into the global state and update instance paths.
|
|
1381
|
+
if edge_state:
|
|
1382
|
+
self.schema, self.state = self.core.combine(
|
|
1350
1383
|
edge_schema, edge_state,
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
except Exception as e:
|
|
1354
|
-
import sys as _sys
|
|
1355
|
-
_sys.stderr.write(f'[INIT_COMBINE_FAIL] edge={path}\n')
|
|
1356
|
-
_sys.stderr.write(f'[INIT_COMBINE_FAIL] new_schema={initial_schema}\n')
|
|
1357
|
-
_sys.stderr.write(f'[INIT_COMBINE_FAIL] err={e}\n')
|
|
1358
|
-
_sys.stderr.flush()
|
|
1359
|
-
raise Exception(
|
|
1360
|
-
f'initial state from edge does not match initial state from other edges:\n'
|
|
1361
|
-
f'{path}\n{edge}\n{edge_state}\n'
|
|
1362
|
-
f'{e}'
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
# Apply the merged edge_state into the global state and update instance paths.
|
|
1366
|
-
if edge_state:
|
|
1367
|
-
self.schema, self.state = self.core.combine(
|
|
1368
|
-
edge_schema, edge_state,
|
|
1369
|
-
self.schema, self.state)
|
|
1384
|
+
self.schema, self.state)
|
|
1370
1385
|
|
|
1371
1386
|
# Wire the input/output schema for the Composite from the bridge config.
|
|
1372
1387
|
self.process_schema = {
|
|
@@ -1439,6 +1454,11 @@ class Composite(Process):
|
|
|
1439
1454
|
- self.process_paths
|
|
1440
1455
|
- self.step_paths
|
|
1441
1456
|
"""
|
|
1457
|
+
# Structural change incoming — drop the compiled-apply cache:
|
|
1458
|
+
# ``apply(dict)`` mutates schemas in place for ``_divide``
|
|
1459
|
+
# sentinels, so cached compiled functions may now reference
|
|
1460
|
+
# stale schema layouts.
|
|
1461
|
+
self.core.invalidate_compiled_apply()
|
|
1442
1462
|
self.process_paths = find_instance_paths(state, 'process_bigraph.composite.Process')
|
|
1443
1463
|
if hasattr(self, 'step_paths'):
|
|
1444
1464
|
previous_step_paths = self.step_paths.keys()
|
|
@@ -1459,6 +1479,94 @@ class Composite(Process):
|
|
|
1459
1479
|
# do we want to do anything with these?
|
|
1460
1480
|
removed_front = self.front.pop(removed_key)
|
|
1461
1481
|
|
|
1482
|
+
def _apply_structural_events(self, events: List[Any]) -> None:
|
|
1483
|
+
"""Update process/step indexes from apply-emitted structural events.
|
|
1484
|
+
|
|
1485
|
+
Replaces the full-state ``find_instance_paths`` rescan when apply
|
|
1486
|
+
tells us exactly which subtrees changed. For each:
|
|
1487
|
+
- ``NodeAdded``: scan the new subtree only and merge any
|
|
1488
|
+
discovered Process/Step instances into the existing index.
|
|
1489
|
+
- ``NodeRemoved``: drop entries at-or-under the removed path.
|
|
1490
|
+
- ``Divided``: drop the mother's entries, scan each daughter
|
|
1491
|
+
subtree.
|
|
1492
|
+
|
|
1493
|
+
The compiled-apply cache is invalidated unconditionally because
|
|
1494
|
+
``apply(dict)``'s ``_divide`` branch mutates schemas in place.
|
|
1495
|
+
|
|
1496
|
+
Step network rebuild is conditional on step_paths actually
|
|
1497
|
+
changing — value-only structural events that don't add/remove
|
|
1498
|
+
any Step instances skip the rebuild.
|
|
1499
|
+
"""
|
|
1500
|
+
self.core.invalidate_compiled_apply()
|
|
1501
|
+
|
|
1502
|
+
previous_step_paths = (set(self.step_paths.keys())
|
|
1503
|
+
if hasattr(self, 'step_paths') else set())
|
|
1504
|
+
if not hasattr(self, 'step_paths'):
|
|
1505
|
+
self.step_paths = {}
|
|
1506
|
+
|
|
1507
|
+
def _index_subtree(root_path: tuple, subtree_state: Any) -> None:
|
|
1508
|
+
"""Walk subtree, merge Process/Step instances into indexes."""
|
|
1509
|
+
# find_instances takes a dict; if subtree is a dict, walk;
|
|
1510
|
+
# if it's a leaf or wrapped instance, check at this level.
|
|
1511
|
+
if not isinstance(subtree_state, dict):
|
|
1512
|
+
return
|
|
1513
|
+
instance = subtree_state.get('instance')
|
|
1514
|
+
from process_bigraph.composite import Process as _Proc
|
|
1515
|
+
from process_bigraph.composite import Step as _Step
|
|
1516
|
+
if isinstance(instance, _Proc):
|
|
1517
|
+
self.process_paths[root_path] = subtree_state
|
|
1518
|
+
return # Don't recurse into a process's internal state
|
|
1519
|
+
if isinstance(instance, _Step):
|
|
1520
|
+
self.step_paths[root_path] = subtree_state
|
|
1521
|
+
return
|
|
1522
|
+
# Recurse into non-instance dict
|
|
1523
|
+
for key, child in subtree_state.items():
|
|
1524
|
+
if is_schema_key(key):
|
|
1525
|
+
continue
|
|
1526
|
+
_index_subtree(root_path + (key,), child)
|
|
1527
|
+
|
|
1528
|
+
def _drop_under(root_path: tuple) -> None:
|
|
1529
|
+
"""Remove process/step/front entries at-or-under root_path."""
|
|
1530
|
+
for path in list(self.process_paths.keys()):
|
|
1531
|
+
if path == root_path or path[:len(root_path)] == root_path:
|
|
1532
|
+
del self.process_paths[path]
|
|
1533
|
+
for path in list(self.step_paths.keys()):
|
|
1534
|
+
if path == root_path or path[:len(root_path)] == root_path:
|
|
1535
|
+
del self.step_paths[path]
|
|
1536
|
+
for path in list(self.front.keys()):
|
|
1537
|
+
if path == root_path or path[:len(root_path)] == root_path:
|
|
1538
|
+
del self.front[path]
|
|
1539
|
+
|
|
1540
|
+
for event in events:
|
|
1541
|
+
if isinstance(event, NodeAdded):
|
|
1542
|
+
added_path = event.path + (event.key,)
|
|
1543
|
+
# Read fresh subtree from realized state — event.state is
|
|
1544
|
+
# pre-realize (no instances yet).
|
|
1545
|
+
subtree = get_path(self.state, list(added_path))
|
|
1546
|
+
if subtree is not None:
|
|
1547
|
+
_index_subtree(added_path, subtree)
|
|
1548
|
+
elif isinstance(event, NodeRemoved):
|
|
1549
|
+
removed_path = event.path + (event.key,)
|
|
1550
|
+
_drop_under(removed_path)
|
|
1551
|
+
elif isinstance(event, Divided):
|
|
1552
|
+
mother_path = event.path + (event.mother_key,)
|
|
1553
|
+
_drop_under(mother_path)
|
|
1554
|
+
for d_key in event.daughter_keys:
|
|
1555
|
+
d_path = event.path + (d_key,)
|
|
1556
|
+
subtree = get_path(self.state, list(d_path))
|
|
1557
|
+
if subtree is not None:
|
|
1558
|
+
_index_subtree(d_path, subtree)
|
|
1559
|
+
|
|
1560
|
+
# Step network only needs rebuild if step_paths changed
|
|
1561
|
+
current_step_paths = set(self.step_paths.keys())
|
|
1562
|
+
if current_step_paths != previous_step_paths:
|
|
1563
|
+
self.build_step_network()
|
|
1564
|
+
|
|
1565
|
+
# Sync front buffer: drop entries for paths no longer present
|
|
1566
|
+
all_paths = set(self.process_paths.keys()) | current_step_paths
|
|
1567
|
+
for removed_key in set(self.front.keys()).difference(all_paths):
|
|
1568
|
+
self.front.pop(removed_key)
|
|
1569
|
+
|
|
1462
1570
|
def _build_view_project_cache(self) -> None:
|
|
1463
1571
|
"""Precompile view/project operations for each process path.
|
|
1464
1572
|
|
|
@@ -1763,7 +1871,12 @@ class Composite(Process):
|
|
|
1763
1871
|
from process_bigraph.bundle import load_bundle
|
|
1764
1872
|
|
|
1765
1873
|
document = load_bundle(bundle_dir, as_numpy=True)
|
|
1766
|
-
|
|
1874
|
+
# The bundle's state is authoritative — it was saved post-init
|
|
1875
|
+
# so it already contains every contribution the per-process
|
|
1876
|
+
# ``link_state`` pass would re-add. Combining ``initial_state()``
|
|
1877
|
+
# on top here would clobber correctly-divided values (e.g. a
|
|
1878
|
+
# daughter's halved bulk array) with mother-shaped originals.
|
|
1879
|
+
config = {'skip_process_state': True, **document, **kwargs}
|
|
1767
1880
|
return cls(config, core=core)
|
|
1768
1881
|
|
|
1769
1882
|
|
|
@@ -2031,12 +2144,13 @@ class Composite(Process):
|
|
|
2031
2144
|
step = get_path(self.state, step_path)
|
|
2032
2145
|
state = self._cached_view(step_path)
|
|
2033
2146
|
instance = step.get('instance')
|
|
2034
|
-
|
|
2147
|
+
# _view_resolved emits only port-key entries; no schema
|
|
2148
|
+
# keys to strip. perform_update sees the same dict.
|
|
2035
2149
|
if instance is not None and hasattr(instance, 'perform_update'):
|
|
2036
|
-
if not instance.perform_update(
|
|
2150
|
+
if not instance.perform_update(state):
|
|
2037
2151
|
return None
|
|
2038
2152
|
return self.process_update(
|
|
2039
|
-
step_path, step,
|
|
2153
|
+
step_path, step, state, -1.0, 'outputs',
|
|
2040
2154
|
already_clean=True)
|
|
2041
2155
|
# list() forces all futures to resolve before continuing
|
|
2042
2156
|
updates = [u for u in pool.map(_run_one, step_paths) if u is not None]
|
|
@@ -2051,17 +2165,15 @@ class Composite(Process):
|
|
|
2051
2165
|
state = self._cached_view(step_path)
|
|
2052
2166
|
|
|
2053
2167
|
instance = step.get('instance')
|
|
2054
|
-
#
|
|
2055
|
-
# perform_update
|
|
2056
|
-
# (invoke trusts the caller and skips its own gate).
|
|
2057
|
-
clean_state = strip_schema_keys(state)
|
|
2168
|
+
# _view_resolved emits only port-key entries; no schema
|
|
2169
|
+
# keys to strip. perform_update sees the same dict.
|
|
2058
2170
|
if instance is not None and hasattr(instance, 'perform_update'):
|
|
2059
|
-
if not instance.perform_update(
|
|
2171
|
+
if not instance.perform_update(state):
|
|
2060
2172
|
continue
|
|
2061
2173
|
|
|
2062
2174
|
# Steps are always invoked with interval = -1.0
|
|
2063
2175
|
step_update = self.process_update(
|
|
2064
|
-
step_path, step,
|
|
2176
|
+
step_path, step, state, -1.0, 'outputs',
|
|
2065
2177
|
already_clean=True)
|
|
2066
2178
|
|
|
2067
2179
|
updates.append(step_update)
|
|
@@ -2216,7 +2328,12 @@ class Composite(Process):
|
|
|
2216
2328
|
|
|
2217
2329
|
# Only proceed if the next step occurs within the target range
|
|
2218
2330
|
if future <= end_time:
|
|
2219
|
-
|
|
2331
|
+
# state came from _cached_view (or future_front whose source
|
|
2332
|
+
# is also _cached_view) — _view_resolved only emits port-key
|
|
2333
|
+
# entries, so the dict has no schema keys to strip.
|
|
2334
|
+
update = self.process_update(
|
|
2335
|
+
path, process, state, process_interval,
|
|
2336
|
+
already_clean=True)
|
|
2220
2337
|
|
|
2221
2338
|
# Store the update to apply when simulation reaches `future` time
|
|
2222
2339
|
self.front[path]['time'] = future
|
|
@@ -2324,39 +2441,6 @@ class Composite(Process):
|
|
|
2324
2441
|
return True
|
|
2325
2442
|
return False
|
|
2326
2443
|
|
|
2327
|
-
@staticmethod
|
|
2328
|
-
def _walk_update(state: Any, path: tuple = ()) -> tuple:
|
|
2329
|
-
"""Single-pass walk over an update tree.
|
|
2330
|
-
|
|
2331
|
-
Combines hierarchy_depth and _has_structural_keys into one
|
|
2332
|
-
traversal — both are called on every apply_updates phase 1
|
|
2333
|
-
invocation and were duplicating the same recursive walk.
|
|
2334
|
-
|
|
2335
|
-
Returns (paths_list, has_structural) where paths_list mirrors
|
|
2336
|
-
what hierarchy_depth would have returned (list of leaf path
|
|
2337
|
-
tuples) and has_structural is True if any _add/_remove/_type
|
|
2338
|
-
sentinel was found anywhere in the tree.
|
|
2339
|
-
"""
|
|
2340
|
-
if not isinstance(state, dict):
|
|
2341
|
-
return [path], False
|
|
2342
|
-
paths = []
|
|
2343
|
-
has_structural = False
|
|
2344
|
-
for key, value in state.items():
|
|
2345
|
-
if isinstance(key, str) and key.startswith('_'):
|
|
2346
|
-
# Schema key — note any structural sentinels.
|
|
2347
|
-
# _divide is structural because it replaces a mother
|
|
2348
|
-
# with two daughters whose process instances need to
|
|
2349
|
-
# be re-discovered by find_instance_paths.
|
|
2350
|
-
if key in ('_add', '_remove', '_type', '_divide'):
|
|
2351
|
-
has_structural = True
|
|
2352
|
-
paths.append(path)
|
|
2353
|
-
continue
|
|
2354
|
-
sub_paths, sub_struct = Composite._walk_update(value, path + (key,))
|
|
2355
|
-
paths.extend(sub_paths)
|
|
2356
|
-
if sub_struct:
|
|
2357
|
-
has_structural = True
|
|
2358
|
-
return paths, has_structural
|
|
2359
|
-
|
|
2360
2444
|
def apply_updates(self, updates: List["Defer"]) -> List[Union[str, Tuple[str, ...]]]:
|
|
2361
2445
|
"""
|
|
2362
2446
|
Apply a series of deferred updates and record the resulting bridge outputs.
|
|
@@ -2373,10 +2457,12 @@ class Composite(Process):
|
|
|
2373
2457
|
"""
|
|
2374
2458
|
update_paths = []
|
|
2375
2459
|
had_structural_changes = False
|
|
2460
|
+
had_structural_sentinels = False # _add/_remove/_divide detected
|
|
2376
2461
|
|
|
2377
|
-
# Phase 1: Resolve all deferred updates and collect them
|
|
2462
|
+
# Phase 1: Resolve all deferred updates and collect them.
|
|
2463
|
+
# Path discovery + structural detection happen during reconcile
|
|
2464
|
+
# (Phase 2) via a ReconcileSummary sink — no separate walk pass.
|
|
2378
2465
|
resolved_updates = []
|
|
2379
|
-
had_structural_sentinels = False # _add/_remove/_divide detected
|
|
2380
2466
|
for defer in updates:
|
|
2381
2467
|
series = defer.get()
|
|
2382
2468
|
if series is None:
|
|
@@ -2385,13 +2471,6 @@ class Composite(Process):
|
|
|
2385
2471
|
series = [series]
|
|
2386
2472
|
|
|
2387
2473
|
for update_schema, update_state in series:
|
|
2388
|
-
# Single-pass walk: collects paths AND detects structural
|
|
2389
|
-
# change sentinels in one traversal instead of two.
|
|
2390
|
-
walk_paths, walk_struct = self._walk_update(update_state)
|
|
2391
|
-
update_paths.extend(walk_paths)
|
|
2392
|
-
if walk_struct and not had_structural_sentinels:
|
|
2393
|
-
had_structural_sentinels = True
|
|
2394
|
-
|
|
2395
2474
|
# read_bridge fast-paths to None when no bridge outputs
|
|
2396
2475
|
# are configured (vEcoli's case) — no walk happens.
|
|
2397
2476
|
bridge_update = self.read_bridge(update_state)
|
|
@@ -2430,9 +2509,22 @@ class Composite(Process):
|
|
|
2430
2509
|
combined_schema,
|
|
2431
2510
|
)
|
|
2432
2511
|
|
|
2433
|
-
# Reconcile all update states using the combined schema
|
|
2434
|
-
|
|
2435
|
-
|
|
2512
|
+
# Reconcile all update states using the combined schema.
|
|
2513
|
+
# Install a summary sink so reconcile populates leaf paths
|
|
2514
|
+
# and the structural-sentinel flag during its existing walk
|
|
2515
|
+
# — eliminates the redundant per-defer _walk_update pass.
|
|
2516
|
+
from bigraph_schema.methods.events import (
|
|
2517
|
+
ReconcileSummary, install_reconcile_sink, uninstall_reconcile_sink)
|
|
2518
|
+
summary = ReconcileSummary(paths=[])
|
|
2519
|
+
prev_sink = install_reconcile_sink(summary)
|
|
2520
|
+
try:
|
|
2521
|
+
all_states = [state for _, state in resolved_updates]
|
|
2522
|
+
combined_update = self.core.reconcile(combined_schema, all_states)
|
|
2523
|
+
finally:
|
|
2524
|
+
uninstall_reconcile_sink(prev_sink)
|
|
2525
|
+
update_paths = summary.paths
|
|
2526
|
+
if summary.has_structural:
|
|
2527
|
+
had_structural_sentinels = True
|
|
2436
2528
|
|
|
2437
2529
|
if combined_update:
|
|
2438
2530
|
# An update that lands inside an existing process's
|
|
@@ -2458,10 +2550,18 @@ class Composite(Process):
|
|
|
2458
2550
|
self.schema, combined_schema)
|
|
2459
2551
|
else:
|
|
2460
2552
|
apply_schema = combined_schema
|
|
2553
|
+
# Collect structural events (NodeAdded/NodeRemoved/
|
|
2554
|
+
# Divided) emitted by sentinel handlers during apply.
|
|
2555
|
+
# On structural ticks, used to update process_paths/
|
|
2556
|
+
# step_paths indexes incrementally instead of re-scanning
|
|
2557
|
+
# the full state via find_instance_paths.
|
|
2558
|
+
structural_events = [] if had_structural_sentinels else None
|
|
2461
2559
|
self.state, merges = self.core.apply(
|
|
2462
2560
|
apply_schema,
|
|
2463
2561
|
self.state,
|
|
2464
|
-
combined_update
|
|
2562
|
+
combined_update,
|
|
2563
|
+
update_has_structural=had_structural_sentinels,
|
|
2564
|
+
events=structural_events)
|
|
2465
2565
|
# For structural sentinels, apply may have mutated
|
|
2466
2566
|
# apply_schema in place (e.g. _divide pops/inserts
|
|
2467
2567
|
# keys in a dict schema). Propagate back to self.schema
|
|
@@ -2487,11 +2587,18 @@ class Composite(Process):
|
|
|
2487
2587
|
if had_structural_sentinels:
|
|
2488
2588
|
# Real structural change: processes may have been
|
|
2489
2589
|
# added/removed/replaced. Realize new state (to
|
|
2490
|
-
# instantiate added process declarations) then
|
|
2491
|
-
#
|
|
2590
|
+
# instantiate added process declarations) then update
|
|
2591
|
+
# the process/step indexes from the events apply emitted.
|
|
2492
2592
|
self.schema, self.state = self.core.realize(
|
|
2493
2593
|
self.schema, self.state)
|
|
2494
|
-
|
|
2594
|
+
if structural_events:
|
|
2595
|
+
self._apply_structural_events(structural_events)
|
|
2596
|
+
else:
|
|
2597
|
+
# Fallback: had_structural_sentinels was set but no
|
|
2598
|
+
# events emitted (e.g. _update_touches_process_path
|
|
2599
|
+
# detected an in-process update with no _add/_remove/
|
|
2600
|
+
# _divide sentinel). Full rescan is correct here.
|
|
2601
|
+
self.find_instance_paths(self.state)
|
|
2495
2602
|
self._build_view_project_cache()
|
|
2496
2603
|
if hasattr(self, 'expire_layer_walk_cache'):
|
|
2497
2604
|
self.expire_layer_walk_cache()
|