AGM Center
AboutResearchLearningGallery
Documentation
  • Introduction
    • KPG 193 Test System
    • Limitations
    • Network Structure
    • Temporal Profiles
    • Reference UC Solutions
    • Renewable Capacity by Bus
    • Scheduled Outages

AGM Center

Advancing power systems for a sustainable future through research, tools, and collaboration.

About

  • Who We Are
  • Our Team
  • What We Do
  • Get Involved

Documentation

  • Introduction
  • KPG TestGrid
  • KPG Run
  • KPG View

Our Location

Korea Institute of Energy Technology (KENTECH)
200 Hyeoksin-ro, Naju-si, Jeollanam-do
Korea 58330

Office Hours
Mon–Fri 09:00–18:00 KST

© 2026 AGM CENTER. All rights reserved

Docs/kpg test system/Reference UC Solutions

Reference UC Solutions

Reference UC Solutions

The KPG 193 test system includes pre-computed unit commitment (UC) solutions for all 365 days, providing reference results for validation and benchmarking of UC algorithms.

flowchart LR A[KPG 193<br/>Test System] --> B[UC Solver<br/>Tight MILP] B --> C[Reference Solutions<br/>365 days<br/>122 generators] C --> D[Your UC<br/>Implementation] D --> E[Compare<br/>Decisions] D --> F[Validate<br/>Costs] D --> G[Benchmark<br/>Performance]

These reference solutions serve multiple purposes:

  1. Validation: Verify your UC implementation produces feasible solutions

  2. Benchmarking: Compare solution quality and computational performance

  3. Initialization: Use as warm-start for advanced formulations

  4. Analysis: Study commitment patterns under realistic conditions

Directory Structure

kpg193_v1_5/
└── profile/
    └── commitment_decision/          # Reference UC solutions
        ├── commitment_decision_1.csv     # Day 1 on/off decisions
        ├── commitment_decision_2.csv     # Day 2 on/off decisions
        ├── ...
        └── commitment_decision_365.csv   # Day 365 on/off decisions

Additionally, supplementary validation data is available in:

data/
└── uc_results/
    └── KPG193_ver1_5/
        ├── up_variables_3.csv        # Extended UC results for selected days
        └── up_variables_4.csv

File Format: commitment_decision_[day].csv

Each file contains binary commitment decisions (on/off) for all 122 generators over 24 hours.

hour,generator_id,status
1,1,1
1,2,0
1,3,1
1,4,1
...
24,122,1

Column

Description

Values

Meaning

hour

Hour of the day

1-24

Hour 1 = 00:00-01:00, Hour 24 = 23:00-24:00

generator_id

Generator index

1-122

Corresponds to row in mpc.gen

status

Commitment decision

0 or 1

0 = off (unavailable), 1 = on (available for dispatch)

Data structure:

  • Rows per file: 24 hours × 122 generators = 2,928 rows

  • Total rows (all files): 2,928 × 365 = 1,068,720 rows

  • Binary decisions: Only values are 0 and 1

Generator ordering:

using MAT

mpc = matread("network/mat/KPG193_ver1_5.mat")["mpc"]
generators = mpc["gen"]

# Generator 1 = generators[1, :]
# Generator 2 = generators[2, :]
# ...
# Generator 122 = generators[122, :]

# Get generator type (from comment in .m file or by bus location)
# Generators are ordered by bus, then by type

Generator types in order:

  • Generators 1-54: LNG (Natural Gas)

  • Generators 55-100: Coal

  • Generators 101-122: Nuclear

Solution Methodology

The reference solutions were computed using a tight MILP formulation for UC with the following characteristics.

Objective function:

Where:

  • : Power output of generator at hour

  • : Startup indicator (1 if starting up at )

  • : Shutdown indicator (1 if shutting down at )

  • : Quadratic fuel cost function

  • : Startup cost

  • : Shutdown cost

