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.
@@ -44,6 +46,25 @@ The recognizer only looks at the shape of the line, never at how fast you drew i
44
46
45
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.
46
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
66
+
ends.
67
+
47
68
## The momentum model
48
69
49
70
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):
|[`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
-
256 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.
0 commit comments