Skip to content

Error Codes

Case loading reports errors in two groups: validation report kinds (diagnostic categories collected while the case is validated) and load-failure kinds (returned when a case fails to load). The page also lists the policy-load errors a checkpoint refusal raises and the runtime errors of a running novomodelo run.

Novomodelo surfaces errors through four user-facing channels:

  • novomodelo validate human report — errors print as error: prefixed lines; warnings (after a successful validation) print as warning: prefixed lines.

  • novomodelo validate --json — on success, one JSON object on stdout: { configured, boundary_date, report }, plus policy_load: { mode, unused_stored_bases } when the run would load a policy (mode warm_start, resume or simulation_only), with no error key; configured is false when no policy.boundary is set. On failure, those three keys are null and an error key holds { phase, message }. The phase field is the name of the failing LoadError variant, one of the six preparation-phase kinds, or a policy-load kind (WarmStartIncompatible, ResumeIncompatible, or IoError for a checkpoint the OS refuses to read); it names a LoadError variant, a preparation kind or a policy-load kind, never the ErrorKind of a collected diagnostic. A case-load failure reports ConstraintError in almost every case, because every per-file read, parse and schema failure and every validation rule is collected into one report; its message holds one [<ErrorKind>] <file>[ (<entity>)]: <message> line per collected error, joined by newlines (the [ (<entity>)] part appears only when the error names an entity). Warnings are not in the JSON. Only a missing case directory prints no JSON object; it reports on stderr and exits 2.

    A case with no policy.boundary succeeds with:

    {
    "configured": false,
    "boundary_date": null,
    "report": null
    }

    A hydro unit group that names a bus that does not exist fails with:

    {
    "configured": null,
    "boundary_date": null,
    "report": null,
    "error": {
    "phase": "ConstraintError",
    "message": "[InvalidReference] system/hydros.json (Hydro 0 unit group 0): Hydro 0 unit group 0 references non-existent Bus 99 via field 'bus_id'"
    }
    }
  • novomodelo.io.validate(...) (Python) — returns { valid, errors: [{ kind, message }], warnings: [{ kind, message, file, entity }] }. errors holds at most one entry: none when valid is true, one on failure. Its kind is the same string as the CLI phase, and its message is the error’s full text, which for a case-load failure starts constraint violation: . For a missing case directory, where the CLI prints no JSON object, Python still returns an IoError entry. Each warning’s kind is the ErrorKind name.

  • novomodelo.errors Python exception classes — case loading raises ValidationError or CaseIoError; a policy-load refusal raises the class of its error: PolicyIncompatibleError for a refused checkpoint or one that is missing or cannot be decoded, CaseIoError for a checkpoint read the operating system refuses, and ValidationError for a missing policy directory or a refused boundary reconciliation (see Policy-load errors). See novomodelo.errors for the full hierarchy and Python Quickstart — Error Handling for catching these classes.

Policy-load failures use different exit codes and exception classes; see How a policy-load failure reaches you. For the cause and fix of the failures a study commonly meets, see Troubleshooting.


Find the kind named on your error line (the word in square brackets, or the phase value of novomodelo validate --json) in the first column; its row gives how the kind is reported, its severity, and the exit code novomodelo returns for it.

KindReported asSeverityExit code
FileNotFound[FileNotFound] report lineError1
ParseError[ParseError] report line; phase valueError1 as a report line; as a phase value, 4 (novomodelo validate) or 1 (novomodelo run)
SchemaViolation[SchemaViolation] report lineError1
InvalidReference[InvalidReference] report lineError1
DuplicateId[DuplicateId] report lineError1
InvalidValue[InvalidValue] report lineError1
CycleDetected[CycleDetected] report lineError1
DimensionMismatch[DimensionMismatch] report lineError1
BusinessRuleViolation[BusinessRuleViolation] report lineError1
WarmStartIncompatiblephase value (refused policy load); warning: lineError; Warning1 (2 under phase IoError); 0 for a warning
ResumeIncompatiblephase value (refused policy load); warning: lineError; Warning1 (2 under phase IoError); 0 for a warning
NotImplemented[NotImplemented] report lineError1
UnusedEntitywarning: lineWarning0
ModelQualitywarning: lineWarning0
SemanticAmbiguitywarning: lineWarning0
IoErrorphase valueError2, including an OS-refused policy read
SchemaErrorphase valueError1, including a policy.path refusal; 4 under novomodelo validate only for the post-report AR-coefficient count mismatch (novomodelo run: 1)
ConstraintErrorphase valueError1
ConfigValidationErrorphase valueError1
StochasticPreparationErrorphase valueError1, or 2 for a stochastic-input read such as a declared opening tree that is absent (both subcommands)
StudySetupErrorphase valueError1
HydroModelsPreparationErrorphase valueError1
GenericConstraintValidationErrorphase valueError1
BoundaryReconciliationErrorphase valueError1
PolicySoftwareMismatchpolicy-load refusalError1
Stored bases not usedwarning: lineWarning0

Runtime failures carry no kind; Runtime errors gives the exit code of each message. A solver-profile rejection carries phase StudySetupError, or BoundaryReconciliationError with policy.boundary configured; Solver profile validation states its exit code.


Each error a case load collects prints as one report line, [<Kind>] <file> (<entity>): <message>. The (<entity>) part appears only when the error names an entity, and <Kind> is one of the values below.

novomodelo validate prints each error as error: followed by its report line, and exits 1 when any error is present. novomodelo run prints error: constraint violation: followed by the same lines. After a successful validation, novomodelo validate prints each warning as a warning: line of the form <file> (<entity>): <message> after the Valid case: ... line, without the bracketed kind, and novomodelo.io.validate (Python) returns the warnings, each with its kind, in its warnings array. A warning of the policy load the run would apply prints after them as warning: <output dir>/<policy.path>: <message>, names no entity, and counts in the Validation: line.

The three warning kinds (UnusedEntity, ModelQuality, SemanticAmbiguity) never stop a run, and neither does a BusinessRuleViolation that novomodelo validate prints as a warning: line; every other report line is an error that must be resolved before the case loads.

WarmStartIncompatible and ResumeIncompatible are not report-line kinds: each names a refused policy load, as the phase value, and that load’s warnings, WarmStartIncompatible for a warm-start or simulation-only load and ResumeIncompatible for a resume load; see their entries below.

Severity: Error

When it occurs: A file that is required by the case structure is missing from the case directory. Emitted when the required files are checked, once for each of them that is not found on disk.

Example: A case directory without system/hydros.json:

[FileNotFound] system/hydros.json: required file 'system/hydros.json' not found in case directory

A case directory without system/buses.json gives two [FileNotFound] lines: the first reports the missing required file, the second the failed read of it:

[FileNotFound] system/buses.json: required file 'system/buses.json' not found in case directory
[FileNotFound] system/buses.json: No such file or directory (os error 2)

Resolution: Create the missing file in the correct subdirectory. The required files are: config.json, penalties.json, stages.json, initial_conditions.json, system/buses.json, system/lines.json, system/hydros.json, and system/thermals.json.


Severity: Error

When it occurs: A file exists and was read but could not be parsed — invalid JSON syntax, an unreadable Parquet header, a missing required field, or an unknown enum variant in a tagged JSON union. Emitted when a file is parsed and the parse fails. config.json and system/hydros.json report a missing field or an unknown variant as a SchemaViolation instead, and system/hydro_production_models.json does so for an unknown variant. A training.stopping_rules entry that carries a field that belongs to a different rule type — for example, seconds (a time_limit field) on a bound_stalling rule — is also rejected as a ParseError with an unknown field message.

Display format:

parse error in {path}: {message}

This format reaches you only for a parse error raised after the validation report is complete (see Load-failure kinds); a parse failure in the report prints as the [ParseError] line shown in the examples below.

Fields:

FieldDescription
pathPath to the file that failed to parse
messageHuman-readable description of the parse failure

Example:

[ParseError] stages.json: parse error: EOF while parsing a list at line 2 column 0

A missing required field, here a constraint entry without slack:

[ParseError] constraints/generic_constraints.json: parse error: missing field `slack` at line 1 column 69

A bound_stalling rule that carries seconds, a time_limit field:

[ParseError] config.json: parse error: unknown field `seconds`, expected `iterations` or `tolerance` at line 19 column 5

Resolution: Fix the syntax error in the indicated file. Use a JSON linter or Parquet viewer to find the exact location. For JSON files, common causes are trailing commas, missing quotation marks, or mismatched braces. For a missing field message, add the field it names. For an unknown field message inside a stopping_rules entry, remove the field that does not belong to that entry’s type, or move it to an entry whose type declares it.

See also: an anticipated_config object in system/thermals.json that matches neither accepted lead shape is collected under this kind — see Invalid anticipated-thermal lead.

Unknown field in a generic-constraint entry

Section titled “Unknown field in a generic-constraint entry”

When it occurs: Each generic-constraint object accepts exactly five fields (id, name, description, expression, slack); any other key is rejected as a ParseError with an unknown field message.

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
messageAn “unknown field” message, naming the offending key and the five accepted field names

Example: A constraint object with an extra type key:

[ParseError] constraints/generic_constraints.json: parse error: unknown field `type`, expected one of `id`, `name`, `description`, `expression`, `slack` at line 10 column 12

Resolution: Remove the unknown field, leaving only the five accepted keys. See Generic Constraints for the grammar.


Severity: Error

When it occurs: A file parses successfully but a field fails a schema constraint: a required field is missing from config.json or system/hydros.json, or a required Parquet column is missing (a missing field in any other JSON file is a ParseError), a value is outside its valid range (e.g., negative capacity, non-positive penalty cost, a reservoir whose min_storage_hm3 exceeds max_storage_hm3, or a stage whose num_openings is 0), a field contains an unexpected type, or an entity id appears twice in a registry file such as system/buses.json. Emitted when the fields of a parsed file are checked.

Example:

A negative penalty cost:

[SchemaViolation] system/buses.json: field buses[0].deficit_segments[0].cost: penalty value must be > 0.0, got -100

A reservoir whose minimum storage exceeds its maximum:

[SchemaViolation] system/hydros.json: field hydros[0].reservoir: min_storage_hm3 (800) must be <= max_storage_hm3 (500)

A stage with no openings:

[SchemaViolation] stages.json: field stages[0].num_openings: num_openings must be > 0

A config.json without training.selection:

[SchemaViolation] config.json: field training.selection: a forward-pass count is required via training.selection

Two buses that share an id in system/buses.json:

[SchemaViolation] system/buses.json: field buses[1].id: duplicate id 0 in buses array

Resolution: Correct the value in the indicated field. Field paths use dot-notation and zero-based array indices. Consult the Case Format page for valid ranges and required fields. For storage bounds, ensure min <= max. For opening counts, ensure num_openings >= 1. For config.json, training.selection (for example { "method": "sampled", "forward_passes": 192 }) and training.stopping_rules are mandatory keys with no default when absent. An absent training.selection is reported with the message a forward-pass count is required via training.selection; add the key to config.json.

See also: a constraints/hydro_bounds.parquet spillage-band inversion or non-negativity rejection is collected under this kind during a full case load — see Spillage-band inversion. Two of the four failures of Invalid anticipated-thermal lead print [SchemaViolation] lines; the other two print [ParseError] and [BusinessRuleViolation] lines. A case load prints every schema failure as a [SchemaViolation] line; the schema error in <path>, field <field>: <message> form is the display format of SchemaError, which reaches you only for a load error raised after the report is complete.

