process-bigraph-lang 0.1.0__py3-none-win_amd64.whl
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_lang/__init__.py +0 -0
- process_bigraph_lang/bin/.gitignore +4 -0
- process_bigraph_lang/bin/cli-native.exe +4 -0
- process_bigraph_lang/py.typed +0 -0
- process_bigraph_lang/v2/__init__.py +56 -0
- process_bigraph_lang/v2/__main__.py +204 -0
- process_bigraph_lang/v2/api.py +192 -0
- process_bigraph_lang/v2/check.py +73 -0
- process_bigraph_lang/v2/interfaces.py +236 -0
- process_bigraph_lang/v2/langium.py +38 -0
- process_bigraph_lang/v2/normalize.py +384 -0
- process_bigraph_lang/v2/sbml.py +234 -0
- process_bigraph_lang/v2/types.py +244 -0
- process_bigraph_lang-0.1.0.dist-info/METADATA +82 -0
- process_bigraph_lang-0.1.0.dist-info/RECORD +17 -0
- process_bigraph_lang-0.1.0.dist-info/WHEEL +5 -0
- process_bigraph_lang-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`describe-model` for SBML: a model file's interface as data (see `docs/v2/design.md`, section 5.6).
|
|
3
|
+
|
|
4
|
+
`describe_sbml` reads an SBML file with the standard library's XML parser (no libsbml, no process code) and
|
|
5
|
+
returns a `ModelHeader`: the model's time unit, compartments, species (as concentrations unless the species has
|
|
6
|
+
only substance units) and parameters, each with its value and its units composed from the model's unit
|
|
7
|
+
definitions in a pint-readable spelling, plus the counts a solver cares about (reactions, events, rules) and the
|
|
8
|
+
SBML level, version and packages. `to_dict` is the JSON form (`pblang-model/0`), which the CLI renders as the
|
|
9
|
+
`.pblang` model header (`print_model_header`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import tempfile
|
|
16
|
+
import xml.etree.ElementTree as ET
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
MODEL_HEADER_VERSION = "pblang-model/0"
|
|
22
|
+
|
|
23
|
+
_KIND_SYMBOLS = {
|
|
24
|
+
"litre": "L",
|
|
25
|
+
"liter": "L",
|
|
26
|
+
"mole": "mol",
|
|
27
|
+
"second": "s",
|
|
28
|
+
"gram": "g",
|
|
29
|
+
"kilogram": "kg",
|
|
30
|
+
"metre": "m",
|
|
31
|
+
"meter": "m",
|
|
32
|
+
"item": "count",
|
|
33
|
+
"dimensionless": "dimensionless",
|
|
34
|
+
"hertz": "Hz",
|
|
35
|
+
"kelvin": "K",
|
|
36
|
+
"joule": "J",
|
|
37
|
+
"newton": "N",
|
|
38
|
+
"pascal": "Pa",
|
|
39
|
+
"watt": "W",
|
|
40
|
+
"volt": "V",
|
|
41
|
+
"ampere": "A",
|
|
42
|
+
"coulomb": "C",
|
|
43
|
+
}
|
|
44
|
+
_SCALE_PREFIX = {3: "k", 2: "h", 1: "da", -1: "d", -2: "c", -3: "m", -6: "u", -9: "n", -12: "p", -15: "f"}
|
|
45
|
+
_TIME_MULTIPLES = {60.0: "min", 3600.0: "h", 86400.0: "day"}
|
|
46
|
+
_SBML_DEFAULTS = {"substance": "mol", "volume": "L", "time": "s", "area": "m**2", "length": "m"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Quantity:
|
|
51
|
+
value: float | None
|
|
52
|
+
units: str | None = None
|
|
53
|
+
constant: bool = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class SpeciesDecl:
|
|
58
|
+
initial: float | None
|
|
59
|
+
units: str | None
|
|
60
|
+
kind: str # concentration | amount
|
|
61
|
+
compartment: str | None
|
|
62
|
+
constant: bool = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ModelHeader:
|
|
67
|
+
name: str
|
|
68
|
+
source: str
|
|
69
|
+
sbml_id: str | None = None
|
|
70
|
+
level: int | None = None
|
|
71
|
+
version: int | None = None
|
|
72
|
+
packages: list[str] = field(default_factory=list)
|
|
73
|
+
time_units: str | None = None
|
|
74
|
+
compartments: dict[str, Quantity] = field(default_factory=dict)
|
|
75
|
+
species: dict[str, SpeciesDecl] = field(default_factory=dict)
|
|
76
|
+
parameters: dict[str, Quantity] = field(default_factory=dict)
|
|
77
|
+
counts: dict[str, int] = field(default_factory=dict)
|
|
78
|
+
notes: list[str] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
return {
|
|
82
|
+
"schema_version": MODEL_HEADER_VERSION,
|
|
83
|
+
"name": self.name,
|
|
84
|
+
"source": self.source,
|
|
85
|
+
"sbml": {"id": self.sbml_id, "level": self.level, "version": self.version, "packages": self.packages},
|
|
86
|
+
"time_units": self.time_units,
|
|
87
|
+
"compartments": {k: {"value": q.value, "units": q.units} for k, q in self.compartments.items()},
|
|
88
|
+
"species": {
|
|
89
|
+
k: {
|
|
90
|
+
"initial": s.initial,
|
|
91
|
+
"units": s.units,
|
|
92
|
+
"kind": s.kind,
|
|
93
|
+
"compartment": s.compartment,
|
|
94
|
+
"constant": s.constant,
|
|
95
|
+
}
|
|
96
|
+
for k, s in self.species.items()
|
|
97
|
+
},
|
|
98
|
+
"parameters": {
|
|
99
|
+
k: {"value": q.value, "units": q.units, "constant": q.constant} for k, q in self.parameters.items()
|
|
100
|
+
},
|
|
101
|
+
"counts": dict(self.counts),
|
|
102
|
+
"notes": list(self.notes),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def describe_sbml(path: str | Path) -> ModelHeader:
|
|
107
|
+
"""The model header of an SBML file."""
|
|
108
|
+
path = Path(path)
|
|
109
|
+
root = ET.parse(path).getroot()
|
|
110
|
+
model = root.find("{*}model")
|
|
111
|
+
if model is None:
|
|
112
|
+
raise ValueError(f"{path}: no <model> element")
|
|
113
|
+
header = ModelHeader(name=_identifier(path.stem), source=path.name, sbml_id=model.get("id"))
|
|
114
|
+
header.level, header.version = _int(root.get("level")), _int(root.get("version"))
|
|
115
|
+
header.packages = sorted({_package(k) for k in root.attrib if k.endswith(":required")} - {""})
|
|
116
|
+
unit_defs = {d.get("id", ""): _unit_text(d) for d in model.findall("{*}listOfUnitDefinitions/{*}unitDefinition")}
|
|
117
|
+
|
|
118
|
+
def units(reference: str | None, default_key: str) -> str | None:
|
|
119
|
+
if reference:
|
|
120
|
+
return unit_defs.get(reference) or _KIND_SYMBOLS.get(reference, reference)
|
|
121
|
+
return unit_defs.get(model.get(f"{default_key}Units") or default_key) or _SBML_DEFAULTS.get(default_key)
|
|
122
|
+
|
|
123
|
+
header.time_units = units(None, "time")
|
|
124
|
+
for c in model.findall("{*}listOfCompartments/{*}compartment"):
|
|
125
|
+
dims = _int(c.get("spatialDimensions")) if c.get("spatialDimensions") else 3
|
|
126
|
+
default = {3: "volume", 2: "area", 1: "length"}.get(dims or 3, "volume")
|
|
127
|
+
header.compartments[c.get("id", "")] = Quantity(_float_or_none(c.get("size")), units(c.get("units"), default))
|
|
128
|
+
for sp in model.findall("{*}listOfSpecies/{*}species"):
|
|
129
|
+
name = sp.get("id", "")
|
|
130
|
+
compartment = sp.get("compartment")
|
|
131
|
+
substance = units(sp.get("substanceUnits"), "substance")
|
|
132
|
+
comp_units = (
|
|
133
|
+
header.compartments[compartment].units if compartment in header.compartments else units(None, "volume")
|
|
134
|
+
)
|
|
135
|
+
only_substance = sp.get("hasOnlySubstanceUnits") == "true"
|
|
136
|
+
if only_substance:
|
|
137
|
+
initial, kind, unit_text = _float_or_none(sp.get("initialAmount")), "amount", substance
|
|
138
|
+
elif sp.get("initialConcentration") is not None:
|
|
139
|
+
initial, kind, unit_text = (
|
|
140
|
+
_float_or_none(sp.get("initialConcentration")),
|
|
141
|
+
"concentration",
|
|
142
|
+
_ratio(substance, comp_units),
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
amount = _float_or_none(sp.get("initialAmount"))
|
|
146
|
+
size = header.compartments[compartment].value if compartment in header.compartments else None
|
|
147
|
+
initial = amount / size if amount is not None and size else amount
|
|
148
|
+
kind, unit_text = "concentration", _ratio(substance, comp_units)
|
|
149
|
+
if amount is not None and size:
|
|
150
|
+
header.notes.append(
|
|
151
|
+
f"species {name}: initial amount {amount:g} converted to concentration by compartment size {size:g}"
|
|
152
|
+
)
|
|
153
|
+
header.species[name] = SpeciesDecl(initial, unit_text, kind, compartment, constant=sp.get("constant") == "true")
|
|
154
|
+
for pr in model.findall("{*}listOfParameters/{*}parameter"):
|
|
155
|
+
header.parameters[pr.get("id", "")] = Quantity(
|
|
156
|
+
_float_or_none(pr.get("value")),
|
|
157
|
+
units(pr.get("units"), "") if pr.get("units") else None,
|
|
158
|
+
pr.get("constant") != "false",
|
|
159
|
+
)
|
|
160
|
+
header.counts = {
|
|
161
|
+
"reactions": len(model.findall("{*}listOfReactions/{*}reaction")),
|
|
162
|
+
"events": len(model.findall("{*}listOfEvents/{*}event")),
|
|
163
|
+
"rules": len(model.findall("{*}listOfRules/*")),
|
|
164
|
+
"function_definitions": len(model.findall("{*}listOfFunctionDefinitions/{*}functionDefinition")),
|
|
165
|
+
}
|
|
166
|
+
return header
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def print_model_header(header: ModelHeader) -> str:
|
|
170
|
+
"""The `.pblang` model header: a generated declaration to `import`, not a composite (rendered by the CLI)."""
|
|
171
|
+
from process_bigraph_lang.v2.langium import run_cli
|
|
172
|
+
|
|
173
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
174
|
+
source = Path(tmp) / "model.json"
|
|
175
|
+
source.write_text(json.dumps(header.to_dict()), encoding="utf-8")
|
|
176
|
+
return run_cli("model-header", str(source)).stdout
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# --- helpers -----------------------------------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _unit_text(definition: ET.Element) -> str:
|
|
183
|
+
parts = []
|
|
184
|
+
for unit in definition.findall("{*}listOfUnits/{*}unit"):
|
|
185
|
+
kind = unit.get("kind", "dimensionless")
|
|
186
|
+
symbol = _KIND_SYMBOLS.get(kind, kind)
|
|
187
|
+
scale = _int(unit.get("scale")) or 0
|
|
188
|
+
multiplier = _float_or_none(unit.get("multiplier")) or 1.0
|
|
189
|
+
exponent = _float_or_none(unit.get("exponent")) or 1.0
|
|
190
|
+
if symbol == "dimensionless" and multiplier == 1.0 and scale == 0:
|
|
191
|
+
continue
|
|
192
|
+
if symbol == "s" and scale == 0 and multiplier in _TIME_MULTIPLES:
|
|
193
|
+
text = _TIME_MULTIPLES[multiplier]
|
|
194
|
+
elif multiplier == 1.0 and scale in _SCALE_PREFIX and symbol != "dimensionless":
|
|
195
|
+
text = f"{_SCALE_PREFIX[scale]}{symbol}"
|
|
196
|
+
elif multiplier == 1.0 and scale == 0:
|
|
197
|
+
text = symbol
|
|
198
|
+
else:
|
|
199
|
+
text = f"({multiplier:g}e{scale} * {symbol})"
|
|
200
|
+
if exponent != 1.0:
|
|
201
|
+
text += f"**{exponent:g}"
|
|
202
|
+
parts.append(text)
|
|
203
|
+
return " * ".join(parts) if parts else "dimensionless"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ratio(numerator: str | None, denominator: str | None) -> str | None:
|
|
207
|
+
if not numerator:
|
|
208
|
+
return None
|
|
209
|
+
if not denominator or denominator == "dimensionless":
|
|
210
|
+
return numerator
|
|
211
|
+
return f"{numerator} / {denominator}" if " " not in denominator else f"{numerator} / ({denominator})"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _package(attribute: str) -> str:
|
|
215
|
+
return attribute.split("}")[0].lstrip("{").rsplit("/", 2)[-2] if "}" in attribute else ""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _identifier(text: str) -> str:
|
|
219
|
+
cleaned = "".join(c if c.isalnum() or c == "_" else "_" for c in text)
|
|
220
|
+
return cleaned if cleaned and not cleaned[0].isdigit() else f"m_{cleaned}"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _int(text: str | None) -> int | None:
|
|
224
|
+
try:
|
|
225
|
+
return int(text) if text is not None else None
|
|
226
|
+
except ValueError:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _float_or_none(text: str | None) -> float | None:
|
|
231
|
+
try:
|
|
232
|
+
return float(text) if text is not None else None
|
|
233
|
+
except ValueError:
|
|
234
|
+
return None
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type expressions as data, for the normalizer and for `describe`: `parse_type` accepts both bigraph-schema
|
|
3
|
+
forms, the string grammar (`list[float]`, `a:float|b:integer`, `A~B`, `array[(3|4),float]`, `float{3.5}`) and
|
|
4
|
+
the dict form (`{"_type": "map", "_value": ...}`), and keeps the original (`source`) so that `to_bigraph` is
|
|
5
|
+
lossless. The DSL spelling of a type lives in TypeScript (`dsl/src/language/v2/types.ts`).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Final
|
|
14
|
+
|
|
15
|
+
from bigraph_schema.parse import parse_expression # type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Missing:
|
|
19
|
+
"""The absence of a default (distinct from `None`)."""
|
|
20
|
+
|
|
21
|
+
_instance: _Missing | None = None
|
|
22
|
+
|
|
23
|
+
def __new__(cls) -> _Missing:
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = super().__new__(cls)
|
|
26
|
+
return cls._instance
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return "MISSING"
|
|
30
|
+
|
|
31
|
+
def __bool__(self) -> bool:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
MISSING: Final = _Missing()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Named:
|
|
40
|
+
"""A (possibly parametric) named type: `float`, `list[float]`, `map[string,float]`, `array[(3|4),float]`."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
args: list[TypeExpr] = field(default_factory=list)
|
|
44
|
+
default: Any = MISSING
|
|
45
|
+
units: str | None = None
|
|
46
|
+
source: Any = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Struct:
|
|
51
|
+
"""A dict schema: `a:float|b:integer` or `{"a": "float", "b": "integer"}`."""
|
|
52
|
+
|
|
53
|
+
fields: dict[str, TypeExpr]
|
|
54
|
+
source: Any = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class UnionType:
|
|
59
|
+
"""A union: `A~B`."""
|
|
60
|
+
|
|
61
|
+
options: list[TypeExpr]
|
|
62
|
+
source: Any = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Group:
|
|
67
|
+
"""A shape group inside an array type: `(3|4)`."""
|
|
68
|
+
|
|
69
|
+
items: list[int | str]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Raw:
|
|
74
|
+
"""A dict-form type with attributes that have no dedicated representation; kept verbatim."""
|
|
75
|
+
|
|
76
|
+
attrs: dict[str, Any]
|
|
77
|
+
source: Any = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
TypeExpr = Named | Struct | UnionType | Group | Raw
|
|
81
|
+
|
|
82
|
+
_PARAM_KEYS: dict[str, tuple[str, ...]] = {
|
|
83
|
+
"list": ("_element",),
|
|
84
|
+
"set": ("_element",),
|
|
85
|
+
"tree": ("_leaf",),
|
|
86
|
+
"map": ("_key", "_value"),
|
|
87
|
+
"tuple": ("_values",),
|
|
88
|
+
"enum": ("_values",),
|
|
89
|
+
"union": ("_options",),
|
|
90
|
+
"maybe": ("_value",),
|
|
91
|
+
"overwrite": ("_value",),
|
|
92
|
+
"const": ("_value",),
|
|
93
|
+
"quote": ("_value",),
|
|
94
|
+
}
|
|
95
|
+
_ARRAY_KEYS = ("_shape", "_data", "_units")
|
|
96
|
+
_BIGRAPH_NAMES = {"int": "integer", "bool": "boolean"}
|
|
97
|
+
_TRAILING_DEFAULT = re.compile(r"^([^{}]*)\{(.*)\}$")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_type(spec: Any) -> TypeExpr:
|
|
101
|
+
"""Parse a bigraph-schema type (string or dict form) into a `TypeExpr` that remembers its source."""
|
|
102
|
+
if isinstance(spec, str):
|
|
103
|
+
return _from_string(spec)
|
|
104
|
+
if isinstance(spec, dict):
|
|
105
|
+
return _from_dict(spec)
|
|
106
|
+
raise TypeError(f"not a type expression: {spec!r}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _from_string(text: str) -> TypeExpr:
|
|
110
|
+
source = text
|
|
111
|
+
stripped = text.strip()
|
|
112
|
+
default: Any = MISSING
|
|
113
|
+
match = _TRAILING_DEFAULT.match(stripped)
|
|
114
|
+
if match:
|
|
115
|
+
stripped, default = match.group(1), _literal(match.group(2))
|
|
116
|
+
expr = _from_parsed(parse_expression(stripped))
|
|
117
|
+
if default is not MISSING and isinstance(expr, Named):
|
|
118
|
+
expr.default = default
|
|
119
|
+
if not isinstance(expr, Group):
|
|
120
|
+
expr.source = source
|
|
121
|
+
return expr
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _literal(text: str) -> Any:
|
|
125
|
+
try:
|
|
126
|
+
return json.loads(text)
|
|
127
|
+
except ValueError:
|
|
128
|
+
return text
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _from_parsed(parsed: Any) -> TypeExpr:
|
|
132
|
+
if isinstance(parsed, str):
|
|
133
|
+
return Named(parsed)
|
|
134
|
+
if isinstance(parsed, list):
|
|
135
|
+
name, params = parsed
|
|
136
|
+
return Named(str(name), [_from_parsed(p) for p in params])
|
|
137
|
+
if isinstance(parsed, tuple):
|
|
138
|
+
return Group([_dim(p) for p in parsed])
|
|
139
|
+
if isinstance(parsed, dict):
|
|
140
|
+
if "_union" in parsed:
|
|
141
|
+
return UnionType([_from_parsed(p) for p in parsed["_union"]])
|
|
142
|
+
return Struct({str(k): _from_parsed(v) for k, v in parsed.items()})
|
|
143
|
+
raise TypeError(f"unexpected parse result: {parsed!r}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _dim(value: Any) -> int | str:
|
|
147
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
148
|
+
return int(value)
|
|
149
|
+
text = str(value)
|
|
150
|
+
return int(text) if text.lstrip("-").isdigit() else text
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _from_dict(spec: dict[str, Any]) -> TypeExpr:
|
|
154
|
+
if "_type" not in spec:
|
|
155
|
+
if any(str(k).startswith("_") for k in spec):
|
|
156
|
+
return Raw(dict(spec), source=spec)
|
|
157
|
+
return Struct({str(k): parse_type(v) for k, v in spec.items()}, source=spec)
|
|
158
|
+
name = spec["_type"]
|
|
159
|
+
if not isinstance(name, str):
|
|
160
|
+
return Raw(dict(spec), source=spec)
|
|
161
|
+
if any(c in name for c in "[{|~") and set(spec) <= {"_type", "_default"}:
|
|
162
|
+
inner = _from_string(name) # `{"_type": "list[string]", "_default": []}`
|
|
163
|
+
if isinstance(inner, Named) and "_default" in spec:
|
|
164
|
+
inner.default = spec["_default"]
|
|
165
|
+
if not isinstance(inner, Group):
|
|
166
|
+
inner.source = spec
|
|
167
|
+
return inner
|
|
168
|
+
keys = set(spec) - {"_type", "_default", "_units"}
|
|
169
|
+
args: list[TypeExpr] = []
|
|
170
|
+
units: str | None = spec.get("_units")
|
|
171
|
+
if name in _PARAM_KEYS and keys <= set(_PARAM_KEYS[name]):
|
|
172
|
+
for key in _PARAM_KEYS[name]:
|
|
173
|
+
if key not in spec:
|
|
174
|
+
continue
|
|
175
|
+
if key in ("_values", "_options"):
|
|
176
|
+
args.extend(Named(str(v)) if name == "enum" else parse_type(v) for v in spec[key])
|
|
177
|
+
else:
|
|
178
|
+
args.append(parse_type(spec[key]))
|
|
179
|
+
elif keys and keys <= set(_ARRAY_KEYS):
|
|
180
|
+
if "_shape" in spec:
|
|
181
|
+
args.append(Group([_dim(d) for d in spec["_shape"]]))
|
|
182
|
+
if "_data" in spec:
|
|
183
|
+
args.append(parse_type(spec["_data"]))
|
|
184
|
+
elif keys:
|
|
185
|
+
return Raw(dict(spec), source=spec)
|
|
186
|
+
return Named(name, args, default=spec.get("_default", MISSING), units=units, source=spec)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def to_bigraph(expr: TypeExpr) -> Any:
|
|
190
|
+
"""Render a `TypeExpr` as a bigraph-schema type (the original form when it came from one)."""
|
|
191
|
+
if not isinstance(expr, Group) and expr.source is not None:
|
|
192
|
+
return expr.source
|
|
193
|
+
if isinstance(expr, Named):
|
|
194
|
+
return _named_to_bigraph(expr)
|
|
195
|
+
if isinstance(expr, Struct):
|
|
196
|
+
return {k: to_bigraph(v) for k, v in expr.fields.items()}
|
|
197
|
+
if isinstance(expr, UnionType):
|
|
198
|
+
rendered = [to_bigraph(o) for o in expr.options]
|
|
199
|
+
if all(isinstance(r, str) for r in rendered):
|
|
200
|
+
return "~".join(rendered)
|
|
201
|
+
return {"_type": "union", "_options": rendered}
|
|
202
|
+
if isinstance(expr, Group):
|
|
203
|
+
return "(" + "|".join(str(i) for i in expr.items) + ")"
|
|
204
|
+
return expr.attrs
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _named_to_bigraph(expr: Named) -> Any:
|
|
208
|
+
name = _BIGRAPH_NAMES.get(expr.name, expr.name)
|
|
209
|
+
shape = next((a for a in expr.args if isinstance(a, Group)), None)
|
|
210
|
+
others = [to_bigraph(a) for a in expr.args if not isinstance(a, Group)]
|
|
211
|
+
rendered: Any
|
|
212
|
+
if not expr.args:
|
|
213
|
+
rendered = name
|
|
214
|
+
elif shape is not None:
|
|
215
|
+
rendered = {"_type": name, "_shape": list(shape.items)}
|
|
216
|
+
if others:
|
|
217
|
+
rendered["_data"] = others[0]
|
|
218
|
+
elif all(isinstance(r, str) for r in others):
|
|
219
|
+
rendered = f"{name}[{','.join(others)}]"
|
|
220
|
+
elif name in ("list", "set"):
|
|
221
|
+
rendered = {"_type": name, "_element": others[0]}
|
|
222
|
+
elif name == "tree":
|
|
223
|
+
rendered = {"_type": name, "_leaf": others[0]}
|
|
224
|
+
elif name == "map":
|
|
225
|
+
rendered = {"_type": name, "_value": others[-1]}
|
|
226
|
+
if len(others) == 2:
|
|
227
|
+
rendered["_key"] = others[0]
|
|
228
|
+
elif name in ("tuple", "enum"):
|
|
229
|
+
rendered = {"_type": name, "_values": others}
|
|
230
|
+
elif name == "union":
|
|
231
|
+
rendered = {"_type": name, "_options": others}
|
|
232
|
+
else:
|
|
233
|
+
rendered = {"_type": name, "_value": others[0]}
|
|
234
|
+
if expr.units is not None:
|
|
235
|
+
rendered = (
|
|
236
|
+
{**rendered, "_units": expr.units}
|
|
237
|
+
if isinstance(rendered, dict)
|
|
238
|
+
else {"_type": rendered, "_units": expr.units}
|
|
239
|
+
)
|
|
240
|
+
if expr.default is not MISSING:
|
|
241
|
+
if isinstance(rendered, dict):
|
|
242
|
+
return {**rendered, "_default": expr.default}
|
|
243
|
+
return {"_type": rendered, "_default": expr.default}
|
|
244
|
+
return rendered
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: process_bigraph_lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A language server, CLI and Python package for a domain specific language that describes process bigraphs.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Jim Schaff
|
|
7
|
+
Author-email: schaff@uchc.edu
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
15
|
+
Requires-Dist: bigraph-schema (>=1.6.0,<2.0.0)
|
|
16
|
+
Requires-Dist: numpy (>=1.26)
|
|
17
|
+
Project-URL: Documentation, https://biosimulations.github.io/process-bigraph-lang/
|
|
18
|
+
Project-URL: Repository, https://github.com/biosimulations/process-bigraph-lang
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# process-bigraph-lang
|
|
22
|
+
|
|
23
|
+
[](https://img.shields.io/github/v/release/biosimulations/process-bigraph-lang)
|
|
24
|
+
[](https://github.com/biosimulations/process-bigraph-lang/actions/workflows/main.yml?query=branch%3Amain)
|
|
25
|
+
[](https://codecov.io/gh/biosimulations/process-bigraph-lang)
|
|
26
|
+
[](https://img.shields.io/github/commit-activity/m/biosimulations/process-bigraph-lang)
|
|
27
|
+
[](https://img.shields.io/github/license/biosimulations/process-bigraph-lang)
|
|
28
|
+
|
|
29
|
+
A language server, CLI and Python package for `.pblang`, a domain specific language that describes
|
|
30
|
+
[process bigraph](https://github.com/vivarium-collective/process-bigraph) composites: a tree-shaped, verifiable
|
|
31
|
+
projection of the composite document, with imported interface declarations, parameters, open sites, units and
|
|
32
|
+
model headers that the checker verifies before anything runs. See [the architecture](docs/architecture.md) and
|
|
33
|
+
[the design](docs/v2/design.md).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install process_bigraph_lang
|
|
37
|
+
python -m process_bigraph_lang.v2 compile composite.pblang --interfaces headers.manifest.json -o composite.json
|
|
38
|
+
python -m process_bigraph_lang.v2 decompile composite.json
|
|
39
|
+
python -m process_bigraph_lang.v2 check composite.pblang --interfaces headers.manifest.json
|
|
40
|
+
python -m process_bigraph_lang.v2 describe my_package.processes --manifest headers.manifest.json --out headers/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Github repository**: <https://github.com/biosimulations/process-bigraph-lang/>
|
|
44
|
+
- **Documentation** <https://biosimulations.github.io/process-bigraph-lang/>
|
|
45
|
+
|
|
46
|
+
## Getting started with your project
|
|
47
|
+
|
|
48
|
+
First, create a repository on GitHub with the same name as this project, and then run the following commands:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git init -b main
|
|
52
|
+
git add .
|
|
53
|
+
git commit -m "init commit"
|
|
54
|
+
git remote add origin git@github.com:biosimulations/process-bigraph-lang.git
|
|
55
|
+
git push -u origin main
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Finally, install the environment and the pre-commit hooks with
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
make install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You are now ready to start development on your project!
|
|
65
|
+
The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release.
|
|
66
|
+
|
|
67
|
+
To finalize the set-up for publishing to PyPI or Artifactory, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/publishing/#set-up-for-pypi).
|
|
68
|
+
For activating the automatic documentation with MkDocs, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/mkdocs/#enabling-the-documentation-on-github).
|
|
69
|
+
To enable the code coverage reports, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/codecov/).
|
|
70
|
+
|
|
71
|
+
## Releasing a new version
|
|
72
|
+
|
|
73
|
+
- Create an API Token on [PyPI](https://pypi.org/).
|
|
74
|
+
- Add the API Token to your projects secrets with the name `PYPI_TOKEN` by visiting [this page](https://github.com/biosimulations/process-bigraph-lang/settings/secrets/actions/new).
|
|
75
|
+
- Create a [new release](https://github.com/biosimulations/process-bigraph-lang/releases/new) on Github.
|
|
76
|
+
- Create a new tag in the form `*.*.*`.
|
|
77
|
+
- For more details, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/cicd/#how-to-trigger-a-release).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
Repository initiated with [fpgmaas/cookiecutter-poetry](https://github.com/fpgmaas/cookiecutter-poetry).
|
|
82
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
process_bigraph_lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
process_bigraph_lang/bin/.gitignore,sha256=Mc0TXahfdSsEA2MHbJ2xOafJsQEV0Lh5ZnAmdeB8C2g,75
|
|
3
|
+
process_bigraph_lang/bin/cli-native.exe,sha256=EssQrGqEByA2sVeOsHo13LF1zdYeccoASv0eet6ttKQ,82848027
|
|
4
|
+
process_bigraph_lang/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
process_bigraph_lang/v2/__init__.py,sha256=XbRZCZq13KSoa5i2qCqUjJ1GhVQpEwFo1kYj7rin7qg,2053
|
|
6
|
+
process_bigraph_lang/v2/__main__.py,sha256=wvL0b5Lr7wvgFHQnIakOunwVBmIarr8fPpAzzZFWNbA,8773
|
|
7
|
+
process_bigraph_lang/v2/api.py,sha256=7dVRu2F6E10IMORQTr5YbeOOZA4d_LhBm9GUH2tQZFU,7481
|
|
8
|
+
process_bigraph_lang/v2/check.py,sha256=FnW39sxCYL1_c0M-9Edqvtx6v4s_Gpbf2HZDkZZFcDM,2835
|
|
9
|
+
process_bigraph_lang/v2/interfaces.py,sha256=AYox41unDoCBjV-8P6OQoWhVqzUAByzNDmQf1GfkAoQ,9277
|
|
10
|
+
process_bigraph_lang/v2/langium.py,sha256=KRT94Ptyy_yVLZ_rWgPW_dxy0ly9ni8HsO-dve4Wcq8,1597
|
|
11
|
+
process_bigraph_lang/v2/normalize.py,sha256=y5aNmM9Q4Z_qietsFJxuf0Uk_M8nzIRNTKezqyg2TWw,17080
|
|
12
|
+
process_bigraph_lang/v2/sbml.py,sha256=J1LFxPzOTVOOy0_6dpME6TJyuz24TDgT_fF16uAU_nk,9732
|
|
13
|
+
process_bigraph_lang/v2/types.py,sha256=mX0LPaBFbnrMHUofVIztTTDpgJQN2WR6mQ_o3DD9z4M,8306
|
|
14
|
+
process_bigraph_lang-0.1.0.dist-info/licenses/LICENSE,sha256=uuKYfnhjRd2IOKltacW_z7JSZLVs9Mqj50o9fVnRFyI,1089
|
|
15
|
+
process_bigraph_lang-0.1.0.dist-info/METADATA,sha256=lNb9wBSZemfTFhRyzPfFEyO17XASWjnWHDXUAbKlnHQ,4510
|
|
16
|
+
process_bigraph_lang-0.1.0.dist-info/WHEEL,sha256=hMN48yOIL1Tonvh8WM1EhFkq5MVedJfjGh-0PiW51jw,96
|
|
17
|
+
process_bigraph_lang-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Jim Schaff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|