Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Thermal Units

Thermal power plants are the dispatchable generation assets that complement hydro in Cobre’s system model. The term “thermal” covers any generator whose output is bounded by installed capacity and whose dispatch incurs an explicit cost per MWh: combustion turbines, combined-cycle plants, coal-fired units, nuclear plants, and diesel generators all map onto the same Cobre Thermal entity type.

Unlike hydro plants, thermal units carry no state between stages. Each stage’s LP sub-problem treats a thermal unit as a simple bounded generation variable with a marginal cost. The solver dispatches thermal units in merit order — from cheapest to most expensive — to meet any residual demand not covered by hydro generation. In a hydrothermal system, the long-run value of stored water is compared against the short-run cost of thermal dispatch at each stage, which is the fundamental trade-off the SDDP algorithm optimizes.

The cost structure of a thermal unit is modeled with a piecewise-linear cost curve (cost_segments). A single-segment plant dispatches all its capacity at a flat cost. A multi-segment plant has increasing marginal costs at higher output levels, reflecting the physical reality that a plant becomes less fuel-efficient as it approaches its rated capacity.

For an introductory walkthrough of writing thermals.json, see Building a System and Anatomy of a Case. This page provides the complete field reference, including multi-segment cost curves and GNL configuration.


JSON Schema

Thermal units are defined in system/thermals.json. The top-level object has a single key "thermals" containing an array of unit objects. The following example shows all fields for a two-segment plant with GNL configuration:

{
  "thermals": [
    {
      "id": 0,
      "name": "UTE1",
      "bus_id": 0,
      "cost_segments": [
        {
          "capacity_mw": 15.0,
          "cost_per_mwh": 5.0
        }
      ],
      "generation": {
        "min_mw": 0.0,
        "max_mw": 15.0
      }
    },
    {
      "id": 1,
      "name": "Angra 1",
      "bus_id": 0,
      "entry_stage_id": null,
      "exit_stage_id": null,
      "cost_segments": [
        {
          "capacity_mw": 300.0,
          "cost_per_mwh": 50.0
        },
        {
          "capacity_mw": 357.0,
          "cost_per_mwh": 80.0
        }
      ],
      "generation": {
        "min_mw": 0.0,
        "max_mw": 657.0
      },
      "gnl_config": {
        "lag_stages": 2
      }
    }
  ]
}

The first plant (UTE1) matches the 1dtoy template format: a single cost segment with no optional fields. The second plant (Angra 1) shows the complete schema with a two-segment cost curve and GNL dispatch anticipation. The fields entry_stage_id, exit_stage_id, and gnl_config are optional and can be omitted.


Core Fields

These fields appear at the top level of each thermal unit object.

FieldTypeRequiredDescription
idintegerYesUnique non-negative integer identifier. Must be unique across all thermal units.
namestringYesHuman-readable plant name. Used in output files, validation messages, and log output.
bus_idintegerYesIdentifier of the electrical bus to which this unit’s generation is injected. Must match an id in buses.json.
entry_stage_idinteger or nullNoStage index at which the unit enters service (inclusive). null means the unit is available from stage 0.
exit_stage_idinteger or nullNoStage index at which the unit is decommissioned (inclusive). null means the unit is never decommissioned.

Generation Bounds

The generation block sets the output limits for the unit (stored internally as min_generation_mw and max_generation_mw on the Thermal struct). These are enforced as hard bounds on the generation variable in each stage LP.

"generation": {
  "min_mw": 0.0,
  "max_mw": 657.0
}
FieldTypeDescription
min_mwnumberMinimum electrical generation (minimum stable load) [MW]. A non-zero value represents a must-run commitment: the solver is required to dispatch at least this much generation whenever the unit is in service.
max_mwnumberMaximum electrical generation (installed capacity) [MW]. This must equal the sum of all capacity_mw values in cost_segments.

A min_mw of 0.0 means the unit can be turned off completely — it is treated as an interruptible resource. A non-zero min_mw (for example, 100.0 for a plant whose turbine must spin continuously for mechanical reasons) means the LP must always dispatch at least that amount whenever the plant is active.

