# Project export: PaperAlive - Turn Scientific Papers into Experiments

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: PaperAlive compiles static scientific papers into evidence-linked, scientifically verified interactive experiments -making complex research explorable, reproducible, and trustworthy, entirely offline.
- Devpost: https://devpost.com/software/paperalive-turn-scientific-papers-into-experiments
- GitHub: https://github.com/victus17/paperalive-codex-plugin?tab=readme-ov-file
- Video: https://www.youtube.com/embed/zaA2Zm1kt5c?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — victus (2 commits)

## Devpost submission (written by the team)

### Inspiration

I have always been fascinated by scientific papers. They contain years of work, difficult ideas, carefully constructed models, and discoveries that can change how we understand the world. But something has always bothered me: after all that work, the result is usually compressed into a static document. A paper may describe a dynamic biological system, a physical process, or a mathematical model—but the reader still sees fixed equations, screenshots, and plots. Even when the underlying system is alive with movement and interaction, the publication itself remains frozen. An equation such as $$ \dot{x}_1 = \frac{\beta}{1+x_3^n} - \gamma x_1 $$ contains behavior. It can oscillate, stabilize, diverge, or react dramatically to a changed parameter. Yet most readers never get to experience that behavior directly. This creates a gap between reading research and understanding research. Students may recognize the symbols without developing intuition. Educators must rebuild demonstrations manually. Reviewers have to trace equations, parameters, figures, and supplementary material across many pages. Researchers often spend time reconstructing models before they can even begin exploring them. I built PaperAlive because I wanted scientific work to feel more tangible. My goal was not to create attractive animations loosely inspired by a paper. I wanted something much stricter: a system that could transform supported scientific papers into interactive experiments while keeping every equation, parameter, control, curve, and result connected to its original evidence. The paper should remain the source of truth. The experiment should make that truth explorable. That idea became PaperAlive: research you can touch—and audit.

### What it does

PaperAlive is a Codex-native scientific compiler that turns supported scientific papers into evidence-linked, scientifically verified interactive experiments. It is deliberately not a free-form visualization generator. A convincing animation is easy to create; a trustworthy scientific representation is much harder. PaperAlive follows a compiler-style pipeline: Extract evidence from a text-based PDF, Markdown document, LaTeX source, or supported data supplement. Evaluate compatibility before attempting to create an experiment. Resolve equations, symbols, parameters, units, initial conditions, domains, and source locations. Compile the evidence into PAER, the typed PaperAlive Experiment Representation. Run structural, semantic, dimensional, provenance, numerical, convergence, and invariant checks. Execute an independent Python reference calculation. Compare the result against a predeclared paper result or scientific invariant when one is available. Select a trusted visualization template from an allowlisted renderer registry. Build a self-contained offline Experiment Studio. Start a local server and return an immediately usable URL. If a paper does not contain enough information for a defensible model, PaperAlive does not guess. It returns a compatibility report explaining exactly what is missing. Possible outcomes include: SUPPORTED_AUTOMATIC SUPPORTED_WITH_REVIEW ILLUSTRATIVE_ONLY UNSUPPORTED Missing parameters, undefined symbols, incompatible units, absent initial conditions, unresolved evidence, unsupported mathematics, or unstable numerical results are hard failures—not details to be silently invented. A concrete demonstration: the repressilator The main PaperAlive demonstration uses the open-access paper “Data-driven model discovery and model selection for noisy biological systems” by Wu, McDermott, and MacLean. The paper describes a simplified repressilator: three proteins connected through a negative feedback loop. Each protein suppresses the next, producing oscillations that eventually settle into a stable limit cycle. PaperAlive turns that static model into: synchronized time-series plots of all three protein concentrations; a labelled three-dimensional mathematical phase space; interactive controls restricted to evidenced values and ranges; an equation walkthrough; comparison with the paper baseline; an independent numerical reference calculation; a visible trust status; and an evidence thread connecting every quantitative element back to the relevant paper location. A user can select a parameter and immediately inspect its source. Selecting a curve, equation, or phase-space object reveals the evidence behind it. Selecting the evidence returns the user to every connected representation. The visualization is therefore not separate from the scientific record. It is an interface into it. Supported experiment types The current version contains three carefully bounded scientific adapters: ODE systems: deterministic models with one to three state variables, shown through time series, phase portraits, and labelled three-state phase spaces. Particle systems: deterministic physical or conceptual particles with trajectories, forces, vectors, scale semantics, and invariant checks. Scalar fields: evidenced one-, two-, or three-dimensional fields represented through heatmaps, contours, and surfaces. PaperAlive chooses the representation from validated model semantics. It does not force every paper into 3D. Who it is for PaperAlive is designed for: students, who need to develop intuition instead of only memorizing equations; educators, who want interactive, source-grounded demonstrations without rebuilding every model manually; researchers, who want a faster path from publication to exploration; reviewers, who need to inspect whether equations, parameters, and reported results agree; science communicators, who want engagement without sacrificing scientific honesty; and curious readers, who want to experience what a paper actually describes.

### How we built it

