Generic Constraints
Generic constraints are written in an authoring language: three input
files, one output, and a grammar (named @expressions, inline relational
right-hand sides, interval bounds). This page is the authority for that
language. For the per-file field tables, see
Constraint Files; for the LP
formulation these constraints desugar into, see
LP Formulation §10.
File set
Section titled “File set”| File | Role |
|---|---|
constraints/generic_constraints.json | Declares the constraints, each a linear expression over the variable catalog with an optional inline bound and a slack setting, and the named expressions they share; field table: Constraint Files. |
constraints/generic_constraint_bounds.parquet | The activation grid: one row per constraint, stage and optional block the constraint is active for, carrying the nullable lower and upper bound endpoints (the parquet base); field table: Constraint Files. |
constraints/generic_parameters.json | Declares the named scalar parameters that constraint expressions, inline bound endpoints and other named expressions reference by @name; field table: Constraint Files. |
generic_constraints/resolved_echo.parquet (output) | The fully-resolved, desugared form of every generic constraint the solver actually built — one row per (constraint, stage, block, term). Written whenever the study has generic constraints; the directory is entirely absent otherwise. |
Interval and shape derivation
Section titled “Interval and shape derivation”A bound’s shape is derived purely from which of bound_lower/bound_upper
are present, after the parquet base and any inline affine remainder (below)
have been folded together:
bound_lower | bound_upper | Derived shape |
|---|---|---|
| present | absent | floor |
| absent | present | cap |
| present | present, different | band |
| present | present, bit-identical | equality |
| absent | absent | rejected (see the activation grid) |
The equal-vs-different check is an exact bit-for-bit comparison of the two
values, never a tolerance — a very narrow but distinct band stays band, not
equality.
For example, one constraint with both endpoints on its bounds row derives the
band shape:
{ "constraints": [ { "id": 1, "name": "southeast_hydro_band", "expression": "hydro_generation(10) + hydro_generation(11)", "slack": { "enabled": true, "penalty": 5000.0 } } ]}| constraint_id | stage_id | block_id | bound_lower | bound_upper |
|---|---|---|---|---|
| 1 | 0 | null | 35.0 | 100.0 |
The two endpoints differ, so the row derives band.
The bounds file’s numeric base and the constraint’s inline affine remainder
compose additively on the same endpoint rather than conflicting:
resolved = base + remainder when both are present on that endpoint, or
either alone when only one is. The fold runs per (stage, block), since the
remainder may itself resolve a stage/block-varying named parameter (a
per_stage_block kind — see
generic_parameters.json
below).
A load-time inversion check (bound_upper < bound_lower) only fires when both
endpoints are statically resolvable — a constant-only remainder, or none
at all. A remainder carrying a live @param term makes the endpoint
stage-varying, so its inversion is left to LP infeasibility at solve time
rather than a combinatorial pre-solve check across every stage and block.
The activation grid
Section titled “The activation grid”generic_constraint_bounds.parquet is the activation grid — there is no
separate enable flag. A constraint is active at (stage, block) if and only
if a row exists for it there.
- A reference with no rows is rejected. A constraint whose only bound
endpoints come from an inline affine remainder, but which has zero rows in
the bounds parquet, fails referential validation:
InvalidReference, “declares a bound reference but has no activation rows in generic_constraint_bounds.parquet: the reference would apply to nothing.” The parquet still decides where the constraint applies, even when it supplies none of the bound value. A constraint with no inline affine remainder and no rows is not an error; it applies to nothing. - A both-null row is legal only when a reference fills a side. A row with
bound_lower = nullandbound_upper = nullis accepted when the constraint’s ownbound_lower_affine/bound_upper_affinesupplies at least one endpoint — the row’s only job is then to activate that cell; the value comes entirely from the remainder (as in the security-curve example). A both-null row on a constraint with no affine remainder on either side isInvalidValue: “has neither bound_lower nor bound_upper: at least one endpoint is required.” - A degenerate equal band is accepted, an inverted one is not.
bound_upper == bound_lowerderives toequalityand is fine;bound_upper < bound_lower(checked only when both are statically resolvable, per above) isInvalidValue: “an inverted interval is not allowed.” block_id = nullapplies to every block of the stage. When the constraint’s expression — and any block-varying coefficient parameter on it — is block-independent, this collapses to a single stage-level LP row priced by the stage’s total hours, rather than one row per block. Aper_stage_block-kind parameter anywhere in the constraint’s affine remainder suppresses that collapse even when the expression itself is block-independent, since one collapsed row would otherwise resolve one arbitrary block’s bound value and silently lose the per-block variation. Full row-materialization derivation: LP Formulation §10.
Expression grammar
Section titled “Expression grammar”An expression string follows the grammar below; whitespace between tokens is
ignored and every name is case-sensitive.
relation ::= side ( ( "<=" | ">=" | "==" ) side )?side ::= ( "+" | "-" )? term ( ( "+" | "-" ) term )*term ::= number "*" "@" name "*" variable | "@" name "*" variable | number "*" "@" name | "@" name | number "*" variable | number "*" group | group | variable | numbergroup ::= "(" ( "+" | "-" )? term ( ( "+" | "-" ) term )* ")"variable ::= var_name "(" integer ( "," integer )? ( "," "bus" "=" integer )? ")" | "line_exchange" "(" bus_pair ")"bus_pair ::= "source_bus" "=" integer "," "target_bus" "=" integer | "target_bus" "=" integer "," "source_bus" "=" integernumber ::= one or more digits with at most one ".", then an optional exponent ("e" or "E", an optional sign, digits)integer ::= a non-negative whole numbername ::= a letter or "_", then letters, digits or "_"The productions do not show these rules:
- A bare
numberterm is accepted only on a side of a relation, never inside agroupand never in an expression with no relational operator. - An expression holds at most one relational operator outside parentheses.
var_nameis one of the names in the variable catalog. Onlyhydro_turbinedandhydro_generationtakebus=;hydro_storage,hydro_withdrawalandanticipated_decisiontake no block argument; onlyline_exchangetakes abus_pair.- Write
@directly against its name (@name, never@ name). How an@nameterm reads (a parameter coefficient or a named-expression reference) is stated under The@namegrammar. - Give at most one positional block argument: a second one, equal or not, is rejected when the case loads
(Error Codes —
SchemaViolation). - A named-expression definition in
expressionstakes neither a relational operator nor a bare number.
Not supported
Section titled “Not supported”Each construct below is rejected when the case loads, reported as
SchemaViolation.
A sign on a later term. Write hydro_generation(0) - thermal_generation(0).
{ "constraints": [ { "id": 1, "name": "c1", "expression": "hydro_generation(0) + -thermal_generation(0)", "slack": { "enabled": false } } ]}Division. Write 0.5 * hydro_generation(0).
{ "constraints": [ { "id": 1, "name": "c1", "expression": "hydro_generation(0) / 2", "slack": { "enabled": false } } ]}A coefficient after the variable. Write 2 * hydro_generation(0).
{ "constraints": [ { "id": 1, "name": "c1", "expression": "hydro_generation(0) * 2", "slack": { "enabled": false } } ]}A bare < or >. Write <= or >=.
{ "constraints": [ { "id": 1, "name": "c1", "expression": "hydro_generation(0) < 30", "slack": { "enabled": false } } ]}A constant inside a group. Write
hydro_generation(0) <= 2 * thermal_generation(0) + 10.
{ "constraints": [ { "id": 1, "name": "c1", "expression": "hydro_generation(0) <= 2 * (thermal_generation(0) + 5)", "slack": { "enabled": false } } ]}Two relational operators. Give both endpoints in the bounds file, or write two constraints.
{ "constraints": [ { "id": 1, "name": "c1", "expression": "10 <= hydro_generation(0) <= 30", "slack": { "enabled": false } } ]}A name in another case. Write hydro_generation(0).
{ "constraints": [ { "id": 1, "name": "c1", "expression": "Hydro_generation(0)", "slack": { "enabled": false } } ]}A second positional block argument on one variable, equal or not.
{ "constraints": [ { "id": 1, "name": "c1", "expression": "thermal_generation(0, 0, 1) <= 10", "slack": { "enabled": false } } ]}Variable catalog
Section titled “Variable catalog”An expression can name any of the 26 variables below. The reference
grammar is var_name(entity_id[, block_id][, bus=bus_id]) — an optional
positional block argument, then an optional named bus= selector, in that
order (f(id), f(id, block), f(id, bus=b), f(id, block, bus=b) all
parse).
| Variable | Block arg | bus= | Notes |
|---|---|---|---|
hydro_storage(id) | No | No | Stage-level stock |
hydro_withdrawal(id) | No | No | Stage-level |
hydro_evaporation(id[, block]) | Yes | No | One stage-level quantity on a parallel stage, one per block on a chronological stage; some references are rejected on their block argument — see block references |
hydro_inflow(id[, block]) | Yes | No | The plant’s inflow in the block (hydro_inflow); a bound with no block_id expands to one row per block; a hydro_inflow term never collapses to a stage-level row, and a term with no block argument reads the block of its row |
hydro_storage_initial(id[, block]) | Yes (boundary) | No | With a block k, the storage at the start of block k; with no block, the stage’s initial storage |
hydro_storage_final(id[, block]) | Yes (boundary) | No | With a block k, the storage at the end of block k; with no block, the stage’s final storage (the hydro_storage value) |
hydro_useful_volume_initial(id[, block]) | Yes (boundary) | No | Same column and block semantics as hydro_storage_initial; stands for storage − reservoir.min_storage_hm3 (see below) |
hydro_useful_volume_final(id[, block]) | Yes (boundary) | No | Same column and block semantics as hydro_storage_final; stands for storage − reservoir.min_storage_hm3 (see below) |
hydro_turbined(id[, block][, bus=]) | Yes | Yes | bus= selects one (hydro, bus) cell; omitted sums over the plant’s cells |
hydro_spillage(id[, block]) | Yes | No | |
hydro_diversion(id[, block]) | Yes | No | |
hydro_outflow(id[, block]) | Yes | No | Derived alias for turbined + spillage, not an independent LP column |
hydro_generation(id[, block][, bus=]) | Yes | Yes | Same bus= semantics as hydro_turbined |
thermal_generation(id[, block]) | Yes | No | |
line_direct(id[, block]) | Yes | No | Forward flow only |
line_reverse(id[, block]) | Yes | No | Reverse flow only |
line_exchange(id[, block]) | Yes | No | Net flow (direct − reverse); also addressable by bus pair, below |
bus_deficit(id[, block]) | Yes | No | |
bus_excess(id[, block]) | Yes | No | |
pumping_flow(id[, block]) | Yes | No | |
pumping_power(id[, block]) | Yes | No | |
contract_import(id[, block]) | Yes | No | |
contract_export(id[, block]) | Yes | No | |
non_controllable_generation(id[, block]) | Yes | No | |
non_controllable_curtailment(id[, block]) | Yes | No | |
anticipated_decision(id) | No (rejected if given) | No | Stage-level scalar commitment column of a thermal that declares anticipated_config; no block index at all — use anticipated_decision(N) |
Only hydro_turbined and hydro_generation accept bus=; any other
variable rejects it as “does not accept a bus selector.” A (hydro, bus) cell
is the part of a plant on one bus: a plant has one cell per distinct bus among
its unit groups, and bus=
must name one of those buses.
A hydro_withdrawal, non_controllable_generation or
non_controllable_curtailment term is accepted when the case loads but adds no
coefficient to the LP row. A contract_import or contract_export term adds a
coefficient only when the named contract exists and has that direction, a
hydro_evaporation term only on a stage where the plant has an evaporation
model and is not PreFilling, and a hydro_generation term on a plant with an
FPHA model only on a stage where the plant is neither PreFilling nor Filling; in
these cases the term’s quantity is zero. The resolved echo
still lists every such term.
A constraint whose every term adds no coefficient leaves a row with no LP variable from its terms: without a slack the LP is infeasible when the bound excludes zero, and the row has no effect otherwise; with a slack, the slack absorbs the excluded amount at the constraint’s slack penalty. When only some terms add no coefficient, the bound applies to the remaining terms alone.
Useful-volume bound fold. A term c * hydro_useful_volume_initial(h) or
c * hydro_useful_volume_final(h) is built on plant h’s storage column, and
c × min_storage_hm3 of plant h — the physical minimum from
reservoir.min_storage_hm3, never a hydro_bounds per-stage override — is
added to every resolved bound endpoint of the constraint, once per such term
(c being the term’s fully resolved coefficient, so a @name coefficient
contributes its resolved value). A negative c lowers the endpoints by the
same rule. The constraint therefore reads as a quantity above dead storage while
the LP column keeps the absolute storage value; no extra LP variable is
created, and the folded endpoints are visible in
the resolved echo.
hydro_evaporation block references
Section titled “hydro_evaporation block references”With h a hydro id and K the number of blocks the stage declares, the
generic-constraint check rejects a hydro_evaporation reference that the
stage’s block_mode cannot
expose:
- On a parallel stage with two or more blocks,
hydro_evaporation(h)andhydro_evaporation(h, 0)address the one stage-level evaporation, and a block from 1 toK - 1is rejected. - On a chronological stage with more than one block, a block is required:
hydro_evaporation(h)is rejected. - On a stage with one block, on either stage mode,
hydro_evaporation(h)andhydro_evaporation(h, 0)both address the single block. - A block at or beyond
Kis rejected on every stage, for an evaporation or a storage reference. - The same check rejects a
hydro_storage_initial,hydro_storage_final,hydro_useful_volume_initialorhydro_useful_volume_finalreference that names an interior block boundary, one between two blocks, on a parallel stage.
hydro_evaporation(h) parallel: the stage-level evaporation; chronological with K > 1: rejectedhydro_evaporation(h, 1) parallel: rejected; chronological with K > 1: block 1The check runs only for a (constraint, stage) pair that has a row in
generic_constraint_bounds.parquet. Each rejection is a BusinessRuleViolation;
the messages are quoted in
Error Codes — Per-block generic-constraint references.
For any reference not covered above, the block argument is not checked when the
case loads: keep it below K on every stage where the constraint is active. A
block at or beyond K is accepted, and the term can then land on a
column outside that entity’s own blocks. novomodelo validate and novomodelo run stop with an
index-out-of-bounds panic when the block addresses past the last column of the
stage LP, and for a block at or beyond K on a hydro_inflow term of a plant
that receives water through an upstream travel_time_hours arc, directly or through an upstream PreFilling plant that has such an arc.
hydro_inflow
Section titled “hydro_inflow”A hydro_inflow term reads the plant’s inflow in the block, a rate in m³/s, made
of:
- the plant’s local inflow;
- the flows diverted into it;
- the upstream releases credited to the block by travel time;
- the share of the in-transit volume maturing at the stage that arrives in the block, as a rate;
- the water of upstream PreFilling plants passing through.
It excludes pumping, the inflow non-negativity slack, the plant’s own outflows, evaporation and withdrawal. On a parallel stage only the duration-weighted stage total matches the water balance; the split across blocks is a convention. For a plant that is PreFilling at the stage, the plant’s inflow enters the water-balance row of its nearest downstream plant that is not PreFilling, so the term equals no row’s own inflow.
Definition: LP Formulation — Hydro Inflow.
line_exchange(source_bus=, target_bus=)
Section titled “line_exchange(source_bus=, target_bus=)”Besides the direct-id form (line_exchange(id[, block])), line_exchange
alone also accepts an endpoint-bus-pair form in place of the positional
line id:
line_exchange(source_bus=3, target_bus=7)source_bus= and target_bus= may appear in either order, but both are
required, and this form takes no block argument — it always resolves to
block_id: None. Novomodelo resolves the (source_bus, target_bus) pair against a
line-topology index built from every declared line’s (source_bus_id, target_bus_id) pair and its reverse:
- If the pair matches a line’s declared direction, the term’s orientation
is forward (scale
+1.0). - If the pair is reversed relative to the line’s declared direction, the
orientation folds
-1.0into the term’s scale — so an author never needs to know which way a given line happened to be declared.
The pair form reads exactly the line that the direct-id form reads, with the
orientation sign applied. When two lines connect the same pair of buses, the
pair identifies no single line, and the case is rejected whenever it has a
constraints/generic_constraints.json file, even if no expression uses the
pair form (Authoring errors).
The @name grammar
Section titled “The @name grammar”Two authoring roles share one namespace: scalar parameters (declared in
constraints/generic_parameters.json) and named expressions (declared in the
constraints file’s own optional top-level expressions array and shared by
every constraint). Declaring the same name in both is rejected: “name … is
declared as both a scalar parameter and a
named expression; the “@name” namespace is shared.”
Disambiguation. @name immediately followed by * variable is a
scalar-parameter coefficient, resolved against generic_parameters.json. A
bare @name, or coefficient * @name with no trailing variable, is a
named-expression reference, resolved against expressions. @param * @name — stacking both roles in one term — is rejected outright: it has no
flat linear-core representation.
Composition. A named expression may reference another named expression; composition resolves transitively, with each reference’s scale distributed into every substituted term:
{ "expressions": [ { "name": "base", "expression": "hydro_generation(0)" }, { "name": "inner", "expression": "3.0 * @base" }, { "name": "outer", "expression": "@inner + hydro_generation(1)" } ], "constraints": [ { "id": 1, "name": "c0", "expression": "2.0 * @outer", "slack": { "enabled": false } } ]}2.0 * @outer inlines to 6.0 * hydro_generation(0) + 2.0 * hydro_generation(1).
Cycle detection. A reference cycle among named expressions, including an
expression that references itself, is rejected before any constraint is built,
even when no constraint uses those expressions; the message names the cycle
(a -> b -> a).
Term budget. Substitution caps at 100,000 materialized terms per
expansion — catching an exponential doubling chain (@e_k = @e_{k-1} + @e_{k-1}) fast. Declaring such an expression is not itself an error: every
reference must resolve when the expression is declared, regardless of its
size, and the term-budget cap only fires when something actually inlines it.
Grouping. A parenthesized group scaled by a leading literal coefficient
(2.0 * (@fnese - hydro_generation(2))) distributes that coefficient into
every inner term at parse time — the same flat term list the hand-expanded
form would produce, to arbitrary nesting depth. Only a literal coefficient
may scale a group; @param * (...) is rejected for the same reason as
@param * @name.
Inline relational RHS
Section titled “Inline relational RHS”An expression carries at most one relational operator (Expression grammar); for a range, give both endpoints in the bounds file or write two constraints.
- No operator:
expressionis the flat one-sided LHS; the interval comes entirely from the parquet base / affine-remainder mechanism described above. - One operator: every RHS variable term moves onto the merged LHS,
sign-flipped; a same-variable, same-coefficient-kind pair (across either
side) merges by summing its effective contribution, and a merged literal
column that cancels to exactly
0.0is dropped. Every non-variable term — a literal constant, or an@namethat resolves as a scalar parameter — folds into the affine remainder the operator assigns:<=to the upper endpoint,>=to the lower,==to both (an identical remainder on each side).
A bound-position @name resolves as a scalar parameter only when it is not
a declared named expression. When it is one, it still inlines into the
merged LHS as variable terms rather than becoming part of the remainder:
{ "expressions": [ { "name": "fnese", "expression": "hydro_generation(10) + hydro_generation(11)" } ], "constraints": [ { "id": 1, "name": "c0", "expression": "hydro_generation(0) >= @fnese", "slack": { "enabled": false } } ]}folds all three hydro_generation terms onto the merged LHS and assigns an
explicit zero-constant lower bound — an all-variable RHS still supplies an
affine bound, never “no bound.”
generic_parameters.json: the five parameter kinds
Section titled “generic_parameters.json: the five parameter kinds”The file’s top-level JSON key is "scalar_parameters" (an array). The field table lists
the field each kind fills.
kind | Value semantics |
|---|---|
constant | One value for every stage |
per_stage | Explicit value per stage; stage_ids must be a contiguous range from 0 |
seasonal | One value per season; stages inherit their season’s value; season_ids must be unique |
computed | Derived from hydro geometry/operational data at LP-build time — one of ten hydro-derived tags (e.g. equivalent_productivity); see Constraint Files |
per_stage_block | Explicit value per (stage_id, block_id) pair; each pair unique |
The vendored generic_parameters.schema.json
lists the five kinds above. For editor validation with the vendored schema, see
JSON Schemas.
per_stage_block is the mechanism that makes a generic-constraint bound (or
coefficient) genuinely block-varying — and, per
the activation grid above, referencing one in an
affine remainder suppresses the stage-level row collapse.
Two-sided slack
Section titled “Two-sided slack”Slack config (slack.enabled/slack.penalty) is declared once per
constraint and applies uniformly to every row the activation grid produces
for it. slack is required on every constraint; with enabled: true,
penalty is required and must be greater than zero
(Authoring errors). How many LP columns the slack adds is
a property of the row’s own resolved endpoint pair, never a
constraint-level label:
| Row shape | Slack columns |
|---|---|
| Disabled | Zero |
One-sided (floor/cap) | One (s⁺) |
Two-sided (band/equality) | Two (s⁺ then s⁻) |
A two-sided bound needs slack in both directions to relax either endpoint independently — one column cannot represent “went below the floor” and “went above the cap” at once.
Cost charges both. Both columns are priced at penalty * block_hours in
the objective (a stage-level row is priced by the stage’s total block hours; a
per-block row, by its own block’s hours) — violating either direction costs
the same per-unit penalty, and the LP can never let a +5 on one side and a
-5 on the other net to a free violation.
The reported value nets them. In the simulation violations output, a
one-sided row reports the raw (non-negative) slack primal. A two-sided row
instead reports the signed net s⁺ − s⁻ (which may be negative) —
deliberately different from the cost, which always sums s⁺ + s⁻. In
practice both slacks are rarely simultaneously nonzero at an LP vertex, so the
net is the more legible number for a results reader while the sum remains the
correct charge.
The resolved echo
Section titled “The resolved echo”generic_constraints/resolved_echo.parquet is written at the output root
(<out>/generic_constraints/) whenever the study declares any generic constraints; for a study with
none, the generic_constraints/ directory does not exist at all.
One row is emitted per resolved LHS term per active (constraint, stage, block) cell, in canonical (constraint, stage, block, term) order — declaration-order
invariant, so the same case always echoes the same rows in the same order. A
term-less (constant-only) constraint contributes a single placeholder row
whose per-term columns are all null.
| Name | Type | Nullable | Units | Description |
|---|---|---|---|---|
stage_id | Int32 | No | — | Study stage id |
block_id | Int32 | Yes | — | Null on a collapsed stage-level row |
constraint_id | Int32 | No | — | Generic constraint id |
constraint_name | Utf8 | No | — | Generic constraint name |
term_index | Int32 | Yes | — | Position in the resolved LHS; null on the term-less placeholder row |
variable_kind | Utf8 | Yes | — | Variable name (e.g. "thermal_generation"); null on the placeholder |
variable | Utf8 | Yes | — | Rendered variable label (e.g. "thermal_generation(id=3, block=all)"); null on the placeholder |
coefficient | Float64 | Yes | — | Fully resolved numeric coefficient (resolve(coefficient, stage, block) * scale); null on the placeholder |
bound_lower | Float64 | Yes | — | Lower interval endpoint after folding — including each useful-volume term’s coef × min_storage_hm3 shift; null when unbounded below |
bound_upper | Float64 | Yes | — | Upper interval endpoint after folding — including each useful-volume term’s coef × min_storage_hm3 shift; null when unbounded above |
derived_shape | Utf8 | No | — | The same floor/cap/band/equality label as above — never an authored sense |
slack_enabled | Boolean | No | — | Whether the constraint carries a slack term |
slack_penalty | Float64 | Yes | — | Slack penalty; null when slack is disabled |
The echo is the place to observe a folded bound: the input
constraints/generic_constraint_bounds.parquet shows the pre-fold value. For
example, a floor hydro_useful_volume_final(0) >= 20 on a plant whose
reservoir.min_storage_hm3 is 100 echoes bound_lower = 120.0
(20 + 1 × 100) with variable = "hydro_useful_volume_final(id=0, block=all)".
This is the fully desugared form: every named-expression reference is
already inlined into flat terms, every inline relational RHS has already
normalized into the bound_lower/bound_upper pair, and every @name
coefficient or bound has already resolved to its numeric value at that
(stage, block). It is exactly what the solver saw, apart from the terms that
add no coefficient (see the note under the variable catalog)
and a term with an out-of-range block
(block references) — the tool for
auditing an authored expression against the LP that was actually built.
Worked example: a security curve
Section titled “Worked example: a security curve”A security curve keeps plant h’s end-of-stage stored energy above a fraction
p of its maximum, stated in energy terms with the useful-range mean
productivity of
Hydro Production Function Models §5.3:
@rho_int * hydro_useful_volume_final(h) >= p * @emax.
constraints/generic_parameters.json declares the two computed parameters for
hydro 0:
{ "scalar_parameters": [ { "id": 1, "name": "rho_int", "kind": "computed", "computed_spec": { "tag": "integrated_accumulated_productivity", "hydro_id": 0 } }, { "id": 2, "name": "emax", "kind": "computed", "computed_spec": { "tag": "max_stored_energy", "hydro_id": 0 } } ]}constraints/generic_constraints.json states the curve for p = 0.3:
{ "constraints": [ { "id": 1, "name": "security_curve_h0", "expression": "@rho_int * hydro_useful_volume_final(0) >= 0.3 * @emax", "slack": { "enabled": true, "penalty": 5000.0 } } ]}The inline RHS carries the whole bound, but the constraint still needs
activation rows in constraints/generic_constraint_bounds.parquet — one
row per active stage, with both bounds null; without them the constraint is
never built (the activation grid):
| constraint_id | stage_id | block_id | bound_lower | bound_upper |
|---|---|---|---|---|
| 1 | 0 | null | null | null |
| 1 | 1 | null | null | null |
| … | … | … | … | … |
Why it cancels. integrated_accumulated_productivity and
max_stored_energy are evaluated by the same useful-range mean evaluator and
in the same raw unit, so @emax is exactly @rho_int times the useful
volume; hydro_useful_volume_final reads storage above the physical minimum,
the same range @emax spans. Pairing max_stored_energy with the
reference-point accumulated_productivity instead does not cancel and raises a
SemanticAmbiguity warning.
What the echo shows. On a plant with reservoir.min_storage_hm3 = 100,
reservoir.max_storage_hm3 = 1000, specific_productivity_mw_per_m3s_per_m = 0.0088
and a three-row volume-height-area geometry table (100, 500 and 1000 hm³),
the stage-0 resolved echo row reads coefficient = 2.8136,
bound_lower = 1041.02 and
variable = "hydro_useful_volume_final(id=0, block=all)": the RHS
0.3 × 2532.2 (0.3 × @emax, where @emax = 2.8136 × (1000 − 100)) plus the
useful-volume fold 2.8136 × 100 (@rho_int × min_storage_hm3).
Writing the same curve by hand on hydro_storage_final(h), with the dead-volume
energy @rho_int × min_storage_hm3 pre-folded into the bound (on the same plant,
bound_lower = 1041.02 on @rho_int * hydro_storage_final(0)) is equivalent.
Authoring errors
Section titled “Authoring errors”The failure modes below are the ones an author is most likely to hit while
writing this grammar; see Error Codes for the
exhaustive catalog and resolutions — it is not repeated here. The Kind column
names each failure’s kind; on an error line, novomodelo validate prints it in
brackets, as in error: [SchemaViolation] ….
| Situation | Kind |
|---|---|
An unknown variable name, including a catalog name written in another case (Hydro_generation(0)) | SchemaViolation |
An unknown @parameter, or any construct under Not supported | SchemaViolation |
bus= on a variable other than hydro_turbined and hydro_generation, or a block argument on hydro_storage, hydro_withdrawal or anticipated_decision, or a second positional block argument (hydro_turbined(5, 0, 1)) | SchemaViolation |
A duplicate constraint id, or a duplicate parameter id or name | SchemaViolation |
| A name declared both as a scalar parameter and as a named expression | SchemaViolation |
| A reference cycle among named expressions, a self-reference included | SchemaViolation |
A key the constraint entry does not declare (for example sense) | ParseError |
A constraint entry with no slack object | ParseError |
slack.enabled: true with no penalty, or with a penalty of zero or less | SchemaViolation |
A term naming an entity id the case does not declare (hydro_generation(7) with no hydro 7) | InvalidReference |
bus= naming a bus on which the plant has no unit group | InvalidReference |
anticipated_decision(N) where thermal N has no anticipated_config | BusinessRuleViolation |
line_exchange(source_bus=, target_bus=) naming two buses that no line connects | SchemaViolation |
Two lines connecting the same pair of buses, in either direction, in a case that has constraints/generic_constraints.json, whether or not an expression uses the bus-pair form | SchemaViolation |
| Constraint declares a bound reference but has no activation rows | InvalidReference |
| A bounds row with neither endpoint, and no affine remainder to fill one | InvalidValue |
Inverted interval (bound_upper < bound_lower, both statically resolvable) | InvalidValue |
A bounds row whose block_id is not a block of its stage | BusinessRuleViolation |
| An evaporation or storage reference naming a block that its stage does not have (block references) | BusinessRuleViolation |
The same bound column (bound_lower or bound_upper) set twice for one (constraint_id, stage_id, block_id) — a bound_lower row and a bound_upper row for the same triple are distinct keys and legal | DuplicateId |
A contract_import or contract_export term naming no declared contract | UnusedEntity — warning, the case still validates |
max_stored_energy(h) and accumulated_productivity(h) for the same hydro in one constraint (the matching coefficient is integrated_accumulated_productivity) | SemanticAmbiguity — warning, the case still validates |
See also
Section titled “See also”- LP Formulation §10 — the math formulation these constraints desugar into.
- Constraint Files — field tables for the three input files.
- Error Codes — the exhaustive
LoadError/ErrorKindcatalog. - JSON Schemas — vendored
generic_constraints.schema.jsonandgeneric_parameters.schema.jsonfor editor integration.