Declare a forecast as a vector of units - #63
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## jg/backends #63 +/- ##
===============================================
+ Coverage 98.53% 99.03% +0.49%
===============================================
Files 16 16
Lines 887 1136 +249
===============================================
+ Hits 874 1125 +251
+ Misses 13 11 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
joaquimg
force-pushed
the
jg/forecast-api
branch
2 times, most recently
from
August 23, 2026 15:25
3483591 to
cb49cc7
Compare
`PredictiveModel` took a vector of networks beside a vector of maps from input
columns to forecast variables, plus the sizes and the column names, as separate
arguments that had to agree. They could disagree, and two of the ways they could
were silent: a map longer than `networks` dropped its trailing entries, and a
duplicate-plus-gap left rows of the prediction reading uninitialized
`Zygote.Buffer` memory, surfacing as a cost computed from garbage rather than as
an error.
A forecast is now a vector of `ForecastModel` units, each one architecture and the
wiring around it:
set_forecast_model(model, [
ForecastModel(inputs = [:temp, :hour], architecture = Dense(2 => 1), outputs = [demand]),
ForecastModel(inputs = [:price], architecture = Dense(1 => 1), outputs = [spill]),
])
There is nothing left to keep aligned, so the checks that were impossible before
are now cheap: every declared variable is predicted, none twice, none foreign, the
architecture's output count matches its `outputs`, and the input schema is either
determined or refused. Each message names the offending unit or variable, and
`test/test_forecast_validation.jl` asserts the messages, since "errors gracefully"
is only testable if what the error says is part of the test.
Two units holding the same architecture object is an error rather than a silent
choice between "same shape" and "same weights" -- the two readings also disagreed
about parameter count between `extract_params` and `Optimisers.destructure`, so
one spelling of a tied model was untrainable.
The container it assembles is `FullForecastModel`, holding one `ResolvedUnit` per
unit with input columns and prediction rows resolved to integers once, at
construction. The prediction loop is now `Yhat[unit.rows, :] =
unit.architecture(X[unit.inputs, :])` -- previously each forward pass re-derived
the destination rows with `findfirst` over JuMP variables, inside the loop Zygote
traces, costing 35% of the allocations at 64 units. Resolving once also moved the
`deepcopy` off the `Functors` rebuild path, where it had been copying every
weight array on every optimiser step.
`outputs` says where a variable's realized values are in `Y`: `variable => :column`
for a named container, `variable => position` for a matrix, or nothing at all to
read the column carrying the variable's own name. The position spelling is the only
way to pin the layout of a matrix `Y`; without it the columns are read in the order
the `Forecast` variables were declared on the model, which nothing states and
nothing checks, so inserting a `@variable` above them silently changes what column
1 means.
The old name is removed outright rather than kept as a throwing stub. A stub
would name its two replacements, which reads better than an `UndefVarError` --
but it also keeps a removed type in the exported surface and in `@doc`, where
it would outlive everyone it was written for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
joaquimg
force-pushed
the
jg/forecast-api
branch
from
August 23, 2026 23:12
cb49cc7 to
0db5fe9
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PredictiveModeltook a vector of networks beside a vector of maps from inputcolumns to forecast variables, plus the sizes and the column names, as separate
arguments that had to agree. They could disagree, and two of the ways they could
were silent: a map longer than
networksdropped its trailing entries, and aduplicate-plus-gap left rows of the prediction reading uninitialized
Zygote.Buffermemory, surfacing as a cost computed from garbage rather than asan error.
A forecast is now a vector of
ForecastModelunits, each one architecture and thewiring around it:
There is nothing left to keep aligned, so the checks that were impossible before
are now cheap: every declared variable is predicted, none twice, none foreign, the
architecture's output count matches its
outputs, and the input schema is eitherdetermined or refused. Each message names the offending unit or variable, and
test/test_forecast_validation.jlasserts the messages, since "errors gracefully"is only testable if what the error says is part of the test.
Two units holding the same architecture object is an error rather than a silent
choice between "same shape" and "same weights" -- the two readings also disagreed
about parameter count between
extract_paramsandOptimisers.destructure, soone spelling of a tied model was untrainable.
The container it assembles is
FullForecastModel, holding oneResolvedUnitperunit with input columns and prediction rows resolved to integers once, at
construction. The prediction loop is now
Yhat[unit.rows, :] = unit.architecture(X[unit.inputs, :])-- previously each forward pass re-derivedthe destination rows with
findfirstover JuMP variables, inside the loop Zygotetraces, costing 35% of the allocations at 64 units. Resolving once also moved the
deepcopyoff theFunctorsrebuild path, where it had been copying everyweight array on every optimiser step.
outputssays where a variable's realized values are inY:variable => :columnfor a named container,
variable => positionfor a matrix, or nothing at all toread the column carrying the variable's own name. The position spelling is the only
way to pin the layout of a matrix
Y; without it the columns are read in the orderthe
Forecastvariables were declared on the model, which nothing states andnothing checks, so inserting a
@variableabove them silently changes what column1 means.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com