You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
31
33
```
32
34
33
35
`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.
@@ -37,12 +39,31 @@ python momentum.py examples/events_arg_egy.json out.png --sport football
37
39
The mechanic, the full hotkey table and the tuning workflow are in [tracer/README.md](tracer/README.md). The short version:
38
40
39
41
- Hold the mouse button when a possession starts and follow the ball. Pass, run, pass, tackle are all one line.
40
-
- Tap `A` or `Space` when the play dies. That, not letting go of the button, is what ends the chain.
42
+
- Tap `A` or `Space` when the play dies. That, not letting go of the button, is what ends the chain. Drawing the ball out of play ends it for you, since in law it is over.
41
43
- The line redraws colour-coded by inferred action while you go, so you can see what the recognizer thinks.
42
44
43
45
The recognizer only looks at the shape of the line, never at how fast you drew it. That was deliberate: I wanted to trace off paused or scrubbed video and get the same answer as tracing live. [`tracer/tests/test_pace_invariance.py`](tracer/tests/test_pace_invariance.py) exists to stop that quietly breaking.
44
46
45
-
Possessions also record how they started (scrum, lineout, penalty, restart, turnover, interception), because in rugby that's a fair chunk of what a possession is worth. Most of it comes off the trace: a kick that ends at the touchline is a lineout, and the kick-to-touch-on-the-full law says where that lineout gets taken. Scrums and penalties are the two a line can't show you, so those are single taps. 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.
47
+
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.
48
+
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
|[`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. |
73
94
|[`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. |
74
95
|[`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. |
75
97
76
98
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.
77
99
78
100
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.
79
101
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
+
80
104
## Tests
81
105
82
-
225 tests, run on every push and pull request:
106
+
326 tests, run on every push and pull request:
83
107
84
108
```bash
85
109
python -m pytest -q
86
110
```
87
111
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.
Six areas of this repo that are hard to reconstruct from the source alone, each with a diagram and
4
+
the reasoning behind it. Read them in order if you're new; jump straight to the one you need if
5
+
you're not.
6
+
7
+
| # | Page | What it answers |
8
+
|---|------|-----------------|
9
+
| 1 |[System composition](architecture/01-system-composition.md)| What the four packages are, and why the momentum maths knows nothing about rugby |
10
+
| 2 |[The recognizer pipeline](architecture/02-recognizer-pipeline.md)| How one mouse drag becomes a list of carries, passes and kicks — and why drawing speed can't change the answer |
11
+
| 3 |[Possession lifecycle](architecture/03-possession-lifecycle.md)| The chain state machine, the two undo snapshots, and the rewind-and-replay correction model |
12
+
| 4 |[Rugby-law inference](architecture/04-law-inference.md)| How the tool decides a lineout from a scrum from a 22 drop-out, and which facts it refuses to guess |
13
+
| 5 |[Export topology](architecture/05-export-topology.md)| Two parallel event logs, two exits, and the record shapes that land in each |
14
+
| 6 |[Calibration loop](architecture/06-calibration-loop.md)| Where every threshold comes from, and why no tool in this repo may write `config.py`|
15
+
16
+
## Viewing the diagrams
17
+
18
+
Diagrams are native `.drawio` files in [diagrams/](diagrams/) — editable, diff-visible XML rather
19
+
than baked images.
20
+
21
+
-**VS Code** — install the *Draw.io Integration* extension (`hediet.vscode-drawio`) and open the
22
+
file. Multi-page diagrams get a page tab bar at the bottom.
23
+
-**Browser** — [app.diagrams.net](https://app.diagrams.net) → *File ▸ Open from ▸ Device*. Nothing
24
+
is uploaded; the editor runs locally in the tab.
25
+
-**Desktop** — draw.io Desktop opens them directly. It also ships a CLI, which is the only way to
0 commit comments