Skip to content

feat: Let each condition name its own positions and move the envelope to schema v5 - #18

Merged
shackmann merged 10 commits into
mainfrom
stefan/v5
Sep 24, 2026
Merged

shackmann merged 10 commits into
mainfrom
stefan/v5

Conversation

@shackmann

@shackmann shackmann commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds support for the served v5 envelope. A condition request no longer carries one ConditionBlock pinned to a single future position. Each EqualityCondition / IntervalCondition now names the positions it covers through its own query_time_indices, and None covers every entry of query_times. A v5 deployment compares schema_version for exact equality before any other validation. So this client must be released before any deployment serves v5, because the released 0.8.0 would be refused on every request, forecast included.

This branch also carries the unreleased work already on main (#17: the log_prob return mode, answering every declared column by default). Releasing it gives 0.9.0, which the examples repo already declares as its floor.

Breaking changes

  • ConditionBlock is removed. condition= now takes one Condition (EqualityCondition | IntervalCondition) or a sequence of them, on forecast, forecast_mean, forecast_samples, forecast_quantiles and forecast_log_prob.
  • SCHEMA_VERSION is now "v5". The wire field condition: {query_time_index, conditions} is replaced by a top-level conditions list, and each entry carries query_time_indices.
  • resolve_conditions is now exported. It normalizes one condition or a sequence and rejects two conditions on one column at a shared position.

Migration:

# before (v4)
condition=ConditionBlock(
    query_time_index=1,
    conditions=[EqualityCondition(column="driver", value=1.5)],
)

# after (v5)
condition=EqualityCondition(column="driver", value=1.5, query_time_indices=[1])

Behavior

  • The response covers every entry of query_times. A position no condition covers carries the unconditioned forecast, which is exact because the deployment draws positions independently.
  • Conditions on different columns may share a position, which is how one step combines a pin and a band. Conditions on the same column must cover disjoint positions.
  • A request must leave at least one column unconditioned at each covered position. Pinning every column is a log_prob question.
  • plausibility numbers are summed across covered positions. At each position, region_log_probability is conditional on that position's pinned values, so the two numbers add up to the plausibility of the whole condition set.
  • diagnostics.interval_estimator appears when some position bounds more than one column. Across several estimated positions it describes the one with the smallest effective sample size.

Docs and notebooks

README, docs/api-reference.md and notebooks/forecast_condition.ipynb now describe the per-position contract. The plausibility-chain wording from stefan/conditioning was dropped when that branch was merged into this one, and is restored here.

Verification

  • task pre-commit passes: ruff, ty, typos, and the full test suite with coverage.
  • The server's own condition parser (joint.model.service.conditions.parse_conditions) parses every v5 request fixture in tests/fixtures/, and each fixture's schema_version equals the server's DEFAULT_SCHEMA_VERSION.
  • Not yet done: a round trip against a live v5 deployment.

Note

High Risk
Breaking contract change (v4 → v5, removed ConditionBlock, new request/response conditioning semantics) affects all forecast and condition callers; misaligned deployments or unmigrated clients will fail compatibility checks or parse errors.

Overview
This PR moves the JointFM Python SDK to schema v5 and reshapes how conditional forecasts are expressed and returned.

Breaking API change: ConditionBlock is removed. condition= on forecast helpers now accepts one EqualityCondition or IntervalCondition, or a list of them. Each condition carries query_time_indices (indices into query_times; None means all horizons). The wire payload uses a top-level conditions array instead of condition: { query_time_index, conditions }. resolve_conditions is exported to normalize and validate lists (including rejecting overlapping conditions on the same column at the same position).

Behavior: Condition responses still include every query_times entry; horizons without a covering condition get the unconditional forecast. Validation and require_condition_support follow the new shape; plausibility and interval-estimator semantics are documented as multi-position (sums across independent positions).

Config/docs: Samples, README, API reference, and the conditional forecast notebook are updated for v5 and the new conditioning model. Tests and JSON fixtures reflect v5 schema and full-horizon condition responses.

Reviewed by Cursor Bugbot for commit 0c1df59. Configure here.

@shackmann
shackmann merged commit 8e43de5 into main Sep 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant