process-bigraph 1.3.1__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.
- {process_bigraph-1.3.1/process_bigraph.egg-info → process_bigraph-1.4.2}/PKG-INFO +9 -1
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/composite.py +137 -31
- process_bigraph-1.4.2/process_bigraph/protocols/ray.py +320 -0
- process_bigraph-1.4.2/process_bigraph/server/__init__.py +0 -0
- process_bigraph-1.4.2/process_bigraph/server/rest.py +91 -0
- process_bigraph-1.4.2/process_bigraph/server/start.py +46 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2/process_bigraph.egg-info}/PKG-INFO +9 -1
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph.egg-info/SOURCES.txt +4 -0
- process_bigraph-1.4.2/process_bigraph.egg-info/requires.txt +16 -0
- process_bigraph-1.4.2/pyproject.toml +64 -0
- process_bigraph-1.3.1/process_bigraph.egg-info/requires.txt +0 -6
- process_bigraph-1.3.1/pyproject.toml +0 -37
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/AUTHORS.md +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/LICENSE +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/README.md +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/__init__.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/emitter.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/nextflow.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/math_expression.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/protocols/socket.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/run.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/units.py +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.3.1 → process_bigraph-1.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,6 +12,14 @@ Requires-Dist: pint>=0.24.4
|
|
|
12
12
|
Requires-Dist: scipy>=1.8
|
|
13
13
|
Requires-Dist: pandas
|
|
14
14
|
Requires-Dist: sympy
|
|
15
|
+
Provides-Extra: ray
|
|
16
|
+
Requires-Dist: ray>=2.10; extra == "ray"
|
|
17
|
+
Provides-Extra: server-rest
|
|
18
|
+
Requires-Dist: fastapi>=0.116.1; extra == "server-rest"
|
|
19
|
+
Requires-Dist: fastapi-utils>=0.8.0; extra == "server-rest"
|
|
20
|
+
Requires-Dist: uvicorn>=0.35.0; extra == "server-rest"
|
|
21
|
+
Requires-Dist: fire; extra == "server-rest"
|
|
22
|
+
Requires-Dist: typing-inspect>=0.9.0; extra == "server-rest"
|
|
15
23
|
Dynamic: license-file
|
|
16
24
|
|
|
17
25
|
# Process-Bigraph
|
|
@@ -530,6 +530,21 @@ def build_step_network(steps):
|
|
|
530
530
|
if producer not in node['after']:
|
|
531
531
|
node['before'].add(producer)
|
|
532
532
|
|
|
533
|
+
# Precompute each step's direct dependents (= the set of steps
|
|
534
|
+
# that consume something this step writes). ``find_downstream``
|
|
535
|
+
# was reconstructing this every tick by walking
|
|
536
|
+
# output_paths × explode_path × nodes lookup; doing it once at
|
|
537
|
+
# graph-build time makes ``find_downstream`` a plain BFS over a
|
|
538
|
+
# cached adjacency list.
|
|
539
|
+
for step_key, ancestor in ancestors.items():
|
|
540
|
+
direct = set()
|
|
541
|
+
for output_path in ancestor['output_paths'] or []:
|
|
542
|
+
for subpath in explode_path(output_path):
|
|
543
|
+
node = nodes.get(subpath)
|
|
544
|
+
if node is not None:
|
|
545
|
+
direct.update(node['after'])
|
|
546
|
+
ancestor['_direct_dependents'] = frozenset(direct)
|
|
547
|
+
|
|
533
548
|
return ancestors, nodes
|
|
534
549
|
|
|
535
550
|
|
|
@@ -552,36 +567,25 @@ def build_trigger_state(nodes, paths):
|
|
|
552
567
|
|
|
553
568
|
def find_downstream(steps, nodes, upstream):
|
|
554
569
|
"""
|
|
555
|
-
Given a set of updated steps, identify all downstream steps that
|
|
556
|
-
|
|
557
|
-
Args:
|
|
558
|
-
steps: Step metadata with input/output info.
|
|
559
|
-
nodes: Dependency graph.
|
|
560
|
-
upstream: Initial set of triggered step paths.
|
|
570
|
+
Given a set of updated steps, identify all downstream steps that
|
|
571
|
+
depend on them — directly or transitively.
|
|
561
572
|
|
|
562
|
-
|
|
563
|
-
|
|
573
|
+
BFS over the cached ``_direct_dependents`` adjacency that
|
|
574
|
+
``build_step_network`` precomputes per step. ``nodes`` is no
|
|
575
|
+
longer consulted on the hot path (kept in the signature for
|
|
576
|
+
backward compatibility with callers that still pass it).
|
|
564
577
|
"""
|
|
565
578
|
downstream = set(upstream)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
for
|
|
573
|
-
if
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
step_outputs = [] # Ensure step_outputs is always an iterable
|
|
577
|
-
for output in step_outputs:
|
|
578
|
-
for subpath in explode_path(output):
|
|
579
|
-
if subpath in nodes:
|
|
580
|
-
for dependent in nodes[subpath]['after']:
|
|
581
|
-
down.add(dependent)
|
|
582
|
-
visited.add(step_path)
|
|
583
|
-
downstream |= down
|
|
584
|
-
|
|
579
|
+
queue = list(upstream)
|
|
580
|
+
while queue:
|
|
581
|
+
step_path = queue.pop()
|
|
582
|
+
meta = steps.get(step_path)
|
|
583
|
+
if meta is None:
|
|
584
|
+
continue
|
|
585
|
+
for dep in meta.get('_direct_dependents', ()):
|
|
586
|
+
if dep not in downstream:
|
|
587
|
+
downstream.add(dep)
|
|
588
|
+
queue.append(dep)
|
|
585
589
|
return downstream
|
|
586
590
|
|
|
587
591
|
|
|
@@ -1281,6 +1285,14 @@ class Composite(Process):
|
|
|
1281
1285
|
# step hot path releases the GIL (numpy / scipy / numba / C
|
|
1282
1286
|
# extensions: yes; pure-Python loops: no).
|
|
1283
1287
|
'parallel_steps': 'boolean{false}',
|
|
1288
|
+
# Time-scheduled processes: when True, the per-tick `run` loop
|
|
1289
|
+
# fans out invocations across a thread pool. Right tool when
|
|
1290
|
+
# processes' update() releases the GIL (network I/O, numpy /
|
|
1291
|
+
# scipy / C extensions, Ray actor calls). Wrong tool when
|
|
1292
|
+
# update() is mostly pure Python — threads will just trade the
|
|
1293
|
+
# GIL and you'll see no speedup. The thread pool is shared with
|
|
1294
|
+
# parallel_steps and bounded by parallel_workers.
|
|
1295
|
+
'parallel_processes': 'boolean{false}',
|
|
1284
1296
|
'parallel_workers': 'maybe[integer]',
|
|
1285
1297
|
# When True, ``initialize`` trusts ``state`` as-is and skips the
|
|
1286
1298
|
# per-process ``link_state`` + ``combine`` pass that normally
|
|
@@ -1401,6 +1413,7 @@ class Composite(Process):
|
|
|
1401
1413
|
# field in interface_schema for the rationale (threading vs.
|
|
1402
1414
|
# multiprocessing for inner parallelism).
|
|
1403
1415
|
self._parallel_steps = bool(self.config.get('parallel_steps', False))
|
|
1416
|
+
self._parallel_processes = bool(self.config.get('parallel_processes', False))
|
|
1404
1417
|
self._parallel_workers = self.config.get('parallel_workers')
|
|
1405
1418
|
self._step_executor = None # lazy: created on first run_steps need
|
|
1406
1419
|
|
|
@@ -2359,10 +2372,14 @@ class Composite(Process):
|
|
|
2359
2372
|
full_step = math.inf
|
|
2360
2373
|
|
|
2361
2374
|
# Run each process and compute the minimum time step that advances simulation
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2375
|
+
if self._parallel_processes and len(self.process_paths) > 1:
|
|
2376
|
+
full_step = self._run_processes_layer_parallel(
|
|
2377
|
+
end_time, full_step, force_complete)
|
|
2378
|
+
else:
|
|
2379
|
+
for path in self.process_paths:
|
|
2380
|
+
process = get_path(self.state, path)
|
|
2381
|
+
full_step = self.run_process(
|
|
2382
|
+
path, process, end_time, full_step, force_complete)
|
|
2366
2383
|
|
|
2367
2384
|
if full_step == math.inf:
|
|
2368
2385
|
# No process ran — jump to the next scheduled process time
|
|
@@ -2492,6 +2509,95 @@ class Composite(Process):
|
|
|
2492
2509
|
|
|
2493
2510
|
return full_step
|
|
2494
2511
|
|
|
2512
|
+
def _run_processes_layer_parallel(
|
|
2513
|
+
self,
|
|
2514
|
+
end_time: float,
|
|
2515
|
+
full_step: float,
|
|
2516
|
+
force_complete: bool,
|
|
2517
|
+
) -> float:
|
|
2518
|
+
"""Parallel analog of the per-tick `for path in self.process_paths`
|
|
2519
|
+
loop. Splits run_process into a schedule pass (sequential, cheap)
|
|
2520
|
+
and an invoke pass (parallelized over the same ThreadPoolExecutor
|
|
2521
|
+
used by parallel_steps).
|
|
2522
|
+
|
|
2523
|
+
Semantics are identical to the serial loop: same schedule decisions,
|
|
2524
|
+
same Defers stored at the same self.front[path] slots. Only the
|
|
2525
|
+
order in which `instance.invoke()` calls happen changes.
|
|
2526
|
+
|
|
2527
|
+
Safe when:
|
|
2528
|
+
- `instance.update()` releases the GIL on its hot path (network
|
|
2529
|
+
I/O, Ray actor calls, numpy/scipy/C extensions).
|
|
2530
|
+
- Processes don't share mutable state across each other.
|
|
2531
|
+
|
|
2532
|
+
Not safe when processes share mutable state or hold global locks.
|
|
2533
|
+
Like `parallel_steps`, this is opt-in for that reason.
|
|
2534
|
+
"""
|
|
2535
|
+
# ---- schedule pass: decide which processes are due now ---------- #
|
|
2536
|
+
due = [] # (path, process_dict, state, future_time, process_interval)
|
|
2537
|
+
for path in self.process_paths:
|
|
2538
|
+
process = get_path(self.state, path)
|
|
2539
|
+
if path not in self.front:
|
|
2540
|
+
self.front[path] = empty_front(self.state['global_time'])
|
|
2541
|
+
process_time = self.front[path]['time']
|
|
2542
|
+
|
|
2543
|
+
if process_time <= self.state['global_time']:
|
|
2544
|
+
if 'future' in self.front[path]:
|
|
2545
|
+
future_front = self.front[path].pop('future')
|
|
2546
|
+
process_interval = future_front['interval']
|
|
2547
|
+
state = future_front['state']
|
|
2548
|
+
else:
|
|
2549
|
+
state = self._cached_view(path)
|
|
2550
|
+
state_interval = process['interval']
|
|
2551
|
+
process_interval = process['instance'].calculate_timestep(
|
|
2552
|
+
state_interval, state)
|
|
2553
|
+
process['interval'] = process_interval
|
|
2554
|
+
|
|
2555
|
+
future = (
|
|
2556
|
+
min(process_time + process_interval, end_time)
|
|
2557
|
+
if force_complete
|
|
2558
|
+
else process_time + process_interval
|
|
2559
|
+
)
|
|
2560
|
+
if self.global_time_precision:
|
|
2561
|
+
future = round(future, self.global_time_precision)
|
|
2562
|
+
|
|
2563
|
+
interval = future - self.state['global_time']
|
|
2564
|
+
if interval < full_step:
|
|
2565
|
+
full_step = interval
|
|
2566
|
+
|
|
2567
|
+
if future <= end_time:
|
|
2568
|
+
due.append((path, process, state, future, process_interval))
|
|
2569
|
+
else:
|
|
2570
|
+
process_delay = process_time - self.state['global_time']
|
|
2571
|
+
if process_delay < full_step:
|
|
2572
|
+
full_step = process_delay
|
|
2573
|
+
|
|
2574
|
+
if not due:
|
|
2575
|
+
return full_step
|
|
2576
|
+
|
|
2577
|
+
# ---- invoke pass: parallel via the shared step executor --------- #
|
|
2578
|
+
# process_update calls instance.invoke(state, interval); that's the
|
|
2579
|
+
# only blocking part. Note: process_update accumulates into
|
|
2580
|
+
# self.process_update_time — there's a benign race on that counter
|
|
2581
|
+
# under contention, but it's only a metric (not correctness).
|
|
2582
|
+
def _invoke(item):
|
|
2583
|
+
path, process, state, _future, process_interval = item
|
|
2584
|
+
return path, self.process_update(
|
|
2585
|
+
path, process, state, process_interval, already_clean=True)
|
|
2586
|
+
|
|
2587
|
+
if len(due) > 1:
|
|
2588
|
+
pool = self._get_step_executor(len(due))
|
|
2589
|
+
results = list(pool.map(_invoke, due))
|
|
2590
|
+
else:
|
|
2591
|
+
results = [_invoke(due[0])]
|
|
2592
|
+
|
|
2593
|
+
# ---- bookkeep pass: store Defers + update self.front time slots - #
|
|
2594
|
+
future_by_path = {p: fut for (p, _, _, fut, _) in due}
|
|
2595
|
+
for path, defer in results:
|
|
2596
|
+
self.front[path]['time'] = future_by_path[path]
|
|
2597
|
+
self.front[path]['update'] = defer
|
|
2598
|
+
|
|
2599
|
+
return full_step
|
|
2600
|
+
|
|
2495
2601
|
def process_update(
|
|
2496
2602
|
self,
|
|
2497
2603
|
path: Union[str, Tuple[str, ...]],
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RayProcess — distributed transport backed by Ray actors.
|
|
3
|
+
|
|
4
|
+
Pair with the ``parallel_processes=True`` flag on Composite so the orchestrator
|
|
5
|
+
can dispatch per-step ``update()`` calls concurrently — that's what turns N
|
|
6
|
+
clients talking to a Ray actor pool into N parallel solves.
|
|
7
|
+
|
|
8
|
+
Install with the optional ray extra::
|
|
9
|
+
|
|
10
|
+
pip install process-bigraph[ray]
|
|
11
|
+
|
|
12
|
+
Architecture: pooled actors
|
|
13
|
+
---------------------------
|
|
14
|
+
Each (process_class, process_config) pair backs a fixed pool of N Ray actors
|
|
15
|
+
(default N = ncpu). Every RayProcess client is round-robin assigned to one
|
|
16
|
+
pool actor; many "logical" processes share the same physical worker. This
|
|
17
|
+
bounds memory at O(ncpu) underlying-process instances instead of O(clients),
|
|
18
|
+
and bounds spawn cost at ncpu actors regardless of how many clients the
|
|
19
|
+
orchestrator wires up.
|
|
20
|
+
|
|
21
|
+
Why pooled, not actor-per-client:
|
|
22
|
+
- One actor per cell at moderate grids (e.g. 256 cells with a 150 MB cobra
|
|
23
|
+
Model each) trivially OOMs a typical workstation.
|
|
24
|
+
- Per-actor spawn cost (process fork + module import + heavy state init)
|
|
25
|
+
is 50-500 ms; paying that 256× per run is minutes of overhead.
|
|
26
|
+
- Ray actor methods are serialized by default — concurrent calls to one
|
|
27
|
+
actor are queued, so non-thread-safe state inside the underlying Process
|
|
28
|
+
isn't a concern.
|
|
29
|
+
|
|
30
|
+
Pool lifecycle:
|
|
31
|
+
- Pools live for the lifetime of the Python interpreter by default.
|
|
32
|
+
Subsequent ``Composite`` runs re-use the same actors — no re-spawn,
|
|
33
|
+
no model reload.
|
|
34
|
+
- Call ``shutdown_pools()`` to tear them down explicitly (useful in tests).
|
|
35
|
+
- ``RayProcess.end()`` is a no-op — clients come and go but actors persist.
|
|
36
|
+
|
|
37
|
+
Usage
|
|
38
|
+
-----
|
|
39
|
+
1. Register the underlying Process classes once at startup so each Ray
|
|
40
|
+
worker can resolve them by name::
|
|
41
|
+
|
|
42
|
+
from process_bigraph.protocols.ray import register_process_class
|
|
43
|
+
from my_pkg.processes import MyProcess
|
|
44
|
+
register_process_class("MyProcess", MyProcess)
|
|
45
|
+
|
|
46
|
+
2. Reference RayProcess in your composite spec::
|
|
47
|
+
|
|
48
|
+
"worker_0": {
|
|
49
|
+
"_type": "process",
|
|
50
|
+
"address": "local:RayProcess",
|
|
51
|
+
"config": {
|
|
52
|
+
"process_class": "MyProcess",
|
|
53
|
+
"process_config": { ... MyProcess's config ... },
|
|
54
|
+
# optional: cap pool size (default = os.cpu_count())
|
|
55
|
+
"pool_size": 8,
|
|
56
|
+
},
|
|
57
|
+
"inputs": { ... },
|
|
58
|
+
"outputs": { ... },
|
|
59
|
+
"interval": 0.1,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
3. Pass ``parallel_processes=True`` to Composite so the orchestrator dispatches
|
|
63
|
+
the per-step ``update()`` calls concurrently.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
from __future__ import annotations
|
|
67
|
+
|
|
68
|
+
import os
|
|
69
|
+
import json
|
|
70
|
+
import hashlib
|
|
71
|
+
from typing import Any, Dict, List, Type, Optional
|
|
72
|
+
|
|
73
|
+
# Ray is optional. We let the module import even when ray isn't installed
|
|
74
|
+
# (so package scanners like discover_packages don't trip), and only raise
|
|
75
|
+
# a helpful error when something tries to actually use it.
|
|
76
|
+
try:
|
|
77
|
+
import ray
|
|
78
|
+
_RAY_IMPORT_ERROR: Optional[ImportError] = None
|
|
79
|
+
except ImportError as _e: # pragma: no cover
|
|
80
|
+
ray = None # type: ignore[assignment]
|
|
81
|
+
_RAY_IMPORT_ERROR = _e
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _require_ray() -> None:
|
|
85
|
+
"""Guard for code paths that need ray. Raises a clear install hint."""
|
|
86
|
+
if ray is None:
|
|
87
|
+
raise ImportError(
|
|
88
|
+
"process_bigraph.protocols.ray requires the optional `ray` "
|
|
89
|
+
"dependency. Install with: pip install process-bigraph[ray]"
|
|
90
|
+
) from _RAY_IMPORT_ERROR
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
from process_bigraph import Process
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Process class registry.
|
|
98
|
+
# Ray pickles this into each new actor at spawn so workers don't need to
|
|
99
|
+
# import the same modules in their startup script.
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
_PROCESS_REGISTRY: Dict[str, Type[Process]] = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def register_process_class(name: str, cls: Type[Process]) -> None:
|
|
105
|
+
"""Register a Process class so RayProcess can resolve it by name."""
|
|
106
|
+
_PROCESS_REGISTRY[name] = cls
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_registry() -> Dict[str, Type[Process]]:
|
|
110
|
+
return dict(_PROCESS_REGISTRY)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Ray actor — one per pool slot. Holds a single Process instance.
|
|
115
|
+
#
|
|
116
|
+
# Declared as a plain class at module load time so this file imports cleanly
|
|
117
|
+
# without ray installed. ``ray.remote(...)`` is applied lazily on first use
|
|
118
|
+
# (cached) inside ``_remote_actor_class()``.
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
class _ProcessActor:
|
|
121
|
+
def __init__(self, registry: Dict[str, Type[Process]],
|
|
122
|
+
class_name: str, config: dict):
|
|
123
|
+
for k, v in registry.items():
|
|
124
|
+
_PROCESS_REGISTRY[k] = v
|
|
125
|
+
cls = _PROCESS_REGISTRY[class_name]
|
|
126
|
+
from process_bigraph import allocate_core
|
|
127
|
+
self.instance = cls(config, core=allocate_core())
|
|
128
|
+
|
|
129
|
+
def inputs(self):
|
|
130
|
+
return self.instance.inputs()
|
|
131
|
+
|
|
132
|
+
def outputs(self):
|
|
133
|
+
return self.instance.outputs()
|
|
134
|
+
|
|
135
|
+
def update(self, state: dict, interval: float):
|
|
136
|
+
return self.instance.update(state, interval)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_REMOTE_ACTOR_CLASS = None # cached ray.remote(_ProcessActor)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _remote_actor_class():
|
|
143
|
+
"""Return the ray-remote-wrapped _ProcessActor, building it on first call."""
|
|
144
|
+
global _REMOTE_ACTOR_CLASS
|
|
145
|
+
if _REMOTE_ACTOR_CLASS is None:
|
|
146
|
+
_require_ray()
|
|
147
|
+
_REMOTE_ACTOR_CLASS = ray.remote(_ProcessActor)
|
|
148
|
+
return _REMOTE_ACTOR_CLASS
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Actor pool. One pool per (process_class, process_config). Persistent across
|
|
153
|
+
# RayProcess instances and across simulation runs.
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
class _ActorPool:
|
|
156
|
+
def __init__(self, class_name: str, config: dict, n_workers: int):
|
|
157
|
+
registry = get_registry()
|
|
158
|
+
actor_cls = _remote_actor_class()
|
|
159
|
+
# Spawn all actors concurrently — actor.remote() returns immediately;
|
|
160
|
+
# we don't ray.get on the constructor. The first .inputs.remote() call
|
|
161
|
+
# implicitly waits for the actor to be ready.
|
|
162
|
+
self.actors = [
|
|
163
|
+
actor_cls.remote(registry, class_name, config)
|
|
164
|
+
for _ in range(n_workers)
|
|
165
|
+
]
|
|
166
|
+
self._next = 0
|
|
167
|
+
|
|
168
|
+
def assign(self):
|
|
169
|
+
actor = self.actors[self._next % len(self.actors)]
|
|
170
|
+
self._next += 1
|
|
171
|
+
return actor
|
|
172
|
+
|
|
173
|
+
def shutdown(self):
|
|
174
|
+
for a in self.actors:
|
|
175
|
+
try:
|
|
176
|
+
ray.kill(a)
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
self.actors = []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Module-level pool registry, keyed by (class_name, config_hash).
|
|
183
|
+
_POOLS: Dict[str, _ActorPool] = {}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _config_hash(config: Any) -> str:
|
|
187
|
+
"""Stable hash of a process_config dict for pool keying."""
|
|
188
|
+
try:
|
|
189
|
+
s = json.dumps(config, sort_keys=True, default=repr)
|
|
190
|
+
except TypeError:
|
|
191
|
+
s = repr(config)
|
|
192
|
+
return hashlib.sha1(s.encode()).hexdigest()[:12]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _pool_key(class_name: str, config: Any) -> str:
|
|
196
|
+
return f"{class_name}:{_config_hash(config)}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _get_or_make_pool(class_name: str, config: dict,
|
|
200
|
+
n_workers: Optional[int]) -> _ActorPool:
|
|
201
|
+
key = _pool_key(class_name, config)
|
|
202
|
+
pool = _POOLS.get(key)
|
|
203
|
+
if pool is None:
|
|
204
|
+
if n_workers is None:
|
|
205
|
+
n_workers = max(1, os.cpu_count() or 4)
|
|
206
|
+
pool = _ActorPool(class_name, config, n_workers)
|
|
207
|
+
_POOLS[key] = pool
|
|
208
|
+
return pool
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def shutdown_pools() -> None:
|
|
212
|
+
"""Tear down all actor pools. Call at program exit / between test runs."""
|
|
213
|
+
for pool in list(_POOLS.values()):
|
|
214
|
+
pool.shutdown()
|
|
215
|
+
_POOLS.clear()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def pool_stats() -> List[dict]:
|
|
219
|
+
"""Diagnostic: list all live pools."""
|
|
220
|
+
return [
|
|
221
|
+
{"key": k, "n_actors": len(p.actors)}
|
|
222
|
+
for k, p in _POOLS.items()
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Client — what the orchestrator sees as a Process.
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
class RayProcess(Process):
|
|
230
|
+
"""A Process whose update() runs on a pooled remote Ray actor.
|
|
231
|
+
|
|
232
|
+
Config:
|
|
233
|
+
process_class : str
|
|
234
|
+
Name of a Process subclass registered via register_process_class().
|
|
235
|
+
process_config : dict
|
|
236
|
+
Config dict passed to the underlying Process subclass.
|
|
237
|
+
pool_size : int (optional)
|
|
238
|
+
Number of actors in the pool for this (class, config). Defaults
|
|
239
|
+
to os.cpu_count(). The first RayProcess instantiation for a
|
|
240
|
+
given (class, config) sizes the pool — subsequent instances
|
|
241
|
+
reuse the existing pool and ignore this field.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
config_schema = {
|
|
245
|
+
"process_class": "string",
|
|
246
|
+
"process_config": "node",
|
|
247
|
+
"pool_size": "maybe[integer]",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def initialize(self, config):
|
|
251
|
+
_require_ray()
|
|
252
|
+
if not ray.is_initialized():
|
|
253
|
+
ray.init(ignore_reinit_error=True, log_to_driver=False)
|
|
254
|
+
|
|
255
|
+
class_name = config["process_class"]
|
|
256
|
+
if class_name not in _PROCESS_REGISTRY:
|
|
257
|
+
raise KeyError(
|
|
258
|
+
f"Process class {class_name!r} not in RayProcess registry. "
|
|
259
|
+
f"Call register_process_class({class_name!r}, <cls>) first."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
pool = _get_or_make_pool(
|
|
263
|
+
class_name,
|
|
264
|
+
config["process_config"],
|
|
265
|
+
config.get("pool_size"),
|
|
266
|
+
)
|
|
267
|
+
self.actor = pool.assign()
|
|
268
|
+
|
|
269
|
+
# Cache port schemas — one round-trip per client at init. (We could
|
|
270
|
+
# cache per-pool to drop this, but it's a single call and the result
|
|
271
|
+
# could in principle differ if the underlying Process introspects
|
|
272
|
+
# config-specific port shapes.)
|
|
273
|
+
self._inputs = ray.get(self.actor.inputs.remote())
|
|
274
|
+
self._outputs = ray.get(self.actor.outputs.remote())
|
|
275
|
+
|
|
276
|
+
def inputs(self):
|
|
277
|
+
return self._inputs
|
|
278
|
+
|
|
279
|
+
def outputs(self):
|
|
280
|
+
return self._outputs
|
|
281
|
+
|
|
282
|
+
def update(self, state, interval):
|
|
283
|
+
# Blocking get: releases the GIL while the actor runs. ParallelComposite
|
|
284
|
+
# gives us N concurrent in-flight calls = N actors busy in parallel.
|
|
285
|
+
return ray.get(self.actor.update.remote(state, float(interval)))
|
|
286
|
+
|
|
287
|
+
def end(self):
|
|
288
|
+
# Pool actors persist across RayProcess instances — DON'T kill them
|
|
289
|
+
# here. Use shutdown_pools() to tear down explicitly.
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Smoke test — wraps IncreaseProcess (a built-in toy Process) in a Ray pool
|
|
295
|
+
# and runs a few updates. Useful as both a sanity check and an example.
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
from process_bigraph import allocate_core
|
|
299
|
+
from process_bigraph.processes.examples import IncreaseProcess
|
|
300
|
+
|
|
301
|
+
register_process_class("IncreaseProcess", IncreaseProcess)
|
|
302
|
+
|
|
303
|
+
proc_a = RayProcess(
|
|
304
|
+
{"process_class": "IncreaseProcess",
|
|
305
|
+
"process_config": {"rate": 0.5},
|
|
306
|
+
"pool_size": 2},
|
|
307
|
+
core=allocate_core(),
|
|
308
|
+
)
|
|
309
|
+
proc_b = RayProcess(
|
|
310
|
+
{"process_class": "IncreaseProcess",
|
|
311
|
+
"process_config": {"rate": 0.5},
|
|
312
|
+
"pool_size": 2}, # ignored — pool already exists
|
|
313
|
+
core=allocate_core(),
|
|
314
|
+
)
|
|
315
|
+
print("pool stats:", pool_stats())
|
|
316
|
+
for proc, label in [(proc_a, "A"), (proc_b, "B")]:
|
|
317
|
+
upd = proc.update({"level": 4.0}, interval=1.0)
|
|
318
|
+
print(f"{label} update :", upd)
|
|
319
|
+
shutdown_pools()
|
|
320
|
+
print("after shutdown:", pool_stats())
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Any, Dict, Union
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi_utils.cbv import cbv
|
|
6
|
+
from fastapi_utils.inferring_router import InferringRouter
|
|
7
|
+
|
|
8
|
+
from bigraph_schema import Edge
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_router(core):
|
|
12
|
+
router = InferringRouter()
|
|
13
|
+
processes = {}
|
|
14
|
+
|
|
15
|
+
@cbv(router)
|
|
16
|
+
class ProcessRouter():
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.core = core
|
|
19
|
+
self.processes = processes
|
|
20
|
+
|
|
21
|
+
def find_process_class(self, process):
|
|
22
|
+
return self.core.link_registry.get(process, Edge)
|
|
23
|
+
|
|
24
|
+
@router.get('/import-types')
|
|
25
|
+
def get_import_types(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@router.get('/type-packages')
|
|
29
|
+
def get_type_packages(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@router.get('/list-types')
|
|
33
|
+
def get_list_types(self):
|
|
34
|
+
return list(self.core.registry.keys())
|
|
35
|
+
|
|
36
|
+
@router.get('/list-processes')
|
|
37
|
+
def get_list_processes(self):
|
|
38
|
+
return list(self.core.link_registry.keys())
|
|
39
|
+
|
|
40
|
+
@router.get('/process/{process}/config-schema')
|
|
41
|
+
def get_config_schema(self, process: str):
|
|
42
|
+
process_class = self.find_process_class(process)
|
|
43
|
+
if process_class is None:
|
|
44
|
+
return {'process-not-found': 'true'}
|
|
45
|
+
else:
|
|
46
|
+
return process_class.config_schema
|
|
47
|
+
|
|
48
|
+
@router.post('/process/{process}/initialize')
|
|
49
|
+
def post_initialize(self, process: str, config: dict):
|
|
50
|
+
process_id = uuid.uuid4()
|
|
51
|
+
process_class = self.find_process_class(process)
|
|
52
|
+
process_instance = process_class(
|
|
53
|
+
config,
|
|
54
|
+
core=self.core)
|
|
55
|
+
self.processes[str(process_id)] = process_instance
|
|
56
|
+
|
|
57
|
+
print(self.processes)
|
|
58
|
+
print(process_id)
|
|
59
|
+
return process_id
|
|
60
|
+
|
|
61
|
+
@router.get('/process/{process}/inputs/{process_id}')
|
|
62
|
+
def get_inputs(self, process: str, process_id: str):
|
|
63
|
+
print(self.processes)
|
|
64
|
+
return self.processes[process_id].inputs()
|
|
65
|
+
|
|
66
|
+
@router.get('/process/{process}/outputs/{process_id}')
|
|
67
|
+
def get_outputs(self, process: str, process_id: str):
|
|
68
|
+
return self.processes[process_id].outputs()
|
|
69
|
+
|
|
70
|
+
@router.post('/process/{process}/update/{process_id}')
|
|
71
|
+
def post_update(self, process: str, process_id: str, data: dict):
|
|
72
|
+
state = data['state']
|
|
73
|
+
interval = data['interval']
|
|
74
|
+
|
|
75
|
+
return self.processes[process_id].update(
|
|
76
|
+
state,
|
|
77
|
+
interval)
|
|
78
|
+
|
|
79
|
+
@router.post('/process/{process}/end/{process_id}')
|
|
80
|
+
def post_end(self, process: str, process_id: str):
|
|
81
|
+
del self.processes[process_id]
|
|
82
|
+
|
|
83
|
+
return router
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def start_server(core):
|
|
87
|
+
app = FastAPI()
|
|
88
|
+
router = make_router(core)
|
|
89
|
+
app.include_router(router)
|
|
90
|
+
|
|
91
|
+
return app
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for hosting process-bigraph processes over a network protocol.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m process_bigraph.server.start --host 0.0.0.0 --port 22222
|
|
6
|
+
python -m process_bigraph.server.start --protocol rest --port 22222
|
|
7
|
+
|
|
8
|
+
Currently supported protocols: rest (FastAPI/uvicorn). Future: grpc, etc.
|
|
9
|
+
Each protocol's server-side dependencies are gated behind an optional extra.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import fire
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def start(host: str = "0.0.0.0", port: int = 22222, protocol: str = "rest"):
|
|
16
|
+
"""Start a process-bigraph protocol server.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
host: bind address (default 0.0.0.0)
|
|
20
|
+
port: bind port (default 22222)
|
|
21
|
+
protocol: which transport to expose (default 'rest'). Each protocol
|
|
22
|
+
has its own server-side dependency extra:
|
|
23
|
+
rest: pip install process-bigraph[server-rest]
|
|
24
|
+
"""
|
|
25
|
+
from process_bigraph import allocate_core
|
|
26
|
+
|
|
27
|
+
if protocol == "rest":
|
|
28
|
+
try:
|
|
29
|
+
import uvicorn
|
|
30
|
+
from process_bigraph.server.rest import start_server
|
|
31
|
+
except ImportError as e:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"REST server requires the [server-rest] extra: "
|
|
34
|
+
"pip install process-bigraph[server-rest]"
|
|
35
|
+
) from e
|
|
36
|
+
core = allocate_core()
|
|
37
|
+
app = start_server(core)
|
|
38
|
+
uvicorn.run(app, host=host, port=port)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Unknown protocol {protocol!r}. Supported: rest"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
fire.Fire(start)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,6 +12,14 @@ Requires-Dist: pint>=0.24.4
|
|
|
12
12
|
Requires-Dist: scipy>=1.8
|
|
13
13
|
Requires-Dist: pandas
|
|
14
14
|
Requires-Dist: sympy
|
|
15
|
+
Provides-Extra: ray
|
|
16
|
+
Requires-Dist: ray>=2.10; extra == "ray"
|
|
17
|
+
Provides-Extra: server-rest
|
|
18
|
+
Requires-Dist: fastapi>=0.116.1; extra == "server-rest"
|
|
19
|
+
Requires-Dist: fastapi-utils>=0.8.0; extra == "server-rest"
|
|
20
|
+
Requires-Dist: uvicorn>=0.35.0; extra == "server-rest"
|
|
21
|
+
Requires-Dist: fire; extra == "server-rest"
|
|
22
|
+
Requires-Dist: typing-inspect>=0.9.0; extra == "server-rest"
|
|
15
23
|
Dynamic: license-file
|
|
16
24
|
|
|
17
25
|
# Process-Bigraph
|
|
@@ -27,7 +27,11 @@ process_bigraph/processes/parameter_scan.py
|
|
|
27
27
|
process_bigraph/processes/reaction.py
|
|
28
28
|
process_bigraph/protocols/__init__.py
|
|
29
29
|
process_bigraph/protocols/parallel.py
|
|
30
|
+
process_bigraph/protocols/ray.py
|
|
30
31
|
process_bigraph/protocols/rest.py
|
|
31
32
|
process_bigraph/protocols/socket.py
|
|
33
|
+
process_bigraph/server/__init__.py
|
|
34
|
+
process_bigraph/server/rest.py
|
|
35
|
+
process_bigraph/server/start.py
|
|
32
36
|
process_bigraph/types/__init__.py
|
|
33
37
|
process_bigraph/types/process.py
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "process-bigraph"
|
|
7
|
+
version = "1.4.2"
|
|
8
|
+
description = "protocol and execution for compositional systems biology"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"bigraph-schema>=1.2.3",
|
|
13
|
+
"matplotlib",
|
|
14
|
+
"pint>=0.24.4",
|
|
15
|
+
"scipy>=1.8",
|
|
16
|
+
"pandas",
|
|
17
|
+
"sympy",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
# Distributed Ray actor backend for processes (RayProcess + actor pool).
|
|
22
|
+
# Install with: pip install process-bigraph[ray]
|
|
23
|
+
ray = [
|
|
24
|
+
"ray>=2.10",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# REST server (FastAPI/uvicorn) that hosts processes over HTTP, paired with
|
|
28
|
+
# the existing RestProcess client.
|
|
29
|
+
# Install with: pip install process-bigraph[server-rest]
|
|
30
|
+
server-rest = [
|
|
31
|
+
"fastapi>=0.116.1",
|
|
32
|
+
"fastapi-utils>=0.8.0",
|
|
33
|
+
"uvicorn>=0.35.0",
|
|
34
|
+
"fire",
|
|
35
|
+
"typing-inspect>=0.9.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"ipdb>=0.13.13",
|
|
41
|
+
"ipykernel",
|
|
42
|
+
"jupyter",
|
|
43
|
+
"notebook",
|
|
44
|
+
"pytest",
|
|
45
|
+
"pytest-cov",
|
|
46
|
+
"httpx",
|
|
47
|
+
# Test the optional extras without forcing them on everyone.
|
|
48
|
+
"ray>=2.10",
|
|
49
|
+
"fastapi>=0.116.1",
|
|
50
|
+
"fastapi-utils>=0.8.0",
|
|
51
|
+
"uvicorn>=0.35.0",
|
|
52
|
+
"fire",
|
|
53
|
+
"typing-inspect>=0.9.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools]
|
|
57
|
+
packages = [
|
|
58
|
+
"process_bigraph",
|
|
59
|
+
"process_bigraph.processes",
|
|
60
|
+
"process_bigraph.protocols",
|
|
61
|
+
"process_bigraph.server",
|
|
62
|
+
"process_bigraph.experiments",
|
|
63
|
+
"process_bigraph.types",
|
|
64
|
+
]
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=68", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "process-bigraph"
|
|
7
|
-
version = "1.3.1"
|
|
8
|
-
description = "protocol and execution for compositional systems biology"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.11"
|
|
11
|
-
dependencies = [
|
|
12
|
-
"bigraph-schema>=1.2.3",
|
|
13
|
-
"matplotlib",
|
|
14
|
-
"pint>=0.24.4",
|
|
15
|
-
"scipy>=1.8",
|
|
16
|
-
"pandas",
|
|
17
|
-
"sympy",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
[dependency-groups]
|
|
21
|
-
dev = [
|
|
22
|
-
"ipdb>=0.13.13",
|
|
23
|
-
"ipykernel",
|
|
24
|
-
"jupyter",
|
|
25
|
-
"notebook",
|
|
26
|
-
"pytest",
|
|
27
|
-
"pytest-cov",
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
[tool.setuptools]
|
|
31
|
-
packages = [
|
|
32
|
-
"process_bigraph",
|
|
33
|
-
"process_bigraph.processes",
|
|
34
|
-
"process_bigraph.protocols",
|
|
35
|
-
"process_bigraph.experiments",
|
|
36
|
-
"process_bigraph.types",
|
|
37
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/experiments/minimal_gillespie.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/dynamic_structure.py
RENAMED
|
File without changes
|
|
File without changes
|
{process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/growth_division.py
RENAMED
|
File without changes
|
{process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph/processes/math_expression.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.3.1 → process_bigraph-1.4.2}/process_bigraph.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|