Constraints included:

  1. Power balance: Generation = Demand + Losses

  2. Generator limits:

  3. Ramp rates:

  4. Minimum up time: Once started, must stay on

  5. Minimum down time: Once stopped, must stay off

  6. Startup/shutdown logic:

  7. Nuclear must-off: Scheduled outages enforced

  8. Renewable curtailment: Can reduce below available generation

  9. Transmission limits: Simplified (aggregated areas)

Solver configuration:

  • Software: HiGHS or Gurobi

  • MIP gap: 0.5% (99.5% optimal)

  • Time limit: 3600 seconds per day

  • Formulation: Tight MILP (Morales-España et al., 2013)

Reference:

Morales-España, G., Latorre, J. M., & Ramos, A. (2013). "Tight and compact MILP formulation for the thermal unit commitment problem." IEEE Transactions on Power Systems, 28(4), 4897-4908.

Loading Reference Solutions

Julia:

using CSV, DataFrames, MAT

# Load network data for context
mpc = matread("network/mat/KPG193_ver1_5.mat")["mpc"]
generators = mpc["gen"]
Pmax = generators[:, 9]
Pmin = generators[:, 10]

# Load commitment decisions for a specific day
day = 1
commitment = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)

# Statistics
total_decisions = nrow(commitment)
generators_on_total = sum(commitment.status)
capacity_on = sum(Pmax[commitment[commitment.status .== 1, :generator_id]])

println("Day $day Commitment Summary:")
println("  Total decisions: $total_decisions (24 hours × 122 generators)")
println("  Generator-hours online: $generators_on_total")
println("  Average online generators: $(round(generators_on_total / 24, digits=1))")
println("  Average online capacity: $(round(capacity_on / 24, digits=0)) MW")

# Analyze by hour
hourly_stats = combine(groupby(commitment, :hour),
                      :status => sum => :n_online)

println("\nHourly commitment pattern:")
for row in eachrow(hourly_stats)
    println("  Hour $(row.hour): $(row.n_online) generators online")
end

Python:





# Load network data
mat = scipy.io.loadmat('network/mat/KPG193_ver1_5.mat')
mpc = mat['mpc']
generators = mpc['gen'][0, 0]
Pmax = generators[:, 8]  # 0-indexed

# Load commitment decisions
day = 1
commitment = pd.read_csv(f'profile/commitment_decision/commitment_decision_{day}.csv')

# Statistics
total_on = commitment['status'].sum()
avg_online = total_on / 24

print(f"Day {day} Commitment Summary:")
print(f"  Generator-hours online: {total_on}")
print(f"  Average online generators: {avg_online:.1f}")

# Hourly pattern
hourly_online = commitment.groupby('hour')['status'].sum()
print(f"\nHourly online generators:")
print(f"  Minimum: {hourly_online.min()} at hour {hourly_online.idxmin()}")
print(f"  Maximum: {hourly_online.max()} at hour {hourly_online.idxmax()}")

MATLAB:

% Load network data
mpc = loadcase('network/m/KPG193_ver1_5.m');
Pmax = mpc.gen(:, 9);

% Load commitment decisions
day = 1;
commitment = readtable(sprintf('profile/commitment_decision/commitment_decision_%d.csv', day));

% Statistics
total_on = sum(commitment.status);
avg_online = total_on / 24;

fprintf('Day %d Commitment Summary:\n', day);
fprintf('  Generator-hours online: %d\n', total_on);
fprintf('  Average online generators: %.1f\n', avg_online);

% Hourly pattern
hourly_on = grpstats(commitment.status, commitment.hour, 'sum');
fprintf('\nOnline generators by hour:\n');
fprintf('  Min: %d\n', min(hourly_on));
fprintf('  Max: %d\n', max(hourly_on));

Validation Use Cases

1. Verify commitment feasibility:

using CSV, DataFrames

# Load your solution
your_solution = # ... your UC results as DataFrame with columns: hour, generator_id, status

