process-bigraph 1.4.4__tar.gz → 1.4.6__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 (43) hide show
  1. {process_bigraph-1.4.4/process_bigraph.egg-info → process_bigraph-1.4.6}/PKG-INFO +3 -1
  2. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/composite.py +249 -2
  3. process_bigraph-1.4.6/process_bigraph/protocols/clusters/__init__.py +17 -0
  4. process_bigraph-1.4.6/process_bigraph/protocols/clusters/ec2_ssm.py +815 -0
  5. process_bigraph-1.4.6/process_bigraph/protocols/pool.py +309 -0
  6. process_bigraph-1.4.6/process_bigraph/protocols/session.py +130 -0
  7. {process_bigraph-1.4.4 → process_bigraph-1.4.6/process_bigraph.egg-info}/PKG-INFO +3 -1
  8. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph.egg-info/SOURCES.txt +4 -0
  9. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph.egg-info/requires.txt +3 -0
  10. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/pyproject.toml +11 -1
  11. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/AUTHORS.md +0 -0
  12. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/LICENSE +0 -0
  13. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/README.md +0 -0
  14. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/__init__.py +0 -0
  15. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/bundle.py +0 -0
  16. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/emitter.py +0 -0
  17. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/experiments/__init__.py +0 -0
  18. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/experiments/minimal_gillespie.py +0 -0
  19. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/nextflow.py +0 -0
  20. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/plumbing.py +0 -0
  21. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/__init__.py +0 -0
  22. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/dynamic_structure.py +0 -0
  23. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/examples.py +0 -0
  24. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/growth_division.py +0 -0
  25. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/math_expression.py +0 -0
  26. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/parameter_scan.py +0 -0
  27. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/processes/reaction.py +0 -0
  28. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/protocols/__init__.py +0 -0
  29. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/protocols/parallel.py +0 -0
  30. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/protocols/ray.py +0 -0
  31. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/protocols/rest.py +0 -0
  32. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/protocols/socket.py +0 -0
  33. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/run.py +0 -0
  34. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/run_step.py +0 -0
  35. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/server/__init__.py +0 -0
  36. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/server/rest.py +0 -0
  37. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/server/start.py +0 -0
  38. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/types/__init__.py +0 -0
  39. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/types/process.py +0 -0
  40. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph/units.py +0 -0
  41. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph.egg-info/dependency_links.txt +0 -0
  42. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/process_bigraph.egg-info/top_level.txt +0 -0
  43. {process_bigraph-1.4.4 → process_bigraph-1.4.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-bigraph
3
- Version: 1.4.4
3
+ Version: 1.4.6
4
4
  Summary: protocol and execution for compositional systems biology
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -20,6 +20,8 @@ Requires-Dist: fastapi-utils>=0.8.0; extra == "server-rest"
20
20
  Requires-Dist: uvicorn>=0.35.0; extra == "server-rest"
21
21
  Requires-Dist: fire; extra == "server-rest"
22
22
  Requires-Dist: typing-inspect>=0.9.0; extra == "server-rest"
23
+ Provides-Extra: ec2-ssm
24
+ Requires-Dist: boto3>=1.28; extra == "ec2-ssm"
23
25
  Dynamic: license-file
24
26
 
25
27
  # Process-Bigraph
@@ -19,6 +19,73 @@ import math
19
19
  import time as _time
20
20
  import numpy as np
21
21
 
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Optional invocation tracing
25
+ # ---------------------------------------------------------------------------
26
+ # When the env var ``PROCESS_BIGRAPH_TRACE_FILE`` is set to a writable path,
27
+ # every invocation of ``Composite.process_update`` (i.e. every Process/Step
28
+ # call routed through the framework) writes one JSONL record describing the
29
+ # call: path, class, global_time, interval, an input summary, and an output
30
+ # summary. Useful for diagnosing per-step divergence between two runs by
31
+ # diffing two trace files. No overhead when the env var is unset.
32
+ _TRACE_PATH = os.environ.get('PROCESS_BIGRAPH_TRACE_FILE')
33
+ _TRACE_FH = open(_TRACE_PATH, 'a', buffering=1) if _TRACE_PATH else None
34
+
35
+
36
+ def _summarize_value(value, depth=0):
37
+ """Lightweight, JSON-safe summary of an update fragment.
38
+
39
+ Tries to surface the values most useful for diffing two traces:
40
+ - Scalars and short strings inlined.
41
+ - Numpy arrays summarized as ``{shape, sum, mean, head}``.
42
+ - Dicts recursed (capped depth).
43
+ - Lists/tuples shown as their first few items.
44
+ """
45
+ if depth > 3:
46
+ return f'<{type(value).__name__}>'
47
+ if value is None or isinstance(value, (bool, int, float, str)):
48
+ return value
49
+ if isinstance(value, np.ndarray):
50
+ try:
51
+ return {
52
+ '_np': True,
53
+ 'shape': list(value.shape),
54
+ 'dtype': str(value.dtype),
55
+ 'sum': float(value.sum()) if value.dtype.kind in 'fi' else None,
56
+ 'head': value.flatten()[:5].tolist() if value.size else [],
57
+ }
58
+ except Exception:
59
+ return f'<ndarray shape={value.shape}>'
60
+ if isinstance(value, dict):
61
+ return {k: _summarize_value(v, depth + 1) for k, v in list(value.items())[:32]}
62
+ if isinstance(value, (list, tuple)):
63
+ return [_summarize_value(v, depth + 1) for v in value[:10]]
64
+ return f'<{type(value).__name__}>'
65
+
66
+
67
+ def _trace_invoke(path, instance, state, interval, update):
68
+ """Append one JSONL record describing a process/step invocation."""
69
+ if _TRACE_FH is None:
70
+ return
71
+ try:
72
+ gt = state.get('global_time') if isinstance(state, dict) else None
73
+ rec = {
74
+ 'path': list(path) if isinstance(path, tuple) else path,
75
+ 'cls': type(instance).__name__,
76
+ 'gt': gt,
77
+ 'interval': interval,
78
+ 'input': _summarize_value(state),
79
+ 'output': _summarize_value(update),
80
+ }
81
+ _TRACE_FH.write(json.dumps(rec, default=str) + '\n')
82
+ except Exception as exc:
83
+ # Tracing must never break a sim. Log a single line and continue.
84
+ try:
85
+ _TRACE_FH.write(json.dumps({'trace_error': str(exc)}) + '\n')
86
+ except Exception:
87
+ pass
88
+
22
89
  from typing import (
23
90
  Any, Dict, List, Optional, Set, Tuple, Union,
24
91
  Mapping, MutableMapping, Sequence,
@@ -1073,6 +1140,33 @@ class Process(Open):
1073
1140
  """
1074
1141
  return {}
1075
1142
 
1143
+ def reconfigure(self, config: Dict[str, Any]) -> None:
1144
+ """Re-bind cheap, per-sim configuration without re-running the
1145
+ expensive parts of ``initialize``.
1146
+
1147
+ Default implementation re-runs ``initialize(config)``, which
1148
+ preserves backwards compatibility but pays the full cold-start
1149
+ cost. Subclasses with expensive state (loaded scientific models,
1150
+ JIT caches, GPU contexts, persistent solver bases) should
1151
+ override to update only the cheap fields, leaving the
1152
+ expensive ones in place.
1153
+
1154
+ Used by ``ActorPool`` + ``Session`` (see
1155
+ ``doc/distributed_lifecycles.md``) to claim a pool actor for
1156
+ one Composite's sim and rebind its per-sim parameters
1157
+ (e.g. cell_keys for a sharded dFBA actor) without paying the
1158
+ cobra-Model load again. Without this hook, every
1159
+ ``with Session(pool, ...)`` would have to spawn fresh actors,
1160
+ defeating the pool's point.
1161
+
1162
+ Args:
1163
+ config: New per-sim configuration. Subclasses decide which
1164
+ keys are cheap to rebind vs. require a full re-init;
1165
+ ones that require full re-init can fall through to
1166
+ ``self.initialize(config)``.
1167
+ """
1168
+ self.initialize(config)
1169
+
1076
1170
 
1077
1171
  def as_step(inputs, outputs, name=None, aliases=None):
1078
1172
  """
@@ -1473,6 +1567,122 @@ class Composite(Process):
1473
1567
  if flush is not None:
1474
1568
  flush()
1475
1569
 
1570
+ def _partition_processes_by_runtime(self):
1571
+ """Inspect every process_path and split into:
1572
+
1573
+ - ``managed_paths``: set of process paths whose ``_protocol_runtime``
1574
+ implements ``tick_lifecycle``. These are skipped in the per-process
1575
+ invoke loop and instead dispatched once-per-runtime via
1576
+ ``_run_tick_lifecycle``.
1577
+ - ``runtime_groups``: list of ``(runtime, [(path, process), ...])``,
1578
+ one entry per distinct runtime that opted into tick_lifecycle.
1579
+
1580
+ Runtimes that only implement the older ``flush_pending`` hook (no
1581
+ ``tick_lifecycle``) keep going through the per-process loop —
1582
+ their batching of remote dispatch is unchanged.
1583
+ """
1584
+ managed: set = set()
1585
+ groups: dict = {}
1586
+ for path in self.process_paths:
1587
+ process = get_path(self.state, path)
1588
+ instance = process.get('instance') if isinstance(process, dict) else None
1589
+ if instance is None:
1590
+ continue
1591
+ rt = getattr(instance, '_protocol_runtime', None)
1592
+ if rt is None or not hasattr(rt, 'tick_lifecycle'):
1593
+ continue
1594
+ managed.add(path)
1595
+ entry = groups.setdefault(id(rt), (rt, []))
1596
+ entry[1].append((path, process))
1597
+ return managed, list(groups.values())
1598
+
1599
+ def _run_tick_lifecycle(
1600
+ self,
1601
+ runtime,
1602
+ group,
1603
+ end_time: float,
1604
+ full_step: float,
1605
+ force_complete: bool,
1606
+ ) -> float:
1607
+ """Delegate the full invoke+apply lifecycle for a runtime's managed
1608
+ processes to its ``tick_lifecycle`` method. Returns the updated
1609
+ ``full_step`` (smallest interval across all groups + plain procs).
1610
+
1611
+ The runtime returns one combined Defer covering all its processes;
1612
+ we stash that under a ``common_path`` key in ``self.front`` so the
1613
+ apply pass walks the schema once for the entire batch instead of
1614
+ N times (one per process).
1615
+ """
1616
+ # Build the list of ProcessTickRequest dicts the runtime expects.
1617
+ requests = [
1618
+ {
1619
+ 'path': path,
1620
+ 'instance': process['instance'],
1621
+ 'interval': process.get('interval'),
1622
+ }
1623
+ for path, process in group
1624
+ ]
1625
+ # Pass composite (self) so the runtime can use whatever it needs
1626
+ # — state (composite.state), schema (composite.schema), core
1627
+ # (composite.core), and projection helpers (composite._cached_project)
1628
+ # — without us prematurely committing to a narrower contract.
1629
+ result = runtime.tick_lifecycle(
1630
+ processes=requests,
1631
+ composite=self,
1632
+ global_time=self.state['global_time'],
1633
+ end_time=end_time,
1634
+ force_complete=force_complete,
1635
+ )
1636
+ if result is None:
1637
+ # Runtime declined — caller should fall back to per-process
1638
+ # path. We don't currently support partial fallback in the
1639
+ # same tick; runtimes that may decline should not implement
1640
+ # tick_lifecycle.
1641
+ return full_step
1642
+
1643
+ next_time = result['next_time']
1644
+ process_paths = result['process_paths']
1645
+ applied = result.get('applied', False)
1646
+
1647
+ interval = next_time - self.state['global_time']
1648
+ if interval < full_step:
1649
+ full_step = interval
1650
+
1651
+ # Update each process's front entry with the next scheduled time —
1652
+ # other Composite logic (expire_process_paths, scheduling) reads
1653
+ # these per-path. Keep them in sync with the runtime's batch.
1654
+ for p in process_paths:
1655
+ if p not in self.front:
1656
+ self.front[p] = empty_front(self.state['global_time'])
1657
+ self.front[p]['time'] = next_time
1658
+ # Per-process update slot empty: the actual delta is either
1659
+ # carried by the combined defer at common_path (v2 path) or
1660
+ # already applied to ``composite.state`` directly by the
1661
+ # runtime (v3 path with ``applied=True``).
1662
+ self.front[p]['update'] = {}
1663
+
1664
+ if applied:
1665
+ # v3 — runtime mutated state directly. Skip apply_updates entirely
1666
+ # for these processes; the framework only needs to advance time.
1667
+ # Useful when per-cell/per-tick reconcile cost dominates and the
1668
+ # runtime can do a vectorized array op instead of a schema walk.
1669
+ #
1670
+ # ``run()`` decides whether to call ``apply_updates`` based on
1671
+ # whether front[path]['update'] is truthy — by leaving them all
1672
+ # empty here AND not placing a defer at common_path, we ensure
1673
+ # apply_updates is never called for this tick's managed group.
1674
+ return full_step
1675
+
1676
+ # v2 — runtime returned a combined Defer; framework runs apply_updates
1677
+ # on it once for the whole batch.
1678
+ common_path = result['common_path']
1679
+ defer = result['defer']
1680
+ if common_path not in self.front:
1681
+ self.front[common_path] = empty_front(self.state['global_time'])
1682
+ self.front[common_path]['time'] = next_time
1683
+ self.front[common_path]['update'] = defer
1684
+ return full_step
1685
+
1476
1686
  def find_instance_paths(self, state: Dict[str, Any]) -> None:
1477
1687
  """
1478
1688
  Identify all Step and Process instances in the current state.
@@ -2399,16 +2609,36 @@ class Composite(Process):
2399
2609
  while self.state['global_time'] < end_time or force_complete:
2400
2610
  full_step = math.inf
2401
2611
 
2402
- # Run each process and compute the minimum time step that advances simulation
2612
+ # Partition processes: ones whose runtime opts into batched
2613
+ # tick_lifecycle vs the regular per-process path. The
2614
+ # runtime-managed group(s) get one method call per runtime,
2615
+ # which (a) skips N _cached_view + N invoke calls in this
2616
+ # tick's invoke pass, and (b) returns one combined Defer that
2617
+ # apply_updates walks once instead of N times.
2618
+ managed_paths, runtime_groups = self._partition_processes_by_runtime()
2619
+
2620
+ # Run each plain (non-runtime-managed) process and compute the
2621
+ # minimum time step that advances simulation.
2403
2622
  if self._parallel_processes and len(self.process_paths) > 1:
2404
2623
  full_step = self._run_processes_layer_parallel(
2405
- end_time, full_step, force_complete)
2624
+ end_time, full_step, force_complete,
2625
+ skip_paths=managed_paths)
2406
2626
  else:
2407
2627
  for path in self.process_paths:
2628
+ if path in managed_paths:
2629
+ continue
2408
2630
  process = get_path(self.state, path)
2409
2631
  full_step = self.run_process(
2410
2632
  path, process, end_time, full_step, force_complete)
2411
2633
 
2634
+ # Now dispatch each runtime group via its tick_lifecycle hook.
2635
+ # Each call returns one combined Defer (placed at a common-path
2636
+ # slot in self.front) so the downstream apply_updates walks
2637
+ # the schema once for the entire batch.
2638
+ for rt, group in runtime_groups:
2639
+ full_step = self._run_tick_lifecycle(
2640
+ rt, group, end_time, full_step, force_complete)
2641
+
2412
2642
  if full_step == math.inf:
2413
2643
  # No process ran — jump to the next scheduled process time
2414
2644
  next_event = end_time
@@ -2543,6 +2773,7 @@ class Composite(Process):
2543
2773
  end_time: float,
2544
2774
  full_step: float,
2545
2775
  force_complete: bool,
2776
+ skip_paths: set = None,
2546
2777
  ) -> float:
2547
2778
  """Parallel analog of the per-tick `for path in self.process_paths`
2548
2779
  loop. Splits run_process into a schedule pass (sequential, cheap)
@@ -2560,10 +2791,17 @@ class Composite(Process):
2560
2791
 
2561
2792
  Not safe when processes share mutable state or hold global locks.
2562
2793
  Like `parallel_steps`, this is opt-in for that reason.
2794
+
2795
+ ``skip_paths`` (set of paths) is consulted in the schedule pass
2796
+ and any matching process is skipped — used by ``run()`` to exclude
2797
+ runtime-managed processes (those handled via ``tick_lifecycle``).
2563
2798
  """
2799
+ skip_paths = skip_paths or set()
2564
2800
  # ---- schedule pass: decide which processes are due now ---------- #
2565
2801
  due = [] # (path, process_dict, state, future_time, process_interval)
2566
2802
  for path in self.process_paths:
2803
+ if path in skip_paths:
2804
+ continue
2567
2805
  process = get_path(self.state, path)
2568
2806
  if path not in self.front:
2569
2807
  self.front[path] = empty_front(self.state['global_time'])
@@ -2662,6 +2900,15 @@ class Composite(Process):
2662
2900
  t0 = _time.monotonic()
2663
2901
  update = process['instance'].invoke(clean_state, interval)
2664
2902
  self.process_update_time += _time.monotonic() - t0
2903
+ if _TRACE_FH is not None:
2904
+ # Resolve SyncUpdate / Defer to plain dict for the trace; the
2905
+ # invoke return is opaque (SyncUpdate wraps a dict). We only need
2906
+ # a snapshot for the trace, so pull .get() if available.
2907
+ try:
2908
+ resolved = update.get() if hasattr(update, 'get') else update
2909
+ except Exception:
2910
+ resolved = '<unresolvable>'
2911
+ _trace_invoke(path, process['instance'], clean_state, interval, resolved)
2665
2912
  # This nested function projects the update into the global state at the given path
2666
2913
  def defer_project(update_results: Any, args: Tuple[Any, Any, Union[str, Tuple[str, ...]]]) -> Any:
2667
2914
  schema, state, process_path = args
@@ -0,0 +1,17 @@
1
+ """Cluster context managers for distributed process_bigraph runs.
2
+
3
+ Each module here defines one context-manager-shaped cluster type:
4
+
5
+ with EC2SSMRayCluster(...) as cluster:
6
+ pool = get_or_create_pool(MyActor, {...}, size=72, cluster=cluster)
7
+ ...
8
+
9
+ Cluster modules import their cloud SDKs lazily so this package itself
10
+ imports cleanly without any cloud extras installed. Install only what
11
+ you need:
12
+
13
+ pip install process-bigraph[ec2-ssm] # boto3 for EC2SSMRayCluster
14
+
15
+ See ``doc/distributed_lifecycles.md`` for how clusters fit into the
16
+ ``cluster ⊃ pool ⊃ session ⊃ tick`` layering.
17
+ """