bigraph-schema 1.3.2__tar.gz → 1.3.4__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.
- {bigraph_schema-1.3.2/bigraph_schema.egg-info → bigraph_schema-1.3.4}/PKG-INFO +4 -1
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/README.md +3 -0
- bigraph_schema-1.3.4/bigraph_schema/json_codec.py +310 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/apply.py +41 -1
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/realize.py +20 -4
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4/bigraph_schema.egg-info}/PKG-INFO +4 -1
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/SOURCES.txt +1 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/pyproject.toml +1 -1
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/AUTHORS.md +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/LICENSE +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/__init__.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/assembly.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/calculi.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/core.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/edge.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/__init__.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/bundle.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/check.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/coerce.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/default.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/derive.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/diff.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/divide.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/events.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/generalize.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/handle_parameters.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/infer.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/is_empty.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/jump.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/merge.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/patch.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/reconcile.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/resolve.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/select.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/serialize.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/transform.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/validate.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/walk.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/package/__init__.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/package/discover.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/parse.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/protocols.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/schema.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/units.py +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/dependency_links.txt +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/requires.txt +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/top_level.txt +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/setup.cfg +0 -0
- {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bigraph-schema
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: A serializable type schema for compositional systems biology
|
|
5
5
|
Author: Eran Agmon, Ryan Spangler
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -21,6 +21,7 @@ Dynamic: license-file
|
|
|
21
21
|
|
|
22
22
|
[](https://pypi.org/project/bigraph-schema/)
|
|
23
23
|
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
|
|
24
|
+
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html)
|
|
24
25
|
|
|
25
26
|
`bigraph-schema` provides a serializable type schema for compositional and multiscale modeling.
|
|
26
27
|
It defines a compact, extensible language for describing hierarchical data structures — the foundation of the Vivarium 2.0 simulation framework.
|
|
@@ -47,6 +48,8 @@ The following resources provide guided introductions and examples:
|
|
|
47
48
|
|
|
48
49
|
- [Bigraph Schema Basics Tutorial](https://vivarium-collective.github.io/bigraph-viz/notebooks/basics.html) – Explains how to compose and visualize schema graphs using the `bigraph-schema` syntax.
|
|
49
50
|
|
|
51
|
+
- [Units Demo](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html) – Demonstrates `Quantity`, `Number._units`, and wire-level unit conversion.
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
---
|
|
52
55
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/bigraph-schema/)
|
|
4
4
|
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
|
|
5
|
+
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html)
|
|
5
6
|
|
|
6
7
|
`bigraph-schema` provides a serializable type schema for compositional and multiscale modeling.
|
|
7
8
|
It defines a compact, extensible language for describing hierarchical data structures — the foundation of the Vivarium 2.0 simulation framework.
|
|
@@ -28,6 +29,8 @@ The following resources provide guided introductions and examples:
|
|
|
28
29
|
|
|
29
30
|
- [Bigraph Schema Basics Tutorial](https://vivarium-collective.github.io/bigraph-viz/notebooks/basics.html) – Explains how to compose and visualize schema graphs using the `bigraph-schema` syntax.
|
|
30
31
|
|
|
32
|
+
- [Units Demo](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html) – Demonstrates `Quantity`, `Number._units`, and wire-level unit conversion.
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
---
|
|
33
36
|
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""JSON codec for bigraph-schema state documents.
|
|
2
|
+
|
|
3
|
+
The dispatch-based :func:`~bigraph_schema.methods.serialize.serialize` /
|
|
4
|
+
:func:`~bigraph_schema.methods.realize.realize` pair handles per-leaf
|
|
5
|
+
serialization when a paired schema is available. Many downstream consumers
|
|
6
|
+
(workspace dashboards, save-state caches, subprocess runners) need to JSON-
|
|
7
|
+
serialize a state document **without** carrying the matched schema, so a
|
|
8
|
+
schema-agnostic codec is required as well.
|
|
9
|
+
|
|
10
|
+
This module exposes that codec. The wire format uses tagged dicts so values
|
|
11
|
+
round-trip without schema info:
|
|
12
|
+
|
|
13
|
+
- ``pint.Quantity`` → ``{"__pint__": true, "magnitude": ..., "units": "..."}``
|
|
14
|
+
(array-valued Quantities also carry ``"__magnitude_dtype__"``)
|
|
15
|
+
- ``numpy.ndarray`` → ``{"__numpy__": true, "dtype": "...", "shape": [...], "data": [...]}``
|
|
16
|
+
- ``numpy`` structured array →
|
|
17
|
+
``{"__numpy_structured__": true, "dtype": [...], "shape": [...], "data": [...]}``
|
|
18
|
+
- ``numpy`` scalars → native ``int``/``float``/``bool``
|
|
19
|
+
- ``set``/``frozenset`` → ``{"__set__": true, "data": [...]}``
|
|
20
|
+
- ``bytes`` → ``{"__bytes__": true, "data": "<hex>"}``
|
|
21
|
+
|
|
22
|
+
Tuples are NOT tagged — ``json.JSONEncoder`` bypasses ``default()`` for
|
|
23
|
+
tuples since they're natively encodable as JSON arrays. They round-trip
|
|
24
|
+
as lists.
|
|
25
|
+
|
|
26
|
+
This matches the format already in use in ``v2ecoli/cache.py:NumpyJSONEncoder``;
|
|
27
|
+
the canonical implementation now lives here and consumers should import from
|
|
28
|
+
this module rather than redefining their own. Saved state files using the
|
|
29
|
+
v2ecoli encoder remain readable.
|
|
30
|
+
|
|
31
|
+
The ``pint`` reconstruction uses
|
|
32
|
+
:func:`~bigraph_schema.units.get_quantity_registry`, so consumers that swap
|
|
33
|
+
the registry (e.g. v2ecoli setting pint's application registry to bigraph-
|
|
34
|
+
schema's shared one) get coherent ``Quantity`` instances.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BigraphJSONEncoder(json.JSONEncoder):
|
|
43
|
+
"""JSON encoder for bigraph-schema state trees.
|
|
44
|
+
|
|
45
|
+
Use directly via ``json.dumps(obj, cls=BigraphJSONEncoder)`` or through
|
|
46
|
+
the :func:`dumps` convenience wrapper. Pair with :func:`bigraph_json_hook`
|
|
47
|
+
on the read side.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def default(self, obj: Any) -> Any:
|
|
51
|
+
# pint.Quantity — emit tagged {magnitude, units} so we round-trip
|
|
52
|
+
# without needing schema info at deserialize time. Array-valued
|
|
53
|
+
# Quantities also carry their magnitude dtype so e.g. counts stay int.
|
|
54
|
+
try:
|
|
55
|
+
import pint
|
|
56
|
+
if isinstance(obj, pint.Quantity):
|
|
57
|
+
mag = obj.magnitude
|
|
58
|
+
try:
|
|
59
|
+
import numpy as np
|
|
60
|
+
if isinstance(mag, np.ndarray):
|
|
61
|
+
return {
|
|
62
|
+
'__pint__': True,
|
|
63
|
+
'magnitude': mag.tolist(),
|
|
64
|
+
'__magnitude_dtype__': str(mag.dtype),
|
|
65
|
+
'units': str(obj.units),
|
|
66
|
+
}
|
|
67
|
+
except ImportError:
|
|
68
|
+
pass
|
|
69
|
+
if isinstance(mag, (int, float)):
|
|
70
|
+
return {'__pint__': True, 'magnitude': mag, 'units': str(obj.units)}
|
|
71
|
+
return {'__pint__': True, 'magnitude': float(mag), 'units': str(obj.units)}
|
|
72
|
+
except ImportError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# numpy
|
|
76
|
+
try:
|
|
77
|
+
import numpy as np
|
|
78
|
+
if isinstance(obj, np.ndarray):
|
|
79
|
+
if obj.dtype.names:
|
|
80
|
+
dtype_list = []
|
|
81
|
+
for name in obj.dtype.names:
|
|
82
|
+
field_dtype = obj.dtype[name]
|
|
83
|
+
if field_dtype.shape:
|
|
84
|
+
dtype_list.append((name, str(field_dtype.base), list(field_dtype.shape)))
|
|
85
|
+
else:
|
|
86
|
+
dtype_list.append((name, str(field_dtype)))
|
|
87
|
+
return {
|
|
88
|
+
'__numpy_structured__': True,
|
|
89
|
+
'dtype': dtype_list,
|
|
90
|
+
'shape': list(obj.shape),
|
|
91
|
+
'data': [list(row) for row in obj.tolist()],
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
'__numpy__': True,
|
|
95
|
+
'dtype': str(obj.dtype),
|
|
96
|
+
'shape': list(obj.shape),
|
|
97
|
+
'data': obj.tolist(),
|
|
98
|
+
}
|
|
99
|
+
if isinstance(obj, np.integer):
|
|
100
|
+
return int(obj)
|
|
101
|
+
if isinstance(obj, np.floating):
|
|
102
|
+
return float(obj)
|
|
103
|
+
if isinstance(obj, np.bool_):
|
|
104
|
+
return bool(obj)
|
|
105
|
+
except ImportError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# builtins json.dumps can't represent
|
|
109
|
+
if isinstance(obj, (set, frozenset)):
|
|
110
|
+
return {'__set__': True, 'data': sorted(obj, key=str)}
|
|
111
|
+
if isinstance(obj, bytes):
|
|
112
|
+
return {'__bytes__': True, 'data': obj.hex()}
|
|
113
|
+
# Note: tuples are NOT tagged. json.JSONEncoder bypasses ``default()``
|
|
114
|
+
# for tuples because they're natively encodable as JSON arrays, so a
|
|
115
|
+
# tag here wouldn't fire. Tuples round-trip as lists. Callers that
|
|
116
|
+
# need tuple-ness preserved should convert at the schema layer.
|
|
117
|
+
|
|
118
|
+
return super().default(obj)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def bigraph_json_hook(obj: Any) -> Any:
|
|
122
|
+
"""JSON ``object_hook`` that reverses :class:`BigraphJSONEncoder`.
|
|
123
|
+
|
|
124
|
+
Pass to ``json.load(f, object_hook=bigraph_json_hook)`` or through
|
|
125
|
+
:func:`loads`. Recognises the tag keys (``__pint__`` etc.) and rebuilds
|
|
126
|
+
the corresponding Python objects. Unrecognised dicts pass through
|
|
127
|
+
unchanged, so plain JSON objects still load as ``dict``.
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(obj, dict):
|
|
130
|
+
return obj
|
|
131
|
+
if obj.get('__pint__') or obj.get('__pint_array__'):
|
|
132
|
+
# ``__pint_array__`` is v2ecoli's legacy variant tag for array-valued
|
|
133
|
+
# Quantities; accept it as an alias of ``__pint__`` so saved state
|
|
134
|
+
# files from before this module existed keep loading.
|
|
135
|
+
from bigraph_schema.units import get_quantity_registry
|
|
136
|
+
ureg = get_quantity_registry()
|
|
137
|
+
mag = obj.get('magnitude', obj.get('value'))
|
|
138
|
+
dtype = obj.get('__magnitude_dtype__')
|
|
139
|
+
if obj.get('__pint_array__') or dtype is not None:
|
|
140
|
+
import numpy as np
|
|
141
|
+
mag = np.array(mag, dtype=dtype) if dtype else np.array(mag)
|
|
142
|
+
return ureg.Quantity(mag, obj['units'])
|
|
143
|
+
if obj.get('__numpy__'):
|
|
144
|
+
import numpy as np
|
|
145
|
+
return np.array(obj['data'], dtype=obj['dtype']).reshape(obj['shape'])
|
|
146
|
+
if obj.get('__numpy_structured__'):
|
|
147
|
+
import numpy as np
|
|
148
|
+
dtype = np.dtype([tuple(field) for field in obj['dtype']])
|
|
149
|
+
return np.array([tuple(row) for row in obj['data']], dtype=dtype).reshape(obj['shape'])
|
|
150
|
+
if obj.get('__set__'):
|
|
151
|
+
return set(obj['data'])
|
|
152
|
+
if obj.get('__bytes__'):
|
|
153
|
+
return bytes.fromhex(obj['data'])
|
|
154
|
+
return obj
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def dumps(obj: Any, **kwargs: Any) -> str:
|
|
158
|
+
"""Serialize ``obj`` to a JSON string via :class:`BigraphJSONEncoder`."""
|
|
159
|
+
return json.dumps(obj, cls=BigraphJSONEncoder, **kwargs)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def loads(s: str, **kwargs: Any) -> Any:
|
|
163
|
+
"""Deserialize a JSON string via :func:`bigraph_json_hook`."""
|
|
164
|
+
return json.loads(s, object_hook=bigraph_json_hook, **kwargs)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Tests (inline, run via `python -m bigraph_schema.json_codec` or pytest).
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def test_pint_scalar_round_trip():
|
|
172
|
+
import pint
|
|
173
|
+
from bigraph_schema.units import units as ureg
|
|
174
|
+
q = ureg.Quantity(5.0, 'femtogram')
|
|
175
|
+
s = dumps(q)
|
|
176
|
+
q2 = loads(s)
|
|
177
|
+
assert isinstance(q2, pint.Quantity)
|
|
178
|
+
assert q2.magnitude == 5.0
|
|
179
|
+
assert str(q2.units) == str(q.units)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_pint_array_round_trip():
|
|
183
|
+
import numpy as np
|
|
184
|
+
import pint
|
|
185
|
+
from bigraph_schema.units import units as ureg
|
|
186
|
+
q = ureg.Quantity(np.array([1, 2, 3], dtype=np.int64), 'count')
|
|
187
|
+
s = dumps(q)
|
|
188
|
+
q2 = loads(s)
|
|
189
|
+
assert isinstance(q2, pint.Quantity)
|
|
190
|
+
assert isinstance(q2.magnitude, np.ndarray)
|
|
191
|
+
assert q2.magnitude.dtype == np.int64
|
|
192
|
+
assert (q2.magnitude == np.array([1, 2, 3])).all()
|
|
193
|
+
assert str(q2.units) == str(q.units)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_pint_int_magnitude_preserved():
|
|
197
|
+
"""Int magnitudes shouldn't silently widen to float on round-trip."""
|
|
198
|
+
from bigraph_schema.units import units as ureg
|
|
199
|
+
q = ureg.Quantity(42, 'count')
|
|
200
|
+
s = dumps(q)
|
|
201
|
+
q2 = loads(s)
|
|
202
|
+
assert q2.magnitude == 42
|
|
203
|
+
assert isinstance(q2.magnitude, int)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_numpy_ndarray_round_trip():
|
|
207
|
+
import numpy as np
|
|
208
|
+
a = np.array([[1.5, 2.5], [3.5, 4.5]])
|
|
209
|
+
s = dumps(a)
|
|
210
|
+
a2 = loads(s)
|
|
211
|
+
assert isinstance(a2, np.ndarray)
|
|
212
|
+
assert a2.dtype == a.dtype
|
|
213
|
+
assert a2.shape == a.shape
|
|
214
|
+
assert (a2 == a).all()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_numpy_scalar_round_trip():
|
|
218
|
+
import numpy as np
|
|
219
|
+
s = dumps({'i': np.int64(7), 'f': np.float32(1.5), 'b': np.bool_(True)})
|
|
220
|
+
obj = loads(s)
|
|
221
|
+
assert obj == {'i': 7, 'f': 1.5, 'b': True}
|
|
222
|
+
assert isinstance(obj['i'], int)
|
|
223
|
+
assert isinstance(obj['f'], float)
|
|
224
|
+
assert isinstance(obj['b'], bool)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_set_round_trip():
|
|
228
|
+
s = dumps({1, 2, 3})
|
|
229
|
+
assert loads(s) == {1, 2, 3}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_bytes_round_trip():
|
|
233
|
+
payload = b'\x00\x01\xff'
|
|
234
|
+
assert loads(dumps(payload)) == payload
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_tuple_becomes_list():
|
|
238
|
+
"""Tuples round-trip as lists — json.JSONEncoder bypasses ``default()``
|
|
239
|
+
for tuples since they're natively JSON-encodable as arrays. Documenting
|
|
240
|
+
this so callers know tuple-ness isn't preserved by this codec."""
|
|
241
|
+
out = loads(dumps((1, 'two', 3.0)))
|
|
242
|
+
assert out == [1, 'two', 3.0]
|
|
243
|
+
assert isinstance(out, list)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_nested_state_tree():
|
|
247
|
+
"""The hook must fire at every level — top-level dict with nested Quantities."""
|
|
248
|
+
import numpy as np
|
|
249
|
+
from bigraph_schema.units import units as ureg
|
|
250
|
+
tree = {
|
|
251
|
+
'agent': {
|
|
252
|
+
'mass': ureg.Quantity(123.4, 'fg'),
|
|
253
|
+
'counts': ureg.Quantity(np.array([10, 20], dtype=np.int64), 'count'),
|
|
254
|
+
'genome': b'ATCG',
|
|
255
|
+
'tags': {'a', 'b'},
|
|
256
|
+
'history': [
|
|
257
|
+
{'t': 0, 'rate': ureg.Quantity(0.5, '1/second')},
|
|
258
|
+
{'t': 1, 'rate': ureg.Quantity(0.6, '1/second')},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
out = loads(dumps(tree))
|
|
263
|
+
assert out['agent']['mass'].magnitude == 123.4
|
|
264
|
+
assert (out['agent']['counts'].magnitude == np.array([10, 20])).all()
|
|
265
|
+
assert out['agent']['counts'].magnitude.dtype == np.int64
|
|
266
|
+
assert out['agent']['genome'] == b'ATCG'
|
|
267
|
+
assert out['agent']['tags'] == {'a', 'b'}
|
|
268
|
+
assert out['agent']['history'][1]['rate'].magnitude == 0.6
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_plain_dict_passes_through():
|
|
272
|
+
"""Dicts without tag keys must round-trip as plain dicts."""
|
|
273
|
+
payload = {'just': 'a', 'plain': {'nested': [1, 2]}}
|
|
274
|
+
assert loads(dumps(payload)) == payload
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_v2ecoli_format_compat():
|
|
278
|
+
"""Saved files using v2ecoli's NumpyJSONEncoder must still load.
|
|
279
|
+
|
|
280
|
+
v2ecoli emits ``__pint__`` for scalars and ``__pint_array__`` for array-
|
|
281
|
+
valued Quantities. This codec accepts both so saved state files written
|
|
282
|
+
before this module existed remain readable.
|
|
283
|
+
"""
|
|
284
|
+
import numpy as np
|
|
285
|
+
import pint
|
|
286
|
+
legacy_blob = (
|
|
287
|
+
'{"mass": {"__pint__": true, "magnitude": 1.5, "units": "femtogram"},'
|
|
288
|
+
' "rates": {"__pint_array__": true, "magnitude": [0.1, 0.2], "units": "1/second"}}'
|
|
289
|
+
)
|
|
290
|
+
out = loads(legacy_blob)
|
|
291
|
+
assert isinstance(out['mass'], pint.Quantity)
|
|
292
|
+
assert out['mass'].magnitude == 1.5
|
|
293
|
+
assert isinstance(out['rates'], pint.Quantity)
|
|
294
|
+
assert isinstance(out['rates'].magnitude, np.ndarray)
|
|
295
|
+
assert (out['rates'].magnitude == np.array([0.1, 0.2])).all()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == '__main__':
|
|
299
|
+
test_pint_scalar_round_trip()
|
|
300
|
+
test_pint_array_round_trip()
|
|
301
|
+
test_pint_int_magnitude_preserved()
|
|
302
|
+
test_numpy_ndarray_round_trip()
|
|
303
|
+
test_numpy_scalar_round_trip()
|
|
304
|
+
test_set_round_trip()
|
|
305
|
+
test_bytes_round_trip()
|
|
306
|
+
test_tuple_becomes_list()
|
|
307
|
+
test_nested_state_tree()
|
|
308
|
+
test_plain_dict_passes_through()
|
|
309
|
+
test_v2ecoli_format_compat()
|
|
310
|
+
print('json_codec: all tests passed')
|
|
@@ -216,6 +216,41 @@ def _deep_merge_into(base, overlay):
|
|
|
216
216
|
return base
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
def _path_copy_merge(base, overlay):
|
|
220
|
+
"""Persistent-data-structure-style merge.
|
|
221
|
+
|
|
222
|
+
Returns a NEW dict whose top-level is a shallow copy of ``base``,
|
|
223
|
+
with keys present in ``overlay`` recursively replaced. Subtrees of
|
|
224
|
+
``base`` not touched by ``overlay`` are shared by reference, not
|
|
225
|
+
copied. Mutation-free w.r.t. ``base`` for the parts ``overlay``
|
|
226
|
+
doesn't reach.
|
|
227
|
+
|
|
228
|
+
Used by ``_handle_divide_sentinel`` so per-daughter overrides
|
|
229
|
+
(e.g. ``{division: {config: {agent_id: '00'}}}``) don't poison the
|
|
230
|
+
shared base produced by the type-driven divide walk (where
|
|
231
|
+
``divide(Link)`` deliberately shares ``config`` by reference
|
|
232
|
+
between daughters so unmodified parameters/sim_data refs aren't
|
|
233
|
+
deep-copied).
|
|
234
|
+
|
|
235
|
+
Allocation cost is proportional to the *depth and width of
|
|
236
|
+
``overlay``*, not the size of ``base``. For an override of depth
|
|
237
|
+
D touching K keys per level, this allocates O(D × K) dicts;
|
|
238
|
+
everything else (numpy arrays, sim_data references, the other
|
|
239
|
+
sibling configs) stays shared.
|
|
240
|
+
"""
|
|
241
|
+
if not isinstance(overlay, dict):
|
|
242
|
+
return overlay
|
|
243
|
+
if not isinstance(base, dict):
|
|
244
|
+
return overlay
|
|
245
|
+
result = dict(base)
|
|
246
|
+
for key, overlay_value in overlay.items():
|
|
247
|
+
if key in base and isinstance(base[key], dict) and isinstance(overlay_value, dict):
|
|
248
|
+
result[key] = _path_copy_merge(base[key], overlay_value)
|
|
249
|
+
else:
|
|
250
|
+
result[key] = overlay_value
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
|
|
219
254
|
def _handle_divide_sentinel(value_schema, state, update, path):
|
|
220
255
|
"""Process a `_divide` sentinel from a Map / dict update.
|
|
221
256
|
|
|
@@ -352,7 +387,12 @@ def _handle_divide_sentinel(value_schema, state, update, path):
|
|
|
352
387
|
daughters_state = {}
|
|
353
388
|
for i, (key, override) in enumerate(daughter_items):
|
|
354
389
|
baseline = baselines[i]
|
|
355
|
-
|
|
390
|
+
# ``_path_copy_merge`` (not the in-place ``_deep_merge_into``)
|
|
391
|
+
# because divide(Link) shares config dicts by reference between
|
|
392
|
+
# both baseline daughters — mutating one daughter's override
|
|
393
|
+
# would silently corrupt the other. Path-copy only allocates
|
|
394
|
+
# along the override spine; everything else stays shared.
|
|
395
|
+
daughter = (_path_copy_merge(baseline, override)
|
|
356
396
|
if override else baseline)
|
|
357
397
|
state[key] = daughter
|
|
358
398
|
daughter_keys.append(key)
|
|
@@ -658,12 +658,28 @@ def realize_link(core, schema: Link, encode, path=()):
|
|
|
658
658
|
else:
|
|
659
659
|
config = encode_config
|
|
660
660
|
|
|
661
|
-
#
|
|
662
|
-
#
|
|
663
|
-
#
|
|
661
|
+
# Pick constructor shape by inspecting the signature, instead
|
|
662
|
+
# of catching TypeError from edge_class(config, core) (which
|
|
663
|
+
# would also swallow real TypeErrors from inside __init__ and
|
|
664
|
+
# mask the underlying bug). Standard bigraph processes accept
|
|
665
|
+
# ``(config, core)``; vivarium-style processes take ``(config)``.
|
|
666
|
+
import inspect
|
|
664
667
|
try:
|
|
668
|
+
sig = inspect.signature(edge_class)
|
|
669
|
+
accepts_core = (
|
|
670
|
+
'core' in sig.parameters
|
|
671
|
+
or any(p.kind == inspect.Parameter.VAR_KEYWORD
|
|
672
|
+
for p in sig.parameters.values())
|
|
673
|
+
or len([p for p in sig.parameters.values()
|
|
674
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY,
|
|
675
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
676
|
+
inspect.Parameter.VAR_POSITIONAL)]) >= 2
|
|
677
|
+
)
|
|
678
|
+
except (ValueError, TypeError):
|
|
679
|
+
accepts_core = True # default to standard bigraph signature
|
|
680
|
+
if accepts_core:
|
|
665
681
|
edge_instance = edge_class(config, core)
|
|
666
|
-
|
|
682
|
+
else:
|
|
667
683
|
edge_instance = edge_class(config)
|
|
668
684
|
# Ensure all instances have core for config_schema resolution
|
|
669
685
|
# (needed by serialize). Vivarium-style processes don't accept
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bigraph-schema
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: A serializable type schema for compositional systems biology
|
|
5
5
|
Author: Eran Agmon, Ryan Spangler
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -21,6 +21,7 @@ Dynamic: license-file
|
|
|
21
21
|
|
|
22
22
|
[](https://pypi.org/project/bigraph-schema/)
|
|
23
23
|
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
|
|
24
|
+
[](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html)
|
|
24
25
|
|
|
25
26
|
`bigraph-schema` provides a serializable type schema for compositional and multiscale modeling.
|
|
26
27
|
It defines a compact, extensible language for describing hierarchical data structures — the foundation of the Vivarium 2.0 simulation framework.
|
|
@@ -47,6 +48,8 @@ The following resources provide guided introductions and examples:
|
|
|
47
48
|
|
|
48
49
|
- [Bigraph Schema Basics Tutorial](https://vivarium-collective.github.io/bigraph-viz/notebooks/basics.html) – Explains how to compose and visualize schema graphs using the `bigraph-schema` syntax.
|
|
49
50
|
|
|
51
|
+
- [Units Demo](https://vivarium-collective.github.io/bigraph-schema/notebooks/units.html) – Demonstrates `Quantity`, `Number._units`, and wire-level unit conversion.
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
---
|
|
52
55
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|