# Load reference solution
day = 1
reference = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)

# Compare commitment patterns
comparison = innerjoin(your_solution, reference, 
                      on=[:hour, :generator_id], 
                      makeunique=true)

# Count differences
n_differences = sum(comparison.status .!= comparison.status_1)
agreement_pct = (1 - n_differences / nrow(comparison)) * 100

println("Commitment Comparison:")
println("  Agreements: $(nrow(comparison) - n_differences) / $(nrow(comparison))")
println("  Agreement rate: $(round(agreement_pct, digits=2))%")
println("  Differences: $n_differences decisions")

# Analyze which generators differ most
comparison.differs = comparison.status .!= comparison.status_1
diff_by_gen = combine(groupby(comparison[comparison.differs, :], :generator_id),
                     nrow => :n_diff_hours)
sort!(diff_by_gen, :n_diff_hours, rev=true)

println("\nGenerators with most differences:")
println(first(diff_by_gen, 10))

2. Cost comparison:

# Calculate cost of reference solution
function calculate_uc_cost(commitment_df, mpc, demand)
    # Load generator cost data
    gencost = mpc["gencost"]
    genthermal = mpc["genthermal"]
    
    total_cost = 0.0
    
    for hour in 1:24
        hour_commitment = filter(row -> row.hour == hour, commitment_df)
        
        # Solve economic dispatch given commitment
        # (simplified - actual ED would optimize power output)
        
        for row in eachrow(hour_commitment)
            if row.status == 1
                gen_id = row.generator_id
                
                # Approximate generation cost at mid-point
                Pmin = mpc["gen"][gen_id, 10]
                Pmax = mpc["gen"][gen_id, 9]
                P_est = (Pmin + Pmax) / 2
                
                # Quadratic cost
                c2 = gencost[gen_id, 5]
                c1 = gencost[gen_id, 6]
                c0 = gencost[gen_id, 7]
                
                fuel_cost = c2 * P_est^2 + c1 * P_est + c0
                total_cost += fuel_cost
            end
        end
    end
    
    return total_cost
end

# Compare
reference_cost = calculate_uc_cost(reference, mpc, demand)
your_cost = calculate_uc_cost(your_solution, mpc, demand)

cost_diff_pct = (your_cost - reference_cost) / reference_cost * 100

println("Cost Comparison:")
println("  Reference cost: \



(round(your_cost, digits=0))")
println(" Difference: $(round(cost_diff_pct, digits=2))%")

3. Startup/shutdown pattern analysis:

# Analyze startup and shutdown events
function analyze_transitions(commitment_df)
    startups = zeros(Int, 122)
    shutdowns = zeros(Int, 122)
    
    for gen_id in 1:122
        gen_schedule = sort(filter(row -> row.generator_id == gen_id, commitment_df), :hour)
        
        for i in 2:24
            if gen_schedule.status[i] == 1 && gen_schedule.status[i-1] == 0
                startups[gen_id] += 1
            elseif gen_schedule.status[i] == 0 && gen_schedule.status[i-1] == 1
                shutdowns[gen_id] += 1
            end
        end
    end
    
    return startups, shutdowns
end

ref_starts, ref_stops = analyze_transitions(reference)
your_starts, your_stops = analyze_transitions(your_solution)

println("Transition Events:")
println("  Reference - Startups: $(sum(ref_starts)), Shutdowns: $(sum(ref_stops))")
println("  Your solution - Startups: $(sum(your_starts)), Shutdowns: $(sum(your_stops))")

# More transitions typically means higher startup costs

Warm-Start Initialization

Use reference solutions as an initial point for your UC solver:

using JuMP, HiGHS

# Load reference solution
day = 1
reference = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)

# Build UC model
model = Model(HiGHS.Optimizer)

# Define variables
@variable(model, u[1:122, 1:24], Bin)  # Commitment variables

