Skip to content

Commit f5bdae1

Browse files
authored
Merge pull request #1 from biosimulators/SimpleRuntime
run toy model from DSL with existing Python Processes
2 parents 04c14b8 + b4c2eac commit f5bdae1

27 files changed

+4143
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.pyc
2+
.idea
3+
.idea/*

poetry.lock

Lines changed: 2838 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

poetry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[virtualenvs]
2+
in-project = true

pyproject.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[project]
2+
name = "simple-process-bigraph-runtime"
3+
version = "0.1.0"
4+
description = ""
5+
authors = [
6+
{name = "Logan Drescher",email = "[email protected]"}
7+
]
8+
license = {text = "MIT"}
9+
readme = "README.md"
10+
requires-python = ">=3.10, <4.0"
11+
packages = [
12+
{include = "simple_process_bigraph_runtime"},
13+
{include = "tests"}
14+
]
15+
dependencies = [
16+
"process-bigraph (>=0.0.35, <1.0)",
17+
"typer (>=0.15.3, <1.0)",
18+
# "process-bigraph-lang @ git+https://github.com/biosimulations/process-bigraph-lang.git#bind-to-object",
19+
"process-bigraph-lang @ ../process-bigraph-lang",
20+
"spatio-flux @ git+https://github.com/vivarium-collective/spatio-flux.git",
21+
"vivarium-interface (>=0.0.5,<0.0.6)"
22+
]
23+
24+
[tool.poetry]
25+
26+
[tool.poetry.group.dev.dependencies]
27+
pytest = "^7.2.0"
28+
pytest-cov = "^6.1.1"
29+
deptry = "^0.23.0"
30+
mypy = "^1.15.0"
31+
pre-commit = "^4.2.0"
32+
tox = "^4.25.0"
33+
34+
[build-system]
35+
requires = ["poetry-core>=2.0.0,<3.0.0"]
36+
build-backend = "poetry.core.masonry.api"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from simple_process_bigraph_runtime.registry import spatio_flux_library
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
3+
from process_bigraph_lang.antlr_dsl.generate import bind_model
4+
from process_bigraph_lang.dsl import generate
5+
from process_bigraph_lang.dsl.model import (
6+
Model,
7+
)
8+
9+
10+
def generateModelAst(pblang_file: os.PathLike[str]) -> Model:
11+
model_json: str = generate.generate_model(pblang_file)
12+
model: Model = Model.model_validate_json(model_json)
13+
bind_model(model)
14+
#result: str = model.model_dump_json(indent=4)
15+
#rich.print(result)
16+
return model

src/simple_process_bigraph_runtime/environment/__init__.py

Whitespace-only changes.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
from typing import Any
5+
6+
from bigraph_schema import is_schema_key, set_path, get_path # type: ignore[import-untyped]
7+
from bigraph_schema.utilities import remove_path # type: ignore[import-untyped]
8+
from process_bigraph import Composite, Process, Step, ProcessTypes # type: ignore[import-untyped]
9+
10+
11+
class ProcessBigraphEnv:
12+
core: ProcessTypes
13+
composite: Composite
14+
emitter_config: dict[str, Any]
15+
emitter_paths: dict[str, tuple[str, ...]]
16+
17+
def __init__(self):
18+
self.core = ProcessTypes()
19+
self.composite = Composite(core=self.core)
20+
self.emitter_config = {}
21+
self.emitter_paths = {}
22+
23+
def add_process(self, name: str, process_id, config=None, inputs=None, outputs=None,
24+
path: tuple[str, ...] | None = None):
25+
edge_type = "process" # TODO -- does it matter if this is a step or a process?
26+
config = config or {}
27+
inputs = inputs or {}
28+
outputs = outputs or {}
29+
path = path or ()
30+
31+
# convert string paths to lists
32+
# TODO -- make this into a separate path-parsing function
33+
for ports in [inputs, outputs]:
34+
for port, port_path in ports.items():
35+
ports[port] = parse_path(port_path)
36+
37+
# make the process spec
38+
state = {
39+
name: {
40+
"_type": edge_type,
41+
"address": f"local:{process_id}", # TODO -- only support local right now?
42+
"config": config,
43+
"inputs": inputs,
44+
"outputs": outputs,
45+
}
46+
}
47+
48+
# nest the process in the composite at the given path
49+
self.composite.merge({}, state, path)
50+
self.reset_emitters()
51+
self.reset_paths()
52+
53+
def add_step(self, name: str, process_id, config=None, inputs=None, outputs=None,
54+
path: tuple[str, ...] | None = None):
55+
edge_type = "step" # TODO -- does it matter if this is a step or a process?
56+
config = config or {}
57+
inputs = inputs or {}
58+
outputs = outputs or {}
59+
path = path or ()
60+
61+
# convert string paths to lists
62+
# TODO -- make this into a separate path-parsing function
63+
for ports in [inputs, outputs]:
64+
for port, port_path in ports.items():
65+
ports[port] = parse_path(port_path)
66+
67+
# make the process spec
68+
state = {
69+
name: {
70+
"_type": edge_type,
71+
"address": f"local:{process_id}", # TODO -- only support local right now?
72+
"config": config,
73+
"inputs": inputs,
74+
"outputs": outputs,
75+
}
76+
}
77+
78+
# nest the process in the composite at the given path
79+
self.composite.merge({}, state, path)
80+
self.reset_emitters()
81+
self.reset_paths()
82+
83+
def reset_emitters(self) -> None:
84+
for path, emitter in self.emitter_paths.items():
85+
remove_path(self.composite.state, path)
86+
self.add_emitter()
87+
# for path, instance in self.composite.step_paths.items():
88+
# if "emitter" in path:
89+
# remove_path(self.composite.state, path)
90+
# self.add_emitter()
91+
92+
def reset_paths(self):
93+
self.composite.find_instance_paths(self.composite.state)
94+
self.composite.build_step_network()
95+
96+
def add_emitter(self):
97+
"""
98+
Add an emitter to the composite.
99+
"""
100+
101+
emitter_state = self._read_emitter_config()
102+
103+
# set the emitter at the path
104+
path = tuple(self.emitter_config.get("path", ('emitter',)))
105+
emitter_state = set_path({}, path, emitter_state)
106+
107+
self.composite.merge({}, emitter_state)
108+
109+
# TODO -- this is a hack to get the emitter to show up in the state
110+
# TODO -- this should be done in the merge function
111+
_, instance = self.core.slice(
112+
self.composite.composition,
113+
self.composite.state,
114+
path)
115+
116+
self.emitter_paths[path] = instance
117+
self.composite.step_paths[path] = instance
118+
119+
# rebuild the step network
120+
self.composite.build_step_network()
121+
122+
def _read_emitter_config(self):
123+
address = self.emitter_config.get("address", "local:ram-emitter")
124+
config = self.emitter_config.get("config", {})
125+
mode = self.emitter_config.get("mode", "all")
126+
127+
if mode == "all":
128+
inputs = {}
129+
for key in self.composite.state.keys():
130+
if is_schema_key(key): # skip schema keys
131+
continue
132+
if self.core.inherits_from(self.composite.composition[key], "edge"): # skip edges
133+
continue
134+
inputs[key] = [self.emitter_config.get("inputs", {}).get(key, key)]
135+
136+
elif mode == "none":
137+
inputs = self.emitter_config.get("emit", {})
138+
139+
elif mode == "bridge":
140+
print("Warning: emitter bridge mode not implemented.")
141+
inputs = {}
142+
143+
elif mode == "ports":
144+
print("Warning: emitter ports mode not implemented.")
145+
inputs = {}
146+
147+
if not "emit" in config:
148+
config["emit"] = {
149+
input: "any"
150+
for input in inputs}
151+
152+
return {
153+
"_type": "step",
154+
"address": address,
155+
"config": config,
156+
"inputs": inputs}
157+
158+
def set_value(self, path: list[str], value: Any) -> None:
159+
# TODO -- make this set the value in the composite using core
160+
set_path(self.composite.state, path=path, value=value)
161+
# self.composite[path] = value
162+
# self.composite.set({}, value, path)
163+
164+
def get_type(self, type_id: str) -> dict[str, Any]:
165+
return render_type(type_name_or_dict=self.core.access(type_id), core=self.core)
166+
167+
def make_document(self) -> dict[str, Any]:
168+
serialized_state = self.composite.serialize_state()
169+
170+
# TODO fix RecursionError
171+
# schema = self.core.representation(self.composite.composition)
172+
schema = self.composite.serialize_schema()
173+
# schema = self.composite.composition
174+
175+
return {
176+
"state": serialized_state,
177+
"composition": schema,
178+
}
179+
180+
def save(self, json_file_path: Path):
181+
document = self.make_document()
182+
with open(json_file_path, "w") as json_file:
183+
json.dump(document, json_file, indent=4)
184+
print(f"Saved file: {json_file_path}")
185+
186+
187+
def run(self, interval):
188+
"""
189+
Run the simulation for a given interval.
190+
"""
191+
if not self.emitter_paths:
192+
self.add_emitter()
193+
194+
self.composite.run(interval)
195+
196+
def step(self):
197+
"""
198+
Run the simulation for a single step.
199+
"""
200+
self.composite.update({}, 0)
201+
202+
203+
204+
def render_type(type_name_or_dict: str | dict[str, Any], core: ProcessTypes) -> dict[str, Any]:
205+
type_dict: dict[str, Any] = core.access(type_name_or_dict)
206+
rendered = {}
207+
for key, value in type_dict.items():
208+
if is_schema_key(key):
209+
if key == '_description':
210+
rendered['description'] = value
211+
elif key == '_default':
212+
rendered['default'] = core.default(type_dict)
213+
elif isinstance(value, dict):
214+
rendered[key] = render_type(value, core)
215+
return rendered
216+
217+
def parse_path(path: list[str] | str | dict[str, Any]) -> list[str] | dict[str, Any]:
218+
if isinstance(path, dict):
219+
return {
220+
key: parse_path(value)
221+
for key, value in path.items()}
222+
elif isinstance(path, str):
223+
if path.startswith('/'):
224+
return path.split('/')[1:]
225+
else:
226+
return [path]
227+
else:
228+
return path
229+
230+
231+
class TypedStep(Step):
232+
def __init__(self, config=None):
233+
super().__init__(config)
234+
self._inputs = self.inputs()
235+
self._outputs = self.outputs()
236+
237+
def inputs(self) -> dict[str, Any]:
238+
return {}
239+
240+
241+
def outputs(self) -> dict[str, Any]:
242+
return {}
243+
244+
def update(self, inputs: dict[str, Any]) -> dict[str, Any]:
245+
raise NotImplementedError("Subclasses should implement this method.")

src/simple_process_bigraph_runtime/generation/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)