Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/skills/grill-me/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: grill-me
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
---

Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time.

If a question can be answered by exploring the codebase, explore the codebase instead.
35 changes: 35 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Checks

on:
push:
pull_request:

jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@v6

- name: Install dependencies
run: uv sync --dev

- name: Check formatting
run: uv run ruff format --check

- name: Lint
run: uv run ruff check

- name: Type check
run: uv run mypy

- name: Test
run: uv run pytest -q
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ wheels/
# All __pycache__ directories
**/__pycache__/
.DS_Store

# Claude Code: track shared skills, ignore machine-local settings and agent memory
.claude/settings.local.json
.claude/projects/
20 changes: 20 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
repos:
- repo: local
hooks:
- id: ruff-format-check
name: ruff format --check
entry: uv run ruff format --check
language: system
pass_filenames: false

- id: ruff-check
name: ruff check
entry: uv run ruff check
language: system
pass_filenames: false

- id: mypy
name: mypy
entry: uv run mypy
language: system
pass_filenames: false
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ value: str | None = None
- Use `ruff` for both linting and formatting:
- Format: `uv run ruff format`
- Lint + fix: `uv run ruff check --fix`
- Required strict type check: `uv run mypy`. Under strict `no_implicit_reexport`, add new public symbols to `__all__`.

### Version Bumping (mandatory)

Expand Down Expand Up @@ -462,4 +463,4 @@ If your test breaks after a refactor that doesn't change behavior, the test was
- Code is ready for production/publishing
- Public API requires clarification

**Rationale**: During prototyping and development, verbose documentation significantly bloats context. Write clear, readable code first. Documentation can be added later when actually needed.
**Rationale**: During prototyping and development, verbose documentation significantly bloats context. Write clear, readable code first. Documentation can be added later when actually needed.
137 changes: 26 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,141 +8,56 @@ uv add forecastinterface

## Documentation

- [Model Interface Specification](docs/model_interface.md)
- [Input Requirement Specification](docs/input_requirement.md)
- [Model Interface Specification](docs/model_interface.md) — the `ForecastModel` protocol, training/lifecycle, and `ModelOutput` types
- [Input Requirement Specification](docs/input_requirement.md) — the `InputRequirement` declaration and the `ModelInputs` bundle

## ModelOutput

Top-level container holding forecast results for one or more variables. Each variable can independently carry deterministic forecasts, quantile forecasts, trajectory ensembles, or any combination.
The result of `predict` / `hindcast` (returned via `ModelResult`). A **station-keyed** container — `variables: dict[station_id, dict[variable_name, VariableOutput]]` — where each `VariableOutput` carries any combination of deterministic, quantile, trajectory, and epistemic-uncertainty data, plus a `status` (`SUCCESS`/`FAILURE`/`PARTIAL`) and quality `flags`. A single-station model returns a one-key outer dict; missing stations are explicit `FAILURE` entries, never absent keys.

### Structure
See the [Model Interface Specification](docs/model_interface.md) for the full schema, DataFrame layouts, and enums.

```
ModelOutput
model_name: str
issue_datetime: datetime
success: bool # derived — True when all variables succeeded
variables: dict[str, VariableOutput] # keyed by variable name

VariableOutput
metadata: VariableMetadata
deterministic: DeterministicData | None
quantiles: QuantileData | None
trajectories: TrajectoryData | None
epistemic_uncertainty: EpistemicUncertaintyData | None
status: VariableStatus # SUCCESS | FAILURE | PARTIAL
flags: frozenset[ForecastFlag]

VariableMetadata
name: str
unit: Unit # e.g. Unit.M3_PER_S → "m³/s"
resolution: TemporalResolution # e.g. TemporalResolution.DAILY
timedelta: timedelta # time step between forecast points
forecast_horizon: int # number of forecast steps (> 0)
offset: int # offset in steps (>= 0)

DeterministicData
data: pl.DataFrame # columns: ["issue_datetime", "datetime", "value"]

QuantileData
quantile_levels: list[float] # e.g. [0.1, 0.5, 0.9] — sorted, in (0, 1)
data: pl.DataFrame # columns: ["issue_datetime", "datetime", "0.1", "0.5", "0.9"]

TrajectoryData
num_samples: int # number of ensemble members (> 0)
data: pl.DataFrame # columns: ["issue_datetime", "datetime", "1", "2", ..., "N"]

EpistemicUncertaintyData
data: pl.DataFrame # columns: ["issue_datetime", "datetime", "std", "range"]
```