# ... add constraints and objective ...

# Set initial values from reference
for row in eachrow(reference)
    set_start_value(u[row.generator_id, row.hour], row.status)
end

# Solve (may converge faster with good initial point)
optimize!(model)

Extended Validation Data

Files: up_variables_3.csv and up_variables_4.csv (located in data/uc_results/KPG193_ver1_5/)

These files provide more detailed UC results for selected days, including:

  • Continuous power output variables

  • Ramping behavior

  • Reserve provision

  • Exact cost breakdown

generator,time,u,p
1,1,1,5.456000000000001
1,2,1,2.9957374675955317
1,3,1,2.0475354742249032
...

Column

Description

Unit

generator

Generator ID

1-122

time

Hour

1-24

u

Commitment status

0 or 1

p

Power output

MW

Usage:

# Load detailed results
detailed_uc = CSV.read("data/uc_results/KPG193_ver1_5/up_variables_3.csv", DataFrame)

# Compare dispatch against your solution
for gen_id in 1:122
    gen_dispatch = filter(row -> row.generator == gen_id, detailed_uc)
    
    # Analyze ramping
    power_output = gen_dispatch.p
    ramp_up = maximum(diff(power_output))
    ramp_down = minimum(diff(power_output))
    
    println("Generator $gen_id:")
    println("  Max ramp up: $(round(ramp_up, digits=2)) MW/h")
    println("  Max ramp down: $(round(abs(ramp_down), digits=2)) MW/h")
end

Annual Statistics

Annual commitment analysis:

using CSV, DataFrames, Statistics

# Analyze all 365 days
annual_stats = DataFrame(
    day = Int[],
    n_online_avg = Float64[],
    n_startups = Int[],
    n_shutdowns = Int[]
)

for day in 1:365
    commitment = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)
    
    # Average online generators
    avg_online = mean(combine(groupby(commitment, :hour), :status => sum).status_sum)
    
    # Count transitions
    starts, stops = analyze_transitions(commitment)
    
    push!(annual_stats, (day, avg_online, sum(starts), sum(stops)))
end

# Statistics
println("Annual Commitment Statistics:")
println("  Avg generators online: $(round(mean(annual_stats.n_online_avg), digits=1))")
println("  Min generators online: $(round(minimum(annual_stats.n_online_avg), digits=1)) on day $(annual_stats.day[argmin(annual_stats.n_online_avg)])")
println("  Max generators online: $(round(maximum(annual_stats.n_online_avg), digits=1)) on day $(annual_stats.day[argmax(annual_stats.n_online_avg)])")
println("  Total annual startups: $(sum(annual_stats.n_startups))")
println("  Total annual shutdowns: $(sum(annual_stats.n_shutdowns))")

Technology-specific patterns:

# Analyze by technology
# Assuming: LNG = 1-54, Coal = 55-100, Nuclear = 101-122

day = 150  # Summer day
commitment = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)

# Tag by technology
commitment.technology = ifelse.(commitment.generator_id .<= 54, "LNG",
                        ifelse.(commitment.generator_id .<= 100, "Coal", "Nuclear"))

# Summary by technology and hour
tech_hourly = combine(groupby(commitment, [:hour, :technology]), 
                     :status => sum => :n_online)

# Pivot for easier viewing
using DataFrames
tech_pivot = unstack(tech_hourly, :hour, :technology, :n_online)

println("Online generators by technology (Day $day):")
println(tech_pivot)

Benchmarking Your UC Implementation

Performance metrics:

  1. Solution quality:

    • Objective value (total cost)

    • Feasibility (all constraints satisfied)

    • Agreement with reference (% matching decisions)

  2. Computational performance:

    • Solve time

    • MIP gap at termination

    • Number of branch-and-bound nodes

  3. Solution characteristics:

    • Number of startups/shutdowns

    • Capacity factor by technology

    • Load following capability

Example benchmark report:

