process-bigraph 1.0.6__tar.gz → 1.0.7__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 (29) hide show
  1. {process_bigraph-1.0.6/process_bigraph.egg-info → process_bigraph-1.0.7}/PKG-INFO +1 -1
  2. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/composite.py +133 -23
  3. {process_bigraph-1.0.6 → process_bigraph-1.0.7/process_bigraph.egg-info}/PKG-INFO +1 -1
  4. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/pyproject.toml +1 -1
  5. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/AUTHORS.md +0 -0
  6. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/LICENSE +0 -0
  7. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/README.md +0 -0
  8. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/__init__.py +0 -0
  9. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/emitter.py +0 -0
  10. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/experiments/__init__.py +0 -0
  11. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/experiments/minimal_gillespie.py +0 -0
  12. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/processes/__init__.py +0 -0
  13. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/processes/examples.py +0 -0
  14. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/processes/growth_division.py +0 -0
  15. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/processes/math_expression.py +0 -0
  16. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/processes/parameter_scan.py +0 -0
  17. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/protocols/__init__.py +0 -0
  18. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/protocols/parallel.py +0 -0
  19. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/protocols/rest.py +0 -0
  20. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/protocols/socket.py +0 -0
  21. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/run.py +0 -0
  22. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/types/__init__.py +0 -0
  23. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/types/process.py +0 -0
  24. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph/units.py +0 -0
  25. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph.egg-info/SOURCES.txt +0 -0
  26. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph.egg-info/dependency_links.txt +0 -0
  27. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph.egg-info/requires.txt +0 -0
  28. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/process_bigraph.egg-info/top_level.txt +0 -0
  29. {process_bigraph-1.0.6 → process_bigraph-1.0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-bigraph
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: protocol and execution for compositional systems biology
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -16,6 +16,8 @@ import os
16
16
  import copy
17
17
  import json
18
18
  import math
19
+ import numpy as np
20
+
19
21
  from typing import (
20
22
  Any, Dict, List, Optional, Set, Tuple, Union,
21
23
  Mapping, MutableMapping, Sequence,
@@ -692,6 +694,9 @@ class Process(Open):
692
694
  update = self.update(state, interval)
693
695
  return SyncUpdate(update)
694
696
 
697
+ def calculate_timestep(self, interval, state):
698
+ return interval
699
+
695
700
  def update(self, state: Dict[str, Any], interval: float) -> Dict[str, Any]:
696
701
  """
697
702
  Override this method to implement the process logic.
@@ -906,7 +911,8 @@ class Composite(Process):
906
911
  'inputs': 'wires',
907
912
  'outputs': 'wires'
908
913
  },
909
- 'global_time_precision': 'maybe[float]'
914
+ 'global_time_precision': 'maybe[float]',
915
+ 'skip_initial_steps': 'maybe[boolean]'
910
916
  }
911
917
 
912
918
 
@@ -1011,11 +1017,16 @@ class Composite(Process):
1011
1017
  # A buffer for updates to be emitted at the composite's output interface.
1012
1018
  self.bridge_updates: List[Any] = []
1013
1019
 
1020
+ # Precompile view/project operations for fast runtime access.
1021
+ self._compiled_links = {}
1022
+ self._build_view_project_cache()
1023
+
1014
1024
  # Build the dependency network between steps and determine which steps should run first.
1015
1025
  self.build_step_network()
1016
1026
 
1017
1027
  # Run all steps that are ready on the first cycle.
1018
- self.run_steps(self.to_run)
1028
+ if not self.config.get('skip_initial_steps', False):
1029
+ self.run_steps(self.to_run)
1019
1030
 
1020
1031
  @classmethod
1021
1032
  def load(cls, path: str, core: Optional[Any] = None) -> "Composite":
@@ -1064,6 +1075,42 @@ class Composite(Process):
1064
1075
  # do we want to do anything with these?
1065
1076
  removed_front = self.front.pop(removed_key)
1066
1077
 
1078
+ def _build_view_project_cache(self) -> None:
1079
+ """Precompile view/project operations for each process path.
1080
+
1081
+ Delegates to core.precompile_link() which pre-resolves wire paths
1082
+ and precomputes projection schemas so that runtime view/project
1083
+ calls bypass schema traversal entirely.
1084
+ """
1085
+ self._compiled_links = {}
1086
+
1087
+ for path in list(self.process_paths) + list(self.step_paths):
1088
+ compiled = self.core.precompile_link(
1089
+ self.schema, self.state, path)
1090
+ if compiled is not None:
1091
+ self._compiled_links[path] = compiled
1092
+
1093
+ def _invalidate_caches(self) -> None:
1094
+ """Invalidate precompiled link caches, forcing rebuild on next use."""
1095
+ self._compiled_links = {}
1096
+
1097
+ def _cached_view(self, path: Tuple[str, ...]) -> Dict[str, Any]:
1098
+ """Fast view using precompiled link when available."""
1099
+ compiled = self._compiled_links.get(path)
1100
+ if compiled is not None and compiled.get('view') is not None:
1101
+ return self.core.view_fast(compiled['view'], self.state)
1102
+ return self.core.view(self.schema, self.state, path)
1103
+
1104
+ def _cached_project(self, path: Tuple[str, ...], view: Any,
1105
+ ports_key: str = 'outputs') -> Any:
1106
+ """Fast project using precompiled link when available."""
1107
+ if ports_key == 'outputs':
1108
+ compiled = self._compiled_links.get(path)
1109
+ if compiled is not None and compiled.get('project') is not None:
1110
+ return self.core.project_ports_fast(compiled['project'], view)
1111
+ return self.core.project(
1112
+ self.schema, self.state, path, view, ports_key)
1113
+
1067
1114
  def merge(self, schema: Dict[str, Any], state: Dict[str, Any], path: Optional[List[str]] = None) -> None:
1068
1115
  """
1069
1116
  Merge a new schema/state subtree into the Composite.
@@ -1082,6 +1129,7 @@ class Composite(Process):
1082
1129
  state)
1083
1130
 
1084
1131
  self.find_instance_paths(self.state)
1132
+ self._build_view_project_cache()
1085
1133
 
1086
1134
  def merge_schema(
1087
1135
  self,
@@ -1173,8 +1221,17 @@ class Composite(Process):
1173
1221
 
1174
1222
  os.makedirs(outdir, exist_ok=True)
1175
1223
  filepath = os.path.join(outdir, filename)
1176
- with open(filepath, 'w') as f:
1177
- json.dump(document, f, indent=4)
1224
+ # outjson = json.dumps(
1225
+ # document,
1226
+ # default=encode_key)
1227
+
1228
+ with open(filepath, 'w') as outfile:
1229
+ json.dump(
1230
+ document,
1231
+ outfile,
1232
+ indent=2,
1233
+ default=encode_key)
1234
+
1178
1235
  print(f"Saved composite to {filepath}")
1179
1236
 
1180
1237
 
@@ -1463,11 +1520,10 @@ class Composite(Process):
1463
1520
  state = future_front['state']
1464
1521
  else:
1465
1522
  # Otherwise, slice the current state for the process
1466
- state = self.core.view(
1467
- self.schema,
1468
- self.state,
1469
- path)
1470
- process_interval = process['interval']
1523
+ state = self._cached_view(path)
1524
+ state_interval = process['interval']
1525
+ process_interval = process['instance'].calculate_timestep(state_interval, state)
1526
+ process['interval'] = process_interval
1471
1527
 
1472
1528
  # Determine the target time for the next update
1473
1529
  future = (
@@ -1537,9 +1593,7 @@ class Composite(Process):
1537
1593
  if not isinstance(update_results, list):
1538
1594
  update_results = [update_results]
1539
1595
 
1540
- return [self.core.project(
1541
- schema,
1542
- state,
1596
+ return [self._cached_project(
1543
1597
  process_path,
1544
1598
  update_result,
1545
1599
  ports_key) for update_result in update_results]
@@ -1547,6 +1601,24 @@ class Composite(Process):
1547
1601
  # Return a deferred object that will project the update when requested
1548
1602
  return Defer(update, defer_project, (self.schema, self.state, path))
1549
1603
 
1604
+ @staticmethod
1605
+ def _has_structural_keys(state: Any) -> bool:
1606
+ """Check if a state dict contains keys that signal structural changes.
1607
+
1608
+ Structural changes (_add, _remove, _type) require re-running
1609
+ realize() and find_instance_paths(). Plain value updates do not.
1610
+ """
1611
+ if not isinstance(state, dict):
1612
+ return False
1613
+ for key, value in state.items():
1614
+ if key in ('_add', '_remove'):
1615
+ return True
1616
+ if key == '_type':
1617
+ return True
1618
+ if isinstance(value, dict) and Composite._has_structural_keys(value):
1619
+ return True
1620
+ return False
1621
+
1550
1622
  def apply_updates(self, updates: List["Defer"]) -> List[Union[str, Tuple[str, ...]]]:
1551
1623
  """
1552
1624
  Apply a series of deferred updates and record the resulting bridge outputs.
@@ -1562,6 +1634,7 @@ class Composite(Process):
1562
1634
  A list of update paths (used to determine which processes to refresh).
1563
1635
  """
1564
1636
  update_paths = []
1637
+ had_structural_changes = False
1565
1638
 
1566
1639
  for defer in updates:
1567
1640
  # Resolve deferred computation to get update(s)
@@ -1572,13 +1645,14 @@ class Composite(Process):
1572
1645
  series = [series]
1573
1646
 
1574
1647
  for update_schema, update_state in series:
1575
- # if update and isinstance(update, dict) and 'environment' in update and update['environment'] and isinstance(update['environment'], dict) and '_react' in update['environment']:
1576
- # import ipdb; ipdb.set_trace()
1577
-
1578
1648
  # Extract all hierarchical paths touched by this update
1579
1649
  paths = hierarchy_depth(update_state)
1580
1650
  update_paths.extend(paths.keys())
1581
1651
 
1652
+ # Detect structural changes before applying
1653
+ if not had_structural_changes:
1654
+ had_structural_changes = self._has_structural_keys(update_state)
1655
+
1582
1656
  # Apply update directly to the internal state,
1583
1657
  # using the schema from the link itself
1584
1658
  self.state, merges = self.core.apply(
@@ -1586,20 +1660,22 @@ class Composite(Process):
1586
1660
  self.state,
1587
1661
  update_state)
1588
1662
 
1589
- self.schema = self.core.resolve_merges(
1590
- self.schema,
1591
- merges)
1663
+ if merges:
1664
+ had_structural_changes = True
1665
+ self.schema = self.core.resolve_merges(
1666
+ self.schema,
1667
+ merges)
1592
1668
 
1593
1669
  # Read updated bridge outputs, if available
1594
1670
  bridge_update = self.read_bridge(update_state)
1595
1671
  if bridge_update:
1596
1672
  self.bridge_updates.append(bridge_update)
1597
1673
 
1598
- self.schema, self.state = self.core.realize(self.schema, self.state)
1599
-
1600
- # TODO: are we doing this twice?
1601
- # Refresh process and step instance paths
1602
- self.find_instance_paths(self.state)
1674
+ # Only run expensive realize and instance discovery when structural changes occurred
1675
+ if had_structural_changes:
1676
+ self.schema, self.state = self.core.realize(self.schema, self.state)
1677
+ self.find_instance_paths(self.state)
1678
+ self._build_view_project_cache()
1603
1679
 
1604
1680
  return update_paths
1605
1681
 
@@ -1613,12 +1689,33 @@ class Composite(Process):
1613
1689
  Args:
1614
1690
  update_paths: A list of hierarchical paths that were modified.
1615
1691
  """
1692
+ # Quick check: if no update path shares a first element with any process path,
1693
+ # then no overlap is possible and we can skip the expensive scan.
1694
+ if not hasattr(self, '_process_path_roots'):
1695
+ self._process_path_roots = set()
1696
+ process_roots = self._process_path_roots
1697
+ if not process_roots:
1698
+ process_roots = {p[0] for p in self.process_paths if p}
1699
+ self._process_path_roots = process_roots
1700
+
1701
+ # Fast rejection: check if any update touches a process-adjacent path
1702
+ needs_check = False
1703
+ for update_path in update_paths:
1704
+ if update_path and update_path[0] in process_roots:
1705
+ needs_check = True
1706
+ break
1707
+
1708
+ if not needs_check:
1709
+ return
1710
+
1616
1711
  for update_path in update_paths:
1617
1712
  for process_path in self.process_paths.copy():
1618
1713
  # Match if update path completely overlaps the process path prefix
1619
1714
  updated = all(update == process for update, process in zip(update_path, process_path))
1620
1715
  if updated:
1621
1716
  self.find_instance_paths(self.state)
1717
+ self._build_view_project_cache()
1718
+ self._process_path_roots = set() # Reset for rebuild
1622
1719
  return # Exit early after one match, as paths are re-evaluated
1623
1720
 
1624
1721
 
@@ -1663,3 +1760,16 @@ class Composite(Process):
1663
1760
  self.run(interval)
1664
1761
 
1665
1762
  return self.bridge_updates
1763
+
1764
+
1765
+ def encode_key(o):
1766
+ if isinstance(o, np.ndarray):
1767
+ o.tolist()
1768
+
1769
+ elif isinstance(o, dict):
1770
+ return {
1771
+ str(k): encode_key(v)
1772
+ for k, v in o.items()}
1773
+
1774
+ else:
1775
+ return o
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-bigraph
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: protocol and execution for compositional systems biology
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "process-bigraph"
3
- version = "1.0.6"
3
+ version = "1.0.7"
4
4
  description = "protocol and execution for compositional systems biology"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
File without changes