The max_mw field caps total generation and must equal the sum of all segment capacities in cost_segments. The validator checks this constraint and reports an error if the values do not match.


Cost Segments

The cost_segments array defines the piecewise-linear generation cost curve. Each segment represents a range of generation capacity and its associated marginal cost. Segments are applied in order: the first capacity_mw MW of output uses the first segment’s cost, the next capacity_mw MW uses the second segment’s cost, and so on.

"cost_segments": [
  {
    "capacity_mw": 300.0,
    "cost_per_mwh": 50.0
  },
  {
    "capacity_mw": 357.0,
    "cost_per_mwh": 80.0
  }
]
FieldTypeDescription
capacity_mwnumberGeneration capacity of this segment [MW]. Must be positive.
cost_per_mwhnumberMarginal cost in this segment [$/MWh].

Single-Segment Plants

Most thermal units in planning studies use a single cost segment, which treats the entire capacity as available at a uniform marginal cost:

"cost_segments": [
  { "capacity_mw": 15.0, "cost_per_mwh": 5.0 }
]

The LP will dispatch this plant at any level between min_mw and max_mw, with the generation cost equal to dispatched_mw * hours_in_block * cost_per_mwh.

Multi-Segment Plants

A multi-segment curve models a plant whose heat rate increases at higher output, which is common for steam turbines and combined-cycle units. For example, a 657 MW plant that is efficient at partial load but increasingly expensive above 300 MW:

"cost_segments": [
  { "capacity_mw": 300.0, "cost_per_mwh": 50.0 },
  { "capacity_mw": 357.0, "cost_per_mwh": 80.0 }
]

The LP sees this as two separate generation variables that are constrained to be dispatched in order: the cheaper 300 MW segment fills first before the solver uses any of the 357 MW higher-cost segment. The total capacity is 300 + 357 = 657 MW, which must equal generation.max_mw.

Segments must be listed in ascending cost order as a convention, though the optimizer will find the merit-order dispatch regardless of the ordering in the file.

Capacity Sum Constraint

The sum of all capacity_mw values must equal generation.max_mw. This is validated by Cobre and reported as a physical feasibility error if violated:

physical error: thermal 1 cost_segments capacity sum (657.0 MW) does not match
  max_generation_mw (700.0 MW)

GNL Configuration

The optional gnl_config block enables GNL (Gás Natural Liquefeito, or liquefied natural gas) dispatch anticipation. This models thermal units that require advance scheduling over multiple stages due to commitment lead times — for example, an LNG-fired plant that must be booked several weeks before the dispatch occurs.

"gnl_config": {
  "lag_stages": 2
}
FieldTypeDescription
lag_stagesintegerNumber of stages of dispatch anticipation. A value of 2 means the generation commitment for stage t must be decided at stage t - 2.

When lag_stages is greater than zero, the LP structure couples the commitment decision at an earlier stage to the dispatch variable at a later stage. This is an advanced feature for detailed operational planning studies. For most long-term planning horizons where monthly stages are used and commitment detail is not the focus, the gnl_config field can be omitted.

When the gnl_config block is absent, there is no dispatch anticipation lag — the unit can be committed and dispatched independently in each stage’s LP.


Validation Rules

Cobre’s five-layer validation pipeline checks the following conditions on thermal units. Violations are reported as error messages with the failing unit’s id.

RuleError ClassDescription
Bus reference integrityReference errorEvery bus_id must match an id in buses.json.
Cost segment capacity sumPhysical feasibilityThe sum of all capacity_mw values in cost_segments must equal max_mw in the generation block.
Generation bounds orderingPhysical feasibilitymin_mw must be less than or equal to max_mw.
Non-empty cost segmentsSchema errorThe cost_segments array must contain at least one segment.
Positive segment capacityPhysical feasibilityEach segment’s capacity_mw must be strictly positive.
GNL lag validityPhysical feasibilityWhen gnl_config is present, lag_stages must be a non-negative integer.