{% if name_chip %}{{ name_chip }}{% endif %}{{ display_name }} {# Task 4: deterministic study kind (biological/computational/theoretical), inferred by lib/study_kind.py and wired into the template context by Task 1. Small pill beside the title, before the phase pill. #} {{ study.kind }} {% if study.phase %} {{ study.phase }} {% endif %}
{# Launch actions only (▶ / ↻). The downloads live where they're used — the ↓ figures button is at the top of the Visualizations tab, and ↓ notebook is in the Model tab; the investigation card carries them too. Keeping the header to the run/reproduce actions declutters it (they need a live backend and are hidden in the snapshot). #} {# item 110: dispatch ANY registered process-bigraph composite_id (e.g. pbg-native's v2ecoli.composites.lineage_ray_batch, item 101/109) to the remote compute backend -- independent of this study's own pinned baseline composite. Remote-pinned only, same gate as "Run current spec" (see _dispatchRemoteComposite in study-detail.js). #}status {% if _cgv and _cgv.diverges_from_authored %} ⚠ code: {{ _cgv.result }} · authored: {{ study.gate_status }} {% endif %} {% for _g in gate_ladder %} {% endfor %} {% if study.implementation_status %} Implementation{{ study.implementation_status }} {% endif %}
Q {{ _q }}
{% endif %}Claim
{{ study.claim or study.question or '' }}
{% if study.biological_summary %}Test
{{ study.experiment or study.objective or '' }}
{% if _bl or _bt %} {% endif %}Result
{{ study.result or _report.main_insight or (_f0.statement if _f0 else '') or '' }}
{% if result_figure %}🔬 Findings — what this study taught us
{% for f in study.findings %} {# Findings may carry `statement` (interpretation-tier) or only `summary`. Skip a finding with neither, so an empty study doesn't render blank ◆ cards; and render whichever text is present. #} {% set _text = f.statement or f.summary %} {% if _text %} {% set _status = f.status or 'novel' %} {% set _glyph = {'confirms': '✓', 'partial': '◐', 'contradicts': '✗', 'novel': '◆'}.get(_status, '◆') %} {% set _ftok = (f.evidence.from_test or '').split(' ')[0] if f.evidence else '' %} {% set _rtok = (f.evidence.from_run or '').split(' ')[0] if f.evidence else '' %}
{{ _glyph }}
{{ _text }}
{% if f.evidence and f.evidence.from_test %}
via {{ f.evidence.from_test }}
{% elif f.evidence and f.evidence.from_run %}
in run {{ f.evidence.from_run }}
{% endif %}
{{ f.evidence.from_test }} · {% endif %}
{% if f.evidence.from_run %}in run {{ f.evidence.from_run }}{% endif %}
{% if f.evidence.window %} ({{ f.evidence.window }}){% endif %}
{% if f.evidence.smoking_gun %}smoking gun
{{ f.evidence.smoking_gun }}{{ rid }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}
pass_if: {{ (_citedtest.pass_if or _citedtest.expect) | humanize_assertion }}{{ c }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}
Expert ref: {{ f.expert_reference.doc }}{% if f.expert_reference.section %} ({{ f.expert_reference.section }}){% endif %}
{% if f.expert_reference.quote %}{{ f.expert_reference.quote }}{% endif %} {% if f.expert_reference.note %}
Model composition
{# Download the runnable notebook for this study's investigation (includes this study, with its composite + exposed parameters). Lives in the Model tab — where the composition/params are — rather than the header. #}Loading composite card(s)…
Analyses to run — which of the workspace's ANALYSIS_REGISTRY entries to compute on dispatch
Runnable models — the composite(s) this study simulates
{% for b in study.baseline %}{{ b.composite }}
{% else %}no composite declared{% endif %}
{{ v }}
{% endfor %}
{% for k, v in _diverging_params.items() %}
{{ k }}={{ v }}
⚠ differs — baseline: {{ b.params[k] }}
{% endfor %}
Loading configuration…
{{ _cond_baseline.composite }}
{% else %}no composite declared{% endif %}
{{ v }}
{% endfor %}
Loading configuration…
Conditions — what we set up to test it
The experimental knobs: how variants perturb the baseline, and which model settings still need a human-set value.
{% if _cond.variants %}| Variant | Composite / base | Parameter overrides | Notes |
|---|---|---|---|
{{ v.name or '?' }} |
{% if v.composite %}{{ v.composite }}
{% elif v.base_composite %}{{ v.base_composite }}
{% else %}(inherits baseline){% endif %}
|
{% if _ovr %}
{% for k, val in _ovr.items() %}
{{ k }}=
{% endfor %}
{% else %}
(no overrides)
{% endif %}
{{ val }} |
{{ v.description or v.notes or '' }} |
Edit a value in the Current column and click Save. The next pbg_runner invocation for this study reads the updated value from study.yaml.
| Name | Type | Default | Current | Range | Gate | |
|---|---|---|---|---|---|---|
{{ e.name }} |
{{ e.type or '' }} | {{ e.default }} |
{% if e.options %} {# A declared finite value set — select from it rather than typing a value that has to exactly match one of e.options from memory. #} {% else %} {% endif %} {# Fable #14 — reuse the SAME required-before-run gate signal the header badge above is computed from, inline per-row. #} {% if e.gate == 'required-before-run' and (e.current is none or e.current == '') %} ⚠ needs a value {% endif %} |
{% if e.range %}[{{ e.range[0] }}, {{ e.range[1] }}]
{% elif e.options %}{{ e.options | join(' | ') }}
{% else %}—{% endif %}
|
{% if e.gate == 'required-before-run' %}required {% else %}optional{% endif %} |
Model change
{% if study.model_change.base_model %}Base model: {{ study.model_change.base_model }}
New processes:
-
{% for p in study.model_change.new_processes %}
{{ p.name }}({{ p.kind or 'Process' }}{% if p.requirement_id %} · req: {{ p.requirement_id }}{% endif %}) {% endfor %}
New state variables:
-
{% for v in study.model_change.new_state_variables %}
{{ v.id or v }}{% if v.kind %} ({{ v.kind }}{% if v.units %}, {{ v.units }}{% endif %}){% endif %} {% endfor %}
New parameters:
-
{% for p in study.model_change.new_parameters %}
{{ p.name }}{% if p.default is defined %} = {{ p.default }}{% endif %}{% if p.units %} ({{ p.units }}){% endif %} {% endfor %}
New listeners:
-
{% for l in study.model_change.new_listeners %}
{{ l.name }}{% if l.emits %} →{{ l.emits | join(', ') }}{% endif %} {% endfor %}
Modified processes:
-
{% for m in study.model_change.modified_processes %}
-
{% if m is mapping %}
{% if m.why %}
{{ m.name }}{% if m.status %}{{ m.status }}{% endif %} {% if m.requirement_id %}req: {{ m.requirement_id }}{% endif %}{{ m.why }}{% endif %} {% else %} {{ m }} {% endif %}
{% endfor %}
{{ study.model_change.notes }}
{% endif %}Implementation requirements ({{ study.implementation_requirements | length }} items)
Concrete pieces of work that gate this study from Design → Build. Each generates a task.
{% for r in study.implementation_requirements %} {% if r._prose or r is string %} {# Prose-form requirement (authored as `implementation_requirements: |`). #}{{ r.id }} — {{ r.title }} {% if r.effort %}{{ r.effort }}{% endif %} {% if r.kind %}[{{ r.kind }}]{% endif %}
{% if r.description %}{{ r.description }}
{% endif %} {% if r.steps %}Steps:
-
{% for s in r.steps %}
- {{ s }} {% endfor %}
Unblocks:
{% for u in r.unblocks %}{{ u }}{% if not loop.last %} · {% endif %}{% endfor %}
Deferred until: {{ r.defer_until }}
Simulations
Loading…
Results
What came back from this study's runs — a per-store preview of the latest run, plus the raw emitter store (zarr/parquet/runs.db) for every run.
Latest run preview
Per-store sparkline + first/last/min/max from the most recent run's emitted trajectory. This is a downsampled PREVIEW, not the full data — use the per-store or Download-all links for the complete arrays.
Analyses
Result files written by this study's analysis steps.
Readouts
The Design-time emit contract: what the emitter is configured to persist, which store paths it declares, and the shape of what lands on disk. No observed values here — see Results for what actually came back from a run.
Emitter & config
The emitter class, its write-path module, emit
interval/buffer, output location, and emit scope — from
lib/emitters.py and this study's (or the workspace's)
runtime.default_emitter.
Emitted paths
Each row is an emitter store_path
the composite will save; authored entries in study.yaml add names,
descriptions, and units on top.
Outputs & shapes
Store path, dtype, shape
(n_steps, …), units, and a best-effort byte estimate for each emitted
leaf.
How observables tie to Expected Behavior
Observables can be referenced by name in an
expected_behavior[i].measure block (e.g. kind: bulk_count, id:
"MONOMER0-160[c]"). When the dashboard validates a study, it cross-checks
that every measure resolves to a declared observable, or to a
built-in helper.
Tests
The bar. What must be true for this model to count as correct — the acceptance criteria, their bands, and the report cards that grade them.
{# C5 moved literature_anchors here verbatim from the dissolved Overview "Plan & provenance" group. C6 restyles it as an "Expectations" band: each anchor is a stated expectation this study is measured against, and where it carries a status (`status_in_v2ecoli`) that status is routed through the SAME G3 outcome_label/_class/_glyph filters the acceptance-criteria band below uses — met / conditional-pass / not met / not assessable — instead of a raw free-text string, so it reads in the page's one shared vocabulary. Read-only (editing anchors is out of scope, §7). Placed immediately before the acceptance-criteria band: a coherent "what we expected, then what the predefined bar is" grouping. Guarded so absent renders nothing (§2 R2). #} {% if study.literature_anchors %} {% set _exp_outcome_colors = { 'met': {'bg': '#d1fae5', 'fg': '#065f46'}, 'conditional': {'bg': '#fef3c7', 'fg': '#92400e'}, 'not-met': {'bg': '#fee2e2', 'fg': '#991b1b'}, 'not-assessable': {'bg': '#f1f5f9', 'fg': '#475569'}, } %}Expectations — from literature, mapped to the model observable that measures each
-
{% for anchor in study.literature_anchors %}
{% set _ocls = anchor.status_in_v2ecoli|outcome_class if anchor.status_in_v2ecoli else None %}
-
{{ anchor.expectation }} {% if anchor.status_in_v2ecoli %} {% set _oc = _exp_outcome_colors[_ocls] %} {{ anchor.status_in_v2ecoli|outcome_glyph }} {{ anchor.status_in_v2ecoli|outcome_label }} {% endif %}Model observable:{% if anchor.source %}
{{ anchor.model_observable }}Source: {{ anchor.source }}{% endif %}
{% endfor %}
Acceptance criteria
— the predefined bar, from {{ _sa.investigation }}'s persisted acceptance roll-up
-
{% for c in _sa.criteria %}
{% set _ocls = c.result|outcome_class %}
{% set _oc = _outcome_colors[_ocls] %}
-
{{ c.result|outcome_glyph }} {{ c.result|outcome_label }}
{{ c.behavior }}{% if c.behavior %} View test ↓ {% endif %}
{% endfor %}
Report cards — cross-card comparison; each card's full scorecard is inline at its row in Behavioral tests below
Behavioral tests
true
data_source: latest_run
-
{% for b in _bt %}
{% set _cls = b.classification or 'unclassified' %}
{% set _border = '#10b981' if _cls == 'primary' else ('#3b82f6' if _cls == 'supporting' else ('#f59e0b' if _cls == 'diagnostic' else ('#94a3b8' if _cls == 'regression' else '#cbd5e1'))) %}
{% set _out = _outcomes.get(b.name) %}
{% set _res = _out.result if _out else None %}
{% set _pill_bg = '#d1fae5' if _res == 'PASS' else ('#fee2e2' if _res == 'FAIL' else ('#fef3c7' if _res == 'SKIP' else ('#fde68a' if _res == 'PARTIAL' else '#f1f5f9'))) %}
{% set _pill_fg = '#065f46' if _res == 'PASS' else ('#991b1b' if _res == 'FAIL' else ('#92400e' if _res == 'SKIP' else ('#92400e' if _res == 'PARTIAL' else '#475569'))) %}
{% set _pill_text = ('✓ PASS' if _res == 'PASS' else ('✗ FAIL' if _res == 'FAIL' else ('⏭ SKIP' if _res == 'SKIP' else ('◐ partial' if _res == 'PARTIAL' else '⏳ pending')))) %}
-
{% if (b.kind or 'behavioral') == 'report_card' %}
{# C6: the card expands INLINE at this row via a native — reveals the same rich content _renderRichReportCard(card) renders on the Tests tab, mounted lazily on first expand (JS binds a `toggle` listener; avoids paying for hidden Plotly/markup for cards nobody opens). The verdict pill stays on the summary (recoloured by the existing _fillReportCardModules); there is no more "View report card ↑" scroll-away link — the top panel is now plotly-comparison-only, so there is nothing up there to jump to for this card any more. #} {% else %} {# --- existing behavioral rendering, UNCHANGED, kept verbatim below --- #}{{ _pill_text }} {{ _cls }} {{ b.name or '(unnamed)' }} {% if b.requires_simulation %} requires:
{{ b.requires_simulation }}{% endif %}{{ b.description or b.en or '' }}{% if _out %}Latest result: {% for k, v in _out.items() if k != 'result' %}{% endif %} {% if b.measure or b.pass_if or b.given or b.expect %}{{ k }}: {{ v }}{% endfor %}{% endif %} {% if b.calibration_anchor %}Assertion
{# `measure` and (in the else branch) `expect` are rendered unconditionally here — only `given`/`pass_if`/`requires`/`cites` are behind their own `{% if %}`. When one of those two is simply absent, Jinja's Undefined sentinel isn't JSON-serializable and a bare `| tojson` raised `TypeError: Object of type Undefined is not JSON serializable` (a real, reproducible 500 for any behavior test that declares `pass_if` but no `measure`). `default(none)` converts the sentinel to a real `None` -> `null` first, matching the JSON-dump style of the rest of this block. #}{% if b.given %}given: {{ b.given|tojson }} {% endif %}measure: {{ b.measure|default(none)|tojson }} {% if b.pass_if %}pass_if: {{ b.pass_if|tojson }}{% else %}expect: {{ b.expect|default(none)|tojson }}{% endif %}{% if b.requires %} requires: {{ b.requires|tojson }}{% endif %}{% if b.cites %} cites: {{ b.cites|tojson }}{% endif %}{% endif %} {% if b.notes %}⚠ Calibration anchor
{{ b.calibration_anchor | tojson(indent=2) }}Note: {{ b.notes }}
{% endif %} {% endif %}
{% endfor %}
- No
expected_behavior:entries declared in study.yaml yet. Add some to see them appear here as test rows. Click "Run tests" to discover and execute additionalstudies/<slug>/tests/test_*.pyfiles alongside the auto-generated assertions.
Audit
Is the bar sound? Whether the tests themselves are rigorous and matched to the problem — sufficiency, where the model was sourced, and the rigor + reproducibility scorecards.
Build
Was it earned? The agentic model-building loop that iterated the model to clear the bar — the draft, the frozen tests, the iteration-by-iteration trajectory, and the result (or an honest give-up).
Verdict & conclusion
{# G7: honest attribution for the frozen verdict. No actor field is persisted with the conclusion card (viz/report_card/conclusion.verdict.json is {schema, overall, tracks, insight} — checked, not guessed) so this always renders the literal "unattributed" token today. `none` is passed explicitly — no actor field is read here, per the "no new schema fields" constraint. #}{{ none | actor_glyph }} {{ none | attribution_text }}
{# G8: frozen-record indicator (Fable §11.4). conclusion_card_frozen is ONLY set (study_spec.load_study_detail_spec) when viz/report_card/conclusion.verdict.json actually exists and parses — the file's presence IS the freeze signal, so this block is absent (never an empty placeholder) for a study whose verdict has never been persisted. `when` is the file's own mtime (fmt_ts, same convention as every run timestamp elsewhere on this page); `digest` is computed on the fly over the exact frozen payload via conclusion_digest — never stored, never fabricated, changes iff the payload changes. #} {% set _frozen = study.conclusion_card_frozen %} {% if _frozen %}
🔒 frozen {{ _frozen.when | fmt_ts }} UTC ·
{{ _frozen.payload | conclusion_digest }}
⚖️ Verdicts — three-track outcome (regression / empirical / explanatory)
Conclusion logic — gate decision
{% if _cl.if_primary_tests_pass %}- {% for u in _cl.if_primary_tests_pass.pipeline_unblocks %}
- {{ u }} {% endfor %}
- {% for d in _cl.if_primary_tests_fail.diagnose %}
- {{ d }} {% endfor %}
Conclusion
Plan & assumptions
{% if study.pipeline_gate %}Pipeline gate
Each study is a model-building gate: it answers "Can we safely build the next layer?"
{% if study.pipeline_gate.prerequisites %}{{ p }}{% if not loop.last %} · {% endif %}{% endfor %}
{{ p }}{% if not loop.last %} · {% endif %}{% endfor %}
Key assumptions
-
{% for a in study.key_assumptions %}
- {{ a }} {% endfor %}
Limitations
What this study explicitly does NOT validate (prevents overclaim).
-
{% for l in study.limitations %}
- {{ l }} {% endfor %}
Evidence
{% if _is_v3 %} {# ── Latest run outcomes ────────────────────────────────────────── #} {% set _latest = (study.runs or [])[-1] if study.runs else None %} {% if _latest %}Latest run outcomes
{{ _roll.total }} test outcome{{ '' if _roll.total == 1 else 's' }} — {{ _roll.PASS }} PASS · {{ _roll.FAIL }} FAIL{% if _roll.PARTIAL %} · {{ _roll.PARTIAL }} partial{% endif %}. Per-test outcomes (measured value · evaluator · band) are canonical on the Tests tab →.
{% endif %}
Synthesised read-only from canonical fields — edit findings,
limitations, and discovery_implications to change
this (and the generated report) together.
Claims (from findings)
{% if study.findings %}-
{% for f in study.findings %}{% if f.statement or f.summary %}
- {{ f.statement or f.summary }}{% if f.tier %} {{ f.tier }}{% endif %} {% endif %}{% endfor %}
No findings recorded yet.
{% endif %}Evidence (from findings)
{% set _has_ev = study.findings and (study.findings | selectattr('evidence') | list) %} {% if _has_ev %}-
{% for f in study.findings %}{% if f.evidence %}
- {% if f.evidence is string %}{{ f.evidence }}{% elif f.evidence is mapping %}{% for k, v in f.evidence.items() %}{{ k }}: {{ v }}{% if not loop.last %}; {% endif %}{% endfor %}{% else %}{% for e in f.evidence %}{{ e }}{% if not loop.last %}; {% endif %}{% endfor %}{% endif %} {% endif %}{% endfor %}
No evidence recorded on findings yet.
{% endif %}Follow-ups & decisions
{# ── Open debts — pre-run expert review ───────────────────────────────── C5: relocated verbatim from the dissolved Overview "Plan & provenance" group. These are open biological questions the planning is contingent on — grouped here under the Decision act's follow-ups/decisions area as "Open debts" (leads the group; they gate the decision). Guarded so absent renders nothing. The G7 attribution line + all {% if %} guards are preserved verbatim from the original card. #} {% if study.expert_decisions_needed is sequence and study.expert_decisions_needed is not string and study.expert_decisions_needed %}Open debts (pre-run expert review)
Open biological questions the planning is contingent on. A "wrong" answer here means a primary test threshold needs to change before the simulation is run, not after. Each question lists alternatives + the impact of the wrong choice + what downstream work it blocks.
- {% for a in q.alternatives %}
- {{ a }} {% endfor %}
- {% for b in q.blocks %}
- {{ b }} {% endfor %}
{{ q.requested_response }}
Discovery implications
Where this study's results leave the mechanism model — and what to investigate next.
{% if _di.resolved_uncertainties or _di.remaining_uncertainties %}✓ Resolved uncertainties
- {% for u in _di.resolved_uncertainties %}
- {{ u }} {% endfor %}
● Remaining uncertainties
- {% for u in _di.remaining_uncertainties %}
- {{ u }} {% endfor %}
Alternate hypotheses ({{ _di.alternate_hypotheses | length }})
{% for h in _di.alternate_hypotheses %}{{ e }}{% if not loop.last %}, {% endif %}{% endfor %}
{{ d }}{% if not loop.last %}, {% endif %}{% endfor %}
Mechanism update proposals ({{ _di.mechanism_update_proposals | length }})
{% for m in _di.mechanism_update_proposals %}{{ m.mechanism_node_or_edge or '(unspecified)' }}
{{ m.update_type or 'revise' }}
{% if m.confidence_change %}Δconfidence: {{ m.confidence_change }}{% endif %}
{% if m.requires_expert_approval %}needs expert approval{% endif %}
Follow-up study proposals (discovered) ({{ _di.followup_study_proposals | length }})
Click ➕ Add to investigation to spawn a new study node in the investigation graph.
{{ t }}{% if not loop.last %}, {% endif %}{% endfor %}
Follow-up studies — pick one to seed (authored)
Click Seed new study → on any non-existing follow-up to scaffold a child study.yaml.
The new study inherits this one as a pipeline_gate prerequisite and starts at phase: Design.
- {% for u in f.unblocks %}
- {{ u }} {% endfor %}
- {% for a in f.acceptance %}
- {{ a }} {% endfor %}
Limitations & provenance
{# ── Read-only synthesis: Next steps (C3) ──────────────────────────────── Next steps ← discovery_implications.followup_study_proposals (or follow_up_studies). Edit those source fields and this synthesis — and the generated report — update together. C5: the Limitations column that used to sit here was REMOVED to reconcile a duplicate — limitations now render once, in the richer moved "Plan & assumptions" block above (Group 1b), for v3 and non-v3 studies alike. #} {% set _di = study.discovery_implications or {} %} {% set _nexts = _di.followup_study_proposals or study.follow_up_studies or [] %}Next steps (from discovery implications)
{% if _nexts %}-
{% for n in _nexts %}
{% if n is string %}
- {{ n }} {% else %}
- {{ n.title or n.name or 'proposal' }}{% if n.why or n.motivation %} — {{ n.why or n.motivation }}{% endif %} {% endif %} {% endfor %}
No follow-up proposals recorded yet.
{% endif %}