Skip to content

Commit 4fb7c7b

Browse files
committed
fix: update test count and improve contributing documentation
1 parent 78672a7 commit 4fb7c7b

3 files changed

Lines changed: 28 additions & 4 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Python 3.11+.
1313

1414
```bash
1515
pip install -e ".[dev]"
16-
python -m pytest -q # 256 tests, ~3 seconds
16+
python -m pytest -q # 326 tests, ~4 seconds
1717
```
1818

1919
CI runs the same `pytest -q` on every push and pull request, on 3.11. If it's

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ python -m tracer.app 8123 # optional port, default 8080
2828
# chart an event file
2929
python momentum.py examples/tracer-sample.json out.png --sport rugby
3030
python momentum.py examples/events_arg_egy.json out.png --sport football
31+
32+
# browse an exported match: open report/index.html and pick the export folder
3133
```
3234

3335
`examples/tracer-sample.json` is an actual tracer export that has been run through the rugby translator, so it doubles as proof that what the tracer writes is what `momentum.py` can read.
@@ -44,6 +46,25 @@ The recognizer only looks at the shape of the line, never at how fast you drew i
4446

4547
Possessions also record how they started (scrum, lineout, penalty, restart, turnover, interception, 22 drop-out), because in rugby that's a fair chunk of what a possession is worth. Most of it comes off the trace: a line crossing the touchline is a lineout, and the kick-to-touch-on-the-full law says where that lineout gets taken. What a line can't show you is a scrum, a penalty, and whether the ball was grounded in the in-goal — the first two are single taps, and the third is a chooser on the chip with the likely answer already picked. Whatever gets inferred turns up as a chip on the pitch, and the chip is also how you correct it. With only two teams to pick from, a wrong guess is one click from right.
4648

49+
## After the match
50+
51+
Two exports, answering different questions.
52+
53+
**Validate + export** writes the momentum JSON `momentum.py` reads — and refuses to write until
54+
[`tracer/validate.py`](tracer/validate.py) has dry-run the real pipeline, so nothing can fail
55+
downstream that didn't already fail there.
56+
57+
**Export data (CSV)** writes an analysis bundle — `match.json` plus `actions.csv`, `players.csv`,
58+
`team.csv`, `positions.csv` — one row per carry, pass and kick, with pitch coordinates in metres.
59+
Every field is optional; a column is blank where nothing was tagged, which is the point.
60+
61+
Open [`report/index.html`](report/) and pick that folder for a pitch map, a Gaussian-KDE heatmap
62+
(including where a team *conceded* penalties), team and player tables, and a momentum curve. No
63+
build step, no server, nothing leaves the machine. That curve is reconstructed and approximate —
64+
[`report/README.md`](report/README.md) says how it differs. Second-half positions fold back into
65+
the first-half frame, so a heatmap aggregates across the whole match instead of splitting to both
66+
ends.
67+
4768
## The momentum model
4869

4970
Every threat event adds momentum energy for its team, and that energy decays exponentially (half-life around 3 minutes for football, set per sport by the translator):
@@ -72,20 +93,23 @@ DataSource.parse() -> Sport.translate() -> MomentumEngine.compute() -> cha
7293
| [`translators/`](translators/) | One `BaseSport` per sport: event weighting plus match structure (duration, half-time marker, decay half-life, axis labels). Static weight tables sit alongside as JSON. |
7394
| [`sources/`](sources/) | One `BaseDataSource` per data provider, parsing raw match data into a common shape. Deliberately independent of `translators/`, so any sport works with any source instead of needing a class per (sport, provider) pair. |
7495
| [`tracer/`](tracer/) | The Live Trace app: capture, recognition, review, export. Writes the same JSON the sources read. |
96+
| [`report/`](report/) | Standalone no-build viewer for an export folder. Client-side only; ports the engine's math to JS for an approximate curve. |
7597

7698
To add a sport, implement `BaseSport` in `translators/`, register it in the `SPORTS` dict in `translators/__init__.py`, and run with `--sport yourname`. [`translators/rugby.py`](translators/rugby.py) is the worked example; its module docstring covers territory-based threat and why cards are markers rather than something fed into the decay sum.
7799

78100
To add a data provider (Opta, StatsBomb, whatever else), implement `BaseDataSource` in `sources/` and map its raw fields into the shape your chosen `Sport.translate()` expects. Nothing downstream changes.
79101

102+
Diagrams for each layer, with the reasoning and the trade-offs behind them, are in [`docs/`](docs/) — six pages covering composition, the recognizer, the possession lifecycle, rugby-law inference, export topology and calibration.
103+
80104
## Tests
81105

82-
256 tests, run on every push and pull request:
106+
326 tests, run on every push and pull request:
83107

84108
```bash
85109
python -m pytest -q
86110
```
87111

88-
The recognizer is gated by a corpus of 39 synthetic trace scenarios in [`tracer/fixtures.py`](tracer/fixtures.py), replayed at baseline config, plus the pace-invariance fence. Every threshold and weight it depends on is a flat constant in [`tracer/config.py`](tracer/config.py). [tracer/TUNING.md](tracer/TUNING.md) covers the loop for moving them (save a trace, promote it with its expected truth, sweep or fit) and where the calibration currently stands.
112+
The recognizer is gated by a corpus of 39 synthetic trace scenarios in [`tracer/fixtures.py`](tracer/fixtures.py), replayed at baseline config, plus the pace-invariance fence. Every threshold and weight it depends on is a flat constant in [`tracer/config.py`](tracer/config.py). [tracer/TUNING.md](tracer/TUNING.md) covers the loop for moving them (save a trace, promote it with its expected truth, sweep or fit) and where the calibration currently stands. Misreads you correct live — clicking a segment to re-cycle it — are logged to a local database and folded into the weight proposal by `python -m tracer.calibrate`, run before the next game. None of the three tools ever writes `config.py`: they print a proposal, you decide.
89113

90114
## Background
91115

tracer/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def _build_header(m, on_undo):
7979
.style(f"background:{m.team_colors['away']}")
8080
w["poss"] = ui.label().classes(
8181
"px-2 py-0.5 rounded-md text-white text-xl font-bold cursor-pointer") \
82-
.on("click", lambda: m.flip_possession()) \
82+
.on("click", lambda: m.flip_possession())
8383
w["status"] = ui.label().classes("text-sm text-gray-500")
8484
ui.button("Halftime flip", on_click=m.halftime_flip).props("outline")
8585
ui.button("Review", on_click=lambda: open_review(m)).props("outline")

0 commit comments

Comments
 (0)