Skip to content

Stochastic authoring - #9

Merged
timbode merged 6 commits into
mainfrom
stochastic-authoring
Jul 29, 2026
Merged

Stochastic authoring#9
timbode merged 6 commits into
mainfrom
stochastic-authoring

Conversation

@timbode

@timbode timbode commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

timbode added 6 commits July 26, 2026 16:58
A stochastic model is an ordinary JuMP model with two decorations. A
random source is a JuMP variable marked with set_source — the importer
emits it as a named declaration and rewrites its uses into source
references, so it never becomes a decision variable, and using it twice
means the same random variable (JuMP's own by-reference semantics is
exactly the wire's identity-by-name). Scenario aggregators (smean,
scvar, sfreq_leq, sfreq_geq) are plain functions returning nonlinear
expressions with stochastic operator heads: JuMP stores them
symbolically and validates heads only when a solver evaluates them,
which this client never does — models ship as data and solve through
the service.

No new modeling surface, no second front door: JuMP algebra composes
the aggregators like any expression, and a model with no stochastic
marks emits byte-identical output (the existing goldens do not move).

The schema gains the stochastic layer (Program fields 9-11 + the
SourceRef expression case), regenerated into src/proto/; the operator
catalog mirror grows the aggregator heads plus max/min, which the
service now serves. Guardrails: bounds or domains on a source are
rejected (a source carries a distribution, not a domain), source names
must be unique and non-anonymous, scenario counts and seeds must be
positive (0 is reserved on the wire for the server default).

Version 0.3.0: new public API (set_source, set_scenarios, four
aggregators), documented under Modeling with a newsvendor quickstart.

Suite: 72 green.
The client pinned HTTP 2.5 while the service pins 1.11.0 — disjoint, so the two
could not co-resolve, and any environment holding both was unresolvable.

Widening the client is the only open direction. The service cannot move up:
Oxygen 1.10.2, its newest release, caps HTTP at "1.8.0 - 1", so the whole
Oxygen-based server stack is confined to the 1.x line until upstream moves.

The widening is safe rather than merely convenient. The client touches exactly
three HTTP symbols — request, header, URIs.escapeuri — none of which changed
across the major, and the suite is green on 1.11.0 including the transport test,
which runs a mock server over real requests. Nothing that resolves today stops
resolving: the bound only adds.

Patch bump, since relaxing a compat is additive.
The README and the docs home opened on "convert it to Quicopt's versioned wire
schema" and "serialize the Program to the versioned, language-neutral wire bytes
(proto3)". That describes the plumbing to someone who already knows it and tells
a reader deciding whether this package solves their problem nothing: it never
says a model goes out and an answer comes back, which is the whole product.

The vocabulary was never load-bearing. What travels is a Program — variables,
expressions and constraints as data, with a published protobuf schema — so the
docs say that once, where the reader meets it, and drop "wire X" as a modifier
everywhere else. The decode-equivalence note stays but now states the fact
(two encodings of one model can differ byte for byte and mean the same thing)
before naming the consequence. The API page called "Transport" becomes "Solving",
which is what a caller is doing on it.

The docs build was also failing, and had been: sfreq_geq carries sfreq_leq's
docstring but appeared in no @docs block, so checkdocs flagged it and its @ref
could not resolve, and KEY_PATH_ENV was cross-referenced from solve without
being on any page. Both are now included — KEY_PATH_ENV alongside
DEFAULT_BASE_URL, since "which server, and where the key is cached" is worth a
section of its own. `make.jl` runs clean; the suite is unchanged at 72 tests.
The repository moved from the personal account to the Quicopt
organization, so every badge, the Documenter canonical URL and the
deploydocs target were resolving against a namespace that no longer
serves this package.
The four authoring functions were the catalog's operator names surfaced
verbatim: `smean`, `scvar`, `sfreq_leq`, `sfreq_geq`, `set_source`. Those names
were never chosen for a reader — they were inherited from what travels on the
wire, and they leak the pipeline's vocabulary into the one surface a user
actually types.

`sfreq_leq` was the worst of them, and not merely for being ugly: its qualifier
binds to the wrong noun. In the only place it appears,

    @constraint(m, sfreq_leq(demand - stock, 0) >= 0.9)

there is a probability being bounded, so "freq leq" competes with the bound the
constraint already carries and the reader cannot rule out the wrong parse from
context. Making the relation an argument fixes it structurally rather than
cosmetically — `prob(demand - stock, ≤, 0)` can only attach the comparison to
the quantity:

    set_source      -> set_distribution
    smean           -> expectation
    scvar           -> cvar
    sfreq_leq/geq   -> prob(x, ≤/≥, τ)

`set_` also promised far less than it delivered: it does not adjust an
attribute the way `set_lower_bound` does, it changes the variable's kind, and a
variable that *has a distribution* is self-evidently not one the solver picks.

Since `≤` and `<=` are the same function object in Julia, one method per
relation accepts both spellings; a fallback method names the two valid relations
instead of leaving a bare MethodError. Only `≤` and `≥` have scenario
counterparts.

None of this reaches the wire. The heads emitted are still `:smean`, `:scvar`,
`:sfreq_leq`, `:sfreq_geq`, so the byte goldens are unchanged — the proof that
the rename is confined to the public surface. The internals keep the wire's
vocabulary (`_register_source!`, the `SourceRef` declarations) deliberately, and
the comment at the head of the layer records the split so it is not "tidied"
into agreement later.

No released version carried the old names: 0.3.1 is neither deployed nor
registered, so this needs no deprecation shim and no further version bump.
The layer shipped in 0.3.0 but was reachable only from the Modeling API page.
A reader landing on the README or the docs home saw three deterministic bullets
and a linear quickstart, so the layer was invisible unless they already knew to
go looking for it.

Both now carry the newsvendor, which is the right example precisely because its
answer is counter-intuitive: stock the mean and you are wrong, because running
out costs more than overstocking. That asymmetry is the argument for modelling
the uncertainty at all, and it lands in one line of output rather than a
paragraph of motivation. The chance constraint comes along to show `prob`'s
shape, since the relation-as-argument is unusual enough to be worth seeing
before the API reference explains it.

The snippets were run: `demand` imports to a named source declaration and drops
out of the decision set, leaving `stock` alone.

Also corrects `prob`'s docstring, which rendered the example constraint as "the
probability that demand exceeds stock in no scenario is at least 0.9" — wrong
twice over: the event is demand *not* exceeding stock, and the bound is on a
per-scenario probability rather than on a count of scenarios.
@timbode
timbode merged commit 191fc6b into main Jul 29, 2026
3 checks passed
@timbode
timbode deleted the stochastic-authoring branch July 29, 2026 08:33
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