I built PaperAlive with Codex and GPT-5.6 as an active engineering partner during OpenAI Build Week. Codex was not used only to generate isolated code snippets. I used it to reason across the entire system: scientific requirements, schema design, security boundaries, numerical methods, frontend behavior, accessibility, test strategy, plugin packaging, and documentation. I defined the central principle early: PaperAlive must fail honestly before it visualizes dishonestly. Codex helped translate that principle into concrete architecture, validation rules, negative test cases, and implementation decisions across the repository. A typed scientific intermediate representation At the center of PaperAlive is PAER, the PaperAlive Experiment Representation. PAER is defined by one canonical JSON Schema. It contains: source artifacts and cryptographic hashes; precise provenance anchors; quantities and units; equations represented as a closed abstract syntax tree; parameters and initial or boundary conditions; model and solver configuration; experiment controls; visualization mappings; validation evidence; trust status; warnings and limitations. TypeScript types are generated from the same schema used by Python validation, preventing the frontend and compiler from silently developing different ideas of what an experiment means. Equations are never stored as executable code. PaperAlive uses a closed, allowlisted mathematical AST and never calls eval, executes author scripts, runs notebook cells, or accepts arbitrary shaders or JavaScript from a paper. Independent scientific execution The interactive browser calculation and the scientific reference calculation are deliberately separate. The frontend uses deterministic TypeScript solvers running inside a Web Worker. The reference path uses Python with NumPy and SciPy. Results are compared using declared tolerances, convergence behavior, paper targets, or appropriate invariants. This separation matters: if the UI and the reference calculation shared the same implementation, they could reproduce the same mistake. Camera position, framerate, interpolation, display sampling, and visual scaling are downstream presentation choices. They cannot feed back into the scientific state. Trusted rendering Validated PAER semantics are mapped to fixed renderer templates: MathJax for accessible equations; Plotly for quantitative charts, fields, contours, and surfaces; React Three Fiber and Three.js for physical particle trajectories and labelled mathematical phase spaces; PDF.js for local evidence inspection. Unsupported mappings produce an evidence report rather than an improvised scene. Offline by design The generated Experiment Studio is a static React application built with TypeScript and Vite. A completed experiment requires: no runtime API key; no OpenAI SDK; no backend; no user login; no runtime model call; no CDN; and no external font service. After compilation, PaperAlive packages the experiment and starts a verified loopback-only server on 127.0.0.1. The user receives a clickable local URL and an exact command for stopping the server. This makes the resulting experiment portable, inspectable, private, and suitable for classrooms, review workflows, or sensitive local research environments.

### Challenges we ran into

Making AI useful without allowing it to invent science The biggest challenge was deciding where AI reasoning belongs—and where it must stop. Codex is excellent at reading context and connecting evidence across a paper. But scientific software cannot accept a value simply because it looks plausible. The solution was a strict boundary: Codex performs evidence-aware semantic interpretation, while deterministic local code enforces the schema, units, symbols, domains, provenance, numerical behavior, resource limits, and renderer selection. Any assumption required to make the model run must be explicitly supported. Otherwise, the quantitative build stops. Treating provenance as part of the product It was not enough to store a citation somewhere in a report. Every slider, equation, axis, curve, initial condition, comparison target, and quantitative spatial object needed a stable connection to evidence. That required provenance to become a first-class part of the data model and the interface—not an afterthought. Separating “looks correct” from “is verified” Scientific visualization can be dangerously persuasive. A smooth curve and polished 3D scene may hide incorrect equations, broken units, or unstable numerics. PaperAlive therefore validates before rendering. The weakest critical gate determines the visible trust status, and warnings remain visible even when other checks pass. Supporting useful papers without pretending to support every paper Many scientific papers cannot be converted honestly from the publication alone. They may omit parameters, depend on unavailable author code, use unsupported numerical methods, or describe qualitative work without an executable model. Instead of hiding those limitations, PaperAlive makes refusal a successful outcome. A precise evidence-backed explanation is more valuable than a fabricated experiment. Building a complete offline experience The final viewer had to remain interactive without a backend or runtime model connection. That required local rendering, Web Worker simulation, safe package import and export, bundled mathematical accessibility, local PDF evidence viewing, and a secure local preview server.

### Accomplishments we're proud of

I am especially proud that PaperAlive is a working end-to-end system rather than a visual prototype. The project now includes: an installable Codex plugin with a complete $paperalive workflow; paper and supplement extraction with source hashes and location anchors; a canonical typed scientific representation; structural, semantic, dimensional, provenance, numerical, convergence, invariant, and reproduction gates; independent Python and TypeScript scientific implementations; three bounded model adapters; a closed renderer registry; an offline interactive Experiment Studio; local evidence inspection and bidirectional provenance navigation; safe package import and export; a verified local preview server; a real open-access paper demonstration; synthetic ground-truth benchmarks for particle and scalar-field behavior; and a complete release gate covering linting, types, security architecture, unit tests, scientific evaluations, browser tests, accessibility, offline operation, and plugin validation. I am also proud of what PaperAlive refuses to do. It does not turn every paper into a generic 3D animation. It does not execute arbitrary author code. It does not silently choose missing values. It does not claim real-world validation when only numerical verification was performed. Scientific honesty is not a limitation of the product. It is the product.

### What we learned

The most important thing I learned is that trust can be an interaction design feature. A provenance link does not have to live in a forgotten appendix. It can be something the user touches. Validation does not have to be a hidden build step. It can be visible and understandable. Limitations do not weaken an experiment when they are communicated clearly; they make the result more useful. I also learned that interactivity changes how a reader approaches a model. Once an equation can be explored, people naturally begin asking better questions: What changes if this parameter moves? Which behavior comes directly from the paper? Which values were derived? Does the numerical result converge? Where did this curve come from? What would make this model unsupported? Codex was particularly powerful for a project like this because PaperAlive crosses many technical boundaries. It combines document processing, scientific computing, type systems, security, frontend visualization, plugin architecture, and accessibility. Codex helped maintain context across those layers while continuously testing whether the implementation still respected the original scientific principles. The project reinforced a simple idea for me: The most impressive scientific visualization is not the one that looks the most complex. It is the one that earns the reader's trust.

### What's next

for PaperAlive The current version establishes a strict and testable foundation. Next, I want to expand PaperAlive without weakening that foundation. Planned directions include: More isolated scientific adapters for additional model classes, each with its own evidence requirements, numerical verification, resource limits, and trusted renderers. A larger open benchmark library of papers with reproducible interactive experiments and clearly documented compatibility decisions. Author workflows that allow researchers to publish a PAER package alongside a traditional paper. Reviewer mode for comparing equations, parameter tables, code outputs, and claimed results during peer review. Classroom mode with guided questions, parameter challenges, accessible explanations, and instructor-defined exploration boundaries. Continuous reproducibility checks that can detect when dependencies, source artifacts, or numerical behavior change. Shareable offline experiment packages that preserve evidence, validation reports, results, and the interactive viewer as one auditable artifact. Community extension points for new adapters and renderers that must pass the same scientific and security contracts before they can be trusted. My long-term vision is for interactive, auditable experiments to become a normal companion to scientific publishing. A PDF should not be the end of a scientific idea. It should be the beginning of an exploration. PaperAlive gives scientific papers a pulse—without disconnecting them from the evidence that makes them trustworthy.

