process-bigraph 1.0.5__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.
- process_bigraph-1.0.7/PKG-INFO +146 -0
- process_bigraph-1.0.7/README.md +126 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/composite.py +184 -60
- process_bigraph-1.0.7/process_bigraph/processes/math_expression.py +525 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/types/process.py +55 -7
- process_bigraph-1.0.7/process_bigraph.egg-info/PKG-INFO +146 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph.egg-info/SOURCES.txt +1 -11
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph.egg-info/requires.txt +6 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/pyproject.toml +7 -1
- process_bigraph-1.0.5/.github/workflows/notebook_to_html.yml +0 -43
- process_bigraph-1.0.5/.github/workflows/pytest.yml +0 -29
- process_bigraph-1.0.5/.gitignore +0 -17
- process_bigraph-1.0.5/CLA.md +0 -113
- process_bigraph-1.0.5/CODE_OF_CONDUCT.md +0 -137
- process_bigraph-1.0.5/CONTRIBUTING.md +0 -44
- process_bigraph-1.0.5/PKG-INFO +0 -63
- process_bigraph-1.0.5/README.md +0 -49
- process_bigraph-1.0.5/doc/_static/process-bigraph.png +0 -0
- process_bigraph-1.0.5/notebooks/process-bigraphs.ipynb +0 -739
- process_bigraph-1.0.5/notebooks/visualize_processes.ipynb +0 -237
- process_bigraph-1.0.5/process_bigraph.egg-info/PKG-INFO +0 -63
- process_bigraph-1.0.5/pytest.ini +0 -4
- process_bigraph-1.0.5/release.sh +0 -41
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/AUTHORS.md +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/LICENSE +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/__init__.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/emitter.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/protocols/socket.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/run.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph/units.py +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.0.5 → process_bigraph-1.0.7}/setup.cfg +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: process-bigraph
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: protocol and execution for compositional systems biology
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: AUTHORS.md
|
|
9
|
+
Requires-Dist: bigraph-schema
|
|
10
|
+
Requires-Dist: ipdb>=0.13.13
|
|
11
|
+
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: pint>=0.24.4
|
|
13
|
+
Requires-Dist: scipy>=1.8
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: sympy
|
|
16
|
+
Requires-Dist: ipykernel
|
|
17
|
+
Requires-Dist: jupyter
|
|
18
|
+
Requires-Dist: notebook
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Process-Bigraph
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/process-bigraph/)
|
|
24
|
+
[](https://vivarium-collective.github.io/process-bigraph/notebooks/index.html)
|
|
25
|
+
|
|
26
|
+
**Process-Bigraph** is a compositional runtime and protocol for building and executing
|
|
27
|
+
**multiscale biological models from interoperable processes**.
|
|
28
|
+
|
|
29
|
+
It provides a shared architectural layer for:
|
|
30
|
+
- declaring **process interfaces**
|
|
31
|
+
- wiring processes through **typed shared state**
|
|
32
|
+
- orchestrating execution across **heterogeneous timescales**
|
|
33
|
+
- supporting **dynamic structure** (workflows, division, graph rewrites)
|
|
34
|
+
|
|
35
|
+
Process-Bigraph is the execution core of **Vivarium 2.0**, designed to integrate models
|
|
36
|
+
built with different formalisms—including ODEs, FBA, agent-based models, spatial solvers,
|
|
37
|
+
and machine-learning components—into a single coherent simulation.
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<img src="https://github.com/vivarium-collective/process-bigraph/blob/main/doc/_static/composition_framework.png?raw=true"
|
|
41
|
+
width="800"
|
|
42
|
+
alt="Process Bigraph composition framework">
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🧩 What is a Process Bigraph?
|
|
48
|
+
|
|
49
|
+
A **process bigraph** combines:
|
|
50
|
+
|
|
51
|
+
- **Typed stores** — hierarchical, schema-validated state defined with
|
|
52
|
+
[**bigraph-schema**](https://github.com/vivarium-collective/bigraph-schema)
|
|
53
|
+
- **Processes** — executable components with explicit input/output ports
|
|
54
|
+
- **Composites** — encapsulated sub-simulations with their own internal structure
|
|
55
|
+
- **Orchestration patterns** — multi-timestepping, directed workflows, and event-driven rewrites
|
|
56
|
+
|
|
57
|
+
Processes do **not** mutate state directly.
|
|
58
|
+
Instead, they emit **typed deltas** that are merged by the runtime.
|
|
59
|
+
|
|
60
|
+
This allows:
|
|
61
|
+
- numerical updates
|
|
62
|
+
- structural rewrites
|
|
63
|
+
- scheduling and orchestration
|
|
64
|
+
|
|
65
|
+
to coexist under a single execution semantics.
|
|
66
|
+
|
|
67
|
+
In this sense, Process-Bigraph is a **composition protocol**, not a domain-specific simulator.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 📄 Paper reference
|
|
72
|
+
|
|
73
|
+
The conceptual framework and formal semantics of process bigraphs are introduced in:
|
|
74
|
+
|
|
75
|
+
> **Agmon, E. & Spangler, R. K.**
|
|
76
|
+
> *Process Bigraphs and the Architecture of Compositional Systems Biology*
|
|
77
|
+
> https://arxiv.org/abs/2512.23754
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🚀 Getting Started
|
|
82
|
+
|
|
83
|
+
### Installation
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
pip install process-bigraph
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 📘 Tutorials
|
|
90
|
+
|
|
91
|
+
The Process-Bigraph tutorials are executable Jupyter notebooks,
|
|
92
|
+
rendered to HTML and published automatically on GitHub Pages.
|
|
93
|
+
|
|
94
|
+
- 📚 **Tutorial Index (all tutorials)**
|
|
95
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/index.html
|
|
96
|
+
|
|
97
|
+
### Learning Path (Featured Tutorials)
|
|
98
|
+
|
|
99
|
+
- **Tutorial 1 — Process-Bigraph Basics**
|
|
100
|
+
*Processes, Steps, ports, Composites, workflows, and emitters*
|
|
101
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_1.html
|
|
102
|
+
|
|
103
|
+
- **Tutorial 2 — Wrapping an ODE Solver (`odeint`)**
|
|
104
|
+
*How to expose an existing scientific API as a Process*
|
|
105
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_2.html
|
|
106
|
+
|
|
107
|
+
- **Tutorial 3 — Declarative Math**
|
|
108
|
+
*Defining mathematical relationships, signal pipelines, and events using `MathExpressionStep`*
|
|
109
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
110
|
+
|
|
111
|
+
More tutorials are added continuously and appear automatically in the index.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🧪 Reference Implementation: spatio-flux
|
|
116
|
+
|
|
117
|
+
Process-Bigraph is exercised end-to-end in **spatio-flux**, a multiscale reference
|
|
118
|
+
model built entirely using the process-bigraph protocol.
|
|
119
|
+
|
|
120
|
+
spatio-flux composes spatial fields, particle dynamics, and metabolic processes
|
|
121
|
+
using typed shared state and declarative orchestration.
|
|
122
|
+
|
|
123
|
+
GitHub: https://github.com/vivarium-collective/spatio-flux
|
|
124
|
+
Live test report: https://vivarium-collective.github.io/spatio-flux/report/index.html
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🔗 Related Resources
|
|
129
|
+
|
|
130
|
+
- **Bigraph Schema Basics**
|
|
131
|
+
https://vivarium-collective.github.io/bigraph-viz/notebooks/basics.html
|
|
132
|
+
*Introduction to the schema language underlying Process-Bigraph*
|
|
133
|
+
|
|
134
|
+
- **Visualization of Bigraph Document** — diagramming and rendering with
|
|
135
|
+
[**bigraph-viz**](https://github.com/vivarium-collective/bigraph-viz)
|
|
136
|
+
https://vivarium-collective.github.io/bigraph-viz/notebooks/format.html
|
|
137
|
+
|
|
138
|
+
- **E. coli Whole-Cell Wiring Diagram**
|
|
139
|
+
https://raw.githubusercontent.com/vivarium-collective/bigraph-viz/main/doc/_static/ecoli.png
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📜 License
|
|
144
|
+
|
|
145
|
+
Process-Bigraph is open-source software released under the
|
|
146
|
+
[Apache 2 License](https://github.com/vivarium-collective/process-bigraph/blob/main/LICENSE).
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Process-Bigraph
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/process-bigraph/)
|
|
4
|
+
[](https://vivarium-collective.github.io/process-bigraph/notebooks/index.html)
|
|
5
|
+
|
|
6
|
+
**Process-Bigraph** is a compositional runtime and protocol for building and executing
|
|
7
|
+
**multiscale biological models from interoperable processes**.
|
|
8
|
+
|
|
9
|
+
It provides a shared architectural layer for:
|
|
10
|
+
- declaring **process interfaces**
|
|
11
|
+
- wiring processes through **typed shared state**
|
|
12
|
+
- orchestrating execution across **heterogeneous timescales**
|
|
13
|
+
- supporting **dynamic structure** (workflows, division, graph rewrites)
|
|
14
|
+
|
|
15
|
+
Process-Bigraph is the execution core of **Vivarium 2.0**, designed to integrate models
|
|
16
|
+
built with different formalisms—including ODEs, FBA, agent-based models, spatial solvers,
|
|
17
|
+
and machine-learning components—into a single coherent simulation.
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://github.com/vivarium-collective/process-bigraph/blob/main/doc/_static/composition_framework.png?raw=true"
|
|
21
|
+
width="800"
|
|
22
|
+
alt="Process Bigraph composition framework">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🧩 What is a Process Bigraph?
|
|
28
|
+
|
|
29
|
+
A **process bigraph** combines:
|
|
30
|
+
|
|
31
|
+
- **Typed stores** — hierarchical, schema-validated state defined with
|
|
32
|
+
[**bigraph-schema**](https://github.com/vivarium-collective/bigraph-schema)
|
|
33
|
+
- **Processes** — executable components with explicit input/output ports
|
|
34
|
+
- **Composites** — encapsulated sub-simulations with their own internal structure
|
|
35
|
+
- **Orchestration patterns** — multi-timestepping, directed workflows, and event-driven rewrites
|
|
36
|
+
|
|
37
|
+
Processes do **not** mutate state directly.
|
|
38
|
+
Instead, they emit **typed deltas** that are merged by the runtime.
|
|
39
|
+
|
|
40
|
+
This allows:
|
|
41
|
+
- numerical updates
|
|
42
|
+
- structural rewrites
|
|
43
|
+
- scheduling and orchestration
|
|
44
|
+
|
|
45
|
+
to coexist under a single execution semantics.
|
|
46
|
+
|
|
47
|
+
In this sense, Process-Bigraph is a **composition protocol**, not a domain-specific simulator.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 📄 Paper reference
|
|
52
|
+
|
|
53
|
+
The conceptual framework and formal semantics of process bigraphs are introduced in:
|
|
54
|
+
|
|
55
|
+
> **Agmon, E. & Spangler, R. K.**
|
|
56
|
+
> *Process Bigraphs and the Architecture of Compositional Systems Biology*
|
|
57
|
+
> https://arxiv.org/abs/2512.23754
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🚀 Getting Started
|
|
62
|
+
|
|
63
|
+
### Installation
|
|
64
|
+
|
|
65
|
+
```console
|
|
66
|
+
pip install process-bigraph
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 📘 Tutorials
|
|
70
|
+
|
|
71
|
+
The Process-Bigraph tutorials are executable Jupyter notebooks,
|
|
72
|
+
rendered to HTML and published automatically on GitHub Pages.
|
|
73
|
+
|
|
74
|
+
- 📚 **Tutorial Index (all tutorials)**
|
|
75
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/index.html
|
|
76
|
+
|
|
77
|
+
### Learning Path (Featured Tutorials)
|
|
78
|
+
|
|
79
|
+
- **Tutorial 1 — Process-Bigraph Basics**
|
|
80
|
+
*Processes, Steps, ports, Composites, workflows, and emitters*
|
|
81
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_1.html
|
|
82
|
+
|
|
83
|
+
- **Tutorial 2 — Wrapping an ODE Solver (`odeint`)**
|
|
84
|
+
*How to expose an existing scientific API as a Process*
|
|
85
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_2.html
|
|
86
|
+
|
|
87
|
+
- **Tutorial 3 — Declarative Math**
|
|
88
|
+
*Defining mathematical relationships, signal pipelines, and events using `MathExpressionStep`*
|
|
89
|
+
https://vivarium-collective.github.io/process-bigraph/notebooks/tutorial_3.html
|
|
90
|
+
|
|
91
|
+
More tutorials are added continuously and appear automatically in the index.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🧪 Reference Implementation: spatio-flux
|
|
96
|
+
|
|
97
|
+
Process-Bigraph is exercised end-to-end in **spatio-flux**, a multiscale reference
|
|
98
|
+
model built entirely using the process-bigraph protocol.
|
|
99
|
+
|
|
100
|
+
spatio-flux composes spatial fields, particle dynamics, and metabolic processes
|
|
101
|
+
using typed shared state and declarative orchestration.
|
|
102
|
+
|
|
103
|
+
GitHub: https://github.com/vivarium-collective/spatio-flux
|
|
104
|
+
Live test report: https://vivarium-collective.github.io/spatio-flux/report/index.html
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🔗 Related Resources
|
|
109
|
+
|
|
110
|
+
- **Bigraph Schema Basics**
|
|
111
|
+
https://vivarium-collective.github.io/bigraph-viz/notebooks/basics.html
|
|
112
|
+
*Introduction to the schema language underlying Process-Bigraph*
|
|
113
|
+
|
|
114
|
+
- **Visualization of Bigraph Document** — diagramming and rendering with
|
|
115
|
+
[**bigraph-viz**](https://github.com/vivarium-collective/bigraph-viz)
|
|
116
|
+
https://vivarium-collective.github.io/bigraph-viz/notebooks/format.html
|
|
117
|
+
|
|
118
|
+
- **E. coli Whole-Cell Wiring Diagram**
|
|
119
|
+
https://raw.githubusercontent.com/vivarium-collective/bigraph-viz/main/doc/_static/ecoli.png
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📜 License
|
|
124
|
+
|
|
125
|
+
Process-Bigraph is open-source software released under the
|
|
126
|
+
[Apache 2 License](https://github.com/vivarium-collective/process-bigraph/blob/main/LICENSE).
|
|
@@ -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,
|
|
@@ -72,8 +74,11 @@ def find_instances(
|
|
|
72
74
|
|
|
73
75
|
for key, inner in state.items():
|
|
74
76
|
if isinstance(inner, dict):
|
|
75
|
-
|
|
77
|
+
instance = inner.get('instance')
|
|
78
|
+
|
|
79
|
+
if isinstance(instance, process_class):
|
|
76
80
|
found[key] = inner
|
|
81
|
+
|
|
77
82
|
elif not is_schema_key(key):
|
|
78
83
|
sub_instances = find_instances(inner, instance_type)
|
|
79
84
|
if sub_instances:
|
|
@@ -261,7 +266,7 @@ def build_step_network(steps):
|
|
|
261
266
|
|
|
262
267
|
# Assign the priority
|
|
263
268
|
if ancestors[step_key]['priority'] is None:
|
|
264
|
-
ancestors[step_key]['priority'] = step
|
|
269
|
+
ancestors[step_key]['priority'] = step.get('priority', 0.0)
|
|
265
270
|
|
|
266
271
|
input_paths = ancestors[step_key]['input_paths'] or []
|
|
267
272
|
output_paths = ancestors[step_key]['output_paths'] or []
|
|
@@ -689,6 +694,9 @@ class Process(Open):
|
|
|
689
694
|
update = self.update(state, interval)
|
|
690
695
|
return SyncUpdate(update)
|
|
691
696
|
|
|
697
|
+
def calculate_timestep(self, interval, state):
|
|
698
|
+
return interval
|
|
699
|
+
|
|
692
700
|
def update(self, state: Dict[str, Any], interval: float) -> Dict[str, Any]:
|
|
693
701
|
"""
|
|
694
702
|
Override this method to implement the process logic.
|
|
@@ -703,64 +711,74 @@ class Process(Open):
|
|
|
703
711
|
return {}
|
|
704
712
|
|
|
705
713
|
|
|
706
|
-
def as_step(inputs, outputs,
|
|
714
|
+
def as_step(inputs, outputs, name=None, aliases=None):
|
|
707
715
|
"""
|
|
708
|
-
Decorator
|
|
709
|
-
|
|
716
|
+
Decorator: convert an `update_*` pure function into a Step subclass.
|
|
717
|
+
|
|
718
|
+
- Does NOT register into any core.
|
|
719
|
+
- Adds metadata so discover_packages can register nice aliases (e.g. "add").
|
|
710
720
|
"""
|
|
711
721
|
def decorator(func):
|
|
712
|
-
|
|
713
|
-
|
|
722
|
+
if not func.__name__.startswith("update_"):
|
|
723
|
+
raise AssertionError("Function name must be of the form update_*")
|
|
714
724
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
725
|
+
step_name = name or func.__name__[len("update_"):]
|
|
726
|
+
step_aliases = list(aliases or [])
|
|
727
|
+
# default alias: the function-derived name, e.g. update_add -> "add"
|
|
728
|
+
if step_name not in step_aliases:
|
|
729
|
+
step_aliases.insert(0, step_name)
|
|
718
730
|
|
|
719
|
-
|
|
720
|
-
|
|
731
|
+
class FunctionStep(Step):
|
|
732
|
+
def inputs(self): return inputs
|
|
733
|
+
def outputs(self): return outputs
|
|
734
|
+
def update(self, state): return func(state)
|
|
721
735
|
|
|
722
|
-
|
|
723
|
-
return func(state)
|
|
736
|
+
FunctionStep.__name__ = f"{step_name}Step"
|
|
724
737
|
|
|
725
|
-
|
|
738
|
+
# IMPORTANT: make this class look like it belongs to the user's module
|
|
739
|
+
FunctionStep.__module__ = func.__module__
|
|
726
740
|
|
|
727
|
-
|
|
728
|
-
|
|
741
|
+
# Discovery metadata
|
|
742
|
+
FunctionStep.__pb_kind__ = "step"
|
|
743
|
+
FunctionStep.__pb_aliases__ = step_aliases
|
|
744
|
+
FunctionStep.__pb_wrapped__ = func
|
|
729
745
|
|
|
730
746
|
return FunctionStep
|
|
731
|
-
|
|
732
747
|
return decorator
|
|
733
748
|
|
|
734
749
|
|
|
735
|
-
def as_process(inputs, outputs,
|
|
750
|
+
def as_process(inputs, outputs, name=None, aliases=None):
|
|
736
751
|
"""
|
|
737
|
-
Decorator
|
|
738
|
-
|
|
752
|
+
Decorator: convert an `update_*` function into a Process subclass.
|
|
753
|
+
|
|
754
|
+
- Does NOT register into any core.
|
|
755
|
+
- Adds metadata so discover_packages can register nice aliases (e.g. "odeint").
|
|
739
756
|
"""
|
|
740
757
|
def decorator(func):
|
|
741
|
-
|
|
742
|
-
|
|
758
|
+
if not func.__name__.startswith("update_"):
|
|
759
|
+
raise AssertionError("Function name must be of the form update_*")
|
|
743
760
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
def inputs(self):
|
|
749
|
-
return inputs
|
|
761
|
+
process_name = name or func.__name__[len("update_"):]
|
|
762
|
+
process_aliases = list(aliases or [])
|
|
763
|
+
if process_name not in process_aliases:
|
|
764
|
+
process_aliases.insert(0, process_name)
|
|
750
765
|
|
|
751
|
-
|
|
752
|
-
|
|
766
|
+
class FunctionProcess(Process):
|
|
767
|
+
def inputs(self): return inputs
|
|
768
|
+
def outputs(self): return outputs
|
|
769
|
+
def update(self, state, interval): return func(state, interval)
|
|
753
770
|
|
|
754
|
-
|
|
755
|
-
return func(state, interval)
|
|
771
|
+
FunctionProcess.__name__ = f"{process_name}Process"
|
|
756
772
|
|
|
757
|
-
|
|
773
|
+
# IMPORTANT: make this class look like it belongs to the user's module
|
|
774
|
+
FunctionProcess.__module__ = func.__module__
|
|
758
775
|
|
|
759
|
-
|
|
760
|
-
|
|
776
|
+
# Discovery metadata
|
|
777
|
+
FunctionProcess.__pb_kind__ = "process"
|
|
778
|
+
FunctionProcess.__pb_aliases__ = process_aliases
|
|
779
|
+
FunctionProcess.__pb_wrapped__ = func
|
|
761
780
|
|
|
762
781
|
return FunctionProcess
|
|
763
|
-
|
|
764
782
|
return decorator
|
|
765
783
|
|
|
766
784
|
|
|
@@ -893,7 +911,8 @@ class Composite(Process):
|
|
|
893
911
|
'inputs': 'wires',
|
|
894
912
|
'outputs': 'wires'
|
|
895
913
|
},
|
|
896
|
-
'global_time_precision': 'maybe[float]'
|
|
914
|
+
'global_time_precision': 'maybe[float]',
|
|
915
|
+
'skip_initial_steps': 'maybe[boolean]'
|
|
897
916
|
}
|
|
898
917
|
|
|
899
918
|
|
|
@@ -998,11 +1017,16 @@ class Composite(Process):
|
|
|
998
1017
|
# A buffer for updates to be emitted at the composite's output interface.
|
|
999
1018
|
self.bridge_updates: List[Any] = []
|
|
1000
1019
|
|
|
1020
|
+
# Precompile view/project operations for fast runtime access.
|
|
1021
|
+
self._compiled_links = {}
|
|
1022
|
+
self._build_view_project_cache()
|
|
1023
|
+
|
|
1001
1024
|
# Build the dependency network between steps and determine which steps should run first.
|
|
1002
1025
|
self.build_step_network()
|
|
1003
1026
|
|
|
1004
1027
|
# Run all steps that are ready on the first cycle.
|
|
1005
|
-
self.
|
|
1028
|
+
if not self.config.get('skip_initial_steps', False):
|
|
1029
|
+
self.run_steps(self.to_run)
|
|
1006
1030
|
|
|
1007
1031
|
@classmethod
|
|
1008
1032
|
def load(cls, path: str, core: Optional[Any] = None) -> "Composite":
|
|
@@ -1051,6 +1075,42 @@ class Composite(Process):
|
|
|
1051
1075
|
# do we want to do anything with these?
|
|
1052
1076
|
removed_front = self.front.pop(removed_key)
|
|
1053
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
|
+
|
|
1054
1114
|
def merge(self, schema: Dict[str, Any], state: Dict[str, Any], path: Optional[List[str]] = None) -> None:
|
|
1055
1115
|
"""
|
|
1056
1116
|
Merge a new schema/state subtree into the Composite.
|
|
@@ -1069,6 +1129,7 @@ class Composite(Process):
|
|
|
1069
1129
|
state)
|
|
1070
1130
|
|
|
1071
1131
|
self.find_instance_paths(self.state)
|
|
1132
|
+
self._build_view_project_cache()
|
|
1072
1133
|
|
|
1073
1134
|
def merge_schema(
|
|
1074
1135
|
self,
|
|
@@ -1160,8 +1221,17 @@ class Composite(Process):
|
|
|
1160
1221
|
|
|
1161
1222
|
os.makedirs(outdir, exist_ok=True)
|
|
1162
1223
|
filepath = os.path.join(outdir, filename)
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
|
|
1165
1235
|
print(f"Saved composite to {filepath}")
|
|
1166
1236
|
|
|
1167
1237
|
|
|
@@ -1401,6 +1471,7 @@ class Composite(Process):
|
|
|
1401
1471
|
paths.append(path)
|
|
1402
1472
|
|
|
1403
1473
|
update_paths = self.apply_updates(updates)
|
|
1474
|
+
update_paths.append(('global_time',)) # updated global time can trigger steps
|
|
1404
1475
|
self.expire_process_paths(update_paths)
|
|
1405
1476
|
self.trigger_steps(update_paths)
|
|
1406
1477
|
|
|
@@ -1449,11 +1520,10 @@ class Composite(Process):
|
|
|
1449
1520
|
state = future_front['state']
|
|
1450
1521
|
else:
|
|
1451
1522
|
# Otherwise, slice the current state for the process
|
|
1452
|
-
state = self.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
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
|
|
1457
1527
|
|
|
1458
1528
|
# Determine the target time for the next update
|
|
1459
1529
|
future = (
|
|
@@ -1523,9 +1593,7 @@ class Composite(Process):
|
|
|
1523
1593
|
if not isinstance(update_results, list):
|
|
1524
1594
|
update_results = [update_results]
|
|
1525
1595
|
|
|
1526
|
-
return [self.
|
|
1527
|
-
schema,
|
|
1528
|
-
state,
|
|
1596
|
+
return [self._cached_project(
|
|
1529
1597
|
process_path,
|
|
1530
1598
|
update_result,
|
|
1531
1599
|
ports_key) for update_result in update_results]
|
|
@@ -1533,6 +1601,24 @@ class Composite(Process):
|
|
|
1533
1601
|
# Return a deferred object that will project the update when requested
|
|
1534
1602
|
return Defer(update, defer_project, (self.schema, self.state, path))
|
|
1535
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
|
+
|
|
1536
1622
|
def apply_updates(self, updates: List["Defer"]) -> List[Union[str, Tuple[str, ...]]]:
|
|
1537
1623
|
"""
|
|
1538
1624
|
Apply a series of deferred updates and record the resulting bridge outputs.
|
|
@@ -1548,6 +1634,7 @@ class Composite(Process):
|
|
|
1548
1634
|
A list of update paths (used to determine which processes to refresh).
|
|
1549
1635
|
"""
|
|
1550
1636
|
update_paths = []
|
|
1637
|
+
had_structural_changes = False
|
|
1551
1638
|
|
|
1552
1639
|
for defer in updates:
|
|
1553
1640
|
# Resolve deferred computation to get update(s)
|
|
@@ -1558,13 +1645,14 @@ class Composite(Process):
|
|
|
1558
1645
|
series = [series]
|
|
1559
1646
|
|
|
1560
1647
|
for update_schema, update_state in series:
|
|
1561
|
-
# if update and isinstance(update, dict) and 'environment' in update and update['environment'] and isinstance(update['environment'], dict) and '_react' in update['environment']:
|
|
1562
|
-
# import ipdb; ipdb.set_trace()
|
|
1563
|
-
|
|
1564
1648
|
# Extract all hierarchical paths touched by this update
|
|
1565
1649
|
paths = hierarchy_depth(update_state)
|
|
1566
1650
|
update_paths.extend(paths.keys())
|
|
1567
1651
|
|
|
1652
|
+
# Detect structural changes before applying
|
|
1653
|
+
if not had_structural_changes:
|
|
1654
|
+
had_structural_changes = self._has_structural_keys(update_state)
|
|
1655
|
+
|
|
1568
1656
|
# Apply update directly to the internal state,
|
|
1569
1657
|
# using the schema from the link itself
|
|
1570
1658
|
self.state, merges = self.core.apply(
|
|
@@ -1572,20 +1660,22 @@ class Composite(Process):
|
|
|
1572
1660
|
self.state,
|
|
1573
1661
|
update_state)
|
|
1574
1662
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1663
|
+
if merges:
|
|
1664
|
+
had_structural_changes = True
|
|
1665
|
+
self.schema = self.core.resolve_merges(
|
|
1666
|
+
self.schema,
|
|
1667
|
+
merges)
|
|
1578
1668
|
|
|
1579
1669
|
# Read updated bridge outputs, if available
|
|
1580
1670
|
bridge_update = self.read_bridge(update_state)
|
|
1581
1671
|
if bridge_update:
|
|
1582
1672
|
self.bridge_updates.append(bridge_update)
|
|
1583
1673
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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()
|
|
1589
1679
|
|
|
1590
1680
|
return update_paths
|
|
1591
1681
|
|
|
@@ -1599,12 +1689,33 @@ class Composite(Process):
|
|
|
1599
1689
|
Args:
|
|
1600
1690
|
update_paths: A list of hierarchical paths that were modified.
|
|
1601
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
|
+
|
|
1602
1711
|
for update_path in update_paths:
|
|
1603
1712
|
for process_path in self.process_paths.copy():
|
|
1604
1713
|
# Match if update path completely overlaps the process path prefix
|
|
1605
1714
|
updated = all(update == process for update, process in zip(update_path, process_path))
|
|
1606
1715
|
if updated:
|
|
1607
1716
|
self.find_instance_paths(self.state)
|
|
1717
|
+
self._build_view_project_cache()
|
|
1718
|
+
self._process_path_roots = set() # Reset for rebuild
|
|
1608
1719
|
return # Exit early after one match, as paths are re-evaluated
|
|
1609
1720
|
|
|
1610
1721
|
|
|
@@ -1649,3 +1760,16 @@ class Composite(Process):
|
|
|
1649
1760
|
self.run(interval)
|
|
1650
1761
|
|
|
1651
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
|