# Impakt

**Crash test data analysis, visualization, and reporting.**

Impakt is a modular, scriptable Python toolkit for working with automotive crash test data. It reads ISO 13499 MME data natively, provides non-destructive signal processing, interactive web-based visualization, injury criteria calculation, and protocol-compliant report generation for Euro NCAP, US NCAP, and IIHS.

## Quick Start

```bash
# Install
uv sync --dev

# Run tests (136 tests)
uv run pytest tests/

# View test metadata
uv run impakt info tests/mme_data/3239

# Launch web UI
uv run impakt serve tests/mme_data/3239

# Python scripting
uv run python -c "
from impakt import Session
s = Session.open('tests/mme_data/3239')
print(s)  # Session(3239, 133 channels)
"
```

> See [[docs/STATUS.md]] for detailed project state, known issues, and next steps.

---

## Contents

- [[#Design Principles]]
- [[#Architecture Overview]]
- [[#Module Breakdown]]
  - [[#impakt.io — Data IO]]
  - [[#impakt.channel — Channel Model]]
  - [[#impakt.transform — Signal Processing]]
  - [[#impakt.criteria — Injury Criteria]]
  - [[#impakt.protocol — Rating Protocols]]
  - [[#impakt.plot — Visualization]]
  - [[#impakt.report — PDF and Report Generation]]
  - [[#impakt.template — Templates and Sessions]]
  - [[#impakt.web — Web UI]]
  - [[#impakt.plugin — Plugin System]]
  - [[#impakt.script — Scripting API]]
- [[#Data Flow]]
- [[#Template and Session Lifecycle]]
- [[#Injury Criteria Pipeline]]
- [[#Web UI Architecture]]
- [[#Plugin Architecture]]
- [[#ISO Channel Naming Intelligence]]
- [[#Directory Structure]]
- [[#Scripting Examples]]

---

## Design Principles

1. **Immutable raw data.** Original MME files are never modified. All state — sessions, cached computations, user overrides — lives in a `.impakt/` subfolder alongside the test data.
2. **Non-destructive transforms.** Filtering, alignment, and math operations produce new channel views. The transformation chain is stored and reproducible.
3. **Scriptable first.** Every operation available in the UI is accessible through a Python API. The web UI is a frontend to the same engine.
4. **Template-driven workflow.** Reusable templates define plot layouts, filter chains, channel selections, corridors, and report configurations. Templates live in a global library; sessions bind templates to specific test data.
5. **Protocol-aware.** Built-in knowledge of Euro NCAP, US NCAP, and IIHS injury criteria, scoring thresholds, and report formats.
6. **Plugin-extensible.** Custom readers, transforms, injury criteria, report templates, and UI components can be registered through a plugin API.

---

## Architecture Overview

```mermaid
graph TB
    subgraph "Data Layer"
        IO[impakt.io]
        CH[impakt.channel]
    end

    subgraph "Processing Layer"
        TR[impakt.transform]
        CR[impakt.criteria]
        PR[impakt.protocol]
    end

    subgraph "Presentation Layer"
        PL[impakt.plot]
        RP[impakt.report]
        WB[impakt.web]
    end

    subgraph "Orchestration Layer"
        TM[impakt.template]
        SC[impakt.script]
        PG[impakt.plugin]
    end

    IO -->|"parse MME/other"| CH
    CH -->|"channel data"| TR
    TR -->|"filtered/aligned"| CR
    CR -->|"injury values"| PR
    PR -->|"scores & ratings"| RP

    CH -->|"raw & derived"| PL
    TR -->|"processed"| PL
    CR -->|"criteria results"| PL

    PL -->|"figures"| RP
    PL -->|"interactive plots"| WB

    TM -->|"orchestrates"| IO
    TM -->|"orchestrates"| TR
    TM -->|"orchestrates"| PL
    TM -->|"orchestrates"| CR

    SC -->|"drives"| TM
    SC -->|"drives"| IO
    SC -->|"drives"| TR
    SC -->|"drives"| PL

    PG -.->|"extends"| IO
    PG -.->|"extends"| TR
    PG -.->|"extends"| CR
    PG -.->|"extends"| PR
    PG -.->|"extends"| RP

    WB -->|"calls"| SC
```

---

## Module Breakdown

### impakt.io — Data IO

Responsible for reading crash test data from disk into the internal channel model.

**Primary format:** ISO 13499 MME (directory-based: `MME.ini` + `.chn`/`.dat` file pairs).

**Architecture:** Reader classes implement a common `Reader` protocol, enabling future format support (TDMS, CSV, UDB) without changing downstream code.

```mermaid
classDiagram
    class ReaderProtocol {
        <<protocol>>
        +read(path: Path) TestData
        +supports(path: Path) bool
        +metadata(path: Path) TestMetadata
    }

    class MMEReader {
        +read(path: Path) TestData
        +supports(path: Path) bool
        +metadata(path: Path) TestMetadata
        -_parse_ini(path: Path) dict
        -_parse_chn(path: Path) ChannelHeader
        -_read_dat(path: Path, header: ChannelHeader) ndarray
    }

    class TDMSReader {
        +read(path: Path) TestData
        +supports(path: Path) bool
        +metadata(path: Path) TestMetadata
    }

    class CSVReader {
        +read(path: Path) TestData
        +supports(path: Path) bool
        +metadata(path: Path) TestMetadata
    }

    class ReaderRegistry {
        +register(reader: ReaderProtocol)
        +detect(path: Path) ReaderProtocol
        +read(path: Path) TestData
    }

    ReaderProtocol <|.. MMEReader
    ReaderProtocol <|.. TDMSReader
    ReaderProtocol <|.. CSVReader
    ReaderRegistry o-- ReaderProtocol
```

**Key responsibilities:**
- Parse `MME.ini` for test-level metadata (vehicle, dummy, impact config)
- Parse `.chn` headers for per-channel metadata (units, sample rate, CFC class, pre-trigger)
- Read `.dat` files (ASCII or binary IEEE float) into NumPy arrays
- Reconstruct time vectors from `dt` and pre-trigger sample count
- Populate `TestData` and `Channel` objects

---

### impakt.channel — Channel Model

The core data model. Channels are immutable value objects wrapping time-series data with rich metadata.

```mermaid
classDiagram
    class TestData {
        +test_id: str
        +metadata: TestMetadata
        +channels: dict~str, Channel~
        +path: Path
        +get(name: str) Channel
        +find(pattern: str) list~Channel~
        +groups() dict~str, ChannelGroup~
    }

    class TestMetadata {
        +test_number: str
        +test_date: date
        +test_type: str
        +vehicle: VehicleInfo
        +dummy: DummyInfo
        +impact: ImpactConfig
    }

    class Channel {
        +name: str
        +code: ChannelCode
        +data: ndarray
        +time: ndarray
        +unit: str
        +sample_rate: float
        +cfc_class: int | None
        +metadata: dict
    }

    class ChannelCode {
        +raw: str
        +test_object: str
        +main_location: str
        +fine_location: str
        +measurement: str
        +direction: str
        +sense: str
        +filter_class: str
        +group_key() str
        +is_component() bool
        +axis() str
    }

    class ChannelGroup {
        +key: str
        +x: Channel | None
        +y: Channel | None
        +z: Channel | None
        +resultant() Channel
        +components() list~Channel~
    }

    TestData *-- TestMetadata
    TestData *-- Channel
    Channel *-- ChannelCode
    ChannelGroup o-- Channel
```

**ISO channel naming intelligence** is embedded in `ChannelCode`. Given a raw 16-character name like `11HEAD0000H3ACXA`, the parser extracts:

| Field | Positions | Example | Meaning |
|---|---|---|---|
| Test Object | 1-2 | `11` | Driver, Hybrid III |
| Main Location | 3-6 | `HEAD` | Head |
| Fine Location | 7-10 | `0000` | Center of gravity |
| Measurement | 11-12 | `AC` | Acceleration |
| Direction | 13 | `X` | Longitudinal axis |
| Sense | 14 | `A` | SAE sign convention A |

**Auto-grouping:** Channels sharing positions 1-12 + 14-16 (differing only at position 13: X/Y/Z) are automatically grouped into `ChannelGroup` objects for one-call resultant computation.

---

### impakt.transform — Signal Processing

Non-destructive signal processing pipeline. Each transform takes a `Channel` and returns a new `Channel` — the original is never mutated.

```mermaid
graph LR
    subgraph "Transform Pipeline"
        RAW[Raw Channel] --> F1[CFC Filter]
        F1 --> F2[X-Axis Align]
        F2 --> F3[Y-Axis Zero]
        F3 --> F4[Math Expression]
        F4 --> OUT[Derived Channel]
    end

    style RAW fill:#e1f5fe
    style OUT fill:#c8e6c9
```

**Available transforms:**

| Transform | Description | Parameters |
|---|---|---|
| `CFCFilter` | SAE J211 CFC filtering (4th-order Butterworth, zero-phase) | `cfc_class`: 60, 180, 600, 1000 |
| `XAlign` | Time-zero shifting — moves t=0 to a user-specified event | `method`: manual, threshold, trigger |
| `YAlign` | Zero-offset correction using average-over-time window | `window`: (t_start, t_end) for baseline |
| `Resultant` | Compute vector magnitude from X/Y/Z components | `group`: ChannelGroup |
| `MathExpr` | Free-form math on channels (e.g., `ch_a + ch_b * 0.5`) | `expression`: str, `channels`: dict |
| `Trim` | Extract time range | `t_start`, `t_end` |
| `Resample` | Change sample rate | `target_rate`: float |

**Transform chain model:**

```mermaid
classDiagram
    class Transform {
        <<protocol>>
        +apply(channel: Channel) Channel
        +name: str
        +params: dict
    }

    class TransformChain {
        +steps: list~Transform~
        +apply(channel: Channel) Channel
        +append(transform: Transform) TransformChain
        +serialize() dict
        +deserialize(data: dict) TransformChain
    }

    class CFCFilter {
        +cfc_class: int
        +apply(channel: Channel) Channel
    }

    class XAlign {
        +method: str
        +reference_time: float
        +apply(channel: Channel) Channel
    }

    class YAlign {
        +window: tuple~float, float~
        +apply(channel: Channel) Channel
    }

    class Resultant {
        +apply(group: ChannelGroup) Channel
    }

    class MathExpr {
        +expression: str
        +apply(channels: dict) Channel
    }

    Transform <|.. CFCFilter
    Transform <|.. XAlign
    Transform <|.. YAlign
    Transform <|.. Resultant
    Transform <|.. MathExpr
    TransformChain o-- Transform
```

**CFC filter implementation** (SAE J211 compliant):
- 4th-order Butterworth low-pass, applied forward-reverse via `scipy.signal.filtfilt`
- Cutoff frequency: `f_c = CFC_class * (5/3)` Hz
- CFC 60 = 100 Hz, CFC 180 = 300 Hz, CFC 600 = 1000 Hz, CFC 1000 = 1650 Hz

---

### impakt.criteria — Injury Criteria

Calculation engine for standard injury criteria. Each criterion is a self-contained function operating on filtered channel data.

```mermaid
classDiagram
    class InjuryCriterion {
        <<protocol>>
        +name: str
        +required_channels: list~str~
        +compute(channels: dict~str, Channel~, dummy: DummyInfo) CriterionResult
    }

    class CriterionResult {
        +criterion: str
        +value: float
        +unit: str
        +time_of_peak: float | None
        +window: tuple~float, float~ | None
        +details: dict
    }

    class HIC {
        +window_ms: int
        +compute(...) CriterionResult
    }

    class Clip3ms {
        +compute(...) CriterionResult
    }

    class Nij {
        +intercepts: NijIntercepts
        +compute(...) CriterionResult
    }

    class ChestDeflection {
        +compute(...) CriterionResult
    }

    class FemurLoad {
        +compute(...) CriterionResult
    }

    class TibiaIndex {
        +compute(...) CriterionResult
    }

    class ViscousCriterion {
        +compute(...) CriterionResult
    }

    InjuryCriterion <|.. HIC
    InjuryCriterion <|.. Clip3ms
    InjuryCriterion <|.. Nij
    InjuryCriterion <|.. ChestDeflection
    InjuryCriterion <|.. FemurLoad
    InjuryCriterion <|.. TibiaIndex
    InjuryCriterion <|.. ViscousCriterion
    InjuryCriterion --> CriterionResult
```

**Implemented criteria:**

| Criterion | Formula / Method | Key Inputs | Reference |
|---|---|---|---|
| **HIC15 / HIC36** | `(t2-t1) * [avg(a)]^2.5`, maximized over window | Head resultant accel (g) | FMVSS 208 |
| **3ms Clip** | Max accel sustained for cumulative 3 ms | Chest resultant accel (g) | SAE J211 |
| **Nij** | `Fz/Fzc + My/Myc`, max of 4 modes | Upper neck Fz, My | FMVSS 208 |
| **Chest Deflection** | Peak sternal displacement | Chest deflection (mm) | FMVSS 208 |
| **Femur Load** | Peak compressive axial force | Femur Fz left/right (kN) | FMVSS 208 |
| **Tibia Index** | `\|M\|/Mc + \|F\|/Fc` | Tibia Mx, My, Fz | Euro NCAP |
| **Viscous Criterion** | `V(t) * C(t)` (velocity x compression) | Chest deflection time-history | Euro NCAP |

**Nij critical intercepts by dummy type:**

| Dummy | Fzc Tension (N) | Fzc Compression (N) | Myc Flexion (Nm) | Myc Extension (Nm) |
|---|---|---|---|---|
| Hybrid III 50th M | 6806 | 6160 | 310 | 135 |
| Hybrid III 5th F | 4287 | 3880 | 155 | 67 |
| Hybrid III 6YO | 2800 | 2800 | 93 | 37 |
| Hybrid III 3YO | 2120 | 2120 | 68 | 27 |

---

### impakt.protocol — Rating Protocols

Maps computed injury criteria to protocol-specific scores and ratings.

```mermaid
graph TB
    subgraph "Criteria Results"
        HIC[HIC15: 423]
        CHEST[Chest Defl: 34mm]
        FEMUR[Femur: 4.2kN]
        NIJ[Nij: 0.61]
        TI[Tibia Index: 0.8]
    end

    subgraph "Protocol Engines"
        ENCAP[Euro NCAP Scorer]
        USNCAP[US NCAP Scorer]
        IIHS_E[IIHS Evaluator]
    end

    subgraph "Outputs"
        ENCAP_R["Euro NCAP\n4 stars\nAdult: 82%"]
        USNCAP_R["US NCAP\n5 stars\nP(injury): 8%"]
        IIHS_R["IIHS\nGood\nAll sub-ratings: G"]
    end

    HIC --> ENCAP & USNCAP & IIHS_E
    CHEST --> ENCAP & USNCAP & IIHS_E
    FEMUR --> ENCAP & USNCAP & IIHS_E
    NIJ --> ENCAP & USNCAP & IIHS_E
    TI --> ENCAP

    ENCAP --> ENCAP_R
    USNCAP --> USNCAP_R
    IIHS_E --> IIHS_R
```

**Protocol scoring:**

| Protocol | Methodology | Output |
|---|---|---|
| **Euro NCAP** | Sliding-scale performance limits per body region, mapped to color codes (Green / Yellow / Orange / Brown / Red) and points. Area percentages determine star rating. | Stars (0-5), body region colors, area scores |
| **US NCAP** | Injury risk functions convert criteria to probability of AIS 3+ injury. Combined probability maps to stars. | Stars (1-5), injury probability |
| **IIHS** | Threshold-based per criterion: Good / Acceptable / Marginal / Poor. Overall = worst sub-rating with adjustment. | G/A/M/P per region, overall rating |

Protocol thresholds are versioned and configurable — scoring rules change every few years and Impakt stores these as versioned protocol definition files.

---

### impakt.plot — Visualization

Plotting engine built on Plotly for interactive web-based visualization.

**Capabilities:**
- Single channel time-history plots
- Multi-channel overlay (same test, different channels)
- Multi-test overlay (same channel, different tests)
- Dual X-axis cursors with value readout for all plotted channels
- Tolerance corridors (user-defined or from templates)
- Zoom, pan, box select
- Resultant plots auto-computed from grouped components

```mermaid
classDiagram
    class PlotSpec {
        +channels: list~ChannelRef~
        +corridors: list~Corridor~
        +x_cursors: tuple~float, float~ | None
        +x_range: tuple~float, float~ | None
        +y_range: tuple~float, float~ | None
        +title: str
        +x_label: str
        +y_label: str
    }

    class ChannelRef {
        +test_id: str
        +channel_name: str
        +transform_chain: TransformChain | None
        +style: PlotStyle
    }

    class Corridor {
        +name: str
        +upper: ndarray
        +lower: ndarray
        +time: ndarray
        +style: CorridorStyle
    }

    class PlotStyle {
        +color: str
        +line_width: float
        +line_dash: str
        +label: str
    }

    class PlotEngine {
        +render(spec: PlotSpec) PlotlyFigure
        +to_image(spec: PlotSpec, format: str) bytes
        +to_html(spec: PlotSpec) str
    }

    PlotSpec *-- ChannelRef
    PlotSpec *-- Corridor
    ChannelRef *-- PlotStyle
    PlotEngine --> PlotSpec
```

**Dual X-axis cursor** — the user selects two time points (click or explicit entry). A table displays the interpolated value of every plotted channel at both cursor positions, plus the delta.

---

### impakt.report — PDF and Report Generation

Generates single-page-per-plot PDFs and multi-section protocol reports.

```mermaid
graph LR
    PLOTS[Plot Specs] --> RE[Report Engine]
    CRITERIA[Criteria Results] --> RE
    PROTOCOL[Protocol Scores] --> RE
    META[Test Metadata] --> RE
    TMPL[Report Template] --> RE
    RE --> PDF[PDF Output]
    RE --> HTML[HTML Output]
```

**Report types:**
- **Plot sheets** — one plot per page, with metadata header (test ID, channel info, filter state)
- **Injury summary** — tabular criteria results with color-coded pass/fail per protocol
- **Full protocol report** — Euro NCAP / US NCAP / IIHS formatted report with all required sections, body region diagrams, and scoring breakdowns

---

### impakt.template — Templates and Sessions

The template/session system enables reusable analysis workflows and per-test state persistence.

| Concept | **Template** | **Session** |
|---|---|---|
| Lives in | Global library (`~/.impakt/templates/`) | Alongside test data (`.impakt/` subfolder) |
| Contains | Plot layouts, filter chains, channel selections, corridors, report configs | Template reference + test-specific overrides + cached results |
| Purpose | Reusable recipe ("show me frontal NCAP analysis") | Specific instance ("test_001 viewed with frontal NCAP, but I moved the cursor") |
| Mutability | Edited in library, versioned | Auto-saved per test, can "promote" to template |

```mermaid
graph TB
    subgraph "Global Library ~/.impakt/templates/"
        T1[frontal_ncap.yaml]
        T2[side_iihs.yaml]
        T3[custom_analysis.yaml]
    end

    subgraph "Test Data Directory"
        MME[test_001/MME.ini]
        subgraph ".impakt/"
            S1[session.yaml]
            CACHE[cache/]
        end
    end

    T1 -->|"user opens test\nwith template"| S1
    S1 -->|"references"| T1
    S1 -->|"stores overrides"| S1
    S1 -->|"promote changes"| T1
```

**Template spec** (YAML):

```yaml
name: "Frontal NCAP Analysis"
version: 2
plots:
  - title: "Head Acceleration"
    channels:
      - pattern: "11HEAD0000AC{X,Y,Z}A"
        transform:
          - type: cfc_filter
            cfc_class: 1000
      - pattern: "11HEAD0000ACRA"
        transform:
          - type: resultant
    corridors:
      - name: "HIC 700 Reference"
        file: "corridors/hic_700.csv"
  - title: "Chest Deflection"
    channels:
      - pattern: "11CHST****DC*A"
    x_cursors: [0.0, 0.080]
filters:
  default_cfc: 180
criteria:
  - hic15
  - clip_3ms
  - nij
  - chest_deflection
  - femur_load
protocol: euro_ncap_2024
report:
  format: pdf
  template: euro_ncap_full
```

**Session spec** (stored in `test_data/.impakt/session.yaml`):

```yaml
template: "frontal_ncap"
template_version: 2
test_path: "/data/tests/test_001"
overrides:
  plots:
    0:
      x_cursors: [0.012, 0.065]
  filters:
    "11HEAD0000ACXA":
      - type: cfc_filter
        cfc_class: 600
cached_results:
  hic15: 423.7
  last_computed: "2026-04-10T14:30:00"
```

**Lifecycle:**

```mermaid
sequenceDiagram
    participant User
    participant Template Library
    participant Session
    participant Engine

    User->>Template Library: Select template
    Template Library->>Session: Instantiate session
    User->>Engine: Open test data
    Engine->>Session: Bind to test data
    Engine->>Engine: Apply template (load channels, filters, plots)
    User->>Session: Modify (move cursors, change filter)
    Session->>Session: Store override
    User->>Template Library: (optional) Promote override to template
    User->>Session: Close
    Session->>Session: Auto-save to .impakt/
```

---

### impakt.web — Web UI

Interactive web application built with Dash (Plotly).

```mermaid
graph TB
    subgraph "Dash Layout"
        direction TB
        HEADER[Header Bar: Test Info + Template Selector]

        subgraph "Main Area"
            direction LR
            subgraph "Left Panel"
                TREE[Channel Tree\nISO-aware hierarchy]
                FILTER_PANEL[Transform Controls\nCFC / Align / Math]
            end

            subgraph "Center"
                PLOT_AREA[Plot Area\nMultiple plot panes]
                CURSOR_TABLE[Cursor Values Table\nx1 | x2 | delta]
            end

            subgraph "Right Panel"
                CORRIDOR_PANEL[Corridor Manager]
                CRITERIA_PANEL[Criteria Results]
            end
        end

        FOOTER[Report Generation + Export Controls]
    end

    TREE -->|"channel selection\ncallback"| PLOT_AREA
    FILTER_PANEL -->|"transform\ncallback"| PLOT_AREA
    PLOT_AREA -->|"cursor position\ncallback"| CURSOR_TABLE
    CORRIDOR_PANEL -->|"corridor data\ncallback"| PLOT_AREA
    CRITERIA_PANEL -->|"annotation\ncallback"| PLOT_AREA
    FOOTER -->|"generate\ncallback"| PLOT_AREA
```

**Key UI components:**
- **Channel tree** — hierarchical browser organized by test object > body region > measurement type, leveraging ISO naming intelligence
- **Plot pane** — Plotly figures with zoom / pan / box-select, corridor overlays
- **Cursor table** — interpolated values at two user-selected X-axis times for all visible channels, plus delta
- **Transform sidebar** — apply / remove filters, alignment, math expressions
- **Template panel** — load, save, promote templates
- **Report panel** — select protocol, generate PDF

---

### impakt.plugin — Plugin System

Formal plugin architecture for extending every layer.

```mermaid
classDiagram
    class PluginRegistry {
        +register(plugin: ImpaktPlugin)
        +discover(path: Path)
        +get_readers() list~ReaderProtocol~
        +get_transforms() list~Transform~
        +get_criteria() list~InjuryCriterion~
        +get_protocols() list~Protocol~
        +get_report_templates() list~ReportTemplate~
    }

    class ImpaktPlugin {
        <<protocol>>
        +name: str
        +version: str
        +register(registry: PluginRegistry)
    }

    class ReaderPlugin {
        +reader: ReaderProtocol
    }

    class TransformPlugin {
        +transform: Transform
    }

    class CriterionPlugin {
        +criterion: InjuryCriterion
    }

    class ProtocolPlugin {
        +protocol: Protocol
    }

    ImpaktPlugin <|.. ReaderPlugin
    ImpaktPlugin <|.. TransformPlugin
    ImpaktPlugin <|.. CriterionPlugin
    ImpaktPlugin <|.. ProtocolPlugin
    PluginRegistry o-- ImpaktPlugin
```

**Discovery mechanisms:**
- Entry points (`pyproject.toml` / `setup.cfg` entry point groups)
- Directory scanning (`~/.impakt/plugins/`)
- Explicit registration via API

**Example plugin integration:**

```mermaid
graph TB
    subgraph "Core Impakt"
        REG[Plugin Registry]
        IO_CORE[io: MMEReader]
        TR_CORE[transform: CFC, Align, ...]
        CR_CORE[criteria: HIC, Nij, ...]
        PR_CORE[protocol: ENCAP, USNCAP, IIHS]
    end

    subgraph "Plugin: impakt-tdms"
        TDMS_P[TDMSReader]
    end

    subgraph "Plugin: impakt-jncap"
        JNCAP_P[JNCAP Scorer]
        JNCAP_R[JNCAP Report Template]
    end

    subgraph "Plugin: impakt-custom-filter"
        CUSTOM_F[WaveletDenoise Transform]
    end

    subgraph "Discovery"
        EP[Entry Points\npyproject.toml]
        DIR[~/.impakt/plugins/]
        API_R[Explicit API\nregistry.register]
    end

    EP --> REG
    DIR --> REG
    API_R --> REG

    REG -->|"extends"| IO_CORE
    REG -->|"extends"| TR_CORE
    REG -->|"extends"| CR_CORE
    REG -->|"extends"| PR_CORE

    TDMS_P --> REG
    JNCAP_P --> REG
    JNCAP_R --> REG
    CUSTOM_F --> REG
```

---

### impakt.script — Scripting API

The top-level API that scripts and the web UI both call.

```python
from impakt import Session, Template

# Load test data
test = Session.open("/data/tests/test_001")

# Browse channels using ISO naming intelligence
head_channels = test.find("11HEAD0000AC*")
# [Channel(11HEAD0000ACXA), Channel(11HEAD0000ACYA), Channel(11HEAD0000ACZA)]

# Auto-detect X/Y/Z group, compute resultant
head_group = test.group("11HEAD0000AC")
head_resultant = head_group.resultant()

# Apply CFC filter (non-destructive)
filtered = head_resultant.transform.cfc(1000)

# Compute HIC15
from impakt.criteria import hic
result = hic(filtered, window_ms=15)
print(f"HIC15 = {result.value:.1f} at t = {result.time_of_peak:.4f}s")

# Apply a template
tmpl = Template.load("frontal_ncap")
session = tmpl.apply(test)

# Generate protocol report
from impakt.protocol import euro_ncap
report = euro_ncap.evaluate(session, version="2024")
report.to_pdf("test_001_encap.pdf")

# Launch web UI
from impakt.web import serve
serve(test, template="frontal_ncap", port=8050)
```

---

## Data Flow

End-to-end data flow from MME files to PDF reports:

```mermaid
graph TB
    MME["MME Directory\n(immutable)"] -->|"impakt.io"| TD[TestData Object]
    TD -->|"channel access"| CH[Channel Objects]
    CH -->|"impakt.transform"| TCH[Transformed Channels]
    TCH -->|"impakt.criteria"| CR[Criterion Results]
    CR -->|"impakt.protocol"| SC[Protocol Scores]

    CH -->|"impakt.plot"| FIG1[Raw Plots]
    TCH -->|"impakt.plot"| FIG2[Filtered Plots]
    CR -->|"impakt.plot"| FIG3[Criteria Annotations]

    FIG2 --> RP[impakt.report]
    SC --> RP
    RP --> PDF[PDF Report]

    TD -.->|"state saved"| IMPAKT_DIR[".impakt/ subfolder"]
    IMPAKT_DIR -.->|"state loaded"| TD

    style MME fill:#e1f5fe
    style IMPAKT_DIR fill:#fff9c4
    style PDF fill:#c8e6c9
```

---

## Template and Session Lifecycle

```mermaid
stateDiagram-v2
    [*] --> TemplateLibrary: User browses templates

    TemplateLibrary --> SessionCreated: Select template + open test data
    SessionCreated --> Active: Engine applies template

    Active --> Active: User modifies\n(cursors, filters, channels)
    Active --> Saved: Auto-save to .impakt/

    Saved --> Active: Reopen test data
    Saved --> TemplateLibrary: Promote overrides\nback to template

    Active --> ReportGenerated: User generates report
    ReportGenerated --> Active: Continue analysis

    Active --> [*]: Close session
```

---

## Injury Criteria Pipeline

How raw channel data flows through to a protocol rating:

```mermaid
graph LR
    subgraph "1. Channel Selection"
        A1["HEAD AC X/Y/Z"]
        A2["NECK FO X/Z"]
        A3["NECK MO Y"]
        A4["CHEST DC"]
        A5["FEMUR FO Z L/R"]
        A6["TIBIA FO + MO"]
    end

    subgraph "2. Transform"
        B1[CFC 1000\n+ Resultant]
        B2[CFC 600]
        B3[CFC 600]
        B4[CFC 180]
        B5[CFC 600]
        B6[CFC 600]
    end

    subgraph "3. Criteria"
        C1[HIC15 / HIC36]
        C2[Nij]
        C3[Nij]
        C4["Chest Defl\n3ms Clip\nViscous"]
        C5[Femur Load]
        C6[Tibia Index]
    end

    subgraph "4. Protocol"
        D1["Euro NCAP\nColor + Points"]
        D2["US NCAP\nP(injury) + Stars"]
        D3["IIHS\nG/A/M/P"]
    end

    A1 --> B1 --> C1
    A2 --> B2 --> C2
    A3 --> B3 --> C3
    A4 --> B4 --> C4
    A5 --> B5 --> C5
    A6 --> B6 --> C6

    C1 & C2 & C3 & C4 & C5 & C6 --> D1
    C1 & C2 & C3 & C4 & C5 --> D2
    C1 & C2 & C3 & C4 & C5 & C6 --> D3
```

---

## Web UI Architecture

```mermaid
graph TB
    subgraph "Browser"
        UI[Dash Frontend]
        PLOTS_V[Interactive Plots]
        CURSOR[Dual X-Cursor + Values Table]
        TREE[Channel Tree Browser]
        TMPL_UI[Template Selector]
        REPORT_UI[Report Generator Panel]
    end

    subgraph "Server"
        DASH[Dash Server]
        API[Impakt Script API]
        CACHE_S[Computation Cache]
    end

    UI --> DASH
    DASH --> API
    API --> CACHE_S

    TREE -->|"select channels"| PLOTS_V
    TMPL_UI -->|"apply template"| API
    CURSOR -->|"x1, x2 positions"| PLOTS_V
    REPORT_UI -->|"generate"| API
```

---

## Plugin Architecture

```mermaid
graph LR
    subgraph "Extension Points"
        R[Readers]
        T[Transforms]
        C[Criteria]
        P[Protocols]
        RT[Report Templates]
    end

    REG[Plugin Registry] --> R & T & C & P & RT

    subgraph "Discovery"
        EP[pyproject.toml\nentry_points]
        DP[~/.impakt/plugins/]
        EX[registry.register]
    end

    EP & DP & EX --> REG
```

---

## ISO Channel Naming Intelligence

The `ChannelCode` parser powers auto-discovery, grouping, and human-readable descriptions throughout the tool.

```mermaid
graph LR
    RAW["11HEAD0000H3ACXA"] --> PARSER[ChannelCode Parser]

    PARSER --> OBJ["Test Object: 11\nDriver, Hybrid III"]
    PARSER --> LOC["Location: HEAD\nHead"]
    PARSER --> FINE["Fine: 0000\nCenter of Gravity"]
    PARSER --> MEAS["Measurement: AC\nAcceleration"]
    PARSER --> DIR["Direction: X\nLongitudinal"]
    PARSER --> SENSE["Sense: A\nSAE Convention"]

    OBJ & LOC & FINE & MEAS & DIR --> GROUP["Group Key:\n11HEAD0000AC_A\n(X/Y/Z family)"]

    GROUP --> AUTO["Auto-features:\n- Resultant computation\n- Channel tree placement\n- Criteria channel matching\n- Human-readable label"]
```

**Lookup tables** for all code segments are bundled with the package and extensible via plugins:

| Segment | Examples |
|---|---|
| Test objects | `11` = Driver H3 50th, `12` = Passenger, `20` = Barrier, `30` = Pedestrian |
| Main locations | `HEAD`, `NECK`, `CHST`, `PELV`, `FEMR`, `TIBI`, `STCL`, `BPIL` |
| Fine locations | `0000` = CG, `UP00` = Upper, `LO00` = Lower, `LE00` = Left, `RI00` = Right |
| Measurements | `AC` = Acceleration, `FO` = Force, `MO` = Moment, `DS` = Displacement, `DC` = Deflection |
| Directions | `X` = Longitudinal, `Y` = Lateral, `Z` = Vertical, `R` = Resultant |

---

## Directory Structure

```
impakt/
├── pyproject.toml
├── README.md
├── src/
│   └── impakt/
│       ├── __init__.py
│       ├── io/
│       │   ├── __init__.py
│       │   ├── reader.py          # ReaderProtocol, ReaderRegistry
│       │   ├── mme.py             # MMEReader
│       │   ├── tdms.py            # TDMSReader (stub)
│       │   └── csv.py             # CSVReader (stub)
│       ├── channel/
│       │   ├── __init__.py
│       │   ├── model.py           # Channel, TestData, TestMetadata
│       │   ├── code.py            # ChannelCode parser
│       │   ├── group.py           # ChannelGroup, auto-grouping
│       │   └── lookup.py          # ISO code lookup tables
│       ├── transform/
│       │   ├── __init__.py
│       │   ├── base.py            # Transform protocol, TransformChain
│       │   ├── cfc.py             # CFCFilter
│       │   ├── align.py           # XAlign, YAlign
│       │   ├── resultant.py       # Resultant
│       │   ├── math_expr.py       # MathExpr
│       │   └── resample.py        # Resample, Trim
│       ├── criteria/
│       │   ├── __init__.py
│       │   ├── base.py            # InjuryCriterion protocol
│       │   ├── hic.py             # HIC15, HIC36
│       │   ├── clip3ms.py         # 3ms chest clip
│       │   ├── nij.py             # Neck injury criterion
│       │   ├── chest.py           # Chest deflection, Viscous criterion
│       │   ├── femur.py           # Femur load
│       │   └── tibia.py           # Tibia index
│       ├── protocol/
│       │   ├── __init__.py
│       │   ├── base.py            # Protocol protocol, version management
│       │   ├── euro_ncap.py       # Euro NCAP scorer
│       │   ├── us_ncap.py         # US NCAP scorer
│       │   ├── iihs.py            # IIHS evaluator
│       │   └── thresholds/        # Versioned threshold YAML files
│       │       ├── euro_ncap_2024.yaml
│       │       ├── us_ncap_2023.yaml
│       │       └── iihs_2024.yaml
│       ├── plot/
│       │   ├── __init__.py
│       │   ├── engine.py          # PlotEngine (Plotly)
│       │   ├── spec.py            # PlotSpec, ChannelRef, Corridor
│       │   ├── cursor.py          # Dual X-cursor logic
│       │   └── export.py          # PNG/SVG/PDF single-plot export
│       ├── report/
│       │   ├── __init__.py
│       │   ├── engine.py          # ReportEngine
│       │   ├── pdf.py             # PDF generation (WeasyPrint)
│       │   └── templates/         # Jinja2 HTML report templates
│       │       ├── plot_sheet.html
│       │       ├── injury_summary.html
│       │       └── protocol_report.html
│       ├── template/
│       │   ├── __init__.py
│       │   ├── model.py           # Template, Session models
│       │   ├── library.py         # Template library manager
│       │   └── session.py         # Session persistence
│       ├── web/
│       │   ├── __init__.py
│       │   ├── app.py             # Dash application
│       │   ├── layout.py          # UI layout components
│       │   ├── callbacks.py       # Dash callbacks
│       │   └── assets/            # CSS, static files
│       ├── plugin/
│       │   ├── __init__.py
│       │   ├── registry.py        # PluginRegistry
│       │   └── discovery.py       # Entry point + directory scanning
│       └── script/
│           ├── __init__.py
│           └── api.py             # Top-level scripting API
├── tests/
│   ├── test_io/
│   ├── test_channel/
│   ├── test_transform/
│   ├── test_criteria/
│   ├── test_protocol/
│   ├── test_plot/
│   └── fixtures/                  # Sample MME data for tests
└── docs/
```

---

## Scripting Examples

### Quick injury summary

```python
from impakt import Session
from impakt.criteria import hic, clip_3ms, nij, chest_deflection, femur_load
from impakt.protocol import euro_ncap

test = Session.open("./test_001")

results = {
    "HIC15": hic(test, window_ms=15),
    "3ms Clip": clip_3ms(test),
    "Nij": nij(test),
    "Chest Deflection": chest_deflection(test),
    "Femur Left": femur_load(test, side="left"),
    "Femur Right": femur_load(test, side="right"),
}

for name, r in results.items():
    print(f"{name}: {r.value:.1f} {r.unit}")

rating = euro_ncap.evaluate(results, version="2024")
print(f"\nEuro NCAP: {rating.stars} stars ({rating.adult_pct:.0f}% adult)")
```

### Batch comparison across tests

```python
from impakt import Session
from impakt.plot import overlay
from pathlib import Path

tests = [Session.open(p) for p in Path("./tests").iterdir() if p.is_dir()]

overlay(
    [t.channel("11HEAD0000ACXA").transform.cfc(1000) for t in tests],
    labels=[t.metadata.test_number for t in tests],
    title="Head X Acceleration Comparison",
    corridors=["corridors/head_ax_corridor.csv"],
).to_pdf("head_comparison.pdf")
```

### Custom math channel

```python
from impakt import Session
from impakt.transform import math_expr

test = Session.open("./test_001")

custom = math_expr(
    expression="0.6 * chest_x + 0.4 * chest_z",
    channels={
        "chest_x": test.channel("11CHST0000ACXA").transform.cfc(180),
        "chest_z": test.channel("11CHST0000ACZA").transform.cfc(180),
    },
    name="Weighted Chest Metric",
    unit="g",
)

custom.plot(title="Custom Weighted Chest Acceleration")
```

### Multi-test overlay with cursors

```python
from impakt import Session
from impakt.plot import overlay, cursor_values

test1 = Session.open("./test_001")
test2 = Session.open("./test_002")

fig = overlay([
    test1.channel("11HEAD0000ACXA").transform.cfc(1000),
    test2.channel("11HEAD0000ACXA").transform.cfc(1000),
])

vals = cursor_values(fig, x1=0.015, x2=0.062)
print(vals)
#          channel  value_at_x1  value_at_x2   delta
# 0  test_001/...        12.3         45.7    33.4
# 1  test_002/...        11.8         42.1    30.3
```

### Launch web UI with template

```python
from impakt.web import serve
from impakt import Session

test = Session.open("./test_001")
serve(test, template="frontal_ncap", port=8050)
# Opens browser at http://localhost:8050
```