# Generate benchmark report
function benchmark_uc_solver(your_solver_func, day_range)
    results = DataFrame(
        day = Int[],
        ref_cost = Float64[],
        your_cost = Float64[],
        cost_diff_pct = Float64[],
        agreement_pct = Float64[],
        solve_time = Float64[]
    )
    
    for day in day_range
        # Load reference
        reference = CSV.read("profile/commitment_decision/commitment_decision_$day.csv", DataFrame)
        
        # Solve with your implementation
        t_start = time()
        your_solution, your_cost = your_solver_func(day)
        solve_time = time() - t_start
        
        # Compare
        ref_cost = calculate_uc_cost(reference, mpc, demand)
        cost_diff = (your_cost - ref_cost) / ref_cost * 100
        
        # Agreement
        comparison = innerjoin(your_solution, reference, on=[:hour, :generator_id])
        agreement = sum(comparison.status_your .== comparison.status_ref) / nrow(comparison) * 100
        
        push!(results, (day, ref_cost, your_cost, cost_diff, agreement, solve_time))
    end
    
    return results
end

# Run benchmark
println("Running benchmark on days 1-30...")
benchmark_results = benchmark_uc_solver(my_uc_solver, 1:30)

# Summary
println("\nBenchmark Summary:")
println("  Avg cost difference: $(round(mean(benchmark_results.cost_diff_pct), digits=2))%")
println("  Avg agreement: $(round(mean(benchmark_results.agreement_pct), digits=2))%")
println("  Avg solve time: $(round(mean(benchmark_results.solve_time), digits=2)) seconds")
println("  Max solve time: $(round(maximum(benchmark_results.solve_time), digits=2)) seconds")

Known Characteristics

Commitment patterns:

  1. Nuclear plants: Run continuously except during scheduled outages (must-off periods)

  2. Coal plants: Provide baseload and mid-merit, typically run 18-24 hours/day

  3. LNG plants: Load following, commitment varies 12-20 hours/day based on demand

  4. Seasonal variation: More units online in summer (high demand) than spring/fall

Typical daily pattern:

Hours 1-6 (Night):     ~70-80 generators online (baseload)
Hours 7-9 (Morning):   ~85-95 generators online (ramp up)
Hours 10-18 (Day):     ~95-105 generators online (peak period)
Hours 19-22 (Evening): ~90-100 generators online (evening peak)
Hours 23-24 (Night):   ~75-85 generators online (ramp down)

Limitations and Notes

  1. Optimality: Solutions are 99.5% optimal (0.5% MIP gap), not necessarily globally optimal

  2. Transmission: Simplified representation; detailed AC power flow not enforced in UC

  3. Reserves: Spinning reserve requirements included but may differ from your formulation

  4. Renewables: Curtailment allowed; reference may differ if you impose must-take constraints

  5. Inter-day coupling: Each day solved independently; no multi-day optimization

References

For detailed information on the validation methodology:

  • KPG 193 Paper: Song & Kim (2024), arXiv:2411.14756

    • Section III: Validation

  • UC Formulation: Morales-España et al. (2013), "Tight and compact MILP formulation for the thermal unit commitment problem," IEEE Transactions on Power Systems

Next Steps

  • Implement your own UC: KPG Run UC Documentation →

  • Review data: Network Structure →

  • Renewables & profiles: Renewables → and Profiles & Time Series →

  • Scheduled outages: Scheduled Outages →

  • Limitations: Limitations →

  • Compare formulations: Solver Comparison →

← Temporal ProfilesRenewable Capacity by Bus →

ON THIS PAGE

Directory StructureFile Format: commitment_decision_[day].csvSolution MethodologyLoading Reference SolutionsValidation Use CasesWarm-Start InitializationExtended Validation DataAnnual StatisticsBenchmarking Your UC ImplementationKnown CharacteristicsLimitations and NotesReferencesNext Steps