process-bigraph 1.2.1__tar.gz → 1.2.2__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.2}/PKG-INFO +2 -6
  2. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/composite.py +206 -70
  3. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/emitter.py +34 -5
  4. {process_bigraph-1.2.1 → process_bigraph-1.2.2/process_bigraph.egg-info}/PKG-INFO +2 -6
  5. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph.egg-info/SOURCES.txt +0 -11
  6. process_bigraph-1.2.2/process_bigraph.egg-info/requires.txt +6 -0
  7. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/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.2}/AUTHORS.md +0 -0
  21. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/LICENSE +0 -0
  22. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/README.md +0 -0
  23. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/__init__.py +0 -0
  24. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/bundle.py +0 -0
  25. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/experiments/__init__.py +0 -0
  26. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/experiments/minimal_gillespie.py +0 -0
  27. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/nextflow.py +0 -0
  28. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/plumbing.py +0 -0
  29. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/__init__.py +0 -0
  30. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/dynamic_structure.py +0 -0
  31. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/examples.py +0 -0
  32. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/growth_division.py +0 -0
  33. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/math_expression.py +0 -0
  34. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/parameter_scan.py +0 -0
  35. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/processes/reaction.py +0 -0
  36. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/protocols/__init__.py +0 -0
  37. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/protocols/parallel.py +0 -0
  38. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/protocols/rest.py +0 -0
  39. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/protocols/socket.py +0 -0
  40. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/run.py +0 -0
  41. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/run_step.py +0 -0
  42. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/types/__init__.py +0 -0
  43. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/types/process.py +0 -0
  44. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph/units.py +0 -0
  45. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph.egg-info/dependency_links.txt +0 -0
  46. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/process_bigraph.egg-info/top_level.txt +0 -0
  47. {process_bigraph-1.2.1 → process_bigraph-1.2.2}/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.2
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
@@ -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
 
@@ -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/
@@ -2574,6 +2707,19 @@ class Composite(Process):
2574
2707
  self.schema = self.core.resolve_merges(
2575
2708
  self.schema,
2576
2709
  merges)
2710
+ # Track exactly which paths gained new schema info so
2711
+ # we can realize only those subtrees instead of the
2712
+ # whole tree (full realize was the dominant per-tick
2713
+ # framework cost on tests with port_merges every tick,
2714
+ # e.g. brownian_particles' 2000 ticks each triggering
2715
+ # a full-state walk).
2716
+ if not hasattr(self, '_merge_paths_pending'):
2717
+ self._merge_paths_pending = []
2718
+ for entry in merges:
2719
+ # merges are (path, subschema, link_path); we
2720
+ # only need the path prefix.
2721
+ if entry and entry[0] is not None:
2722
+ self._merge_paths_pending.append(tuple(entry[0]))
2577
2723
 
2578
2724
  # Schema merges (from process outputs introducing new fields)
2579
2725
  # require realize to fill defaults but do NOT add/remove
@@ -2581,23 +2727,42 @@ class Composite(Process):
2581
2727
  # Only actual structural sentinels (_add/_remove/_divide)
2582
2728
  # require find_instance_paths + build_step_network.
2583
2729
  if had_structural_changes:
2584
- self.schema, self.state = self.core.realize(self.schema, self.state)
2730
+ merge_paths = getattr(self, '_merge_paths_pending', None)
2731
+ if merge_paths:
2732
+ # Realize only the affected subtrees. Each merge path is
2733
+ # already covered by the incremental walk; deduplicate
2734
+ # under shortest-prefix containment so we don't visit
2735
+ # ancestor and descendant separately.
2736
+ unique = []
2737
+ for p in sorted(set(merge_paths), key=len):
2738
+ if any(p[:len(u)] == u for u in unique):
2739
+ continue
2740
+ unique.append(p)
2741
+ self._realize_merge_subtrees(unique)
2742
+ self._merge_paths_pending = []
2743
+ else:
2744
+ # Fallback: no path info available — full realize.
2745
+ self.schema, self.state = self.core.realize(
2746
+ self.schema, self.state)
2585
2747
  self._build_view_project_cache()
2586
2748
 
2587
2749
  if had_structural_sentinels:
2588
2750
  # 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)
2751
+ # added/removed/replaced. Realize the affected subtrees
2752
+ # only (instantiating new daughter processes) instead of
2753
+ # re-walking the entire schema full realize scales O(N)
2754
+ # per division and so O(N²) over a sim with N divisions.
2594
2755
  if structural_events:
2756
+ self._realize_structural_subtrees(structural_events)
2595
2757
  self._apply_structural_events(structural_events)
2596
2758
  else:
2597
2759
  # Fallback: had_structural_sentinels was set but no
2598
2760
  # events emitted (e.g. _update_touches_process_path
2599
2761
  # detected an in-process update with no _add/_remove/
2600
- # _divide sentinel). Full rescan is correct here.
2762
+ # _divide sentinel). Full realize + rescan is correct
2763
+ # here — we don't know which subtrees changed.
2764
+ self.schema, self.state = self.core.realize(
2765
+ self.schema, self.state)
2601
2766
  self.find_instance_paths(self.state)
2602
2767
  self._build_view_project_cache()
2603
2768
  if hasattr(self, 'expire_layer_walk_cache'):
@@ -2612,52 +2777,23 @@ class Composite(Process):
2612
2777
  """
2613
2778
  Invalidate and refresh process paths if affected by recent updates.
2614
2779
 
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.
2780
+ Now a no-op: ``apply_updates`` already maintains ``process_paths``,
2781
+ ``step_paths``, and the view-project cache via either
2782
+ ``_apply_structural_events`` (incremental, when structural
2783
+ sentinels emit events) or its fallback ``find_instance_paths``
2784
+ + ``_build_view_project_cache`` (when the sentinel was detected
2785
+ but no events surfaced). On value-only ticks no structural
2786
+ change occurred, so there is nothing to re-discover. Repeating
2787
+ the work here was the dominant framework cost on
2788
+ particle/division-heavy tests — ``find_instance_paths`` is a
2789
+ full-state walk that scaled with population, turning each
2790
+ division into O(N) work and the simulation into O(N²) overall.
2624
2791
 
2625
2792
  Args:
2626
- update_paths: A list of hierarchical paths that were modified.
2793
+ update_paths: A list of hierarchical paths that were
2794
+ modified. Retained for caller compatibility; unused.
2627
2795
  """
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
2796
+ return
2661
2797
 
2662
2798
 
2663
2799
  # ====================
@@ -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.2
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.2"
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
@@ -1,18 +0,0 @@
1
- .python-version
2
- .claude/settings.local.json
3
- __pycache__/
4
- .idea/
5
- *.ipynb_checkpoints/
6
- dist/
7
- .DS_Store
8
- .empty
9
- build/
10
- .venv/
11
- venv/
12
-
13
- process_bigraph.egg-info/
14
- out/
15
- .devenv*
16
- devenv.lock
17
- containers/
18
- uv.lock