QA-Improver
This commit is contained in:
85
.claude/agents/qa-improver.md
Normal file
85
.claude/agents/qa-improver.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: qa-improver
|
||||
description: Automatically fix code quality issues identified by a QA assessment. Resolves lint violations, type errors, missing exports, and other mechanical improvements without changing behavior. Use after a quality-scorer run to action its recommendations.
|
||||
tools: Bash Read Write Edit Grep Glob
|
||||
---
|
||||
|
||||
You are a code quality improvement agent for the Impakt project. You receive a QA report and systematically fix the mechanical issues it identifies — without changing any runtime behavior.
|
||||
|
||||
## Inputs
|
||||
|
||||
Read the most recent `docs/QA-*.md` report (excluding QA-TEMPLATE.md and QA-INSTRUCTIONS.md). Extract the "Recommended Actions" table.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Assess what can be auto-fixed
|
||||
|
||||
Categorize each recommended action:
|
||||
|
||||
- **Auto-fixable**: lint auto-fix, import sorting, unused imports/vars
|
||||
- **Mechanical**: adding type annotations, `__all__` exports, duplicate dict keys, line-length fixes
|
||||
- **Requires judgment**: refactoring complex files, adding test coverage, security changes
|
||||
|
||||
Only perform auto-fixable and mechanical fixes. Skip anything that requires judgment or behavioral changes.
|
||||
|
||||
### 2. Execute fixes in order of safety
|
||||
|
||||
Run fixes from safest to most involved:
|
||||
|
||||
**a) Lint auto-fix (safest)**
|
||||
```bash
|
||||
uv run python -m ruff check --fix src/
|
||||
```
|
||||
|
||||
**b) Remaining lint violations**
|
||||
Read the ruff output. For each remaining violation:
|
||||
- F601 (duplicate dict keys): read the file, determine which entry to keep, remove the duplicate
|
||||
- F841 (unused variables): remove or prefix with `_`
|
||||
- E501 (line too long): break the line naturally
|
||||
- F541 (empty f-string): convert to regular string
|
||||
|
||||
**c) Type annotation fixes**
|
||||
Run `uv run python -m mypy src/impakt --ignore-missing-imports` and fix `[type-arg]` errors by adding proper generic parameters (e.g., `dict` -> `dict[str, Any]`, `list` -> `list[str]`). Read context around each error to determine the correct type.
|
||||
|
||||
**d) Missing `__all__` exports**
|
||||
For any module `__init__.py` that lacks `__all__`:
|
||||
- List the public classes and functions in the module's files
|
||||
- Add imports and `__all__` following the pattern in `src/impakt/channel/__init__.py`
|
||||
|
||||
**e) Coverage config (if not already present)**
|
||||
Check if `addopts` in `[tool.pytest.ini_options]` includes `--cov`. If not, add it.
|
||||
|
||||
### 3. Verify after each category
|
||||
|
||||
After each category of fixes, run:
|
||||
```bash
|
||||
uv run python -m ruff check src/
|
||||
uv run python -m pytest --tb=short -q
|
||||
```
|
||||
|
||||
If tests fail, revert the last change and move on. Never leave the codebase in a broken state.
|
||||
|
||||
### 4. Final verification
|
||||
|
||||
Run all three quality tools:
|
||||
```bash
|
||||
uv run python -m ruff check src/
|
||||
uv run python -m mypy src/impakt --ignore-missing-imports
|
||||
uv run python -m pytest --tb=short -q
|
||||
```
|
||||
|
||||
### 5. Report results
|
||||
|
||||
Return a summary:
|
||||
- Which actions were completed
|
||||
- Which were skipped and why
|
||||
- Before/after counts for each tool (lint violations, mypy errors, test results)
|
||||
- Any actions that still require human judgment
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never change runtime behavior.** Only annotations, imports, formatting, and dead code removal.
|
||||
- **Never modify test files.** Only source code under `src/`.
|
||||
- **Verify after every category.** If tests break, revert immediately.
|
||||
- **Be conservative with type annotations.** Use `Any` when the actual type is genuinely dynamic. Don't guess complex types — leave them for human review.
|
||||
- **Don't touch security-sensitive code** (eval, exec, subprocess) unless the QA report explicitly flags an unsafe pattern AND the fix is mechanical.
|
||||
@@ -62,6 +62,7 @@ packages = ["src/impakt"]
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
addopts = "--cov=impakt --cov-report=term-missing:skip-covered --cov-fail-under=60"
|
||||
filterwarnings = [
|
||||
"ignore::pytest.PytestCollectionWarning",
|
||||
]
|
||||
|
||||
136
scripts/qa-improve.sh
Executable file
136
scripts/qa-improve.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# qa-improve.sh — Score the codebase, then auto-fix what's found
|
||||
#
|
||||
# Runs two agents in sequence:
|
||||
# 1. quality-scorer → produces a QA report in docs/
|
||||
# 2. qa-improver → reads the report and fixes mechanical issues
|
||||
# 3. quality-scorer → re-scores to measure improvement
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/qa-improve.sh # default: sonnet
|
||||
# ./scripts/qa-improve.sh --model opus # use opus for deeper analysis
|
||||
# ./scripts/qa-improve.sh --score-only # skip the improvement step
|
||||
# ./scripts/qa-improve.sh --fix-only # skip initial scoring (use latest report)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MODEL="sonnet"
|
||||
BUDGET="1.50"
|
||||
SKIP_SCORE=false
|
||||
SKIP_FIX=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--model) MODEL="$2"; shift 2 ;;
|
||||
--budget) BUDGET="$2"; shift 2 ;;
|
||||
--score-only) SKIP_FIX=true; shift ;;
|
||||
--fix-only) SKIP_SCORE=true; shift ;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--model sonnet|opus|haiku] [--budget USD] [--score-only] [--fix-only]"
|
||||
echo ""
|
||||
echo "Score the codebase, auto-fix issues, then re-score to measure improvement."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --model MODEL Claude model (default: sonnet)"
|
||||
echo " --budget USD Max spend per agent run (default: 1.50)"
|
||||
echo " --score-only Run scoring only, skip auto-fix"
|
||||
echo " --fix-only Skip initial score, fix based on latest report"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Verify prerequisites
|
||||
for cmd in claude uv; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
echo "Error: $cmd not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -f "docs/QA-INSTRUCTIONS.md" ]]; then
|
||||
echo "Error: docs/QA-INSTRUCTIONS.md not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
uv sync --dev --quiet
|
||||
|
||||
SCORE_PROMPT="Run a full codebase quality assessment. \
|
||||
Read docs/QA-INSTRUCTIONS.md for the methodology and rubrics. \
|
||||
Read docs/QA-TEMPLATE.md for the report structure. \
|
||||
Check docs/ for previous QA-*.md reports and compute deltas if any exist. \
|
||||
Collect all raw metrics by running every command in Step 1. \
|
||||
Score each dimension using the Step 2 rubrics. \
|
||||
Compute the composite score using the Step 3 formula. \
|
||||
Write the completed report to docs/QA-<datetime>.md. \
|
||||
Print the composite score, grade, per-dimension scores, and top 3 actions."
|
||||
|
||||
FIX_PROMPT="Read the most recent QA report in docs/ (the QA-*.md file with the latest date, \
|
||||
excluding QA-TEMPLATE.md and QA-INSTRUCTIONS.md). \
|
||||
Extract the Recommended Actions table. \
|
||||
Execute all auto-fixable and mechanical fixes: \
|
||||
1) Run uv run ruff check --fix src/ for lint auto-fixes. \
|
||||
2) Fix remaining lint violations (duplicate keys, unused vars, long lines). \
|
||||
3) Fix mypy type-arg errors by adding proper generic parameters. \
|
||||
4) Add __all__ exports to any modules missing them. \
|
||||
5) Verify after each category with ruff check and pytest. \
|
||||
Never change runtime behavior. Never modify test files. \
|
||||
Report what was fixed and what was skipped."
|
||||
|
||||
# ── Phase 1: Score ──
|
||||
if [[ "$SKIP_SCORE" == false ]]; then
|
||||
echo "Phase 1: Scoring codebase (model: $MODEL)..."
|
||||
echo "────────────────────────────────────────────"
|
||||
|
||||
claude -p \
|
||||
--agent quality-scorer \
|
||||
--model "$MODEL" \
|
||||
--max-budget-usd "$BUDGET" \
|
||||
--allowedTools "Bash Read Write Grep Glob" \
|
||||
--output-format text \
|
||||
"$SCORE_PROMPT"
|
||||
|
||||
REPORT=$(ls -t docs/QA-2*.md 2>/dev/null | head -1)
|
||||
echo ""
|
||||
echo "Report: ${REPORT:-none found}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Phase 2: Fix ──
|
||||
if [[ "$SKIP_FIX" == false ]]; then
|
||||
echo "Phase 2: Auto-fixing issues (model: $MODEL)..."
|
||||
echo "────────────────────────────────────────────"
|
||||
|
||||
claude -p \
|
||||
--agent qa-improver \
|
||||
--model "$MODEL" \
|
||||
--max-budget-usd "$BUDGET" \
|
||||
--allowedTools "Bash Read Write Edit Grep Glob" \
|
||||
--output-format text \
|
||||
"$FIX_PROMPT"
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Phase 3: Re-score ──
|
||||
echo "Phase 3: Re-scoring after fixes (model: $MODEL)..."
|
||||
echo "────────────────────────────────────────────"
|
||||
|
||||
claude -p \
|
||||
--agent quality-scorer \
|
||||
--model "$MODEL" \
|
||||
--max-budget-usd "$BUDGET" \
|
||||
--allowedTools "Bash Read Write Grep Glob" \
|
||||
--output-format text \
|
||||
"$SCORE_PROMPT"
|
||||
|
||||
FINAL_REPORT=$(ls -t docs/QA-2*.md 2>/dev/null | head -1)
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────"
|
||||
echo "Final report: ${FINAL_REPORT:-none found}"
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
@@ -60,10 +60,9 @@ MAIN_LOCATIONS: dict[str, str] = {
|
||||
"CLAV": "Clavicle",
|
||||
"RIBS": "Ribs",
|
||||
"STRN": "Sternum",
|
||||
"SHLR": "Shoulder",
|
||||
"SHLR": "Shoulder Right",
|
||||
"SHLD": "Shoulder",
|
||||
"SHLL": "Shoulder Left",
|
||||
"SHLR": "Shoulder Right",
|
||||
# Spine
|
||||
"SPIN": "Spine",
|
||||
"LUSP": "Lumbar Spine",
|
||||
@@ -74,7 +73,6 @@ MAIN_LOCATIONS: dict[str, str] = {
|
||||
"SACR": "Sacrum",
|
||||
"PUBC": "Pubic Symphysis",
|
||||
# Upper extremities
|
||||
"SHLD": "Shoulder",
|
||||
"UPRA": "Upper Arm",
|
||||
"ELBO": "Elbow",
|
||||
"FORA": "Forearm",
|
||||
@@ -122,7 +120,7 @@ MAIN_LOCATIONS: dict[str, str] = {
|
||||
# Wheel
|
||||
"WHEL": "Wheel",
|
||||
# Structural — additional
|
||||
"FORA": "Floor Rail",
|
||||
"FRAL": "Floor Rail",
|
||||
"FBAR": "Barrier Face",
|
||||
"KNSL": "Knee Slider",
|
||||
# Simulation / other
|
||||
@@ -181,11 +179,6 @@ FINE_LOCATIONS: dict[str, str] = {
|
||||
"URXX": "Upper Right",
|
||||
"LLXX": "Lower Left",
|
||||
"LRXX": "Lower Right",
|
||||
# Tibia positions (real MME data uses these)
|
||||
"LEUP": "Left Upper",
|
||||
"RIUP": "Right Upper",
|
||||
"LELO": "Left Lower",
|
||||
"RILO": "Right Lower",
|
||||
# Fine location with number suffix
|
||||
"0001": "Secondary",
|
||||
"0100": "Lower Position",
|
||||
|
||||
@@ -8,17 +8,17 @@ from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from impakt.channel.code import ChannelCode
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Metadata models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,14 +6,11 @@ Viscous Criterion (VC): V(t) * C(t) where V = deflection velocity, C = compressi
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from impakt.channel.model import Channel, DummyInfo
|
||||
from impakt.criteria.base import CriterionResult
|
||||
|
||||
|
||||
# Initial chest depth by dummy type (mm)
|
||||
CHEST_DEPTH: dict[str, float] = {
|
||||
"H3-50M": 229.0,
|
||||
|
||||
@@ -7,8 +7,6 @@ where the total time the signal exceeds that level equals at least 3 ms.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from impakt.channel.model import Channel, ChannelGroup, DummyInfo
|
||||
|
||||
@@ -6,8 +6,6 @@ Evaluated separately for left and right femur.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from impakt.channel.model import Channel, DummyInfo
|
||||
|
||||
@@ -14,7 +14,6 @@ The reported Nij is the maximum across all four modes and all time steps.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ where:
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -1 +1,17 @@
|
||||
"""Data I/O readers for crash test formats."""
|
||||
|
||||
from impakt.io.csv import CSVReader
|
||||
from impakt.io.mme import MMEReader
|
||||
from impakt.io.reader import ReaderProtocol, ReaderRegistry, get_registry, read, register_reader
|
||||
from impakt.io.tdms import TDMSReader
|
||||
|
||||
__all__ = [
|
||||
"CSVReader",
|
||||
"MMEReader",
|
||||
"ReaderProtocol",
|
||||
"ReaderRegistry",
|
||||
"TDMSReader",
|
||||
"get_registry",
|
||||
"read",
|
||||
"register_reader",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -89,7 +89,6 @@ UNIT_MAP: dict[str, str] = {
|
||||
"Nm": "N·m",
|
||||
"N.m": "N·m",
|
||||
"N*m": "N·m",
|
||||
"Nm": "N·m",
|
||||
"mm": "mm",
|
||||
"m": "m",
|
||||
"cm": "cm",
|
||||
@@ -626,7 +625,7 @@ class MMEReader:
|
||||
unit = _normalize_unit(header.get("unit", ""))
|
||||
dt = _parse_float(header.get("sampling interval", "0"))
|
||||
t_first = _parse_float(header.get("time of first sample", "0"))
|
||||
num_samples_declared = _parse_int(header.get("number of samples", "0"))
|
||||
_parse_int(header.get("number of samples", "0")) # validated by data length
|
||||
cfc_str = header.get("channel frequency class", "")
|
||||
cfc_class: int | None = None
|
||||
if cfc_str and cfc_str.upper() != "NOVALUE":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,8 +6,6 @@ across all plotted channels.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import plotly.graph_objects as go
|
||||
|
||||
from impakt.channel.model import Channel
|
||||
from impakt.plot.spec import ChannelRef, Corridor, CursorValues, PlotSpec, PlotStyle
|
||||
from impakt.plot.spec import Corridor, CursorValues, PlotSpec
|
||||
|
||||
# Default color palette (colorblind-friendly)
|
||||
DEFAULT_COLORS = [
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from impakt.plot.engine import PlotEngine
|
||||
from impakt.plot.spec import PlotSpec
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
"""Plugin system for extensibility."""
|
||||
|
||||
from impakt.plugin.registry import (
|
||||
ImpaktPlugin,
|
||||
PluginRegistry,
|
||||
discover_all,
|
||||
discover_directory,
|
||||
discover_entry_points,
|
||||
get_plugin_registry,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ImpaktPlugin",
|
||||
"PluginRegistry",
|
||||
"discover_all",
|
||||
"discover_directory",
|
||||
"discover_entry_points",
|
||||
"get_plugin_registry",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,6 @@ Threshold values are versioned — this module supports multiple protocol years.
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from impakt.criteria.base import CriterionResult
|
||||
from impakt.protocol.base import BodyRegionScore, Color, ProtocolResult
|
||||
|
||||
@@ -7,7 +7,6 @@ The overall rating is determined by the worst sub-rating.
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from impakt.criteria.base import CriterionResult
|
||||
from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating
|
||||
|
||||
@@ -8,7 +8,6 @@ injury probability, which are then combined.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from impakt.criteria.base import CriterionResult
|
||||
from impakt.protocol.base import BodyRegionScore, ProtocolResult, Rating
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
"""PDF and report generation."""
|
||||
|
||||
from impakt.report.engine import (
|
||||
generate_injury_summary,
|
||||
generate_plot_sheet,
|
||||
generate_protocol_report,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"generate_injury_summary",
|
||||
"generate_plot_sheet",
|
||||
"generate_protocol_report",
|
||||
]
|
||||
|
||||
@@ -155,7 +155,8 @@ def _fallback_protocol_html(
|
||||
test_info = f"""
|
||||
<div class="test-info">
|
||||
<p><strong>Test:</strong> {metadata.test_number}</p>
|
||||
<p><strong>Vehicle:</strong> {metadata.vehicle.year} {metadata.vehicle.make} {metadata.vehicle.model}</p>
|
||||
<p><strong>Vehicle:</strong> {metadata.vehicle.year} {metadata.vehicle.make} \
|
||||
{metadata.vehicle.model}</p>
|
||||
<p><strong>Dummy:</strong> {metadata.dummy.dummy_type} ({metadata.dummy.position})</p>
|
||||
</div>
|
||||
"""
|
||||
@@ -178,7 +179,11 @@ def _fallback_protocol_html(
|
||||
color_badge = f'<span class="badge" style="background:{bg}">{rs.rating.value}</span>'
|
||||
|
||||
points_str = f"{rs.points:.1f}/{rs.max_points:.1f}" if rs.max_points > 0 else ""
|
||||
rows += f"<tr><td>{rs.region}</td><td>{rs.criterion}</td><td>{rs.value:.2f} {rs.unit}</td><td>{color_badge}</td><td>{points_str}</td></tr>"
|
||||
rows += (
|
||||
f"<tr><td>{rs.region}</td><td>{rs.criterion}</td>"
|
||||
f"<td>{rs.value:.2f} {rs.unit}</td>"
|
||||
f"<td>{color_badge}</td><td>{points_str}</td></tr>"
|
||||
)
|
||||
|
||||
stars_display = ""
|
||||
if result.stars is not None:
|
||||
@@ -207,10 +212,12 @@ def _fallback_protocol_html(
|
||||
<div class="summary">
|
||||
<p><span class="stars">{stars_display}</span></p>
|
||||
<p><strong>Overall:</strong> {result.overall_rating}</p>
|
||||
<p><strong>Score:</strong> {result.total_points:.1f}/{result.max_points:.1f} ({result.percentage:.0f}%)</p>
|
||||
<p><strong>Score:</strong> \
|
||||
{result.total_points:.1f}/{result.max_points:.1f} ({result.percentage:.0f}%)</p>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>Body Region</th><th>Criterion</th><th>Value</th><th>Rating</th><th>Points</th></tr>
|
||||
<tr><th>Body Region</th><th>Criterion</th><th>Value</th>\
|
||||
<th>Rating</th><th>Points</th></tr>
|
||||
{rows}
|
||||
</table>
|
||||
</body>
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
"""Scripting API and CLI."""
|
||||
|
||||
from impakt.script.api import ChannelHandle, Session, Template, TransformProxy
|
||||
from impakt.script.cli import main
|
||||
|
||||
__all__ = [
|
||||
"ChannelHandle",
|
||||
"Session",
|
||||
"Template",
|
||||
"TransformProxy",
|
||||
"main",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -32,12 +32,12 @@ import numpy as np
|
||||
from impakt.channel.model import Channel, ChannelGroup, TestData, TestMetadata
|
||||
from impakt.criteria.base import CriterionResult
|
||||
from impakt.io.mme import MMEReader
|
||||
from impakt.io.reader import ReaderRegistry, get_registry, register_reader
|
||||
from impakt.plot.engine import PlotEngine, cursor_values
|
||||
from impakt.plot.spec import ChannelRef, CursorValues, PlotSpec, PlotStyle
|
||||
from impakt.io.reader import get_registry, register_reader
|
||||
from impakt.plot.engine import PlotEngine
|
||||
from impakt.plot.spec import ChannelRef, PlotSpec, PlotStyle
|
||||
from impakt.protocol.base import ProtocolResult
|
||||
from impakt.template.library import TemplateLibrary
|
||||
from impakt.template.model import SessionState, TemplateSpec
|
||||
from impakt.template.model import TemplateSpec
|
||||
from impakt.template.session import SessionManager
|
||||
from impakt.transform.cfc import CFCFilter
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
@@ -121,7 +119,6 @@ def _cmd_channels(args: argparse.Namespace) -> None:
|
||||
|
||||
def _cmd_evaluate(args: argparse.Namespace) -> None:
|
||||
from impakt.script.api import Session
|
||||
from impakt.criteria import hic, clip_3ms, nij, chest_deflection, femur_load
|
||||
|
||||
session = Session.open(args.path)
|
||||
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
"""Template and session management."""
|
||||
|
||||
from impakt.template.library import TemplateLibrary
|
||||
from impakt.template.model import PlotDefinition, SessionState, TemplateSpec
|
||||
from impakt.template.session import SessionManager
|
||||
|
||||
__all__ = [
|
||||
"PlotDefinition",
|
||||
"SessionManager",
|
||||
"SessionState",
|
||||
"TemplateLibrary",
|
||||
"TemplateSpec",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,8 +6,8 @@ Manages the global template library at ``~/.impakt/templates/``.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
from impakt.template.model import TemplateSpec
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ Sessions bind templates to specific test data and track user overrides.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -94,7 +93,6 @@ class TemplateSpec:
|
||||
for plot_data in data.get("plots", []):
|
||||
channel_patterns = []
|
||||
transforms = []
|
||||
corridors = []
|
||||
|
||||
for ch in plot_data.get("channels", []):
|
||||
if isinstance(ch, str):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -76,7 +76,7 @@ class XAlign:
|
||||
# Threshold never crossed — no shift
|
||||
return channel.with_data(
|
||||
data=channel.data,
|
||||
transform_note=f"X-align threshold (no crossing found)",
|
||||
transform_note="X-align threshold (no crossing found)",
|
||||
)
|
||||
|
||||
crossing_time = float(channel.time[indices[0]])
|
||||
|
||||
@@ -7,7 +7,7 @@ TransformChain composes multiple transforms into a reproducible pipeline.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from impakt.channel.model import Channel
|
||||
|
||||
@@ -15,13 +15,11 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
from impakt.channel.lookup import CFC_CLASSES, CFC_MINIMUM_SAMPLE_RATES
|
||||
from impakt.channel.model import Channel
|
||||
|
||||
|
||||
VALID_CFC_CLASSES = frozenset(CFC_CLASSES.keys())
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ with numpy functions.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
@@ -16,7 +15,6 @@ import numpy as np
|
||||
from impakt.channel.code import ChannelCode
|
||||
from impakt.channel.model import Channel
|
||||
|
||||
|
||||
# Allowed numpy functions in expressions
|
||||
_SAFE_NUMPY = {
|
||||
"abs": np.abs,
|
||||
|
||||
@@ -6,9 +6,6 @@ The AppState holds Session objects server-side; Dash stores hold lightweight UI
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import dash
|
||||
from dash import ALL, Input, Output, State, html, no_update
|
||||
from dash.exceptions import PreventUpdate
|
||||
from dash import ALL, Input, Output, State
|
||||
|
||||
from impakt.plot.engine import DEFAULT_COLORS
|
||||
from impakt.web.components.channel_grid import (
|
||||
@@ -52,7 +51,6 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
if visible_data is None:
|
||||
return prev_selected or []
|
||||
|
||||
prev = set(prev_selected or [])
|
||||
visible_keys = {row["key"] for row in visible_data}
|
||||
|
||||
# Keys currently checked in the visible table
|
||||
@@ -77,7 +75,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
Output("selected-channels-badges", "children"),
|
||||
[Input("selected-channels-store", "data")],
|
||||
)
|
||||
def update_badges(selected_keys: list[str] | None) -> list:
|
||||
def update_badges(selected_keys: list[str] | None) -> list[Any]:
|
||||
return build_selected_channels_badges(selected_keys or [], app_state)
|
||||
|
||||
@app.callback(
|
||||
@@ -104,7 +102,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
direction: str | None,
|
||||
selected_keys: list[str] | None,
|
||||
all_rows: list[dict[str, Any]] | None,
|
||||
) -> tuple[list[dict[str, Any]], list[int], list[dict]]:
|
||||
) -> tuple[list[dict[str, Any]], list[int], list[dict[str, Any]]]:
|
||||
"""Filter rows AND recompute selected_rows + color styling."""
|
||||
if not all_rows:
|
||||
return [], [], []
|
||||
@@ -135,7 +133,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
selected_indices.append(idx)
|
||||
|
||||
# Build style_data_conditional for coloring selected rows
|
||||
style_cond: list[dict] = []
|
||||
style_cond: list[dict[str, Any]] = []
|
||||
for idx, row in enumerate(filtered):
|
||||
if row["key"] in color_map:
|
||||
ci = color_map[row["key"]]
|
||||
@@ -168,7 +166,7 @@ def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
Output("per-channel-overrides", "children"),
|
||||
[Input("selected-channels-store", "data")],
|
||||
)
|
||||
def update_per_channel_overrides(selected_keys: list[str] | None) -> list:
|
||||
def update_per_channel_overrides(selected_keys: list[str] | None) -> list[Any]:
|
||||
"""Show per-channel CFC override controls for selected channels."""
|
||||
return build_per_channel_override_rows(
|
||||
selected_keys or [],
|
||||
|
||||
@@ -9,11 +9,9 @@ Handles:
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
from typing import Any
|
||||
|
||||
import dash
|
||||
import numpy as np
|
||||
from dash import Input, Output, State, html
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
@@ -42,7 +40,7 @@ def register_corridor_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
filename: str | None,
|
||||
corridor_name: str | None,
|
||||
current_corridors: list[dict[str, Any]] | None,
|
||||
) -> tuple[list[dict[str, Any]], Any, list]:
|
||||
) -> tuple[list[dict[str, Any]], Any, list[Any]]:
|
||||
if contents is None:
|
||||
raise PreventUpdate
|
||||
|
||||
@@ -123,7 +121,7 @@ def register_corridor_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _build_corridor_list(corridors: list[dict[str, Any]]) -> list:
|
||||
def _build_corridor_list(corridors: list[dict[str, Any]]) -> list[Any]:
|
||||
"""Build the active corridors display list."""
|
||||
if not corridors:
|
||||
return [html.Div("No corridors loaded", className="text-muted", style={"fontSize": "10px"})]
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Any
|
||||
|
||||
import dash
|
||||
from dash import Input, Output, State, html
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
from impakt.web.components.criteria import build_criteria_results_display
|
||||
from impakt.web.state import AppState
|
||||
|
||||
@@ -10,8 +10,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import dash
|
||||
from dash import Input, Output, State, html
|
||||
from dash.exceptions import PreventUpdate
|
||||
from dash import Input, Output, State
|
||||
|
||||
from impakt.plot.engine import DEFAULT_COLORS
|
||||
from impakt.web.callbacks.plot_callbacks import _resolve_channels
|
||||
@@ -61,7 +60,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
selected_keys: list[str] | None,
|
||||
cfc_value: str,
|
||||
y_align: bool,
|
||||
channel_overrides: dict | None,
|
||||
channel_overrides: dict[str, Any] | None,
|
||||
x_align_method: str,
|
||||
x_align_value: float | None,
|
||||
show_resultant: bool,
|
||||
@@ -105,7 +104,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
x_align_method: str,
|
||||
x_align_value: float | None,
|
||||
show_resultant: bool,
|
||||
) -> list[dict]:
|
||||
) -> list[dict[str, Any]]:
|
||||
if not selected_keys:
|
||||
return []
|
||||
|
||||
@@ -119,7 +118,7 @@ def register_cursor_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
show_resultant,
|
||||
)
|
||||
|
||||
style_cond: list[dict] = []
|
||||
style_cond: list[dict[str, Any]] = []
|
||||
for i in range(len(channels)):
|
||||
color = DEFAULT_COLORS[i % len(DEFAULT_COLORS)]
|
||||
style_cond.append(
|
||||
|
||||
@@ -8,14 +8,12 @@ Handles:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import dash
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from dash import Input, Output, State, dcc, html, no_update
|
||||
from dash import Input, Output, State, dcc, html
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
from impakt.web.callbacks.plot_callbacks import _resolve_channels
|
||||
@@ -116,7 +114,6 @@ def register_export_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
|
||||
# Generate report
|
||||
try:
|
||||
from impakt.report.engine import generate_protocol_report
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w") as f:
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import dash
|
||||
from dash import Input, Output, State, html, no_update
|
||||
from dash import Input, Output, State, no_update
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
from impakt.web.state import AppState
|
||||
|
||||
@@ -17,7 +17,6 @@ import dash
|
||||
import numpy as np
|
||||
import plotly.graph_objects as go
|
||||
from dash import Input, Output, State
|
||||
from dash.exceptions import PreventUpdate
|
||||
|
||||
from impakt.channel.model import Channel
|
||||
from impakt.plot.engine import DEFAULT_COLORS, PlotEngine
|
||||
@@ -148,7 +147,7 @@ def _build_plot_spec(
|
||||
channels: list[tuple[str, Channel]],
|
||||
cursor_x1: float | None,
|
||||
cursor_x2: float | None,
|
||||
corridors: list[dict] | None = None,
|
||||
corridors: list[dict[str, Any]] | None = None,
|
||||
) -> PlotSpec:
|
||||
"""Build a PlotSpec from resolved channels and UI state.
|
||||
|
||||
@@ -228,8 +227,8 @@ def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None:
|
||||
x_align_method: str,
|
||||
cursor_x1: float | None,
|
||||
cursor_x2: float | None,
|
||||
channel_overrides: dict | None,
|
||||
corridors_data: list[dict] | None,
|
||||
channel_overrides: dict[str, Any] | None,
|
||||
corridors_data: list[dict[str, Any]] | None,
|
||||
x_align_value: float | None,
|
||||
) -> go.Figure:
|
||||
if not selected_keys:
|
||||
|
||||
@@ -14,13 +14,12 @@ without reordering.
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dash_table, dcc, html
|
||||
|
||||
from impakt.channel.lookup import DIRECTIONS, MAIN_LOCATIONS, MEASUREMENTS
|
||||
from impakt.channel.lookup import MAIN_LOCATIONS, MEASUREMENTS
|
||||
from impakt.plot.engine import DEFAULT_COLORS
|
||||
from impakt.web.state import AppState
|
||||
|
||||
@@ -277,7 +276,7 @@ def build_channel_grid(app_state: AppState) -> dbc.Card:
|
||||
def build_selected_channels_badges(
|
||||
selected_keys: list[str],
|
||||
app_state: AppState,
|
||||
) -> list:
|
||||
) -> list[Any]:
|
||||
"""Build badge pills showing currently selected channels."""
|
||||
if not selected_keys:
|
||||
return [
|
||||
|
||||
@@ -14,9 +14,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
import numpy as np
|
||||
from dash import dash_table, dcc, html
|
||||
|
||||
from impakt.channel.model import Channel
|
||||
@@ -152,7 +151,7 @@ def build_channel_values_panel() -> dbc.Card:
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
style_cell_conditional=[
|
||||
# Fixed-width columns (percentages sum to ~74%, leaving ~26% for Description)
|
||||
# Fixed-width columns (~74%, leaving ~26% for Description)
|
||||
{
|
||||
"if": {"column_id": "ch_num"},
|
||||
"width": "3%",
|
||||
|
||||
@@ -8,8 +8,6 @@ Provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Any
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import html
|
||||
|
||||
from impakt.channel.model import Channel, TestData
|
||||
from impakt.channel.model import TestData
|
||||
from impakt.criteria import (
|
||||
CriterionResult,
|
||||
chest_deflection,
|
||||
@@ -21,7 +21,6 @@ from impakt.criteria import (
|
||||
hic15,
|
||||
nij,
|
||||
tibia_index,
|
||||
viscous_criterion,
|
||||
)
|
||||
from impakt.protocol.base import Color, ProtocolResult, Rating
|
||||
|
||||
@@ -229,7 +228,7 @@ def build_criteria_results_display(
|
||||
protocol_result: ProtocolResult | None = None,
|
||||
) -> html.Div:
|
||||
"""Build the criteria results display."""
|
||||
elements: list = []
|
||||
elements: list[Any] = []
|
||||
|
||||
# Protocol summary
|
||||
if protocol_result:
|
||||
|
||||
@@ -7,7 +7,7 @@ and template selector.
|
||||
from __future__ import annotations
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
from dash import html
|
||||
|
||||
from impakt.web.state import AppState
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ Example: sqrt(ax**2 + az**2) with ax=Head Accel X, az=Head Accel Z
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
from dash import html
|
||||
|
||||
from impakt.web.state import AppState
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ plot panes. Each pane has its own channel selection and axis labels.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
|
||||
|
||||
# Layout presets: (rows, cols)
|
||||
LAYOUT_PRESETS: dict[str, tuple[int, int]] = {
|
||||
"1x1": (1, 1),
|
||||
@@ -57,7 +58,7 @@ def build_plot_grid(layout: str = "1x1") -> html.Div:
|
||||
)
|
||||
|
||||
|
||||
def _build_panes(layout: str) -> list:
|
||||
def _build_panes(layout: str) -> list[Any]:
|
||||
"""Build plot pane elements for a given layout."""
|
||||
rows, cols = LAYOUT_PRESETS.get(layout, (1, 1))
|
||||
pane_count = rows * cols
|
||||
@@ -110,7 +111,7 @@ def _build_single_pane(pane_idx: int, total_panes: int) -> dbc.Card:
|
||||
)
|
||||
|
||||
|
||||
def build_empty_plot_figure() -> dict:
|
||||
def build_empty_plot_figure() -> dict[str, Any]:
|
||||
"""Build an empty plot figure with instruction text."""
|
||||
return {
|
||||
"data": [],
|
||||
|
||||
@@ -10,10 +10,8 @@ Provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
from dash import html
|
||||
|
||||
from impakt.web.state import AppState
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ Provides global transform defaults and per-channel override capability.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
|
||||
@@ -92,7 +94,7 @@ def build_transform_panel() -> dbc.Card:
|
||||
def build_per_channel_override_rows(
|
||||
selected_keys: list[str],
|
||||
overrides: dict[str, dict[str, str]],
|
||||
) -> list:
|
||||
) -> list[Any]:
|
||||
"""Build per-channel override controls for selected channels.
|
||||
|
||||
Shows a compact row per selected channel with a CFC dropdown override.
|
||||
|
||||
@@ -8,13 +8,12 @@ Two tabs:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash import dcc, html
|
||||
|
||||
from impakt.web.components.channel_grid import build_channel_grid
|
||||
from impakt.web.components.channel_values import build_channel_values_panel
|
||||
from impakt.web.components.corridors import build_corridor_panel
|
||||
from impakt.web.components.criteria import build_criteria_panel
|
||||
from impakt.web.components.header import (
|
||||
build_header,
|
||||
@@ -22,9 +21,8 @@ from impakt.web.components.header import (
|
||||
build_overlay_modal,
|
||||
build_test_info_panel,
|
||||
)
|
||||
from impakt.web.components.plot_grid import build_plot_grid
|
||||
from impakt.web.components.corridors import build_corridor_panel
|
||||
from impakt.web.components.math_builder import build_math_panel
|
||||
from impakt.web.components.plot_grid import build_plot_grid
|
||||
from impakt.web.components.report import build_report_panel
|
||||
from impakt.web.components.templates import build_template_panel
|
||||
from impakt.web.components.transforms import build_transform_panel
|
||||
|
||||
@@ -15,12 +15,10 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from impakt.channel.model import Channel, TestData
|
||||
from impakt.channel.model import Channel
|
||||
from impakt.script.api import Session
|
||||
from impakt.template.library import TemplateLibrary
|
||||
from impakt.template.model import PlotDefinition, TemplateSpec
|
||||
from impakt.transform.align import YAlign
|
||||
from impakt.transform.cfc import CFCFilter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user