process-bigraph 1.2.1__tar.gz → 1.2.3__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.2.1/process_bigraph.egg-info → process_bigraph-1.2.3}/PKG-INFO +2 -6
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/composite.py +222 -81
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/emitter.py +34 -5
- {process_bigraph-1.2.1 → process_bigraph-1.2.3/process_bigraph.egg-info}/PKG-INFO +2 -6
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/SOURCES.txt +0 -11
- process_bigraph-1.2.3/process_bigraph.egg-info/requires.txt +6 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/pyproject.toml +13 -6
- process_bigraph-1.2.1/.github/workflows/notebook_to_html.yml +0 -43
- process_bigraph-1.2.1/.github/workflows/pytest.yml +0 -29
- process_bigraph-1.2.1/.gitignore +0 -18
- process_bigraph-1.2.1/CLA.md +0 -113
- process_bigraph-1.2.1/CODE_OF_CONDUCT.md +0 -137
- process_bigraph-1.2.1/CONTRIBUTING.md +0 -44
- process_bigraph-1.2.1/doc/_static/process-bigraph.png +0 -0
- process_bigraph-1.2.1/notebooks/process-bigraphs.ipynb +0 -739
- process_bigraph-1.2.1/notebooks/visualize_processes.ipynb +0 -237
- process_bigraph-1.2.1/process_bigraph.egg-info/requires.txt +0 -10
- process_bigraph-1.2.1/pytest.ini +0 -4
- process_bigraph-1.2.1/release.sh +0 -41
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/AUTHORS.md +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/LICENSE +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/README.md +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/__init__.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/nextflow.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/math_expression.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/socket.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/run.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/units.py +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.2.1 → process_bigraph-1.2.3}/setup.cfg +0 -0
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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
|
|
10
|
-
Requires-Dist: ipdb>=0.13.13
|
|
9
|
+
Requires-Dist: bigraph-schema>=1.2.3
|
|
11
10
|
Requires-Dist: matplotlib
|
|
12
11
|
Requires-Dist: pint>=0.24.4
|
|
13
12
|
Requires-Dist: scipy>=1.8
|
|
14
13
|
Requires-Dist: pandas
|
|
15
14
|
Requires-Dist: sympy
|
|
16
|
-
Requires-Dist: ipykernel
|
|
17
|
-
Requires-Dist: jupyter
|
|
18
|
-
Requires-Dist: notebook
|
|
19
15
|
Dynamic: license-file
|
|
20
16
|
|
|
21
17
|
# Process-Bigraph
|
|
@@ -1454,11 +1454,11 @@ class Composite(Process):
|
|
|
1454
1454
|
- self.process_paths
|
|
1455
1455
|
- self.step_paths
|
|
1456
1456
|
"""
|
|
1457
|
-
# Structural change incoming — drop
|
|
1457
|
+
# Structural change incoming — drop schema-derived caches:
|
|
1458
1458
|
# ``apply(dict)`` mutates schemas in place for ``_divide``
|
|
1459
|
-
# sentinels, so
|
|
1460
|
-
#
|
|
1461
|
-
self.core.
|
|
1459
|
+
# sentinels, so the promote() memo may now key off stale
|
|
1460
|
+
# schema layouts.
|
|
1461
|
+
self.core.invalidate_caches()
|
|
1462
1462
|
self.process_paths = find_instance_paths(state, 'process_bigraph.composite.Process')
|
|
1463
1463
|
if hasattr(self, 'step_paths'):
|
|
1464
1464
|
previous_step_paths = self.step_paths.keys()
|
|
@@ -1479,6 +1479,97 @@ class Composite(Process):
|
|
|
1479
1479
|
# do we want to do anything with these?
|
|
1480
1480
|
removed_front = self.front.pop(removed_key)
|
|
1481
1481
|
|
|
1482
|
+
def _realize_merge_subtrees(self, paths: List[tuple]) -> None:
|
|
1483
|
+
"""Realize only the subtrees touched by ``port_merges``.
|
|
1484
|
+
|
|
1485
|
+
``apply_updates`` collects every path that gained new schema
|
|
1486
|
+
info from a process's port defaults. The full-state realize
|
|
1487
|
+
that previously followed walked the whole tree on every tick
|
|
1488
|
+
with merges; on the brownian-particles test that meant ~2000
|
|
1489
|
+
full walks for 200s of sim. Incremental realize over just the
|
|
1490
|
+
affected paths is bounded by the merge count (~ports per tick).
|
|
1491
|
+
"""
|
|
1492
|
+
for path in paths:
|
|
1493
|
+
try:
|
|
1494
|
+
sub_schema, sub_state = self.core.traverse(
|
|
1495
|
+
self.schema, self.state, list(path))
|
|
1496
|
+
except Exception:
|
|
1497
|
+
continue
|
|
1498
|
+
if sub_schema is None:
|
|
1499
|
+
continue
|
|
1500
|
+
new_sub_schema, new_sub_state = self.core.realize(
|
|
1501
|
+
sub_schema, sub_state, path=tuple(path))
|
|
1502
|
+
if not path:
|
|
1503
|
+
self.state = new_sub_state
|
|
1504
|
+
continue
|
|
1505
|
+
parent_state = self.state
|
|
1506
|
+
missing = False
|
|
1507
|
+
for key in path[:-1]:
|
|
1508
|
+
if not isinstance(parent_state, dict) or key not in parent_state:
|
|
1509
|
+
missing = True
|
|
1510
|
+
break
|
|
1511
|
+
parent_state = parent_state[key]
|
|
1512
|
+
if missing or not isinstance(parent_state, dict):
|
|
1513
|
+
continue
|
|
1514
|
+
parent_state[path[-1]] = new_sub_state
|
|
1515
|
+
|
|
1516
|
+
def _realize_structural_subtrees(self, events: List[Any]) -> None:
|
|
1517
|
+
"""Realize only the subtrees that structural events touched.
|
|
1518
|
+
|
|
1519
|
+
After ``apply`` emits ``NodeAdded`` / ``Divided`` events, the new
|
|
1520
|
+
subtrees contain raw process declarations (config dicts) that
|
|
1521
|
+
need realizing into Process instances. The rest of the tree is
|
|
1522
|
+
already realized — re-walking it is wasted work that scales
|
|
1523
|
+
O(N) per division and so O(N²) over the simulation.
|
|
1524
|
+
|
|
1525
|
+
This walks each affected subtree via ``core.traverse`` to fetch
|
|
1526
|
+
its (sub_schema, sub_state), realizes that pair, and splices the
|
|
1527
|
+
realized state back into ``self.state``. ``NodeRemoved`` is a
|
|
1528
|
+
no-op here — there's nothing to realize at a removed path.
|
|
1529
|
+
``self.schema`` is left untouched: container schemas (Map,
|
|
1530
|
+
Tree) don't change shape on add/divide; dict containers were
|
|
1531
|
+
already mutated in place by ``apply``'s divide handler.
|
|
1532
|
+
"""
|
|
1533
|
+
affected_paths: List[tuple] = []
|
|
1534
|
+
for event in events:
|
|
1535
|
+
if isinstance(event, NodeAdded):
|
|
1536
|
+
affected_paths.append(event.path + (event.key,))
|
|
1537
|
+
elif isinstance(event, Divided):
|
|
1538
|
+
for d_key in event.daughter_keys:
|
|
1539
|
+
affected_paths.append(event.path + (d_key,))
|
|
1540
|
+
|
|
1541
|
+
for path in affected_paths:
|
|
1542
|
+
try:
|
|
1543
|
+
sub_schema, sub_state = self.core.traverse(
|
|
1544
|
+
self.schema, self.state, list(path))
|
|
1545
|
+
except Exception:
|
|
1546
|
+
continue
|
|
1547
|
+
if sub_schema is None:
|
|
1548
|
+
continue
|
|
1549
|
+
new_sub_schema, new_sub_state = self.core.realize(
|
|
1550
|
+
sub_schema, sub_state, path=tuple(path))
|
|
1551
|
+
# Splice the realized state back at its path. The parent
|
|
1552
|
+
# container is a mutable dict (Map keys are dict-keyed at
|
|
1553
|
+
# state level), so we rewrite the leaf entry. Schema dicts
|
|
1554
|
+
# may have been mutated by apply's divide handler already.
|
|
1555
|
+
#
|
|
1556
|
+
# Skip if any intermediate key has vanished — events on the
|
|
1557
|
+
# same tick can interact (e.g. an added daughter immediately
|
|
1558
|
+
# divided again, or a divided daughter removed by a step
|
|
1559
|
+
# cascade), leaving the path stale by the time we splice.
|
|
1560
|
+
# The corresponding NodeRemoved/Divided event covers the
|
|
1561
|
+
# cleanup; nothing to realize here.
|
|
1562
|
+
parent_state = self.state
|
|
1563
|
+
missing = False
|
|
1564
|
+
for key in path[:-1]:
|
|
1565
|
+
if not isinstance(parent_state, dict) or key not in parent_state:
|
|
1566
|
+
missing = True
|
|
1567
|
+
break
|
|
1568
|
+
parent_state = parent_state[key]
|
|
1569
|
+
if missing or not isinstance(parent_state, dict):
|
|
1570
|
+
continue
|
|
1571
|
+
parent_state[path[-1]] = new_sub_state
|
|
1572
|
+
|
|
1482
1573
|
def _apply_structural_events(self, events: List[Any]) -> None:
|
|
1483
1574
|
"""Update process/step indexes from apply-emitted structural events.
|
|
1484
1575
|
|
|
@@ -1490,14 +1581,14 @@ class Composite(Process):
|
|
|
1490
1581
|
- ``Divided``: drop the mother's entries, scan each daughter
|
|
1491
1582
|
subtree.
|
|
1492
1583
|
|
|
1493
|
-
|
|
1584
|
+
Schema-derived caches are invalidated unconditionally because
|
|
1494
1585
|
``apply(dict)``'s ``_divide`` branch mutates schemas in place.
|
|
1495
1586
|
|
|
1496
1587
|
Step network rebuild is conditional on step_paths actually
|
|
1497
1588
|
changing — value-only structural events that don't add/remove
|
|
1498
1589
|
any Step instances skip the rebuild.
|
|
1499
1590
|
"""
|
|
1500
|
-
self.core.
|
|
1591
|
+
self.core.invalidate_caches()
|
|
1501
1592
|
|
|
1502
1593
|
previous_step_paths = (set(self.step_paths.keys())
|
|
1503
1594
|
if hasattr(self, 'step_paths') else set())
|
|
@@ -1876,7 +1967,21 @@ class Composite(Process):
|
|
|
1876
1967
|
# ``link_state`` pass would re-add. Combining ``initial_state()``
|
|
1877
1968
|
# on top here would clobber correctly-divided values (e.g. a
|
|
1878
1969
|
# daughter's halved bulk array) with mother-shaped originals.
|
|
1879
|
-
|
|
1970
|
+
#
|
|
1971
|
+
# ``run_steps_on_init`` fires derivers (mass listeners,
|
|
1972
|
+
# post-division-mass-listener) at startup, mirroring v1's
|
|
1973
|
+
# ``Engine.run_steps()`` call in ``__init__``. The bundle was
|
|
1974
|
+
# saved with mother's pre-divide derived state (the post-divide
|
|
1975
|
+
# cascade halts at the structural change to match v1's
|
|
1976
|
+
# DivisionDetected exception). Running derivers on load
|
|
1977
|
+
# refreshes those values from the daughter's halved bulk before
|
|
1978
|
+
# any process reads them.
|
|
1979
|
+
config = {
|
|
1980
|
+
'skip_process_state': True,
|
|
1981
|
+
'run_steps_on_init': True,
|
|
1982
|
+
**document,
|
|
1983
|
+
**kwargs,
|
|
1984
|
+
}
|
|
1880
1985
|
return cls(config, core=core)
|
|
1881
1986
|
|
|
1882
1987
|
|
|
@@ -2181,6 +2286,16 @@ class Composite(Process):
|
|
|
2181
2286
|
update_paths = self.apply_updates(updates)
|
|
2182
2287
|
self.expire_process_paths(update_paths)
|
|
2183
2288
|
|
|
2289
|
+
# Opt-in halt: caller (e.g. EcoliSim) sets
|
|
2290
|
+
# ``_halt_after_structural`` to abort the post-divide step
|
|
2291
|
+
# cascade so daughters save with mother's pre-divide derived
|
|
2292
|
+
# state — mirroring v1 vivarium's ``DivisionDetected``
|
|
2293
|
+
# behavior. Default behavior continues cascading for
|
|
2294
|
+
# dynamic-structure tests that rely on spawn chains.
|
|
2295
|
+
if (getattr(self, '_halt_after_structural', False)
|
|
2296
|
+
and getattr(self, '_last_apply_structural', False)):
|
|
2297
|
+
return
|
|
2298
|
+
|
|
2184
2299
|
to_run = self.cycle_step_state()
|
|
2185
2300
|
|
|
2186
2301
|
if to_run:
|
|
@@ -2252,6 +2367,14 @@ class Composite(Process):
|
|
|
2252
2367
|
update_paths.append(('global_time',)) # updated global time can trigger steps
|
|
2253
2368
|
self.expire_process_paths(update_paths)
|
|
2254
2369
|
self.steps_run = set() # Reset for new timestep
|
|
2370
|
+
# Opt-in halt after structural change. EcoliSim sets
|
|
2371
|
+
# ``_halt_after_structural=True`` to mirror v1's halt
|
|
2372
|
+
# via DivisionDetected. Dynamic-structure tests leave
|
|
2373
|
+
# the flag unset for natural cascading.
|
|
2374
|
+
if (getattr(self, '_halt_after_structural', False)
|
|
2375
|
+
and getattr(self, '_last_apply_structural', False)):
|
|
2376
|
+
self.framework_time += _time.monotonic() - fw_start
|
|
2377
|
+
return
|
|
2255
2378
|
self.trigger_steps(update_paths)
|
|
2256
2379
|
self.framework_time += _time.monotonic() - fw_start
|
|
2257
2380
|
|
|
@@ -2527,29 +2650,39 @@ class Composite(Process):
|
|
|
2527
2650
|
had_structural_sentinels = True
|
|
2528
2651
|
|
|
2529
2652
|
if combined_update:
|
|
2530
|
-
#
|
|
2531
|
-
#
|
|
2532
|
-
#
|
|
2533
|
-
#
|
|
2534
|
-
#
|
|
2535
|
-
#
|
|
2536
|
-
#
|
|
2537
|
-
#
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
#
|
|
2544
|
-
# the
|
|
2545
|
-
#
|
|
2546
|
-
#
|
|
2547
|
-
#
|
|
2653
|
+
# NOTE: previously this path also forced
|
|
2654
|
+
# ``had_structural_sentinels = True`` whenever an
|
|
2655
|
+
# update touched a process subtree, because the older
|
|
2656
|
+
# code applied with the stripped ``combined_schema``
|
|
2657
|
+
# and would clobber link state. With ``promote``
|
|
2658
|
+
# supplying typed nodes (Array, Map, ProcessLink, ...)
|
|
2659
|
+
# at each apply path, value-only updates into a
|
|
2660
|
+
# process's path apply correctly without a full
|
|
2661
|
+
# realize. Only true sentinels (_add/_remove/_divide,
|
|
2662
|
+
# detected by the reconcile sink above) flip the
|
|
2663
|
+
# structural flag.
|
|
2664
|
+
pass
|
|
2665
|
+
|
|
2666
|
+
# Apply needs the live state schema so dispatch sees
|
|
2667
|
+
# the underlying types (Array, Map, ProcessLink, etc.)
|
|
2668
|
+
# at each path. ``combined_schema`` alone reflects the
|
|
2669
|
+
# update's wire shape (often dict-of-int-keys for
|
|
2670
|
+
# array-cell projections) and loses the typed
|
|
2671
|
+
# information present in ``self.schema``. Without
|
|
2672
|
+
# promoting, an update of shape ``{i: {j: delta}}``
|
|
2673
|
+
# would land at apply with a dict schema but an
|
|
2674
|
+
# ndarray state — bypassing the Array-aware overload.
|
|
2675
|
+
# ``promote`` walks only the paths combined_schema
|
|
2676
|
+
# touches (vs ``resolve`` which walks all of
|
|
2677
|
+
# self.schema each tick); structural sentinels still
|
|
2678
|
+
# use the full resolve so divide/add/remove see the
|
|
2679
|
+
# mother's existing schema.
|
|
2548
2680
|
if had_structural_sentinels:
|
|
2549
2681
|
apply_schema = self.core.resolve(
|
|
2550
2682
|
self.schema, combined_schema)
|
|
2551
2683
|
else:
|
|
2552
|
-
apply_schema =
|
|
2684
|
+
apply_schema = self.core.promote(
|
|
2685
|
+
self.schema, combined_schema)
|
|
2553
2686
|
# Collect structural events (NodeAdded/NodeRemoved/
|
|
2554
2687
|
# Divided) emitted by sentinel handlers during apply.
|
|
2555
2688
|
# On structural ticks, used to update process_paths/
|
|
@@ -2562,18 +2695,36 @@ class Composite(Process):
|
|
|
2562
2695
|
combined_update,
|
|
2563
2696
|
update_has_structural=had_structural_sentinels,
|
|
2564
2697
|
events=structural_events)
|
|
2565
|
-
# For structural sentinels, apply
|
|
2566
|
-
# apply_schema in place
|
|
2567
|
-
#
|
|
2568
|
-
#
|
|
2698
|
+
# For structural sentinels, apply mutates the
|
|
2699
|
+
# access-normalized form of ``apply_schema`` in place
|
|
2700
|
+
# (e.g. ``_divide`` pops the mother key and inserts
|
|
2701
|
+
# daughter keys in a dict subschema). ``apply_schema``
|
|
2702
|
+
# itself is the un-normalized input — it does NOT see
|
|
2703
|
+
# those mutations. Pull the cached normalized form
|
|
2704
|
+
# via ``access`` (cache hit since ``core.apply`` just
|
|
2705
|
+
# populated it) so ``self.schema`` reflects the post-
|
|
2706
|
+
# divide structure for downstream consumers.
|
|
2569
2707
|
if had_structural_sentinels:
|
|
2570
|
-
self.schema = apply_schema
|
|
2708
|
+
self.schema = self.core.access(apply_schema)
|
|
2571
2709
|
|
|
2572
2710
|
if merges:
|
|
2573
2711
|
had_structural_changes = True
|
|
2574
2712
|
self.schema = self.core.resolve_merges(
|
|
2575
2713
|
self.schema,
|
|
2576
2714
|
merges)
|
|
2715
|
+
# Track exactly which paths gained new schema info so
|
|
2716
|
+
# we can realize only those subtrees instead of the
|
|
2717
|
+
# whole tree (full realize was the dominant per-tick
|
|
2718
|
+
# framework cost on tests with port_merges every tick,
|
|
2719
|
+
# e.g. brownian_particles' 2000 ticks each triggering
|
|
2720
|
+
# a full-state walk).
|
|
2721
|
+
if not hasattr(self, '_merge_paths_pending'):
|
|
2722
|
+
self._merge_paths_pending = []
|
|
2723
|
+
for entry in merges:
|
|
2724
|
+
# merges are (path, subschema, link_path); we
|
|
2725
|
+
# only need the path prefix.
|
|
2726
|
+
if entry and entry[0] is not None:
|
|
2727
|
+
self._merge_paths_pending.append(tuple(entry[0]))
|
|
2577
2728
|
|
|
2578
2729
|
# Schema merges (from process outputs introducing new fields)
|
|
2579
2730
|
# require realize to fill defaults but do NOT add/remove
|
|
@@ -2581,23 +2732,42 @@ class Composite(Process):
|
|
|
2581
2732
|
# Only actual structural sentinels (_add/_remove/_divide)
|
|
2582
2733
|
# require find_instance_paths + build_step_network.
|
|
2583
2734
|
if had_structural_changes:
|
|
2584
|
-
|
|
2735
|
+
merge_paths = getattr(self, '_merge_paths_pending', None)
|
|
2736
|
+
if merge_paths:
|
|
2737
|
+
# Realize only the affected subtrees. Each merge path is
|
|
2738
|
+
# already covered by the incremental walk; deduplicate
|
|
2739
|
+
# under shortest-prefix containment so we don't visit
|
|
2740
|
+
# ancestor and descendant separately.
|
|
2741
|
+
unique = []
|
|
2742
|
+
for p in sorted(set(merge_paths), key=len):
|
|
2743
|
+
if any(p[:len(u)] == u for u in unique):
|
|
2744
|
+
continue
|
|
2745
|
+
unique.append(p)
|
|
2746
|
+
self._realize_merge_subtrees(unique)
|
|
2747
|
+
self._merge_paths_pending = []
|
|
2748
|
+
else:
|
|
2749
|
+
# Fallback: no path info available — full realize.
|
|
2750
|
+
self.schema, self.state = self.core.realize(
|
|
2751
|
+
self.schema, self.state)
|
|
2585
2752
|
self._build_view_project_cache()
|
|
2586
2753
|
|
|
2587
2754
|
if had_structural_sentinels:
|
|
2588
2755
|
# Real structural change: processes may have been
|
|
2589
|
-
# added/removed/replaced. Realize
|
|
2590
|
-
#
|
|
2591
|
-
# the
|
|
2592
|
-
|
|
2593
|
-
self.schema, self.state)
|
|
2756
|
+
# added/removed/replaced. Realize the affected subtrees
|
|
2757
|
+
# only (instantiating new daughter processes) instead of
|
|
2758
|
+
# re-walking the entire schema — full realize scales O(N)
|
|
2759
|
+
# per division and so O(N²) over a sim with N divisions.
|
|
2594
2760
|
if structural_events:
|
|
2761
|
+
self._realize_structural_subtrees(structural_events)
|
|
2595
2762
|
self._apply_structural_events(structural_events)
|
|
2596
2763
|
else:
|
|
2597
2764
|
# Fallback: had_structural_sentinels was set but no
|
|
2598
2765
|
# events emitted (e.g. _update_touches_process_path
|
|
2599
2766
|
# detected an in-process update with no _add/_remove/
|
|
2600
|
-
# _divide sentinel). Full rescan is correct
|
|
2767
|
+
# _divide sentinel). Full realize + rescan is correct
|
|
2768
|
+
# here — we don't know which subtrees changed.
|
|
2769
|
+
self.schema, self.state = self.core.realize(
|
|
2770
|
+
self.schema, self.state)
|
|
2601
2771
|
self.find_instance_paths(self.state)
|
|
2602
2772
|
self._build_view_project_cache()
|
|
2603
2773
|
if hasattr(self, 'expire_layer_walk_cache'):
|
|
@@ -2612,52 +2782,23 @@ class Composite(Process):
|
|
|
2612
2782
|
"""
|
|
2613
2783
|
Invalidate and refresh process paths if affected by recent updates.
|
|
2614
2784
|
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2785
|
+
Now a no-op: ``apply_updates`` already maintains ``process_paths``,
|
|
2786
|
+
``step_paths``, and the view-project cache via either
|
|
2787
|
+
``_apply_structural_events`` (incremental, when structural
|
|
2788
|
+
sentinels emit events) or its fallback ``find_instance_paths``
|
|
2789
|
+
+ ``_build_view_project_cache`` (when the sentinel was detected
|
|
2790
|
+
but no events surfaced). On value-only ticks no structural
|
|
2791
|
+
change occurred, so there is nothing to re-discover. Repeating
|
|
2792
|
+
the work here was the dominant framework cost on
|
|
2793
|
+
particle/division-heavy tests — ``find_instance_paths`` is a
|
|
2794
|
+
full-state walk that scaled with population, turning each
|
|
2795
|
+
division into O(N) work and the simulation into O(N²) overall.
|
|
2624
2796
|
|
|
2625
2797
|
Args:
|
|
2626
|
-
update_paths: A list of hierarchical paths that were
|
|
2798
|
+
update_paths: A list of hierarchical paths that were
|
|
2799
|
+
modified. Retained for caller compatibility; unused.
|
|
2627
2800
|
"""
|
|
2628
|
-
|
|
2629
|
-
# `_last_apply_structural` so we know whether to bother.
|
|
2630
|
-
if not getattr(self, '_last_apply_structural', True):
|
|
2631
|
-
return
|
|
2632
|
-
|
|
2633
|
-
# Quick check: if no update path shares a first element with any process path,
|
|
2634
|
-
# then no overlap is possible and we can skip the expensive scan.
|
|
2635
|
-
if not hasattr(self, '_process_path_roots'):
|
|
2636
|
-
self._process_path_roots = set()
|
|
2637
|
-
process_roots = self._process_path_roots
|
|
2638
|
-
if not process_roots:
|
|
2639
|
-
process_roots = {p[0] for p in self.process_paths if p}
|
|
2640
|
-
self._process_path_roots = process_roots
|
|
2641
|
-
|
|
2642
|
-
# Fast rejection: check if any update touches a process-adjacent path
|
|
2643
|
-
needs_check = False
|
|
2644
|
-
for update_path in update_paths:
|
|
2645
|
-
if update_path and update_path[0] in process_roots:
|
|
2646
|
-
needs_check = True
|
|
2647
|
-
break
|
|
2648
|
-
|
|
2649
|
-
if not needs_check:
|
|
2650
|
-
return
|
|
2651
|
-
|
|
2652
|
-
for update_path in update_paths:
|
|
2653
|
-
for process_path in self.process_paths.copy():
|
|
2654
|
-
# Match if update path completely overlaps the process path prefix
|
|
2655
|
-
updated = all(update == process for update, process in zip(update_path, process_path))
|
|
2656
|
-
if updated:
|
|
2657
|
-
self.find_instance_paths(self.state)
|
|
2658
|
-
self._build_view_project_cache()
|
|
2659
|
-
self._process_path_roots = set() # Reset for rebuild
|
|
2660
|
-
return # Exit early after one match, as paths are re-evaluated
|
|
2801
|
+
return
|
|
2661
2802
|
|
|
2662
2803
|
|
|
2663
2804
|
# ====================
|
|
@@ -37,13 +37,19 @@ def anyize_paths(tree):
|
|
|
37
37
|
else:
|
|
38
38
|
return 'node'
|
|
39
39
|
|
|
40
|
-
def emitter_from_wires(wires, address='local:RAMEmitter'):
|
|
41
|
-
'''Create an emitter step spec from wire mappings.
|
|
40
|
+
def emitter_from_wires(wires, address='local:RAMEmitter', subsample=1):
|
|
41
|
+
'''Create an emitter step spec from wire mappings.
|
|
42
|
+
|
|
43
|
+
``subsample`` (RAMEmitter / SQLiteEmitter only): record every
|
|
44
|
+
Nth composite tick. Default 1 records every tick.
|
|
45
|
+
'''
|
|
46
|
+
config = {'emit': anyize_paths(wires)}
|
|
47
|
+
if subsample is not None and int(subsample) > 1:
|
|
48
|
+
config['subsample'] = int(subsample)
|
|
42
49
|
return {
|
|
43
50
|
'_type': 'step',
|
|
44
51
|
'address': address,
|
|
45
|
-
'config':
|
|
46
|
-
'emit': anyize_paths(wires)},
|
|
52
|
+
'config': config,
|
|
47
53
|
'inputs': wires}
|
|
48
54
|
|
|
49
55
|
def collect_input_ports(state, path=None):
|
|
@@ -180,12 +186,35 @@ def tree_copy(state):
|
|
|
180
186
|
|
|
181
187
|
|
|
182
188
|
class RAMEmitter(Emitter):
|
|
183
|
-
'''Store historical states in memory.
|
|
189
|
+
'''Store historical states in memory.
|
|
190
|
+
|
|
191
|
+
``subsample`` records only every Nth composite tick (default 1 =
|
|
192
|
+
every tick). Use this for long runs or composites with heavy
|
|
193
|
+
state (large fields, many agents) to keep RAM bounded — the
|
|
194
|
+
saved time-series still reflects the simulation's true cadence
|
|
195
|
+
via each row's ``global_time`` field.
|
|
196
|
+
'''
|
|
197
|
+
config_schema = {
|
|
198
|
+
**Emitter.config_schema,
|
|
199
|
+
'subsample': {'_type': 'integer', '_default': 1},
|
|
200
|
+
}
|
|
201
|
+
|
|
184
202
|
def __init__(self, config, core):
|
|
185
203
|
super().__init__(config, core)
|
|
204
|
+
subsample = config.get('subsample')
|
|
205
|
+
self.subsample = 1 if subsample is None else int(subsample)
|
|
206
|
+
if self.subsample < 1:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f'RAMEmitter subsample must be >= 1, got {self.subsample}'
|
|
209
|
+
)
|
|
186
210
|
self.history = []
|
|
211
|
+
self._step = 0
|
|
187
212
|
|
|
188
213
|
def update(self, state) -> Dict:
|
|
214
|
+
step = self._step
|
|
215
|
+
self._step += 1
|
|
216
|
+
if step % self.subsample != 0:
|
|
217
|
+
return {}
|
|
189
218
|
self.history.append(tree_copy(state))
|
|
190
219
|
return {}
|
|
191
220
|
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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
|
|
10
|
-
Requires-Dist: ipdb>=0.13.13
|
|
9
|
+
Requires-Dist: bigraph-schema>=1.2.3
|
|
11
10
|
Requires-Dist: matplotlib
|
|
12
11
|
Requires-Dist: pint>=0.24.4
|
|
13
12
|
Requires-Dist: scipy>=1.8
|
|
14
13
|
Requires-Dist: pandas
|
|
15
14
|
Requires-Dist: sympy
|
|
16
|
-
Requires-Dist: ipykernel
|
|
17
|
-
Requires-Dist: jupyter
|
|
18
|
-
Requires-Dist: notebook
|
|
19
15
|
Dynamic: license-file
|
|
20
16
|
|
|
21
17
|
# Process-Bigraph
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
.gitignore
|
|
2
1
|
AUTHORS.md
|
|
3
|
-
CLA.md
|
|
4
|
-
CODE_OF_CONDUCT.md
|
|
5
|
-
CONTRIBUTING.md
|
|
6
2
|
LICENSE
|
|
7
3
|
README.md
|
|
8
4
|
pyproject.toml
|
|
9
|
-
pytest.ini
|
|
10
|
-
release.sh
|
|
11
|
-
.github/workflows/notebook_to_html.yml
|
|
12
|
-
.github/workflows/pytest.yml
|
|
13
|
-
doc/_static/process-bigraph.png
|
|
14
|
-
notebooks/process-bigraphs.ipynb
|
|
15
|
-
notebooks/visualize_processes.ipynb
|
|
16
5
|
process_bigraph/__init__.py
|
|
17
6
|
process_bigraph/bundle.py
|
|
18
7
|
process_bigraph/composite.py
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
1
5
|
[project]
|
|
2
6
|
name = "process-bigraph"
|
|
3
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.3"
|
|
4
8
|
description = "protocol and execution for compositional systems biology"
|
|
5
9
|
readme = "README.md"
|
|
6
10
|
requires-python = ">=3.11"
|
|
7
11
|
dependencies = [
|
|
8
|
-
"bigraph-schema",
|
|
9
|
-
"ipdb>=0.13.13",
|
|
12
|
+
"bigraph-schema>=1.2.3",
|
|
10
13
|
"matplotlib",
|
|
11
14
|
"pint>=0.24.4",
|
|
12
15
|
"scipy>=1.8",
|
|
13
16
|
"pandas",
|
|
14
17
|
"sympy",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"ipdb>=0.13.13",
|
|
15
23
|
"ipykernel",
|
|
16
24
|
"jupyter",
|
|
17
25
|
"notebook",
|
|
26
|
+
"pytest",
|
|
27
|
+
"pytest-cov",
|
|
18
28
|
]
|
|
19
29
|
|
|
20
30
|
[tool.setuptools]
|
|
@@ -25,6 +35,3 @@ packages = [
|
|
|
25
35
|
"process_bigraph.experiments",
|
|
26
36
|
"process_bigraph.types",
|
|
27
37
|
]
|
|
28
|
-
|
|
29
|
-
[tool.uv.sources]
|
|
30
|
-
bigraph-schema = { path = "../bigraph-schema", editable = true }
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
name: Convert Jupyter Notebook to HTML
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
paths:
|
|
6
|
-
- 'notebooks/process-bigraphs.ipynb'
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
convert:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
steps:
|
|
12
|
-
- name: Check out repository
|
|
13
|
-
uses: actions/checkout@main
|
|
14
|
-
with:
|
|
15
|
-
ref: main
|
|
16
|
-
fetch-depth: 0 # Fetch all history to have access to the gh-pages branch
|
|
17
|
-
|
|
18
|
-
- name: Set up Python
|
|
19
|
-
uses: actions/setup-python@main
|
|
20
|
-
with:
|
|
21
|
-
python-version: 3.x
|
|
22
|
-
|
|
23
|
-
- name: Install dependencies
|
|
24
|
-
run: |
|
|
25
|
-
python -m pip install --upgrade pip
|
|
26
|
-
pip install nbconvert
|
|
27
|
-
|
|
28
|
-
- name: Convert Jupyter Notebook to HTML
|
|
29
|
-
run: |
|
|
30
|
-
jupyter nbconvert --to html notebooks/process-bigraphs.ipynb
|
|
31
|
-
|
|
32
|
-
- name: Commit and push HTML to gh-pages branch
|
|
33
|
-
run: |
|
|
34
|
-
git config --local user.email "eagmon@github.com"
|
|
35
|
-
git config --local user.name "GitHub Action"
|
|
36
|
-
git fetch origin
|
|
37
|
-
mv notebooks/process-bigraphs.html /tmp/process-bigraphs.html
|
|
38
|
-
git checkout gh-pages || git checkout -b gh-pages
|
|
39
|
-
git pull origin gh-pages
|
|
40
|
-
mv /tmp/process-bigraphs.html notebooks/process-bigraphs.html
|
|
41
|
-
git add notebooks/process-bigraphs.html
|
|
42
|
-
git diff-index --quiet HEAD || git commit -m "Update HTML file"
|
|
43
|
-
git push origin gh-pages || true
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Run Pytest
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
steps:
|
|
12
|
-
- name: Check out repository
|
|
13
|
-
uses: actions/checkout@v2
|
|
14
|
-
|
|
15
|
-
- name: Set up Python
|
|
16
|
-
uses: actions/setup-python@v2
|
|
17
|
-
with:
|
|
18
|
-
python-version: 3.x
|
|
19
|
-
|
|
20
|
-
- name: Install dependencies
|
|
21
|
-
run: |
|
|
22
|
-
python -m pip install --upgrade pip
|
|
23
|
-
pip install pytest pytest-cov
|
|
24
|
-
pip install -e .
|
|
25
|
-
|
|
26
|
-
- name: Run pytest
|
|
27
|
-
run: |
|
|
28
|
-
pip install pytest
|
|
29
|
-
pytest --cov=process_bigraph --cov-report=term
|