Add SWOPP3 analysis script and violations module#67
Conversation
f38a116 to
011bdd8
Compare
011bdd8 to
6b79cc0
Compare
fjsuarez
left a comment
There was a problem hiding this comment.
Review: PR #67 — Add SWOPP3 analysis script and violations module
Scope: 2 new files, +2974 lines, 1 commit, targeting feat/swopp3-clean
What's good
routetools/violations.pyis well-structured: typed dataclasses, Protocol for land-checker, proper separation of concerns, imports fromroutetools.weatherconstants (not hardcoded thresholds).scripts/swopp3_analysis.pyis thorough — 13 publication-quality figures with consistent IE branding, good docstrings, graceful degradation for missing data.- Clean merge against current
feat/swopp3-clean. - Good module docstring in
violations.pywith usage documentation. - Threshold constants reused from
routetools.weather.DEFAULT_TWS_LIMIT/DEFAULT_HS_LIMIT— consistent with the CodaBench scorer.
Issues to address
1. No tests for violations.py (blocking)
violations.py is being added to the core library (routetools/), not scripts. Per repo conventions, library modules need test coverage. There should be at least:
count_summary_weather_violations— unit test with a temp CSVcount_land_violations— unit test with mock land checkercount_folder_violations— integration test with a small temp folder structurefind_team_prefix— test with known file patterns + FileNotFoundError caseis_gc_case— quick parametric testScenarioViolationCounts.total_violations/total_penalty— property testsformat_grouped_violation_table/write_grouped_violation_csv— roundtrip test
2. Very stale base — needs rebase (blocking)
The branch diverges from a point before all test files were added — the diff shows 5213 deleted test lines across 18 files. This means the branch is forked from before:
- USNYS port fix
- MWA wrapping fix
- FMS branch merge (PR #64)
- Circular interpolation tests
- All current test infrastructure
Please rebase onto current feat/swopp3-clean so the branch includes these and tests pass in isolation.
3. Global mutable state in swopp3_analysis.py
OUTPUT_DIR: Path = _REPO_ROOT / "output"
FIGS_DIR: Path = _REPO_ROOT / "output" / "analysis"
# ...
def main():
global OUTPUT_DIR, FIGS_DIREvery figure function reads OUTPUT_DIR / FIGS_DIR as globals. This makes the functions untestable in isolation and fragile. Consider passing these as parameters (or a simple config dataclass) through main() → figure functions.
4. swopp3_analysis.py is 2353 lines — consider splitting
The script handles 13 figures + summary table + data loading + style. Consider:
- Extract shared data loading into a small helper module (or reuse parts of
violations.py) - Group related figures into smaller modules
Not blocking, but the current size will make maintenance painful.
5. Hardcoded experiment folder names
EXPERIMENTS = {
"no_penalty": {"folder": "swopp3_no_penalty", ...},
"no_penalty_fms": {"folder": "swopp3_no_penalty_fms", ...},
...
}The 4 experiment folder names and the team prefix IEUniversity-1 are hardcoded in both files. If PR #65 (experiment config profiles) lands first, consider reading folder names from config.toml instead of duplicating them.
6. violations.py duplicates ERA5 loading pattern
_loadable_era5_paths() and load_default_weather_resources() duplicate logic already in scripts/swopp3_run.py and routetools/era5/loader.py. Consider refactoring the shared ERA5 path resolution into a single utility.
7. load_tracks() uses random_state=42 without documenting why
sample = summary.sample(min(n_sample, len(summary)), replace=False, random_state=42)This is fine for reproducibility but the magic number should have a comment noting it's for deterministic track sampling in figures.
8. Cartopy + shapefile as implicit dependencies
violations.py imports cartopy and shapefile/shapely at function level, which is good for lazy loading. But neither appears in pyproject.toml dependencies. They should be listed as optional extras or documented as required for the violations module.
Minor nits
_red_line_rule()is defined but never called — dead code._outlier_capname suggests it returns capped data, but it returns a boolean mask. Consider_outlier_mask.- The PR description says "Violation counts validated against expected totals from
output/cmaes_weather" — this validation should be captured as an automated test, not a one-time manual check.
Suggested actions
- Rebase onto current
feat/swopp3-clean(blocking) - Add
tests/test_violations.pywith unit tests for the core library module (blocking) - Pass output paths as parameters instead of globals
- Consider extracting shared ERA5 loading to avoid duplication
|
Addressed the review comments on this PR. What changed
Rebase statusI fetched Current state is:
So this branch is not stale relative to the target branch in its current state. ValidationBoth commands pass on the updated branch:
Follow-upI did not split I also did not wire experiment folder names to |
|
Follow-up after I revisited the experiment-folder comment and updated A few details:
Validation still passes:
|
fjsuarez
left a comment
There was a problem hiding this comment.
Review of #67: Add SWOPP3 analysis script and violations module
Good work overall — the violations.py module is well-structured with clean separation between threshold counting, land checking, and smooth penalty accumulation. The test coverage for violations.py is solid.
Summary
Approve with minor suggestions. Nothing is a blocker; the code is functional and correct.
What works well
violations.pyhas a clean public API (count_folder_violations,format_grouped_violation_table,write_grouped_violation_csv)- The Codabench subsampling rule (
step = max(1, len(waypoints) // 50)) is faithfully reproduced ScenarioViolationCountsas a frozen dataclass is the right patternloadable_era5_paths()inera5/loader.pyis a useful generalized helpertest_violations.pycovers the key paths well (235 lines of solid tests)- The analysis script produces publication-quality figures with consistent IE branding
Issues (all minor)
- Unused
case_idparameter incount_summary_weather_violations()(see inline comment) travel_timeunit ambiguity: The code correctly passespassage_hours, matching how the rest of the codebase works — butwind_penalty_smooth's docstring says "seconds". Worth a clarifying comment (see inline)pd.concatin a loop infig_penalty_tradeoff()— O(n²) pattern, easy to replace with list + single concat- Global
warnings.filterwarnings("ignore")— could mask real warnings when importing individual figure functions; better scoped insidemain() - No test for
loadable_era5_paths()— new public function inera5/loader.pywith branching logic (regex match, next-year file, glob fallback) that would benefit from a few unit tests importlibtest pattern — fragile for testing script internals; consider extracting shared helpers into an importable module
Questions
- The analysis script hardcodes 4 experiment configs (
no_penalty,no_penalty_fms,penalty,penalty_fms). With the penalty sweep work we're doing now, we'll likely want more configs. Is the plan to parametrize these later via config.toml, or is this script specifically for the publication comparison? load_default_land_checker()creates aunary_unionof all 10m Natural Earth shapes on every call (no caching). For a one-shot script this is fine but ifcount_folder_violationsis called in a loop over many folders, the caller should pre-build the checker. Worth noting in the docstring?
| wind_violations = 0 | ||
| wave_violations = 0 | ||
| with summary_path.open(newline="") as handle: | ||
| reader = csv.DictReader(handle) |
There was a problem hiding this comment.
Unused parameter: case_id is accepted but never referenced in the function body. Either remove it or use it (e.g., for logging/validation).
| curve, | ||
| resources.wavefield, | ||
| weight=wave_penalty_weight, | ||
| travel_time=passage_hours, |
There was a problem hiding this comment.
The wind_penalty_smooth docstring says travel_time is in seconds, but in practice the entire codebase (cmaes.py, swopp3_runner.py) passes passage_hours — i.e., hours. This code correctly follows the same convention (hours), but the mismatch with the docstring is confusing. Might be worth adding a comment here:
# travel_time is in hours, matching time_offset (hours since epoch).
# The wind_penalty_smooth/wave_penalty_smooth docstrings say "seconds"
# but the actual implementation just needs consistent units with time_offset.| tracks/ | ||
| <details_filename from summary CSV> # per-voyage track | ||
|
|
||
| Missing experiment folders or individual CSVs are silently skipped; |
There was a problem hiding this comment.
This suppresses ALL UserWarning globally for any code that imports this module as a library. This is fine for a CLI script but could mask real warnings if someone imports individual figure functions. Consider scoping it inside main() or using a context manager.
| "Oct", | ||
| "Nov", | ||
| "Dec", | ||
| ] |
There was a problem hiding this comment.
pd.concat inside a loop is O(n²). Consider collecting dicts in a list and creating the DataFrame once at the end:
records = []
for case_id in OPT_CASES:
for exp_key in ["no_penalty", "penalty"]:
piece = sub[(sub["experiment"] == exp_key) & (sub["case_id"] == case_id)]
if piece.empty:
continue
records.append({
"case_id": case_id,
"experiment": exp_key,
"wind_violation_pct": piece["wind_viol"].mean() * 100,
"wave_violation_pct": piece["wave_viol"].mean() * 100,
"any_violation_pct": piece["any_viol"].mean() * 100,
"mean_energy": piece["energy_cons_mwh"].mean(),
})
metrics = pd.DataFrame(records)| if exact_next_year.exists(): | ||
| return [path, exact_next_year] | ||
|
|
||
| continuation_paths = sorted(path.parent.glob(f"{prefix}{next_year}_*.nc")) |
There was a problem hiding this comment.
Missing test coverage: loadable_era5_paths() is a new public function called by violations.py, but there's no test for it. Would be good to add a simple test verifying:
- Returns
[path]when filename doesn't match the ERA5 pattern - Returns
[path, next_year_path]when a continuation file exists - Handles the glob case (
prefix_{next_year}_*.nc)
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
There was a problem hiding this comment.
Loading the script via importlib is fragile and pollutes sys.modules. If the helpers being tested (AnalysisPaths, _configured_output_dirs, _experiment_folder) are stable public-ish API, consider extracting them into a small importable module (e.g., routetools/analysis_config.py) that both the script and the tests can import normally. That would also make the 2500-line script slightly more manageable.
336be60 to
6619210
Compare
Summary
Adds two files for SWOPP3 post-processing and competition analysis.
routetools/violations.pyNew core library module that counts Codabench-style route violations for SWOPP3 output folders:
max_wind_mps > 20max_hs_m > 7step = max(1, len(waypoints) // 50))Optionally accumulates smooth ERA5 wind/wave penalties alongside threshold counts. Great-circle cases are excluded by default (
include_gc=False).Key public API:
count_folder_violations,format_grouped_violation_table,write_grouped_violation_csv.scripts/swopp3_analysis.pyPublication-quality analysis script producing 13 matplotlib figures and summary CSVs for the four SWOPP3 experiments (
no_penalty,no_penalty_fms,penalty,penalty_fms).Usage:
Validation
output/cmaes_weather