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,1330 @@
1
+ """
2
+ =========================
3
+ Tests for Process Bigraph
4
+ =========================
5
+ """
6
+
7
+ import pytest
8
+ import random
9
+
10
+ from urllib.parse import urlparse, urlunparse
11
+
12
+ from bigraph_schema import default
13
+ from process_bigraph import register_types, ProcessTypes
14
+
15
+ from process_bigraph.composite import (
16
+ Process, Step, Composite, merge_collections, match_star_path, as_process, as_step,
17
+ )
18
+
19
+ from process_bigraph.processes.growth_division import grow_divide_agent, Grow, Divide
20
+ from process_bigraph.emitter import emitter_from_wires, gather_emitter_results
21
+ from process_bigraph.protocols.rest import rest_get, rest_post
22
+
23
+ @pytest.fixture
24
+ def core():
25
+ core = ProcessTypes()
26
+ return register_types(core)
27
+
28
+
29
+ class IncreaseProcess(Process):
30
+ config_schema = {
31
+ 'rate': {
32
+ '_type': 'float',
33
+ '_default': '0.1'}}
34
+
35
+ def inputs(self):
36
+ return {
37
+ 'level': 'float'}
38
+
39
+ def outputs(self):
40
+ return {
41
+ 'level': 'float'}
42
+
43
+ def accelerate(self, delta):
44
+ self.config['rate'] += delta
45
+
46
+ def initial_state(self):
47
+ return {
48
+ 'level': 4.4}
49
+
50
+ def update(self, state, interval):
51
+ return {
52
+ 'level': state['level'] * self.config['rate']}
53
+
54
+
55
+ class IncreaseRate(Step):
56
+ config_schema = {
57
+ 'acceleration': default('float', 0.001)}
58
+
59
+ def inputs(self):
60
+ return {
61
+ 'level': 'float'}
62
+
63
+ def update(self, state):
64
+ # TODO: this is ludicrous.... never do this
65
+ # probably this feature should only be used for reading
66
+ self.instance.accelerate(
67
+ self.config['acceleration'] * state['level'])
68
+
69
+
70
+ def test_default_config(core):
71
+ process = IncreaseProcess(core=core)
72
+
73
+ assert process.config['rate'] == 0.1
74
+
75
+
76
+ def test_merge_collections(core):
77
+ a = {('what',): [1, 2, 3]}
78
+ b = {('okay', 'yes'): [3, 3], ('what',): [4, 5, 11]}
79
+
80
+ c = merge_collections(a, b)
81
+
82
+ assert c[('what',)] == [1, 2, 3, 4, 5, 11]
83
+
84
+
85
+ def test_process(core):
86
+ process = IncreaseProcess({'rate': 0.2}, core=core)
87
+ interface = process.interface()
88
+ state = core.fill(interface['inputs'])
89
+ state = core.fill(interface['outputs'])
90
+ update = process.update({'level': 5.5}, 1.0)
91
+
92
+ new_state = core.apply(
93
+ interface['outputs'],
94
+ state,
95
+ update)
96
+
97
+ assert new_state['level'] == 1.1
98
+
99
+
100
+ def test_composite(core):
101
+ # TODO: add support for the various vivarium emitter
102
+
103
+ # increase = IncreaseProcess({'rate': 0.3})
104
+ # TODO: This is the config of the composite,
105
+ # we also need a way to serialize the entire composite
106
+
107
+ composite = Composite({
108
+ 'composition': {
109
+ 'increase': 'process[level:float,level:float]',
110
+ 'value': 'float'},
111
+ 'interface': {
112
+ 'inputs': {
113
+ 'exchange': 'float'},
114
+ 'outputs': {
115
+ 'exchange': 'float'}},
116
+ 'bridge': {
117
+ 'inputs': {
118
+ 'exchange': ['value']},
119
+ 'outputs': {
120
+ 'exchange': ['value']}},
121
+ 'state': {
122
+ 'increase': {
123
+ 'address': 'local:!process_bigraph.tests.IncreaseProcess',
124
+ 'config': {'rate': 0.3},
125
+ 'interval': 1.0,
126
+ 'inputs': {'level': ['value']},
127
+ 'outputs': {'level': ['value']}},
128
+ 'value': '11.11'}}, core=core)
129
+
130
+ initial_state = {'exchange': 3.33}
131
+
132
+ updates = composite.update(initial_state, 10.0)
133
+
134
+ final_exchange = sum([
135
+ update['exchange']
136
+ for update in [initial_state] + updates])
137
+
138
+ assert composite.state['value'] > 45
139
+ assert 'exchange' in updates[0]
140
+ assert updates[0]['exchange'] == 0.999
141
+
142
+
143
+ def test_infer(core):
144
+ composite = Composite({
145
+ 'state': {
146
+ 'increase': {
147
+ '_type': 'process',
148
+ 'address': 'local:!process_bigraph.tests.IncreaseProcess',
149
+ 'config': {'rate': '0.3'},
150
+ 'inputs': {'level': ['value']},
151
+ 'outputs': {'level': ['value']}},
152
+ 'value': '11.11'}}, core=core)
153
+
154
+ assert composite.composition['value']['_type'] == 'float'
155
+ assert composite.state['value'] == 4.4
156
+
157
+
158
+ def test_process_type(core):
159
+ assert core.access('process')['_type'] == 'process'
160
+
161
+
162
+ class OperatorStep(Step):
163
+ config_schema = {
164
+ 'operator': 'string'}
165
+
166
+
167
+ def inputs(self):
168
+ return {
169
+ 'a': 'float',
170
+ 'b': 'float'}
171
+
172
+
173
+ def outputs(self):
174
+ return {
175
+ 'c': 'float'}
176
+
177
+
178
+ def update(self, inputs):
179
+ a = inputs['a']
180
+ b = inputs['b']
181
+
182
+ if self.config['operator'] == '+':
183
+ c = a + b
184
+ elif self.config['operator'] == '*':
185
+ c = a * b
186
+ elif self.config['operator'] == '-':
187
+ c = a - b
188
+
189
+ return {'c': c}
190
+
191
+
192
+ def test_step_initialization(core):
193
+ composite = Composite({
194
+ 'state': {
195
+ 'A': 13,
196
+ 'B': 21,
197
+ 'step1': {
198
+ '_type': 'step',
199
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
200
+ 'config': {
201
+ 'operator': '+'},
202
+ 'inputs': {
203
+ 'a': ['A'],
204
+ 'b': ['B']},
205
+ 'outputs': {
206
+ 'c': ['C']}},
207
+ 'step2': {
208
+ '_type': 'step',
209
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
210
+ 'config': {
211
+ 'operator': '*'},
212
+ 'inputs': {
213
+ 'a': ['B'],
214
+ 'b': ['C']},
215
+ 'outputs': {
216
+ 'c': ['D']}}}}, core=core)
217
+
218
+ composite.run(0.0)
219
+ assert composite.state['D'] == (13 + 21) * 21
220
+
221
+
222
+ def test_dependencies(core):
223
+ operation = {
224
+ 'a': 11.111,
225
+ 'b': 22.2,
226
+ 'c': 555.555,
227
+
228
+ '1': {
229
+ '_type': 'step',
230
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
231
+ 'config': {
232
+ 'operator': '+'},
233
+ 'inputs': {
234
+ 'a': ['a'],
235
+ 'b': ['b']},
236
+ 'outputs': {
237
+ 'c': ['e']}},
238
+ '2.1': {
239
+ '_type': 'step',
240
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
241
+ 'config': {
242
+ 'operator': '-'},
243
+ 'inputs': {
244
+ 'a': ['c'],
245
+ 'b': ['e']},
246
+ 'outputs': {
247
+ 'c': ['f']}},
248
+ '2.2': {
249
+ '_type': 'step',
250
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
251
+ 'config': {
252
+ 'operator': '-'},
253
+ 'inputs': {
254
+ 'a': ['d'],
255
+ 'b': ['e']},
256
+ 'outputs': {
257
+ 'c': ['g']}},
258
+ '3': {
259
+ '_type': 'step',
260
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
261
+ 'config': {
262
+ 'operator': '*'},
263
+ 'inputs': {
264
+ 'a': ['f'],
265
+ 'b': ['g']},
266
+ 'outputs': {
267
+ 'c': ['h']}},
268
+ '4': {
269
+ '_type': 'step',
270
+ 'address': 'local:!process_bigraph.tests.OperatorStep',
271
+ 'config': {
272
+ 'operator': '+'},
273
+ 'inputs': {
274
+ 'a': ['e'],
275
+ 'b': ['h']},
276
+ 'outputs': {
277
+ 'c': ['i']}}}
278
+
279
+ composite = Composite(
280
+ {'state': operation},
281
+ core=core)
282
+
283
+ composite.run(0.0)
284
+
285
+ assert composite.state['h'] == -17396.469884
286
+
287
+
288
+ def test_dependency_cycle():
289
+ # test a step network with cycles in a few ways
290
+ pass
291
+
292
+
293
+ class SimpleCompartment(Process):
294
+ config_schema = {
295
+ 'id': 'string'}
296
+
297
+
298
+ def interface(self):
299
+ return {
300
+ 'outer': 'tree[process]',
301
+ 'inner': 'tree[process]'}
302
+
303
+
304
+ def update(self, state, interval):
305
+ choice = random.random()
306
+ update = {}
307
+
308
+ outer = state['outer']
309
+ inner = state['inner']
310
+
311
+ # TODO: implement divide_state(_)
312
+ divisions = self.core.divide_state(
313
+ self.interface(),
314
+ inner)
315
+
316
+ if choice < 0.2:
317
+ # update = {
318
+ # 'outer': {
319
+ # '_divide': {
320
+ # 'mother': self.config['id'],
321
+ # 'daughters': [
322
+ # {'id': self.config['id'] + '0'},
323
+ # {'id': self.config['id'] + '1'}]}}}
324
+
325
+ # daughter_ids = [self.config['id'] + str(i)
326
+ # for i in range(2)]
327
+
328
+ # update = {
329
+ # 'outer': {
330
+ # '_react': {
331
+ # 'redex': {
332
+ # 'inner': {
333
+ # self.config['id']: {}}},
334
+ # 'reactum': {
335
+ # 'inner': {
336
+ # daughter_config['id']: {
337
+ # '_type': 'process',
338
+ # 'address': 'local:!process_bigraph.tests.SimpleCompartment',
339
+ # 'config': daughter_config,
340
+ # 'inner': daughter_inner,
341
+ # 'wires': {
342
+ # 'outer': ['..']}}
343
+ # for daughter_config, daughter_inner in zip(daughter_configs, divisions)}}}}}
344
+
345
+ update = {
346
+ 'outer': {
347
+ 'inner': {
348
+ '_react': {
349
+ 'reaction': 'divide',
350
+ 'config': {
351
+ 'id': self.config['id'],
352
+ 'daughters': [{
353
+ 'id': daughter_id,
354
+ 'state': daughter_state}
355
+ for daughter_id, daughter_state in zip(
356
+ daughter_ids,
357
+ divisions)]}}}}}
358
+
359
+ return update
360
+
361
+
362
+ def engulf_reaction(config):
363
+ return {
364
+ 'redex': {},
365
+ 'reactum': {}}
366
+
367
+
368
+ def burst_reaction(config):
369
+ return {
370
+ 'redex': {},
371
+ 'reactum': {}}
372
+
373
+
374
+ def test_reaction():
375
+ composite = {
376
+ 'state': {
377
+ 'environment': {
378
+ 'concentrations': {},
379
+ 'inner': {
380
+ 'agent1': {
381
+ '_type': 'process',
382
+ 'address': 'local:!process_bigraph.tests.SimpleCompartment',
383
+ 'config': {'id': '0'},
384
+ 'concentrations': {},
385
+ 'inner': {
386
+ 'agent2': {
387
+ '_type': 'process',
388
+ 'address': 'local:!process_bigraph.tests.SimpleCompartment',
389
+ 'config': {'id': '0'},
390
+ 'inner': {},
391
+ 'inputs': {
392
+ 'outer': ['..', '..'],
393
+ 'inner': ['inner']},
394
+ 'outputs': {
395
+ 'outer': ['..', '..'],
396
+ 'inner': ['inner']}}},
397
+ 'inputs': {
398
+ 'outer': ['..', '..'],
399
+ 'inner': ['inner']},
400
+ 'outputs': {
401
+ 'outer': ['..', '..'],
402
+ 'inner': ['inner']}}}}}}
403
+
404
+
405
+ def test_emitter(core):
406
+ composite_schema = {
407
+ 'bridge': {
408
+ 'inputs': {
409
+ 'DNA': ['DNA'],
410
+ 'mRNA': ['mRNA']},
411
+ 'outputs': {
412
+ 'DNA': ['DNA'],
413
+ 'mRNA': ['mRNA']}},
414
+
415
+ 'state': {
416
+ 'interval': {
417
+ '_type': 'step',
418
+ 'address': 'local:!process_bigraph.experiments.minimal_gillespie.GillespieInterval',
419
+ 'config': {'ktsc': '6e0'},
420
+ 'inputs': {
421
+ 'DNA': ['DNA'],
422
+ 'mRNA': ['mRNA']},
423
+ 'outputs': {
424
+ 'interval': ['event', 'interval']}},
425
+
426
+ 'event': {
427
+ '_type': 'process',
428
+ 'address': 'local:!process_bigraph.experiments.minimal_gillespie.GillespieEvent',
429
+ 'config': {'ktsc': 6e0},
430
+ 'inputs': {
431
+ 'DNA': ['DNA'],
432
+ 'mRNA': ['mRNA']},
433
+ 'outputs': {
434
+ 'mRNA': ['mRNA']},
435
+ 'interval': '3.0'}},
436
+ 'emitter': emitter_from_wires({
437
+ 'mRNA': ['mRNA'],
438
+ 'interval': ['event', 'interval']})}
439
+
440
+ gillespie = Composite(
441
+ composite_schema,
442
+ core=core)
443
+
444
+ updates = gillespie.update({
445
+ 'DNA': {
446
+ 'A gene': 11.0,
447
+ 'B gene': 5.0},
448
+ 'mRNA': {
449
+ 'A mRNA': 33.3,
450
+ 'B mRNA': 2.1}},
451
+ 1000.0)
452
+
453
+ # TODO: make this work
454
+ results = gather_emitter_results(gillespie)
455
+
456
+ assert 'mRNA' in updates[0]
457
+ # TODO: support omit as well as emit
458
+
459
+
460
+ def test_run_process(core):
461
+ timestep = 0.1
462
+ runtime = 10.0
463
+ initial_A = 11.11
464
+
465
+ state = {
466
+ 'species': {
467
+ 'A': initial_A},
468
+ 'run': {
469
+ '_type': 'step',
470
+ 'address': 'local:RunProcess',
471
+ 'config': {
472
+ 'process_address': 'local:ToySystem',
473
+ 'process_config': {
474
+ 'rates': {
475
+ 'A': {
476
+ 'kdeg': 1.1,
477
+ 'ksynth': 0.9}}},
478
+ 'observables': [['species']],
479
+ 'timestep': timestep,
480
+ 'runtime': runtime},
481
+ 'inputs': {'species': ['species']},
482
+ 'outputs': {'results': ['A_results']}}}
483
+
484
+ run = Composite({
485
+ 'bridge': {
486
+ 'outputs': {
487
+ 'results': ['A_results']}},
488
+ 'state': state},
489
+ core=core)
490
+
491
+ results = run.read_bridge()['results']
492
+
493
+ assert results['time'][-1] == runtime
494
+ assert results['species'][0]['A'] == initial_A
495
+
496
+
497
+ def test_nested_wires(core):
498
+ timestep = 0.1
499
+ runtime = 10.0
500
+ initial_A = 11.11
501
+
502
+ state = {
503
+ 'species': {'A': initial_A},
504
+ 'run': {
505
+ '_type': 'step',
506
+ 'address': 'local:RunProcess',
507
+ 'config': {
508
+ 'process_address': 'local:ToySystem',
509
+ 'process_config': {
510
+ 'rates': {
511
+ 'A': {
512
+ 'kdeg': 1.1,
513
+ 'ksynth': 0.9}}},
514
+ 'observables': [['species', 'A']],
515
+ 'timestep': timestep,
516
+ 'runtime': runtime},
517
+ # '_outputs': {'results': {'_emit': True}},
518
+ 'inputs': {'species': ['species']},
519
+ 'outputs': {'results': ['A_results']}}}
520
+
521
+ process = Composite({
522
+ 'bridge': {
523
+ 'outputs': {
524
+ 'results': ['A_results']}},
525
+ 'state': state},
526
+ core=core)
527
+
528
+ results = process.update({}, 0.0)
529
+
530
+ assert results[0]['results']['time'][-1] == runtime
531
+ assert results[0]['results']['species']['A'][0] == initial_A
532
+
533
+
534
+ def test_parameter_scan(core):
535
+ # TODO: make a parameter scan with a biosimulator process,
536
+ # ie - Copasi
537
+
538
+ state = {
539
+ 'scan': {
540
+ '_type': 'step',
541
+ 'address': 'local:ParameterScan',
542
+ 'config': {
543
+ 'parameter_ranges': [(
544
+ ['rates', 'A', 'kdeg'], [0.0, 0.1, 1.0, 10.0])],
545
+ 'process_address': 'local:ToySystem',
546
+ 'process_config': {
547
+ 'rates': {
548
+ 'A': {
549
+ 'ksynth': 1.0}}},
550
+ 'observables': [
551
+ ['species', 'A']],
552
+ 'initial_state': {
553
+ 'species': {
554
+ 'A': 13.3333}},
555
+ 'timestep': 1.0,
556
+ 'runtime': 10},
557
+ 'outputs': {
558
+ 'results': ['results']}}}
559
+
560
+ scan = Composite({
561
+ 'bridge': {
562
+ 'outputs': {
563
+ 'results': ['results']}},
564
+ 'state': state},
565
+ core=core)
566
+
567
+ # TODO: make a method so we can run it directly, provide some way to get the result out
568
+ # result = scan.update({})
569
+ result = scan.update({}, 0.0)
570
+
571
+
572
+ def test_composite_workflow(core):
573
+ # TODO: Make a workflow with a composite inside
574
+ pass
575
+
576
+
577
+ def test_grow_divide(core):
578
+ initial_mass = 1.0
579
+
580
+ grow_divide = grow_divide_agent(
581
+ {'grow': {'rate': 0.03}},
582
+ {},
583
+ # {'mass': initial_mass},
584
+ ['environment', '0'])
585
+
586
+ environment = {
587
+ 'environment': {
588
+ '0': {
589
+ 'mass': initial_mass,
590
+ 'grow_divide': grow_divide}}}
591
+
592
+ composite = Composite({
593
+ 'state': environment,
594
+ 'bridge': {
595
+ 'inputs': {
596
+ 'environment': ['environment']}}},
597
+ core=core)
598
+
599
+ updates = composite.update({
600
+ 'environment': {
601
+ '0': {
602
+ 'mass': 1.1}}},
603
+ 100.0)
604
+
605
+ # TODO: mass is not synchronized between inside and outside the composite?
606
+
607
+ assert '0_0_0_0_1' in composite.state['environment']
608
+ assert composite.state['environment']['0_0_0_0_1']['mass'] == composite.state['environment']['0_0_0_0_1']['grow_divide']['instance'].state['mass']
609
+
610
+ # check recursive schema reference
611
+ assert id(composite.composition['environment']['_value']['grow_divide']['_outputs']['environment']) == id(composite.composition['environment']['_value']['grow_divide']['_outputs']['environment']['_value']['grow_divide']['_outputs']['environment'])
612
+
613
+ composite.save('test_grow_divide_saved.json')
614
+
615
+ c2 = Composite.load(
616
+ 'out/test_grow_divide_saved.json',
617
+ core=core)
618
+
619
+ assert id(composite.composition['environment']['_value']['grow_divide']['_outputs']['environment']) == id(composite.composition['environment']['_value']['grow_divide']['_outputs']['environment']['_value']['grow_divide']['_outputs']['environment'])
620
+
621
+
622
+ def test_gillespie_composite(core):
623
+ composite_schema = {
624
+ 'bridge': {
625
+ 'inputs': {
626
+ 'DNA': ['DNA'],
627
+ 'mRNA': ['mRNA']},
628
+ 'outputs': {
629
+ 'DNA': ['DNA'],
630
+ 'mRNA': ['mRNA']}},
631
+
632
+ 'state': {
633
+ 'interval': {
634
+ '_type': 'step',
635
+ 'address': 'local:!process_bigraph.experiments.minimal_gillespie.GillespieInterval',
636
+ 'config': {'ktsc': '6e0'},
637
+ 'inputs': {
638
+ 'DNA': ['DNA'],
639
+ 'mRNA': ['mRNA']},
640
+ 'outputs': {
641
+ 'interval': ['event', 'interval']}},
642
+
643
+ 'event': {
644
+ '_type': 'process',
645
+ 'address': 'local:!process_bigraph.experiments.minimal_gillespie.GillespieEvent',
646
+ 'config': {'ktsc': 6e0},
647
+ 'inputs': {
648
+ 'DNA': ['DNA'],
649
+ 'mRNA': ['mRNA']},
650
+ 'outputs': {
651
+ 'mRNA': ['mRNA']},
652
+ 'interval': '3.0'},
653
+
654
+ 'emitter': {
655
+ '_type': 'step',
656
+ 'address': 'local:ram-emitter',
657
+ 'config': {
658
+ 'emit': {
659
+ 'time': 'float',
660
+ 'mRNA': 'map[float]',
661
+ 'interval': 'interval'}},
662
+ 'inputs': {
663
+ 'time': ['global_time'],
664
+ 'mRNA': ['mRNA'],
665
+ 'interval': ['event', 'interval']}}}}
666
+
667
+ gillespie = Composite(
668
+ composite_schema,
669
+ core=core)
670
+
671
+ updates = gillespie.update({
672
+ 'DNA': {
673
+ 'A gene': 11.0,
674
+ 'B gene': 5.0},
675
+ 'mRNA': {
676
+ 'A mRNA': 33.3,
677
+ 'B mRNA': 2.1}},
678
+ 1000.0)
679
+
680
+ # TODO: make this work
681
+ results = gather_emitter_results(gillespie)
682
+
683
+ assert 'mRNA' in updates[0]
684
+
685
+
686
+ def test_union_tree(core):
687
+ tree_union = core.access('list[string]~tree[list[string]]')
688
+ assert core.check(
689
+ tree_union,
690
+ {'a': ['what', 'is', 'happening']})
691
+
692
+
693
+ def test_merge_schema(core):
694
+ state = {'a': 11.0}
695
+ composite = Composite({
696
+ 'state': state}, core=core)
697
+
698
+ increase_schema = {
699
+ 'increase': {
700
+ '_type': 'process',
701
+ 'address': default('string', 'local:!process_bigraph.tests.IncreaseProcess'),
702
+ 'config': default('quote', {'rate': 0.0001}),
703
+ 'inputs': default('wires', {'level': ['b']}),
704
+ 'outputs': default('wires', {'level': ['a']})}}
705
+
706
+ composite.merge(
707
+ increase_schema,
708
+ {})
709
+
710
+ # composite.merge_schema(
711
+ # increase_schema)
712
+
713
+ assert composite.composition['increase']['_type'] == 'process'
714
+ assert isinstance(composite.state['increase']['instance'], Process)
715
+
716
+ state = {
717
+ 'x': -3.33,
718
+ 'atoms': {
719
+ 'A': {
720
+ 'lll': 55}}}
721
+
722
+ composition = {
723
+ 'atoms': 'map[lll:integer]'}
724
+
725
+ merge = Composite({
726
+ 'composition': composition,
727
+ 'state': state}, core=core)
728
+
729
+ nested_increase_schema = {
730
+ 'increase': {
731
+ '_type': 'process',
732
+ 'address': default('string', 'local:!process_bigraph.tests.IncreaseProcess'),
733
+ 'config': default('quote', {'rate': 0.0001}),
734
+ 'inputs': default('wires', {'level': ['..', '..', 'b']}),
735
+ 'outputs': default('wires', {'level': ['..', '..', 'a']})}}
736
+
737
+ merge.merge(
738
+ {'atoms': {'_value': nested_increase_schema}},
739
+ {})
740
+
741
+ # TODO: do we need merge_schema if merge works for schema and state?
742
+ # merge.merge_schema(
743
+ # nested_increase_schema,
744
+ # path=['atoms', '_value'])
745
+
746
+ assert isinstance(merge.state['atoms']['A']['increase']['instance'], Process)
747
+ assert merge.composition['atoms']['_value']['increase']['_type'] == 'process'
748
+ assert ('atoms', 'A', 'increase') in merge.process_paths
749
+
750
+ merge.merge(
751
+ {},
752
+ {'atoms': {'B': {'lll': 11111}}})
753
+
754
+ assert isinstance(merge.state['atoms']['B']['increase']['instance'], Process)
755
+ assert ('atoms', 'B', 'increase') in merge.process_paths
756
+
757
+
758
+ def test_shared_steps(core):
759
+ initial_rate = 0.4
760
+
761
+ state = {
762
+ 'value': 1.1,
763
+ 'increase': {
764
+ '_type': 'process',
765
+ 'address': 'local:!process_bigraph.tests.IncreaseProcess',
766
+ 'config': {'rate': initial_rate},
767
+ 'inputs': {'level': ['value']},
768
+ 'outputs': {'level': ['value']},
769
+ 'shared': {
770
+ 'accelerate': {
771
+ 'address': 'local:!process_bigraph.tests.IncreaseRate',
772
+ 'config': {'acceleration': '3e-20'},
773
+ 'inputs': {'level': ['..', '..', 'value']}}}},
774
+ 'emitter': emitter_from_wires({
775
+ 'level': ['value']})}
776
+
777
+ shared = Composite(
778
+ {'state': state},
779
+ core=core)
780
+
781
+ shared.run(100)
782
+
783
+ results = gather_emitter_results(shared)
784
+
785
+ assert shared.state['increase']['shared']['accelerate']['instance'].instance.config['rate'] == shared.state['increase']['instance'].config['rate']
786
+ assert shared.state['increase']['instance'].config['rate'] > initial_rate
787
+
788
+
789
+ class WriteCounts(Step):
790
+ def inputs(self):
791
+ return {
792
+ 'volumes': 'map[float]',
793
+ 'concentrations': 'map[map[float]]'}
794
+
795
+
796
+ def outputs(self):
797
+ return {
798
+ 'counts': 'map[map[integer]]'}
799
+
800
+
801
+ def update(self, state):
802
+ counts = {}
803
+
804
+ for key, local in state['concentrations'].items():
805
+ counts[key] = {}
806
+ for substrate, concentration in local.items():
807
+ count = int(concentration * state['volumes'][key])
808
+ counts[key][substrate] = count
809
+
810
+ return {
811
+ 'counts': counts}
812
+
813
+
814
+ def test_star_update(core):
815
+ composition = {
816
+ 'Compartments': {
817
+ '_type': 'map',
818
+ '_value': {
819
+ 'Shared Environment': {
820
+ 'counts': 'map[integer]',
821
+ 'concentrations': 'map[float]',
822
+ 'volume': 'float'},
823
+ 'position': 'list[float]'}}}
824
+
825
+ state = {
826
+ 'write': {
827
+ '_type': 'process',
828
+ 'address': 'local:!process_bigraph.tests.WriteCounts',
829
+ 'inputs': {
830
+ 'volumes': ['Compartments', '*', 'Shared Environment', 'volume'],
831
+ 'concentrations': ['Compartments', '*', 'Shared Environment', 'concentrations']},
832
+ 'outputs': {
833
+ 'counts': ['Compartments', '*', 'Shared Environment', 'counts']}},
834
+
835
+ 'Compartments': {
836
+ '0': {
837
+ 'Shared Environment': {
838
+ 'concentrations': {
839
+ 'acetate': 1.123976466801866,
840
+ 'biomass': 5.484002382436302,
841
+ 'glucose': 5.054266524967003},
842
+ 'counts': {
843
+ 'acetate': 1.123976466801866,
844
+ 'biomass': 5.484002382436302,
845
+ 'glucose': 5.054266524967003},
846
+ 'volume': 100},
847
+ 'position': [0.5, 0.5, 0.0]},
848
+ '1': {
849
+ 'Shared Environment': {
850
+ 'concentrations': {
851
+ 'acetate': 1.1582833546687243,
852
+ 'biomass': 5.2088139570269405,
853
+ 'glucose': 2.4652858010098577},
854
+ 'counts': {
855
+ 'acetate': 1.1582833546687243,
856
+ 'biomass': 5.2088139570269405,
857
+ 'glucose': 2.4652858010098577},
858
+ 'volume': 200},
859
+ 'position': [0.5, 1.5, 0.0]},
860
+ '2': {
861
+ 'Shared Environment': {
862
+ 'concentrations': {
863
+ 'acetate': 2.644399921259828,
864
+ 'biomass': 9.63480818091309,
865
+ 'glucose': 2.375172278348736},
866
+ 'counts': {
867
+ 'acetate': 2.644399921259828,
868
+ 'biomass': 9.63480818091309,
869
+ 'glucose': 2.375172278348736},
870
+ 'volume': 300},
871
+ 'position': [0.5, 2.5, 0.0]}}}
872
+
873
+ star = Composite({
874
+ 'composition': composition,
875
+ 'state': state}, core=core)
876
+
877
+ assert star.state['Compartments']['2']['Shared Environment']['counts']['biomass'] == 2899
878
+
879
+
880
+ def test_default_process_state(core):
881
+ # provide some initial values
882
+ default_rate = {
883
+ 'config': {
884
+ 'rate': 0.001}}
885
+
886
+ # generate a default state for the Grow process
887
+ default_grow = core.default_state(
888
+ Grow,
889
+ default_rate)
890
+
891
+ # create a composite from the default process state
892
+ composite = Composite({
893
+ 'state': {
894
+ 'grow': default_grow,
895
+ 'mass': 1.0}},
896
+ core=core)
897
+
898
+ # run the composite
899
+ composite.run(10.0)
900
+
901
+ # assert the process ran and the mass increased
902
+ assert composite.state['mass'] > 1.0
903
+
904
+ # try a step as well
905
+ default_divide = core.default_state(
906
+ Divide)
907
+
908
+ # the step should not have an 'interval' as they do not consume time
909
+ assert 'interval' not in default_divide
910
+
911
+
912
+ class AboveProcess(Process):
913
+ config_schema = {
914
+ 'rate': 'float'}
915
+
916
+ def inputs(self):
917
+ return {
918
+ 'below': 'map[mass:float]'}
919
+
920
+ def outputs(self):
921
+ return {
922
+ 'below': 'map[mass:float]'}
923
+
924
+ def update(self, state, interval):
925
+ update = {
926
+ 'below': {}}
927
+
928
+ for id, pod in state['below'].items():
929
+ update['below'][id] = {
930
+ 'mass': self.config['rate'] * pod['mass']}
931
+
932
+ return update
933
+
934
+
935
+ class BelowProcess(Process):
936
+ next_id = 1
937
+
938
+ config_schema = {
939
+ 'id': 'string',
940
+ 'creation_probability': 'float',
941
+ 'annihilation_probability': 'float'}
942
+
943
+ def inputs(self):
944
+ return {
945
+ 'mass': 'float',
946
+ 'entropy': 'float'}
947
+
948
+ def outputs(self):
949
+ return {
950
+ 'entropy': 'float',
951
+ 'environment': 'map[mass:float]'}
952
+
953
+ def update(self, state, interval):
954
+ creation = random.random() < self.config['creation_probability'] * state['entropy']
955
+ annihilation = random.random() < self.config['annihilation_probability'] * state['entropy']
956
+
957
+ update = {}
958
+
959
+ if creation or annihilation:
960
+ update['environment'] = {}
961
+
962
+ if creation:
963
+ new_id = str(BelowProcess.next_id)
964
+ BelowProcess.next_id += 1
965
+
966
+ update['environment']['_add'] = {
967
+ new_id: {
968
+ 'mass': 1.1,
969
+ 'below': {
970
+ 'config': {
971
+ 'id': new_id}}}}
972
+
973
+ if annihilation:
974
+ update['environment']['_remove'] = [self.config['id']]
975
+
976
+ if not 'environment' in update:
977
+ update['entropy'] = 0.1
978
+ else:
979
+ update['entropy'] = -state['entropy']
980
+
981
+ return update
982
+
983
+
984
+ def test_update_removal(core):
985
+ composition = {
986
+ 'environment': {
987
+ '_type': 'map',
988
+ '_value': {
989
+ 'below': {
990
+ '_type': 'process',
991
+ 'address': default(
992
+ 'string',
993
+ 'local:!process_bigraph.tests.BelowProcess'),
994
+ 'config': default('quote', {
995
+ 'creation_probability': 0.01,
996
+ 'annihilation_probability': 0.007}),
997
+ 'inputs': default('wires', {
998
+ 'mass': ['mass'],
999
+ 'entropy': ['entropy']}),
1000
+ 'outputs': default('wires', {
1001
+ 'entropy': ['entropy'],
1002
+ 'environment': ['..']}),
1003
+ 'interval': default('float', 0.4)}}}}
1004
+
1005
+ state = {
1006
+ 'above': {
1007
+ '_type': 'process',
1008
+ 'address': 'local:!process_bigraph.tests.AboveProcess',
1009
+ 'config': {
1010
+ 'rate': 0.001},
1011
+ 'inputs': {
1012
+ 'below': ['environment']},
1013
+ 'outputs': {
1014
+ 'below': ['environment']},
1015
+ 'interval': 3.33},
1016
+ 'environment': {
1017
+ '0': {
1018
+ 'below': {
1019
+ 'config': {
1020
+ 'id': '0'}},
1021
+ 'mass': 1.001,
1022
+ 'entropy': 0.03}}}
1023
+
1024
+ composite = Composite({
1025
+ 'composition': composition,
1026
+ 'state': state}, core=core)
1027
+
1028
+ composite.run(50)
1029
+
1030
+
1031
+ def test_stochastic_deterministic_composite(core):
1032
+ # TODO make the demo for a hybrid stochastic/deterministic simulator
1033
+ pass
1034
+
1035
+
1036
+ def test_match_star_path(core):
1037
+ assert match_star_path(["first", "list", "test"], ["first", "*", "test"])
1038
+ assert not match_star_path(["first", "list", "tent"], ["first", "*", "test"])
1039
+ assert match_star_path(["first", "list", "test"], ["first", "list", "test"])
1040
+
1041
+
1042
+ def test_function_wrappers(core):
1043
+ # --- STEP with core ---
1044
+ @as_step(inputs={'a': 'float', 'b': 'float'},
1045
+ outputs={'sum': 'float'},
1046
+ core=core)
1047
+ def update_add(state):
1048
+ return {'sum': state['a'] + state['b']}
1049
+
1050
+ step = update_add(config={}, core=core)
1051
+ out = step.update({'a': 5, 'b': 7})
1052
+ assert out == {'sum': 12}
1053
+ assert core.find('add')
1054
+ print("Step with core:", out)
1055
+
1056
+ # --- PROCESS with core ---
1057
+ @as_process(inputs={'x': 'float'},
1058
+ outputs={'x': 'float'},
1059
+ core=core)
1060
+ def update_decay(state, interval):
1061
+ return {'x': state['x'] * (1 - 0.2 * interval)}
1062
+
1063
+ proc = update_decay(config={}, core=core)
1064
+ out = proc.update({'x': 50.0}, 1.0)
1065
+ assert round(out['x'], 2) == 40.0
1066
+ assert core.find('decay')
1067
+ print("Process with core:", out)
1068
+
1069
+ def test_registered_functions_in_composite(core):
1070
+ @as_step(inputs={'a': 'float', 'b': 'float'},
1071
+ outputs={'sum': 'float'},
1072
+ core=core)
1073
+ def update_add(state):
1074
+ return {'sum': state['a'] + state['b']}
1075
+
1076
+ @as_process(inputs={'x': 'float'},
1077
+ outputs={'x': 'float'},
1078
+ core=core)
1079
+ def update_decay(state, interval):
1080
+ return {'x': state['x'] * (1 - 0.1 * interval)}
1081
+
1082
+ # Define Composite
1083
+ state = {
1084
+ 'adder': {
1085
+ '_type': 'process',
1086
+ 'address': 'local:add',
1087
+ 'inputs': {
1088
+ 'a': ['Env', 'a'],
1089
+ 'b': ['Env', 'b']
1090
+ },
1091
+ 'outputs': {
1092
+ 'sum': ['Env', 'sum']
1093
+ }
1094
+ },
1095
+ 'decayer': {
1096
+ '_type': 'process',
1097
+ 'address': 'local:decay',
1098
+ 'inputs': {
1099
+ 'x': ['Env', 'sum']
1100
+ },
1101
+ 'outputs': {
1102
+ 'x': ['Env', 'x']
1103
+ }
1104
+ },
1105
+ 'Env': {
1106
+ 'a': 3.0,
1107
+ 'b': 2.0,
1108
+ 'sum': 0.0,
1109
+ 'x': 0.0
1110
+ }
1111
+ }
1112
+
1113
+ # Run Composite
1114
+ sim = Composite({
1115
+ 'state': state
1116
+ }, core=core)
1117
+
1118
+ sim.run(1.0) # One time step
1119
+
1120
+ final = sim.state['Env']
1121
+ assert round(final['sum'], 2) == 5.0, f"Adder failed: {final}"
1122
+ assert round(final['x'], 2) == 4.5, f"Decay failed: {final}"
1123
+ print("✅ test_registered_functions_in_composite passed:", final)
1124
+
1125
+
1126
+ def test_docker_process(core):
1127
+ state = {
1128
+ 'mass': 1.0,
1129
+ 'julia-process': {
1130
+ '_type': 'process',
1131
+ 'address': {
1132
+ 'protocol': 'docker',
1133
+ 'data': {
1134
+ 'image': 'julia-process:latest',
1135
+ 'port': 11111}},
1136
+ 'config': {
1137
+ 'rate': 0.005},
1138
+ 'inputs': {
1139
+ 'mass': ['mass']},
1140
+ 'outputs': {
1141
+ 'mass_delta': ['mass']},
1142
+ 'interval': 0.7}}
1143
+
1144
+ composite = Composite({
1145
+ 'state': state}, core=core)
1146
+
1147
+ composite.run(11.111)
1148
+
1149
+ assert composite.state['mass'] > 1.0
1150
+
1151
+
1152
+ def apply_non_negative(schema, current, update, top_schema, top_state, path, core):
1153
+ new_value = current + update
1154
+ return max(0, new_value)
1155
+
1156
+
1157
+ def apply_non_negative_array(schema, current, update, top_schema, top_state, path, core):
1158
+ def recursive_update(result_array, current_array, update_dict, index_path=()):
1159
+ if isinstance(update_dict, dict):
1160
+ for key, val in update_dict.items():
1161
+ recursive_update(result_array, current_array, val, index_path + (key,))
1162
+ else:
1163
+ if isinstance(current_array, np.ndarray):
1164
+ current_value = current_array[index_path]
1165
+ result_array[index_path] = np.maximum(0, current_value + update_dict)
1166
+ else:
1167
+ # Scalar fallback
1168
+ return np.maximum(0, current_array + update_dict)
1169
+
1170
+ if not isinstance(current, np.ndarray):
1171
+ if isinstance(update, dict):
1172
+ raise ValueError("Cannot apply dict update to scalar current")
1173
+ return np.maximum(0, current + update)
1174
+
1175
+ result = np.copy(current)
1176
+ recursive_update(result, current, update)
1177
+ return result
1178
+
1179
+
1180
+ def test_dfba_process(core):
1181
+ base_url = urlparse('http://localhost:22222')
1182
+ types_url = base_url._replace(path='/list-types')
1183
+ types = rest_get(types_url)
1184
+
1185
+ processes_url = base_url._replace(path='/list-processes')
1186
+ processes = rest_get(processes_url)
1187
+
1188
+ # TODO: import types from the server
1189
+ core.register('positive_float', {
1190
+ '_inherit': 'float',
1191
+ '_apply': apply_non_negative})
1192
+
1193
+ core.register('positive_array', {
1194
+ '_inherit': 'array',
1195
+ '_apply': apply_non_negative_array})
1196
+
1197
+ core.register('bounds', {
1198
+ 'lower': 'maybe[float]',
1199
+ 'upper': 'maybe[float]'})
1200
+
1201
+ dfba_name = 'spatio_flux.processes.DynamicFBA'
1202
+
1203
+ schema_url = base_url._replace(
1204
+ path=f'/process/{dfba_name}/config-schema')
1205
+ dfba_config_schema = rest_get(schema_url)
1206
+
1207
+ # import ipdb; ipdb.set_trace()
1208
+
1209
+ dfba_config = {
1210
+ 'model_file': 'textbook',
1211
+ 'substrate_update_reactions': {
1212
+ 'glucose': 'EX_glc__D_e',
1213
+ 'acetate': 'EX_ac_e'},
1214
+ 'kinetic_params': {
1215
+ 'glucose': (0.5, 1),
1216
+ 'acetate': (0.5, 2)},
1217
+ 'bounds': {
1218
+ 'EX_o2_e': {'lower': -2, 'upper': None},
1219
+ 'ATPM': {'lower': 1, 'upper': 1}}}
1220
+
1221
+ # assert core.check(
1222
+ # dfba_config_schema,
1223
+ # dfba_config)
1224
+
1225
+ biomass_id = 'biomass'
1226
+ substrates = dfba_config['substrate_update_reactions'].keys()
1227
+
1228
+ initial_biomass = 0.1
1229
+ initial_fields = {
1230
+ 'glucose': 2,
1231
+ 'acetate': 0,
1232
+ biomass_id: initial_biomass}
1233
+
1234
+ for substrate in substrates:
1235
+ if substrate not in initial_fields:
1236
+ initial_fields[substrate] = 10.0
1237
+
1238
+ path = ['fields']
1239
+
1240
+ state = {
1241
+ 'fields': initial_fields,
1242
+ 'rest-dfba': {
1243
+ '_type': 'process',
1244
+ 'address': {
1245
+ 'protocol': 'rest',
1246
+ 'data': {
1247
+ 'process': dfba_name,
1248
+ 'host': 'localhost',
1249
+ 'port': 22222}},
1250
+ 'config': dfba_config,
1251
+ 'inputs': {
1252
+ 'substrates': {
1253
+ substrate: path + [substrate]
1254
+ for substrate in substrates},
1255
+ 'biomass': path + [biomass_id]},
1256
+ 'outputs': {
1257
+ 'substrates': {
1258
+ substrate: path + [substrate]
1259
+ for substrate in substrates},
1260
+ 'biomass': path + [biomass_id]},
1261
+ 'interval': 0.7}}
1262
+
1263
+ composite = Composite({
1264
+ 'state': state}, core=core)
1265
+
1266
+ composite.run(11.111)
1267
+
1268
+ assert composite.state['fields'][biomass_id] > initial_biomass
1269
+
1270
+
1271
+ def test_rest_process(core):
1272
+ state = {
1273
+ 'mass': 1.0,
1274
+ 'rest-process': {
1275
+ '_type': 'process',
1276
+ 'address': {
1277
+ 'protocol': 'rest',
1278
+ 'data': {
1279
+ 'process': 'grow',
1280
+ 'host': 'localhost',
1281
+ 'port': 22222}},
1282
+ 'config': {
1283
+ 'rate': 0.005},
1284
+ 'inputs': {
1285
+ 'mass': ['mass']},
1286
+ 'outputs': {
1287
+ 'mass_delta': ['mass']},
1288
+ 'interval': 0.7}}
1289
+
1290
+ composite = Composite({
1291
+ 'state': state}, core=core)
1292
+
1293
+ composite.run(11.111)
1294
+
1295
+ assert composite.state['mass'] > 1.0
1296
+
1297
+
1298
+ if __name__ == '__main__':
1299
+ core = ProcessTypes()
1300
+ core = register_types(core)
1301
+
1302
+ test_default_config(core)
1303
+ test_default_process_state(core)
1304
+ test_merge_collections(core)
1305
+ test_process(core)
1306
+ test_composite(core)
1307
+ test_infer(core)
1308
+ test_step_initialization(core)
1309
+ test_dependencies(core)
1310
+ test_emitter(core)
1311
+ test_union_tree(core)
1312
+
1313
+ test_gillespie_composite(core)
1314
+ test_run_process(core)
1315
+ test_nested_wires(core)
1316
+ test_parameter_scan(core)
1317
+ test_shared_steps(core)
1318
+
1319
+ test_stochastic_deterministic_composite(core)
1320
+ test_merge_schema(core)
1321
+ test_grow_divide(core)
1322
+ test_star_update(core)
1323
+ test_match_star_path(core)
1324
+ test_function_wrappers(core)
1325
+ test_registered_functions_in_composite(core)
1326
+ test_update_removal(core)
1327
+ test_docker_process(core)
1328
+
1329
+ test_rest_process(core)
1330
+ test_dfba_process(core)