process-bigraph 1.4.18__tar.gz → 1.5.0__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.18/process_bigraph.egg-info → process_bigraph-1.5.0}/PKG-INFO +1 -1
  2. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/__init__.py +9 -5
  3. process_bigraph-1.5.0/process_bigraph/composite_spec.py +320 -0
  4. {process_bigraph-1.4.18 → process_bigraph-1.5.0/process_bigraph.egg-info}/PKG-INFO +1 -1
  5. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph.egg-info/SOURCES.txt +1 -0
  6. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/pyproject.toml +1 -1
  7. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/AUTHORS.md +0 -0
  8. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/LICENSE +0 -0
  9. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/README.md +0 -0
  10. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/bundle.py +0 -0
  11. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/composite.py +0 -0
  12. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/emitter.py +0 -0
  13. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/experiments/__init__.py +0 -0
  14. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/experiments/minimal_gillespie.py +0 -0
  15. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/nextflow.py +0 -0
  16. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/plumbing.py +0 -0
  17. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/__init__.py +0 -0
  18. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/bigraphical_reactive_system.py +0 -0
  19. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/dynamic_structure.py +0 -0
  20. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/examples.py +0 -0
  21. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/growth_division.py +0 -0
  22. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/parameter_scan.py +0 -0
  23. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/processes/reaction.py +0 -0
  24. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/__init__.py +0 -0
  25. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/clusters/__init__.py +0 -0
  26. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/clusters/ec2_ssm.py +0 -0
  27. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/parallel.py +0 -0
  28. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/pool.py +0 -0
  29. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/ray.py +0 -0
  30. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/rest.py +0 -0
  31. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/protocols/session.py +0 -0
  32. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/run.py +0 -0
  33. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/run_step.py +0 -0
  34. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/scheduling.py +0 -0
  35. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/server/__init__.py +0 -0
  36. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/server/rest.py +0 -0
  37. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/server/start.py +0 -0
  38. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/types/__init__.py +0 -0
  39. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph/types/process.py +0 -0
  40. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph.egg-info/dependency_links.txt +0 -0
  41. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph.egg-info/requires.txt +0 -0
  42. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/process_bigraph.egg-info/top_level.txt +0 -0
  43. {process_bigraph-1.4.18 → process_bigraph-1.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-bigraph
3
- Version: 1.4.18
3
+ Version: 1.5.0
4
4
  Summary: protocol and execution for compositional systems biology
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,11 +1,15 @@
1
- from bigraph_schema import allocate_core
1
+ from bigraph_schema import allocate_core # noqa: F401
2
2
 
3
- from process_bigraph.composite import (
3
+ from process_bigraph.composite import ( # noqa: F401
4
4
  Process, Step, Composite, TimingSummary, interval_time_precision, wire_step_layers,
5
5
  )
6
- from process_bigraph.bundle import load_bundle
7
- from process_bigraph.emitter import Emitter, gather_emitter_results, generate_emitter_state
8
- from process_bigraph.types import StepLink, ProcessLink, CompositeLink
6
+ from process_bigraph.composite_spec import ( # noqa: F401
7
+ CompositeSpec, discover_specs, regenerate_default_state,
8
+ normalize_type, CANONICAL_TYPES,
9
+ )
10
+ from process_bigraph.bundle import load_bundle # noqa: F401
11
+ from process_bigraph.emitter import Emitter, gather_emitter_results, generate_emitter_state # noqa: F401
12
+ from process_bigraph.types import StepLink, ProcessLink, CompositeLink # noqa: F401
9
13
 
10
14
 
11
15
  import pprint
@@ -0,0 +1,320 @@
1
+ """Unified composite declaration for process-bigraph.
2
+
3
+ A CompositeSpec is the declarative descriptor a composite is authored as —
4
+ either inline data (``state`` + optional ``schema``) for a static composite, or
5
+ a ``builder`` callable for a generator composite. It carries the dashboard/UI
6
+ metadata (parameters, default_n_steps, visualizations, analyses, emitters,
7
+ requires) and produces a runtime ``process_bigraph.Composite``.
8
+
9
+ This unifies (and is the new home of) the two front-ends that previously lived
10
+ separately in pbg-superpowers: the static ``composite_spec`` spec-file format
11
+ and the ``composite_generator`` decorator. Those become thin shims over this.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import json
17
+ import importlib
18
+ from pathlib import Path
19
+ from dataclasses import dataclass, field, asdict
20
+ from typing import Callable, Any
21
+
22
+ CANONICAL_TYPES = {"integer", "float", "string", "boolean", "list", "map"}
23
+
24
+ _TYPE_ALIASES = {
25
+ "integer": "integer", "int": "integer",
26
+ "float": "float", "number": "float", "double": "float",
27
+ "string": "string", "str": "string",
28
+ "boolean": "boolean", "bool": "boolean",
29
+ "list": "list", "array": "list",
30
+ "map": "map", "object": "map", "dict": "map",
31
+ }
32
+
33
+
34
+ def normalize_type(t: str) -> str:
35
+ """Map a parameter ``type`` string onto the canonical vocabulary.
36
+
37
+ Known aliases collapse (int→integer, number→float, bool→boolean,
38
+ object→map, array→list); an unknown type passes through unchanged.
39
+ """
40
+ return _TYPE_ALIASES.get(t, t)
41
+
42
+
43
+ _FULL_PLACEHOLDER = re.compile(r"^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$")
44
+ _INLINE_PLACEHOLDER = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
45
+
46
+
47
+ def _cast(value: Any, declared_type: "str | None") -> Any:
48
+ if declared_type is None:
49
+ return value
50
+ t = normalize_type(declared_type)
51
+ if t == "float":
52
+ return float(value)
53
+ if t == "integer":
54
+ return int(value)
55
+ if t == "string":
56
+ return str(value)
57
+ if t == "boolean":
58
+ if isinstance(value, str):
59
+ return value.strip().lower() in ("true", "1", "yes")
60
+ return bool(value)
61
+ return value
62
+
63
+
64
+ def _resolve_value(value, params, overrides):
65
+ if not isinstance(value, str):
66
+ return value
67
+ m = _FULL_PLACEHOLDER.match(value)
68
+ if m:
69
+ pname = m.group(1)
70
+ if pname not in params:
71
+ raise KeyError(f"parameter '{pname}' referenced in state but not declared")
72
+ pdef = params[pname]
73
+ raw = overrides.get(pname, pdef.get("default"))
74
+ if raw is None and "default" not in pdef:
75
+ raise KeyError(f"parameter '{pname}' has no default and no override")
76
+ return _cast(raw, pdef.get("type"))
77
+ if _INLINE_PLACEHOLDER.search(value):
78
+ def repl(match):
79
+ pname = match.group(1)
80
+ if pname not in params:
81
+ raise KeyError(f"parameter '{pname}' referenced in state but not declared")
82
+ raw = overrides.get(pname, params[pname].get("default"))
83
+ return str(raw)
84
+ return _INLINE_PLACEHOLDER.sub(repl, value)
85
+ return value
86
+
87
+
88
+ def substitute_parameters(state, params, overrides=None):
89
+ """Recursively substitute ``${name}`` placeholders. Returns a new structure."""
90
+ overrides = overrides or {}
91
+ if isinstance(state, dict):
92
+ return {k: substitute_parameters(v, params, overrides) for k, v in state.items()}
93
+ if isinstance(state, list):
94
+ return [substitute_parameters(v, params, overrides) for v in state]
95
+ return _resolve_value(state, params, overrides)
96
+
97
+
98
+ def _resolve_builder(builder, module):
99
+ """Resolve a builder callable; a dotted ``pkg.mod:fn`` string is imported."""
100
+ if callable(builder):
101
+ return builder
102
+ mod_name, _, qual = str(builder).partition(":")
103
+ mod = importlib.import_module(mod_name or module)
104
+ obj = mod
105
+ for part in qual.split("."):
106
+ obj = getattr(obj, part)
107
+ return obj
108
+
109
+
110
+ @dataclass
111
+ class CompositeSpec:
112
+ id: str
113
+ name: str
114
+ description: str = ""
115
+ tags: list = field(default_factory=list)
116
+ parameters: dict = field(default_factory=dict)
117
+ default_n_steps: "int | None" = None
118
+ visualizations: list = field(default_factory=list)
119
+ analyses: list = field(default_factory=list)
120
+ emitters: list = field(default_factory=list)
121
+ requires: dict = field(default_factory=dict)
122
+ schema: dict = field(default_factory=dict)
123
+ state: "dict | None" = None
124
+ builder: "Callable | str | None" = None
125
+ default_state_ref: "str | None" = None
126
+ module: str = ""
127
+ core_extensions: list = field(default_factory=list)
128
+
129
+ def __post_init__(self) -> None:
130
+ if not isinstance(self.name, str) or not self.name:
131
+ raise ValueError("CompositeSpec.name is required (non-empty string)")
132
+ has_state = self.state is not None
133
+ has_builder = self.builder is not None
134
+ if has_state == has_builder:
135
+ raise ValueError("CompositeSpec needs exactly one of `state` or `builder`")
136
+ if has_builder and self.schema:
137
+ raise ValueError("`schema` is for static specs; a generator's schema comes "
138
+ "from the builder document")
139
+ if self.default_state_ref is not None and not has_builder:
140
+ raise ValueError("`default_state_ref` requires a `builder`")
141
+ # normalize parameter types (non-mutating rebuild)
142
+ self.parameters = {
143
+ k: ({**v, "type": normalize_type(v["type"])}
144
+ if isinstance(v, dict) and "type" in v else v)
145
+ for k, v in self.parameters.items()
146
+ }
147
+
148
+ @property
149
+ def kind(self) -> str:
150
+ return "generator" if self.builder is not None else "spec"
151
+
152
+ def to_dict(self) -> dict:
153
+ d = asdict(self)
154
+ d.pop("core_extensions", None) # callables are not serializable
155
+ if callable(self.builder):
156
+ module = self.module if self.module else getattr(self.builder, '__module__', '')
157
+ d["builder"] = f"{module}:{self.builder.__name__}"
158
+ return d
159
+
160
+ @classmethod
161
+ def from_dict(cls, d: dict) -> "CompositeSpec":
162
+ d = dict(d)
163
+ d.pop("core_extensions", None)
164
+ return cls(**d)
165
+
166
+ @classmethod
167
+ def from_file(cls, path) -> "CompositeSpec":
168
+ path = Path(path)
169
+ text = path.read_text(encoding="utf-8")
170
+ if path.suffix.lower() == ".json":
171
+ raw = json.loads(text)
172
+ else:
173
+ import yaml
174
+ raw = yaml.safe_load(text)
175
+ if not isinstance(raw, dict):
176
+ raise ValueError(f"composite spec {path} must parse to a dict")
177
+ name = raw.get("name")
178
+ # id: prefer an explicit id, else "<file-stem-stripped>.<name>"; keep stable + simple
179
+ stem = path.name.replace(".composite.yaml", "").replace(".composite.json", "")
180
+ spec_id = raw.get("id") or f"{stem}.{name}"
181
+ builder = raw.get("builder")
182
+ return cls(
183
+ id=spec_id, name=name, description=raw.get("description", ""),
184
+ tags=list(raw.get("tags") or []),
185
+ parameters=dict(raw.get("parameters") or {}),
186
+ default_n_steps=raw.get("default_n_steps"),
187
+ visualizations=list(raw.get("visualizations") or []),
188
+ analyses=list(raw.get("analyses") or []),
189
+ emitters=list(raw.get("emitters") or []),
190
+ requires=dict(raw.get("requires") or {}),
191
+ schema=dict(raw.get("schema") or {}) if builder is None else {},
192
+ state=raw.get("state") if builder is None else None,
193
+ builder=builder,
194
+ default_state_ref=raw.get("default_state_ref"),
195
+ module=raw.get("module", ""),
196
+ )
197
+
198
+ def _merged_params(self, overrides):
199
+ overrides = overrides or {}
200
+ unknown = set(overrides) - set(self.parameters)
201
+ if unknown:
202
+ raise KeyError(f"unknown override(s): {sorted(unknown)}")
203
+ merged = {k: v.get("default") for k, v in self.parameters.items()}
204
+ merged.update(overrides)
205
+ return merged
206
+
207
+ def to_document(self, overrides=None, core=None) -> dict:
208
+ # Validate overrides for both static and generator specs
209
+ self._merged_params(overrides)
210
+ if self.kind == "spec":
211
+ return {
212
+ "schema": substitute_parameters(self.schema, self.parameters, overrides),
213
+ "state": substitute_parameters(self.state, self.parameters, overrides),
214
+ }
215
+ fn = _resolve_builder(self.builder, self.module)
216
+ return fn(core=core, **self._merged_params(overrides))
217
+
218
+ def to_composite(self, overrides=None, core=None):
219
+ from process_bigraph import Composite, allocate_core
220
+ if core is None:
221
+ core = allocate_core()
222
+ for ext in self.core_extensions:
223
+ ext(core)
224
+ doc = self.to_document(overrides, core=core)
225
+ return Composite(doc, core=core)
226
+
227
+ def default_state(self, base_dir=None) -> "dict | None":
228
+ if self.state is not None:
229
+ return self.state
230
+ if self.default_state_ref and base_dir is not None:
231
+ artifact = Path(base_dir) / self.default_state_ref
232
+ if artifact.is_file():
233
+ data = json.loads(artifact.read_text(encoding="utf-8"))
234
+ return data.get("state", data)
235
+ return None
236
+
237
+
238
+ def regenerate_default_state(spec: CompositeSpec, base_dir, core=None) -> "Path":
239
+ """Run a generator's builder with default params, serialize the materialized
240
+ state, and write the ``default_state_ref`` artifact (+ a provenance stamp).
241
+
242
+ This is the one step that needs the heavy build environment (e.g. a ParCa
243
+ cache for v2ecoli baseline). Display thereafter reads the artifact, no build.
244
+ """
245
+ if spec.kind != "generator" or not spec.default_state_ref:
246
+ raise ValueError("regenerate_default_state requires a generator with default_state_ref")
247
+ comp = spec.to_composite(core=core)
248
+ state = comp.serialize_state()
249
+ param_sig = {k: v.get("default") for k, v in spec.parameters.items()}
250
+ out = Path(base_dir) / spec.default_state_ref
251
+ out.parent.mkdir(parents=True, exist_ok=True)
252
+ out.write_text(json.dumps({"state": state, "_provenance": {"param_signature": param_sig}},
253
+ indent=2), encoding="utf-8")
254
+ return out
255
+
256
+
257
+ _REGISTRY: "dict[str, CompositeSpec]" = {}
258
+
259
+
260
+ def register(spec: CompositeSpec) -> None:
261
+ _REGISTRY[spec.id] = spec
262
+
263
+
264
+ def get(spec_id: str) -> "CompositeSpec | None":
265
+ return _REGISTRY.get(spec_id)
266
+
267
+
268
+ def all_specs() -> "dict[str, CompositeSpec]":
269
+ return dict(_REGISTRY)
270
+
271
+
272
+ def clear_registry() -> None:
273
+ _REGISTRY.clear()
274
+
275
+
276
+ def composite_spec(*, name, description="", parameters=None, visualizations=None,
277
+ emitters=None, analyses=None, tags=None, default_n_steps=None,
278
+ core_extensions=None, default_state_ref=None):
279
+ """Decorator: register a generator function as a CompositeSpec.
280
+
281
+ The wrapped fn becomes the spec's ``builder``; its id is
282
+ ``"<fn.__module__>.<name>"``. Returns the original fn unchanged.
283
+ """
284
+ def decorate(fn):
285
+ spec = CompositeSpec(
286
+ id=f"{fn.__module__}.{name}",
287
+ name=name,
288
+ description=description or (fn.__doc__ or "").strip().split("\n")[0],
289
+ tags=list(tags or []),
290
+ parameters=dict(parameters or {}),
291
+ default_n_steps=default_n_steps,
292
+ visualizations=list(visualizations or []),
293
+ analyses=list(analyses or []),
294
+ emitters=list(emitters or []),
295
+ builder=fn,
296
+ module=fn.__module__,
297
+ default_state_ref=default_state_ref,
298
+ core_extensions=list(core_extensions or []),
299
+ )
300
+ register(spec)
301
+ return fn
302
+ return decorate
303
+
304
+
305
+ def discover_specs(workspace=None) -> "dict[str, CompositeSpec]":
306
+ """Populate + return the registry: import decorator-registered generators
307
+ AND scan a workspace for ``*.composite.{yaml,json}`` files."""
308
+ try:
309
+ from pbg_superpowers.composite_generator import discover_generators
310
+ discover_generators() # fires @composite_spec / @composite_generator on import
311
+ except Exception:
312
+ pass # discovery of code generators is best-effort
313
+ if workspace is not None:
314
+ for pat in ("*.composite.yaml", "*.composite.json"):
315
+ for fp in Path(workspace).rglob(pat):
316
+ try:
317
+ register(CompositeSpec.from_file(fp))
318
+ except Exception:
319
+ continue # a malformed file must not break discovery
320
+ return all_specs()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-bigraph
3
- Version: 1.4.18
3
+ Version: 1.5.0
4
4
  Summary: protocol and execution for compositional systems biology
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  process_bigraph/__init__.py
6
6
  process_bigraph/bundle.py
7
7
  process_bigraph/composite.py
8
+ process_bigraph/composite_spec.py
8
9
  process_bigraph/emitter.py
9
10
  process_bigraph/nextflow.py
10
11
  process_bigraph/plumbing.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "process-bigraph"
7
- version = "1.4.18"
7
+ version = "1.5.0"
8
8
  description = "protocol and execution for compositional systems biology"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"