# Getting started

## Installing the package

UnitCommitment.jl was tested and developed with Julia 1.10. To install Julia, please follow the installation guide on the official Julia website. To install UnitCommitment.jl, run the Julia interpreter, type `]` to open the package manager, then type:

    pkg> add UnitCommitment@0.4

To solve the optimization models, a mixed-integer linear programming (MILP) solver is also required. Please see the JuMP installation guide for more instructions on installing a solver. Typical open-source choices are HiGHS, Cbc and GLPK. In the instructions below, HiGHS will be used, but any other MILP solver should also be compatible.

## Solving a benchmark instance

We start this tutorial by illustrating how to use UnitCommitment.jl to solve one of the provided benchmark instances. The package contains a large number of deterministic benchmark instances collected from the literature and converted into a common data format, which can be used to evaluate the performance of different solution methods. See Instances for more details. The first step is to import `UnitCommitment` and HiGHS.

    using HiGHS
    using UnitCommitment

Next, we use the function `UnitCommitment.read_benchmark` to read the instance.

    instance = UnitCommitment.read_benchmark("matpower/case14/2017-01-01");

Now that we have the instance loaded in memory, we build the JuMP optimization model using `UnitCommitment.build_model`:

    model =
        UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer);

    [ Info: Building model...
    [ Info: Building scenario s1 with probability 1.0
    [ Info: Computing injection shift factors...
    [ Info: Computed ISF in 0.00 seconds
    [ Info: Computing line outage factors...
    [ Info: Computed LODF in 0.00 seconds
    [ Info: Applying PTDF and LODF cutoffs (0.00500, 0.00100)
    [ Info: Built model in 0.01 seconds

Next, we run the optimization process, with `UnitCommitment.optimize!`:

    UnitCommitment.optimize!(model)

    [ Info: Setting MILP time limit to 86400.00 seconds
    [ Info: Solving MILP...
    Running HiGHS 1.12.0 (git hash: 755a8e027a): Copyright (c) 2025 HiGHS under MIT licence terms
    MIP has 4744 rows; 4104 cols; 15633 nonzeros; 1080 integer variables (1080 binary)
    Coefficient ranges:
      Matrix  [1e+00, 3e+02]
      Cost    [3e+01, 3e+04]
      Bound   [1e+00, 1e+02]
      RHS     [1e+00, 4e+02]
    Presolving model
    4382 rows, 2704 cols, 14776 nonzeros  0s
    3177 rows, 1985 cols, 11301 nonzeros  0s
    3148 rows, 1965 cols, 11570 nonzeros  0s
    Presolve reductions: rows 3148(-1596); columns 1965(-2139); nonzeros 11570(-4063)

    Solving MIP model with:
       3148 rows
       1965 cols (862 binary, 0 integer, 0 implied int., 1103 continuous, 0 domain fixed)
       11570 nonzeros

    Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
         I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
         S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
         Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

            Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work
    Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

             0       0         0   0.00%   1.86264515e-09  inf                  inf        0      0      0         0     0.1s
     R       0       0         0   0.00%   360642.328869   360642.544974      0.00%        0      0      0       958     0.1s
             1       0         1 100.00%   360642.328869   360642.544974      0.00%        0      0      0       958     0.1s

    Solving report
      Status            Optimal
      Primal bound      360642.544974
      Dual bound        360642.328869
      Gap               6e-05% (tolerance: 0.01%)
      P-D integral      4.76315069837e-10
      Solution status   feasible
                        360642.544974 (objective)
                        0 (bound viol.)
                        0 (int. viol.)
                        0 (row viol.)
      Timing            0.10
      Max sub-MIP depth 0
      Nodes             1
      Repair LPs        0
      LP iterations     958
                        0 (strong br.)
                        0 (separation)
                        0 (heuristics)
    [ Info: Verifying transmission limits...
    [ Info: Verified transmission limits in 0.00 seconds
    [ Info: No violations found

Finally, we extract the optimal solution from the model:

    solution = UnitCommitment.solution(model)

    OrderedCollections.OrderedDict{Any, Any} with 15 entries:
      "Thermal production (MW)"         => OrderedDict("g1"=>[181.92, 172.85, 166.8…
      "Thermal production cost (\$)"    => OrderedDict("g1"=>[7241.95, 6839.01, 657…
      "Startup cost (\$)"               => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Is on"                           => OrderedDict("g1"=>[1.0, 1.0, 1.0, 1.0, 1…
      "Switch on"                       => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Switch off"                      => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Net injection (MW)"              => OrderedDict("b1"=>[181.92, 172.85, 166.8…
      "Load curtail (MW)"               => OrderedDict("b1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Line overflow (MW)"              => OrderedDict("l1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Spinning reserve (MW)"           => OrderedDict("r1"=>OrderedDict("g1"=>[4.6…
      "Spinning reserve shortfall (MW)" => OrderedDict("r1"=>[0.0, 0.0, 0.0, 0.0, 0…
      "Up-flexiramp (MW)"               => OrderedDict{Any, Any}()
      "Up-flexiramp shortfall (MW)"     => OrderedDict{Any, Any}()
      "Down-flexiramp (MW)"             => OrderedDict{Any, Any}()
      "Down-flexiramp shortfall (MW)"   => OrderedDict{Any, Any}()

We can then explore the solution using Julia:

    @show solution["Thermal production (MW)"]["g1"]

    36-element Vector{Float64}:
     181.92024301829747
     172.8503730182975
     166.80678301829755
     163.23845301829746
     165.5149530182975
     169.95052301829747
     182.3719130182975
     191.4327730182975
     196.6723430182975
     202.53524301829748
       ⋮
     165.0181030182975
     167.2404030182975
     180.89039301829752
     196.9523830182975
     206.5552530182975
     214.18877301829752
     225.48092301829752
     226.8179230182975
     222.72565301829746

Or export the entire solution to a JSON file:

    UnitCommitment.write("solution.json", solution)

## Solving a custom deterministic instance

In the previous example, we solved a benchmark instance provided by the package. To solve a custom instance, the first step is to create an input file describing the list of elements (generators, loads and transmission lines) in the network. See Data Format for a complete description of the data format UC.jl expects. To keep this tutorial self-contained, we will create the input JSON file using Julia; however, this step can also be done with a simple text editor. First, we define the contents of the file:

    json_contents = """
    {
        "Parameters": {
            "Version": "0.4",
            "Time horizon (h)": 4
        },
        "Buses": {
            "b1": {
                "Load (MW)": [100, 150, 200, 250]
            }
        },
        "Generators": {
            "g1": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 200],
                "Production cost curve (\$)": [0, 1000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            },
            "g2": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 300],
                "Production cost curve (\$)": [0, 3000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            }
        }
    }
    """;

Next, we write it to `example.json`.

    open("example.json", "w") do file
        return write(file, json_contents)
    end;

Now that we have the input file, we can proceed as before, but using `UnitCommitment.read` instead of `UnitCommitment.read_benchmark`:

    instance = UnitCommitment.read("example.json");
    model =
        UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer);
    UnitCommitment.optimize!(model)

    [ Info: Building model...
    [ Info: Building scenario s1 with probability 1.0
    [ Info: Built model in 0.00 seconds
    [ Info: Setting MILP time limit to 86400.00 seconds
    [ Info: Solving MILP...
    Running HiGHS 1.12.0 (git hash: 755a8e027a): Copyright (c) 2025 HiGHS under MIT licence terms
    MIP has 108 rows; 64 cols; 230 nonzeros; 32 integer variables (32 binary)
    Coefficient ranges:
      Matrix  [1e+00, 3e+02]
      Cost    [5e+00, 1e+03]
      Bound   [1e+00, 3e+02]
      RHS     [1e+00, 1e+06]
    Presolving model
    74 rows, 36 cols, 166 nonzeros  0s
    47 rows, 21 cols, 121 nonzeros  0s
    28 rows, 12 cols, 78 nonzeros  0s
    0 rows, 0 cols, 0 nonzeros  0s
    Presolve reductions: rows 0(-108); columns 0(-64); nonzeros 0(-230) - Reduced to empty
    Presolve: Optimal

    Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
         I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
         S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
         Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

            Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work
    Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

             0       0         0   0.00%   3750            3750               0.00%        0      0      0         0     0.0s

    Solving report
      Status            Optimal
      Primal bound      3750
      Dual bound        3750
      Gap               0% (tolerance: 0.01%)
      P-D integral      0
      Solution status   feasible
                        3750 (objective)
                        0 (bound viol.)
                        0 (int. viol.)
                        0 (row viol.)
      Timing            0.00
      Max sub-MIP depth 0
      Nodes             0
      Repair LPs        0
      LP iterations     0

Finally, we extract and display the solution:

    solution = UnitCommitment.solution(model)

    OrderedCollections.OrderedDict{Any, Any} with 14 entries:
      "Thermal production (MW)"         => OrderedDict("g1"=>[100.0, 150.0, 200.0, …
      "Thermal production cost (\$)"    => OrderedDict("g1"=>[500.0, 750.0, 1000.0,…
      "Startup cost (\$)"               => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0], …
      "Is on"                           => OrderedDict("g1"=>[1.0, 1.0, 1.0, 1.0], …
      "Switch on"                       => OrderedDict("g1"=>[1.0, 0.0, 0.0, 0.0], …
      "Switch off"                      => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0], …
      "Net injection (MW)"              => OrderedDict("b1"=>[0.0, 0.0, 0.0, 0.0])
      "Load curtail (MW)"               => OrderedDict("b1"=>[0.0, 0.0, 0.0, 0.0])
      "Spinning reserve (MW)"           => OrderedDict{Any, Any}()
      "Spinning reserve shortfall (MW)" => OrderedDict{Any, Any}()
      "Up-flexiramp (MW)"               => OrderedDict{Any, Any}()
      "Up-flexiramp shortfall (MW)"     => OrderedDict{Any, Any}()
      "Down-flexiramp (MW)"             => OrderedDict{Any, Any}()
      "Down-flexiramp shortfall (MW)"   => OrderedDict{Any, Any}()

    @show solution["Thermal production (MW)"]["g1"]

    4-element Vector{Float64}:
     100.0
     150.0
     200.0
     200.0

    @show solution["Thermal production (MW)"]["g2"]

    4-element Vector{Float64}:
      0.0
      0.0
      0.0
     50.0

## Solving a custom stochastic instance

In addition to deterministic test cases, UnitCommitment.jl can also solve two-stage stochastic instances of the problem. In this section, we demonstrate the most simple form, which builds a single (extensive form) model containing information for all scenarios. See Decomposition for more advanced methods.

First, we need to create one JSON input file for each scenario. Parameters that are allowed to change across scenarios are marked as "uncertain" in the JSON data format page. It is also possible to specify the name and weight of each scenario, as shown below.

We start by creating `example_s1.json`, the first scenario file:

    json_contents_s1 = """
    {
        "Parameters": {
            "Version": "0.4",
            "Time horizon (h)": 4,
            "Scenario name": "s1",
            "Scenario weight": 3.0
        },
        "Buses": {
            "b1": {
                "Load (MW)": [100, 150, 200, 250]
            }
        },
        "Generators": {
            "g1": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 200],
                "Production cost curve (\$)": [0, 1000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            },
            "g2": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 300],
                "Production cost curve (\$)": [0, 3000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            }
        }
    }
    """
    open("example_s1.json", "w") do file
        return write(file, json_contents_s1)
    end;

Next, we create `example_s2.json`, the second scenario file:

    json_contents_s2 = """
    {
        "Parameters": {
            "Version": "0.4",
            "Time horizon (h)": 4,
            "Scenario name": "s2",
            "Scenario weight": 1.0
        },
        "Buses": {
            "b1": {
                "Load (MW)": [200, 300, 400, 500]
            }
        },
        "Generators": {
            "g1": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 200],
                "Production cost curve (\$)": [0, 1000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            },
            "g2": {
                "Bus": "b1",
                "Type": "Thermal",
                "Production cost curve (MW)": [0, 300],
                "Production cost curve (\$)": [0, 3000],
                "Initial status (h)": -24,
                "Initial power (MW)": 0
            }
        }
    }
    """;
    open("example_s2.json", "w") do file
        return write(file, json_contents_s2)
    end;

Now that we have our two scenario files, we can read them using `UnitCommitment.read`. Note that, instead of a single file, we now provide a list.

    instance = UnitCommitment.read(["example_s1.json", "example_s2.json"])

    UnitCommitmentInstance(2 scenarios, 2 thermal units, 0 profiled units, 1 buses, 0 lines, 0 contingencies, 0 price sensitive loads, 4 time steps)

If we have a large number of scenario files, the Glob package can also be used to avoid having to list them individually:

    using Glob
    instance = UnitCommitment.read(glob("example_s*.json"))

    UnitCommitmentInstance(2 scenarios, 2 thermal units, 0 profiled units, 1 buses, 0 lines, 0 contingencies, 0 price sensitive loads, 4 time steps)

Finally, we build the model and optimize as before:

    model =
        UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer);
    UnitCommitment.optimize!(model)

    [ Info: Building model...
    [ Info: Building scenario s1 with probability 0.75
    [ Info: Building scenario s2 with probability 0.25
    [ Info: Built model in 0.01 seconds
    [ Info: Setting MILP time limit to 86400.00 seconds
    [ Info: Solving MILP...
    Running HiGHS 1.12.0 (git hash: 755a8e027a): Copyright (c) 2025 HiGHS under MIT licence terms
    MIP has 174 rows; 96 cols; 366 nonzeros; 32 integer variables (32 binary)
    Coefficient ranges:
      Matrix  [1e+00, 3e+02]
      Cost    [1e+00, 8e+02]
      Bound   [1e+00, 5e+02]
      RHS     [1e+00, 1e+06]
    Presolving model
    115 rows, 47 cols, 251 nonzeros  0s
    47 rows, 43 cols, 113 nonzeros  0s
    31 rows, 29 cols, 96 nonzeros  0s
    5 rows, 9 cols, 13 nonzeros  0s
    Presolve reductions: rows 5(-169); columns 9(-87); nonzeros 13(-353)

    Solving MIP model with:
       5 rows
       9 cols (3 binary, 0 integer, 0 implied int., 6 continuous, 0 domain fixed)
       13 nonzeros

    Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
         I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
         S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
         Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

            Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work
    Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

     J       0       0         0   0.00%   -inf            30312.5            Large        0      0      0         0     0.0s
     T       0       0         0   0.00%   4300            5312.5            19.06%        0      0      0         0     0.0s
             1       0         1 100.00%   5312.5          5312.5             0.00%        0      0      0         0     0.0s

    Solving report
      Status            Optimal
      Primal bound      5312.5
      Dual bound        5312.5
      Gap               0% (tolerance: 0.01%)
      P-D integral      0.000122052910402
      Solution status   feasible
                        5312.5 (objective)
                        0 (bound viol.)
                        0 (int. viol.)
                        0 (row viol.)
      Timing            0.00
      Max sub-MIP depth 0
      Nodes             1
      Repair LPs        0
      LP iterations     0

The solution to stochastic instances follows a slightly different format, as shown below:

    solution = UnitCommitment.solution(model)

    OrderedCollections.OrderedDict{Any, Any} with 2 entries:
      "s1" => OrderedDict{Any, Any}("Thermal production (MW)"=>OrderedDict("g1"=>[1…
      "s2" => OrderedDict{Any, Any}("Thermal production (MW)"=>OrderedDict("g1"=>[2…

The solution for each scenario can be accessed through `solution[scenario_name]`. For conveniance, this includes both first- and second-stage optimal decisions:

    solution["s1"]

    OrderedCollections.OrderedDict{Any, Any} with 14 entries:
      "Thermal production (MW)"         => OrderedDict("g1"=>[100.0, 150.0, 200.0, …
      "Thermal production cost (\$)"    => OrderedDict("g1"=>[500.0, 750.0, 1000.0,…
      "Startup cost (\$)"               => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0], …
      "Is on"                           => OrderedDict("g1"=>[1.0, 1.0, 1.0, 1.0], …
      "Switch on"                       => OrderedDict("g1"=>[1.0, 0.0, 0.0, 0.0], …
      "Switch off"                      => OrderedDict("g1"=>[0.0, 0.0, 0.0, 0.0], …
      "Net injection (MW)"              => OrderedDict("b1"=>[0.0, 0.0, 0.0, 0.0])
      "Load curtail (MW)"               => OrderedDict("b1"=>[0.0, 0.0, 0.0, 0.0])
      "Spinning reserve (MW)"           => OrderedDict{Any, Any}()
      "Spinning reserve shortfall (MW)" => OrderedDict{Any, Any}()
      "Up-flexiramp (MW)"               => OrderedDict{Any, Any}()
      "Up-flexiramp shortfall (MW)"     => OrderedDict{Any, Any}()
      "Down-flexiramp (MW)"             => OrderedDict{Any, Any}()
      "Down-flexiramp shortfall (MW)"   => OrderedDict{Any, Any}()

Theme

This document was generated with Documenter.jl version 1.15.0 on Wednesday 5 November 2025. Using Julia version 1.12.1.