process-bigraph 0.0.43__py3-none-any.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.
@@ -0,0 +1,473 @@
1
+ """
2
+ =============
3
+ Process Types
4
+ =============
5
+
6
+ This module contains the process methods and types for the process bigraph schema.
7
+ Additionally, it defines the `ProcessTypes` class, which extends the `TypeSystem`
8
+ class to include process types, and maintains a registry of process types, protocols,
9
+ and emitters.
10
+ """
11
+
12
+ import copy
13
+
14
+ from bigraph_schema import Registry, Edge, TypeSystem, deep_merge, visit_method, get_path
15
+
16
+ from process_bigraph.protocols import BASE_PROTOCOLS
17
+ from process_bigraph.composite import Composite
18
+ from process_bigraph.emitter import BASE_EMITTERS
19
+
20
+
21
+ # ======================
22
+ # Process Type Functions
23
+ # ======================
24
+
25
+ def apply_process(schema, current, update, top_schema, top_state, path, core):
26
+ """
27
+ Apply an update to a process instance using the core's update mechanism.
28
+
29
+ Args:
30
+ schema (dict): The schema for the current process.
31
+ current (dict): The current process state.
32
+ update (dict): The update to apply.
33
+ top_schema (dict): The top-level (composite) schema.
34
+ top_state (dict): The top-level state.
35
+ path (tuple): Path to the current process in the schema tree.
36
+ core: The type system or composition engine.
37
+
38
+ Returns:
39
+ dict: Updated process state.
40
+ """
41
+ process_schema = schema.copy()
42
+ process_schema.pop('_apply', None)
43
+
44
+ return core.apply_update(
45
+ process_schema,
46
+ current,
47
+ update,
48
+ top_schema=top_schema,
49
+ top_state=top_state,
50
+ path=path
51
+ )
52
+
53
+
54
+ def check_process(schema, state, core):
55
+ """
56
+ Check if a given state belongs to a valid process instance.
57
+
58
+ Args:
59
+ schema (dict): The expected schema (unused here).
60
+ state (dict): The process state.
61
+ core: The type system (unused here).
62
+
63
+ Returns:
64
+ bool: True if the state contains a valid Edge instance.
65
+ """
66
+ return 'instance' in state and isinstance(state['instance'], Edge)
67
+
68
+
69
+ def fold_visit(schema, state, method, values, core):
70
+ """
71
+ Wrapper for visiting a process state using a specific method.
72
+
73
+ Args:
74
+ schema (dict): The process schema.
75
+ state (dict): The process state.
76
+ method (str): The method name to invoke (e.g., 'apply', 'serialize').
77
+ values (dict): Inputs to the visitor method.
78
+ core: The type system.
79
+
80
+ Returns:
81
+ Any: Result of the visitor operation.
82
+ """
83
+ return visit_method(schema, state, method, values, core)
84
+
85
+
86
+ def divide_process(schema, state, values, core):
87
+ """
88
+ Divide a process into multiple daughter process states.
89
+
90
+ Args:
91
+ schema (dict): The schema for the process.
92
+ state (dict): The current process state.
93
+ values (dict): Division parameters including:
94
+ - 'divisions' (int): Number of daughters
95
+ - 'daughter_configs' (list[dict], optional): Config overrides
96
+
97
+ Returns:
98
+ list[dict]: A list of daughter states.
99
+ """
100
+ num_daughters = values['divisions']
101
+ daughter_configs = values.get('daughter_configs', [{} for _ in range(num_daughters)])
102
+
103
+ if 'config' not in state:
104
+ return daughter_configs
105
+
106
+ existing_config = state['config']
107
+ divisions = []
108
+
109
+ for i in range(num_daughters):
110
+ daughter_config = deep_merge(copy.deepcopy(existing_config), daughter_configs[i])
111
+
112
+ daughter_state = {
113
+ 'address': state['address'],
114
+ 'config': daughter_config,
115
+ 'inputs': copy.deepcopy(state['inputs']),
116
+ 'outputs': copy.deepcopy(state['outputs']),
117
+ }
118
+
119
+ if 'interval' in state:
120
+ daughter_state['interval'] = state['interval']
121
+
122
+ divisions.append(daughter_state)
123
+
124
+ return divisions
125
+
126
+
127
+ def serialize_process(schema, value, core):
128
+ """
129
+ Serialize a process state to a JSON-safe format.
130
+
131
+ Args:
132
+ schema (dict): The schema (unused here).
133
+ value (dict): The full process state.
134
+ core: The type system to handle sub-serialization.
135
+
136
+ Returns:
137
+ dict: Serialized process data.
138
+ """
139
+ process = value.copy()
140
+
141
+ process['config'] = core.serialize(
142
+ process['instance'].config_schema,
143
+ process['config']
144
+ )
145
+
146
+ del process['instance'] # Remove the live instance for serialization
147
+
148
+ return process
149
+
150
+
151
+ def deserialize_process(schema, encoded, core):
152
+ """
153
+ Deserialize a process from a saved (serialized) state.
154
+
155
+ Args:
156
+ schema (dict): The expected process schema.
157
+ encoded (dict): Serialized process state.
158
+ core: The type system, needed to resolve classes and configs.
159
+
160
+ Returns:
161
+ dict: Fully rehydrated process state with instance attached.
162
+ """
163
+ encoded = encoded or {}
164
+ schema = schema or {}
165
+
166
+ # Base deserialization
167
+ default = core.default(schema)
168
+ deserialized = deep_merge(default, encoded)
169
+ address = deserialized.get('address')
170
+
171
+ if not address:
172
+ return deserialized
173
+
174
+ # protocol, address = deserialized['address'].split(':', 1)
175
+
176
+ # # Determine the process class to instantiate
177
+ # if instance:
178
+ # instantiate = type(instance)
179
+ # else:
180
+ # process_lookup = core.protocol_registry.access(protocol)
181
+ # if not process_lookup:
182
+ # raise Exception(f'Protocol "{protocol}" not implemented')
183
+ # instantiate = process_lookup(core, address)
184
+ # if not instantiate:
185
+ # raise Exception(f'Process "{address}" not found')
186
+
187
+ instantiate = core.parse_protocol(address)
188
+
189
+ # Deserialize the configuration
190
+ config = core.deserialize(instantiate.config_schema, deserialized.get('config', {}))
191
+ interval = core.deserialize('interval', deserialized.get('interval'))
192
+
193
+ if interval is None:
194
+ interval = core.default(schema.get('interval', 'interval'))
195
+
196
+ instance = deserialized.get('instance')
197
+ if not instance:
198
+ instance = instantiate(config, core=core)
199
+ deserialized['instance'] = instance
200
+
201
+ # Deserialize shared steps if any
202
+ shared = deserialized.get('shared', {})
203
+ deserialized['shared'] = {}
204
+
205
+ for step_id, step_config in shared.items():
206
+ step = deserialize_step('step', step_config, core)
207
+ step['instance'].register_shared(instance)
208
+ deserialized['shared'][step_id] = step
209
+
210
+ # Finalize state
211
+ deserialized['config'] = config
212
+ deserialized['interval'] = interval
213
+ deserialized['_inputs'] = copy.deepcopy(instance.inputs())
214
+ deserialized['_outputs'] = copy.deepcopy(instance.outputs())
215
+
216
+ return deserialized
217
+
218
+
219
+ def deserialize_step(schema, encoded, core):
220
+ """
221
+ Deserialize a single process step (sub-process in a composite).
222
+
223
+ Args:
224
+ schema (str): Schema key, typically 'step'.
225
+ encoded (dict): Serialized step data.
226
+ core: The type system.
227
+
228
+ Returns:
229
+ dict: Deserialized step state with process instance.
230
+ """
231
+ default = core.default(schema)
232
+ deserialized = deep_merge(default, encoded)
233
+ address = deserialized.get('address')
234
+
235
+ if not deserialized.get('address'):
236
+ return deserialized
237
+
238
+ instantiate = core.parse_protocol(address)
239
+ # protocol, address = deserialized['address'].split(':', 1)
240
+
241
+ # # Get class or factory function
242
+ # if instance:
243
+ # instantiate = type(instance)
244
+ # else:
245
+ # protocol = core.protocol_registry.access(protocol)
246
+ # if not protocol:
247
+ # raise Exception(f'Protocol "{protocol}" not implemented')
248
+ # instantiate = protocol.interface(core, address)
249
+ # if not instantiate:
250
+ # raise Exception(f'Process "{address}" not found')
251
+
252
+ # Deserialize config and create instance if needed
253
+ config = core.deserialize(instantiate.config_schema, deserialized.get('config', {}))
254
+
255
+ instance = deserialized.get('instance')
256
+ if not instance:
257
+ instance = instantiate(config, core=core)
258
+ deserialized['instance'] = instance
259
+
260
+ deserialized['config'] = config
261
+ deserialized['_inputs'] = copy.deepcopy(instance.inputs())
262
+ deserialized['_outputs'] = copy.deepcopy(instance.outputs())
263
+
264
+ return deserialized
265
+
266
+
267
+ # ===================
268
+ # Process Type System
269
+ # ===================
270
+
271
+ class ProcessTypes(TypeSystem):
272
+ """
273
+ Extends the TypeSystem to manage simulation process types,
274
+ including registries for process classes, protocols, and emitters.
275
+
276
+ Responsibilities:
277
+ - Registering new process types, protocols, and emitters
278
+ - Initializing edge states for composed processes
279
+ - Providing a default configuration/state template for processes
280
+ """
281
+
282
+ def __init__(self):
283
+ super().__init__()
284
+
285
+ # Registries to store user-defined and built-in components
286
+ self.process_registry = Registry()
287
+ self.protocol_registry = Registry()
288
+
289
+ # Initialize the core type system with known types and protocols
290
+ self.update_types(PROCESS_TYPES)
291
+ self.register_protocols(BASE_PROTOCOLS)
292
+ self.register_processes(BASE_EMITTERS)
293
+
294
+ # Explicitly register Composite process type
295
+ self.register_process('composite', Composite)
296
+
297
+ def register_protocols(self, protocols):
298
+ """
299
+ Register a dictionary of protocol types with the core type system.
300
+
301
+ Args:
302
+ protocols (dict): Mapping of protocol names to protocol definitions.
303
+ """
304
+ self.protocol_registry.register_multiple(protocols)
305
+
306
+ def register_process(self, name, process_data):
307
+ """
308
+ Register a new process type into the process registry.
309
+
310
+ Args:
311
+ name (str): Unique name for the process.
312
+ process_data: Associated class or factory function for the process.
313
+ """
314
+ self.process_registry.register(name, process_data)
315
+
316
+ def register_processes(self, processes):
317
+ """
318
+ Register multiple process types.
319
+
320
+ Args:
321
+ processes (dict): Mapping of process names to process data.
322
+ """
323
+ for process_key, process_data in processes.items():
324
+ self.register_process(process_key, process_data)
325
+
326
+ def initialize_edge_state(self, schema, path, edge):
327
+ """
328
+ Compute the initial state for a given edge in a composite process.
329
+
330
+ This combines the default input and output state projections
331
+ for a process based on its edge mapping and schema.
332
+
333
+ Args:
334
+ schema (dict): The complete schema of the composite.
335
+ path (tuple): Path to the process within the schema.
336
+ edge (dict): The edge entry with an instance and port mappings.
337
+
338
+ Returns:
339
+ dict: The initial merged state for the edge.
340
+ """
341
+ # Get initial state from the process instance
342
+ initial_state = edge['instance'].initial_state()
343
+ if not initial_state:
344
+ return initial_state
345
+
346
+ # Extract and clone port mappings from the schema
347
+ input_ports = copy.deepcopy(get_path(schema, path + ('_inputs',)))
348
+ output_ports = copy.deepcopy(get_path(schema, path + ('_outputs',)))
349
+ ports = {
350
+ '_inputs': input_ports,
351
+ '_outputs': output_ports
352
+ }
353
+
354
+ # Project the edge's initial state onto its inputs and outputs
355
+ input_state = self.project_edge(
356
+ ports, edge, path[:-1], initial_state, ports_key='inputs'
357
+ ) if input_ports else {}
358
+
359
+ output_state = self.project_edge(
360
+ ports, edge, path[:-1], initial_state, ports_key='outputs'
361
+ ) if output_ports else {}
362
+
363
+ return deep_merge(input_state, output_state)
364
+
365
+ def parse_protocol(self, address):
366
+ if isinstance(address, str):
367
+ protocol_name, protocol_data = address.split(':', 1)
368
+ else:
369
+ protocol_name = address['protocol']
370
+ protocol_data = address['data']
371
+
372
+ protocol = self.protocol_registry.access(protocol_name)
373
+ if not protocol:
374
+ raise Exception(f'Protocol "{protocol_name}" not implemented')
375
+
376
+ instantiate = protocol.interface(self, protocol_data)
377
+ if not instantiate:
378
+ raise Exception(f'Process "{address}" not found')
379
+
380
+ return instantiate
381
+
382
+ def default_state(self, process_class, initial_state=None):
383
+ """
384
+ Construct the default runtime state for a given process class.
385
+
386
+ Args:
387
+ process_class (class): The process class to instantiate.
388
+ initial_state (dict, optional): Overrides for the default state.
389
+
390
+ Returns:
391
+ dict: The fully constructed default process state.
392
+ """
393
+ # Get default config from the process's schema
394
+ default_config = self.default(process_class.config_schema)
395
+
396
+ # Instantiate process with default config
397
+ instance = process_class(default_config, core=self)
398
+
399
+ # Build standard process state structure
400
+ state = {
401
+ '_type': 'process',
402
+ 'address': f'local:!{process_class.__module__}.{process_class.__name__}',
403
+ 'config': default_config,
404
+ 'inputs': instance.default_inputs(),
405
+ 'outputs': instance.default_outputs()
406
+ }
407
+
408
+ # Add default interval if it's a subclass of Process
409
+ if isinstance(process_class, type):
410
+ try:
411
+ from vivarium.core.process import Process # Delayed import to avoid circularity
412
+ if issubclass(process_class, Process):
413
+ state['interval'] = 1.0
414
+ except ImportError:
415
+ pass # Skip interval assignment if Process class is unavailable
416
+
417
+ # Apply any user-provided state overrides
418
+ if initial_state:
419
+ state = deep_merge(state, initial_state)
420
+
421
+ return state
422
+
423
+
424
+ # ========================
425
+ # Process Types Dictionary
426
+ # ========================
427
+
428
+ PROCESS_TYPES = {
429
+ 'protocol': {
430
+ '_type': 'protocol',
431
+ '_inherit': 'any'},
432
+
433
+ 'emitter_mode': 'enum[none,all,stores,bridge,paths,ports]',
434
+
435
+ 'interval': {
436
+ '_type': 'interval',
437
+ '_inherit': 'float',
438
+ '_apply': 'set',
439
+ '_default': '1.0'},
440
+
441
+ 'step': {
442
+ '_type': 'step',
443
+ '_inherit': 'edge',
444
+ '_apply': apply_process,
445
+ '_serialize': serialize_process,
446
+ '_deserialize': deserialize_step,
447
+ '_check': check_process,
448
+ '_fold': fold_visit,
449
+ '_divide': divide_process,
450
+ '_description': '',
451
+ # TODO: support reference to type parameters from other states
452
+ 'address': 'protocol',
453
+ 'config': 'quote'},
454
+
455
+ # TODO: slice process to allow for navigating through a port
456
+ 'process': {
457
+ '_type': 'process',
458
+ '_inherit': 'edge',
459
+ '_apply': apply_process,
460
+ '_serialize': serialize_process,
461
+ '_deserialize': deserialize_process,
462
+ '_check': check_process,
463
+ '_fold': fold_visit,
464
+ '_divide': divide_process,
465
+ '_description': '',
466
+ # TODO: support reference to type parameters from other states
467
+ 'interval': 'interval',
468
+ 'address': 'protocol',
469
+ 'config': 'quote',
470
+ 'shared': 'map[step]'},
471
+ }
472
+
473
+
@@ -0,0 +1,26 @@
1
+ from process_bigraph.processes.parameter_scan import ToySystem, ODE, RunProcess, ParameterScan
2
+ from process_bigraph.processes.growth_division import Grow, Divide
3
+ from process_bigraph.emitter import BASE_EMITTERS
4
+ # from process_bigraph.experiments.minimal_gillespie import GillespieInterval, GillespieEvent
5
+
6
+
7
+ TOY_PROCESSES = {
8
+ 'ToySystem': ToySystem,
9
+ 'ToyODE': ODE,
10
+ 'RunProcess': RunProcess,
11
+ 'ParameterScan': ParameterScan,
12
+ 'grow': Grow,
13
+ 'divide': Divide,
14
+ # 'GillespieInterval': GillespieInterval,
15
+ # 'GillespieEvent': GillespieEvent
16
+ }
17
+
18
+
19
+ def register_processes(core):
20
+ for name, process in TOY_PROCESSES.items():
21
+ core.register_process(name, process)
22
+
23
+ core = core.register_processes(
24
+ BASE_EMITTERS)
25
+
26
+ return core
@@ -0,0 +1,167 @@
1
+ from process_bigraph.composite import Step, Process, deep_merge
2
+
3
+
4
+ class Grow(Process):
5
+ config_schema = {
6
+ 'rate': 'float'}
7
+
8
+ def inputs(self):
9
+ return {
10
+ 'mass': 'float'}
11
+
12
+ def outputs(self):
13
+ return {
14
+ 'mass': 'float'}
15
+
16
+ def update(self, state, interval):
17
+ # this calculates a delta
18
+
19
+ return {
20
+ 'mass': state['mass'] * self.config['rate'] * interval}
21
+
22
+
23
+ # TODO: build composite and divide within it
24
+
25
+ class Divide(Step):
26
+ # assume the agent_schema has the right divide methods present
27
+ config_schema = {
28
+ 'agent_id': 'string',
29
+ 'agent_schema': 'schema',
30
+ 'threshold': 'float',
31
+ 'divisions': {
32
+ '_type': 'integer',
33
+ '_default': 2}}
34
+
35
+
36
+ def __init__(self, config, core=None):
37
+ super().__init__(config, core)
38
+
39
+
40
+ def inputs(self):
41
+ return {
42
+ 'trigger': 'float'}
43
+
44
+
45
+ def outputs(self):
46
+ return {
47
+ 'environment': {
48
+ '_type': 'map',
49
+ '_value': self.config['agent_schema']}}
50
+
51
+
52
+ # this should be generalized to some function that depends on
53
+ # state from the self.config['agent_schema'] (instead of trigger > threshold)
54
+ def update(self, state):
55
+ if state['trigger'] > self.config['threshold']:
56
+ mother = self.config['agent_id']
57
+ daughters = [(
58
+ f'{mother}_{i}', {
59
+ 'state': {
60
+ 'divide': {
61
+ 'config': {
62
+ 'agent_id': f'{mother}_{i}'}}}})
63
+ for i in range(self.config['divisions'])]
64
+
65
+ # import ipdb; ipdb.set_trace()
66
+
67
+ # return divide reaction
68
+ return {
69
+ 'environment': {
70
+ '_react': {
71
+ 'divide': {
72
+ 'mother': mother,
73
+ 'daughters': daughters}}}}
74
+
75
+
76
+ def generate_bridge_wires(schema):
77
+ return {
78
+ key: [key]
79
+ for key in schema
80
+ if not key.startswith('_')}
81
+
82
+
83
+ def generate_bridge(schema, state, interval=1.0):
84
+ bridge = {
85
+ port: generate_bridge_wires(schema[port])
86
+ for port in ['inputs', 'outputs']}
87
+
88
+ config = {
89
+ '_type': 'quote',
90
+ 'state': state,
91
+ 'bridge': bridge}
92
+
93
+ composite = {
94
+ '_type': 'process',
95
+ 'address': 'parallel:composite',
96
+ 'interval': interval,
97
+ 'config': config,
98
+ 'inputs': generate_bridge_wires(schema['inputs']),
99
+ 'outputs': generate_bridge_wires(schema['outputs'])}
100
+
101
+ return composite
102
+
103
+
104
+ def grow_divide_agent(config=None, state=None, path=None):
105
+ agent_id = path[-1]
106
+
107
+ config = config or {}
108
+ state = state or {}
109
+ path = path or []
110
+
111
+ agent_schema = config.get(
112
+ 'agent_schema',
113
+ {'mass': 'float'})
114
+
115
+ grow_config = {
116
+ 'rate': 0.1}
117
+
118
+ grow_config = deep_merge(
119
+ grow_config,
120
+ config.get(
121
+ 'grow'))
122
+
123
+ divide_config = {
124
+ 'agent_id': agent_id,
125
+ 'agent_schema': agent_schema,
126
+ 'threshold': 2.0,
127
+ 'divisions': 2}
128
+
129
+ divide_config = deep_merge(
130
+ divide_config,
131
+ config.get(
132
+ 'divide'))
133
+
134
+ grow_divide_state = {
135
+ 'grow': {
136
+ '_type': 'process',
137
+ 'address': 'local:grow',
138
+ 'config': grow_config,
139
+ 'inputs': {
140
+ 'mass': ['mass']},
141
+ 'outputs': {
142
+ 'mass': ['mass']}},
143
+
144
+ 'divide': {
145
+ '_type': 'process',
146
+ 'address': 'local:divide',
147
+ 'config': divide_config,
148
+ 'inputs': {
149
+ 'trigger': ['mass']},
150
+ 'outputs': {
151
+ 'environment': ['environment']}}}
152
+
153
+ grow_divide_state = deep_merge(
154
+ grow_divide_state,
155
+ state)
156
+
157
+ composite = generate_bridge({
158
+ 'inputs': {'mass': ['mass']},
159
+ 'outputs': agent_schema},
160
+ grow_divide_state)
161
+
162
+ composite['config']['bridge']['outputs']['environment'] = ['environment']
163
+ composite['outputs']['environment'] = ['..']
164
+
165
+ return composite
166
+
167
+