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.
Files changed (49) hide show
  1. {bigraph_schema-1.3.2/bigraph_schema.egg-info → bigraph_schema-1.3.4}/PKG-INFO +4 -1
  2. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/README.md +3 -0
  3. bigraph_schema-1.3.4/bigraph_schema/json_codec.py +310 -0
  4. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/apply.py +41 -1
  5. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/realize.py +20 -4
  6. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4/bigraph_schema.egg-info}/PKG-INFO +4 -1
  7. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/SOURCES.txt +1 -0
  8. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/pyproject.toml +1 -1
  9. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/AUTHORS.md +0 -0
  10. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/LICENSE +0 -0
  11. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/__init__.py +0 -0
  12. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/assembly.py +0 -0
  13. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/calculi.py +0 -0
  14. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/core.py +0 -0
  15. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/edge.py +0 -0
  16. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/__init__.py +0 -0
  17. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/bundle.py +0 -0
  18. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/check.py +0 -0
  19. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/coerce.py +0 -0
  20. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/default.py +0 -0
  21. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/derive.py +0 -0
  22. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/diff.py +0 -0
  23. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/divide.py +0 -0
  24. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/events.py +0 -0
  25. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/generalize.py +0 -0
  26. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/handle_parameters.py +0 -0
  27. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/infer.py +0 -0
  28. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/is_empty.py +0 -0
  29. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/jump.py +0 -0
  30. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/merge.py +0 -0
  31. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/patch.py +0 -0
  32. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/reconcile.py +0 -0
  33. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/resolve.py +0 -0
  34. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/select.py +0 -0
  35. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/serialize.py +0 -0
  36. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/transform.py +0 -0
  37. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/validate.py +0 -0
  38. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/methods/walk.py +0 -0
  39. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/package/__init__.py +0 -0
  40. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/package/discover.py +0 -0
  41. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/parse.py +0 -0
  42. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/protocols.py +0 -0
  43. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/schema.py +0 -0
  44. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema/units.py +0 -0
  45. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/dependency_links.txt +0 -0
  46. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/requires.txt +0 -0
  47. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/bigraph_schema.egg-info/top_level.txt +0 -0
  48. {bigraph_schema-1.3.2 → bigraph_schema-1.3.4}/setup.cfg +0 -0
  49. {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.2
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
  [![PyPI version](https://img.shields.io/pypi/v/bigraph-schema.svg)](https://pypi.org/project/bigraph-schema/)
23
23
  [![Tutorial](https://img.shields.io/badge/GitHub%20Pages-Tutorial-brightgreen)](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
24
+ [![Units Demo](https://img.shields.io/badge/GitHub%20Pages-Units%20demo-blue)](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
  [![PyPI version](https://img.shields.io/pypi/v/bigraph-schema.svg)](https://pypi.org/project/bigraph-schema/)
4
4
  [![Tutorial](https://img.shields.io/badge/GitHub%20Pages-Tutorial-brightgreen)](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
5
+ [![Units Demo](https://img.shields.io/badge/GitHub%20Pages-Units%20demo-blue)](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
- daughter = (_deep_merge_into(baseline, override)
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
- # Try (config, core) first (standard bigraph signature), then
662
- # fall back to (config) only (vivarium-style processes whose
663
- # __init__ doesn't accept core).
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
- except TypeError:
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.2
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
  [![PyPI version](https://img.shields.io/pypi/v/bigraph-schema.svg)](https://pypi.org/project/bigraph-schema/)
23
23
  [![Tutorial](https://img.shields.io/badge/GitHub%20Pages-Tutorial-brightgreen)](https://vivarium-collective.github.io/bigraph-schema/notebooks/demo.html)
24
+ [![Units Demo](https://img.shields.io/badge/GitHub%20Pages-Units%20demo-blue)](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
 
@@ -8,6 +8,7 @@ bigraph_schema/assembly.py
8
8
  bigraph_schema/calculi.py
9
9
  bigraph_schema/core.py
10
10
  bigraph_schema/edge.py
11
+ bigraph_schema/json_codec.py
11
12
  bigraph_schema/parse.py
12
13
  bigraph_schema/protocols.py
13
14
  bigraph_schema/schema.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bigraph-schema"
7
- version = "1.3.2"
7
+ version = "1.3.4"
8
8
  description = "A serializable type schema for compositional systems biology"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes