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.
Files changed (47) hide show
  1. {process_bigraph-1.2.1/process_bigraph.egg-info → process_bigraph-1.2.3}/PKG-INFO +2 -6
  2. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/composite.py +222 -81
  3. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/emitter.py +34 -5
  4. {process_bigraph-1.2.1 → process_bigraph-1.2.3/process_bigraph.egg-info}/PKG-INFO +2 -6
  5. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/SOURCES.txt +0 -11
  6. process_bigraph-1.2.3/process_bigraph.egg-info/requires.txt +6 -0
  7. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/pyproject.toml +13 -6
  8. process_bigraph-1.2.1/.github/workflows/notebook_to_html.yml +0 -43
  9. process_bigraph-1.2.1/.github/workflows/pytest.yml +0 -29
  10. process_bigraph-1.2.1/.gitignore +0 -18
  11. process_bigraph-1.2.1/CLA.md +0 -113
  12. process_bigraph-1.2.1/CODE_OF_CONDUCT.md +0 -137
  13. process_bigraph-1.2.1/CONTRIBUTING.md +0 -44
  14. process_bigraph-1.2.1/doc/_static/process-bigraph.png +0 -0
  15. process_bigraph-1.2.1/notebooks/process-bigraphs.ipynb +0 -739
  16. process_bigraph-1.2.1/notebooks/visualize_processes.ipynb +0 -237
  17. process_bigraph-1.2.1/process_bigraph.egg-info/requires.txt +0 -10
  18. process_bigraph-1.2.1/pytest.ini +0 -4
  19. process_bigraph-1.2.1/release.sh +0 -41
  20. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/AUTHORS.md +0 -0
  21. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/LICENSE +0 -0
  22. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/README.md +0 -0
  23. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/__init__.py +0 -0
  24. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/bundle.py +0 -0
  25. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/experiments/__init__.py +0 -0
  26. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/experiments/minimal_gillespie.py +0 -0
  27. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/nextflow.py +0 -0
  28. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/plumbing.py +0 -0
  29. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/__init__.py +0 -0
  30. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/dynamic_structure.py +0 -0
  31. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/examples.py +0 -0
  32. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/growth_division.py +0 -0
  33. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/math_expression.py +0 -0
  34. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/parameter_scan.py +0 -0
  35. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/processes/reaction.py +0 -0
  36. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/__init__.py +0 -0
  37. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/parallel.py +0 -0
  38. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/rest.py +0 -0
  39. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/protocols/socket.py +0 -0
  40. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/run.py +0 -0
  41. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/run_step.py +0 -0
  42. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/types/__init__.py +0 -0
  43. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/types/process.py +0 -0
  44. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph/units.py +0 -0
  45. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/dependency_links.txt +0 -0
  46. {process_bigraph-1.2.1 → process_bigraph-1.2.3}/process_bigraph.egg-info/top_level.txt +0 -0
  47. {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.1
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 the compiled-apply cache:
1457
+ # Structural change incoming — drop schema-derived caches:
1458
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()
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
- The compiled-apply cache is invalidated unconditionally because
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.invalidate_compiled_apply()
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
- config = {'skip_process_state': True, **document, **kwargs}
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
- # An update that lands inside an existing process's
2531
- # path wires, config, anything — is just as structural
2532
- # as _add/_remove: the link's compiled cache is stale
2533
- # and apply must see the full ProcessLink schema (else
2534
- # the stripped combined_schema falls through to
2535
- # apply(Node) and clobbers the link state). The same
2536
- # post-apply pipeline reuses the existing instance via
2537
- # realize_link's "instance already exists" branch.
2538
- if (not had_structural_sentinels
2539
- and self._update_touches_process_path(update_paths)):
2540
- had_structural_sentinels = True
2541
-
2542
- # Apply needs the live state schema for structural
2543
- # sentinels (_divide, _add, _remove) they must see
2544
- # the mother's existing schema to split/remove it.
2545
- # For non-structural updates the combined_schema is
2546
- # fine; resolving would drag in the entire state
2547
- # schema and make apply walk everything.
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 = combined_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 may have mutated
2566
- # apply_schema in place (e.g. _divide pops/inserts
2567
- # keys in a dict schema). Propagate back to self.schema
2568
- # so downstream realize sees the split.
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
- self.schema, self.state = self.core.realize(self.schema, self.state)
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 new state (to
2590
- # instantiate added process declarations) then update
2591
- # the process/step indexes from the events apply emitted.
2592
- self.schema, self.state = self.core.realize(
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 here.
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
- This is used to ensure that processes are rediscovered if a state update
2616
- altered a region where a process instance may be added, removed, or replaced.
2617
-
2618
- Fast path: skip entirely on value-only ticks. apply_updates already
2619
- runs find_instance_paths + _build_view_project_cache when it
2620
- detects structural changes (`_add` / `_remove` / `_type` keys),
2621
- and value-only updates can never add / remove / replace a process
2622
- slot. The walk is only needed if a structural change targeted a
2623
- process-adjacent path.
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 modified.
2798
+ update_paths: A list of hierarchical paths that were
2799
+ modified. Retained for caller compatibility; unused.
2627
2800
  """
2628
- # Skip the walk on value-only ticks. apply_updates exposes
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.1
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
@@ -0,0 +1,6 @@
1
+ bigraph-schema>=1.2.3
2
+ matplotlib
3
+ pint>=0.24.4
4
+ scipy>=1.8
5
+ pandas
6
+ sympy
@@ -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.1"
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