## README (from the GitHub repository)

<div align="center">

![PaperAlive — A Codex Plugin](docs/paperalive-thumbnail.png)

# PaperAlive — A Codex Plugin

### Turn scientific papers into experiments you can touch — and audit.

[![Codex Plugin](https://img.shields.io/badge/Codex-Plugin-111827?style=flat-square)](https://github.com/victus17/paperalive-codex-plugin)
[![Built with GPT-5.6](https://img.shields.io/badge/Built_with-GPT--5.6-54E1C1?style=flat-square)](#built-with-codex-and-gpt-56)
[![License: MIT](https://img.shields.io/badge/Code-MIT-54E1C1?style=flat-square)](LICENSE)
[![Runtime API](https://img.shields.io/badge/Runtime_API-None-54E1C1?style=flat-square)](#local-first-by-design)
[![Scientific status](https://img.shields.io/badge/Scientific_Output-Evidence--linked-F7C65C?style=flat-square)](#scientific-honesty-first)

</div>

Scientific papers are full of equations, parameters, initial conditions and results—but they are still usually experienced as static pages. **PaperAlive is a Codex-native scientific compiler that turns supported papers into interactive, evidence-linked experiments.**

It does not invent missing science and it does not force every paper into a flashy 3D scene. It extracts the evidence, checks whether the paper is computationally representable, builds a typed experiment model, runs deterministic scientific gates, reproduces supported results and opens a local interactive viewer.

![PaperAlive experiment studio showing a Repressilator phase-space view, synchronized plot, parameters and source evidence](docs/paperalive-hero.png)

## Why PaperAlive matters

A conventional visualization can make a paper easier to look at. PaperAlive is designed to make it **easier to understand, test and question**:

- change a reported parameter and immediately see its effect;
- compare a variation against the paper baseline;
- follow every quantitative control back to a source location;
- inspect equations, units, assumptions and validation evidence;
- export the experiment as a static offline package;
- receive an explicit refusal when the source cannot support an honest model.

The result is useful for students, educators, reviewers, researchers and curious readers who want to move from _reading a claim_ to _interacting with the model behind it_.

## Install in Codex

### Requirements

- Codex with plugin support
- Python 3.12 or newer
- internet access once to install the pinned Python packages
- no API key, backend, account or Node.js build step

### 1. Add the public marketplace

```bash
codex plugin marketplace add victus17/paperalive-codex-plugin
```

### 2. Install PaperAlive

```bash
codex plugin add paperalive@paperalive
```

### 3. Start a new Codex task

Attach a text-based scientific PDF and write:

```text
$paperalive Compile this paper into a validated interactive experiment.
```

PaperAlive returns the exact compatibility and trust status, the evidence-linked PAER, validation reports, reference results, a static site, an offline package and—after a successful compilation—a verified local URL such as `http://127.0.0.1:43127/`.

## Try the included Repressilator paper

This repository includes a real open-access paper and a reviewed PAER example:

- [`Wu_2025_Repressilator.pdf`](examples/repressilator/Wu_2025_Repressilator.pdf)
- [`repressilator-wu-2025.paperalive.json`](examples/repressilator/repressilator-wu-2025.paperalive.json)

Clone the repository, attach the PDF from `examples/repressilator/` to a new Codex task and use the prompt above. The paper contains a three-protein repressilator model with an oscillating limit cycle—an ideal example of how a static biological system can become an explorable experiment.

The source is Wu, McDermott & MacLean (2025), _Data-driven model discovery and model selection for noisy biological systems_, PLOS Computational Biology, DOI [`10.1371/journal.pcbi.1012762`](https://doi.org/10.1371/journal.pcbi.1012762), distributed under CC BY 4.0. Full attribution and the verified source hash are in [`examples/repressilator/ATTRIBUTION.md`](examples/repressilator/ATTRIBUTION.md).

## How it works

```mermaid
flowchart LR
  A["Paper + supplements"] --> B["Evidence extraction"]
  B --> C["Compatibility hard gates"]
  C -->|supported| D["Typed PAER"]
  C -->|insufficient evidence| X["Evidence report + refusal"]
  D --> E["Symbols · units · provenance"]
  E --> F["Reference simulation"]
  F --> G["Reproduction checks"]
  G --> H["Allowlisted renderer"]
  H --> I["Offline experiment studio"]
```

Codex performs the evidence-aware interpretation inside the authenticated task. The bundled compiler scripts then handle extraction, validation, units, numerical reference execution, rendering, packaging and local serving with deterministic local code.

## Built with Codex and GPT-5.6

PaperAlive was designed and implemented during OpenAI Build Week as a Codex-native developer tool. GPT-5.6 in Codex was an active engineering and scientific-reasoning collaborator throughout the primary build task—not a decorative mention added to the submission. Codex carried the project from the product specification through architecture, plugin implementation, scientific guardrails, the offline experiment studio, real-paper evaluation, packaging, documentation and release verification.

### Where Codex accelerated the workflow

| Workstream | How Codex and GPT-5.6 were used | Evidence in this release |
|---|---|---|
| Product architecture | Converted the product brief into an explicit compiler pipeline: bounded evidence extraction, compatibility gates, a typed intermediate representation, deterministic validation, independent reference execution and allowlisted rendering. | [PaperAlive skill](plugins/paperalive/skills/paperalive/SKILL.md), [scientific method](plugins/paperalive/skills/paperalive/references/scientific-method.md) |
| Scientific reasoning | Traced equations, symbols, parameter tables, units, initial conditions, source locations, solver settings and reproduction targets without treating a plausible guess as evidence. | [PAER authoring rules](plugins/paperalive/skills/paperalive/references/paer-authoring.md), [support matrix](plugins/paperalive/skills/paperalive/references/support-matrix.md) |
| Safety and failure behavior | Translated scientific-honesty requirements into hard gates: no inferred critical quantities, no author-code execution, no arbitrary executable expressions, no unsupported renderer generation and no silent downgrade of trust. | [failure policy](plugins/paperalive/skills/paperalive/references/failure-policy.md), deterministic compiler scripts |
| Plugin engineering | Implemented and iterated on the portable Codex skill, extraction and validation CLIs, unit checking, independent reference simulation, static viewer build, package creation and loopback-only preview server. | [installable plugin](plugins/paperalive/), [manifest](plugins/paperalive/.codex-plugin/plugin.json) |
| Product and interaction design | Refined the experiment studio around synchronized equation, parameter, plot, spatial and source-evidence navigation; baseline comparison; time control; explicit trust; limitations; and offline exports. | bundled static viewer under [plugin assets](plugins/paperalive/assets/viewer/) |
| Evaluation and release | Used a real open-access Repressilator paper to exercise the complete path, audited the output, prepared the focused no-build release and documented a judge-friendly installation and test route. | [sample paper and PAER](examples/repressilator/), this README |

### Key product and engineering decisions made with GPT-5.6 in Codex

1. **Build a scientific compiler, not a free-form visualization generator.** PaperAlive introduces PAER as a typed boundary between semantic paper understanding and execution, so a visually convincing scene can never substitute for a complete model.
2. **Make honest refusal a first-class product result.** Missing central equations, parameters, units, initial/boundary conditions, provenance or an 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 31 recognized source files, 7392 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — claimed on Devpost, not found in the code
- TypeScript (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (46 of 46)

```
.agents/plugins/marketplace.json
.gitignore
examples/repressilator/ATTRIBUTION.md
examples/repressilator/README.md
examples/repressilator/repressilator-wu-2025.paperalive.json
LICENSE
plugins/paperalive/.codex-plugin/plugin.json
plugins/paperalive/assets/paer.schema.json
plugins/paperalive/assets/requirements.txt
plugins/paperalive/assets/viewer/.vite/manifest.json
plugins/paperalive/assets/viewer/assets/index-Bu16gLoE.js
plugins/paperalive/assets/viewer/assets/index-wzXBae-a.css
plugins/paperalive/assets/viewer/assets/mathjax-B3RD-xLR.js
plugins/paperalive/assets/viewer/assets/pdf.worker.min-Cr_QfRGn.mjs
plugins/paperalive/assets/viewer/assets/pdfjs-DrumwR7S.js
plugins/paperalive/assets/viewer/assets/plotly-cartesian-O1LZrDR-.js
plugins/paperalive/assets/viewer/assets/plotly-gl3d-CBn4r4_j.js
plugins/paperalive/assets/viewer/assets/rolldown-runtime-aKtaBQYM.js
plugins/paperalive/assets/viewer/assets/simulation.worker-BFq1SIHp.js
plugins/paperalive/assets/viewer/assets/spatial-BYYkaVm8.js
plugins/paperalive/assets/viewer/assets/SpatialScene-C1Daznu-.js
plugins/paperalive/assets/viewer/assets/validation.worker-DMjJeQOd.js
plugins/paperalive/assets/viewer/index.html
plugins/paperalive/assets/viewer/paperalive-assets.json
plugins/paperalive/assets/viewer/sre/mathmaps/base.json
plugins/paperalive/assets/viewer/sre/mathmaps/en.json
plugins/paperalive/assets/viewer/sre/mathmaps/euro.json
plugins/paperalive/assets/viewer/sre/mathmaps/nemeth.json
plugins/paperalive/assets/viewer/sre/speech-worker.js
plugins/paperalive/LICENSE
plugins/paperalive/skills/paperalive/references/failure-policy.md
plugins/paperalive/skills/paperalive/references/paer-authoring.md
plugins/paperalive/skills/paperalive/references/scientific-method.md
plugins/paperalive/skills/paperalive/references/support-matrix.md
plugins/paperalive/skills/paperalive/references/visualization-rules.md
plugins/paperalive/skills/paperalive/scripts/_paperalive.py
plugins/paperalive/skills/paperalive/scripts/build_experiment.py
plugins/paperalive/skills/paperalive/scripts/extract_paper.py
plugins/paperalive/skills/paperalive/scripts/package_experiment.py
plugins/paperalive/skills/paperalive/scripts/run_reference.py
plugins/paperalive/skills/paperalive/scripts/serve_experiment.py
plugins/paperalive/skills/paperalive/scripts/validate_paer.py
plugins/paperalive/skills/paperalive/scripts/validate_units.py
plugins/paperalive/skills/paperalive/SKILL.md
README.md
THIRD-PARTY-NOTICES.md
```

### Dependencies

- plugins/paperalive/assets/requirements.txt: hypothesis@==6.156.6, jsonschema@==4.26.0, numpy@==2.5.1, pint@==0.25.3, PyMuPDF@==1.28.0, pytest@==9.1.1, PyYAML@==6.0.3, ruff@==0.15.21, scipy@==1.18.0

### Recent commits (newest first)

- docs: detail Codex and GPT-5.6 Build Week usage
- Release PaperAlive Codex plugin v1.0.0

## Key source files (fetched from GitHub, selected and truncated for size)

### THIRD-PARTY-NOTICES.md

```markdown
# Third-party notices

PaperAlive's original code is released under the MIT License. Bundled JavaScript and Python dependencies retain their upstream licenses.

## Repressilator example paper

Xiaojun Wu, MeiLu McDermott and Adam L. MacLean (2025), “Data-driven model discovery and model selection for noisy biological systems,” *PLOS Computational Biology* 21(1): e1012762. DOI [`10.1371/journal.pcbi.1012762`](https://doi.org/10.1371/journal.pcbi.1012762).

The unmodified publisher PDF is distributed under [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/) and is included at `examples/repressilator/Wu_2025_Repressilator.pdf`. Its SHA-256 is `f714ad44f892d1aa9d0c984ca412879e66ab4d107469ae4abdd4daacfd6a0e6d`.

PaperAlive's extracted evidence anchors, PAER encoding and independent numerical diagnostics are adaptations or derived material and are identified as such in the example and viewer.

## Runtime dependencies

The offline viewer bundles code from projects including React, Plotly.js, Three.js, React Three Fiber, MathJax and PDF.js. The local scientific compiler uses the exact direct Python versions pinned in `plugins/paperalive/assets/requirements.txt`, including PyMuPDF, NumPy, SciPy, Pint and jsonschema. All dependencies retain their respective upstream licenses; no paid runtime service is required.

```

### examples/repressilator/ATTRIBUTION.md

```markdown
# Source attribution

Xiaojun Wu, MeiLu McDermott and Adam L. MacLean (2025), “Data-driven model discovery and model selection for noisy biological systems,” *PLOS Computational Biology* 21(1): e1012762. DOI [`10.1371/journal.pcbi.1012762`](https://doi.org/10.1371/journal.pcbi.1012762).

Files in this directory were downloaded from the official PLOS publisher endpoints on 2026-07-15:

- `article.pdf` — [printable PDF](https://journals.plos.org/ploscompbiol/article/file?id=10.1371/journal.pcbi.1012762&type=printable), SHA-256 `f714ad44f892d1aa9d0c984ca412879e66ab4d107469ae4abdd4daacfd6a0e6d`;
- `article.xml` — [publisher XML/MathML](https://journals.plos.org/ploscompbiol/article/file?id=10.1371/journal.pcbi.1012762&type=manuscript), SHA-256 `3a7932b5a635e5a64d3d128260d6a8cb64f1946f0857a3e7cf53c03582c4b875`.

The article is distributed under [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/). The files are unmodified publisher artifacts. PaperAlive's extracted anchors, PAER encoding and independent numerical diagnostics are adaptations/derived material and are identified as such in the demo.


```

### plugins/paperalive/assets/requirements.txt

```
jsonschema==4.26.0
hypothesis==6.156.6
numpy==2.5.1
pint==0.25.3
PyMuPDF==1.28.0
PyYAML==6.0.3
pytest==9.1.1
ruff==0.15.21
scipy==1.18.0

```

### plugins/paperalive/assets/viewer/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="theme-color" content="#080b0d" />
    <link rel="icon" href="./favicon.svg" type="image/svg+xml" />
    <meta name="color-scheme" content="dark" />
    <meta
      name="description"
      content="PaperAlive turns supported scientific papers into traceable, testable interactive experiments."
    />
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
    />
    <title>PaperAlive — Research you can touch and audit</title>
    <script type="module" crossorigin src="./assets/index-Bu16gLoE.js"></script>
    <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-aKtaBQYM.js">
    <link rel="modulepreload" crossorigin href="./assets/spatial-BYYkaVm8.js">
    <link rel="modulepreload" crossorigin href="./assets/pdfjs-DrumwR7S.js">
    <link rel="stylesheet" crossorigin href="./assets/index-wzXBae-a.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

```

### plugins/paperalive/assets/viewer/assets/rolldown-runtime-aKtaBQYM.js

```javascript
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),u=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});export{l as i,s as n,u as r,o as t};
```

### plugins/paperalive/skills/paperalive/scripts/validate_units.py

```python
#!/usr/bin/env python3
"""Validate PAER original/SI conversions and equation dimensions with Pint."""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

from _paperalive import (
    PaperAliveError,
    issue_report,
    load_paer,
    schema_errors,
    scientific_input_hash,
    unit_errors,
    write_json,
)


def validate(path: Path) -> dict[str, object]:
    paer = load_paer(path)
    structural = schema_errors(paer)
    issues = structural if structural else unit_errors(paer)
    report = issue_report(issues)
    report.update(
        {
            "validator": "paperalive-units-dimensions",
            "version": "1.0.0",
            "input": str(path.resolve()),
            "inputHash": scientific_input_hash(paer),
        }
    )
    return report


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(description=__doc__)
    result.add_argument("paer", type=Path, help="Path to experiment.paperalive.json")
    result.add_argument("--report", type=Path, required=True, help="Destination JSON unit report")
    return result


def main(argv: list[str] | None = None) -> int:
    args = parser().parse_args(argv)
    try:
        report = validate(args.paer)
    except PaperAliveError as exc:
        report = {
            "valid": False,
            "validator": "paperalive-units-dimensions",
            "version": "1.0.0",
            "hardFailCount": 1,
            "warningCount": 0,
            "issues": [{"severity": "hard-fail", "code": exc.code, "message": str(exc), "path": "/"}],
        }
    write_json(args.report, report)
    print(
        f"units={'pass' if report['valid'] else 'fail'}: {report['hardFailCount']} hard fail(s); report={args.report.resolve()}"
    )
    return 0 if report["valid"] else 2


if __name__ == "__main__":
    sys.exit(main())

```

### plugins/paperalive/skills/paperalive/scripts/validate_paer.py

```python
#!/usr/bin/env python3
"""Validate canonical PAER structure, references, safety limits, semantics, and provenance."""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

from _paperalive import (
    PaperAliveError,
    issue_report,
    load_paer,
    schema_errors,
    scientific_input_hash,
    semantic_errors,
    write_json,
)


def validate(path: Path) -> dict[str, object]:
    paer = load_paer(path)
    structural = schema_errors(paer)
    issues = structural if structural else semantic_errors(paer)
    report = issue_report(issues)
    report.update(
        {
            "validator": "paperalive-structural-semantic",
            "version": "1.0.0",
            "input": str(path.resolve()),
            "inputHash": scientific_input_hash(paer),
            "declaredCompatibility": paer.get("compatibility", {}).get("status", "UNSUPPORTED"),
            "effectiveCompatibility": "UNSUPPORTED"
            if not report["valid"]
            else paer.get("compatibility", {}).get("status"),
            "effectiveTrustCeiling": "X"
            if not report["valid"]
            else ("D" if paer.get("visualization", {}).get("claim") == "illustrative" else "C"),
            "note": "Serialized validationEvidence and trust are untrusted claims; A/B requires a freshly computed local reproduction result.",
        }
    )
    return report


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(description=__doc__)
    result.add_argument("paer", type=Path, help="Path to experiment.paperalive.json")
    result.add_argument("--report", type=Path, required=True, help="Destination JSON validation report")
    return result


def main(argv: list[str] | None = None) -> int:
    args = parser().parse_args(argv)
    try:
        report = validate(args.paer)
    except PaperAliveError as exc:
        report = {
            "valid": False,
            "validator": "paperalive-structural-semantic",
            "version": "1.0.0",
            "hardFailCount": 1,
            "warningCount": 0,
            "issues": [{"severity": "hard-fail", "code": exc.code, "message": str(exc), "path": "/"}],
            "effectiveCompatibility": "UNSUPPORTED",
            "effectiveTrustCeiling": "X",
        }
    write_json(args.report, report)
    print(f"{report['effectiveCompatibility']}: {report['hardFailCount']} hard fail(s); report={args.report.resolve()}")
    return 0 if report["valid"] else 2


if __name__ == "__main__":
    sys.exit(main())

```

### plugins/paperalive/assets/viewer/assets/SpatialScene-C1Daznu-.js

```javascript
import{i as e}from"./rolldown-runtime-aKtaBQYM.js";import{a as t,c as n,i as r,n as i,o as a,r as o,s,t as c,u as l}from"./spatial-BYYkaVm8.js";import{a as u,i as d,n as f,r as p,t as m}from"./index-Bu16gLoE.js";var h=e(l(),1),g=n(),_=[`#75dbc8`,`#e9c46a`,`#8fb8ff`];function v({index:e}){let t=i(e=>e.invalidate);return(0,h.useEffect)(()=>t(),[e,t]),null}function y({position:e,target:t}){let n=i(e=>e.camera),r=i(e=>e.invalidate);return(0,h.useEffect)(()=>{n.position.set(...e),n.lookAt(new s(...t)),n.updateProjectionMatrix(),r()},[n,r,e,t]),null}function b({points:e,color:n,opacity:i=1}){let o=(0,h.useMemo)(()=>new t(new r().setFromPoints(e.map(e=>new s(...e))),new a({color:n,transparent:i<1,opacity:i})),[n,i,e]);return(0,h.useEffect)(()=>()=>{o.geometry.dispose(),o.material.dispose()},[o]),(0,g.jsx)(`primitive`,{object:o})}function x(){return(0,g.jsxs)(`group`,{children:[(0,g.jsx)(b,{points:[[-2,0,0],[2,0,0]],color:`#445154`,opacity:.8}),(0,g.jsx)(b,{points:[[0,-2,0],[0,2,0]],color:`#445154`,opacity:.8}),(0,g.jsx)(b,{points:[[0,0,-2],[0,0,2]],color:`#445154`,opacity:.8})]})}function S({origin:e,vector:t,color:n}){let r=(0,h.useMemo)(()=>{let r=new s(...t),i=r.length();return i>0&&r.normalize(),new o(r,new s(...e),i>0?.48:0,n,.12,.07)},[n,e,t]);return(0,h.useEffect)(()=>()=>{r.line.geometry.dispose(),r.cone.geometry.dispose(),r.line.material.dispose(),r.cone.material.dispose()},[r]),(0,g.jsx)(`primitive`,{object:r})}function C({paer:e,result:t,baseline:n,compare:r,index:i}){let a=(0,h.useMemo)(()=>e.model.stateQuantityIds.slice(0,3),[e.model.stateQuantityIds]),o=(0,h.useMemo)(()=>f(t,a),[a,t]),s=(0,h.useMemo)(()=>n?f(n,a):[],[n,a]),c=(0,h.useMemo)(()=>p(e,o,s),[s,e,o]),l=(0,h.useMemo)(()=>m(o,c),[c,o]),u=(0,h.useMemo)(()=>s.length?m(s,c):[],[s,c]),d=l[i]??l[0];return(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`ambientLight`,{intensity:.85}),(0,g.jsx)(`pointLight`,{position:[3,4,5],intensity:18,color:`#b7fff1`}),(0,g.jsx)(x,{}),r&&u.length>0&&(0,g.jsx)(b,{points:u,color:`#71807e`,opacity:.45}),(0,g.jsx)(b,{points:l,color:`#75dbc8`,opacity:.92}),(0,g.jsxs)(`mesh`,{position:d,children:[(0,g.jsx)(`sphereGeometry`,{args:[.095,24,24]}),(0,g.jsx)(`meshStandardMaterial`,{color:`#f3d58a`,emissive:`#9b6e20`,emissiveIntensity:1.8})]})]})}function w({paer:e,result:t,baseline:n,compare:r,index:i,selectedId:a,onSelect:o,inspectionScale:s}){let c=e.model.dynamics,l=(0,h.useMemo)(()=>{if(!s)return 1;let e=[...Object.values(t.positions??{}).flat(),...Object.values(n?.positions??{}).flat()];return 1.65/Math.max(1e-9,...e.flatMap(e=>e.map(Math.abs)))},[n?.positions,s,t.positions]);return c.kind!==`particle`||!t.positions?null:(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`ambientLight`,{intensity:.75}),(0,g.jsx)(`pointLight`,{position:[2,3,4],intensity:25,color:`#fff0bd`}),(0,g.jsx)(`gridHelper`,{args:[4,16,`#27383a`,`#172326`],rotation:[Math.PI/2,0,0]}),Array.from({length:c.particleCount},(e,s)=>{let c=`particle_${s+1}`,u=_[s%_.length],d=(t.positions?.[c]??[]).map(e=>e.map(e=>e*l)),f=d[i]??d[0],p=t.velocities?.[c]?.[i]??[0,0,0],m=t.forces?.[c]?.[i]??[0,0,0],h=(n?.positions?.[c]??[]).map(e=>e.map(e=>e*l));return(0,g.jsxs)(`group`,{children:[r&&h.length>0&&(0,g.jsx)(b,{points:h,color:`#71807e`,opacity:.34}),(0,g.jsx)(b,{points:d,color:u,opacity:a&&a!==c?.24:.65}),(0,g.jsxs)(`mesh`,{position:f,onClick:e=>{e.stopPropagation(),o?.(c)},scale:a===c?1.25:1,children:[(0,g.jsx)(`sphereGeometry`,{args:[.085,24,24]}),(0,g.jsx)(`meshStandardMaterial`,{color:u,emissive:u,emissiveIntensity:a===c?1.4:.42})]}),(0,g.jsx)(S,{origin:f,vector:p,color:`#8fb8ff`}),(0,g.jsx)(S,{origin:f,vector:m,color:`#e6c879`})]},c)})]})}function T({paer:e,result:t,baseline:n,compare:r,time:i,selectedId:a,onSelect:o,inspectionScale:s}){let l=u(t.times,i),f=e.visualization.camera,p=(0,h.useMemo)(()=>d(e,t,n),[n,e,t]),m=p.position,_=p.target;return(0,g.jsxs)(c,{frameloop:`demand`,dpr:[1,1.75],camera:{position:m,fov:38,near:.1,far:Math.max(100,f?.maxDistance??0)},gl:{antialias:!0,alpha:!0,powerPreference:`high-performance`},children:[(0,g.jsx)(`color`,{attach:`background`,args:[`#091013`]}),(0,g.jsx)(`fog`,{attach:`fog`,args:[`#091013`,7,13]}),(0,g.jsx)(y,{position:m,target:_}),(0,g.jsx)(v,{index:l}),e.model.adapter===`ode-system-v1`?(0,g.jsx)(C,{paer:e,result:t,baseline:n,compare:r,index:l}):(0,g.jsx)(w,{paer:e,result:t,baseline:n,compare:r,index:l,selectedId:a,onSelect:o,inspectionScale:s})]})}export{T as default};
```

### plugins/paperalive/skills/paperalive/scripts/package_experiment.py

```python
#!/usr/bin/env python3
"""Create a deterministic, manifest-hashed PaperAlive ZIP from a static experiment build."""

from __future__ import annotations

import argparse
import json
import os
import sys
import zipfile
from pathlib import Path

from _paperalive import (
    MAX_ARCHIVE_FILE_BYTES,
    MAX_ARCHIVE_FILES,
    MAX_ARCHIVE_TOTAL_BYTES,
    MAX_ARCHIVE_ZIP_BYTES,
    PaperAliveError,
    regular_files,
    sha256_file,
)

MANIFEST = "paperalive-manifest.json"


def zip_info(path: str) -> zipfile.ZipInfo:
    info = zipfile.ZipInfo(path, date_time=(1980, 1, 1, 0, 0, 0))
    info.compress_type = zipfile.ZIP_DEFLATED
    info.create_system = 3
    info.external_attr = (0o100644 & 0xFFFF) << 16
    return info


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(description=__doc__)
    result.add_argument("build_dir", type=Path)
    result.add_argument("--output", type=Path, required=True)
    return result


def main(argv: list[str] | None = None) -> int:
    args = parser().parse_args(argv)
    try:
        if not args.build_dir.is_dir():
            raise PaperAliveError("Build directory does not exist", "PACKAGE_INPUT")
        if args.output.resolve().is_relative_to(args.build_dir.resolve()):
            raise PaperAliveError("Package output must be outside build_dir", "PACKAGE_OUTPUT")
        files = sorted(
            ((relative, path) for relative, path in regular_files(args.build_dir) if relative != MANIFEST),
            key=lambda item: item[0].encode("utf-16-be"),
        )
        if len(files) > MAX_ARCHIVE_FILES - 1:
            raise PaperAliveError("Package file-count limit exceeded (manifest counts as one entry)", "PACKAGE_COUNT")
        folded_paths = [relative.casefold() for relative, _ in files]
        if len(folded_paths) != len(set(folded_paths)):
            raise PaperAliveError("Case-insensitive duplicate build paths are forbidden", "PACKAGE_DUPLICATE")
        records = []
        total = 0
        for relative, path in files:
            size = path.stat().st_size
            if size > MAX_ARCHIVE_FILE_BYTES:
                raise PaperAliveError(f"Package file too large: {relative}", "PACKAGE_SIZE")
            total += size
            if total > MAX_ARCHIVE_TOTAL_BYTES:
                raise PaperAliveError("Package total size limit exceeded", "PACKAGE_SIZE")
            records.append(
                {"path": relative, "size": size, "sha256": sha256_file(path, max_bytes=MAX_ARCHIVE_FILE_BYTES)}
            )
        paer = "experiment.paperalive.json"
        if paer not in {record["path"] for record in records}:
            raise PaperAliveError(f"Primary PAER {paer} is missing", "PACKAGE_PAER")
        manifest = {
            "format": "paperalive-package",
            "formatVersion": "1.0.0",
            "paer": paer,
            "limits": {
                "maxFiles": MAX_ARCHIVE_FILES,
                "maxFileBytes": MAX_ARCHIVE_FILE_BYTES,
                "maxTotalBytes": MAX_ARCHIVE_TOTAL_BYTES,
            },
            "files": records,
        }
        manifest_bytes = (
            json.dumps(manifest, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
        ).encode()
        if len(manifest_bytes) > 256 * 1024:
            raise PaperAliveError("Package manifest exceeds 256 KiB", "PACKAGE_MANIFEST_SIZE")
        if total + len(manifest_bytes) > MAX_ARCHIVE_TOTAL_BYTES:
            raise PaperAliveError("Package total size including manifest exceeds limit", "PACKAGE_SIZE")
        args.output.parent.mkdir(parents=True, exist_ok=True)
        temporary = args.output.with_name(args.output.name + ".tmp")
        if temporary.exists():
            temporary.unlink()
        with zipfile.ZipFile(
            temporary, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=True
        ) as archive:
            archive.writestr(zip_info(MANIFEST), manifest_bytes, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
            for record, (_, path) in zip(records, files, strict=True):
                archive.writestr(
                    zip_info(record["path"]), path.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9
                )
        if temporary.stat().st_size > MAX_ARCHIVE_ZIP_BYTES:
            temporary.unlink()
            raise PaperAliveError("Compressed ZIP size limit exceeded", "PACKAGE_ZIP_SIZE")
        os.replace(temporary, args.output)
        print(f"package=pass files={len(records)} bytes={total} output={args.output.resolve()}")
        return 0
    except (PaperAliveError, OSError, zipfile.BadZipFile) as exc:
        print(f"package=fail [{getattr(exc, 'code', 'PACKAGE_IO')}] {exc}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    sys.exit(main())

```

### plugins/paperalive/skills/paperalive/scripts/build_experiment.py

```python
#!/usr/bin/env python3
"""Build a validated PAER into the trusted prebuilt offline viewer shell."""

from __future__ import annotations

import argparse
import html
import json
import shutil
import sys
from pathlib import Path

from _paperalive import (
    PLUGIN_ROOT,
    PaperAliveError,
    issue_report,
    load_paer,
    regular_files,
    schema_errors,
    scientific_input_hash,
    semantic_errors,
    unit_errors,
    write_json,
)
from run_reference import dependencies, ode_reference, particle_reference, scalar_reference

RENDERERS = {
    "ode-system-v1": {"plotly-cartesian", "r3f-phase-space"},
    "particle-system-v1": {"r3f-particles", "plotly-cartesian"},
    "scalar-field-v1": {"plotly-cartesian", "plotly-gl3d"},
}
MAX_VIEWER_FILES = 2_000
MAX_VIEWER_BYTES = 256 * 1024 * 1024


def viewer_root(requested: Path | None) -> Path:
    candidate = requested.resolve() if requested else (PLUGIN_ROOT / "assets" / "viewer").resolve()
    if (candidate / "dist" / "index.html").is_file():
        candidate = candidate / "dist"
    if not (candidate / "index.html").is_file():
        raise PaperAliveError(
            f"Prebuilt viewer index.html not found at {candidate}. Build viewer/dist or reinstall bundled plugin assets.",
            "VIEWER_MISSING",
        )
    return candidate


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(description=__doc__)
    result.add_argument("paer", type=Path, help="Validated PAER JSON")
    result.add_argument(
        "--viewer-dir", type=Path, help="Optional prebuilt viewer directory; defaults to plugin assets/viewer"
    )
    result.add_argument("--output", type=Path, required=True, help="Empty static-site destination")
    return result


def main(argv: list[str] | None = None) -> int:
    args = parser().parse_args(argv)
    output_preexisting = args.output.exists()
    output_started = False
    try:
        paer = load_paer(args.paer)
        issues = schema_errors(paer)
        if not issues:
            issues = [*semantic_errors(paer), *unit_errors(paer)]
        if issues:
            raise PaperAliveError(f"Build refused: {len(issues)} scientific hard fail(s)", "BUILD_VALIDATION")
        renderer = paer["visualization"]["rendererAdapter"]
        if renderer not in RENDERERS[paer["model"]["adapter"]]:
            raise PaperAliveError(
                f"Renderer {renderer} is not allowlisted for {paer['model']['adapter']}", "RENDERER_MAPPING"
            )
        dynamics = paer["model"]["dynamics"]
        if renderer == "r3f-phase-space" and (
            dynamics.get("kind") != "ode"
            or len(dynamics.get("derivatives", [])) != 3
            or paer["visualization"]["coordinateSystem"] != "cartesian-phase"
        ):
            raise PaperAliveError(
                "3D phase space requires exactly three ODE states and cartesian-phase coordinates",
                "RENDERER_PHASE_SPACE",
            )
        if renderer == "r3f-particles" and dynamics.get("kind") != "particle":
            raise PaperAliveError("Particle renderer requires particle dynamics", "RENDERER_PARTICLE")
        if renderer == "plotly-gl3d" and (dynamics.get("kind") != "scalar-field" or dynamics.get("dimensions", 0) < 2):
            raise PaperAliveError("GL3D requires a scalar field with at least two dimensions", "RENDERER_GL3D")
        if (
            paer["visualization"]["claim"] == "quantitative"
            and paer["visualization"]["coordinateSystem"] == "illustrative"
        ):
            raise PaperAliveError("Quantitative rendering cannot use illustrative coordinates", "RENDERER_CLAIM")
        source = viewer_root(args.viewer_dir)
        if args.output.exists() and (not args.output.is_dir() or any(args.output.iterdir())):
            raise PaperAliveError("Build output must be a new or empty directory", "BUILD_OUTPUT")
        args.output.mkdir(parents=True, exist_ok=True)
        output_started = True
        reference_result = None
        if paer["visualization"]["claim"] == "quantitative":
            reference_directory = args.output / "reference"
            reference_directory.mkdir(parents=True, exist_ok=True)
            np, solve_ivp = dependencies()
            if paer["model"]["adapter"] == "ode-system-v1":
                reference_result = ode_reference(paer, reference_directory, np, solve_ivp)
            elif paer["model"]["adapter"] == "particle-system-v1":
                reference_result = particle_reference(paer, reference_directory, np)
            else:
                reference_result = scalar_reference(paer, reference_directory, np)
            if not reference_result["passed"]:
                raise PaperAliveError(
                    "Quantitative build refused because local numerical reference failed", "BUILD_REFERENCE"
                )
            write_json(
                reference_directory / "reference-report.json",
                {
                    "validator": "paperalive-python-reference",
                    "version": "1.0.0",
                    "inputHash": scientific_input_hash(paer),
                    **reference_result,
                },
            )
        total = 0
        count = 0
        for relative, path in regular_files(source):
            count += 1
            total += path.stat().st_size
            if count > MAX_VIEWER_FILES or total > MAX_VIEWER_BYTES:
                raise PaperAliveError("Viewer asset resource limit exceeded", "VIEWER_LIMIT")
            target = args.output / relative
            target.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(path, target)
        shutil.copyfile(args.paer, args.output / "experiment.paperalive.json")
        index_path = args.output / "index.html"
        index = index_path.read_text(encoding="utf-8")
        if "</body>" not in index:
            raise PaperAliveError("Viewer index has no closing body element", "VIEWER_INDEX")
        serialized = (
          
[truncated — 2093 more characters]
```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]