### DataFrame Schemas

All DataFrames are validated on construction:

| Container | `issue_datetime` column | `datetime` column | Value columns |
|---|---|---|---|
| `DeterministicData` | `Datetime` | `Datetime` | `value` (numeric) |
| `QuantileData` | `Datetime` | `Datetime` | One per level, named as float strings: `"0.1"`, `"0.5"`, ... |
| `TrajectoryData` | `Datetime` | `Datetime` | One per sample, named `"1"`, `"2"`, ..., `"N"` |
| `EpistemicUncertaintyData` | `Datetime` | `Datetime` | `std` (numeric), `range` (numeric) |

### Enums

**Unit** -- `M3_PER_S`, `MM_PER_DAY`, `MM_PER_S`, `MM`, `CM`, `M`, `DEG_C`, `UNITLESS`

**TemporalResolution** -- `SUB_HOURLY`, `HOURLY`, `SUB_DAILY`, `DAILY`, `WEEKLY`, `MONTHLY`, `SEASONAL`, `ANNUAL`
## InputRequirement

**VariableStatus** -- `SUCCESS`, `FAILURE`, `PARTIAL`
Declares what data a model needs: forecast `targets`, `dynamic` inputs nested as `timedelta` time step → spatial representation → past/future → product → variable (each with its `unit`, `lookback`/`future_steps`, `max_nan`, and optional `aggregation`), and `static` attributes. At run time the model receives a `ModelInputs` bundle isomorphic to this declaration.

**ForecastFlag** -- `HIGH_EPISTEMIC_UNCERTAINTY`, `DATA_AVAILABILITY`
See the [Input Requirement Specification](docs/input_requirement.md) for the full structure and examples.

### Usage
## Usage

```python
from datetime import datetime, timedelta
import polars as pl
from forecast_interface import (
ModelOutput, VariableOutput, VariableMetadata,
DeterministicData, QuantileData, Unit, TemporalResolution, VariableStatus,
DeterministicData, Unit, VariableStatus,
)

issue_dt = datetime(2024, 6, 1, 6, 0)
output = ModelOutput(
model_name="MyModel",
issue_datetime=issue_dt,
variables={
"streamflow": VariableOutput(
metadata=VariableMetadata(
name="streamflow",
unit=Unit.M3_PER_S,
resolution=TemporalResolution.DAILY,
timedelta=timedelta(days=1),
forecast_horizon=10,
offset=0,
),
deterministic=DeterministicData(
data=pl.DataFrame({
"issue_datetime": [issue_dt, issue_dt],
"datetime": [datetime(2024, 6, 1), datetime(2024, 6, 2)],
"value": [42.0, 43.5],
}),
"station_1": {
"streamflow": VariableOutput(
metadata=VariableMetadata(
unit=Unit.M3_PER_S,
timedelta=timedelta(days=1),
forecast_horizon=2,
offset=0,
),
deterministic=DeterministicData(
data=pl.DataFrame({
"issue_datetime": [issue_dt, issue_dt],
"datetime": [datetime(2024, 6, 1), datetime(2024, 6, 2)],
"value": [42.0, 43.5],
}),
),
status=VariableStatus.SUCCESS,
),
status=VariableStatus.SUCCESS,
),
},
},
)

assert output.success is True
```

## InputRequirement

Declares what data a forecasting model needs. The preprocessing pipeline reads this spec and provides exactly the required inputs.

See [Input Requirement Specification](docs/input_requirement.md) for full documentation.

### Structure

```
InputRequirement
dynamic: dict[TemporalResolution, SpatialInputSpec]
static: set[str]

SpatialInputSpec
distributed: DynamicInputSpec | None
lumped: DynamicInputSpec | None

DynamicInputSpec
past_known: dict[str, dict[str, PastKnownVariable]]
future_known: dict[str, dict[str, FutureKnownVariable]]

PastKnownVariable
lookback: int
max_nan: int

FutureKnownVariable
future_steps: int
max_nan: int
ensemble_mode: EnsembleMode # SINGLE or ENSEMBLE
```
2 changes: 0 additions & 2 deletions TODO.md

This file was deleted.

Loading
Loading