config.json loading refuses these values; each prints a [SchemaViolation] config.json: field <field>: <message> line, and both commands exit 1.

RuleFieldMessage
No iteration_limit ruletraining.stopping_rulesmust contain an iteration_limit rule
interval_iterations absent or below 1, checkpointing enabledpolicy.checkpointing.interval_iterationsmust be at least 1 when policy.checkpointing.enabled is true; got {found}
policy.path naming no subdirectory ("", ., ..)policy.path"{value}" names the output directory or one of its ancestors, which a checkpoint write would replace; choose another directory, such as "./policy"

{found} is no value when the key is absent and 0 otherwise. {value} is the configured policy.path.

When it occurs: policy.path is resolved against --output, and a link is checked where it sits and where it points. The path is refused when it names the output directory or one of its ancestors, or names, contains or lies inside a directory a run rewrites or removes. Configuration — policy states the rule. In a case that trains, the path is refused too when it (for a link, its target) or its .staging or .previous sibling is not a directory or holds an entry no checkpoint writer leaves there; the message is refusing to write a checkpoint: …, quoted under Policy Management — Checkpoint Directory Contents.

"{value}" names the output directory or one of its ancestors, which a checkpoint write would replace; choose another directory, such as "./policy"
"{value}" {relation} {dir}, which a run removes whole before writing its outputs; choose a directory that neither contains nor lies inside it, such as "./policy"
"{value}" {relation} {dir}, which holds files a run writes and a checkpoint write would replace; choose a directory that neither names nor contains it, such as "./policy"

{relation} is names, contains or lies inside, and {dir} is the output-relative directory, such as simulation/hydros. For a link, {value} reads {path} -> {target}. novomodelo validate reports phase SchemaError (exit 1), or IoError (exit 2) when the path cannot be inspected; novomodelo run prints error: schema error in {path}, field policy.path: {message} and exits 1.

Overlapping seasons in one resolution level

Section titled “Overlapping seasons in one resolution level”

When it occurs: Seasons whose spans differ by at most 7 days form one resolution level, and two seasons of one level may not share a calendar day. The load refuses an overlapping pair of season_definitions with [SchemaViolation] stages.json: field season_definitions.seasons: <message>, where <message> is:

seasons {a} and {b} overlap within one resolution level; seasons whose spans differ by at most 7 days form one level and must not share a calendar day

{a} and {b} are the ids of the two seasons. Stage Files — stages.json describes season_definitions.

Namespace collision between a scalar parameter and a named expression

Section titled “Namespace collision between a scalar parameter and a named expression”

When it occurs: constraints/generic_constraints.json’s expressions array declares named linear expressions that share the @name reference namespace with constraints/generic_parameters.json’s scalar parameters. A named-expression entry whose name matches an already-loaded scalar parameter’s name is rejected — a @name token must resolve unambiguously to exactly one kind of thing.

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
fieldexpressions[i].name — the colliding entry’s index
messagename "<name>" is declared as both a scalar parameter and a named expression; the "@name" namespace is shared

Example:

[SchemaViolation] constraints/generic_constraints.json: field expressions[0].name: name "demanda" is declared as both a scalar parameter and a named expression; the "@name" namespace is shared

Resolution: Rename the named expression (or the scalar parameter) so the two @name tables do not overlap. See Generic Constraints for the @name grammar shared between the two files.

When it occurs: expressions[] entries reference each other by @name; a cycle in that reference graph (including a length-1 self-reference) is rejected before any expression is inlined, even if no constraint ever uses it.

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
fieldAlways expressions
messagenamed-expression reference cycle detected: <a> -> <b> -> ... -> <a>

Example:

[SchemaViolation] constraints/generic_constraints.json: field expressions: named-expression reference cycle detected: fnese -> fnese_margin -> fnese

Resolution: Break the cycle — rewrite one of the named expressions in the reported chain so it no longer refers back to an ancestor. See Generic Constraints for the @name composition rules.

Named-expression inlining term budget exceeded

Section titled “Named-expression inlining term budget exceeded”

When it occurs: Inlining substitutes every @name reference with its referenced expression’s terms. An acyclic but exponentially-expanding declaration — e.g. a doubling chain @e_k = @e_{k-1} + @e_{k-1} — is caught once the flattened term count would exceed a hard cap of 100,000 terms, far above any legitimately authored expression, aborting the blow-up before the full vector materializes. The cap applies only when a constraint (or another expression) actually references the runaway declaration — declaring it without referencing it is a cheap existence check, not an expansion.

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
fieldconstraints[i].expression (or the referencing expression’s own field)
messagenamed-expression reference "@<name>" expands to more than 100000 inlined terms; this indicates an exponential reference pattern (e.g. a doubling chain "@e = @prev + @prev")

Example:

[SchemaViolation] constraints/generic_constraints.json: field constraints[0].expression: named-expression reference "@e60" expands to more than 100000 inlined terms; this indicates an exponential reference pattern (e.g. a doubling chain "@e = @prev + @prev")

Resolution: Rewrite the named-expression chain to grow linearly rather than by repeated self-addition — e.g. accumulate into one running expression instead of doubling a reference at each step. See Generic Constraints for the @name composition rules.

When it occurs: The expression grammar accepts at most one @-prefixed reference per term, and a parenthesized group takes only a literal coefficient. Four distinct authoring mistakes are rejected at the same parse site (constraints[i].expression, or expressions[i].expression for a named expression’s own definition):

MistakeExample message
Two @ references in one term (@param * @name, or two @params)only one @parameter reference is allowed per term; found "@a" and "@b"
@param scaling a parenthesized group (@param * (...))parameter "@a" cannot scale a parenthesized group: a group takes only a literal coefficient, not "@a * (...)"
@name naming no loaded scalar parameter, in a coefficient positionunknown parameter "@a": no definition with this name was loaded
@name naming no declared expression, in a reference positionundeclared named-expression reference "@a": no expression with this name was declared

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
fieldconstraints[i].expression or expressions[i].expression
messageOne of the four messages in the table above

Resolution: A term carries at most one @ reference, and only a literal number may scale a (...) group. Check that a bound @name matches a parameter actually declared in constraints/generic_parameters.json, and that a reference @name matches an entry actually declared in this file’s own expressions array. See Generic Constraints for the full grammar.

When it occurs: A variable takes at most one positional block argument. A second one, equal or not, is rejected when the expression is parsed, for example hydro_turbined(5, 0, 1).

Fields:

FieldDescription
pathAlways constraints/generic_constraints.json
fieldconstraints[<i>].expression or expressions[<i>].expression
messagerepeated block argument in variable "{name}", where {name} is the variable, such as hydro_turbined

Resolution: Give the variable one block argument. See Generic Constraints — Expression grammar for the full grammar.

When it occurs: A Parquet input that lacks a required column, under whatever other name the file carries it, is a schema failure naming the required column.

Fields:

FieldDescription
pathThe Parquet file
fieldThe required column name
messagemissing required column "<name>"

Example: A scenarios/inflow_seasonal_stats.parquet whose hydro_id column is named hydro:

[SchemaViolation] scenarios/inflow_seasonal_stats.parquet: field hydro_id: missing required column "hydro_id"

Resolution: Rename the column to the name the file requires, keeping its data unchanged. The column tables of Case Format list the required columns of every file.

When it occurs: A training.scenario_source or simulation.scenario_source block of config.json breaks an admission rule. The rules are checked when config.json loads, so novomodelo validate and novomodelo run both exit 1. In the table, {section} stands for training or simulation, whichever block broke the rule, and {from} and {to} stand for the first and last year of the range as the case declares them. The printed message shows those values in place of the placeholders.

RuleFieldMessage
openings under simulationsimulation.scenario_source.openingsopenings is only valid under training.scenario_source, not simulation.scenario_source
historical_years without a historical class{section}.scenario_source.historical_yearshistorical_years is specified but no class uses the 'historical' scheme
historical on load or ncs{section}.scenario_source.load.scheme or {section}.scenario_source.ncs.schemehistorical scheme is only valid for the inflow class
Missing seed{section}.scenario_source.seedseed is required when any class uses out_of_sample or external scheme
Inverted historical_years range{section}.scenario_source.historical_yearsrange 'from' ({from}) must be <= 'to' ({to})

Example: novomodelo validate on a case whose training.scenario_source sets the inflow class to out_of_sample and no seed:

[SchemaViolation] config.json: field training.scenario_source.seed: seed is required when any class uses out_of_sample or external scheme

novomodelo validate prints this line after an error: prefix. novomodelo run prints it after error: constraint violation: .

Resolution: Configuration lists the scenario_source keys. Find the message in the table above, then fix that rule:

  • openings: declare it under training.scenario_source only.
  • historical_years: remove it, or set the inflow class to the historical scheme.
  • load or ncs: use another scheme; only the inflow class takes historical.
  • seed: add it to the block.
  • Inverted historical_years range: set from no later than to.

A declared opening-tree file that is missing is reported in the stochastic phase: Opening-tree file missing.

When it occurs: A thermal’s anticipated_config in system/thermals.json takes exactly one of two keys: lead_stages (an integer of at least 1) or lead_time_hours (a finite number above 0). A lead longer than the whole study horizon must also reach a declared post-study stage. Four failures each add one line to the validation report:

  • lead_stages is 0.
  • lead_time_hours is zero or negative.
  • The object matches neither accepted shape, for example because it has both keys, has neither key, or has a negative lead_stages.
  • The lead is longer than the whole study horizon and reaches no declared post-study stage.

Messages: novomodelo validate prints one error: line for each failure. The first three failures stop system/thermals.json at its first offending entry, so a file with several bad leads reports only the first; the horizon rule reports every offending thermal. The captures follow the order of the list above.

error: [SchemaViolation] system/thermals.json: field thermals[0].anticipated_config.lead_stages: lead_stages must be >= 1, got 0
error: [SchemaViolation] system/thermals.json: field thermals[0].anticipated_config.lead_time_hours: lead_time_hours must be finite and > 0.0, got -1
error: [ParseError] system/thermals.json: parse error: data did not match any variant of untagged enum RawAnticipatedConfig at line 18 column 5
error: [BusinessRuleViolation] system/thermals.json (thermals[id=0].anticipated_config.lead_stages): Thermal 0: lead_stages exceeds study horizon (lead_stages=9, n_stages=4); the plant can never deliver within the study horizon

A lead_time_hours lead that fails the horizon rule produces this message instead of the lead_stages one above:

Thermal {thermal_id}: lead_time exceeds study horizon (lead_time={delta_hours}, total_horizon_hours={total_horizon_hours}); the plant can never deliver within the study horizon

{thermal_id} is the thermal’s id, {delta_hours} is its lead in hours, and {total_horizon_hours} is the length of the whole study horizon in hours. In the SchemaViolation lines, the index in thermals[0] is the entry’s position in the thermals array, counting from 0; the horizon message names the thermal id instead, as in thermals[id=0].

Resolution: Keep one of the two keys: give lead_stages an integer of at least 1, or lead_time_hours a number above 0. For a lead longer than the study horizon, shorten the lead, or declare, in post_study_stages.json, a post-study stage that the lead reaches. See System Element Modeling Overview §4 for the anticipated-thermal model and system/thermals.json for the file’s fields.

