process-bigraph 1.8.4__tar.gz → 1.8.5__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.4/process_bigraph.egg-info → process_bigraph-1.8.5}/PKG-INFO +2 -1
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/composite.py +137 -77
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/composite_generator.py +15 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/emitter.py +10 -0
- process_bigraph-1.8.5/process_bigraph/events.py +824 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/nextflow.py +23 -1
- process_bigraph-1.8.5/process_bigraph/nextflow_deploy.py +430 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/ray.py +24 -3
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/run_composite.py +74 -3
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/run_step.py +57 -1
- {process_bigraph-1.8.4 → process_bigraph-1.8.5/process_bigraph.egg-info}/PKG-INFO +2 -1
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph.egg-info/SOURCES.txt +2 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph.egg-info/requires.txt +1 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/pyproject.toml +6 -1
- process_bigraph-1.8.5/tests/test_composite_generator_analyses.py +14 -0
- process_bigraph-1.8.4/process_bigraph/nextflow_deploy.py +0 -140
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/AUTHORS.md +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/LICENSE +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/README.md +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/artifacts.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/bundle.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/composite_discovery.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/composite_spec.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/config_helpers.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/core_introspection.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/draft_process.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/experiments/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/experiments/minimal_gillespie.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/plumbing.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/bigraphical_reactive_system.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/cached.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/dynamic_structure.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/examples.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/growth_division.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/parameter_scan.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/reaction.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/processes/simulation.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/_git_worker.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/clusters/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/clusters/ec2_ssm.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/git.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/parallel.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/pool.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/rest.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/protocols/session.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/run.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/scheduling.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/server/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/server/rest.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/server/start.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/templates.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/types/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/types/process.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualization.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/distribution.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/heatmap.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/param_vs_observable.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/phase_space.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/time_series.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/visualizations/timeseries_from_observables.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/wiring_translator.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/workflow/__init__.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/workflow/backend.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/workflow/provision.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/workflow/recipe.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph/workflow/tasks.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph.egg-info/dependency_links.txt +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/process_bigraph.egg-info/top_level.txt +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/setup.cfg +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/tests/test_process_decorator.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/tests/test_reaction_step.py +0 -0
- {process_bigraph-1.8.4 → process_bigraph-1.8.5}/tests/test_units.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: process-bigraph
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5
|
|
4
4
|
Summary: protocol and execution for compositional systems biology
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
License-File: AUTHORS.md
|
|
9
9
|
Requires-Dist: bigraph-schema>=1.6.0
|
|
10
|
+
Requires-Dist: requests>=2.31
|
|
10
11
|
Requires-Dist: matplotlib
|
|
11
12
|
Requires-Dist: pint>=0.24.4
|
|
12
13
|
Requires-Dist: scipy>=1.8
|
|
@@ -18,7 +18,6 @@ import math
|
|
|
18
18
|
import inspect
|
|
19
19
|
import time as _time
|
|
20
20
|
import contextvars
|
|
21
|
-
import numpy as np
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
# Set by ``Composite.run`` for the duration of the run loop so Steps
|
|
@@ -38,76 +37,14 @@ def get_current_composite():
|
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
# ---------------------------------------------------------------------------
|
|
41
|
-
#
|
|
40
|
+
# Observability
|
|
42
41
|
# ---------------------------------------------------------------------------
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
_TRACE_PATH = os.environ.get('PROCESS_BIGRAPH_TRACE_FILE')
|
|
50
|
-
_TRACE_FH = open(_TRACE_PATH, 'a', buffering=1) if _TRACE_PATH else None
|
|
51
|
-
|
|
52
|
-
# Default for Composite._profile_per_process (per-path invoke attribution in
|
|
53
|
-
# TimingSummary.per_process). Off by default — the write is on the hot path —
|
|
54
|
-
# and flippable per-run via the env var without a code change.
|
|
55
|
-
_PROFILE_PER_PROCESS_DEFAULT = bool(
|
|
56
|
-
os.environ.get('PROCESS_BIGRAPH_PROFILE_PROCESSES'))
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _summarize_value(value, depth=0):
|
|
60
|
-
"""Lightweight, JSON-safe summary of an update fragment.
|
|
61
|
-
|
|
62
|
-
Tries to surface the values most useful for diffing two traces:
|
|
63
|
-
- Scalars and short strings inlined.
|
|
64
|
-
- Numpy arrays summarized as ``{shape, sum, mean, head}``.
|
|
65
|
-
- Dicts recursed (capped depth).
|
|
66
|
-
- Lists/tuples shown as their first few items.
|
|
67
|
-
"""
|
|
68
|
-
if depth > 3:
|
|
69
|
-
return f'<{type(value).__name__}>'
|
|
70
|
-
if value is None or isinstance(value, (bool, int, float, str)):
|
|
71
|
-
return value
|
|
72
|
-
if isinstance(value, np.ndarray):
|
|
73
|
-
try:
|
|
74
|
-
return {
|
|
75
|
-
'_np': True,
|
|
76
|
-
'shape': list(value.shape),
|
|
77
|
-
'dtype': str(value.dtype),
|
|
78
|
-
'sum': float(value.sum()) if value.dtype.kind in 'fi' else None,
|
|
79
|
-
'head': value.flatten()[:5].tolist() if value.size else [],
|
|
80
|
-
}
|
|
81
|
-
except Exception:
|
|
82
|
-
return f'<ndarray shape={value.shape}>'
|
|
83
|
-
if isinstance(value, dict):
|
|
84
|
-
return {k: _summarize_value(v, depth + 1) for k, v in list(value.items())[:32]}
|
|
85
|
-
if isinstance(value, (list, tuple)):
|
|
86
|
-
return [_summarize_value(v, depth + 1) for v in value[:10]]
|
|
87
|
-
return f'<{type(value).__name__}>'
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _trace_invoke(path, instance, state, interval, update):
|
|
91
|
-
"""Append one JSONL record describing a process/step invocation."""
|
|
92
|
-
if _TRACE_FH is None:
|
|
93
|
-
return
|
|
94
|
-
try:
|
|
95
|
-
gt = state.get('global_time') if isinstance(state, dict) else None
|
|
96
|
-
rec = {
|
|
97
|
-
'path': list(path) if isinstance(path, tuple) else path,
|
|
98
|
-
'cls': type(instance).__name__,
|
|
99
|
-
'gt': gt,
|
|
100
|
-
'interval': interval,
|
|
101
|
-
'input': _summarize_value(state),
|
|
102
|
-
'output': _summarize_value(update),
|
|
103
|
-
}
|
|
104
|
-
_TRACE_FH.write(json.dumps(rec, default=str) + '\n')
|
|
105
|
-
except Exception as exc:
|
|
106
|
-
# Tracing must never break a sim. Log a single line and continue.
|
|
107
|
-
try:
|
|
108
|
-
_TRACE_FH.write(json.dumps({'trace_error': str(exc)}) + '\n')
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
42
|
+
# Invocation tracing, per-process profiling, the run heartbeat and exception
|
|
43
|
+
# context all live in ``process_bigraph.events`` (one module, one set of
|
|
44
|
+
# switches -- see its docstring). ``_summarize_value`` is re-exported here for
|
|
45
|
+
# callers that imported it from ``composite``.
|
|
46
|
+
from process_bigraph import events as _events
|
|
47
|
+
from process_bigraph.events import _summarize_value # noqa: F401 (re-export)
|
|
111
48
|
|
|
112
49
|
from dataclasses import dataclass, field
|
|
113
50
|
from typing import (
|
|
@@ -1302,6 +1239,16 @@ class Composite(Process):
|
|
|
1302
1239
|
# Initialization & Configuration
|
|
1303
1240
|
# ==============================
|
|
1304
1241
|
|
|
1242
|
+
@property
|
|
1243
|
+
def _em(self) -> '_events.EventEmitter':
|
|
1244
|
+
"""The process-wide event emitter (process_bigraph.events); resolved
|
|
1245
|
+
lazily so a Composite built before ``events.configure()`` still
|
|
1246
|
+
picks up the configured sinks."""
|
|
1247
|
+
em = self.__dict__.get('_events')
|
|
1248
|
+
if em is None:
|
|
1249
|
+
em = self._events = _events.get_emitter()
|
|
1250
|
+
return em
|
|
1251
|
+
|
|
1305
1252
|
def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
|
|
1306
1253
|
"""
|
|
1307
1254
|
Initialize the composite model from its config.
|
|
@@ -1505,7 +1452,19 @@ class Composite(Process):
|
|
|
1505
1452
|
# ``run()`` to get the per-path breakdown. Defaults to the
|
|
1506
1453
|
# ``PROCESS_BIGRAPH_PROFILE_PROCESSES`` env var so a run can be profiled
|
|
1507
1454
|
# without code changes.
|
|
1508
|
-
self.
|
|
1455
|
+
self._events = _events.get_emitter()
|
|
1456
|
+
self._profile_per_process: bool = self._events.detail_timing
|
|
1457
|
+
# ``run.start``/``run.end`` rate limit (per Composite instance). A
|
|
1458
|
+
# protocol runtime drives one Composite.run per remote step, so an
|
|
1459
|
+
# unthrottled pair is 2 x cells x ticks events. The first run and any
|
|
1460
|
+
# run that ends in error always emit; otherwise at most one pair per
|
|
1461
|
+
# ``heartbeat_s``, and the runs skipped in between are folded into the
|
|
1462
|
+
# next ``run.end`` (``runs=N`` with the summed totals).
|
|
1463
|
+
self._run_emitted_at: Optional[float] = None
|
|
1464
|
+
self._runs_folded: int = 0
|
|
1465
|
+
self._folded_total: float = 0.0
|
|
1466
|
+
self._folded_process_time: float = 0.0
|
|
1467
|
+
self._folded_ticks: int = 0
|
|
1509
1468
|
|
|
1510
1469
|
# Precompile view/project operations for fast runtime access.
|
|
1511
1470
|
self._compiled_links = {}
|
|
@@ -1548,7 +1507,13 @@ class Composite(Process):
|
|
|
1548
1507
|
for rt in runtimes:
|
|
1549
1508
|
flush = getattr(rt, 'flush_pending', None)
|
|
1550
1509
|
if flush is not None:
|
|
1551
|
-
|
|
1510
|
+
try:
|
|
1511
|
+
flush()
|
|
1512
|
+
except BaseException as exc:
|
|
1513
|
+
self._em.exception(
|
|
1514
|
+
exc, event_name='runtime.error', runtime=type(rt).__name__,
|
|
1515
|
+
global_time=self.state.get('global_time'))
|
|
1516
|
+
raise
|
|
1552
1517
|
|
|
1553
1518
|
def _partition_processes_by_runtime(self):
|
|
1554
1519
|
"""Inspect every process_path and split into:
|
|
@@ -2617,6 +2582,29 @@ class Composite(Process):
|
|
|
2617
2582
|
self._per_process_time = {}
|
|
2618
2583
|
run_start = _time.monotonic()
|
|
2619
2584
|
|
|
2585
|
+
# Observability (process_bigraph.events): a ``run`` span when spans
|
|
2586
|
+
# are on, ``run.start``/``run.end`` always (no-ops when no sink is
|
|
2587
|
+
# configured), heartbeat counters reset per run.
|
|
2588
|
+
em = self._events = _events.get_emitter()
|
|
2589
|
+
if em.detail_timing:
|
|
2590
|
+
self._profile_per_process = True
|
|
2591
|
+
self._run_ticks = 0 # per-Composite, so nested runs do not clobber the outer count
|
|
2592
|
+
# Rate limit: emit this pair if it is the first run on this Composite or
|
|
2593
|
+
# ``heartbeat_s`` has elapsed since the last emitted pair. An error at
|
|
2594
|
+
# the end overrides the decision (see ``finally``), because a failure is
|
|
2595
|
+
# never the event to drop.
|
|
2596
|
+
emit_pair = (
|
|
2597
|
+
self._run_emitted_at is None
|
|
2598
|
+
or (run_start - self._run_emitted_at) >= em.heartbeat_s)
|
|
2599
|
+
span = em.start_span('run', interval=interval) if (
|
|
2600
|
+
em.enabled and em.detail_spans and emit_pair) else None
|
|
2601
|
+
if emit_pair:
|
|
2602
|
+
em.event('run.start', interval=interval,
|
|
2603
|
+
n_processes=len(self.process_paths), n_steps=len(self.step_paths),
|
|
2604
|
+
parallel=bool(getattr(self, '_parallel_processes', False)),
|
|
2605
|
+
global_time=self.state.get('global_time'))
|
|
2606
|
+
status, error = 'ok', None
|
|
2607
|
+
|
|
2620
2608
|
# Set the current-Composite contextvar so nested Steps can
|
|
2621
2609
|
# reach the parent Composite (e.g. for CompositeDivision to
|
|
2622
2610
|
# read parent cell tree state without going through wires).
|
|
@@ -2625,8 +2613,51 @@ class Composite(Process):
|
|
|
2625
2613
|
_cc_token = current_composite_var.set(self)
|
|
2626
2614
|
try:
|
|
2627
2615
|
return self._run_inner(interval, force_complete, run_start)
|
|
2616
|
+
except BaseException as exc:
|
|
2617
|
+
status, error = 'error', f'{type(exc).__name__}: {exc}'
|
|
2618
|
+
raise
|
|
2628
2619
|
finally:
|
|
2629
2620
|
current_composite_var.reset(_cc_token)
|
|
2621
|
+
if em.enabled:
|
|
2622
|
+
total = _time.monotonic() - run_start
|
|
2623
|
+
# An error always reports, even when the rate limit had
|
|
2624
|
+
# suppressed this run's ``run.start``.
|
|
2625
|
+
report = emit_pair or status == 'error'
|
|
2626
|
+
if not report:
|
|
2627
|
+
self._runs_folded += 1
|
|
2628
|
+
self._folded_total += total
|
|
2629
|
+
self._folded_process_time += self.process_update_time
|
|
2630
|
+
self._folded_ticks += self._run_ticks
|
|
2631
|
+
else:
|
|
2632
|
+
runs = self._runs_folded + 1
|
|
2633
|
+
agg_total = self._folded_total + total
|
|
2634
|
+
agg_process = self._folded_process_time + self.process_update_time
|
|
2635
|
+
payload = {
|
|
2636
|
+
'status': status, 'error': error,
|
|
2637
|
+
'runs': runs,
|
|
2638
|
+
'total': round(agg_total, 6),
|
|
2639
|
+
'process_time': round(agg_process, 6),
|
|
2640
|
+
'framework_time': round(agg_total - agg_process, 6),
|
|
2641
|
+
'ticks': self._folded_ticks + self._run_ticks,
|
|
2642
|
+
'global_time': self.state.get('global_time'),
|
|
2643
|
+
}
|
|
2644
|
+
if not emit_pair:
|
|
2645
|
+
# The pair is unbalanced: this end has no matching
|
|
2646
|
+
# start, because the error overrode the rate limit.
|
|
2647
|
+
payload['start_suppressed'] = True
|
|
2648
|
+
if self._per_process_time:
|
|
2649
|
+
top = sorted(self._per_process_time.items(), key=lambda kv: kv[1], reverse=True)[:5]
|
|
2650
|
+
payload['top5'] = [
|
|
2651
|
+
['/'.join(str(p) for p in k) if isinstance(k, tuple) else str(k), round(v, 6)]
|
|
2652
|
+
for k, v in top]
|
|
2653
|
+
em.event('run.end', level='error' if status == 'error' else 'info', **payload)
|
|
2654
|
+
self._run_emitted_at = _time.monotonic()
|
|
2655
|
+
self._runs_folded = 0
|
|
2656
|
+
self._folded_total = 0.0
|
|
2657
|
+
self._folded_process_time = 0.0
|
|
2658
|
+
self._folded_ticks = 0
|
|
2659
|
+
if span is not None:
|
|
2660
|
+
span.end(status, error)
|
|
2630
2661
|
|
|
2631
2662
|
def _run_inner(self, interval: float, force_complete: bool,
|
|
2632
2663
|
run_start: float) -> None:
|
|
@@ -2637,8 +2668,11 @@ class Composite(Process):
|
|
|
2637
2668
|
self.run_steps(self.to_run)
|
|
2638
2669
|
self.to_run = []
|
|
2639
2670
|
|
|
2671
|
+
em = self._em
|
|
2640
2672
|
while self.state['global_time'] < end_time or force_complete:
|
|
2641
2673
|
full_step = math.inf
|
|
2674
|
+
self._run_ticks = getattr(self, '_run_ticks', 0) + 1
|
|
2675
|
+
em.heartbeat(global_time=self.state['global_time'])
|
|
2642
2676
|
|
|
2643
2677
|
# Partition processes: ones whose runtime opts into batched
|
|
2644
2678
|
# tick_lifecycle vs the regular per-process path. The
|
|
@@ -2991,7 +3025,11 @@ class Composite(Process):
|
|
|
2991
3025
|
|
|
2992
3026
|
if len(due) > 1:
|
|
2993
3027
|
pool = self._get_step_executor(len(due))
|
|
2994
|
-
|
|
3028
|
+
# Carry the observability span context (a ContextVar) into the
|
|
3029
|
+
# worker threads: each call runs in its own copy of this thread's
|
|
3030
|
+
# context, so events emitted from a worker carry the run's ids.
|
|
3031
|
+
snapshot = contextvars.copy_context()
|
|
3032
|
+
results = list(pool.map(lambda item: snapshot.copy().run(_invoke, item), due))
|
|
2995
3033
|
else:
|
|
2996
3034
|
results = [_invoke(due[0])]
|
|
2997
3035
|
|
|
@@ -3034,9 +3072,25 @@ class Composite(Process):
|
|
|
3034
3072
|
# Strip schema-specific metadata from the state (once, unless caller did)
|
|
3035
3073
|
clean_state = states if already_clean else strip_schema_keys(states)
|
|
3036
3074
|
|
|
3037
|
-
# Invoke the process and retrieve a wrapped SyncUpdate object
|
|
3075
|
+
# Invoke the process and retrieve a wrapped SyncUpdate object.
|
|
3076
|
+
# On failure, record WHICH process/step, at what global_time, with a
|
|
3077
|
+
# summary of the state it was handed (process_bigraph.events), then
|
|
3078
|
+
# re-raise the ORIGINAL exception unchanged -- callers may classify
|
|
3079
|
+
# control-flow exceptions by type, so the type must propagate as is.
|
|
3038
3080
|
t0 = _time.monotonic()
|
|
3039
|
-
|
|
3081
|
+
try:
|
|
3082
|
+
update = process['instance'].invoke(clean_state, interval)
|
|
3083
|
+
except BaseException as exc:
|
|
3084
|
+
self._em.exception(
|
|
3085
|
+
exc,
|
|
3086
|
+
path='/'.join(str(p) for p in path) if isinstance(path, (list, tuple)) else str(path),
|
|
3087
|
+
cls=type(process['instance']).__name__,
|
|
3088
|
+
address=process.get('address'),
|
|
3089
|
+
is_step=(interval == -1.0),
|
|
3090
|
+
interval=interval,
|
|
3091
|
+
global_time=self.state.get('global_time') if isinstance(self.state, dict) else None,
|
|
3092
|
+
state_summary=_events.summarize_state(clean_state))
|
|
3093
|
+
raise
|
|
3040
3094
|
dt = _time.monotonic() - t0
|
|
3041
3095
|
self.process_update_time += dt
|
|
3042
3096
|
# Per-process attribution is opt-in (a dict write on the hot path);
|
|
@@ -3046,7 +3100,7 @@ class Composite(Process):
|
|
|
3046
3100
|
if self._profile_per_process:
|
|
3047
3101
|
key = tuple(path) if isinstance(path, (list, tuple)) else (path,)
|
|
3048
3102
|
self._per_process_time[key] = self._per_process_time.get(key, 0.0) + dt
|
|
3049
|
-
if
|
|
3103
|
+
if self._em.detail_invoke:
|
|
3050
3104
|
# Resolve SyncUpdate / Defer to plain dict for the trace; the
|
|
3051
3105
|
# invoke return is opaque (SyncUpdate wraps a dict). We only need
|
|
3052
3106
|
# a snapshot for the trace, so pull .get() if available.
|
|
@@ -3054,7 +3108,7 @@ class Composite(Process):
|
|
|
3054
3108
|
resolved = update.get() if hasattr(update, 'get') else update
|
|
3055
3109
|
except Exception:
|
|
3056
3110
|
resolved = '<unresolvable>'
|
|
3057
|
-
|
|
3111
|
+
self._em.invoke(path, process['instance'], clean_state, interval, resolved)
|
|
3058
3112
|
|
|
3059
3113
|
# Return a deferred object that will project the update when requested.
|
|
3060
3114
|
# The projector is the module-level ``_project_process_update`` (not a
|
|
@@ -3219,8 +3273,14 @@ class Composite(Process):
|
|
|
3219
3273
|
finally:
|
|
3220
3274
|
uninstall_reconcile_sink(prev_sink)
|
|
3221
3275
|
update_paths = summary.paths
|
|
3276
|
+
self._em.count(leaf_paths_touched=len(update_paths))
|
|
3222
3277
|
if summary.has_structural:
|
|
3223
3278
|
had_structural_sentinels = True
|
|
3279
|
+
self._em.count(structural_changes=1)
|
|
3280
|
+
self._em.event(
|
|
3281
|
+
'structure.changed', n_paths=len(update_paths),
|
|
3282
|
+
sample_paths=['/'.join(str(p) for p in path) for path in update_paths[:8]],
|
|
3283
|
+
global_time=self.state.get('global_time'))
|
|
3224
3284
|
|
|
3225
3285
|
if combined_update:
|
|
3226
3286
|
# NOTE: previously this path also forced
|
|
@@ -42,6 +42,11 @@ class GeneratorEntry:
|
|
|
42
42
|
# a Study is built on top of this composite the dashboard merges these
|
|
43
43
|
# defaults into its visualizations list; Studies can still declare extras.
|
|
44
44
|
visualizations: list[dict] = field(default_factory=list)
|
|
45
|
+
# Canonical analyses that ship with this composite. Each entry is a
|
|
46
|
+
# Study-spec analysis dict ({name, params?}) or a scale-grouped block,
|
|
47
|
+
# matching CompositeSpec.analyses. Merged under a run's config-declared
|
|
48
|
+
# analyses when a composite runs standalone (config wins).
|
|
49
|
+
analyses: list[dict] = field(default_factory=list)
|
|
45
50
|
# Emitter(s) this composite ships as its default observation sink. Each
|
|
46
51
|
# entry is a lightweight ``{address, config, paths?}`` dict: ``address`` is
|
|
47
52
|
# the registered emitter link (e.g. ``"local:ParquetEmitter"``), ``config``
|
|
@@ -80,6 +85,7 @@ def _entry_for(spec) -> GeneratorEntry:
|
|
|
80
85
|
module=spec.module,
|
|
81
86
|
default_n_steps=spec.default_n_steps,
|
|
82
87
|
visualizations=spec.visualizations,
|
|
88
|
+
analyses=spec.analyses,
|
|
83
89
|
emitters=spec.emitters,
|
|
84
90
|
core_extensions=spec.core_extensions,
|
|
85
91
|
)
|
|
@@ -155,6 +161,7 @@ class _RegistryView:
|
|
|
155
161
|
module=getattr(entry, "module", ""),
|
|
156
162
|
default_n_steps=getattr(entry, "default_n_steps", None),
|
|
157
163
|
visualizations=list(getattr(entry, "visualizations", []) or []),
|
|
164
|
+
analyses=list(getattr(entry, "analyses", []) or []),
|
|
158
165
|
emitters=list(getattr(entry, "emitters", []) or []),
|
|
159
166
|
core_extensions=list(getattr(entry, "core_extensions", []) or []),
|
|
160
167
|
)
|
|
@@ -180,6 +187,7 @@ def composite_generator(
|
|
|
180
187
|
description: str = "",
|
|
181
188
|
parameters: dict[str, dict] | None = None,
|
|
182
189
|
visualizations: list[dict] | None = None,
|
|
190
|
+
analyses: list[dict] | None = None,
|
|
183
191
|
emitters: list[dict] | None = None,
|
|
184
192
|
default_n_steps: int | None = None,
|
|
185
193
|
core_extensions: list[Callable[[Any], Any]] | None = None,
|
|
@@ -209,6 +217,12 @@ def composite_generator(
|
|
|
209
217
|
composite author considers canonical) without having to hand-author them
|
|
210
218
|
in every Study spec.
|
|
211
219
|
|
|
220
|
+
`analyses` declares the canonical analyses that ship with this composite.
|
|
221
|
+
Each entry is a Study-spec analysis dict (``{name, params?}``) or a
|
|
222
|
+
scale-grouped block, matching ``CompositeSpec.analyses``. Mirrors
|
|
223
|
+
``visualizations``: a run built on this composite standalone merges these
|
|
224
|
+
defaults under any config-declared analyses (config wins).
|
|
225
|
+
|
|
212
226
|
`emitters` (optional) declares the default observation sink(s) this
|
|
213
227
|
composite ships with. Each entry is a lightweight
|
|
214
228
|
``{"address": "local:ParquetEmitter", "config": {...}, "paths": [...]}``
|
|
@@ -266,6 +280,7 @@ def composite_generator(
|
|
|
266
280
|
description=description,
|
|
267
281
|
parameters=parameters,
|
|
268
282
|
visualizations=visualizations,
|
|
283
|
+
analyses=analyses,
|
|
269
284
|
emitters=validated_emitters,
|
|
270
285
|
default_n_steps=default_n_steps,
|
|
271
286
|
core_extensions=core_extensions,
|
|
@@ -584,6 +584,16 @@ class ConsoleEmitter(Emitter):
|
|
|
584
584
|
def tree_copy(state):
|
|
585
585
|
'''Deep copy utility for nested simulation state (excluding Edge instances).'''
|
|
586
586
|
if isinstance(state, dict):
|
|
587
|
+
# A realized process/step node carries its live Edge under 'instance'.
|
|
588
|
+
# Its remaining keys (address/config/inputs/outputs/interval/_type) are
|
|
589
|
+
# static wiring, not observable state — deep-copying them every tick is
|
|
590
|
+
# a large, pointless cost for composites that embed a process under each
|
|
591
|
+
# agent (e.g. cells/<id>/chemotaxis: ~34 of 49 nodes/cell are the process
|
|
592
|
+
# spec, a ~15x blow-up paid every emit). The live Edge was already
|
|
593
|
+
# dropped below; drop the whole node so process specs never reach the
|
|
594
|
+
# emitted history.
|
|
595
|
+
if isinstance(state.get('instance'), Edge):
|
|
596
|
+
return None
|
|
587
597
|
return {k: v for k, v in ((k, tree_copy(v)) for k, v in state.items()) if v is not None}
|
|
588
598
|
if isinstance(state, np.ndarray):
|
|
589
599
|
return state.copy()
|