process-bigraph 1.8.0__tar.gz → 1.8.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.8.0/process_bigraph.egg-info → process_bigraph-1.8.2}/PKG-INFO +1 -1
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/__init__.py +5 -1
- process_bigraph-1.8.2/process_bigraph/visualizations/__init__.py +30 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/distribution.py +44 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/heatmap.py +44 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/param_vs_observable.py +51 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/phase_space.py +40 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/time_series.py +91 -0
- process_bigraph-1.8.2/process_bigraph/visualizations/timeseries_from_observables.py +349 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2/process_bigraph.egg-info}/PKG-INFO +1 -1
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph.egg-info/SOURCES.txt +8 -1
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/pyproject.toml +2 -1
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/AUTHORS.md +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/LICENSE +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/README.md +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/artifacts.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/composite.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/composite_discovery.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/composite_generator.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/composite_spec.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/config_helpers.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/core_introspection.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/emitter.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/nextflow.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/bigraphical_reactive_system.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/cached.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/simulation.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/_git_worker.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/clusters/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/clusters/ec2_ssm.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/git.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/pool.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/ray.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/session.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/run.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/run_step.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/scheduling.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/server/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/server/rest.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/server/start.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/templates.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/visualization.py +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph.egg-info/requires.txt +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.8.0 → process_bigraph-1.8.2}/setup.cfg +0 -0
|
@@ -27,8 +27,12 @@ from process_bigraph.bundle import load_bundle # noqa: F401
|
|
|
27
27
|
from process_bigraph.emitter import Emitter, gather_emitter_results, generate_emitter_state # noqa: F401
|
|
28
28
|
from process_bigraph.types import StepLink, ProcessLink, CompositeLink # noqa: F401
|
|
29
29
|
from process_bigraph import core_introspection # noqa: F401
|
|
30
|
+
# NOTE: do NOT re-export the `composite_generator` decorator here — that name
|
|
31
|
+
# is the submodule `process_bigraph.composite_generator`, and exporting the
|
|
32
|
+
# same-named function shadows it (breaks `import process_bigraph.composite_generator`).
|
|
33
|
+
# Get the decorator via `from process_bigraph.composite_generator import composite_generator`.
|
|
30
34
|
from process_bigraph.composite_generator import ( # noqa: F401
|
|
31
|
-
|
|
35
|
+
discover_generators, build_generator,
|
|
32
36
|
install_default_emitters, emitter_defaults,
|
|
33
37
|
)
|
|
34
38
|
from process_bigraph.composite_discovery import discover_all # noqa: F401
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Default Visualization classes shipped with process-bigraph.
|
|
2
|
+
|
|
3
|
+
All five inherit ``process_bigraph.visualization.Visualization`` and implement
|
|
4
|
+
``render_final(results, *, config) -> str``. They are auto-discovered via
|
|
5
|
+
``bigraph_schema.package.discover`` so workspaces don't need to register them
|
|
6
|
+
manually.
|
|
7
|
+
|
|
8
|
+
Usage (from a composite or investigation spec):
|
|
9
|
+
visualizations:
|
|
10
|
+
- name: trajectory
|
|
11
|
+
address: "local:TimeSeriesPlot"
|
|
12
|
+
config: {observable: free_DnaA, sources: [baseline]}
|
|
13
|
+
"""
|
|
14
|
+
from process_bigraph.visualizations.time_series import TimeSeriesPlot
|
|
15
|
+
from process_bigraph.visualizations.timeseries_from_observables import (
|
|
16
|
+
TimeSeriesFromObservables,
|
|
17
|
+
)
|
|
18
|
+
from process_bigraph.visualizations.param_vs_observable import ParamVsObservable
|
|
19
|
+
from process_bigraph.visualizations.distribution import Distribution
|
|
20
|
+
from process_bigraph.visualizations.phase_space import PhaseSpace
|
|
21
|
+
from process_bigraph.visualizations.heatmap import Heatmap
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"TimeSeriesPlot",
|
|
25
|
+
"TimeSeriesFromObservables",
|
|
26
|
+
"ParamVsObservable",
|
|
27
|
+
"Distribution",
|
|
28
|
+
"PhaseSpace",
|
|
29
|
+
"Heatmap",
|
|
30
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Distribution — histogram of a sample list.
|
|
2
|
+
|
|
3
|
+
Orchestrator collects the samples (e.g., final-step values across N seed
|
|
4
|
+
runs) into a flat list; this Step renders a histogram.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import html as _html
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from process_bigraph.visualization import Visualization
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Distribution(Visualization):
|
|
14
|
+
"""Histogram of an observable's distribution.
|
|
15
|
+
|
|
16
|
+
Inputs (declared types):
|
|
17
|
+
samples: list[float] — the values to bin
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def inputs(self):
|
|
21
|
+
return {'samples': 'list[float]'}
|
|
22
|
+
|
|
23
|
+
def update(self, state):
|
|
24
|
+
samples = state.get('samples') or []
|
|
25
|
+
title = (getattr(self, 'config', None) or {}).get('title', '')
|
|
26
|
+
traces = [{
|
|
27
|
+
'x': samples, 'type': 'histogram',
|
|
28
|
+
'marker': {'color': '#6366f1'},
|
|
29
|
+
'opacity': 0.85,
|
|
30
|
+
}]
|
|
31
|
+
layout = {
|
|
32
|
+
'title': {'text': _html.escape(title), 'font': {'size': 14}},
|
|
33
|
+
'yaxis': {'title': {'text': 'count'}},
|
|
34
|
+
'margin': {'l': 55, 'r': 15, 't': 40, 'b': 40},
|
|
35
|
+
'showlegend': False,
|
|
36
|
+
'bargap': 0.05,
|
|
37
|
+
}
|
|
38
|
+
return {'html': (
|
|
39
|
+
'<div id="viz" style="height:380px"></div>'
|
|
40
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
41
|
+
'<script>Plotly.newPlot("viz", '
|
|
42
|
+
+ json.dumps(traces) + ', ' + json.dumps(layout)
|
|
43
|
+
+ ', {responsive:true, displayModeBar:false});</script>'
|
|
44
|
+
)}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Heatmap — 2D parameter sweep, color = reduced observable."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import html as _html
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from process_bigraph.visualization import Visualization
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Heatmap(Visualization):
|
|
10
|
+
"""Color matrix over a 2D parameter sweep.
|
|
11
|
+
|
|
12
|
+
Inputs (declared types):
|
|
13
|
+
x_params: list[float]
|
|
14
|
+
y_params: list[float]
|
|
15
|
+
z_values: list[list[float]] — z[y_idx][x_idx]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def inputs(self):
|
|
19
|
+
return {
|
|
20
|
+
'x_params': 'list[float]',
|
|
21
|
+
'y_params': 'list[float]',
|
|
22
|
+
'z_values': 'list[list[float]]',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def update(self, state):
|
|
26
|
+
xs = state.get('x_params') or []
|
|
27
|
+
ys = state.get('y_params') or []
|
|
28
|
+
zs = state.get('z_values') or []
|
|
29
|
+
title = (getattr(self, 'config', None) or {}).get('title', '')
|
|
30
|
+
traces = [{
|
|
31
|
+
'z': zs, 'x': xs, 'y': ys, 'type': 'heatmap',
|
|
32
|
+
'colorscale': 'Viridis',
|
|
33
|
+
}]
|
|
34
|
+
layout = {
|
|
35
|
+
'title': {'text': _html.escape(title), 'font': {'size': 14}},
|
|
36
|
+
'margin': {'l': 55, 'r': 60, 't': 40, 'b': 40},
|
|
37
|
+
}
|
|
38
|
+
return {'html': (
|
|
39
|
+
'<div id="viz" style="height:420px"></div>'
|
|
40
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
41
|
+
'<script>Plotly.newPlot("viz", '
|
|
42
|
+
+ json.dumps(traces) + ', ' + json.dumps(layout)
|
|
43
|
+
+ ', {responsive:true, displayModeBar:false});</script>'
|
|
44
|
+
)}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""ParamVsObservable — sweep parameter value vs reduced observable.
|
|
2
|
+
|
|
3
|
+
The orchestrator does the reduction (final/mean/max/...) before populating
|
|
4
|
+
the input store; this Step just plots ``y vs x`` as a line+marker chart.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import html as _html
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from process_bigraph.visualization import Visualization
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParamVsObservable(Visualization):
|
|
14
|
+
"""Plot reduced observable values across a sweep.
|
|
15
|
+
|
|
16
|
+
Inputs (declared types):
|
|
17
|
+
sweep_param_values: list[float] — x-axis (one value per run in the sweep)
|
|
18
|
+
reduced_observable: list[float] — y-axis (reduced trajectory per run)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def inputs(self):
|
|
22
|
+
return {
|
|
23
|
+
'sweep_param_values': 'list[float]',
|
|
24
|
+
'reduced_observable': 'list[float]',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def update(self, state):
|
|
28
|
+
xs = state.get('sweep_param_values') or []
|
|
29
|
+
ys = state.get('reduced_observable') or []
|
|
30
|
+
title = (getattr(self, 'config', None) or {}).get('title', '')
|
|
31
|
+
if xs and ys:
|
|
32
|
+
pairs = sorted(zip(xs, ys))
|
|
33
|
+
xs = [p[0] for p in pairs]
|
|
34
|
+
ys = [p[1] for p in pairs]
|
|
35
|
+
traces = [{
|
|
36
|
+
'x': xs, 'y': ys, 'type': 'scatter', 'mode': 'lines+markers',
|
|
37
|
+
'line': {'color': '#6366f1', 'width': 2},
|
|
38
|
+
'marker': {'color': '#6366f1', 'size': 8},
|
|
39
|
+
}]
|
|
40
|
+
layout = {
|
|
41
|
+
'title': {'text': _html.escape(title), 'font': {'size': 14}},
|
|
42
|
+
'margin': {'l': 55, 'r': 15, 't': 40, 'b': 40},
|
|
43
|
+
'showlegend': False,
|
|
44
|
+
}
|
|
45
|
+
return {'html': (
|
|
46
|
+
'<div id="viz" style="height:380px"></div>'
|
|
47
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
48
|
+
'<script>Plotly.newPlot("viz", '
|
|
49
|
+
+ json.dumps(traces) + ', ' + json.dumps(layout)
|
|
50
|
+
+ ', {responsive:true, displayModeBar:false});</script>'
|
|
51
|
+
)}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""PhaseSpace — two observables plotted against each other."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import html as _html
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from process_bigraph.visualization import Visualization
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PhaseSpace(Visualization):
|
|
10
|
+
"""XY trajectory of two observables.
|
|
11
|
+
|
|
12
|
+
Inputs (declared types):
|
|
13
|
+
x: list[float]
|
|
14
|
+
y: list[float]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def inputs(self):
|
|
18
|
+
return {'x': 'list[float]', 'y': 'list[float]'}
|
|
19
|
+
|
|
20
|
+
def update(self, state):
|
|
21
|
+
xs = state.get('x') or []
|
|
22
|
+
ys = state.get('y') or []
|
|
23
|
+
title = (getattr(self, 'config', None) or {}).get('title', '')
|
|
24
|
+
traces = [{
|
|
25
|
+
'x': xs, 'y': ys, 'type': 'scatter', 'mode': 'lines+markers',
|
|
26
|
+
'line': {'color': '#6366f1', 'width': 2},
|
|
27
|
+
'marker': {'size': 5},
|
|
28
|
+
}]
|
|
29
|
+
layout = {
|
|
30
|
+
'title': {'text': _html.escape(title), 'font': {'size': 14}},
|
|
31
|
+
'margin': {'l': 55, 'r': 15, 't': 40, 'b': 40},
|
|
32
|
+
'showlegend': False,
|
|
33
|
+
}
|
|
34
|
+
return {'html': (
|
|
35
|
+
'<div id="viz" style="height:380px"></div>'
|
|
36
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
37
|
+
'<script>Plotly.newPlot("viz", '
|
|
38
|
+
+ json.dumps(traces) + ', ' + json.dumps(layout)
|
|
39
|
+
+ ', {responsive:true, displayModeBar:false});</script>'
|
|
40
|
+
)}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""TimeSeriesPlot — observable(s) vs time, one line per run."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import html as _html
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from process_bigraph.visualization import Visualization
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_PALETTE = ['#6366f1', '#10b981', '#f43f5e', '#f59e0b',
|
|
10
|
+
'#8b5cf6', '#06b6d4', '#84cc16', '#ec4899']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimeSeriesPlot(Visualization):
|
|
14
|
+
"""Plot one or more observables vs time.
|
|
15
|
+
|
|
16
|
+
Inputs (declared types):
|
|
17
|
+
observable: list[float] — trajectory values, or list-of-lists for multi-run
|
|
18
|
+
time: list[float] — same shape as observable
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def inputs(self):
|
|
22
|
+
return {'observable': 'list[float]', 'time': 'list[float]'}
|
|
23
|
+
|
|
24
|
+
def update(self, state):
|
|
25
|
+
obs = state.get('observable')
|
|
26
|
+
ts = state.get('time')
|
|
27
|
+
if obs is None or ts is None:
|
|
28
|
+
return {'html': '<p style="color:#991b1b">missing observable or time input</p>'}
|
|
29
|
+
|
|
30
|
+
if obs and isinstance(obs[0], list):
|
|
31
|
+
runs = list(zip(ts, obs))
|
|
32
|
+
else:
|
|
33
|
+
runs = [(ts, obs)]
|
|
34
|
+
labels = state.get('_run_labels') or [''] * len(runs)
|
|
35
|
+
overlays = state.get('_overlays') or []
|
|
36
|
+
|
|
37
|
+
traces = []
|
|
38
|
+
for i, (xs, ys) in enumerate(runs):
|
|
39
|
+
traces.append({
|
|
40
|
+
'x': xs, 'y': ys, 'type': 'scatter', 'mode': 'lines',
|
|
41
|
+
'name': labels[i] if i < len(labels) else f'run {i}',
|
|
42
|
+
'line': {'color': _PALETTE[i % len(_PALETTE)], 'width': 2},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
shapes = []
|
|
46
|
+
annotations = []
|
|
47
|
+
for ov in overlays:
|
|
48
|
+
kind = ov.get('kind')
|
|
49
|
+
if kind == 'reference-range':
|
|
50
|
+
y_min, y_max = ov.get('y_min'), ov.get('y_max')
|
|
51
|
+
if y_min is not None and y_max is not None:
|
|
52
|
+
shapes.append({
|
|
53
|
+
'type': 'rect', 'xref': 'paper', 'yref': 'y',
|
|
54
|
+
'x0': 0, 'x1': 1, 'y0': y_min, 'y1': y_max,
|
|
55
|
+
'fillcolor': '#fef3c7', 'opacity': 0.3,
|
|
56
|
+
'line': {'width': 0},
|
|
57
|
+
})
|
|
58
|
+
annotations.append({
|
|
59
|
+
'xref': 'paper', 'yref': 'y',
|
|
60
|
+
'x': 0.02, 'y': y_max,
|
|
61
|
+
'text': _html.escape(ov.get('label', 'reference range')),
|
|
62
|
+
'showarrow': False,
|
|
63
|
+
'font': {'size': 11, 'color': '#92400e'},
|
|
64
|
+
})
|
|
65
|
+
elif kind == 'experimental-points':
|
|
66
|
+
pts = ov.get('points') or []
|
|
67
|
+
if pts:
|
|
68
|
+
traces.append({
|
|
69
|
+
'x': [p['x'] for p in pts],
|
|
70
|
+
'y': [p['y'] for p in pts],
|
|
71
|
+
'type': 'scatter', 'mode': 'markers',
|
|
72
|
+
'name': ov.get('label', 'experimental'),
|
|
73
|
+
'marker': {'color': '#000', 'size': 8, 'symbol': 'circle-open'},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
title = (getattr(self, 'config', None) or {}).get('title', '')
|
|
77
|
+
layout = {
|
|
78
|
+
'title': {'text': _html.escape(title), 'font': {'size': 14}},
|
|
79
|
+
'xaxis': {'title': {'text': 'time'}},
|
|
80
|
+
'margin': {'l': 55, 'r': 15, 't': 40, 'b': 40},
|
|
81
|
+
'legend': {'orientation': 'h', 'y': -0.2},
|
|
82
|
+
'shapes': shapes,
|
|
83
|
+
'annotations': annotations,
|
|
84
|
+
}
|
|
85
|
+
return {'html': (
|
|
86
|
+
'<div id="viz" style="height:380px"></div>'
|
|
87
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
88
|
+
'<script>Plotly.newPlot("viz", '
|
|
89
|
+
+ json.dumps(traces) + ', ' + json.dumps(layout)
|
|
90
|
+
+ ', {responsive:true, displayModeBar:false});</script>'
|
|
91
|
+
)}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""TimeSeriesFromObservables — self-contained multi-observable time-series.
|
|
2
|
+
|
|
3
|
+
Backs Finding #26 in the v2ecoli investigation walkthrough notes:
|
|
4
|
+
|
|
5
|
+
> Critical missing piece: a generic ``TimeSeriesFromObservables``
|
|
6
|
+
> Visualization Step that consumes any list of observable specs +
|
|
7
|
+
> a runs.db and produces a time-series plot. Without this, studies
|
|
8
|
+
> that declare ``address: local:TimeSeriesPlot`` have no backing
|
|
9
|
+
> code and can't be previewed.
|
|
10
|
+
|
|
11
|
+
The existing :class:`TimeSeriesPlot` is the right *renderer*, but its
|
|
12
|
+
``observable: list[float]`` / ``time: list[float]`` inputs need to be
|
|
13
|
+
plumbed in by an upstream Step. The dashboard's ``build_viz_composite``
|
|
14
|
+
handles that plumbing for *single*-observable cases via an
|
|
15
|
+
``inputs_map`` convention, but most study visualizations want to plot
|
|
16
|
+
several observables together with shared time + per-run colors — and
|
|
17
|
+
the wiring boilerplate has to be written per study, which the field
|
|
18
|
+
notes flagged as the main reason "Visualizations tab is empty" was
|
|
19
|
+
the default state.
|
|
20
|
+
|
|
21
|
+
This class fixes it by being **self-contained**:
|
|
22
|
+
|
|
23
|
+
- Its only required config is ``observables: list[str]`` — the names
|
|
24
|
+
of observables (matching ``study.yaml.observables[].name``) to plot.
|
|
25
|
+
- The dashboard's renderer injects ``_runs_db_path`` and
|
|
26
|
+
``_study_yaml_path`` (the two private config keys this class
|
|
27
|
+
reads) when it builds the composite. Both are optional; the class
|
|
28
|
+
degrades gracefully when either is missing.
|
|
29
|
+
- The class reads runs.db itself (stdlib sqlite3 — no extra deps)
|
|
30
|
+
and renders one Plotly trace per (observable × run) pair with
|
|
31
|
+
units pulled from ``study.yaml.observables[].units`` when present.
|
|
32
|
+
|
|
33
|
+
The result is a single Viz address (``local:TimeSeriesFromObservables``)
|
|
34
|
+
that lights up most studies' declared visualizations without any
|
|
35
|
+
per-study code. The note recommends shipping this in the framework so
|
|
36
|
+
every workspace gets it; that's why this lives in
|
|
37
|
+
``process_bigraph.visualizations`` alongside the other defaults.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import html as _html
|
|
42
|
+
import json
|
|
43
|
+
import sqlite3
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
from process_bigraph.visualization import Visualization
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_PALETTE = [
|
|
50
|
+
"#6366f1", "#10b981", "#f43f5e", "#f59e0b",
|
|
51
|
+
"#8b5cf6", "#06b6d4", "#84cc16", "#ec4899",
|
|
52
|
+
"#14b8a6", "#dc2626", "#3b82f6", "#a855f7",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TimeSeriesFromObservables(Visualization):
|
|
57
|
+
"""Multi-observable time series, self-contained.
|
|
58
|
+
|
|
59
|
+
Config keys (user-set in ``study.yaml.visualizations[].config``):
|
|
60
|
+
|
|
61
|
+
- ``observables: list[str]`` (required) — observable names to plot.
|
|
62
|
+
Matched against ``study.yaml.observables[].name`` for units
|
|
63
|
+
+ against the ``observables`` map in runs.db ``history.state``
|
|
64
|
+
for data.
|
|
65
|
+
- ``title: str`` (optional) — chart title. Defaults to ``""``.
|
|
66
|
+
- ``sources: list[str]`` (optional) — restrict to runs whose
|
|
67
|
+
``sim_name`` is in this list. Default: all runs.
|
|
68
|
+
|
|
69
|
+
Config keys injected by the renderer (private; users don't set
|
|
70
|
+
these):
|
|
71
|
+
|
|
72
|
+
- ``_runs_db_path: str`` — absolute path to the study's runs.db.
|
|
73
|
+
When absent, the chart renders an empty-data placeholder.
|
|
74
|
+
- ``_study_yaml_path: str`` — absolute path to study.yaml. Used
|
|
75
|
+
to look up per-observable ``units``. When absent, units are
|
|
76
|
+
omitted from axis labels.
|
|
77
|
+
|
|
78
|
+
Inputs: none (the class plumbs its own data). The renderer's
|
|
79
|
+
``build_viz_composite`` skips inputs_map plumbing when a viz class
|
|
80
|
+
declares no inputs.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
config_schema = {
|
|
84
|
+
# Inherits title, render_mode, sample_every from Visualization.
|
|
85
|
+
**Visualization.config_schema,
|
|
86
|
+
"observables": {"_type": "list[string]", "_default": []},
|
|
87
|
+
"sources": {"_type": "list[string]", "_default": []},
|
|
88
|
+
# Private — injected by the dashboard renderer; the schema
|
|
89
|
+
# accepts string so YAML stays clean.
|
|
90
|
+
"runs_db_path": {"_type": "string", "_default": ""},
|
|
91
|
+
"run_id": {"_type": "string", "_default": ""},
|
|
92
|
+
"study_yaml_path": {"_type": "string", "_default": ""},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def inputs(self) -> dict:
|
|
96
|
+
# Self-contained: no bigraph inputs. The renderer detects this
|
|
97
|
+
# and skips inputs_map plumbing.
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
# The class is rendered in one shot (legacy contract). Override
|
|
101
|
+
# update() directly so the baseclass orchestrator stays out of the
|
|
102
|
+
# way — we don't accumulate per-tick state; runs.db already has it.
|
|
103
|
+
def update(self, state: dict) -> dict:
|
|
104
|
+
cfg = dict(getattr(self, "config", None) or {})
|
|
105
|
+
return {"html": _render_html(cfg)}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Pure helpers (no Visualization runtime — easy to unit-test)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _load_study_observable_meta(study_yaml_path: str | None) -> dict[str, dict]:
|
|
114
|
+
"""Return ``{observable_name: {units, description, store_path}}``.
|
|
115
|
+
|
|
116
|
+
Tolerant of missing file, malformed YAML, or absent ``observables``
|
|
117
|
+
block — returns an empty dict in any of those cases.
|
|
118
|
+
"""
|
|
119
|
+
if not study_yaml_path:
|
|
120
|
+
return {}
|
|
121
|
+
p = Path(study_yaml_path)
|
|
122
|
+
if not p.is_file():
|
|
123
|
+
return {}
|
|
124
|
+
# PyYAML is not a hard process-bigraph dependency (same convention as
|
|
125
|
+
# composite_spec.py); import lazily so importing this module never
|
|
126
|
+
# requires it unless a caller actually supplies a study_yaml_path.
|
|
127
|
+
import yaml
|
|
128
|
+
try:
|
|
129
|
+
# study.yaml is UTF-8 (often non-ASCII prose); decode explicitly so a
|
|
130
|
+
# bare-CLI render under an ASCII locale doesn't crash on read.
|
|
131
|
+
spec = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
132
|
+
except yaml.YAMLError:
|
|
133
|
+
return {}
|
|
134
|
+
out: dict[str, dict] = {}
|
|
135
|
+
for o in (spec.get("observables") or []):
|
|
136
|
+
if isinstance(o, dict) and isinstance(o.get("name"), str):
|
|
137
|
+
out[o["name"]] = {
|
|
138
|
+
"units": o.get("units") or "",
|
|
139
|
+
"description": o.get("description") or "",
|
|
140
|
+
"store_path": o.get("store_path") or "",
|
|
141
|
+
}
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _load_runs(runs_db_path: str | None, sources: list[str], run_id: str = "") -> list[dict]:
|
|
146
|
+
"""Read runs.db and return a list of ``{run_id, sim_name, params,
|
|
147
|
+
observables, time}`` dicts.
|
|
148
|
+
|
|
149
|
+
``observables`` is ``{name: list[number]}`` keyed by the names the
|
|
150
|
+
emitter wrote (typically the observable name as declared in
|
|
151
|
+
``study.yaml``). ``time`` is the trajectory's time axis.
|
|
152
|
+
|
|
153
|
+
Returns ``[]`` if the db file is missing, malformed, or empty.
|
|
154
|
+
Restricts to runs in ``sources`` when provided (matching
|
|
155
|
+
``runs_meta.sim_name``).
|
|
156
|
+
"""
|
|
157
|
+
if not runs_db_path:
|
|
158
|
+
return []
|
|
159
|
+
p = Path(runs_db_path)
|
|
160
|
+
if not p.is_file():
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
sources_set = set(sources or [])
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
conn = sqlite3.connect(str(p))
|
|
167
|
+
except sqlite3.Error:
|
|
168
|
+
return []
|
|
169
|
+
conn.row_factory = sqlite3.Row
|
|
170
|
+
try:
|
|
171
|
+
try:
|
|
172
|
+
meta_rows = conn.execute(
|
|
173
|
+
"SELECT run_id, sim_name, params_json FROM runs_meta"
|
|
174
|
+
).fetchall()
|
|
175
|
+
except sqlite3.OperationalError:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
has_history = conn.execute(
|
|
179
|
+
"SELECT name FROM sqlite_master "
|
|
180
|
+
"WHERE type='table' AND name='history'"
|
|
181
|
+
).fetchone() is not None
|
|
182
|
+
if not has_history:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
out: list[dict] = []
|
|
186
|
+
for r in meta_rows:
|
|
187
|
+
if run_id and r["run_id"] != run_id:
|
|
188
|
+
continue
|
|
189
|
+
sim_name = r["sim_name"] or "default"
|
|
190
|
+
if sources_set and sim_name not in sources_set:
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
params = json.loads(r["params_json"] or "{}")
|
|
194
|
+
except (json.JSONDecodeError, TypeError):
|
|
195
|
+
params = {}
|
|
196
|
+
history_rows = conn.execute(
|
|
197
|
+
"SELECT step, global_time, state FROM history "
|
|
198
|
+
"WHERE simulation_id=? ORDER BY step ASC",
|
|
199
|
+
(r["run_id"],),
|
|
200
|
+
).fetchall()
|
|
201
|
+
observables: dict[str, list] = {}
|
|
202
|
+
time_axis: list[float] = []
|
|
203
|
+
for hrow in history_rows:
|
|
204
|
+
try:
|
|
205
|
+
s = json.loads(hrow["state"]) if hrow["state"] else {}
|
|
206
|
+
except (json.JSONDecodeError, TypeError):
|
|
207
|
+
continue
|
|
208
|
+
for k, v in s.items():
|
|
209
|
+
observables.setdefault(k, []).append(v)
|
|
210
|
+
if "time" in s:
|
|
211
|
+
# Already captured under observables["time"]; mirror
|
|
212
|
+
# to the dedicated axis for convenience.
|
|
213
|
+
time_axis.append(s["time"])
|
|
214
|
+
else:
|
|
215
|
+
time_axis.append(hrow["global_time"])
|
|
216
|
+
out.append({
|
|
217
|
+
"run_id": r["run_id"],
|
|
218
|
+
"sim_name": sim_name,
|
|
219
|
+
"params": params,
|
|
220
|
+
"observables": observables,
|
|
221
|
+
"time": time_axis,
|
|
222
|
+
})
|
|
223
|
+
return out
|
|
224
|
+
finally:
|
|
225
|
+
conn.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _label_for_run(run: dict, index: int) -> str:
|
|
229
|
+
"""Compact label for a run, preferring params; falling back to
|
|
230
|
+
sim_name; finally the last 6 chars of run_id."""
|
|
231
|
+
params = run.get("params") or {}
|
|
232
|
+
if params:
|
|
233
|
+
return ", ".join(f"{k}={v}" for k, v in sorted(params.items()))
|
|
234
|
+
sim_name = run.get("sim_name") or ""
|
|
235
|
+
if sim_name and sim_name != "default":
|
|
236
|
+
return sim_name
|
|
237
|
+
rid = run.get("run_id") or f"run-{index}"
|
|
238
|
+
return rid[-6:]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _build_traces(
|
|
242
|
+
runs: list[dict],
|
|
243
|
+
observable_names: list[str],
|
|
244
|
+
) -> list[dict]:
|
|
245
|
+
"""Build Plotly traces — one line per (observable × run) pair.
|
|
246
|
+
|
|
247
|
+
Lines are colored by observable; runs of the same observable share
|
|
248
|
+
a color but get distinct dash patterns when there are multiple
|
|
249
|
+
runs, so an expert can tell apart calibration sweeps at a glance.
|
|
250
|
+
"""
|
|
251
|
+
traces: list[dict] = []
|
|
252
|
+
dash_patterns = ["solid", "dash", "dot", "dashdot", "longdash"]
|
|
253
|
+
for obs_idx, name in enumerate(observable_names):
|
|
254
|
+
color = _PALETTE[obs_idx % len(_PALETTE)]
|
|
255
|
+
runs_with_obs = [
|
|
256
|
+
(i, r) for i, r in enumerate(runs)
|
|
257
|
+
if name in (r.get("observables") or {})
|
|
258
|
+
]
|
|
259
|
+
for trace_idx, (run_idx, run) in enumerate(runs_with_obs):
|
|
260
|
+
y = run["observables"][name]
|
|
261
|
+
x = run.get("time") or list(range(len(y)))
|
|
262
|
+
dash = dash_patterns[trace_idx % len(dash_patterns)] \
|
|
263
|
+
if len(runs_with_obs) > 1 else "solid"
|
|
264
|
+
label = name
|
|
265
|
+
if len(runs_with_obs) > 1:
|
|
266
|
+
label = f"{name} — {_label_for_run(run, run_idx)}"
|
|
267
|
+
traces.append({
|
|
268
|
+
"x": x, "y": y, "type": "scatter", "mode": "lines",
|
|
269
|
+
"name": label,
|
|
270
|
+
"line": {"color": color, "width": 2, "dash": dash},
|
|
271
|
+
})
|
|
272
|
+
return traces
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _y_axis_label(meta: dict[str, dict], names: list[str]) -> str:
|
|
276
|
+
"""Compose a Y axis label from the requested observables.
|
|
277
|
+
|
|
278
|
+
- Single observable, units known: ``"<name> (<units>)"``.
|
|
279
|
+
- Multiple observables, all sharing a unit: ``"<units>"``.
|
|
280
|
+
- Multiple observables, mixed units: ``"value"`` (no label).
|
|
281
|
+
- No units known: ``""``.
|
|
282
|
+
"""
|
|
283
|
+
units = [meta.get(n, {}).get("units") or "" for n in names]
|
|
284
|
+
units_set = {u for u in units if u}
|
|
285
|
+
if len(names) == 1:
|
|
286
|
+
if units[0]:
|
|
287
|
+
return f"{names[0]} ({units[0]})"
|
|
288
|
+
return names[0]
|
|
289
|
+
if len(units_set) == 1:
|
|
290
|
+
return next(iter(units_set))
|
|
291
|
+
return "value" if any(units) else ""
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _render_html(cfg: dict) -> str:
|
|
295
|
+
"""Pure renderer: build the Plotly HTML from a viz config dict.
|
|
296
|
+
|
|
297
|
+
Exposed as a free function so tests can drive it without
|
|
298
|
+
constructing a Visualization instance.
|
|
299
|
+
"""
|
|
300
|
+
observable_names = list(cfg.get("observables") or [])
|
|
301
|
+
if not observable_names:
|
|
302
|
+
return (
|
|
303
|
+
'<div style="padding:12px;color:#991b1b">'
|
|
304
|
+
'TimeSeriesFromObservables: no observables declared in config. '
|
|
305
|
+
'Set <code>observables: [name1, name2]</code> in the viz config.'
|
|
306
|
+
'</div>'
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
runs = _load_runs(cfg.get("runs_db_path") or cfg.get("_runs_db_path"), cfg.get("sources") or [], cfg.get("run_id") or "")
|
|
310
|
+
meta = _load_study_observable_meta(cfg.get("study_yaml_path") or cfg.get("_study_yaml_path"))
|
|
311
|
+
|
|
312
|
+
if not runs:
|
|
313
|
+
return (
|
|
314
|
+
'<div style="padding:12px;color:#92400e;background:#fef3c7;'
|
|
315
|
+
'border:1px solid #fcd34d;border-radius:4px">'
|
|
316
|
+
'<strong>No run data yet.</strong><br>'
|
|
317
|
+
'Run a baseline (or wait for a Simulate-phase run to complete) '
|
|
318
|
+
'and re-render. The viz config is valid; the data side just '
|
|
319
|
+
'isn\'t populated.'
|
|
320
|
+
'</div>'
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
traces = _build_traces(runs, observable_names)
|
|
324
|
+
if not traces:
|
|
325
|
+
return (
|
|
326
|
+
'<div style="padding:12px;color:#991b1b">'
|
|
327
|
+
'None of the declared observables '
|
|
328
|
+
f'({", ".join(observable_names)}) were found in any run\'s '
|
|
329
|
+
'emitted state. Check that the run\'s emitter is configured '
|
|
330
|
+
'to record these names.'
|
|
331
|
+
'</div>'
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
title = cfg.get("title") or ""
|
|
335
|
+
y_label = _y_axis_label(meta, observable_names)
|
|
336
|
+
layout = {
|
|
337
|
+
"title": {"text": _html.escape(title), "font": {"size": 14}},
|
|
338
|
+
"xaxis": {"title": {"text": "time"}},
|
|
339
|
+
"yaxis": {"title": {"text": _html.escape(y_label)}},
|
|
340
|
+
"margin": {"l": 60, "r": 15, "t": 40, "b": 50},
|
|
341
|
+
"legend": {"orientation": "h", "y": -0.2},
|
|
342
|
+
}
|
|
343
|
+
return (
|
|
344
|
+
'<div id="viz" style="height:380px"></div>'
|
|
345
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
|
|
346
|
+
'<script>Plotly.newPlot("viz", '
|
|
347
|
+
+ json.dumps(traces) + ", " + json.dumps(layout)
|
|
348
|
+
+ ", {responsive:true, displayModeBar:false});</script>"
|
|
349
|
+
)
|
|
@@ -49,4 +49,11 @@ process_bigraph/server/__init__.py
|
|
|
49
49
|
process_bigraph/server/rest.py
|
|
50
50
|
process_bigraph/server/start.py
|
|
51
51
|
process_bigraph/types/__init__.py
|
|
52
|
-
process_bigraph/types/process.py
|
|
52
|
+
process_bigraph/types/process.py
|
|
53
|
+
process_bigraph/visualizations/__init__.py
|
|
54
|
+
process_bigraph/visualizations/distribution.py
|
|
55
|
+
process_bigraph/visualizations/heatmap.py
|
|
56
|
+
process_bigraph/visualizations/param_vs_observable.py
|
|
57
|
+
process_bigraph/visualizations/phase_space.py
|
|
58
|
+
process_bigraph/visualizations/time_series.py
|
|
59
|
+
process_bigraph/visualizations/timeseries_from_observables.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "process-bigraph"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.2"
|
|
8
8
|
description = "protocol and execution for compositional systems biology"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -93,4 +93,5 @@ packages = [
|
|
|
93
93
|
"process_bigraph.server",
|
|
94
94
|
"process_bigraph.experiments",
|
|
95
95
|
"process_bigraph.types",
|
|
96
|
+
"process_bigraph.visualizations",
|
|
96
97
|
]
|
|
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
|
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/experiments/minimal_gillespie.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/dynamic_structure.py
RENAMED
|
File without changes
|
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/processes/growth_division.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/clusters/__init__.py
RENAMED
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph/protocols/clusters/ec2_ssm.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{process_bigraph-1.8.0 → process_bigraph-1.8.2}/process_bigraph.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|