When it occurs: An inverted spillage band (min_spillage_m3s > max_spillage_m3s) or a negative min_diversion_m3s, min_spillage_m3s, or max_spillage_m3s in constraints/hydro_bounds.parquet is reported here (tagged [SchemaViolation]) when a full case is loaded, and surfaces as a standalone SchemaError when that file is checked on its own.

Fields:

FieldDescription
descriptionContains a [SchemaViolation] constraints/hydro_bounds.parquet: field hydro_bounds[<i>].<column>: <message> line — see below

Example:

[SchemaViolation] constraints/hydro_bounds.parquet: field hydro_bounds[0].max_spillage_m3s: max_spillage_m3s (5) must be >= min_spillage_m3s (10)
[SchemaViolation] constraints/hydro_bounds.parquet: field hydro_bounds[0].min_diversion_m3s: value must be >= 0.0, got -5

Resolution: Correct the offending row so min_spillage_m3s <= max_spillage_m3s, and so min_diversion_m3s, min_spillage_m3s, and max_spillage_m3s are all non-negative. See Case Format for hydro_bounds.parquet’s column reference.


Severity: Error

When it occurs: A cross-entity foreign-key reference points to an entity that does not exist in the expected registry. For example, one of a hydro plant’s unit groups’ bus_id references a bus that is not in system/buses.json. Emitted when the cross-entity references are resolved.

Example:

[InvalidReference] system/hydros.json (Hydro 0 unit group 0): Hydro 0 unit group 0 references non-existent Bus 99 via field 'bus_id'

Resolution: Either add the referenced entity to its registry file, or correct the ID in the referencing file. Check all ID references: unit_groups[].bus_id, thermals.bus_id, lines.source_bus_id, lines.target_bus_id, hydros.downstream_id.

See also: a constraints/ncs_bounds.parquet row with an undeclared stage_id is reported under this kind — see Undeclared stage_id in bound-override parquet.

Generic-constraint bound reference with no activation rows

Section titled “Generic-constraint bound reference with no activation rows”

When it occurs: constraints/generic_constraint_bounds.parquet is the activation grid for generic constraints — a constraint applies at (stage, block) if and only if a row exists for it there, independent of whether the row supplies a numeric endpoint or the endpoint comes entirely from the constraint’s own inline affine remainder (an inline <= @demanda, say). A constraint whose only endpoints come from that affine remainder, but which has zero rows in the bounds parquet, is collected as InvalidReference — the reference would otherwise apply to nothing and be silently inert.

Fields:

FieldDescription
descriptionContains a [InvalidReference] constraints/generic_constraints.json (GenericConstraint <id>): GenericConstraint <id> declares a bound reference but has no activation rows in generic_constraint_bounds.parquet: the reference would apply to nothing line

Example:

constraint violation: [InvalidReference] constraints/generic_constraints.json (GenericConstraint 3): GenericConstraint 3 declares a bound reference but has no activation rows in generic_constraint_bounds.parquet: the reference would apply to nothing

Resolution: Add at least one row for the constraint’s id to generic_constraint_bounds.parquet at every (stage, block) where it should apply — the row may leave both bound_lower/bound_upper null as long as the constraint’s own affine remainder supplies the value; the row’s job is then only to activate the cell. See Generic Constraints — the activation grid for the full model.


Severity: Error

