bigraph-schema 1.3.4__tar.gz → 1.4.2__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.4/bigraph_schema.egg-info → bigraph_schema-1.4.2}/PKG-INFO +1 -2
  2. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/__init__.py +0 -10
  3. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/core.py +43 -137
  4. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/edge.py +21 -0
  5. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/bundle.py +94 -2
  6. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/merge.py +0 -11
  7. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/resolve.py +9 -3
  8. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/parse.py +120 -2
  9. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2/bigraph_schema.egg-info}/PKG-INFO +1 -2
  10. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema.egg-info/requires.txt +0 -1
  11. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/pyproject.toml +1 -2
  12. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/AUTHORS.md +0 -0
  13. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/LICENSE +0 -0
  14. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/README.md +0 -0
  15. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/assembly.py +0 -0
  16. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/calculi.py +0 -0
  17. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/json_codec.py +0 -0
  18. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/__init__.py +0 -0
  19. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/apply.py +0 -0
  20. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/check.py +0 -0
  21. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/coerce.py +0 -0
  22. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/default.py +0 -0
  23. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/derive.py +0 -0
  24. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/diff.py +0 -0
  25. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/divide.py +0 -0
  26. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/events.py +0 -0
  27. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/generalize.py +0 -0
  28. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/handle_parameters.py +0 -0
  29. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/infer.py +0 -0
  30. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/is_empty.py +0 -0
  31. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/jump.py +0 -0
  32. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/patch.py +0 -0
  33. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/realize.py +0 -0
  34. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/reconcile.py +0 -0
  35. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/select.py +0 -0
  36. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/serialize.py +0 -0
  37. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/transform.py +0 -0
  38. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/validate.py +0 -0
  39. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/methods/walk.py +0 -0
  40. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/package/__init__.py +0 -0
  41. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/package/discover.py +0 -0
  42. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/protocols.py +0 -0
  43. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/schema.py +0 -0
  44. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema/units.py +0 -0
  45. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema.egg-info/SOURCES.txt +0 -0
  46. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema.egg-info/dependency_links.txt +0 -0
  47. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/bigraph_schema.egg-info/top_level.txt +0 -0
  48. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/setup.cfg +0 -0
  49. {bigraph_schema-1.3.4 → bigraph_schema-1.4.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bigraph-schema
3
- Version: 1.3.4
3
+ Version: 1.4.2
4
4
  Summary: A serializable type schema for compositional systems biology
5
5
  Author: Eran Agmon, Ryan Spangler
6
6
  Requires-Python: >=3.9
@@ -9,7 +9,6 @@ License-File: LICENSE
9
9
  License-File: AUTHORS.md
10
10
  Requires-Dist: fire
11
11
  Requires-Dist: numpy
12
- Requires-Dist: orjson
13
12
  Requires-Dist: pandas
14
13
  Requires-Dist: parsimonious
15
14
  Requires-Dist: pint
@@ -1,13 +1,3 @@
1
- # from bigraph_schema.registry import (
2
- # deep_merge, validate_merge, default, Registry, hierarchy_depth, is_schema_key, establish_path,
3
- # strip_schema_keys, type_parameter_key, non_schema_keys, set_path, transform_path)
4
-
5
- # from bigraph_schema.utilities import get_path, visit_method
6
-
7
- # from bigraph_schema.type_system import TypeSystem
8
- # from bigraph_schema.type_functions import FUNCTION_TYPE, METHOD_TYPE, type_schema_keys, resolve_path
9
-
10
-
11
1
  from bigraph_schema.edge import Edge
12
2
  from bigraph_schema.protocols import local_lookup_module
13
3
 
@@ -19,23 +19,12 @@ validation, and data transformation.
19
19
  expressions into structured schema nodes (`Union`, `Tuple`, `Array`, `Link`, etc.).
20
20
  """
21
21
 
22
- import sys
23
22
  import copy
24
- import typing
25
- import inspect
26
23
  import collections
27
24
 
28
25
  from pprint import pformat as pf
29
26
 
30
- import numpy as np
31
- from numpy import dtype
32
- import numpy.lib.format as nf
33
-
34
- import logging
35
-
36
- from plum import dispatch
37
- from parsimonious.nodes import NodeVisitor
38
- from dataclasses import dataclass, is_dataclass, replace
27
+ from dataclasses import dataclass, replace
39
28
  import importlib.metadata
40
29
 
41
30
  from bigraph_schema.schema import (
@@ -79,11 +68,10 @@ from bigraph_schema.schema import (
79
68
  LocalProtocol
80
69
  )
81
70
 
82
- from bigraph_schema.parse import visit_expression
71
+ from bigraph_schema.parse import visit_expression, CoreVisitor
83
72
  from bigraph_schema.edge import Edge
84
73
  from bigraph_schema.methods import (
85
74
  reify_schema,
86
- handle_parameters,
87
75
  infer,
88
76
  default,
89
77
  resolve,
@@ -126,122 +114,6 @@ def append_link_path(schema, link_path):
126
114
  return schema
127
115
 
128
116
 
129
- class CoreVisitor(NodeVisitor):
130
- """Visitor that converts parsed bigraph expressions into schema node structures.
131
-
132
- Operates within a `Core` context, mapping grammar constructs
133
- (unions, merges, type parameters, and defaults) into dataclass-based nodes.
134
- Handles normalization of nested expressions (e.g. `tuple[int,float]`,
135
- `link[a:int|b:string]`, `(x:y|z:w)`) into instances of `Union`, `Tuple`,
136
- or structured dicts.
137
- """
138
-
139
- def __init__(self, operation):
140
- """Initialize with the active `Core`."""
141
- self.operation = operation
142
-
143
- def visit_expression(self, node, visit):
144
- """Top-level entry; returns first child."""
145
- return visit[0]
146
-
147
- def visit_union(self, node, visit):
148
- """Parse `a~b~c` into a `Union(_options=[a,b,c])`."""
149
- head = [visit[0]]
150
- tail = [tree['visit'][1] for tree in visit[1]['visit']]
151
- return Union(_options=head + tail)
152
-
153
- def visit_merge(self, node, visit):
154
- """Parse `a|b|c`; dicts merge to one mapping, others form a `Tuple`."""
155
- head = [visit[0]]
156
- tail = [tree['visit'][1] for tree in visit[1]['visit']]
157
- nodes = head + tail
158
- if all(isinstance(tree, dict) for tree in nodes):
159
- merged = {}
160
- for tree in nodes:
161
- merged.update(tree)
162
- return merged
163
- else:
164
- try:
165
- values = tuple([int(x) for x in nodes])
166
-
167
- return values
168
- except Exception as e:
169
- return Tuple(_values=nodes)
170
-
171
- def visit_tree(self, node, visit):
172
- """Delegate directly to nested element."""
173
- return visit[0]
174
-
175
- def visit_bigraph(self, node, visit):
176
- """Alias for tree; allows recursion within nested bigraphs."""
177
- return visit[0]
178
-
179
- def visit_group(self, node, visit):
180
- """Handle grouped subexpression `( ... )`; return tuple or dict."""
181
- group_value = visit[1]
182
- return group_value if isinstance(group_value, (list, tuple, dict, Tuple)) else (group_value,)
183
-
184
- def visit_nest(self, node, visit):
185
- """Handle `key:subtype` pairs (used in trees/maps).
186
-
187
- The left side of the colon is a dict key — always a plain string,
188
- not a resolved type. We use the raw text from the parse node
189
- rather than the visited (type-resolved) value, since key names
190
- like ``process`` may collide with registered type names.
191
- """
192
- key = node.children[0].text
193
- return {key: visit[2]}
194
-
195
- def visit_type_name(self, node, visit):
196
- """Resolve base type, parameters, and defaults into schema nodes."""
197
- schema = visit[0]
198
-
199
- # Parse parameter list
200
- type_parameters = [
201
- parameter
202
- for parameter in visit[1]['visit']]
203
-
204
- if type_parameters:
205
- schema = handle_parameters(self.operation, schema, type_parameters[0])
206
-
207
- # Parse default value `{...}`
208
- default_visit = visit[2]['visit']
209
- if default_visit:
210
- default = default_visit[0]
211
- if isinstance(schema, Node):
212
- schema._default = default
213
- elif isinstance(schema, dict):
214
- schema['_default'] = default
215
-
216
- return schema
217
-
218
- def visit_parameter_list(self, node, visit):
219
- """Return ordered list of parameters `[A,B,C]`."""
220
- first = [visit[1]]
221
- rest = [inner['visit'][1] for inner in visit[2]['visit']]
222
- return first + rest
223
-
224
- def visit_default_block(self, node, visit):
225
- """Extract contents of `{...}` blocks."""
226
- return visit[1]
227
-
228
- def visit_default(self, node, visit):
229
- """Return text inside default braces as string."""
230
- return node.text
231
-
232
- def visit_symbol(self, node, visit):
233
- """Resolve bare symbol names via the operation registry or parse visitor."""
234
- return self.operation.access(node.text)
235
-
236
- def visit_nothing(self, node, visit):
237
- """Handle empty productions (e.g., trailing commas)."""
238
- return None
239
-
240
- def generic_visit(self, node, visit):
241
- """Fallback: return raw parse node and visited children."""
242
- return {'node': node, 'visit': visit}
243
-
244
-
245
117
  class Core:
246
118
  """Bigraph-schema operation: registry, parsing, normalization, and ops.
247
119
 
@@ -1619,24 +1491,58 @@ class Core:
1619
1491
 
1620
1492
  _cached_base_core = None
1621
1493
 
1494
+
1495
+ def _isolated_copy(base):
1496
+ """Return a Core that shares nothing mutable with ``base``.
1497
+
1498
+ ``copy.copy`` alone is a *shallow* copy: the new object would share
1499
+ the very same ``registry``, ``link_registry``, ``method_registry`` and
1500
+ cache dicts as ``base``, so registering a type on one leaks into all
1501
+ others. Here we shallow-copy the object (preserving cheaply-derived
1502
+ state like ``packages_distributions``) and then give the copy its own
1503
+ registry containers plus fresh, empty per-instance caches.
1504
+
1505
+ The copy depth is deliberately container-level, not deep: registry
1506
+ *entries* are immutable schema nodes/dicts, so duplicating the dicts
1507
+ that hold them is enough to prevent key-level leakage between cores
1508
+ without the cost of deep-copying every schema.
1509
+ """
1510
+ import copy
1511
+ clone = copy.copy(base)
1512
+ clone.registry = dict(base.registry)
1513
+ clone.link_registry = dict(base.link_registry)
1514
+ clone.method_registry = dict(base.method_registry)
1515
+ # Fresh, empty per-instance caches so warm entries (and the id-keyed
1516
+ # witnesses they hold) never bleed between independently-allocated cores.
1517
+ clone._access_cache = collections.OrderedDict()
1518
+ clone._access_string_cache = {}
1519
+ clone._link_cache = {}
1520
+ clone._resolve_cache = collections.OrderedDict()
1521
+ clone._promote_cache = collections.OrderedDict()
1522
+ # The parse visitor closes over its owning core; rebind it to the clone.
1523
+ clone.parse_visitor = CoreVisitor(clone)
1524
+ return clone
1525
+
1526
+
1622
1527
  def allocate_core(top=None):
1623
1528
  """Allocate a new Core with all discovered packages.
1624
1529
 
1625
- The base core (without ``top``) is cached after the first call
1626
- to avoid repeated expensive package discovery. Each call returns
1627
- a fresh copy so callers can register additional types independently.
1530
+ The base core (without ``top``) is built once and cached to avoid
1531
+ repeated expensive package discovery. Each call returns an *isolated*
1532
+ copy: it shares no mutable state with any other allocated core, so
1533
+ types/links/methods registered on one core never leak into another,
1534
+ while the base types from discovery are present on every copy. See
1535
+ :func:`_isolated_copy` for the (container-level) copy semantics.
1628
1536
  """
1629
1537
  global _cached_base_core
1630
1538
  if top is None and _cached_base_core is not None:
1631
- import copy
1632
- return copy.copy(_cached_base_core)
1539
+ return _isolated_copy(_cached_base_core)
1633
1540
 
1634
1541
  core = Core(BASE_TYPES)
1635
1542
  core = discover_packages(core, top)
1636
1543
 
1637
1544
  if top is None:
1638
1545
  _cached_base_core = core
1639
- import copy
1640
- return copy.copy(core)
1546
+ return _isolated_copy(core)
1641
1547
 
1642
1548
  return core
@@ -6,6 +6,9 @@ Edge
6
6
  Base class for all edges in the bigraph schema.
7
7
  """
8
8
 
9
+ import inspect
10
+
11
+
9
12
  def default_wires(schema):
10
13
  """
11
14
  Create default wiring for a schema by connecting each port to a store of the same name.
@@ -28,6 +31,12 @@ class Edge:
28
31
 
29
32
  config_schema = {}
30
33
 
34
+ # Canonical formal description of this edge — a string (markdown / LaTeX) that
35
+ # states what the process/step computes (governing equations, assumptions).
36
+ # Override on subclasses. Tooling reads it via `describe()`, which falls back
37
+ # to the class docstring when this is left empty.
38
+ description = ''
39
+
31
40
  def __init__(self, config=None, core=None):
32
41
  """
33
42
  Initialize the edge with a config and simulation core.
@@ -135,4 +144,16 @@ class Edge:
135
144
  'outputs': self.outputs()
136
145
  }
137
146
 
147
+ def describe(self):
148
+ """
149
+ Return this edge's formal description.
150
+
151
+ Returns the ``description`` class attribute — the canonical formal
152
+ (mathematical) description of what this process/step computes. Falls
153
+ back to the class docstring when ``description`` is unset, so existing
154
+ edges still surface something. Subclasses should set ``description`` to
155
+ a string (markdown / LaTeX) for a precise, standardized description.
156
+ """
157
+ return self.description or inspect.getdoc(type(self)) or ''
158
+
138
159
 
@@ -74,6 +74,100 @@ REF_KEY = '$bundle_ref'
74
74
  MIN_ARRAY_BYTES = 10_000 # ~10 KB threshold for externalizing
75
75
 
76
76
 
77
+ # ---------------------------------------------------------------------------
78
+ # Parquet array I/O
79
+ # ---------------------------------------------------------------------------
80
+ # The bundle WRITE side lives here because bigraph-schema owns the bundle
81
+ # format (``BundleContext`` + the dispatched ``bundle`` method). The matching
82
+ # READ side, ``_load_array_parquet`` / ``resolve_refs`` / ``load_bundle``, lives
83
+ # in ``process_bigraph.bundle`` (the layer that orchestrates loading). The two
84
+ # are a format pair — keep their column/metadata layout in sync. pyarrow is
85
+ # imported lazily so bigraph-schema does not gain a hard pyarrow dependency
86
+ # (bundling is opt-in).
87
+
88
+ def _save_array_parquet(arr: "np.ndarray", filepath: str) -> None:
89
+ """Save a numpy array to a Parquet file.
90
+
91
+ Strategy:
92
+ - 1D/2D numeric arrays: store as columnar Parquet (one column per
93
+ array column, or a single column for 1D).
94
+ - Higher-dimensional or structured arrays: store as a binary blob
95
+ with shape/dtype metadata.
96
+
97
+ Counterpart reader: ``process_bigraph.bundle._load_array_parquet``.
98
+ """
99
+ import pyarrow as pa
100
+ import pyarrow.parquet as pq
101
+
102
+ if arr.dtype.names:
103
+ # Structured array — each field becomes one or more Parquet columns.
104
+ # Sub-array fields (e.g. shape (N, 2)) are flattened into separate
105
+ # columns: field.0, field.1, etc.
106
+ columns = {}
107
+ subarray_meta = {} # field_name -> {'shape': [...], 'dtype': '...'}
108
+
109
+ for name in arr.dtype.names:
110
+ field_data = arr[name]
111
+ if field_data.dtype.kind == 'U':
112
+ columns[name] = pa.array(field_data.tolist(), type=pa.string())
113
+ elif field_data.ndim > 1:
114
+ # Sub-array field: flatten into N columns
115
+ sub_shape = field_data.shape[1:]
116
+ sub_count = int(np.prod(sub_shape))
117
+ flat = field_data.reshape(len(field_data), sub_count)
118
+ for i in range(sub_count):
119
+ columns[f'{name}.{i}'] = pa.array(flat[:, i])
120
+ subarray_meta[name] = {
121
+ 'shape': list(sub_shape),
122
+ 'dtype': str(field_data.dtype.base),
123
+ 'count': sub_count,
124
+ }
125
+ else:
126
+ columns[name] = pa.array(field_data)
127
+
128
+ table = pa.table(columns)
129
+
130
+ # Store the original dtype string and sub-array info in file metadata
131
+ file_meta = {
132
+ 'dtype': str(arr.dtype),
133
+ 'subarray_fields': subarray_meta,
134
+ }
135
+ schema_meta = table.schema.metadata or {}
136
+ schema_meta[b'bundle_structured'] = json.dumps(file_meta).encode()
137
+ table = table.replace_schema_metadata(schema_meta)
138
+
139
+ pq.write_table(table, filepath, compression='zstd')
140
+
141
+ elif arr.ndim == 1 or (arr.ndim == 2 and arr.shape[1] <= 500):
142
+ # Dense 1D or moderate 2D array — columnar storage.
143
+ if arr.ndim == 1:
144
+ table = pa.table({'c0': pa.array(arr)})
145
+ else:
146
+ columns = {
147
+ f'c{i}': pa.array(arr[:, i])
148
+ for i in range(arr.shape[1])
149
+ }
150
+ table = pa.table(columns)
151
+
152
+ # Store original dtype in metadata
153
+ meta = {b'dtype': str(arr.dtype).encode(),
154
+ b'shape': json.dumps(list(arr.shape)).encode()}
155
+ table = table.replace_schema_metadata(meta)
156
+ pq.write_table(table, filepath, compression='zstd')
157
+
158
+ else:
159
+ # Wide 2D (>500 cols) or 3D+ — binary blob with compression.
160
+ # Storing as raw bytes in a single Parquet row with zstd
161
+ # compression. This is still very efficient: a 50MB int8 array
162
+ # compresses to ~15MB.
163
+ table = pa.table({
164
+ 'data': [arr.tobytes()],
165
+ 'shape': [json.dumps(list(arr.shape))],
166
+ 'dtype': [str(arr.dtype)],
167
+ })
168
+ pq.write_table(table, filepath, compression='zstd')
169
+
170
+
77
171
  @dataclass
78
172
  class BundleContext:
79
173
  """Accumulates array files during a bundle operation.
@@ -93,8 +187,6 @@ class BundleContext:
93
187
  If an array with identical content has already been saved, the
94
188
  existing file is reused (content-addressed deduplication).
95
189
  """
96
- from process_bigraph.bundle import _save_array_parquet
97
-
98
190
  os.makedirs(self.arrays_dir, exist_ok=True)
99
191
 
100
192
  content_id = hashlib.sha256(
@@ -358,17 +358,6 @@ def _value_is_empty(value):
358
358
  return not value
359
359
 
360
360
 
361
- def tuplify_dict(d):
362
- if isinstance(d, dict):
363
- tulip = []
364
- for key, value in d.items():
365
- tulip.append((
366
- key,
367
- tuplify_dict(value)))
368
- else:
369
- return d
370
-
371
-
372
361
  @dispatch
373
362
  def merge(schema: dict, current, update, path=()):
374
363
  if path:
@@ -189,7 +189,15 @@ def resolve(current: Wrap, update: Wrap, path=None):
189
189
  return schema
190
190
  else:
191
191
  # TODO: resolve wrappings somehow?
192
- raise Exception(f'cannot resolve two different wrappings {current} {update}')
192
+ import traceback as _tb
193
+ _frames = ''.join(_tb.format_stack(limit=12)[:-1])
194
+ raise Exception(
195
+ f'cannot resolve two different wrappings at path={path}:\n'
196
+ f' current ({type(current).__name__}, _default={getattr(current, "_default", "?")}, '
197
+ f'_value type={type(getattr(current, "_value", None)).__name__})\n'
198
+ f' update ({type(update).__name__}, _default={getattr(update, "_default", "?")}, '
199
+ f'_value type={type(getattr(update, "_value", None)).__name__})\n'
200
+ f' call stack:\n{_frames}')
193
201
 
194
202
 
195
203
  @dispatch
@@ -661,7 +669,6 @@ def resolve(current: Array, update: Node, path=None):
661
669
  # @dispatch
662
670
  # def resolve(current: Node, update: Array, path=None):
663
671
  # if path:
664
- # import ipdb; ipdb.set_trace()
665
672
 
666
673
  # return resolve_array_path(update, current, path=path)
667
674
 
@@ -840,7 +847,6 @@ def resolve(current: String, update: Node, path=None):
840
847
 
841
848
  # @dispatch
842
849
  # def resolve(current: Node, update: String):
843
- # # import ipdb; ipdb.set_trace()
844
850
  # if update._default:
845
851
  # current = replace(current, **{'_default': update._default})
846
852
  # return current
@@ -9,6 +9,8 @@ such as parameterized types, nested trees, merges (`|`), and unions (`~`).
9
9
 
10
10
  from parsimonious.grammar import Grammar
11
11
  from parsimonious.nodes import NodeVisitor
12
+ from bigraph_schema.schema import Union, Tuple, Node
13
+ from bigraph_schema.methods import handle_parameters
12
14
 
13
15
  # --- Example Expressions -----------------------------------------------------
14
16
  parameter_examples = {
@@ -122,6 +124,122 @@ class ParameterVisitor(NodeVisitor):
122
124
  return {'node': node, 'visit': visit}
123
125
 
124
126
  # --- API ---------------------------------------------------------------------
127
+ class CoreVisitor(NodeVisitor):
128
+ """Visitor that converts parsed bigraph expressions into schema node structures.
129
+
130
+ Operates within a `Core` context, mapping grammar constructs
131
+ (unions, merges, type parameters, and defaults) into dataclass-based nodes.
132
+ Handles normalization of nested expressions (e.g. `tuple[int,float]`,
133
+ `link[a:int|b:string]`, `(x:y|z:w)`) into instances of `Union`, `Tuple`,
134
+ or structured dicts.
135
+ """
136
+
137
+ def __init__(self, operation):
138
+ """Initialize with the active `Core`."""
139
+ self.operation = operation
140
+
141
+ def visit_expression(self, node, visit):
142
+ """Top-level entry; returns first child."""
143
+ return visit[0]
144
+
145
+ def visit_union(self, node, visit):
146
+ """Parse `a~b~c` into a `Union(_options=[a,b,c])`."""
147
+ head = [visit[0]]
148
+ tail = [tree['visit'][1] for tree in visit[1]['visit']]
149
+ return Union(_options=head + tail)
150
+
151
+ def visit_merge(self, node, visit):
152
+ """Parse `a|b|c`; dicts merge to one mapping, others form a `Tuple`."""
153
+ head = [visit[0]]
154
+ tail = [tree['visit'][1] for tree in visit[1]['visit']]
155
+ nodes = head + tail
156
+ if all(isinstance(tree, dict) for tree in nodes):
157
+ merged = {}
158
+ for tree in nodes:
159
+ merged.update(tree)
160
+ return merged
161
+ else:
162
+ try:
163
+ values = tuple([int(x) for x in nodes])
164
+
165
+ return values
166
+ except Exception as e:
167
+ return Tuple(_values=nodes)
168
+
169
+ def visit_tree(self, node, visit):
170
+ """Delegate directly to nested element."""
171
+ return visit[0]
172
+
173
+ def visit_bigraph(self, node, visit):
174
+ """Alias for tree; allows recursion within nested bigraphs."""
175
+ return visit[0]
176
+
177
+ def visit_group(self, node, visit):
178
+ """Handle grouped subexpression `( ... )`; return tuple or dict."""
179
+ group_value = visit[1]
180
+ return group_value if isinstance(group_value, (list, tuple, dict, Tuple)) else (group_value,)
181
+
182
+ def visit_nest(self, node, visit):
183
+ """Handle `key:subtype` pairs (used in trees/maps).
184
+
185
+ The left side of the colon is a dict key — always a plain string,
186
+ not a resolved type. We use the raw text from the parse node
187
+ rather than the visited (type-resolved) value, since key names
188
+ like ``process`` may collide with registered type names.
189
+ """
190
+ key = node.children[0].text
191
+ return {key: visit[2]}
192
+
193
+ def visit_type_name(self, node, visit):
194
+ """Resolve base type, parameters, and defaults into schema nodes."""
195
+ schema = visit[0]
196
+
197
+ # Parse parameter list
198
+ type_parameters = [
199
+ parameter
200
+ for parameter in visit[1]['visit']]
201
+
202
+ if type_parameters:
203
+ schema = handle_parameters(self.operation, schema, type_parameters[0])
204
+
205
+ # Parse default value `{...}`
206
+ default_visit = visit[2]['visit']
207
+ if default_visit:
208
+ default = default_visit[0]
209
+ if isinstance(schema, Node):
210
+ schema._default = default
211
+ elif isinstance(schema, dict):
212
+ schema['_default'] = default
213
+
214
+ return schema
215
+
216
+ def visit_parameter_list(self, node, visit):
217
+ """Return ordered list of parameters `[A,B,C]`."""
218
+ first = [visit[1]]
219
+ rest = [inner['visit'][1] for inner in visit[2]['visit']]
220
+ return first + rest
221
+
222
+ def visit_default_block(self, node, visit):
223
+ """Extract contents of `{...}` blocks."""
224
+ return visit[1]
225
+
226
+ def visit_default(self, node, visit):
227
+ """Return text inside default braces as string."""
228
+ return node.text
229
+
230
+ def visit_symbol(self, node, visit):
231
+ """Resolve bare symbol names via the operation registry or parse visitor."""
232
+ return self.operation.access(node.text)
233
+
234
+ def visit_nothing(self, node, visit):
235
+ """Handle empty productions (e.g., trailing commas)."""
236
+ return None
237
+
238
+ def generic_visit(self, node, visit):
239
+ """Fallback: return raw parse node and visited children."""
240
+ return {'node': node, 'visit': visit}
241
+
242
+
125
243
  def parsed_leftmost_leaf(parsed):
126
244
  if len(parsed.children) == 0:
127
245
  return parsed
@@ -170,7 +288,7 @@ def render_expression(expression):
170
288
  return str(expression) # fallback for unknown structures
171
289
 
172
290
  # --- Debugging/Testing ---------------------------------------------------------
173
- def test_parse_parameters():
291
+ def _demo_parse_parameters():
174
292
  """Test parsing and rendering for all example expressions."""
175
293
  for label, expr in parameter_examples.items():
176
294
  parsed = parse_expression(expr)
@@ -180,4 +298,4 @@ def test_parse_parameters():
180
298
  print(f' Rendered: {render_expression(parsed)}')
181
299
 
182
300
  if __name__ == '__main__':
183
- test_parse_parameters()
301
+ _demo_parse_parameters()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bigraph-schema
3
- Version: 1.3.4
3
+ Version: 1.4.2
4
4
  Summary: A serializable type schema for compositional systems biology
5
5
  Author: Eran Agmon, Ryan Spangler
6
6
  Requires-Python: >=3.9
@@ -9,7 +9,6 @@ License-File: LICENSE
9
9
  License-File: AUTHORS.md
10
10
  Requires-Dist: fire
11
11
  Requires-Dist: numpy
12
- Requires-Dist: orjson
13
12
  Requires-Dist: pandas
14
13
  Requires-Dist: parsimonious
15
14
  Requires-Dist: pint
@@ -1,6 +1,5 @@
1
1
  fire
2
2
  numpy
3
- orjson
4
3
  pandas
5
4
  parsimonious
6
5
  pint
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bigraph-schema"
7
- version = "1.3.4"
7
+ version = "1.4.2"
8
8
  description = "A serializable type schema for compositional systems biology"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -15,7 +15,6 @@ authors = [
15
15
  dependencies = [
16
16
  "fire",
17
17
  "numpy",
18
- "orjson",
19
18
  "pandas",
20
19
  "parsimonious",
21
20
  "pint",
File without changes
File without changes
File without changes
File without changes