When it occurs: An id or key is declared twice where it must be unique. Three cases emit this kind: a unit-group id declared twice in one hydro plant (unit-group ids are unique within a plant, not globally); a policy-graph node id declared twice in policy_graph.nodes[] of stages.json; and two rows of a constraints/*_bounds.parquet override file that set the same column for the same entity, stage and block. An entity id declared twice in a registry file, such as two buses with the same id in system/buses.json, is a SchemaViolation, not a DuplicateId; see the two-buses example there.

Example:

[DuplicateId] system/hydros.json (Hydro 0): Hydro 0: unit group id 0 is declared more than once; unit group ids must be unique within a plant

Resolution: Rename the repeated unit-group or node id, or remove or merge the bound-override rows that set the same column twice.


Severity: Error

When it occurs: A value violates a domain rule that novomodelo validate checks once the files have loaded. Examples: the outgoing transition probabilities from a stage in stages.json do not sum to 1.0, or a study stage has an empty blocks array. A reservoir whose min_storage_hm3 exceeds max_storage_hm3, and a stage whose num_openings is 0, are reported as SchemaViolation lines instead.

Example:

[InvalidValue] stages.json: outgoing transition probabilities from stage 0 sum to 0.50000000 (expected 1.0 ±0.000001); probability must sum to 1.0
[InvalidValue] stages.json: stage 1 declares no blocks; every study stage must declare at least one block

Resolution: Correct the value named in the message. Consult the Case Format page for documented constraints. For transitions, make the outgoing probabilities from each stage sum to 1.0. For stages, declare at least one block in every study stage’s blocks array.

See also: a min_diversion_m3s floor on a hydro with no declared diversion channel is reported under this kind — see Diversion floor without a declared channel.

Generic-constraint bounds row with neither endpoint and no affine remainder

Section titled “Generic-constraint bounds row with neither endpoint and no affine remainder”

When it occurs: A generic_constraint_bounds.parquet row with bound_lower = null and bound_upper = null is legal only when the constraint’s own bound_lower_affine/bound_upper_affine supplies at least one endpoint (the row then only activates the cell, per above). A both-null row on a constraint with no affine remainder on either side has no endpoint at all and is collected as InvalidValue.

Fields:

FieldDescription
descriptionContains a [InvalidValue] constraints/generic_constraint_bounds.parquet (GenericConstraintBoundsRow[<i>]): GenericConstraintBoundsRow[<i>] on constraint <id> has neither bound_lower nor bound_upper: at least one endpoint is required line

Example:

constraint violation: [InvalidValue] constraints/generic_constraint_bounds.parquet (GenericConstraintBoundsRow[4]): GenericConstraintBoundsRow[4] on constraint 3 has neither bound_lower nor bound_upper: at least one endpoint is required

Resolution: Either supply bound_lower and/or bound_upper on the row, or add an inline relational operator (<=, >=, ==) to the constraint’s expression so an affine remainder fills the missing endpoint. See Generic Constraints — the activation grid.

Diversion floor without a declared channel

Section titled “Diversion floor without a declared channel”

When it occurs: constraints/hydro_bounds.parquet may override a hydro’s min_diversion_m3s floor per (hydro, stage). When the hydro declares no diversion channel, its diversion flow is fixed at [0, 0] — a positive floor is then the infeasible interval [min > 0, 0]. The business-rule check rejects the override before it ever reaches the LP.

Fields:

FieldDescription
descriptionContains a [InvalidValue] constraints/hydro_bounds.parquet (Hydro <id>): Hydro <id>: hydro_bounds row at stage_id=<stage> sets min_diversion_m3s=<value>, but the hydro declares no diversion channel; diversion is pinned [0, 0] with no channel, making a positive floor infeasible line

Example:

[InvalidValue] constraints/hydro_bounds.parquet (Hydro 0): Hydro 0: hydro_bounds row at stage_id=1 sets min_diversion_m3s=2, but the hydro declares no diversion channel; diversion is pinned [0, 0] with no channel, making a positive floor infeasible

Resolution: Either declare a diversion channel on the hydro in system/hydros.json (see System Element Modeling Overview), or remove the min_diversion_m3s override for that (hydro, stage) in constraints/hydro_bounds.parquet.


Severity: Error

When it occurs: A directed graph that must be acyclic contains a cycle. Two graphs are checked. The hydro cascade: the downstream_id links among hydro plants must form a directed forest (no cycles), so a cycle would mean plant A drains into plant B which drains back into plant A. The policy graph: the policy_graph.nodes[] of stages.json, linked by transitions[], must be acyclic; this check applies only when nodes[] is declared.

Messages:

[CycleDetected] system/hydros.json: hydro cascade contains a cycle involving hydro IDs: [0, 1]

The policy-graph nodes message:

policy-graph nodes contain a cycle; the node graph must be acyclic

novomodelo validate prints it as [CycleDetected] stages.json: <message>, where <message> is the text above.

Resolution: Review the downstream_id chain for the listed plants and remove the cycle. Every hydro cascade must be a directed tree rooted at plants with no downstream (tailwater discharge). For the policy graph, remove the transitions[] entry that closes the cycle.


Severity: Error

When it occurs: A cross-file coverage check fails. For example, when scenarios/inflow_seasonal_stats.parquet is present, every hydro needs a row for each study stage in which it is active, and each missing (hydro, stage) pair is one error that names the hydro and the stage. A mismatch means an optional per-entity file provides data for some entities but not all that require it.

Example:

[DimensionMismatch] scenarios/inflow_seasonal_stats.parquet (Hydro 1): Hydro 1 missing inflow seasonal stats for stage 0

Resolution: Add the missing rows to the Parquet file; the message names the hydro and the stage that lack one. When that file is present, every hydro needs a row for each study stage in which it is active.


Severity: Error

When it occurs: A domain-specific business rule is violated that cannot be expressed as a simple range constraint. Examples: a negative turbined_cost on a hydro whose generation model is fpha, a filling schedule that cannot reach the dead volume before the hydro enters operation, the anticipated-thermal and post-study-boundary rules, a bound-override row that names an undeclared stage_id, a pumping station in service at a stage where its source or destination hydro is not Operating, or a per-block generic-constraint reference that the stage’s blocks cannot satisfy. One check reports a BusinessRuleViolation as a warning: when the inflow model is estimated from scenarios/inflow_history.parquet and a hydro has more than one observation in one season of a year, novomodelo validate prints a warning: line (exit 0) saying the observations are aggregated to season resolution.

Example:

error: [BusinessRuleViolation] penalties.json (Hydro 0): Hydro 0: turbined_cost (-0.05) must be non-negative (>= 0) for FPHA hydros; negative values distort LP dispatch

Resolution: Read the message carefully — it describes the specific rule that was violated and which entities are involved. For the two penalty rules, see Load-time penalty checks.

See also: the entries below hold the anticipated-thermal and post-study boundary rules (Commitment on a non-anticipated thermal, Missing commitment window on an anticipated thermal, Post-study boundary unanchored or malformed and Uncovered or over-covered past_anticipated_commitments window), the undeclared-stage_id bound-override rule (Undeclared stage_id in bound-override parquet) and the per-block generic-constraint references (Per-block generic-constraint references). The pumping-station rule is under Pumping station outside its endpoints’ Operating windows. The horizon rule of Invalid anticipated-thermal lead also prints a [BusinessRuleViolation] line; that entry sits under SchemaViolation, where two of its four lines print.

Deterministic external inflow column under an autoregressive model

Section titled “Deterministic external inflow column under an autoregressive model”

When it occurs: Under the external inflow sampling scheme, an inflow’s seasonal mean and standard deviation are replaced by the sample moments of its external scenario file only for a hydro of autoregressive order 0 with no annual component; a hydro whose files declare a lag coefficient or an annual component keeps the statistics its own files declare. The substitution is per class: a load or non-controllable-source class takes its moments from its external file only when that class is itself external.

A constant (σ = 0) column is accepted for load, NCS, and a hydro of order 0 with no annual component, but rejected for a hydro whose files declare an autoregressive order above 0 or an annual component: a single deterministic value cannot stand in for such a model, since it would have to equal that model’s own stage-by-stage deterministic PAR output, which the loader does not reconstruct from a flat column.

Fields:

FieldDescription
descriptionContains a [BusinessRuleViolation] <external inflow file> (inflow entity <e> stage <stage_id>): <message> line

Example:

constraint violation: [BusinessRuleViolation] scenarios/external_inflow_scenarios.parquet (inflow entity 7 stage 3): inflow external library at stage 3, entity 7: every scenario value is constant (σ = 0), but this hydro's inflow follows an autoregressive model of order > 0; a deterministic value here would have to equal that model's own deterministic PAR output at every stage, which this loader does not compute upstream

Resolution: Give the external column genuine stage-to-stage variation, or drop the plant’s autoregressive inflow model to order 0 (no lag coefficient, no annual component) if its inflow really is deterministic. See Scenario Generation §4.4 for the accept/reject rule and scenario_source for the scheme configuration.

Scalar parameters outside the constraints directory

Section titled “Scalar parameters outside the constraints directory”

When it occurs: Scalar parameters are read from constraints/generic_parameters.json only, beside the generic constraints that reference its @name parameters. A file at system/scalar_parameters.json is rejected whatever its content; the loader never reads it, so a well-formed file at that path is still refused.

Fields:

FieldDescription
descriptionContains a [BusinessRuleViolation] system/scalar_parameters.json: system/scalar_parameters.json is no longer read; scalar parameters are now read from constraints/generic_parameters.json, beside the constraints that reference them by @name. Move the file (its contents are unchanged). line

Example:

[BusinessRuleViolation] system/scalar_parameters.json: system/scalar_parameters.json is no longer read; scalar parameters are now read from constraints/generic_parameters.json, beside the constraints that reference them by @name. Move the file (its contents are unchanged).

Resolution: mkdir -p <case>/constraints && git mv <case>/system/scalar_parameters.json <case>/constraints/generic_parameters.json — the contents do not change. See Generic Constraints — the five parameter kinds for the current file’s schema.

When it occurs: initial_conditions.json’s past_anticipated_commitments entries declare an externally-decided commitment for an anticipated thermal, keyed by thermal_id. Case validation rejects any entry whose thermal_id names no thermal with an anticipated_config — either the thermal does not exist, or it exists but is not configured as anticipated.

Fields:

FieldDescription
descriptionContains a [BusinessRuleViolation] initial_conditions.json (initial_conditions.past_anticipated_commitments[thermal_id=<id>]): Thermal <id>: referenced in past_anticipated_commitments but is not an anticipated thermal (anticipated_config is None or thermal does not exist) line

Example:

constraint violation: [BusinessRuleViolation] initial_conditions.json (initial_conditions.past_anticipated_commitments[thermal_id=12]): Thermal 12: referenced in past_anticipated_commitments but is not an anticipated thermal (anticipated_config is None or thermal does not exist)

Resolution: Either set anticipated_config on thermal <id> in system/thermals.json, or remove/correct the thermal_id in the offending past_anticipated_commitments entry. See System Element Modeling Overview §4 for anticipated-thermal configuration and Case Format for initial_conditions.json’s schema.

Missing commitment window on an anticipated thermal

Section titled “Missing commitment window on an anticipated thermal”

When it occurs: Every thermal with an anticipated_config needs at least one window in initial_conditions.json’s past_anticipated_commitments, keyed by thermal_id. Case validation rejects a thermal that has none, for a lead given in stages and for a lead given in hours alike.

Fields:

FieldDescription
descriptionContains a [BusinessRuleViolation] initial_conditions.json (initial_conditions.past_anticipated_commitments): Thermal <id>: missing entry in initial_conditions.past_anticipated_commitments; every anticipated thermal must have at least one commitment window line

Example:

[BusinessRuleViolation] initial_conditions.json (initial_conditions.past_anticipated_commitments): Thermal 0: missing entry in initial_conditions.past_anticipated_commitments; every anticipated thermal must have at least one commitment window

Resolution: Add a window for the thermal to past_anticipated_commitments (a committed 0 MW is an explicit window), or remove the thermal’s anticipated_config. See Uncovered or over-covered past_anticipated_commitments window for the coverage the windows must reach and Case Format for initial_conditions.json’s schema.

Post-study boundary unanchored or malformed

Section titled “Post-study boundary unanchored or malformed”

When it occurs: The business-rule check validates post_study_stages.json — the post-horizon boundary calendar — and the anticipated-thermal deliveries that resolve onto it. Several distinct failures are collected under this one check. (A plant whose lead is longer than the whole study horizon and reaches no declared post-study stage is caught earlier, at anticipated-thermal validation, with a system/thermals.json … the plant can never deliver within the study horizon message (see Invalid anticipated-thermal lead); to clear it, shorten the lead or declare a post-study stage the lead reaches — see System Element Modeling Overview §4.) The post-study failures, keyed to post_study_stages.json:

  • The post-study calendar’s first stage does not start exactly at the study horizon end.
  • The post-study stages are not date-contiguous (a gap or overlap between consecutive stages).
  • A post-study stage’s duration_hours rounds to a non-positive whole-day span.
  • Rule 1 — a plant’s lead reaches a post-study stage with no thermal_bounds cell for its (thermal_id, post_study_stage_index).
  • V2 — a plant’s commitments decided before the study and delivered past the horizon do not tile the post-study stages they cover at coverage 1.0.
  • V3 — a commitment window covers a post-study stage the study itself decides (carried), or one past the plant’s decision reach.
  • V5 — a non-zero fixed commitment covers a post-study stage outside the plant’s commissioning window.

Fields:

FieldDescription
descriptionContains one [BusinessRuleViolation] post_study_stages.json (...): <message> line per failure — see the messages below

Example:

constraint violation: [BusinessRuleViolation] post_study_stages.json (stages[0].start_date): first post-study stage starts 2026-09-08 but the study horizon ends 2026-09-05; the post-study calendar must begin exactly at the study horizon end.
constraint violation: [BusinessRuleViolation] post_study_stages.json (stages start_date=2026-10-01): post-study stages are not date-contiguous: the stage starting 2026-09-05 ends 2026-09-28 but the next stage starts 2026-10-01; each stage must end exactly where the next begins.
constraint violation: [BusinessRuleViolation] post_study_stages.json (thermals[id=12].anticipated_config): Thermal 12: anticipated lead reaches post-study stage index 0, but post_study_stages.json has no thermal_bounds entry for (thermal_id 12, post_study_stage_index 0).
constraint violation: [BusinessRuleViolation] post_study_stages.json (thermals[id=12].anticipated_config): Thermal 12: past_anticipated_commitments do not tile post-study stage index(es) [0] at coverage 1.0; declare the fixed commitment for each (a committed 0 MW is explicit).
constraint violation: [BusinessRuleViolation] post_study_stages.json (thermals[id=12].anticipated_config): Thermal 12: past_anticipated_commitments cover post-study stage index(es) [1], which the study itself decides; a declared fixed value there contradicts the study's own decision. Remove the window, or lengthen anticipated_config's lead so the delivery becomes pre-study-decided.
constraint violation: [BusinessRuleViolation] post_study_stages.json (thermals[id=12].anticipated_config): Thermal 12: past_anticipated_commitments cover post-study stage index(es) [2], which are past the plant's decision reach and cannot be represented. Remove the window, or shorten anticipated_config's lead so the delivery falls inside the reach.
constraint violation: [BusinessRuleViolation] post_study_stages.json (thermals[id=12].anticipated_config): Thermal 12: past_anticipated_commitments window [2026-10-01, 2026-11-01) value_mw = 120 covers post-study stage index 0, which is outside the plant's commissioning window [entry=Some(0), exit=Some(6)); the plant is not in service for this stage and the fixed commitment cannot be delivered. Declare a zero commitment at this stage, or widen the commissioning window.

Resolution: Declare post_study_stages.json with its first stage starting exactly at the study horizon end and every stage date-contiguous with the next. Add a thermal_bounds cell for every (thermal_id, post_study_stage_index) a plant’s in-study-decided lead reaches. Tile every post-study stage that a past_anticipated_commitments window for a commitment decided before the study covers, at coverage 1.0, never covering a stage the study decides or one past the plant’s reach, and keep a non-zero fixed commitment inside the plant’s commissioning window. See Post-Study Boundary & Chained Studies for the boundary model and Case Format for post_study_stages.json’s schema.

Uncovered or over-covered past_anticipated_commitments window

Section titled “Uncovered or over-covered past_anticipated_commitments window”

When it occurs: An anticipated thermal’s past_anticipated_commitments windows must tile exactly the in-study delivery stages the plant decided before the study — every such stage covered at fraction 1.0 — and never a stage the study itself decides. The business-rule check reports a gap (an uncovered leading stage), an over-reach (a window covering a study-decided stage), and a horizon-straddle (a single window crossing the horizon end) as separate diagnostics; each can fire independently. A window may legitimately extend past the horizon (a commitment decided before the study and delivered past the horizon; for the equivalent term in other planning tools, see the Glossary) — that post-study coverage is validated separately, under Post-study boundary above.

Fields:

FieldDescription
descriptionContains a [BusinessRuleViolation] initial_conditions.json (thermals[id=<id>].anticipated_config): <message> line — see the messages below

Example:

constraint violation: [BusinessRuleViolation] initial_conditions.json (thermals[id=7].anticipated_config): Thermal 7: past_anticipated_commitments do not tile the leading 2 delivery stage(s) at coverage 1.0; study stage id(s) [0] are not covered exactly once. Write a commitment window (a committed 0 MW is explicit) for every leading delivery stage.
constraint violation: [BusinessRuleViolation] initial_conditions.json (thermals[id=7].anticipated_config): Thermal 7: past_anticipated_commitments cover study stage id(s) [3] beyond the leading 2 calendar-derived delivery stage(s); a commitment window may not cover a study stage the study itself decides. Shorten the plant's anticipated_config lead so its window stays within the leading 2 delivery stage(s).
constraint violation: [BusinessRuleViolation] initial_conditions.json (thermals[id=7].anticipated_config): Thermal 7: past_anticipated_commitments window [2026-08-15, 2026-09-15) straddles the study horizon end (2026-09-01); a single window may not cover both in-study and post-study delivery. Declare two windows split at the horizon instead: one ending at 2026-09-01 for the in-study coverage, one starting at 2026-09-01 for the post-study coverage.

Resolution: Write a commitment window for every in-study delivery stage the plant decided before the study (a committed 0 MW is a legitimate, explicit window — not an omission), never covering a stage the study itself decides (end the window earlier, or lengthen the plant’s anticipated_config lead if that stage is in fact decided before the study), and split any window at the horizon end rather than straddling it. Those are the in-study delivery stages whose decision falls before the first study stage: the first lead_stages stages, or, for lead_time_hours, every stage whose decision instant — one lead before the stage’s end — is at or before the study start. See System Element Modeling Overview §4.

Undeclared stage_id in bound-override parquet

Section titled “Undeclared stage_id in bound-override parquet”

When it occurs: A row in constraints/{thermal,hydro,line,pumping,contract,hydro_unit_group}_bounds.parquet whose stage_id is not a declared study stage (membership in the declared ids of stages.json, which may be gapped or start at 1) is rejected at novomodelo validate as BusinessRuleViolation; a constraints/ncs_bounds.parquet row is rejected as InvalidReference with NcsBoundsRow[i] has invalid stage_id N (not a valid study stage). Rows in constraints/generic_constraint_bounds.parquet and constraints/penalty_overrides_*.parquet with an undeclared stage_id are not rejected and have no effect.

Fields:

FieldDescription
descriptionOne [BusinessRuleViolation] constraints/{family}_bounds.parquet (...): ... line per rejected row for the six families, or one [InvalidReference] constraints/ncs_bounds.parquet: NcsBoundsRow[i] has invalid stage_id N ... line for NCS

Example:

error: [BusinessRuleViolation] constraints/hydro_bounds.parquet (hydro_id=0, stage_id=15): Hydro 0: hydro_bounds override names stage_id=15, which is not a declared study stage

Resolution: Correct the stage_id value to match one of the declared stage IDs in stages.json. For a typo (e.g., 15 instead of 5), fix the row; for a stale override referencing a removed stage, delete the row or update the stages declaration.

When it occurs: A generic-constraint term names a block that its stage cannot expose. The check runs only for a (constraint, stage) pair that has a row in constraints/generic_constraint_bounds.parquet. An evaporation reference is a hydro_evaporation term; a storage reference is a hydro_storage_initial, hydro_storage_final, hydro_useful_volume_initial, or hydro_useful_volume_final term. With K the number of blocks the stage declares, the check rejects four conditions:

  • A block index of K or more in an evaporation or storage reference, on any stage.
  • An evaporation reference to a block from 1 to K - 1 on a parallel stage with K > 1.
  • An evaporation reference with no block on a chronological stage with K > 1.
  • A storage reference to an interior block boundary, a boundary between two blocks, on a parallel stage with K > 1.

The rules are stated under hydro_evaporation block references.

Kind, file, entity: BusinessRuleViolation, constraints/generic_constraints.json, and the entity constraint[id=N], where N is the constraint’s id.

Messages: novomodelo validate prints one error: line per rejection. The five lines below show one message per template, in this order:

  1. An evaporation block of K or more.
  2. An evaporation block from 1 to K - 1 on a parallel stage.
  3. An interior storage boundary on a parallel stage.
  4. A storage block of K or more.
  5. An evaporation reference with no block on a chronological stage.
error: [BusinessRuleViolation] constraints/generic_constraints.json (constraint[id=0]): Constraint "evap_out_of_range": per-block evaporation reference `hydro_evaporation(0, 5)` at stage 0 references block 5 which does not exist at stage 0 (K = 2)
error: [BusinessRuleViolation] constraints/generic_constraints.json (constraint[id=1]): Constraint "evap_parallel_block": per-block evaporation reference `hydro_evaporation(0, 1)` at stage 0 names block 1, past the stage-level evaporation, which requires chronological block mode (stage 0 is parallel with 2 blocks); use block 0 or no block
error: [BusinessRuleViolation] constraints/generic_constraints.json (constraint[id=2]): Constraint "storage_interior": per-block storage reference `hydro_storage_final(0, 0)` at stage 0 resolves to an interior boundary, which requires chronological block mode (stage 0 is parallel with 2 blocks)
error: [BusinessRuleViolation] constraints/generic_constraints.json (constraint[id=3]): Constraint "storage_out_of_range": per-block storage reference `hydro_storage_initial(0, 5)` at stage 0 references block 5 which does not exist at stage 0 (K = 2)
error: [BusinessRuleViolation] constraints/generic_constraints.json (constraint[id=4]): Constraint "evap_chronological_bare": stage-level `hydro_evaporation(0)` at chronological stage 1 is ambiguous — evaporation is per-block there (K = 2); name a block, e.g. `hydro_evaporation(0, 0)`

Each per-block message quotes the whole term accessor(hydro, block); the stage-level ambiguity message quotes accessor(hydro) and suggests a block term.

Resolution: Name a block that exists on the stage: block indices run from 0 to K - 1. On a parallel stage, address evaporation with block 0 or with no block, and address a storage reference only at the stage’s start or end: block 0 or no block for an initial reference, the last block or no block for a final reference. To address per-block quantities, set block_mode to "chronological" on the stage in stages.json; a chronological stage requires a named block for evaporation, as in hydro_evaporation(<hydro>, 0).

Pumping station outside its endpoints’ Operating windows

Section titled “Pumping station outside its endpoints’ Operating windows”

When it occurs: A pumping station in system/pumping_stations.json is in service at a stage where its source or destination hydro is not Operating: before that hydro’s entry_stage_id, at or after its exit_stage_id, or while it is Filling. One line prints for each offending endpoint and names the earliest such stage.

PumpingStation {id}: active at stage {stage_id} while its {side} hydro {hid} is not Operating there (before entry_stage_id, at or after exit_stage_id, or Filling); a pumping station may operate only inside both endpoints' Operating windows

{side} is source or destination. The line prints as [BusinessRuleViolation] system/pumping_stations.json (PumpingStation {id}): <message>.

Resolution: Keep the station in service only at stages where both hydros are Operating: adjust the entry_stage_id and exit_stage_id of the station, or those of the hydros.


novomodelo validate reports a refused warm-start or simulation-only load as its phase, with the message labelled <output dir>/<policy.path>: , and exits 1; a checkpoint the OS refuses to read reports IoError and exits 2. The same kind carries the load’s warnings, which exit 0: Stored bases not used, entity manifest absent (source slots: {n}, current slots: {m}); slot identity could not be verified, relying on state_dimension alone and slot {i} (entity_type={entity_type}, entity_id={entity_id}, subindex={subindex}) was dormant in the source policy but is active in the current study; loading its cut. Policy-load errors gives the refusal messages.


novomodelo validate reports a refused resume load as its phase, with the message labelled <output dir>/<policy.path>: , and exits 1; a checkpoint the OS refuses to read reports IoError and exits 2. The same kind carries the load’s warnings, which exit 0: Stored bases not used, entity manifest absent (source slots: {n}, current slots: {m}); slot identity could not be verified, relying on state_dimension alone and slot {i} (entity_type={entity_type}, entity_id={entity_id}, subindex={subindex}) was dormant in the source policy but is active in the current study; loading its cut. Policy-load errors gives the refusal messages.

A full-FCF load uses a stored LP basis only when it fits its stage LP (Policy Management — Stored-basis gate states the rule); the others are left out with one warning, and the load proceeds:

warning: stored bases not used: {n} of {m} do not fit the current LP (first: node {node}, {reason}); a stored basis is used only when its column count equals the LP's, its row count equals the LP's template rows plus its recorded cut rows, and its basic count equals its row count; the policy was trained on a different LP

{reason} is one of {found} columns, the LP has {expected}, {found} rows, the LP expects {expected} or {found} basic entries for {expected} rows.

novomodelo run prints it as warning: {message} on stderr (--quiet hides it); novomodelo validate prints warning: <output dir>/<policy.path>: {message}, and --json gives the count as policy_load.unused_stored_bases. Python prints novomodelo-python: policy validation warning: {message}. The exit code is unaffected.


Severity: Error

When it occurs: Two or more hydros that declare a positive travel_time_hours feed the same downstream plant with values that differ by more than 1e-9, and at least one study stage in stages.json uses "chronological" block_mode. The check rejects the case whenever any study stage is chronological, not only the stages where the arcs’ timing would differ. Emitted by the business-rule check, with one line for each affected downstream plant, on system/hydros.json.

Example:

Hydro {downstream_id}: chronological confluence with heterogeneous travel times is unsupported in v1 — {n} declared arcs feed this downstream plant with differing travel_time_hours ({arcs}); align the arcs' travel_time_hours or keep every study stage in Parallel mode

{downstream_id} is the id of the downstream plant, {n} is the number of arcs that feed it, and {arcs} lists those arcs as comma-separated hydro {id} (travel_time_hours={t}) items.

Resolution: Give every arc into that downstream plant the same travel_time_hours in system/hydros.json, or set block_mode to "parallel" on every study stage in stages.json.


Severity: Warning (does not block execution)

When it occurs: A contract_import or contract_export term of a generic constraint names a contract id that system/energy_contracts.json does not declare. The term has no effect and the case still loads. The warning alerts the user to a possible input error.

Example:

{label} references Contract {id} which is a stub entity with no LP effect

{label} is GenericConstraint N term[M], naming the constraint id and the zero-based position of the term in its expression; {id} is the undeclared contract id.

Resolution: Correct the contract id in the term, or declare that contract in system/energy_contracts.json. If the term is intentional, this warning can be ignored.


Severity: Warning (does not block execution)

When it occurs: A concern about the input model is detected that leaves the case valid. Examples: an inverted penalty ordering, inflow-lag state disabled on every study stage despite supplied autoregressive coefficients, a negative value in scenarios/inflow_history.parquet, a season with no observations in that file, a hydro-season with at least one, but fewer than estimation.min_observations_per_season, fully covered stage occurrences in that file, or an advisory on a hydro’s declared travel_time_hours. Each is reported as a warning.

Example:

warning: penalties.json: Penalty ordering violation: max(deficit_segment_costs) (7500) should be > generation_violation_below_cost (10000) (both $/MWh) -- 1 hydro(s) affected, worst case: Hydro 0

Every penalty-ordering warning starts Penalty ordering violation:; the three messages are quoted under Load-time penalty checks.

The inflow-lag message:

inflow lags are disabled on all study stages (state_variables.inflow_lags = false) despite a PAR(p>0) inflow model (AR order {max_order}), so the inflow-lag dimensions are omitted from the per-stage state. This is a valid configuration for external-solver interoperability; otherwise it is likely a misconfiguration

{max_order} is replaced by the AR order when the warning is issued. When it fires and what it changes: stages[].state_variables — Cut Projection.

The observation-count message:

hydro {hid} season {sid} has {n} observations (minimum recommended: {min_obs}); estimation accuracy may be insufficient with so few observations

{min_obs} is estimation.min_observations_per_season. novomodelo validate reports the warning only when the inflow model is estimated from scenarios/inflow_history.parquet (that is, the case does not supply both scenarios/inflow_seasonal_stats.parquet and scenarios/inflow_ar_coefficients.parquet) and stages.json declares season_definitions.

Resolution: Review the flagged input. For a penalty-ordering warning, see Load-time penalty checks; for the inflow-lag warning, see stages[].state_variables — Cut Projection. Warnings of this type do not prevent the solver from running.


Severity: Warning (does not block execution)

When it occurs: A valid construct whose semantics are ambiguous or stage-dependent in a way that is likely to surprise the user. Two constructs trigger it, both emitted by the business-rule check in constraints/generic_constraints.json:

  1. thermal_generation(N) on an anticipated thermal. Using thermal_generation(N) in a generic constraint when thermal N is an anticipated thermal. thermal_generation refers to the per-block generation measured at the delivery stage (when the commitment matures), not the commitment decision made at the current stage. Users who intend to constrain the commitment itself should use anticipated_decision(N) instead.

    Example: A generic constraint thermal_generation(0) on an anticipated thermal:

    warning: constraints/generic_constraints.json (constraint[id=0]): Constraint "peak_cap": thermal_generation(0) references an anticipated thermal. thermal_generation refers to the per-block generation at the delivery stage, not the forward commitment. If you intend to constrain the commitment itself, use anticipated_decision(0) instead.

    Resolution: Review the constraint expression. If you want to bound the generation dispatched at the delivery stage, thermal_generation(N) is correct and the warning can be ignored. If you want to bound the advance commitment decision itself, replace thermal_generation(N) with anticipated_decision(N).

  2. max_stored_energy(h) paired with accumulated_productivity(h). One constraint references both computed tags for the same hydro — in its expression coefficients or in its bounds (an @emax on the right-hand side counts). The two are evaluated differently (accumulated_productivity at the plant’s reference point, max_stored_energy over the useful range), so a stored-energy comparison built from them does not cancel. One warning is emitted per such hydro; the case still validates.

    Example: A constraint that pairs the two tags for hydro 0:

    warning: constraints/generic_constraints.json (sec_mismatch): Constraint "sec_mismatch": pairs max_stored_energy(0) with accumulated_productivity(0) for hydro 0; the two ride different evaluators and would not cancel. The coefficient matching max_stored_energy is integrated_accumulated_productivity, not accumulated_productivity.

    Resolution: Use integrated_accumulated_productivity as the coefficient that matches max_stored_energy; see the security-curve example.

The warning is printed by novomodelo validate in its human-readable output; the --json success output does not carry warnings.


The phase value of novomodelo validate --json and the kind of an entry in the errors array of novomodelo.io.validate (Python) name the failure that stopped the load. Almost every case-load failure reports ConstraintError, because the load collects its problems into one report. The kinds are listed below in the order of the load steps in which they typically occur: reading a file, parsing it, checking its fields, then validating the whole case.

During a case load, a read, parse or schema failure in one file is collected into the validation report rather than returned on its own. It reaches you as a line of a ConstraintError: [FileNotFound] for a read failure, [ParseError] <file>: parse error: <message> for a parse failure, and [SchemaViolation] <file>: field <field>: <message> for a schema failure. The display formats below reach you only for a load error raised after that report is complete, such as a policy.path refusal raised once the case has loaded; see Exit Codes.

Severity: Error

When it occurs: A required file exists in the file manifest but cannot be read from disk — file not found, permission denied, or other OS-level I/O failure. Occurs when a JSON or Parquet file is read for parsing and the read returns an error. It is also returned when a policy.path cannot be inspected (exit 2).

Display format:

I/O error reading {path}: {source}

Fields:

FieldDescription
pathPath to the file that could not be read
sourceUnderlying OS I/O error

Resolution: Verify the file exists in the case directory. Check that the process has read permissions for the directory and file. For a full case load, the case root must contain all required files (see Case Format). A problem collected during a case load prints as a report line; see Validation report kinds.


ParseError — see ParseError; a parse failure collected in the report prints as a [ParseError] line.


Severity: Error

When it occurs: A file parses successfully but a field violates a schema constraint: a required field is missing, a value is outside its valid range, or an enum discriminator names an unknown variant. Occurs when the fields of a parsed file are checked. Also returned when training.selection (which carries the forward-pass count) or training.stopping_rules is absent and the config is parsed or merged outside a case load (for example, Python config overrides); a case load reports the same absence as a SchemaViolation line. It is also returned for a policy.path that the run refuses after the case loads (exit 1; see Configuration — policy).

Display format:

schema error in {path}, field {field}: {message}

Fields:

FieldDescription
pathPath to the file containing the invalid entry
fieldDot-separated path to the offending field (e.g., "hydros[3].bus_id")
messageHuman-readable description of the violation

Resolution: The field value identifies the exact location of the problem. Check that required fields are present and that values fall within documented ranges. A problem collected during a case load prints as a report line; see Validation report kinds.


Severity: Error

When it occurs: A catch-all for all validation diagnostics collected across any validation step, for system assembly rejections, and for guards raised directly by loaders after assembly (for example, a non-finite closure-derived innovation scale is rejected here naming the hydro and season). The description field contains every collected error message joined by newlines, each prefixed with its [<Kind>], source file, optional entity identifier, and message text.

Display format:

constraint violation: {description}

Fields:

FieldDescription
descriptionAll error messages joined by newlines

Example: The --json failure capture under How these errors reach you reports ConstraintError as its phase, with the collected report line in message.

Resolution: Read every line in description — each line is a separate problem. Address them all and re-run. The [<Kind>] prefix identifies the category of each problem, and each kind section gives its resolution. A problem collected during a case load prints as a report line; see Validation report kinds.


The six phase values below appear only in novomodelo validate’s pre-solver validation checks and are not LoadError kinds. They surface in novomodelo validate --json’s error phase field when a prep-phase check fails before the solver is initialized.

Severity: Error

When it occurs: A config.json validation failure in the study-setup preparation phase, after the config has been parsed and before stochastic model setup begins. File label: config.json.

Example: A gap stopping rule with neither tolerance nor relative_tolerance:

config.json: configuration validation error: gap stopping rule requires at least one of tolerance / relative_tolerance to be present

Severity: Error

When it occurs: A failure during stochastic model preparation — scenario generation, inflow-model setup, or external scenario validation. File label: scenarios/inflow_history.parquet for a stochastic-model error (after the label, its message begins stochastic error:) and scenarios/ for every other failure in this phase, such as a load error (after the label, its message begins I/O error:). The label depends on how the error is classified, not on the file it traces to.

Example: A historical library that holds no complete window, a non-stationary fitted inflow model (non-finite innovation scale), or an autoregressive stage whose standard deviation is zero, or a declared opening-tree file that is absent. Historical-library refusals, Inflow-model refusals and Opening-tree file missing quote the messages.

When it occurs: Three refusals of the historical window pool are reachable from a case. The pool serves the forward historical scheme and the stages that use the historical_residuals sampling method (see Historical Window Pool and Historical-Residual Openings). Each message is insufficient data: followed by the text below:

  • No year is admissible, so the pool is empty:
    no valid historical windows found: ensure that inflow history covers the required seasons for at least one starting year
  • A study stage has no season:
    V2.1: stage {stage_id} (index {index}) has season_id: None; all study stages must have a season_id assigned
  • A residual of the pool is not finite:
    V2.3: historical library contains non-finite eta (NEG_INFINITY or NaN) at window year {year}, stage id {stage_id}, hydro id {hydro_id} — sigma=0 with non-matching historical observation or numerical failure

V2.1 names the stage id, then the stage’s 0-based index. A stage has no season when it declares none and the case either has no season definitions or has a custom season map that leaves the stage’s start date uncovered. For example, with an inflow model of order 1 and a custom season map that covers the first stage and its lag season but leaves one later stage’s start date uncovered, novomodelo validate reports V2.1.

V2.3 names the window’s year, the stage id and the hydro id.

Rendering under novomodelo validate: the phase is StochasticPreparationError and the exit code is 1. The error: line carries the file label and the stochastic error: prefix. This excerpt omits the Validation: summary line, which carries the case path.

error: scenarios/inflow_history.parquet: stochastic error: insufficient data: no valid historical windows found: ensure that inflow history covers the required seasons for at least one starting year

novomodelo run on the same case exits 1. The line has no file label, and the validate hint follows it. This excerpt omits the banner, the Execution block and the Loading case: line.

error: stochastic error: insufficient data: no valid historical windows found: ensure that inflow history covers the required seasons for at least one starting year
-> run `novomodelo validate <CASE_DIR>` for a full diagnostic report

novomodelo validate builds the study as novomodelo run does, so it reports a refusal of the forward historical scheme’s library too: as StudySetupError (scenarios/: stochastic error: …) without policy.boundary, and as BoundaryReconciliationError (policy.boundary: …) with it; both commands exit 1. When a stage also uses historical_residuals, the refusal is reported as rendered above, with or without policy.boundary.

When it occurs: The inflow model, declared by the case’s autoregressive and statistics files or fitted from inflow_history.parquet, cannot support the study. Two refusals carry a fixed message:

  • An autoregressive stage has a standard deviation of zero. The message names the hydro id and the stage id, and ar_order is the stage’s autoregressive order:
    invalid PAR parameters for hydro {hydro_id} at stage {stage_id}: zero standard deviation with ar_order={ar_order}: AR model requires nonzero variance to normalize coefficients
    This is a stochastic-model error. novomodelo validate prints it after the scenarios/inflow_history.parquet: stochastic error: prefix and exits 1. novomodelo run prints it as error: stochastic error: invalid PAR parameters … with the validate hint, and exits 1.
  • The coefficients fitted from the history imply a non-stationary process, so a derived residual standard-deviation ratio is not finite. The message names the hydro id and the season id:
    I/O error: constraint violation: derived residual_std_ratio is non-finite for hydro_id={hydro_id} season={season}: the coefficients imply a non-stationary process (implied residual variance 1 - sum(psi*rho) is negative)
    This is a ConstraintError raised while the model is fitted, so it reaches the command as a load error. novomodelo validate prints it with the scenarios/ file label and exits 1. novomodelo run prints it without the I/O error: prefix, followed by a hint to run novomodelo validate, and also exits 1.

When it occurs: training.scenario_source.openings is {"source": "file"} and scenarios/noise_openings.parquet is absent. The stochastic preparation phase of novomodelo validate stops with exit 2, as novomodelo run does, and under --json the error’s phase is StochasticPreparationError. The message is:

openings source is 'file' but the conventional scenarios/noise_openings.parquet is absent

Example: novomodelo validate prints the message inside an I/O error line. The scenarios/ label and the I/O error: wording are how this phase reports a missing file, and <case> stands for the path of your case directory:

error: scenarios/: I/O error: I/O error reading <case>/scenarios/noise_openings.parquet: openings source is 'file' but the conventional scenarios/noise_openings.parquet is absent
error: I/O error in <case>/scenarios/noise_openings.parquet: openings source is 'file' but the conventional scenarios/noise_openings.parquet is absent
-> check that the path exists and you have read/write permissions

Resolution: Supply scenarios/noise_openings.parquet, or declare openings as {"source": "generated"}. Configuration describes the openings key.


Severity: Error

When it occurs: Study construction fails on a case without policy.boundary: a scenario library (the forward historical scheme’s included), an external-library check, a solver-profile check, or the admission gate of the gap rule and of dynamic cut selection. File label: scenarios/ for a stochastic-model error, config.json otherwise. On a case with policy.boundary, the same failures report BoundaryReconciliationError. novomodelo validate exits 1.

Example:

scenarios/: stochastic error: insufficient data: no valid historical windows found: ensure that inflow history covers the required seasons for at least one starting year

Severity: Error

When it occurs: A failure during computed-FPHA fitting or hydro production model preparation. File label: system/hydro_production_models.json.

Example: A plant whose generation.model is fpha and that has no entry in system/hydro_production_models.json:

system/hydro_production_models.json: configuration validation error: hydro UHE1 (id=0) has generation_model: "fpha" in hydros.json but no entry in hydro_production_models.json. Add an entry with source: "precomputed" to specify the hyperplane source.

Severity: Error

When it occurs: A failure during generic-constraint preparation, including expression inlining, parameter resolution, and slack setup. File label: constraints/.

An unresolved @parameter scalar — a generic-constraint parameter declared in constraints/generic_parameters.json that has no value for a season, stage or block the study needs — fails here at novomodelo validate time. An @name that no file declares is reported at load time; see @parameter / @name reference misuse.

Example: A seasonal parameter whose only value is for season 5, in a case with no season definitions:

constraints/: configuration validation error: parameter 'sea': no seasonal value for season_id=0 (needed by stage 0)

This phase also reports energy-conversion failures of a hydro plant computed at the same step — a forebay (VHA) table in system/hydro_geometry.parquet that fails to build, or a non-positive useful-range mean equivalent head — for any plant with VHA geometry and a specific productivity, independent of its generation model and of whether the case declares generic constraints (see Hydro Production Function Models §5.3).

A plant whose geometry and losses leave a negative head:

constraints/: configuration validation error: hydro EntityId(0) has non-positive equivalent head h_eq=-50

Severity: Error

When it occurs: A failure when reconciling an injected boundary policy with the current study. File label: policy.boundary.

Example: A boundary checkpoint written by another software or version reports the message of PolicySoftwareMismatch prefixed with the file label:

policy.boundary: policy was written by {writer}, but this is novomodelo {running}; a policy loads only in the software and version that wrote it: re-run the program that produced it with novomodelo {running}; for a converted boundary policy, convert it again

The phase reports a refusal from any of the boundary checks listed under Compatibility requirements once the checkpoint is read. A checkpoint that cannot be read reports in this phase too, as policy.boundary: configuration validation error: failed to read boundary policy checkpoint at {path}: {e} (exit 1; --json prints the object). On a case with policy.boundary, every study-construction failure reports this phase and label.


A policy load refuses a checkpoint that does not fit the study loading it. Two loads raise these errors: a full-FCF load of the checkpoint at policy.path (warm-start, resume, and simulation-only runs) and a boundary load of the checkpoint at policy.boundary.path. novomodelo run and the Python entry points raise them before training or simulation starts. novomodelo validate runs both loads the run would apply, reading the full-FCF checkpoint from <output dir>/<policy.path> (--output, default <CASE_DIR>/output/), and exits 1 on every refusal novomodelo run makes. These errors are not LoadError variants, and case loading does not return them.

A full-FCF load runs its checks in the order given under Check order. The checks of a boundary load are listed under Compatibility requirements. A manifest.bin whose format_version is not 3 fails before these checks: failed to read policy checkpoint: serialization error for entity checkpoint_manifest: unsupported checkpoint manifest format_version {n}; expected 3; re-run the program that produced it with novomodelo {running}; for a converted boundary policy, convert it again, exit 1.

ChannelRefusalUnreadable or missing checkpoint
novomodelo run, full-FCF loadExit 1. error: {message} and the validate hint on stderr.Exit 1: error: Policy directory not found: {path}. {requirement} or error: failed to read policy checkpoint: {e}. Exit 2 for a checkpoint the OS refuses to read: error: I/O error in {path}: {source} and the permissions hint.
novomodelo validate, full-FCF loadExit 1. Validation: 1 errors, 0 warnings in <case> and error: <output dir>/<policy.path>: {message}; --json phase WarmStartIncompatible or ResumeIncompatible.Exit 1 (phase as for a refusal), or exit 2 with phase IoError for a checkpoint the OS refuses to read.
novomodelo run and novomodelo validate, boundary loadExit 1. novomodelo run prints error: {message} and a hint line. novomodelo validate labels the message policy.boundary: ; under --json it reports phase BoundaryReconciliationError.Exit 1. error: failed to read boundary policy checkpoint at {path}: {e} and a hint line on stderr. Under novomodelo validate --json, phase BoundaryReconciliationError.
Python, full-FCF load (novomodelo.run.run, Study.train, Study.load_policy)PolicyIncompatibleError. The message starts policy validation error: .ValidationError for a missing directory, PolicyIncompatibleError for a read or decode failure, CaseIoError for a read the operating system refuses. The message is Policy directory not found: {path}. {requirement} or failed to read policy checkpoint: {e}, with no prefix.
Python, boundary load (novomodelo.run.run, Study, Study.train)novomodelo.run.run and Study.train raise PolicyIncompatibleError for a version mismatch, with the message prefix policy validation error: , and ValidationError for every other refusal, with the message prefix boundary cut error: . Study(...) constructs without error on a checkpoint it can read.ValidationError. The message starts setup validation error: . It is raised when the study is built, before training.
novomodelo.results.load_policyNo refusal: it reads a checkpoint of any novomodelo version without comparing it with a study.FileNotFoundError for a missing directory or a missing manifest.bin. OutputError for a file whose content cannot be decoded.

{requirement} names the run mode: Cannot warm-start without a prior policy., Cannot resume without a prior checkpoint., or Cannot run simulation-only mode without a trained policy.

Severity: Error

When it occurs: A full-FCF load or a boundary load reads a checkpoint whose manifest.bin records another software or version than the running build: the software and software_version compare exactly, so a different patch or pre-release build is refused as well. A checkpoint whose format_version is not 3 is refused before this check (How a policy-load failure reaches you). Version gate states the rule.

Message:

policy was written by {writer}, but this is novomodelo {running}; a policy loads only in the software and version that wrote it: re-run the program that produced it with novomodelo {running}; for a converted boundary policy, convert it again

{writer} is <software> <version>, <software>, which recorded no version, software that recorded no name, version <version> or software that recorded no name or version; {running} is the version of the novomodelo that loads it.

Python class: Python raises PolicyIncompatibleError with the message prefix policy validation error: , for a full-FCF load and for a boundary load. Under novomodelo validate --json, a boundary checkpoint written by another software or version reports phase BoundaryReconciliationError and the message prefixed policy.boundary: .

Resolution: Re-run the program that produced the checkpoint with the running novomodelo version; for a converted boundary policy, convert it again. Version gate describes both routes.

When it occurs: A full-FCF load finds that the checkpoint records no cost scale, or that it describes a different state space or study graph than the current study. The cost-scale check is step 2 of the check order and the state and graph checks are step 4. The entity-identity and graph checks run only when both sides carry the manifest.

Messages:

Cost scale (step 2):

policy checkpoint predates self-describing cuts (its resolved cuts/<pool>.bin carries no cost_scale_factor); re-run the program that produced it with novomodelo {running}; for a converted boundary policy, convert it again

State and graph (step 4):

policy state_dimension mismatch: policy has {n}, current system has {n} (a lag-state depth mismatch is a common cause)
policy num_stages mismatch: policy has {n}, current system has {n}
policy n_pools mismatch: policy has {n}, current system has {n}
entity manifest length mismatch: source has {n} slots, current study has {n}
entity-identity mismatch at slot {i}: source (entity_type={t}, entity_id={id}, subindex={s}) != current (entity_type={t}, entity_id={id}, subindex={s}); the cut coefficient at this slot would attach to the wrong state variable
graph manifest n_pools mismatch: source has {n}, current study has {n}
graph manifest node-count mismatch: source has {n} nodes, current study has {n}
graph manifest node {i} mismatch: source (id={id}, stage_id={id}, pool_id={id}) != current (id={id}, stage_id={id}, pool_id={id})
graph manifest edge-count mismatch: source has {n} edges, current study has {n}
graph manifest edge {i} mismatch: source ({source_id} -> {target_id}) != current ({source_id} -> {target_id})

A boundary load refuses a resolved pool without a cost_scale_factor with its own message:

boundary policy checkpoint at {path} predates self-describing cuts (its resolved cuts/<pool>.bin carries no cost_scale_factor); re-run the program that produced it with novomodelo {running}; for a converted boundary policy, convert it again

Python class: Python raises PolicyIncompatibleError with the message prefix policy validation error: for the full-FCF messages, and ValidationError with the message prefix boundary cut error: for the boundary message.

Resolution: Train a new policy for the changed study or restore the matching inputs; see Check order. For a cost-scale refusal, re-run the program that produced the checkpoint with the running novomodelo version, as its message states.

When it occurs: A boundary load (policy.boundary) finds an inflow-lag slot of the current study that the boundary checkpoint cannot supply. Either the checkpoint prices no inflow-lag coefficient for a hydro at a lag depth the study needs, or the coefficient it holds references a different past than the study: both sides carry a reference date and the two dates differ. Compatibility requirements gives the order of the boundary checks.

Messages:

No coefficient for a hydro at a lag depth; {names} lists every missing pair as hydro {id} at lag depth {depth}, separated by commas:

boundary policy has no inflow-lag coefficient for {names}: the boundary is lag-depth-incompatible with the current study

A coefficient that references a different past; the two dates print as YYYY-MM-DD:

boundary policy's inflow-lag coefficient for hydro {id} at lag depth {depth} references a different past than the current study: boundary {date}, current {date}

Python class: Python raises ValidationError with the message prefix boundary cut error: . novomodelo validate labels the message policy.boundary: .

Resolution: If the checkpoint has no coefficient for a hydro at a lag depth, train the source checkpoint with inflow_lags: true on the stage that follows the source pool’s stage. If the two dates differ, give the source pool’s stage and the study’s last stage the same start date, and do the same for each earlier pair of stages; Compatibility requirements describes both conditions.


Per-phase solver-profile overrides (training.solver.backward, training.solver.forward, simulation.solver) are validated against the compiled backend’s support matrix at study setup, before any stage LP is built, on every MPI process; every rejection below is a configuration error with its own message.

novomodelo validate runs these checks on every case: it reports a rejection with phase StudySetupError (config.json: configuration validation error: …), or BoundaryReconciliationError with policy.boundary configured. novomodelo run refuses the case before training. Both exit 1.

When it occurs: A binary built with the CLP backend (see Installation — CLP build) rejects every solver-profile override field outright — CLP’s own option surface has not been measured against these fields, so novomodelo refuses to silently apply a HiGHS-flavored value to it. An empty "solver": {} block and an absent solver block are both legal; only a field that is actually set triggers the error. When a profile sets more than one field, only the first field in declaration order is named, so fixing one field at a time surfaces one error per fix cycle rather than a combined report.

Example:

solver profile field "dual_edge_weight" for phase "backward" is unsupported on backend "clp": rejected until CLP is re-measured

For primal_feasibility_tolerance, the field token additionally carries the value that was set:

solver profile field "primal_feasibility_tolerance" (1e-7) for phase "forward" is unsupported on backend "clp": rejected until CLP is re-measured

"<phase>" is one of forward, backward, or simulation.

Resolution: Remove the override field from config.json on a CLP-built binary, or switch to the default HiGHS backend if the override is required.

HiGHS backend: seven per-field range checks

Section titled “HiGHS backend: seven per-field range checks”

The default (highs) backend accepts every solver-profile field. The closed enums (dual_edge_weight, scale, price, presolve) and use_warm_start are unconditionally valid — every variant is checked when config.json is read, so there is no separate runtime range check for them. The remaining seven numeric fields are range-checked at study setup; each failure names the field, the rejected value, and the phase (forward / backward / simulation) whose profile set it.

FieldRuleMessage
primal_feasibility_tolerancemust be finiteunsupported primal_feasibility_tolerance {value} for backend "highs" in phase "{phase}": value must be finite
dual_feasibility_tolerancemust be finite and >= 0.0000000001unsupported dual_feasibility_tolerance {value} for backend "highs" in phase "{phase}": value must be finite and >= 0.0000000001
simplex_update_limitmust be <= 2147483647unsupported simplex_update_limit {value} for backend "highs" in phase "{phase}": value must be <= 2147483647
cost_perturbationmust be finite and >= 0unsupported cost_perturbation {value} for backend "highs" in phase "{phase}": value must be finite and >= 0
refactor_error_tolerancemust be finite and >= 0unsupported refactor_error_tolerance {value} for backend "highs" in phase "{phase}": value must be finite and >= 0
factor_pivot_thresholdmust be in [0.0008, 0.5]unsupported factor_pivot_threshold {value} for backend "highs" in phase "{phase}": value must be in [0.0008, 0.5]
steepest_edge_devex_fallback_thresholdmust be finite and >= 1.0unsupported steepest_edge_devex_fallback_threshold {value} for backend "highs" in phase "{phase}": value must be finite and >= 1.0

0.0000000001 is 1e-10 printed in full; 2147483647 is the largest 32-bit signed integer; [0.0008, 0.5] is HiGHS’s own accepted pivot-threshold range.

Resolution: Adjust the offending field to satisfy the rule in the table. {phase} in the message identifies which of training.solver.backward, training.solver.forward, or simulation.solver set the invalid value.


A failure during novomodelo run prints error: {message} on stderr, followed by hint lines that depend on the exit code:

  • Exit 1, a refusal met during the run (for example a --threads mismatch), adds -> run `novomodelo validate <CASE_DIR>` for a full diagnostic report.
  • Exit 2, whose message is I/O error in {context}: {source}, adds -> check that the path exists and you have read/write permissions.
  • Exit 3 adds -> check constraint bounds (hydros may have conflicting min/max storage) and -> run `novomodelo validate <CASE_DIR>` for a full diagnostic report.
  • Exit 4 adds -> this may indicate a software or environment problem and -> report this at https://github.com/ons-ccee-epe/novomodelo/issues.

On two or more MPI processes, each failing process (an MPI rank) first prints rank {rank}: on its own line and then the same lines, and the job stops with that process’s exit code. When rank 0 fails to write the run outputs, every rank exits with rank 0’s code, and the others print rank 0 failed to write the run outputs; failing on every rank in lockstep. A checkpoint that does not fit the study is refused before training or simulation starts; see Policy-load errors.

A failure inside a training iteration ends training and exits with the code of its error: 3 for an LP or solver failure, 4 for a communication failure, 1 for a refusal. A run without --quiet first prints Training failed after {n} iterations. Partial outputs written to {dir}. The run also logs ERROR training failed after {n} iterations: {error} on stderr, with --quiet as well. Under MPI, a process that fails first can abort the job before the root process prints these lines. The same LP failures in simulation exit 3; see Simulation failures.

MessageExit codeWhen it occursResolution
LP infeasible at stage {stage}, iteration {iteration}, scenario {scenario}3A stage LP of a training iteration has no feasible solution; {stage} and {scenario} are 0-based positions.Check the constraint bounds at that stage, such as a generic constraint whose slack is disabled or conflicting hydro bounds; novomodelo validate does not solve the stage LPs, so it can accept such a case.
{solver_message}3A stage LP of a training iteration ends in a solver failure other than infeasibility; {solver_message} is one of the solver messages below, for example LP is unbounded.Look up {solver_message} in the rows below.
LP is unbounded3The objective of the stage LP is unbounded below.Look for a missing bound or a cost with the wrong sign in the case.
numerical difficulty: {message}3The solver reports numerical difficulties that persist through all its retries; {message} is the solver’s description.Report the case with the message.
time limit exceeded after {seconds}s or iteration limit reached after {iterations} iterations3A single LP solve ends at its time budget or at the solver’s simplex iteration limit.Report the case with the message.
internal solver error (code {code}): {message} or internal solver error: {message}3The solver fails in a way it cannot classify.Report the case with the message.
basis inconsistent: num_row={num_row}, total_basic={total_basic} (col_basic={col_basic}, row_basic={row_basic}) or basis row count mismatch: lp_rows={lp_rows}, basis_rows={basis_rows}3The solver rejects a warm-start basis that does not fit the LP.Report the case with the message.
non-uniform n_workers_local across MPI ranks: local={local}, min={min}, max={max}; all ranks must run with the same --threads value1The MPI processes of one job started with different --threads values, and the backward pass of the first iteration detects it; local is the printing process’s thread count, and min and max are the smallest and largest counts.Start every process of the job with the same --threads value.
{solver} initialisation failed: {e}3The solver backend (HiGHS or CLP) cannot create its solver instance when training starts; {e} is the backend’s message.Report it with the full message.
{solver} initialisation failed for simulation pool: {e}3The solver backend cannot create the solver instances of the simulation workers.Report it with the full message.

An infeasible stage LP in the first training iteration of a single-process run, on a case that novomodelo validate accepts, prints:

ERROR training failed after 0 iterations: infeasible subproblem at stage 0, iteration 1, scenario 0
Training failed after 0 iterations. Partial outputs written to ./out.
error: LP infeasible at stage 0, iteration 1, scenario 0
-> check constraint bounds (hydros may have conflicting min/max storage)
-> run `novomodelo validate <CASE_DIR>` for a full diagnostic report

A stage-LP failure in simulation exits 3, and every other simulation failure exits 4.

MessageExit codeWhen it occursResolution
LP infeasible at scenario {scenario_id}, stage {stage_id}: {solver_message}3A stage LP of a simulation scenario has no feasible solution; {solver_message} reads LP infeasible.Check the constraint bounds at that stage, such as a generic constraint whose slack is disabled; novomodelo validate does not solve the stage LPs, so it can accept such a case.
solver error at scenario {scenario_id}, stage {stage_id}: {solver_message}3A stage LP of a simulation scenario ends in a solver failure other than infeasibility; {solver_message} is one of the solver messages of Solver failures.Find the solver message in those rows.
stochastic error: {detail}4The scenario sampler of the simulation cannot be built or fails to sample; {detail} is the stochastic model’s message.See Stochastic failures during a run.
invalid simulation configuration: {detail}4The simulation setup fails an internal consistency check.Report the case with the message.
simulation output channel closed unexpectedly or simulation drain thread panicked4The thread that writes the simulation results stops before the simulation ends.Report the case with the full output of the run.
simulation cost aggregation error: I/O error during simulation output: {message}4The gather of the per-scenario costs across the MPI processes fails at the end of the simulation; {message} names the gather, allgatherv(counts) or allgatherv(costs), and the communication error. This is a communication failure, not a filesystem error; it exits 4.See Communication failures.
a peer rank failed simulation; failing on every rank in lockstep4Another MPI process fails during the simulation, and this process stops with it.Read the error printed under the rank {rank}: line of the process that failed first.
MessageExit codeWhen it occursResolution
communication backend error: communication backend 'mpi' is not available in this build (available: local)4--comm-backend mpi is set on a binary built without MPI support, which offers only the local backend.Run the novomodelo-mpi build under an MPI launcher (see HPC & Cluster Deployment), or drop --comm-backend mpi to run on one process.
communication backend error: 'mpi' backend initialization failed: {source}4The MPI runtime fails to initialise in an MPI build; {source} carries the MPI library’s message.Check that the MPI runtime is installed and that the job starts under mpiexec, mpirun or srun.
collective operation '{operation}' failed with MPI error code {code}: {message}4An MPI collective operation (broadcast, barrier, allreduce or allgatherv) fails during the run. The text follows a label that names the step, such as broadcast error (data): , post-training barrier error: or simulation path gather error: .Read the MPI library’s message and the output of the other processes to find the process that failed first.
cut_wire: unsupported version {version}1In the backward pass of a training iteration, a process reads a cut record whose version byte is not the one its own binary writes, which happens when the processes of one job run builds with different cut wire-format versions. {version} is the byte the sending process wrote. The message adds the validate hint.Restart every process with the same novomodelo binary.
try_from_broadcast_payload: unsupported wire version {version} at stage {stage} (expected {expected})1At the end of training, a process other than rank 0 reads the warm-start bases that rank 0 broadcasts and finds a wire-format version other than its own; {version} is the version rank 0 wrote and {stage} the 0-based node position. The novomodelo validate hint after it does not apply.Restart every process with the same novomodelo binary.
rank 0 signaled broadcast failure (length 0)4Rank 0 fails while loading the case, before it broadcasts the study, so the other processes receive an empty broadcast; rank 0 exits with the code of its own error.Fix the error that rank 0 prints under its rank 0: line.
hydro model preprocessing error on non-root rank: {e}4A process other than rank 0 fails to prepare the hydro models, which it rebuilds from the case directory.Give every process read access to the same case directory, and read {e} for the cause.
enumerated training at world >= 2 requires a graph with no interior branching (a deterministic trunk + terminal fan): node {node_id} is a non-leaf node not shared by every root→leaf path — the enumerated forward's per-rank state exchange is elided for it, so the replicated backward would cut against a zeroed incoming state on any rank that never visits it, pending the interior-node state-exchange fix1Enumerated training (training.selection.method "enumerated") runs on two or more MPI processes over a policy graph with an interior branching node; the message has no prefix and is followed by -> run `novomodelo validate <CASE_DIR>` for a full diagnostic report, although novomodelo validate does not check this and exits 0 on the case.Keep all branching at the last stage, run on one process, or use sampled selection.

Requesting the MPI backend from a binary built without MPI support prints:

error: communication backend error: communication backend 'mpi' is not available in this build (available: local)
-> this may indicate a software or environment problem
-> report this at https://github.com/ons-ccee-epe/novomodelo/issues

A filesystem failure while writing the policy checkpoint or the training and simulation results exits 2, except the write of one simulation scenario’s results, which the last row describes. The training/ files written before training starts exit 4 on any failure, so an unwritable --output directory ends with exit 4 unless removing a stale output fails first, which exits 2.

MessageExit codeWhen it occursResolution
I/O error in {context}: {source}2A write of the policy checkpoint or of training or simulation results after training starts fails on the filesystem, for example when the disk is full or a results file cannot be created; {context} is the path.Check that {context} exists and is writable and that the disk has free space, then rerun.
I/O error in checkpoint write at iteration {n}: {path}: {source}2A periodic checkpoint cannot be written to policy.path.Check that the policy directory is writable and the disk has space.
checkpoint write at iteration {n}: refusing to write a checkpoint: {entry}, found in {dir}, is not part of a checkpoint (manifest.bin, metadata.json, cuts/, basis/, states/); move it elsewhere or write the checkpoint to another directory1The policy directory holds an entry no checkpoint writer leaves there; nothing on disk changes.Move the entry, or set policy.path to another directory.
checkpoint write at iteration {n}: {e}4Encoding a periodic checkpoint fails; {e} is the encoder’s message.Report the case with the message.
I/O error in {path}: {source}2A stale output in --output, such as a _SUCCESS marker, cannot be removed before training starts; {path} is the entry.Make the entry removable, or point --output at a directory without it.
failed to write hydro model summary: {e}, failed to write provenance report: {e} or failed to write scaling report: {e}4One of the training/ files that novomodelo run writes before training starts cannot be written; {e} is the writer’s message, for example I/O error accessing {path}: {source}.Point --output at a directory the process can create and write.
rank 0 pre-training export failed; failing on every rank in lockstep4On MPI, rank 0 fails one of those writes or removals, and every other process prints this message.Fix the error that rank 0 prints under its rank 0: line.
serialization error for entity {entity}: {message}4Encoding an output table fails for the named entity collection.Report the case with the message.
manifest error for {manifest_type}: {message}4Building an output manifest fails.Report the case with the message.
ERROR simulation write error: {e}0The results of one simulation scenario cannot be written. The run continues and exits 0, and scenarios.failed in simulation/metadata.json counts the failed scenarios.Fix the cause named in {e}, rerun the simulation, and check scenarios.failed after every run.

A stochastic-model refusal raised while the study is set up exits 1 in both commands; StochasticPreparationError quotes it. The simulation sampler’s stochastic error: … exits 4 (Simulation failures).