Compare commits

...

59 Commits

Author SHA1 Message Date
1254780e42 web: backend: Implement view endpoint 2025-11-07 11:32:14 -06:00
ad8ee6fe6b web: backend: Make JobProcessor more abstract 2025-11-07 11:16:13 -06:00
e52798da7a web: backend: minor fixes 2025-11-07 10:59:00 -06:00
35dd5ab1a9 web: backend: Implement job queue 2025-11-06 15:22:19 -06:00
5c7b8038a1 web: Initial backend implementation 2025-11-06 13:49:02 -06:00
c2d5e58c75 web: Reorganize into frontend/backend 2025-11-06 12:41:03 -06:00
54b5b9dd7f docs: Fix broken image link 2025-11-05 09:57:17 -06:00
395c041202 Merge branch 'hotfix/0.4.1' into dev 2025-11-05 09:52:36 -06:00
03575d5dc4 Update CHANGELOG 2025-11-05 09:36:27 -06:00
8e2769dc0e web: Update favicon 2025-09-10 15:02:04 -05:00
e96557bed8 web: Add placeholder text 2025-09-10 14:54:26 -05:00
5b9727b0ba web: Add support for transmission contingencies 2025-09-10 14:28:14 -05:00
9f560df4f5 web: Add support for price-sensitive loads 2025-09-10 12:30:11 -05:00
356046be7b web: Standardize capitalization in section headers 2025-09-10 11:55:18 -05:00
201dd34b30 web: Add support for storage units 2025-09-10 11:54:17 -05:00
fd95cefefc web: Handle error during table data update 2025-09-10 10:58:54 -05:00
930c6a3277 web: Optimize table data updates 2025-09-10 10:04:24 -05:00
3eb4cceb54 web: Clean up console logs and reset active cell after edit 2025-09-09 12:11:54 -05:00
5fbf9af286 web: Fix failing tests 2025-09-09 12:05:01 -05:00
1c821dde14 web: changeTimeHorizon, changeTimeStep: Adjust profiled units 2025-09-09 11:39:20 -05:00
055faefa28 web: Sync TextInputRow value with initialValue changes 2025-09-09 11:28:38 -05:00
af7cb92282 web: Adjust padding and margins 2025-09-09 11:10:04 -05:00
872cb7a66e web: Preserve active cell state during table updates 2025-09-09 10:56:57 -05:00
771eb5fa6d web: Fix update columns 2025-09-09 10:56:56 -05:00
840eea9879 web: Add Dockerfile 2025-06-27 11:54:49 -05:00
0dc0a5b460 Implement web case builder
Co-authored-by: Alinson S. Xavier <git@axavier.org>
Co-authored-by: Shaoming Xu <xsm90827@gmail.com>
2025-06-27 11:42:03 -05:00
a09e25db0f web: Remove TEST_SCENARIO and update scenario initialization 2025-06-27 11:41:03 -05:00
53489c1638 web: Update nullable number handling 2025-06-27 11:37:50 -05:00
fff70cce67 Fix ucjl-0.2.json.gz fixture 2025-06-27 10:59:16 -05:00
869498fa97 web: implement data migration, reorganize data folder 2025-06-27 10:59:02 -05:00
cac9d7e230 web: Transmission lines 2025-06-27 10:30:14 -05:00
eb3d39b1ab web: ThermalUnits: onDataChanged 2025-06-25 13:59:07 -05:00
3bf028577e web: ThermalUnits: CSV upload 2025-06-25 13:17:04 -05:00
3f10ad23ca web: ProfiledUnits: Revise CSV upload 2025-06-25 12:15:09 -05:00
7c752e4c31 web: ThermalUnits: Add, delete, and rename 2025-06-25 11:47:44 -05:00
dea5217916 web: ThermalUnits: Implement CSV download 2025-06-25 10:55:45 -05:00
012331c4bd web: DataTable: Use list editor for boolean values 2025-06-25 10:46:55 -05:00
1fea873ddf web: ThermalUnits: Build table data 2025-06-25 10:32:04 -05:00
d78700bdc6 web: Start implementation of ThermalUnitsComponent 2025-06-25 10:12:32 -05:00
02ddaf20dc web: Flatten dir structure 2025-06-25 08:58:39 -05:00
be500b920e web: Add undo functionality 2025-06-24 12:19:41 -05:00
9d48112bb9 web: Propagate bus deletion and renaming 2025-06-24 11:28:21 -05:00
5bfc3ffa55 web: Improve CSV validation 2025-06-24 11:06:26 -05:00
1b37af82e3 web: ProfiledUnits: Add data change and rename functionality 2025-06-24 10:39:06 -05:00
86aababf33 web: ProfiledUnits: Rename and delete 2025-06-24 09:21:54 -05:00
8397571c11 web: Add createProfiledUnit 2025-06-23 16:48:10 -05:00
8827f9e6c8 web: profiled units: Allow CSV upload 2025-06-23 16:06:23 -05:00
eb862e5701 web: Update busOperations to support time-indexed loads 2025-06-23 10:57:46 -05:00
80d8bb838c web: Profiled units 2025-05-29 12:33:06 -05:00
ee7a948a78 web: display toast, maintain table stage, localStorage 2025-05-21 12:01:04 -05:00
0cf93e7aa0 web: use defaults; calculate table height 2025-05-20 10:27:24 -05:00
6d9bbaab4e web: Reorganize 2025-05-16 14:37:53 -05:00
957294f220 web: Accept gz files 2025-05-16 14:32:03 -05:00
d8feef5431 web: Allow changing parameters 2025-05-16 13:44:14 -05:00
6469840f0a Validation; reformat source code 2025-05-15 14:04:16 -05:00
062b38514b Buses 2025-05-15 11:49:42 -05:00
ea58cf1615 web: Initial version 2025-05-12 14:36:57 -05:00
facc9faabf Update README.md 2024-08-19 10:17:16 -05:00
Feng
d34378c660 Update decomposition.md
put application names in subtitle: production cost modeling for time decomposition; stochastic UC for scenario decomposition.
2024-07-14 14:35:02 -05:00
89 changed files with 24026 additions and 10 deletions

11
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*-off.md
*.bak
*.gz
*.ipynb
@@ -19,6 +20,7 @@
.apdisk
.com.apple.timemachine.donotpresent
.fseventsd
.idea
.ipy*
.vscode
Icon
@@ -32,12 +34,11 @@ benchmark/tables
benchmark/tmp.json
build
docs/_build
docs/src/tutorials/customizing.md
docs/src/tutorials/lmp.md
docs/src/tutorials/market.md
docs/src/tutorials/usage.md
instances/**/*.json
instances/_source
local
notebooks
docs/src/tutorials/usage.md
docs/src/tutorials/customizing.md
docs/src/tutorials/market.md
docs/src/tutorials/lmp.md
*-off.md

View File

@@ -11,6 +11,13 @@ All notable changes to this project will be documented in this file.
[semver]: https://semver.org/spec/v2.0.0.html
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
## [0.4.1] - 2025-11-05
### Fixed
- Fix multi-threading issues in Julia 1.12
### Changed
- The package now requires Julia 1.10 or newer
## [0.4.0] - 2024-05-21
### Added
- Add support for two-stage stochastic problems

View File

@@ -108,7 +108,7 @@ See official documentation at: https://anl-ceeesa.github.io/UnitCommitment.jl/
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows:
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.4)". Zenodo (2024). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Jun He, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.4)". Zenodo (2024). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
If you use the instances, we additionally request that you cite the original sources, as described in the documentation.

View File

@@ -107,7 +107,7 @@ Note that this curve also specifies the production limits. Specifically, the fir
```@raw html
<center>
<img src="../assets/cost_curve.png" style="max-width: 500px"/>
<img src="../../assets/cost_curve.png" style="max-width: 500px"/>
<div><b>Figure 1.</b> Piecewise-linear production cost curve.</div>
<br/>
</center>

View File

@@ -1,8 +1,8 @@
# Decomposition methods
## 1. Time decomposition
## 1. Time decomposition for production cost modeling
Solving unit commitment instances that have long time horizons (for example, year-long 8760-hour instances) requires a substantial amount of computational power. To address this issue, UC.jl offers a time decomposition method, which breaks the instance down into multiple overlapping subproblems, solves them sequentially, then reassembles the solution.
Solving unit commitment instances that have long time horizons (for example, year-long 8760-hour instances in production cost modeling) requires a substantial amount of computational power. To address this issue, UC.jl offers a time decomposition method, which breaks the instance down into multiple overlapping subproblems, solves them sequentially, then reassembles the solution.
When solving a unit commitment instance with a dense time slot structure, computational complexity can become a significant challenge. For instance, if the instance contains hourly data for an entire year (8760 hours), solving such a model can require a substantial amount of computational power. To address this issue, UC.jl provides a time_decomposition method within the `optimize!` function. This method decomposes the problem into multiple sub-problems, solving them sequentially.
@@ -57,7 +57,7 @@ solution = UnitCommitment.optimize!(
)
```
## 2. Scenario decomposition with Progressive Hedging
## 2. Scenario decomposition with Progressive Hedging for stochstic UC
By default, UC.jl uses the Extensive Form (EF) when solving stochastic instances. This approach involves constructing a single JuMP model that contains data and decision variables for all scenarios. Although EF has optimality guarantees and performs well with small test cases, it can become computationally intractable for large instances or substantial number of scenarios.

Binary file not shown.

1
web/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
jobs

17
web/backend/Project.toml Normal file
View File

@@ -0,0 +1,17 @@
name = "Backend"
uuid = "948642ed-e3f9-4642-9296-0f1eaf40c938"
version = "0.1.0"
authors = ["Alinson S. Xavier <git@axavier.org>"]
[deps]
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
[compat]
CodecZlib = "0.7.8"
HTTP = "1.10.19"
JSON = "0.21.4"
Random = "1.11.0"

View File

@@ -0,0 +1,12 @@
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
module Backend
basedir = joinpath(dirname(@__FILE__), "..")
include("jobs.jl")
include("server.jl")
end

69
web/backend/src/jobs.jl Normal file
View File

@@ -0,0 +1,69 @@
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import Base: put!
Base.@kwdef mutable struct JobProcessor
pending::Channel{String} = Channel{String}(Inf)
processing::Channel{String} = Channel{String}(Inf)
shutdown::Channel{Bool} = Channel{Bool}(1)
worker_task::Union{Task,Nothing} = nothing
work_fn::Function
end
function Base.put!(processor::JobProcessor, job_id::String)
@info "New job received: $job_id"
return put!(processor.pending, job_id)
end
function isbusy(processor::JobProcessor)
return isready(processor.pending) || isready(processor.processing)
end
function run!(processor::JobProcessor)
while true
# Check for shutdown signal
if isready(processor.shutdown)
break
end
# Wait for a job with timeout
if !isready(processor.pending)
sleep(0.1)
continue
end
# Move job from pending to processing queue
job_id = take!(processor.pending)
put!(processor.processing, job_id)
# Run work function
processor.work_fn(job_id)
# Remove job from processing queue
take!(processor.processing)
end
end
function start(processor::JobProcessor)
processor.worker_task = @async run!(processor)
return
end
function stop(processor::JobProcessor)
# Signal worker to stop
put!(processor.shutdown, true)
# Wait for worker to finish
if processor.worker_task !== nothing
try
wait(processor.worker_task)
catch
# Worker may have already exited
end
end
return
end
export JobProcessor, start, stop, put!, isbusy

132
web/backend/src/server.jl Normal file
View File

@@ -0,0 +1,132 @@
using HTTP
using Random
using JSON
using CodecZlib
using UnitCommitment
struct ServerHandle
server::HTTP.Server
processor::JobProcessor
end
function submit(req, processor::JobProcessor)
# Check if request body is empty
compressed_body = HTTP.payload(req)
if isempty(compressed_body)
return HTTP.Response(400, "Error: No file provided")
end
# Validate compressed JSON by decompressing and parsing
try
decompressed_data = transcode(GzipDecompressor, compressed_body)
JSON.parse(String(decompressed_data))
catch e
return HTTP.Response(400, "Error: Invalid compressed JSON")
end
# Generate random job ID (lowercase letters and numbers)
job_id = randstring(['a':'z'; '0':'9'], 16)
# Create job directory
job_dir = joinpath(basedir, "jobs", job_id)
mkpath(job_dir)
# Save input file
json_path = joinpath(job_dir, "input.json.gz")
write(json_path, compressed_body)
# Add job to queue
put!(processor, job_id)
# Return job ID as JSON
response_body = JSON.json(Dict("job_id" => job_id))
return HTTP.Response(200, response_body)
end
function jobs_view(req)
# Extract job_id from URL path /jobs/{job_id}/view
path_parts = split(req.target, '/')
job_id = path_parts[3] # /jobs/{job_id}/view -> index 3
# Construct job directory path
job_dir = joinpath(basedir, "jobs", job_id)
# Check if job directory exists
if !isdir(job_dir)
return HTTP.Response(404, "Job not found")
end
# Read log file if it exists
log_path = joinpath(job_dir, "output.log")
log_content = isfile(log_path) ? read(log_path, String) : nothing
# Read output.json if it exists
output_path = joinpath(job_dir, "output.json")
output_content = isfile(output_path) ? read(output_path, String) : nothing
# Create response JSON
response_data = Dict(
"log" => log_content,
"solution" => output_content
)
response_body = JSON.json(response_data)
return HTTP.Response(200, response_body)
end
function start_server(port::Int = 8080; optimizer)
Random.seed!()
function work_fn(job_id)
job_dir = joinpath(basedir, "jobs", job_id)
mkpath(job_dir)
input_filename = joinpath(job_dir, "input.json.gz")
log_filename = joinpath(job_dir, "output.log")
solution_filename = joinpath(job_dir, "output.json")
try
open(log_filename, "w") do io
redirect_stdout(io) do
redirect_stderr(io) do
instance = UnitCommitment.read(input_filename)
model = UnitCommitment.build_model(;
instance,
optimizer = optimizer,
)
UnitCommitment.optimize!(model)
solution = UnitCommitment.solution(model)
UnitCommitment.write(solution_filename, solution)
end
end
end
catch e
@error "Failed job: $job_id" e
open(log_filename, "a") do io
println(io, "\nError: ", e)
println(io, "\nStacktrace:")
return Base.show_backtrace(io, catch_backtrace())
end
end
return
end
# Create and start job processor
processor = JobProcessor(; work_fn)
start(processor)
router = HTTP.Router()
# Register /submit endpoint
HTTP.register!(router, "POST", "/submit", req -> submit(req, processor))
# Register job/*/view endpoint
HTTP.register!(router, "GET", "/jobs/*/view", jobs_view)
server = HTTP.serve!(router, port; verbose = false)
return ServerHandle(server, processor)
end
function stop(handle::ServerHandle)
stop(handle.processor)
close(handle.server)
return nothing
end

View File

@@ -0,0 +1,23 @@
name = "BackendT"
uuid = "27da795e-16fd-43bd-a2ba-f77bdecaf977"
version = "0.1.0"
authors = ["Alinson S. Xavier <git@axavier.org>"]
[deps]
Backend = "948642ed-e3f9-4642-9296-0f1eaf40c938"
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat]
CodecZlib = "0.7.8"
HTTP = "1.10.19"
HiGHS = "1.20.1"
JSON = "0.21.4"
JuliaFormatter = "2.2.0"
Revise = "3.12.0"
Test = "1.11.0"

View File

@@ -0,0 +1,40 @@
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
module BackendT
using Test
using HTTP
using JSON
using CodecZlib
import Backend
import JuliaFormatter
using HiGHS
BASEDIR = dirname(@__FILE__)
include("jobs_test.jl")
include("server_test.jl")
function fixture(path::String)::String
return "$BASEDIR/../fixtures/$path"
end
function runtests()
@testset "UCJL Backend" begin
server_test_usage()
jobs_test_usage()
end
return
end
function format()
JuliaFormatter.format(BASEDIR, verbose = true)
JuliaFormatter.format("$BASEDIR/../../src", verbose = true)
return
end
export runtests, format
end

View File

@@ -0,0 +1,32 @@
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using Backend
using Test
function jobs_test_usage()
@testset "JobProcessor" begin
# Define dummy work function
received_job_id = []
function work_fn(job_id)
push!(received_job_id, job_id)
end
# Create processor with work function
processor = JobProcessor(; work_fn)
# Start the worker
start(processor)
# Push job to queue
put!(processor, "test")
# Wait for job to complete
sleep(0.1)
stop(processor)
# Check that the work function was called with correct job_id
@test received_job_id[1] == "test"
end
end

View File

@@ -0,0 +1,60 @@
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
const PORT = 32617
function server_test_usage()
server = Backend.start_server(PORT; optimizer = HiGHS.Optimizer)
try
# Read the compressed fixture file
compressed_data = read(fixture("case14.json.gz"))
# Submit test case
response = HTTP.post(
"http://localhost:$PORT/submit",
["Content-Type" => "application/gzip"],
compressed_data,
)
@test response.status == 200
# Check response
response_data = JSON.parse(String(response.body))
@test haskey(response_data, "job_id")
job_id = response_data["job_id"]
@test length(job_id) == 16
# Wait for jobs to finish
sleep(0.1)
while isbusy(server.processor)
sleep(0.1)
end
# Verify the compressed file was saved correctly
job_dir = joinpath(Backend.basedir, "jobs", job_id)
saved_input_path = joinpath(job_dir, "input.json.gz")
saved_log_path = joinpath(job_dir, "output.log")
saved_output_path = joinpath(job_dir, "output.json")
@test isfile(saved_input_path)
@test isfile(saved_log_path)
@test isfile(saved_output_path)
saved_data = read(saved_input_path)
@test saved_data == compressed_data
# Query job information
view_response = HTTP.get("http://localhost:$PORT/jobs/$job_id/view")
@test view_response.status == 200
# Check response
view_data = JSON.parse(String(view_response.body))
@test haskey(view_data, "log")
@test haskey(view_data, "solution")
@test view_data["log"] !== nothing
@test view_data["solution"] !== nothing
# Clean up
rm(job_dir, recursive=true)
finally
stop(server)
end
end

View File

@@ -0,0 +1,7 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
.env

1
web/frontend/.env Normal file
View File

@@ -0,0 +1 @@
FAST_REFRESH=false

25
web/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
assets

View File

@@ -0,0 +1 @@
{}

13
web/frontend/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
# Build Stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production Stage
FROM nginx:stable-alpine AS production
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

17746
web/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
web/frontend/package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/pako": "^2.0.3",
"@types/papaparse": "^5.3.16",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"ajv": "^8.17.1",
"eslint": "^8.57.1",
"pako": "^2.1.0",
"papaparse": "^5.5.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "^5.0.1",
"tabulator-tables": "^6.3.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"semi": [
"error",
"always"
]
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/tabulator-tables": "^6.2.6",
"prettier": "3.5.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="UnitCommitment.jl Case Builder" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Case Builder - UnitCommitment.jl</title>
<style>
:root {
--site-max-width: 1500px;
--site-min-width: 900px;
--box-border: 1px solid rgba(0, 0, 0, 0.2);
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
--border-radius: 4px;
--primary: #0d6efd;
--contrast-100: #202020;
--contrast-80: #606060;
--contrast-60: #909090;
--contrast-20: #d6d6d6;
--contrast-10: #f6f6f6;
--contrast-0: #fefefe;
}
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #333;
}
.content {
background-color: var(--contrast-10);
padding-bottom: 36px;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,48 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import assert from "node:assert";
import { BusesColumnSpec, generateBusesData } from "./Buses";
import { generateCsv, parseCsv } from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
test("generate CSV", () => {
const [data, columns] = generateBusesData(TEST_DATA_1);
const actualCsv = generateCsv(data, columns);
const expectedCsv =
"Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" +
"b1,35.79534,34.38835,33.45083,32.89729,33.25044\n" +
"b2,14.03739,13.48563,13.11797,12.9009,13.03939\n" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
assert.strictEqual(actualCsv, expectedCsv);
});
test("parse CSV", () => {
const csvContents =
"Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" +
"b1,0,1,2,3,4\n" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
const [newBuses, err] = parseCsv(csvContents, BusesColumnSpec, TEST_DATA_1);
assert(err === null);
assert.deepEqual(newBuses, {
b1: {
"Load (MW)": [0, 1, 2, 3, 4],
},
b3: {
"Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268],
},
});
});
test("parse CSV with duplicated names", () => {
const csvContents =
"Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" +
"b1,0,0,0,0,0\n" +
"b1,0,0,0,0,0";
const [, err] = parseCsv(csvContents, BusesColumnSpec, TEST_DATA_1);
assert(err !== null);
assert.equal(err.message, `Name "b1" is duplicated (row 2)`);
});

View File

@@ -0,0 +1,150 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import { offerDownload } from "../Common/io";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { useRef } from "react";
import { ValidationError } from "../../core/Data/validate";
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { ColumnDefinition } from "tabulator-tables";
import {
changeBusData,
createBus,
deleteBus,
renameBus,
} from "../../core/Operations/busOps";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { UnitCommitmentScenario } from "../../core/Data/types";
export const BusesColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Load (MW)",
type: "number[T]",
width: 60,
},
];
export const generateBusesData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, BusesColumnSpec);
const data = generateTableData(scenario.Buses, BusesColumnSpec, scenario);
return [data, columns];
};
function BusesComponent(props: CaseBuilderSectionProps) {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generateBusesData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "buses.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csvContents: any) => {
const [newBuses, err] = parseCsv(
csvContents,
BusesColumnSpec,
props.scenario,
);
if (err) {
props.onError(err.message);
return;
}
const newScenario = {
...props.scenario,
Buses: newBuses,
};
props.onDataChanged(newScenario);
});
};
const onAdd = () => {
const newScenario = createBus(props.scenario);
props.onDataChanged(newScenario);
};
const onDataChanged = (
bus: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changeBusData(
bus,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onDelete = (bus: string): ValidationError | null => {
const newScenario = deleteBus(bus, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renameBus(oldName, newName, props.scenario);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Buses">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generateBusesData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
}
export default BusesComponent;

View File

@@ -0,0 +1,145 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import Header from "./Header";
import Parameters from "./Parameters";
import BusesComponent from "./Buses";
import { BLANK_SCENARIO } from "../../core/Data/fixtures";
import "tabulator-tables/dist/css/tabulator.min.css";
import "../Common/Forms/Tables.css";
import { useState } from "react";
import Footer from "./Footer";
import { offerDownload } from "../Common/io";
import { preprocess } from "../../core/Operations/preprocessing";
import Toast from "../Common/Forms/Toast";
import ProfiledUnitsComponent from "./ProfiledUnits";
import ThermalUnitsComponent from "./ThermalUnits";
import TransmissionLinesComponent from "./TransmissionLines";
import { UnitCommitmentScenario } from "../../core/Data/types";
import StorageComponent from "./StorageUnits";
import PriceSensitiveLoadsComponent from "./Psload";
export interface CaseBuilderSectionProps {
scenario: UnitCommitmentScenario;
onDataChanged: (scenario: UnitCommitmentScenario) => void;
onError: (msg: string) => void;
}
const CaseBuilder = () => {
const [scenario, setScenario] = useState(() => {
const savedScenario = localStorage.getItem("scenario");
if (!savedScenario) return BLANK_SCENARIO;
const [processedScenario, err] = preprocess(JSON.parse(savedScenario));
if (err) {
console.log(err);
return BLANK_SCENARIO;
}
return processedScenario!!;
});
const [undoStack, setUndoStack] = useState<UnitCommitmentScenario[]>([]);
const [toastMessage, setToastMessage] = useState<string>("");
const setAndSaveScenario = (
newScenario: UnitCommitmentScenario,
updateUndoStack = true,
) => {
if (updateUndoStack) {
const newUndoStack = [...undoStack, scenario];
if (newUndoStack.length > 25) {
newUndoStack.splice(0, newUndoStack.length - 25);
}
setUndoStack(newUndoStack);
}
setScenario(newScenario);
localStorage.setItem("scenario", JSON.stringify(newScenario));
};
const onClear = () => {
setAndSaveScenario(BLANK_SCENARIO);
};
const onSave = () => {
offerDownload(
JSON.stringify(scenario, null, 2),
"application/json",
"case.json",
);
};
const onDataChanged = (newScenario: UnitCommitmentScenario) => {
setAndSaveScenario(newScenario);
};
const onLoad = (data: any) => {
const json = JSON.parse(data);
const [scenario, err] = preprocess(json);
if (err) {
setToastMessage(err.message);
return;
}
setAndSaveScenario(scenario!);
setToastMessage("Data loaded successfully");
};
const onUndo = () => {
if (undoStack.length === 0) return;
setUndoStack(undoStack.slice(0, -1));
setAndSaveScenario(undoStack[undoStack.length - 1]!, false);
};
return (
<div>
<Header
onClear={onClear}
onSave={onSave}
onLoad={onLoad}
onUndo={onUndo}
/>
<div className="content">
<Parameters
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<BusesComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<ThermalUnitsComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<ProfiledUnitsComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<StorageComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<PriceSensitiveLoadsComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<TransmissionLinesComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<Toast message={toastMessage} />
</div>
<Footer />
</div>
);
};
export default CaseBuilder;

View File

@@ -0,0 +1,14 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.Footer {
background-color: #333;
text-align: center;
color: #aaa;
font-size: 14px;
padding: 16px;
line-height: 24px;
}

View File

@@ -0,0 +1,19 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./Footer.module.css";
function Footer() {
return (
<div className={styles.Footer}>
UnitCommitment.jl: Optimization Package for Security-Constrained Unit
Commitment <br />
Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
</div>
);
}
export default Footer;

View File

@@ -0,0 +1,41 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.HeaderBox {
background-color: var(--contrast-0);
border-bottom: var(--box-border);
box-shadow: var(--box-shadow);
padding: 0;
margin: 0;
}
.HeaderContent {
margin: 0 auto;
max-width: var(--site-max-width);
min-width: var(--site-min-width);
}
.HeaderContent h1,
h2 {
color: var(--contrast-100);
display: inline-block;
line-height: 48px;
font-size: 28px;
margin: 0;
padding: 12px 24px;
}
.HeaderContent h2 {
display: inline-block;
font-size: 22px;
color: var(--contrast-80);
font-weight: normal;
}
.buttonContainer {
float: right;
padding: 16px 12px;
}

View File

@@ -0,0 +1,46 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./Header.module.css";
import SiteHeaderButton from "../Common/Buttons/SiteHeaderButton";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { UnitCommitmentScenario } from "../../core/Data/types";
interface HeaderProps {
onClear: () => void;
onSave: () => void;
onUndo: () => void;
onLoad: (data: UnitCommitmentScenario) => void;
}
function Header(props: HeaderProps) {
const fileElem = useRef<FileUploadElement>(null);
function onLoad() {
fileElem.current!.showFilePicker((data: any) => {
props.onLoad(data);
});
}
return (
<div className={styles.HeaderBox}>
<div className={styles.HeaderContent}>
<h1>UnitCommitment.jl</h1>
<h2>Case Builder</h2>
<div className={styles.buttonContainer}>
<SiteHeaderButton title="Undo" onClick={props.onUndo} />
<SiteHeaderButton title="Clear" onClick={props.onClear} />
<SiteHeaderButton title="Load" onClick={onLoad} />
<SiteHeaderButton title="Save" onClick={props.onSave} />
</div>
<FileUploadElement ref={fileElem} accept=".json,.json.gz" />
</div>
</div>
);
}
export default Header;

View File

@@ -0,0 +1,71 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import Form from "../Common/Forms/Form";
import TextInputRow from "../Common/Forms/TextInputRow";
import {
changeParameter,
changeTimeHorizon,
changeTimeStep,
} from "../../core/Operations/parameterOps";
import { UnitCommitmentScenario } from "../../core/Data/types";
interface ParametersProps {
scenario: UnitCommitmentScenario;
onError: (msg: string) => void;
onDataChanged: (scenario: UnitCommitmentScenario) => void;
}
function Parameters(props: ParametersProps) {
const onDataChanged = (key: string, value: string) => {
let newScenario, err;
if (key === "Time horizon (h)") {
[newScenario, err] = changeTimeHorizon(props.scenario, value);
} else if (key === "Time step (min)") {
[newScenario, err] = changeTimeStep(props.scenario, value);
} else {
[newScenario, err] = changeParameter(props.scenario, key, value);
}
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Parameters"></SectionHeader>
<Form>
<TextInputRow
label="Time horizon"
unit="h"
tooltip="Length of the planning horizon (in hours)."
initialValue={`${props.scenario.Parameters["Time horizon (h)"]}`}
onChange={(v) => onDataChanged("Time horizon (h)", v)}
/>
<TextInputRow
label="Time step"
unit="min"
tooltip="Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc)."
initialValue={`${props.scenario.Parameters["Time step (min)"]}`}
onChange={(v) => onDataChanged("Time step (min)", v)}
/>
<TextInputRow
label="Power balance penalty"
unit="$/MW"
tooltip="Penalty for system-wide shortage or surplus in production (in /MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged."
initialValue={`${props.scenario.Parameters["Power balance penalty ($/MW)"]}`}
onChange={(v) => onDataChanged("Power balance penalty ($/MW)", v)}
/>
</Form>
</div>
);
}
export default Parameters;

View File

@@ -0,0 +1,111 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import {
floatFormatter,
generateTableColumns,
parseCsv,
} from "../Common/Forms/DataTable";
import {
parseProfiledUnitsCsv,
ProfiledUnitsColumnSpec,
} from "./ProfiledUnits";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
import assert from "node:assert";
import {
getProfiledGenerators,
getThermalGenerators,
} from "../../core/Data/types";
test("parse CSV", () => {
const csvContents =
"Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," +
"Maximum power (MW) 02:00,Maximum power (MW) 03:00," +
"Maximum power (MW) 04:00,Minimum power (MW) 00:00," +
"Minimum power (MW) 01:00,Minimum power (MW) 02:00," +
"Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" +
"pu1,b1,50,260.25384545,72.89148068,377.17886108,336.66732361," +
"376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" +
"pu2,b1,0,0,0,0,0,0,0,0,0,0,0";
const [scenario, err] = parseProfiledUnitsCsv(csvContents, TEST_DATA_1);
assert(err === null);
const thermalGens = getThermalGenerators(scenario);
const profGens = getProfiledGenerators(scenario);
assert.equal(Object.keys(thermalGens).length, 1);
assert.equal(Object.keys(profGens).length, 2);
assert.deepEqual(profGens, {
pu1: {
Bus: "b1",
"Minimum power (MW)": [
52.05076909, 14.57829614, 75.43577222, 67.33346472, 75.36556352,
],
"Maximum power (MW)": [
260.25384545, 72.89148068, 377.17886108, 336.66732361, 376.82781758,
],
"Cost ($/MW)": 50.0,
Type: "Profiled",
},
pu2: {
Bus: "b1",
"Minimum power (MW)": [0, 0, 0, 0, 0],
"Maximum power (MW)": [0, 0, 0, 0, 0],
"Cost ($/MW)": 0.0,
Type: "Profiled",
},
});
});
test("parse CSV with invalid bus", () => {
const csvContents =
"Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," +
"Maximum power (MW) 02:00,Maximum power (MW) 03:00," +
"Maximum power (MW) 04:00,Minimum power (MW) 00:00," +
"Minimum power (MW) 01:00,Minimum power (MW) 02:00," +
"Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" +
"pu1,b99,50,260.25384545,72.89148068,377.17886108,336.66732361," +
"376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" +
"pu2,b1,0,0,0,0,0,0,0,0,0,0,0";
const [, err] = parseCsv(csvContents, ProfiledUnitsColumnSpec, TEST_DATA_1);
assert(err !== null);
assert.equal(err.message, 'Bus "b99" does not exist (row 1)');
});
test("generateTableColumns", () => {
const columns = generateTableColumns(TEST_DATA_1, ProfiledUnitsColumnSpec);
assert.equal(columns.length, 5);
assert.deepEqual(columns[0], {
editor: "input",
editorParams: {
selectContents: true,
},
field: "Name",
formatter: "plaintext",
headerHozAlign: "left",
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 100,
resizable: false,
title: "Name",
});
assert.equal(columns[3]!["columns"]!.length, 5);
assert.deepEqual(columns[3]!["columns"]![0], {
editor: "input",
editorParams: {
selectContents: true,
},
field: "Maximum power (MW) 00:00",
formatter: floatFormatter,
headerHozAlign: "left",
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 75,
resizable: false,
title: "00:00",
});
});

View File

@@ -0,0 +1,197 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { ColumnDefinition } from "tabulator-tables";
import { offerDownload } from "../Common/io";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { useRef } from "react";
import {
changeProfiledUnitData,
createProfiledUnit,
deleteGenerator,
renameGenerator,
} from "../../core/Operations/generatorOps";
import { ValidationError } from "../../core/Data/validate";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/Data/types";
export const ProfiledUnitsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Bus",
type: "busRef",
width: 100,
},
{
title: "Cost ($/MW)",
type: "number",
width: 100,
},
{
title: "Maximum power (MW)",
type: "number[T]",
width: 75,
},
{
title: "Minimum power (MW)",
type: "number[T]",
width: 75,
},
];
const generateProfiledUnitsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, ProfiledUnitsColumnSpec);
const data = generateTableData(
getProfiledGenerators(scenario),
ProfiledUnitsColumnSpec,
scenario,
);
return [data, columns];
};
export const parseProfiledUnitsCsv = (
csvContents: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [profGens, err] = parseCsv(
csvContents,
ProfiledUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
// Process imported generators
for (const gen in profGens) {
profGens[gen]["Type"] = "Profiled";
}
// Merge with existing data
const thermalGens = getThermalGenerators(scenario);
const newScenario = {
...scenario,
Generators: { ...thermalGens, ...profGens },
};
return [newScenario, null];
};
const ProfiledUnitsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generateProfiledUnitsData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "profiled_units.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csv: any) => {
const [newScenario, err] = parseProfiledUnitsCsv(csv, props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
});
};
const onAdd = () => {
const [newScenario, err] = createProfiledUnit(props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
};
const onDelete = (name: string): ValidationError | null => {
const newScenario = deleteGenerator(name, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onDataChanged = (
name: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changeProfiledUnitData(
name,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renameGenerator(
oldName,
newName,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Profiled units">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generateProfiledUnitsData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default ProfiledUnitsComponent;

View File

@@ -0,0 +1,175 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { ValidationError } from "../../core/Data/validate";
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import { UnitCommitmentScenario } from "../../core/Data/types";
import { ColumnDefinition } from "tabulator-tables";
import {
changePriceSensitiveLoadData,
createPriceSensitiveLoad,
deletePriceSensitiveLoad,
renamePriceSensitiveLoad,
} from "../../core/Operations/psloadOps";
import { offerDownload } from "../Common/io";
export const PriceSensitiveLoadsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Bus",
type: "busRef",
width: 100,
},
{
title: "Revenue ($/MW)",
type: "number",
width: 100,
},
{
title: "Demand (MW)",
type: "number[T]",
width: 70,
},
];
export const generatePriceSensitiveLoadsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, PriceSensitiveLoadsColumnSpec);
const data = generateTableData(
scenario["Price-sensitive loads"],
PriceSensitiveLoadsColumnSpec,
scenario,
);
return [data, columns];
};
const PriceSensitiveLoadsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generatePriceSensitiveLoadsData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "psloads.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csv: any) => {
// Parse provided CSV file
const [psloads, err] = parseCsv(
csv,
PriceSensitiveLoadsColumnSpec,
props.scenario,
);
// Handle validation errors
if (err) {
props.onError(err.message);
return;
}
// Generate new scenario
props.onDataChanged({
...props.scenario,
"Price-sensitive loads": psloads,
});
});
};
const onAdd = () => {
const [newScenario, err] = createPriceSensitiveLoad(props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
};
const onDelete = (name: string): ValidationError | null => {
const newScenario = deletePriceSensitiveLoad(name, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onDataChanged = (
name: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changePriceSensitiveLoadData(
name,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renamePriceSensitiveLoad(
oldName,
newName,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Price-sensitive loads">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generatePriceSensitiveLoadsData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default PriceSensitiveLoadsComponent;

View File

@@ -0,0 +1,235 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { ValidationError } from "../../core/Data/validate";
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import { UnitCommitmentScenario } from "../../core/Data/types";
import { ColumnDefinition } from "tabulator-tables";
import {
changeStorageUnitData,
createStorageUnit,
deleteStorageUnit,
renameStorageUnit,
} from "../../core/Operations/storageOps";
import { offerDownload } from "../Common/io";
export const StorageUnitsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Bus",
type: "busRef",
width: 100,
},
{
title: "Minimum level (MWh)",
type: "number",
width: 100,
},
{
title: "Maximum level (MWh)",
type: "number",
width: 100,
},
{
title: "Charge cost ($/MW)",
type: "number",
width: 100,
},
{
title: "Discharge cost ($/MW)",
type: "number",
width: 100,
},
{
title: "Charge efficiency",
type: "number",
width: 100,
},
{
title: "Discharge efficiency",
type: "number",
width: 100,
},
{
title: "Loss factor",
type: "number",
width: 80,
},
{
title: "Minimum charge rate (MW)",
type: "number",
width: 140,
},
{
title: "Maximum charge rate (MW)",
type: "number",
width: 140,
},
{
title: "Minimum discharge rate (MW)",
type: "number",
width: 140,
},
{
title: "Maximum discharge rate (MW)",
type: "number",
width: 150,
},
{
title: "Initial level (MWh)",
type: "number",
width: 100,
},
{
title: "Last period minimum level (MWh)",
type: "number",
width: 160,
},
{
title: "Last period maximum level (MWh)",
type: "number",
width: 160,
},
];
export const generateStorageUnitsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, StorageUnitsColumnSpec);
const data = generateTableData(
scenario["Storage units"],
StorageUnitsColumnSpec,
scenario,
);
return [data, columns];
};
const StorageUnitsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generateStorageUnitsData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "storage_units.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csv: any) => {
// Parse provided CSV file
const [storageUnits, err] = parseCsv(
csv,
StorageUnitsColumnSpec,
props.scenario,
);
// Handle validation errors
if (err) {
props.onError(err.message);
return;
}
// Generate new scenario
props.onDataChanged({
...props.scenario,
"Storage units": storageUnits,
});
});
};
const onAdd = () => {
const [newScenario, err] = createStorageUnit(props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
};
const onDelete = (name: string): ValidationError | null => {
const newScenario = deleteStorageUnit(name, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onDataChanged = (
name: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changeStorageUnitData(
name,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renameStorageUnit(
oldName,
newName,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Storage units">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generateStorageUnitsData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default StorageUnitsComponent;

View File

@@ -0,0 +1,209 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import {
floatFormatter,
generateCsv,
generateTableColumns,
generateTableData,
} from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
import {
generateThermalUnitsData,
parseThermalUnitsCsv,
ThermalUnitsColumnSpec,
} from "./ThermalUnits";
import assert from "node:assert";
import {
getProfiledGenerators,
getThermalGenerators,
} from "../../core/Data/types";
test("generateTableColumns", () => {
const columns = generateTableColumns(TEST_DATA_1, ThermalUnitsColumnSpec);
assert.equal(columns[2]!["columns"]!.length, 10);
assert.deepEqual(columns[2]!["columns"]![0], {
editor: "input",
editorParams: {
selectContents: true,
},
field: "Production cost curve (MW) 1",
formatter: floatFormatter,
headerHozAlign: "left",
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 80,
resizable: false,
title: "1",
});
});
test("generateTableData", () => {
const data = generateTableData(
getThermalGenerators(TEST_DATA_1),
ThermalUnitsColumnSpec,
TEST_DATA_1,
);
assert.deepEqual(data[0], {
Name: "g1",
Bus: "b1",
"Initial power (MW)": 115,
"Initial status (h)": 12,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Ramp down limit (MW)": 232.68,
"Ramp up limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Production cost curve ($) 1": 1400,
"Production cost curve ($) 2": 1600,
"Production cost curve ($) 3": 2200,
"Production cost curve ($) 4": 2400,
"Production cost curve ($) 5": "",
"Production cost curve ($) 6": "",
"Production cost curve ($) 7": "",
"Production cost curve ($) 8": "",
"Production cost curve ($) 9": "",
"Production cost curve ($) 10": "",
"Production cost curve (MW) 1": 100,
"Production cost curve (MW) 2": 110,
"Production cost curve (MW) 3": 130,
"Production cost curve (MW) 4": 135,
"Production cost curve (MW) 5": "",
"Production cost curve (MW) 6": "",
"Production cost curve (MW) 7": "",
"Production cost curve (MW) 8": "",
"Production cost curve (MW) 9": "",
"Production cost curve (MW) 10": "",
"Startup costs ($) 1": 300,
"Startup costs ($) 2": 400,
"Startup costs ($) 3": "",
"Startup costs ($) 4": "",
"Startup costs ($) 5": "",
"Startup delays (h) 1": 1,
"Startup delays (h) 2": 4,
"Startup delays (h) 3": "",
"Startup delays (h) 4": "",
"Startup delays (h) 5": "",
"Must run?": false,
});
});
const expectedCsvContents =
"Name,Bus," +
"Production cost curve (MW) 1," +
"Production cost curve (MW) 2," +
"Production cost curve (MW) 3," +
"Production cost curve (MW) 4," +
"Production cost curve (MW) 5," +
"Production cost curve (MW) 6," +
"Production cost curve (MW) 7," +
"Production cost curve (MW) 8," +
"Production cost curve (MW) 9," +
"Production cost curve (MW) 10," +
"Production cost curve ($) 1," +
"Production cost curve ($) 2," +
"Production cost curve ($) 3," +
"Production cost curve ($) 4," +
"Production cost curve ($) 5," +
"Production cost curve ($) 6," +
"Production cost curve ($) 7," +
"Production cost curve ($) 8," +
"Production cost curve ($) 9," +
"Production cost curve ($) 10," +
"Startup costs ($) 1," +
"Startup costs ($) 2," +
"Startup costs ($) 3," +
"Startup costs ($) 4," +
"Startup costs ($) 5," +
"Startup delays (h) 1," +
"Startup delays (h) 2," +
"Startup delays (h) 3," +
"Startup delays (h) 4," +
"Startup delays (h) 5," +
"Minimum uptime (h),Minimum downtime (h),Ramp up limit (MW)," +
"Ramp down limit (MW),Startup limit (MW),Shutdown limit (MW)," +
"Initial status (h),Initial power (MW),Must run?\n" +
"g1,b1,100,110,130,135,,,,,,,1400,1600,2200,2400,,,,,,,300,400,,,,1,4,,,,4,4,232.68,232.68,232.68,232.68,12,115,false";
const invalidCsv =
"Name,Bus," +
"Production cost curve (MW) 1," +
"Production cost curve (MW) 2," +
"Production cost curve (MW) 3," +
"Production cost curve (MW) 4," +
"Production cost curve (MW) 5," +
"Production cost curve (MW) 6," +
"Production cost curve (MW) 7," +
"Production cost curve (MW) 8," +
"Production cost curve (MW) 9," +
"Production cost curve (MW) 10," +
"Production cost curve ($) 1," +
"Production cost curve ($) 2," +
"Production cost curve ($) 3," +
"Production cost curve ($) 4," +
"Production cost curve ($) 5," +
"Production cost curve ($) 6," +
"Production cost curve ($) 7," +
"Production cost curve ($) 8," +
"Production cost curve ($) 9," +
"Production cost curve ($) 10," +
"Startup costs ($) 1," +
"Startup costs ($) 2," +
"Startup costs ($) 3," +
"Startup costs ($) 4," +
"Startup costs ($) 5," +
"Startup delays (h) 1," +
"Startup delays (h) 2," +
"Startup delays (h) 3," +
"Startup delays (h) 4," +
"Startup delays (h) 5," +
"Minimum uptime (h),Minimum downtime (h),Ramp up limit (MW)," +
"Ramp down limit (MW),Startup limit (MW),Shutdown limit (MW)," +
"Initial status (h),Initial power (MW),Must run?\n" +
"g1,b1,100,110,130,x,,,,,,,1400,1600,2200,2400,,,,,,,300,400,,,,1,4,,,,4,4,232.68,232.68,232.68,232.68,12,115,false";
test("generateCSV", () => {
const [data, columns] = generateThermalUnitsData(TEST_DATA_1);
const actualCsvContents = generateCsv(data, columns);
assert.equal(actualCsvContents, expectedCsvContents);
});
test("parseCSV", () => {
const [scenario, err] = parseThermalUnitsCsv(
expectedCsvContents,
TEST_DATA_1,
);
assert(!err);
const thermalGens = getThermalGenerators(scenario);
const profGens = getProfiledGenerators(scenario);
assert.equal(Object.keys(thermalGens).length, 1);
assert.equal(Object.keys(profGens).length, 2);
assert.deepEqual(thermalGens["g1"], {
Bus: "b1",
Type: "Thermal",
"Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0],
"Startup costs ($)": [300.0, 400.0],
"Startup delays (h)": [1, 4],
"Ramp up limit (MW)": 232.68,
"Ramp down limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Initial status (h)": 12,
"Initial power (MW)": 115,
"Must run?": false,
});
});
test("parseCSV with invalid number[T]", () => {
const [, err] = parseThermalUnitsCsv(invalidCsv, TEST_DATA_1);
assert(err);
assert.equal(err.message, '"x" is not a valid number (row 1)');
});

View File

@@ -0,0 +1,251 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { ValidationError } from "../../core/Data/validate";
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import { ColumnDefinition } from "tabulator-tables";
import { offerDownload } from "../Common/io";
import {
changeThermalUnitData,
createThermalUnit,
deleteGenerator,
renameGenerator,
} from "../../core/Operations/generatorOps";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/Data/types";
export const ThermalUnitsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Bus",
type: "busRef",
width: 100,
},
{
title: "Production cost curve (MW)",
type: "number[N]",
length: 10,
width: 80,
},
{
title: "Production cost curve ($)",
type: "number[N]",
length: 10,
width: 80,
},
{
title: "Startup costs ($)",
type: "number[N]",
length: 5,
width: 75,
},
{
title: "Startup delays (h)",
type: "number[N]",
length: 5,
width: 60,
},
{
title: "Minimum uptime (h)",
type: "number",
width: 80,
},
{
title: "Minimum downtime (h)",
type: "number",
width: 100,
},
{
title: "Ramp up limit (MW)",
type: "number?",
width: 100,
},
{
title: "Ramp down limit (MW)",
type: "number?",
width: 100,
},
{
title: "Startup limit (MW)",
type: "number?",
width: 80,
},
{
title: "Shutdown limit (MW)",
type: "number?",
width: 100,
},
{
title: "Initial status (h)",
type: "number",
width: 80,
},
{
title: "Initial power (MW)",
type: "number",
width: 100,
},
{
title: "Must run?",
type: "boolean",
width: 80,
},
];
export const generateThermalUnitsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, ThermalUnitsColumnSpec);
const data = generateTableData(
getThermalGenerators(scenario),
ThermalUnitsColumnSpec,
scenario,
);
return [data, columns];
};
export const parseThermalUnitsCsv = (
csvContents: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [thermalGens, err] = parseCsv(
csvContents,
ThermalUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
// Process imported generators
for (const gen in thermalGens) {
thermalGens[gen]["Type"] = "Thermal";
}
// Merge with existing data
const profGens = getProfiledGenerators(scenario);
const newScenario = {
...scenario,
Generators: { ...thermalGens, ...profGens },
};
return [newScenario, null];
};
const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generateThermalUnitsData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "thermal_units.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csv: any) => {
const [newScenario, err] = parseThermalUnitsCsv(csv, props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
});
};
const onAdd = () => {
const [newScenario, err] = createThermalUnit(props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
};
const onDelete = (name: string): ValidationError | null => {
const newScenario = deleteGenerator(name, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onDataChanged = (
name: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changeThermalUnitData(
name,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renameGenerator(
oldName,
newName,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Thermal units">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generateThermalUnitsData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default ThermalUnitsComponent;

View File

@@ -0,0 +1,203 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { ColumnDefinition } from "tabulator-tables";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { useRef } from "react";
import { ValidationError } from "../../core/Data/validate";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import {
changeTransmissionLineData,
createTransmissionLine,
deleteTransmissionLine,
rebuildContingencies,
renameTransmissionLine,
} from "../../core/Operations/transmissionOps";
import { offerDownload } from "../Common/io";
import { UnitCommitmentScenario } from "../../core/Data/types";
export const TransmissionLinesColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Source bus",
type: "busRef",
width: 100,
},
{
title: "Target bus",
type: "busRef",
width: 100,
},
{
title: "Susceptance (S)",
type: "number",
width: 60,
},
{
title: "Normal flow limit (MW)",
type: "number?",
width: 60,
},
{
title: "Emergency flow limit (MW)",
type: "number?",
width: 60,
},
{
title: "Flow limit penalty ($/MW)",
type: "number",
width: 60,
},
{
title: "Contingency?",
type: "lineContingency",
width: 50,
},
];
const generateTransmissionLinesData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, TransmissionLinesColumnSpec);
const data = generateTableData(
scenario["Transmission lines"],
TransmissionLinesColumnSpec,
scenario,
);
return [data, columns];
};
const TransmissionLinesComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
const onDownload = () => {
const [data, columns] = generateTransmissionLinesData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "transmission.csv");
};
const onUpload = () => {
fileUploadElem.current!.showFilePicker((csv: any) => {
// Parse the CSV data
const [newLines, err] = parseCsv(
csv,
TransmissionLinesColumnSpec,
props.scenario,
);
if (err) {
props.onError(err.message);
return;
}
// Remove contingency field from line and rebuild the contingencies section
const lineContingencies = new Set<String>();
Object.entries(newLines).forEach(([lineName, line]: [string, any]) => {
if (line["Contingency?"]) lineContingencies.add(lineName);
delete line["Contingency?"];
});
const contingencies = rebuildContingencies(lineContingencies);
const newScenario = {
...props.scenario,
"Transmission lines": newLines,
Contingencies: contingencies,
};
props.onDataChanged(newScenario);
});
};
const onAdd = () => {
const [newScenario, err] = createTransmissionLine(props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
};
const onDelete = (name: string): ValidationError | null => {
const newScenario = deleteTransmissionLine(name, props.scenario);
props.onDataChanged(newScenario);
return null;
};
const onDataChanged = (
name: string,
field: string,
newValue: string,
): ValidationError | null => {
const [newScenario, err] = changeTransmissionLineData(
name,
field,
newValue,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
const onRename = (
oldName: string,
newName: string,
): ValidationError | null => {
const [newScenario, err] = renameTransmissionLine(
oldName,
newName,
props.scenario,
);
if (err) {
props.onError(err.message);
return err;
}
props.onDataChanged(newScenario);
return null;
};
return (
<div>
<SectionHeader title="Transmission lines">
<SectionButton icon={faPlus} tooltip="Add" onClick={onAdd} />
<SectionButton
icon={faDownload}
tooltip="Download"
onClick={onDownload}
/>
<SectionButton icon={faUpload} tooltip="Upload" onClick={onUpload} />
</SectionHeader>
<DataTable
onRowDeleted={onDelete}
onRowRenamed={onRename}
onDataChanged={onDataChanged}
generateData={() => generateTransmissionLinesData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default TransmissionLinesComponent;

View File

@@ -0,0 +1,58 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import pako from "pako";
import React, { Component } from "react";
class FileUploadElement extends Component<any> {
private inputRef = React.createRef<HTMLInputElement>();
private callback: (data: any) => void = () => {};
showFilePicker = (callback: (data: any) => void) => {
this.callback = callback;
this.inputRef.current?.click();
};
onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files![0]!;
let isCompressed = file.name.endsWith(".gz");
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
let content = e.target?.result;
if (isCompressed) {
const compressed = new Uint8Array(content as ArrayBuffer);
const decompressed = pako.inflate(compressed);
content = new TextDecoder().decode(decompressed);
}
this.callback(content as string);
this.callback = () => {};
};
if (isCompressed) {
reader.readAsArrayBuffer(file);
} else {
reader.readAsText(file);
}
}
event.target.value = "";
};
override render() {
return (
<input
ref={this.inputRef}
type="file"
accept={this.props.accept}
style={{ display: "none" }}
onChange={this.onFileSelected}
/>
);
}
}
export default FileUploadElement;

View File

@@ -0,0 +1,43 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.tooltip {
visibility: hidden;
background-color: var(--contrast-80);
color: var(--contrast-10);
opacity: 0;
width: 250px;
margin-top: 36px;
margin-left: -250px;
position: absolute;
z-index: 100;
font-size: 14px;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
line-height: 20px;
transition: opacity 0.5s;
font-weight: normal;
text-align: left;
padding: 6px 12px;
}
.icon {
color: var(--contrast-60);
font-size: 16px;
padding: 8px 8px 8px 0;
}
.HelpButton {
border: 0;
background-color: transparent;
cursor: pointer;
}
.HelpButton:hover .tooltip {
visibility: visible;
opacity: 100%;
transition: opacity 0.5s;
}

View File

@@ -0,0 +1,22 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./HelpButton.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
function HelpButton({ text }: { text: String }) {
return (
<button className={styles.HelpButton}>
<span className={styles.tooltip}>{text}</span>
<span className={styles.icon}>
<FontAwesomeIcon icon={faCircleQuestion} />
</span>
</button>
);
}
export default HelpButton;

View File

@@ -0,0 +1,26 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.SectionButton {
height: 48px;
width: 48px;
font-size: 16px;
border: 0;
background-color: transparent;
margin: 8px 0 8px 0px;
cursor: pointer;
color: var(--contrast-60);
}
.SectionButton:hover {
color: var(--contrast-100);
background-color: var(--contrast-20);
border-radius: var(--border-radius);
}
.SectionButton:active {
background-color: var(--contrast-60);
}

View File

@@ -0,0 +1,29 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./SectionButton.module.css";
interface SectionButtonProps {
icon: IconDefinition;
tooltip: string;
onClick?: () => void;
}
function SectionButton(props: SectionButtonProps) {
return (
<button
className={styles.SectionButton}
title={props.tooltip}
onClick={props.onClick}
>
<FontAwesomeIcon icon={props.icon} />
</button>
);
}
export default SectionButton;

View File

@@ -0,0 +1,28 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.SiteHeaderButton {
padding: 6px 24px;
margin: 0 0 0 8px;
line-height: 24px;
border: var(--box-border);
box-shadow: var(--box-shadow);
border-radius: var(--border-radius);
cursor: pointer;
color: var(--contrast-80);
text-transform: uppercase;
font-weight: bold;
font-size: 12px;
background: linear-gradient(var(--contrast-0) 25%, var(--contrast-10) 100%);
}
.SiteHeaderButton:hover {
background: rgb(245, 245, 245);
}
.SiteHeaderButton:active {
background: rgba(220, 220, 220);
}

View File

@@ -0,0 +1,23 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./SiteHeaderButton.module.css";
function SiteHeaderButton({
title,
onClick,
}: {
title: string;
onClick?: () => void;
}) {
return (
<button className={styles.SiteHeaderButton} onClick={onClick}>
{title}
</button>
);
}
export default SiteHeaderButton;

View File

@@ -0,0 +1,531 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { useEffect, useRef, useState } from "react";
import {
CellComponent,
ColumnDefinition,
TabulatorFull as Tabulator,
} from "tabulator-tables";
import { ValidationError } from "../../../core/Data/validate";
import Papa from "papaparse";
import {
parseBool,
parseNullableNumber,
parseNumber,
} from "../../../core/Operations/commonOps";
import { UnitCommitmentScenario } from "../../../core/Data/types";
import { getContingencyTransmissionLines } from "../../../core/Operations/transmissionOps";
export interface ColumnSpec {
title: string;
type:
| "string"
| "number"
| "number?"
| "number[N]"
| "number[T]"
| "busRef"
| "boolean"
| "lineContingency";
length?: number;
width: number;
}
export const generateTableColumns = (
scenario: UnitCommitmentScenario,
colSpecs: ColumnSpec[],
) => {
const timeSlots = generateTimeslots(scenario);
const columns: ColumnDefinition[] = [];
colSpecs.forEach((spec) => {
const subColumns: ColumnDefinition[] = [];
switch (spec.type) {
case "string":
case "busRef":
columns.push({
...columnsCommonAttrs,
title: spec.title,
field: spec.title,
minWidth: spec.width,
});
break;
case "boolean":
case "lineContingency":
columns.push({
...columnsCommonAttrs,
title: spec.title,
field: spec.title,
minWidth: spec.width,
editor: "list",
editorParams: {
values: [true, false],
},
});
break;
case "number":
case "number?":
columns.push({
...columnsCommonAttrs,
title: spec.title,
field: spec.title,
minWidth: spec.width,
formatter: floatFormatter,
});
break;
case "number[T]":
timeSlots.forEach((t) => {
subColumns.push({
...columnsCommonAttrs,
title: `${t}`,
field: `${spec.title} ${t}`,
minWidth: spec.width,
formatter: floatFormatter,
});
});
columns.push({
title: spec.title,
columns: subColumns,
});
break;
case "number[N]":
for (let i = 1; i <= spec.length!; i++) {
subColumns.push({
...columnsCommonAttrs,
title: `${i}`,
field: `${spec.title} ${i}`,
minWidth: spec.width,
formatter: floatFormatter,
});
}
columns.push({
title: spec.title,
columns: subColumns,
});
break;
default:
throw Error(`Unknown type: ${spec.type}`);
}
});
return columns;
};
export const generateTableData = (
container: any,
colSpecs: ColumnSpec[],
scenario: UnitCommitmentScenario,
): any[] => {
const data: any[] = [];
const timeslots = generateTimeslots(scenario);
let contingencyLines = null;
for (const [entryName, entryData] of Object.entries(container) as [
string,
any,
]) {
const entry: any = {};
for (const spec of colSpecs) {
if (spec.title === "Name") {
entry["Name"] = entryName;
continue;
}
switch (spec.type) {
case "string":
case "number":
case "number?":
case "boolean":
case "busRef":
entry[spec.title] = entryData[spec.title];
break;
case "lineContingency":
if (contingencyLines === null) {
contingencyLines = getContingencyTransmissionLines(scenario);
console.log(contingencyLines);
}
entry[spec.title] = contingencyLines.has(entryName);
break;
case "number[T]":
for (let i = 0; i < timeslots.length; i++) {
entry[`${spec.title} ${timeslots[i]}`] = entryData[spec.title][i];
}
break;
case "number[N]":
for (let i = 0; i < spec.length!; i++) {
let v = entryData[spec.title][i];
if (v === undefined || v === null) v = "";
entry[`${spec.title} ${i + 1}`] = v;
}
break;
default:
throw Error(`Unknown type: ${spec.type}`);
}
}
data.push(entry);
}
return data;
};
export const generateCsv = (data: any[], columns: ColumnDefinition[]) => {
const header: string[] = [];
const body: string[][] = data.map(() => []);
columns.forEach((column) => {
if (column.columns) {
column.columns.forEach((subcolumn) => {
header.push(subcolumn.field!);
for (let i = 0; i < data.length; i++) {
body[i]!.push(data[i]![subcolumn["field"]!]);
}
});
} else {
header.push(column.field!);
for (let i = 0; i < data.length; i++) {
body[i]!.push(data[i]![column["field"]!]);
}
}
});
const csvHeader = header.join(",");
const csvBody = body.map((row) => row.join(",")).join("\n");
return `${csvHeader}\n${csvBody}`;
};
export const parseCsv = (
csvContents: string,
colSpecs: ColumnSpec[],
scenario: UnitCommitmentScenario,
): [any, ValidationError | null] => {
// Parse contents
const csv = Papa.parse(csvContents, {
header: true,
skipEmptyLines: true,
transformHeader: (header) => header.trim(),
transform: (value) => value.trim(),
});
// Check for parsing errors
if (csv.errors.length > 0) {
console.error(csv.errors);
return [null, { message: "Could not parse CSV file" }];
}
// Check CSV headers
const columns = generateTableColumns(scenario, colSpecs);
const expectedHeader: string[] = [];
columns.forEach((column) => {
if (column.columns) {
column.columns.forEach((subcolumn) => {
expectedHeader.push(subcolumn.field!);
});
} else {
expectedHeader.push(column.field!);
}
});
const actualHeader = csv.meta.fields!;
for (let i = 0; i < expectedHeader.length; i++) {
if (expectedHeader[i] !== actualHeader[i]) {
return [
null,
{
message: `Invalid CSV: Header mismatch at column ${i + 1}.
Expected "${expectedHeader[i]}", found "${actualHeader[i]}"`,
},
];
}
}
// Parse each row
const timeslots = generateTimeslots(scenario);
const data: { [key: string]: any } = {};
for (let i = 0; i < csv.data.length; i++) {
const row = csv.data[i] as { [key: string]: any };
const rowRef = ` (row ${i + 1})`;
const name = row["Name"] as string;
if (name in data) {
return [null, { message: `Name "${name}" is duplicated` + rowRef }];
}
data[name] = {};
for (const spec of colSpecs) {
if (spec.title === "Name") continue;
switch (spec.type) {
case "string":
data[name][spec.title] = row[spec.title];
break;
case "number": {
const [val, err] = parseNumber(row[spec.title]);
if (err) return [null, { message: err.message + rowRef }];
data[name][spec.title] = val;
break;
}
case "number?": {
const [val, err] = parseNullableNumber(row[spec.title]);
if (err) return [null, { message: err.message + rowRef }];
data[name][spec.title] = val;
break;
}
case "busRef":
const busName = row[spec.title];
if (!(busName in scenario.Buses)) {
return [
null,
{ message: `Bus "${busName}" does not exist` + rowRef },
];
}
data[name][spec.title] = row[spec.title];
break;
case "number[T]": {
data[name][spec.title] = Array(timeslots.length);
for (let i = 0; i < timeslots.length; i++) {
const [vf, err] = parseNumber(row[`${spec.title} ${timeslots[i]}`]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title][i] = vf;
}
break;
}
case "number[N]": {
data[name][spec.title] = Array(spec.length).fill(0);
for (let i = 0; i < spec.length!; i++) {
let v = row[`${spec.title} ${i + 1}`];
if (v.trim() === "") {
data[name][spec.title].splice(i, spec.length! - i);
break;
} else {
const [vf, err] = parseNumber(row[`${spec.title} ${i + 1}`]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title][i] = vf;
}
}
break;
}
case "boolean":
case "lineContingency":
const [val, err] = parseBool(row[spec.title]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title] = val;
break;
default:
throw Error(`Unknown type: ${spec.type}`);
}
}
}
return [data, null];
};
export const floatFormatter = (cell: CellComponent) => {
const v = cell.getValue();
if (v === "" || v === null) {
return "&mdash;";
} else {
return parseFloat(cell.getValue()).toLocaleString("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
}
};
export const generateTimeslots = (scenario: UnitCommitmentScenario) => {
const timeHorizonHours = scenario["Parameters"]["Time horizon (h)"];
const timeStepMin = scenario["Parameters"]["Time step (min)"];
const timeslots: string[] = [];
for (
let m = 0, offset = 0;
m < timeHorizonHours * 60;
m += timeStepMin, offset += 1
) {
const hours = Math.floor(m / 60);
const mins = m % 60;
const formattedTime = `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
timeslots.push(formattedTime);
}
return timeslots;
};
export const columnsCommonAttrs: ColumnDefinition = {
headerHozAlign: "left",
hozAlign: "left",
title: "",
editor: "input",
editorParams: {
selectContents: true,
},
headerWordWrap: true,
formatter: "plaintext",
headerSort: false,
resizable: false,
};
interface DataTableProps {
onRowDeleted: (rowName: string) => ValidationError | null;
onRowRenamed: (
oldRowName: string,
newRowName: string,
) => ValidationError | null;
onDataChanged: (
rowName: string,
key: string,
newValue: string,
) => ValidationError | null;
generateData: () => [any[], ColumnDefinition[]];
}
function computeTableHeight(data: any[]): string {
const numRows = data.length;
const height = 70 + Math.max(Math.min(numRows, 15), 1) * 28;
return `${height}px`;
}
const DataTable = (props: DataTableProps) => {
const tableContainerRef = useRef<HTMLDivElement | null>(null);
const tableRef = useRef<Tabulator | null>(null);
const [isTableBuilt, setTableBuilt] = useState<Boolean>(false);
const [activeCell, setActiveCell] = useState<CellComponent | null>(null);
const [currentTableData, setCurrentTableData] = useState<any[]>([]);
useEffect(() => {
const onCellEdited = (cell: CellComponent) => {
let newValue = `${cell.getValue()}`;
let oldValue = `${cell.getOldValue()}`;
if (newValue === oldValue) return;
if (cell.getField() === "Name") {
if (newValue === "") {
const err = props.onRowDeleted(oldValue);
if (err) {
cell.restoreOldValue();
} else {
cell
.getRow()
.delete()
.then((r) => {});
}
} else {
const err = props.onRowRenamed(oldValue, newValue);
if (err) {
cell.restoreOldValue();
}
}
} else {
const row = cell.getRow().getData();
const bus = row["Name"];
const err = props.onDataChanged(bus, cell.getField(), newValue);
if (err) {
cell.restoreOldValue();
}
}
};
if (tableContainerRef.current === null) return;
const [data, columns] = props.generateData();
const height = computeTableHeight(data);
if (tableRef.current === null) {
tableRef.current = new Tabulator(tableContainerRef.current, {
layout: "fitColumns",
data: data,
columns: columns,
height: height,
index: "Name",
placeholder: "No data",
});
tableRef.current.on("tableBuilt", () => {
setTableBuilt(true);
});
}
if (isTableBuilt) {
const newHeight = height;
const newColumns = columns;
const newTableData = data;
const oldRows = tableRef.current.getRows();
const activeRowPosition = activeCell?.getRow().getPosition() as number;
const activeField = activeCell?.getField();
// Update data
if (newTableData.length === currentTableData.length) {
const updatedRows = newTableData.filter((_, i) => {
return (
JSON.stringify(newTableData[i]) !==
JSON.stringify(currentTableData[i])
);
});
if (updatedRows.length > 0) {
tableRef.current
.updateData(updatedRows)
.then(() => {})
.catch((e) => {
// WORKAROUND: Updating the same row twice triggers an exception.
// In that case, we just update the whole table.
console.log(e);
tableRef.current!!.replaceData(newTableData).then(() => {});
});
}
} else {
tableRef.current.replaceData(newTableData).then(() => {});
}
setCurrentTableData(newTableData);
// Restore active cell selection
if (activeCell) {
tableRef.current
?.getRowFromPosition(activeRowPosition!!)
?.getCell(activeField!!)
?.edit();
}
// Update columns
let newColCount = 0;
newColumns.forEach((col) => {
if (col.columns) newColCount += col.columns.length;
else newColCount += 1;
});
if (newColCount !== tableRef.current.getColumns().length) {
tableRef.current.setColumns(newColumns);
const rows = tableRef.current!.getRows()!;
const firstRow = rows[0];
if (firstRow) firstRow.scrollTo().then((r) => {});
}
// Update height
if (tableRef.current.options.height !== newHeight) {
tableRef.current.setHeight(newHeight);
}
// Scroll to bottom
if (tableRef.current.getRows().length === oldRows.length + 1) {
setTimeout(() => {
const rows = tableRef.current!.getRows()!;
const lastRow = rows[rows.length - 1]!;
lastRow.scrollTo().then((r) => {});
lastRow.getCell("Name").edit();
}, 10);
}
// Remove old callbacks
tableRef.current.off("cellEdited");
tableRef.current.off("cellEditing");
tableRef.current.off("cellEditCancelled");
// Set new callbacks
tableRef.current.on("cellEditing", (cell) => {
setActiveCell(cell);
});
tableRef.current.on("cellEditCancelled", (cell) => {
setActiveCell(null);
});
tableRef.current.on("cellEdited", (cell) => {
setActiveCell(null);
onCellEdited(cell);
});
}
}, [props, isTableBuilt]);
return (
<div className="tableWrapper">
<div ref={tableContainerRef} />
</div>
);
};
export default DataTable;

View File

@@ -0,0 +1,46 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.FormWrapper {
margin: 0 auto;
max-width: var(--site-max-width);
}
.Form {
background-color: var(--contrast-0);
border: var(--box-border);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
min-height: 48px;
margin: 0 12px;
min-width: var(--site-min-width);
max-height: 500px;
padding: 12px;
}
.FormRow {
display: flex;
line-height: 24px;
}
.FormRow label {
width: 350px;
padding: 6px 12px;
text-align: right;
}
.FormRow input {
flex: 1;
font-family: monospace;
border: var(--box-border);
border-radius: var(--border-radius);
padding: 4px;
margin: 2px 3px;
}
.FormRow_unit {
color: rgba(0, 0, 0, 0.4);
}

View File

@@ -0,0 +1,18 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ReactNode } from "react";
import styles from "./Form.module.css";
function Form({ children }: { children: ReactNode }) {
return (
<div className={styles.FormWrapper}>
<div className={styles.Form}>{children}</div>
</div>
);
}
export default Form;

View File

@@ -0,0 +1,96 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.tableWrapper {
margin: 0 auto;
max-width: var(--site-max-width);
}
.tabulator {
background-color: var(--contrast-0);
border: var(--box-border) !important;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
min-height: 48px;
min-width: var(--site-min-width);
padding: 0;
margin: 0 12px;
}
.tabulator .tabulator-header {
border-bottom: 1px solid #ccc;
font-size: 13px;
font-weight: bold;
color: var(--contrast-100);
line-height: 18px;
}
.tabulator .tabulator-header .subtitle {
color: var(--contrast-80);
font-weight: normal;
}
.tabulator .tabulator-header .tabulator-col {
border-right: 1px solid rgba(0, 0, 0, 0.1) !important;
vertical-align: middle !important;
}
.tabulator .tabulator-header .tabulator-col .tabulator-col-content {
text-align: left;
padding: 0 8px;
line-height: 24px;
}
.tabulator .tabulator-header .tabulator-col:last-child {
border-right: 1px solid rgba(0, 0, 0, 0.1) !important;
}
.tabulator-row .tabulator-cell {
font-family: monospace;
font-size: 12px;
line-height: 28px;
height: 28px;
text-align: right;
vertical-align: middle !important;
border-right: 1px solid rgba(0, 0, 0, 0.1) !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important;
padding: 0 8px;
}
.tabulator-row-even {
background-color: rgba(0, 0, 0, 0.03) !important;
}
.tabulator-row-odd {
background-color: rgba(0, 0, 0, 0) !important;
}
.tabulator-row .tabulator-cell.tabulator-editing {
border: 0;
padding: 0 4px;
background-color: #cee;
}
.tabulator-row .tabulator-cell.tabulator-editing input {
font-family: monospace;
text-align: left;
font-size: 12px;
}
.tabulator-col-group-cols {
font-size: 12px;
}
.tabulator-placeholder {
width: 100px !important;
}
.tabulator-placeholder * {
font-weight: normal !important;
font-size: 14px !important;
color: var(--contrast-60) !important;
}

View File

@@ -0,0 +1,58 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import formStyles from "./Form.module.css";
import HelpButton from "../Buttons/HelpButton";
import React, { useEffect, useRef, useState } from "react";
import { ValidationError } from "../../../core/Data/validate";
interface TextInputRowProps {
label: string;
unit: string;
tooltip: string;
initialValue: string;
onChange: (newValue: string) => ValidationError | null;
}
function TextInputRow(props: TextInputRowProps) {
const [savedValue, setSavedValue] = useState(props.initialValue);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = props.initialValue;
}
setSavedValue(props.initialValue);
}, [props.initialValue]);
const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const newValue = event.target.value;
if (newValue === savedValue) return;
const err = props.onChange(newValue);
if (err) {
inputRef.current!.value = savedValue;
return;
}
};
return (
<div className={formStyles.FormRow}>
<label>
{props.label}
<span className={formStyles.FormRow_unit}> ({props.unit})</span>
</label>
<input
ref={inputRef}
type="text"
defaultValue={savedValue}
onBlur={onBlur}
/>
<HelpButton text={props.tooltip} />
</div>
);
}
export default TextInputRow;

View File

@@ -0,0 +1,23 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.Toast {
width: 600px;
border-radius: var(--border-radius);
box-shadow: 4px 4px 16px -2px rgba(0, 0, 0, 0.5);
margin: 0 auto;
background-color: #424242;
color: white;
padding: 0 16px;
position: fixed;
top: 48px;
left: 50%;
transform: translate(-50%, 0);
transition: opacity 0.5s ease;
cursor: default;
font-size: 15px;
line-height: 48px;
}

View File

@@ -0,0 +1,35 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./Toast.module.css";
import { useEffect, useState } from "react";
interface ToastProps {
message: string;
}
const Toast = (props: ToastProps) => {
const [isVisible, setVisible] = useState(true);
useEffect(() => {
if (props.message.length === 0) return;
setVisible(true);
const timer = setTimeout(() => {
setVisible(false);
}, 5000);
return () => clearTimeout(timer);
}, [props.message]);
return (
<div>
<div className={styles.Toast} style={{ opacity: isVisible ? 1 : 0 }}>
{props.message}
</div>
</div>
);
};
export default Toast;

View File

@@ -0,0 +1,25 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
.SectionHeader {
max-width: var(--site-max-width);
min-width: var(--site-min-width);
margin: 0 auto;
color: var(--contrast-100);
}
.SectionHeader h1 {
margin: 0;
padding: 0 24px;
font-size: 16px;
line-height: 64px;
}
.SectionButtonsContainer {
float: right;
height: 64px;
margin-right: 12px;
}

View File

@@ -0,0 +1,24 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import styles from "./SectionHeader.module.css";
import { ReactNode } from "react";
interface SectionHeaderProps {
title: string;
children?: ReactNode;
}
function SectionHeader({ title, children }: SectionHeaderProps) {
return (
<div className={styles.SectionHeader}>
<div className={styles.SectionButtonsContainer}>{children}</div>
<h1>{title}</h1>
</div>
);
}
export default SectionHeader;

View File

@@ -0,0 +1,17 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
export function offerDownload(data: string, type: string, filename: string) {
const dataBlob = new Blob([data], { type: type });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}

View File

@@ -0,0 +1,132 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { UnitCommitmentScenario } from "./types";
export const TEST_DATA_1: UnitCommitmentScenario = {
Parameters: {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 5,
"Time step (min)": 60,
},
Buses: {
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
b2: { "Load (MW)": [14.03739, 13.48563, 13.11797, 12.9009, 13.03939] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
},
Generators: {
g1: {
Bus: "b1",
Type: "Thermal",
"Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0],
"Startup costs ($)": [300.0, 400.0],
"Startup delays (h)": [1, 4],
"Ramp up limit (MW)": 232.68,
"Ramp down limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Initial status (h)": 12,
"Initial power (MW)": 115,
"Must run?": false,
},
pu1: {
Bus: "b1",
Type: "Profiled",
"Cost ($/MW)": 12.5,
"Maximum power (MW)": [10, 12, 13, 15, 20],
"Minimum power (MW)": [0, 0, 0, 0, 0],
},
pu2: {
Bus: "b1",
Type: "Profiled",
"Cost ($/MW)": 120,
"Maximum power (MW)": [50, 50, 50, 50, 50],
"Minimum power (MW)": [0, 0, 0, 0, 0],
},
},
"Transmission lines": {
l1: {
"Source bus": "b1",
"Target bus": "b2",
"Susceptance (S)": 29.49686,
"Normal flow limit (MW)": 15000.0,
"Emergency flow limit (MW)": 20000.0,
"Flow limit penalty ($/MW)": 5000.0,
},
},
"Storage units": {
su1: {
Bus: "b1",
"Minimum level (MWh)": 10.0,
"Maximum level (MWh)": 100.0,
"Charge cost ($/MW)": 2.0,
"Discharge cost ($/MW)": 1.0,
"Charge efficiency": 0.8,
"Discharge efficiency": 0.85,
"Loss factor": 0.01,
"Minimum charge rate (MW)": 5.0,
"Maximum charge rate (MW)": 10.0,
"Minimum discharge rate (MW)": 4.0,
"Maximum discharge rate (MW)": 8.0,
"Initial level (MWh)": 20.0,
"Last period minimum level (MWh)": 21.0,
"Last period maximum level (MWh)": 22.0,
},
},
"Price-sensitive loads": {
ps1: {
Bus: "b3",
"Revenue ($/MW)": 23.0,
"Demand (MW)": [50, 50, 50, 50, 50],
},
},
Contingencies: {
l1: {
"Affected generators": [],
"Affected lines": ["l1"],
},
},
};
export const TEST_DATA_2: UnitCommitmentScenario = {
Parameters: {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 2,
"Time step (min)": 30,
},
Buses: {
b1: { "Load (MW)": [30, 30, 30, 30] },
b2: { "Load (MW)": [10, 20, 30, 40] },
b3: { "Load (MW)": [0, 30, 0, 40] },
},
Contingencies: {},
Generators: {},
"Transmission lines": {},
"Storage units": {},
"Price-sensitive loads": {},
};
export const TEST_DATA_BLANK: UnitCommitmentScenario = {
Parameters: {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 5,
"Time step (min)": 60,
},
Buses: {},
Contingencies: {},
Generators: {},
"Transmission lines": {},
"Storage units": {},
"Price-sensitive loads": {},
};
test("fixtures", () => {});

View File

@@ -0,0 +1,26 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { UnitCommitmentScenario } from "./types";
export interface Buses {
[busName: string]: { "Load (MW)": number[] };
}
export const BLANK_SCENARIO: UnitCommitmentScenario = {
Parameters: {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 24,
"Time step (min)": 60,
},
Buses: {},
Generators: {},
"Transmission lines": {},
"Storage units": {},
"Price-sensitive loads": {},
Contingencies: {},
};

View File

@@ -0,0 +1,34 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import assert from "node:assert";
import fs from "node:fs";
import pako from "pako";
import { migrateToV03, migrateToV04 } from "./migrate";
function readJsonGz(filename: string) {
const compressedData = fs.readFileSync(filename);
const decompressedData = pako.inflate(compressedData, { to: "string" });
return JSON.parse(decompressedData);
}
test("migrateToV03", () => {
const jsonData = readJsonGz("../test/fixtures/ucjl-0.2.json.gz");
migrateToV03(jsonData);
assert.deepEqual(jsonData.Reserves, {
r1: {
"Amount (MW)": 100,
"Shortfall penalty ($/MW)": 1000,
Type: "spinning",
},
});
});
test("migrateToV04", () => {
const jsonData = readJsonGz("../test/fixtures/ucjl-0.3.json.gz");
migrateToV04(jsonData);
assert.equal(jsonData.Generators["g1"].Type, "Thermal");
});

View File

@@ -0,0 +1,56 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ValidationError } from "./validate";
export const migrate = (json: any): ValidationError | null => {
const version = json.Parameters?.Version;
if (!version) {
return {
message:
"The provided input file cannot be loaded because it does not " +
"specify what version of UnitCommitment.jl it was written for.",
};
}
if (!["0.2", "0.3", "0.4"].includes(version)) {
return { message: `Unsupported file version: ${version}` };
}
if (version < "0.3") migrateToV03(json);
if (version < "0.4") migrateToV04(json);
json.Parameters.Version = "0.4";
return null;
};
export const migrateToV03 = (json: any): void => {
if (json.Reserves && json.Reserves["Spinning (MW)"] != null) {
const amount = json.Reserves["Spinning (MW)"];
json.Reserves = {
r1: {
Type: "spinning",
"Amount (MW)": amount,
},
};
if (json.Generators) {
for (const genName in json.Generators) {
const gen = json.Generators[genName];
if (gen["Provides spinning reserves?"] === true) {
gen["Reserve eligibility"] = ["r1"];
}
}
}
}
};
export const migrateToV04 = (json: any): void => {
if (json.Generators) {
for (const genName in json.Generators) {
const gen = json.Generators[genName];
if (gen.Type == null) {
gen.Type = "Thermal";
}
}
}
};

View File

@@ -0,0 +1,373 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
export const schema = {
$schema: "http://json-schema.org/draft-07/schema#",
title: "Schema for Unit Commitment Input File",
definitions: {
Parameters: {
type: "object",
properties: {
Version: {
type: "string",
const: "0.4",
description: "Version of UnitCommitment.jl",
},
"Time horizon (min)": {
type: "number",
exclusiveMinimum: 0,
description: "Length of the planning horizon in minutes",
},
"Time horizon (h)": {
type: "number",
exclusiveMinimum: 0,
description: "Length of the planning horizon in hours",
},
"Time step (min)": {
type: "number",
default: 60,
enum: [60, 30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1],
description: "Must be a divisor of 60",
},
"Power balance penalty ($/MW)": {
type: "number",
default: 1000.0,
minimum: 0,
description: "Penalty for system-wide shortage or surplus",
},
"Scenario name": {
type: "string",
default: "s1",
description: "Name of the scenario",
},
"Scenario weight": {
type: "number",
default: 1.0,
exclusiveMinimum: 0,
description: "Weight of the scenario",
},
},
required: ["Time step (min)", "Power balance penalty ($/MW)"],
oneOf: [
{ required: ["Time horizon (min)"] },
{ required: ["Time horizon (h)"] },
],
not: {
required: ["Time horizon (min)", "Time horizon (h)"],
},
},
Bus: {
type: "object",
additionalProperties: {
type: "object",
properties: {
"Load (MW)": {
oneOf: [
{ type: "null" },
{ type: "number" },
{
type: "array",
items: {
oneOf: [{ type: "number" }, { type: "null" }],
},
},
],
},
},
},
},
TransmissionLines: {
type: "object",
additionalProperties: {
type: "object",
properties: {
"Source bus": {
type: "string",
minLength: 1,
},
"Target bus": {
type: "string",
minLength: 1,
not: {
const: { $data: "1/Source bus" },
},
},
"Susceptance (S)": {
type: "number",
},
"Normal flow limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Emergency flow limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Flow limit penalty ($/MW)": {
type: "number",
minimum: 0,
default: 5000.0,
},
},
required: ["Source bus", "Target bus", "Susceptance (S)"],
},
},
StorageUnits: {
type: "object",
additionalProperties: {
type: "object",
properties: {
Bus: {
type: "string",
minLength: 1,
},
"Minimum level (MWh)": {
type: "number",
},
"Maximum level (MWh)": {
type: "number",
minimum: 0,
},
"Allow simultaneous charging and discharging": {
type: "boolean",
default: true,
},
"Charge cost ($/MW)": {
type: "number",
minimum: 0,
},
"Discharge cost ($/MW)": {
type: "number",
minimum: 0,
},
"Charge efficiency": {
type: "number",
minimum: 0,
maximum: 1,
},
"Discharge efficiency": {
type: "number",
minimum: 0,
maximum: 1,
},
"Loss factor": {
type: "number",
minimum: 0,
},
"Minimum charge rate (MW)": {
type: "number",
minimum: 0,
},
"Maximum charge rate (MW)": {
type: "number",
minimum: 0,
},
"Minimum discharge rate (MW)": {
type: "number",
minimum: 0,
},
"Maximum discharge rate (MW)": {
type: "number",
minimum: 0,
},
"Initial level (MWh)": {
type: "number",
minimum: 0,
},
"Last period minimum level (MWh)": {
type: "number",
minimum: 0,
},
"Last period maximum level (MWh)": {
type: "number",
minimum: 0,
},
},
required: ["Bus"],
},
},
Generators: {
type: "object",
additionalProperties: {
type: "object",
if: {
properties: {
Type: { const: "Thermal" },
},
},
then: {
properties: {
Bus: {
type: "string",
minLength: 1,
},
Type: {
type: "string",
const: "Thermal",
},
"Production cost curve (MW)": {
type: "array",
items: {
type: "number",
minimum: 0,
},
minItems: 1,
},
"Production cost curve ($)": {
type: "array",
items: {
type: "number",
minimum: 0,
},
minItems: 1,
},
"Startup costs ($)": {
type: "array",
items: {
type: "number",
minimum: 0,
},
default: [0.0],
},
"Startup delays (h)": {
type: "array",
items: {
type: "integer",
minimum: 1,
},
default: [1],
},
"Minimum uptime (h)": {
type: "integer",
default: 1,
minimum: 0,
},
"Minimum downtime (h)": {
type: "integer",
default: 1,
minimum: 0,
},
"Ramp up limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Ramp down limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Startup limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Shutdown limit (MW)": {
type: "number",
minimum: 0,
nullable: true,
default: null,
},
"Initial status (h)": {
type: "integer",
default: 1,
not: { const: 0 },
},
"Initial power (MW)": {
type: "number",
minimum: 0,
},
"Must run?": {
type: "boolean",
default: false,
},
},
required: [
"Bus",
"Type",
"Production cost curve (MW)",
"Production cost curve ($)",
"Initial status (h)",
"Initial power (MW)",
],
},
else: {
properties: {
Type: { const: "Profiled" },
Bus: {
type: "string",
minLength: 1,
},
"Maximum power (MW)": {
oneOf: [
{
type: "number",
},
{
type: "array",
items: {
type: "number",
},
},
],
},
"Cost ($/MW)": {
type: "number",
minimum: 0,
},
},
required: ["Type", "Bus", "Maximum power (MW)", "Cost ($/MW)"],
},
},
},
Contingencies: {
type: "object",
additionalProperties: {
type: "object",
properties: {
"Affected lines": {
type: "array",
items: {
type: "string",
},
maxItems: 1,
minItems: 1,
},
},
required: ["Affected lines"],
},
},
},
type: "object",
properties: {
Parameters: {
$ref: "#/definitions/Parameters",
},
Buses: {
$ref: "#/definitions/Bus",
},
"Transmission lines": {
$ref: "#/definitions/TransmissionLines",
},
"Storage units": {
$ref: "#/definitions/StorageUnits",
},
Generators: {
$ref: "#/definitions/Generators",
},
Contingencies: {
$ref: "#/definitions/Contingencies",
},
},
required: ["Parameters"],
};

View File

@@ -0,0 +1,119 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Buses } from "./fixtures";
export interface Generators {
[name: string]: ProfiledUnit | ThermalUnit;
}
export interface ProfiledUnit {
Bus: string;
Type: "Profiled";
"Minimum power (MW)": number[];
"Maximum power (MW)": number[];
"Cost ($/MW)": number;
}
export interface ThermalUnit {
Bus: string;
Type: "Thermal";
"Production cost curve (MW)": number[];
"Production cost curve ($)": number[];
"Startup costs ($)": number[];
"Startup delays (h)": number[];
"Ramp up limit (MW)": number | null;
"Ramp down limit (MW)": number | null;
"Startup limit (MW)": number | null;
"Shutdown limit (MW)": number | null;
"Minimum downtime (h)": number;
"Minimum uptime (h)": number;
"Initial status (h)": number;
"Initial power (MW)": number;
"Must run?": boolean;
}
export interface TransmissionLine {
"Source bus": string;
"Target bus": string;
"Susceptance (S)": number;
"Normal flow limit (MW)": number | null;
"Emergency flow limit (MW)": number | null;
"Flow limit penalty ($/MW)": number;
}
export interface StorageUnit {
Bus: string;
"Minimum level (MWh)": number;
"Maximum level (MWh)": number;
"Charge cost ($/MW)": number;
"Discharge cost ($/MW)": number;
"Charge efficiency": number;
"Discharge efficiency": number;
"Loss factor": number;
"Minimum charge rate (MW)": number;
"Maximum charge rate (MW)": number;
"Minimum discharge rate (MW)": number;
"Maximum discharge rate (MW)": number;
"Initial level (MWh)": number;
"Last period minimum level (MWh)": number;
"Last period maximum level (MWh)": number;
}
export interface PriceSensitiveLoad {
Bus: string;
"Revenue ($/MW)": number;
"Demand (MW)": number[];
}
export interface Contingency {
"Affected lines": string[];
"Affected generators": string[];
}
export interface UnitCommitmentScenario {
Parameters: {
Version: string;
"Power balance penalty ($/MW)": number;
"Time horizon (h)": number;
"Time step (min)": number;
};
Buses: Buses;
Generators: Generators;
"Transmission lines": {
[name: string]: TransmissionLine;
};
"Storage units": {
[name: string]: StorageUnit;
};
"Price-sensitive loads": {
[name: string]: PriceSensitiveLoad;
};
Contingencies: {
[name: string]: Contingency;
};
}
const getTypedGenerators = <T extends any>(
scenario: UnitCommitmentScenario,
type: string,
): {
[key: string]: T;
} => {
const selected: { [key: string]: T } = {};
for (const [name, gen] of Object.entries(scenario.Generators)) {
if (gen["Type"] === type) selected[name] = gen as T;
}
return selected;
};
export const getProfiledGenerators = (
scenario: UnitCommitmentScenario,
): { [key: string]: ProfiledUnit } =>
getTypedGenerators<ProfiledUnit>(scenario, "Profiled");
export const getThermalGenerators = (
scenario: UnitCommitmentScenario,
): { [key: string]: ThermalUnit } =>
getTypedGenerators<ThermalUnit>(scenario, "Thermal");

View File

@@ -0,0 +1,22 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { schema } from "./schema";
import Ajv from "ajv";
// Create Ajv instance with detailed debug options
const ajv = new Ajv({
useDefaults: true,
verbose: true,
allErrors: true,
$data: true,
});
export interface ValidationError {
message: string;
}
export const validate = ajv.compile(schema);

View File

@@ -0,0 +1,71 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { changeBusData, createBus, deleteBus, renameBus } from "./busOps";
import assert from "node:assert";
import { TEST_DATA_1 } from "../Data/fixtures.test";
test("createBus", () => {
const newScenario = createBus(TEST_DATA_1);
assert.deepEqual(Object.keys(newScenario.Buses), ["b1", "b2", "b3", "b4"]);
});
test("changeBusData", () => {
let scenario = TEST_DATA_1;
let err = null;
[scenario, err] = changeBusData("b1", "Load (MW) 00:00", "99", scenario);
assert.equal(err, null);
[scenario, err] = changeBusData("b1", "Load (MW) 03:00", "99", scenario);
assert.equal(err, null);
[scenario, err] = changeBusData("b3", "Load (MW) 04:00", "99", scenario);
assert.equal(err, null);
assert.deepEqual(scenario.Buses, {
b1: { "Load (MW)": [99, 34.38835, 33.45083, 99, 33.25044] },
b2: { "Load (MW)": [14.03739, 13.48563, 13.11797, 12.9009, 13.03939] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 99] },
});
});
test("changeBusData with invalid numbers", () => {
let [, err] = changeBusData("b1", "Load (MW) 00:00", "xx", TEST_DATA_1);
assert(err !== null);
assert.equal(err.message, '"xx" is not a valid number');
});
test("deleteBus", () => {
let scenario = TEST_DATA_1;
scenario = deleteBus("b2", scenario);
assert.deepEqual(scenario.Buses, {
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
});
});
test("renameBus", () => {
let [scenario, err] = renameBus("b1", "b99", TEST_DATA_1);
assert(err === null);
assert.deepEqual(scenario.Buses, {
b99: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
b2: { "Load (MW)": [14.03739, 13.48563, 13.11797, 12.9009, 13.03939] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
});
assert.deepEqual(scenario.Generators["pu1"], {
Bus: "b99",
Type: "Profiled",
"Cost ($/MW)": 12.5,
"Maximum power (MW)": [10, 12, 13, 15, 20],
"Minimum power (MW)": [0, 0, 0, 0, 0],
});
});
test("renameBus with duplicated name", () => {
let [, err] = renameBus("b3", "b1", TEST_DATA_1);
assert(err != null);
assert.equal(err.message, `b1 already exists`);
});

View File

@@ -0,0 +1,87 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Buses } from "../Data/fixtures";
import { ValidationError } from "../Data/validate";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import {
changeData,
generateUniqueName,
renameItemInObject,
} from "./commonOps";
import { BusesColumnSpec } from "../../components/CaseBuilder/Buses";
import { UnitCommitmentScenario } from "../Data/types";
export const createBus = (scenario: UnitCommitmentScenario) => {
const name = generateUniqueName(scenario.Buses, "b");
const timeslots = generateTimeslots(scenario);
return {
...scenario,
Buses: {
...scenario.Buses,
[name]: {
"Load (MW)": Array(timeslots.length).fill(0),
},
},
};
};
export const changeBusData = (
bus: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newBus, err] = changeData(
field,
newValueStr,
scenario.Buses[bus]!,
BusesColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
Buses: {
...scenario.Buses,
[bus]: newBus,
} as Buses,
},
null,
];
};
export const deleteBus = (bus: string, scenario: UnitCommitmentScenario) => {
const { [bus]: _, ...newBuses } = scenario.Buses;
const newGenerators = { ...scenario.Generators };
// Update generators
for (const genName in scenario.Generators) {
let gen = scenario.Generators[genName]!;
if (gen["Bus"] === bus) delete newGenerators[genName];
}
return { ...scenario, Buses: newBuses, Generators: newGenerators };
};
export const renameBus = (
oldName: string,
newName: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newBuses, err] = renameItemInObject(oldName, newName, scenario.Buses);
if (err) return [scenario, err];
// Update generators
const newGenerators = { ...scenario.Generators };
for (const genName in scenario.Generators) {
let gen = newGenerators[genName]!;
if (gen["Bus"] === oldName) {
newGenerators[genName] = { ...gen, Bus: newName };
}
}
return [{ ...scenario, Buses: newBuses, Generators: newGenerators }, null];
};

View File

@@ -0,0 +1,30 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { parseBool } from "./commonOps";
import assert from "node:assert";
test("parseBool", () => {
// True values
for (const str of ["true", "TRUE", "1"]) {
let [v, err] = parseBool(str);
assert(!err);
assert.equal(v, true);
}
// False values
for (const str of ["false", "FALSE", "0"]) {
let [v, err] = parseBool(str);
assert(!err);
assert.equal(v, false);
}
// Invalid values
for (const str of ["qwe", ""]) {
let [, err] = parseBool(str);
assert(err);
}
});

View File

@@ -0,0 +1,258 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ValidationError } from "../Data/validate";
import { ColumnSpec } from "../../components/Common/Forms/DataTable";
import { UnitCommitmentScenario } from "../Data/types";
export const renameItemInObject = <T>(
oldName: string,
newName: string,
container: { [key: string]: T },
): [{ [key: string]: T }, ValidationError | null] => {
if (newName in container) {
return [container, { message: `${newName} already exists` }];
}
const newContainer = Object.keys(container).reduce(
(acc, val) => {
if (val === oldName) {
acc[newName] = container[val]!;
} else {
acc[val] = container[val]!;
}
return acc;
},
{} as { [key: string]: T },
);
return [newContainer, null];
};
export const generateUniqueName = (container: any, prefix: string): string => {
let counter = 1;
let name = `${prefix}${counter}`;
while (name in container) {
counter++;
name = `${prefix}${counter}`;
}
return name;
};
export const parseNumber = (
valueStr: string,
): [number, ValidationError | null] => {
if (valueStr === "") {
return [0, { message: "Field must not be blank" }];
}
const valueFloat = parseFloat(valueStr);
if (isNaN(valueFloat)) {
return [0, { message: `"${valueStr}" is not a valid number` }];
} else {
return [valueFloat, null];
}
};
export const parseNullableNumber = (
valueStr: string,
): [number | null, ValidationError | null] => {
if (valueStr === "") return [null, null];
return parseNumber(valueStr);
};
export const parseBool = (
valueStr: string,
): [boolean, ValidationError | null] => {
if (["true", "1"].includes(valueStr.toLowerCase())) {
return [true, null];
}
if (["false", "0"].includes(valueStr.toLowerCase())) {
return [false, null];
}
return [true, { message: `"${valueStr}" is not a valid boolean value` }];
};
export const changeStringData = (
field: string,
newValue: string,
container: { [key: string]: any },
): [{ [key: string]: any }, ValidationError | null] => {
return [
{
...container,
[field]: newValue,
},
null,
];
};
export const changeBusRefData = (
field: string,
newValue: string,
container: { [key: string]: any },
scenario: UnitCommitmentScenario,
): [{ [key: string]: any }, ValidationError | null] => {
if (!(newValue in scenario.Buses)) {
return [scenario, { message: `Bus "${newValue}" does not exist` }];
}
return changeStringData(field, newValue, container);
};
export const changeNumberData = (
field: string,
newValueStr: string,
container: { [key: string]: any },
nullable: boolean = false,
): [{ [key: string]: any }, ValidationError | null] => {
// Parse value
const [newValueFloat, err] = nullable
? parseNullableNumber(newValueStr)
: parseNumber(newValueStr);
if (err) return [container, err];
// Build the new object
return [
{
...container,
[field]: newValueFloat,
},
null,
];
};
export const changeBooleanData = (
field: string,
newValueStr: string,
container: { [key: string]: any },
): [{ [key: string]: any }, ValidationError | null] => {
// Parse value
const [newValueBool, err] = parseBool(newValueStr);
if (err) return [container, err];
// Build the new object
return [
{
...container,
[field]: newValueBool,
},
null,
];
};
export const changeNumberVecTData = (
field: string,
time: string,
newValueStr: string,
container: { [key: string]: any },
scenario: UnitCommitmentScenario,
): [{ [key: string]: any }, ValidationError | null] => {
// Parse value
const [newValueFloat, err] = parseNumber(newValueStr);
if (err) return [container, err];
// Convert HH:MM to offset
const hours = parseInt(time.split(":")[0]!, 10);
const min = parseInt(time.split(":")[1]!, 10);
const idx = (hours * 60 + min) / scenario.Parameters["Time step (min)"];
// Build the new vector
const newVec = [...container[field]];
newVec[idx] = newValueFloat;
return [
{
...container,
[field]: newVec,
},
null,
];
};
export const changeNumberVecNData = (
field: string,
offset: string,
newValueStr: string,
container: { [key: string]: any },
): [{ [key: string]: any }, ValidationError | null] => {
const oldVec = container[field];
const newVec = [...container[field]];
const idx = parseInt(offset) - 1;
if (newValueStr === "") {
// Trim the vector
newVec.splice(idx, oldVec.length - idx);
} else {
// Parse new value
const [newValueFloat, err] = parseNumber(newValueStr);
if (err) return [container, err];
// Increase the length of the vector
if (idx >= oldVec.length) {
for (let i = oldVec.length; i < idx; i++) {
newVec[i] = 0;
}
}
// Assign new value
newVec[idx] = newValueFloat;
}
return [
{
...container,
[field]: newVec,
},
null,
];
};
export const changeData = (
field: string,
newValueStr: string,
container: { [key: string]: any },
colSpecs: ColumnSpec[],
scenario: UnitCommitmentScenario,
): [{ [key: string]: any }, ValidationError | null] => {
const match = field.match(/^([^0-9]+)([0-9:]+)?$/);
const fieldName = match![1]!.trim();
const fieldOffset = match![2];
for (const spec of colSpecs) {
if (spec.title !== fieldName) continue;
switch (spec.type) {
case "string":
return changeStringData(fieldName, newValueStr, container);
case "busRef":
return changeBusRefData(fieldName, newValueStr, container, scenario);
case "number":
return changeNumberData(fieldName, newValueStr, container);
case "number?":
return changeNumberData(fieldName, newValueStr, container, true);
case "number[T]":
return changeNumberVecTData(
fieldName,
fieldOffset!,
newValueStr,
container,
scenario,
);
case "number[N]":
return changeNumberVecNData(
fieldName,
fieldOffset!,
newValueStr,
container,
);
case "boolean":
return changeBooleanData(fieldName, newValueStr, container);
default:
throw Error(`Unknown type: ${spec.type}`);
}
}
throw Error(`Unknown field: ${fieldName}`);
};
export const assertBusesNotEmpty = (
scenario: UnitCommitmentScenario,
): ValidationError | null => {
if (Object.keys(scenario.Buses).length === 0)
return { message: "This component requires an existing bus." };
return null;
};

View File

@@ -0,0 +1,151 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { TEST_DATA_1, TEST_DATA_BLANK } from "../Data/fixtures.test";
import assert from "node:assert";
import {
changeProfiledUnitData,
changeThermalUnitData,
createProfiledUnit,
createThermalUnit,
deleteGenerator,
renameGenerator,
} from "./generatorOps";
import { ValidationError } from "../Data/validate";
test("createProfiledUnit", () => {
const [newScenario, err] = createProfiledUnit(TEST_DATA_1);
assert(err === null);
assert.equal(Object.keys(newScenario.Generators).length, 4);
assert("pu3" in newScenario.Generators);
});
test("createThermalUnit", () => {
const [newScenario, err] = createThermalUnit(TEST_DATA_1);
assert(err === null);
assert.equal(Object.keys(newScenario.Generators).length, 4);
assert("g2" in newScenario.Generators);
});
test("createProfiledUnit with blank file", () => {
const [, err] = createProfiledUnit(TEST_DATA_BLANK);
assert(err !== null);
assert.equal(err.message, "This component requires an existing bus.");
});
test("changeProfiledUnitData", () => {
let scenario = TEST_DATA_1;
let err: ValidationError | null;
[scenario, err] = changeProfiledUnitData(
"pu1",
"Cost ($/MW)",
"99",
scenario,
);
assert.equal(err, null);
[scenario, err] = changeProfiledUnitData(
"pu1",
"Maximum power (MW) 03:00",
"99",
scenario,
);
assert.equal(err, null);
[scenario, err] = changeProfiledUnitData("pu2", "Bus", "b3", scenario);
assert.equal(err, null);
assert.deepEqual(scenario.Generators["pu2"], {
Bus: "b3",
Type: "Profiled",
"Cost ($/MW)": 120,
"Maximum power (MW)": [50, 50, 50, 50, 50],
"Minimum power (MW)": [0, 0, 0, 0, 0],
});
});
test("changeThermalUnitData", () => {
let scenario = TEST_DATA_1;
let err: ValidationError | null;
[scenario, err] = changeThermalUnitData(
"g1",
"Ramp up limit (MW)",
"99",
scenario,
);
assert(!err);
[scenario, err] = changeThermalUnitData(
"g1",
"Startup costs ($) 2",
"99",
scenario,
);
assert(!err);
[scenario, err] = changeThermalUnitData(
"g1",
"Production cost curve ($) 7",
"99",
scenario,
);
assert(!err);
[scenario, err] = changeThermalUnitData(
"g1",
"Production cost curve (MW) 3",
"",
scenario,
);
assert(!err);
[scenario, err] = changeThermalUnitData("g1", "Must run?", "true", scenario);
assert(!err);
assert.deepEqual(scenario.Generators["g1"], {
Bus: "b1",
Type: "Thermal",
"Production cost curve (MW)": [100.0, 110],
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0, 0, 0, 99],
"Startup costs ($)": [300.0, 99.0],
"Startup delays (h)": [1, 4],
"Ramp up limit (MW)": 99,
"Ramp down limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Initial status (h)": 12,
"Initial power (MW)": 115,
"Must run?": true,
});
});
test("changeProfiledUnitData with invalid bus", () => {
let scenario = TEST_DATA_1;
let err = null;
[scenario, err] = changeProfiledUnitData("pu1", "Bus", "b99", scenario);
assert(err !== null);
assert.equal(err.message, 'Bus "b99" does not exist');
});
test("deleteGenerator", () => {
const newScenario = deleteGenerator("pu1", TEST_DATA_1);
assert.equal(Object.keys(newScenario.Generators).length, 2);
assert("g1" in newScenario.Generators);
assert("pu2" in newScenario.Generators);
});
test("renameGenerator", () => {
const [newScenario, err] = renameGenerator("pu1", "pu5", TEST_DATA_1);
assert(err === null);
assert.deepEqual(newScenario.Generators["pu5"], {
Bus: "b1",
Type: "Profiled",
"Cost ($/MW)": 12.5,
"Maximum power (MW)": [10, 12, 13, 15, 20],
"Minimum power (MW)": [0, 0, 0, 0, 0],
});
assert.deepEqual(newScenario.Generators["pu2"], {
Bus: "b1",
Type: "Profiled",
"Cost ($/MW)": 120,
"Maximum power (MW)": [50, 50, 50, 50, 50],
"Minimum power (MW)": [0, 0, 0, 0, 0],
});
});

View File

@@ -0,0 +1,152 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import { ValidationError } from "../Data/validate";
import {
assertBusesNotEmpty,
changeData,
generateUniqueName,
renameItemInObject,
} from "./commonOps";
import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits";
import { ThermalUnitsColumnSpec } from "../../components/CaseBuilder/ThermalUnits";
import { Generators, UnitCommitmentScenario } from "../Data/types";
export const createProfiledUnit = (
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const err = assertBusesNotEmpty(scenario);
if (err) return [scenario, err];
const busName = Object.keys(scenario.Buses)[0]!;
const timeslots = generateTimeslots(scenario);
const name = generateUniqueName(scenario.Generators, "pu");
return [
{
...scenario,
Generators: {
...scenario.Generators,
[name]: {
Bus: busName,
Type: "Profiled",
"Cost ($/MW)": 0,
"Minimum power (MW)": Array(timeslots.length).fill(0),
"Maximum power (MW)": Array(timeslots.length).fill(0),
},
},
},
null,
];
};
export const createThermalUnit = (
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const err = assertBusesNotEmpty(scenario);
if (err) return [scenario, err];
const busName = Object.keys(scenario.Buses)[0]!;
const name = generateUniqueName(scenario.Generators, "g");
return [
{
...scenario,
Generators: {
...scenario.Generators,
[name]: {
Bus: busName,
Type: "Thermal",
"Production cost curve (MW)": [0, 100],
"Production cost curve ($)": [0, 10],
"Startup costs ($)": [0],
"Startup delays (h)": [1],
"Ramp up limit (MW)": null,
"Ramp down limit (MW)": null,
"Startup limit (MW)": null,
"Shutdown limit (MW)": null,
"Minimum downtime (h)": 1,
"Minimum uptime (h)": 1,
"Initial status (h)": -24,
"Initial power (MW)": 0,
"Must run?": false,
},
},
},
null,
];
};
export const changeProfiledUnitData = (
generator: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newGen, err] = changeData(
field,
newValueStr,
scenario.Generators[generator]!,
ProfiledUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
Generators: {
...scenario.Generators,
[generator]: newGen,
} as Generators,
},
null,
];
};
export const changeThermalUnitData = (
generator: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newGen, err] = changeData(
field,
newValueStr,
scenario.Generators[generator]!,
ThermalUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
Generators: {
...scenario.Generators,
[generator]: newGen,
} as Generators,
},
null,
];
};
export const deleteGenerator = (
name: string,
scenario: UnitCommitmentScenario,
): UnitCommitmentScenario => {
const { [name]: _, ...newGenerators } = scenario.Generators;
return { ...scenario, Generators: newGenerators };
};
export const renameGenerator = (
oldName: string,
newName: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newGen, err] = renameItemInObject(
oldName,
newName,
scenario.Generators,
);
if (err) return [scenario, err];
return [{ ...scenario, Generators: newGen }, null];
};

View File

@@ -0,0 +1,137 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import {
changeTimeHorizon,
changeTimeStep,
evaluatePwlFunction,
} from "./parameterOps";
import assert from "node:assert";
import { TEST_DATA_1, TEST_DATA_2 } from "../Data/fixtures.test";
test("changeTimeHorizon: Shrink 1", () => {
const [newScenario, err] = changeTimeHorizon(TEST_DATA_1, "3");
assert(err === null);
assert.deepEqual(newScenario.Parameters, {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 3,
"Time step (min)": 60,
});
assert.deepEqual(newScenario.Buses, {
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083] },
b2: { "Load (MW)": [14.03739, 13.48563, 13.11797] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005] },
});
});
test("changeTimeHorizon: Shrink 2", () => {
const [newScenario, err] = changeTimeHorizon(TEST_DATA_2, "1");
assert(err === null);
assert.deepEqual(newScenario.Parameters, {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 1,
"Time step (min)": 30,
});
assert.deepEqual(newScenario.Buses, {
b1: { "Load (MW)": [30, 30] },
b2: { "Load (MW)": [10, 20] },
b3: { "Load (MW)": [0, 30] },
});
});
test("changeTimeHorizon grow", () => {
const [newScenario, err] = changeTimeHorizon(TEST_DATA_1, "7");
assert(err === null);
assert.deepEqual(newScenario.Parameters, {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 7,
"Time step (min)": 60,
});
assert.deepEqual(newScenario.Buses, {
b1: {
"Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044, 0, 0],
},
b2: {
"Load (MW)": [14.03739, 13.48563, 13.11797, 12.9009, 13.03939, 0, 0],
},
b3: {
"Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268, 0, 0],
},
});
});
test("changeTimeHorizon invalid", () => {
let [, err] = changeTimeHorizon(TEST_DATA_1, "x");
assert(err !== null);
assert.equal(err.message, "Invalid value: x");
[, err] = changeTimeHorizon(TEST_DATA_1, "-3");
assert(err !== null);
assert.equal(err.message, "Invalid value: -3");
});
test("evaluatePwlFunction", () => {
const data_x = [0, 60, 120, 180];
const data_y = [100, 200, 250, 100];
assert.equal(evaluatePwlFunction(data_x, data_y, 0), 100);
assert.equal(evaluatePwlFunction(data_x, data_y, 15), 125);
assert.equal(evaluatePwlFunction(data_x, data_y, 30), 150);
assert.equal(evaluatePwlFunction(data_x, data_y, 60), 200);
assert.equal(evaluatePwlFunction(data_x, data_y, 180), 100);
});
test("changeTimeStep", () => {
let [scenario, err] = changeTimeStep(TEST_DATA_2, "15");
assert(err === null);
assert.deepEqual(scenario.Parameters, {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 2,
"Time step (min)": 15,
});
assert.deepEqual(scenario.Buses, {
b1: { "Load (MW)": [30, 30, 30, 30, 30, 30, 30, 30] },
b2: { "Load (MW)": [10, 15, 20, 25, 30, 35, 40, 25] },
b3: { "Load (MW)": [0, 15, 30, 15, 0, 20, 40, 20] },
});
[scenario, err] = changeTimeStep(TEST_DATA_2, "60");
assert(err === null);
assert.deepEqual(scenario.Parameters, {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 2,
"Time step (min)": 60,
});
assert.deepEqual(scenario.Buses, {
b1: { "Load (MW)": [30, 30] },
b2: { "Load (MW)": [10, 30] },
b3: { "Load (MW)": [0, 0] },
});
});
test("changeTimeStep invalid", () => {
let [, err] = changeTimeStep(TEST_DATA_2, "x");
assert(err !== null);
assert.equal(err.message, "Invalid value: x");
[, err] = changeTimeStep(TEST_DATA_2, "-10");
assert(err !== null);
assert.equal(err.message, "Invalid value: -10");
[, err] = changeTimeStep(TEST_DATA_2, "120");
assert(err !== null);
assert.equal(err.message, "Invalid value: 120");
[, err] = changeTimeStep(TEST_DATA_2, "7");
assert(err !== null);
assert.equal(err.message, "Time step must be a divisor of 60: 7");
});
export {};

View File

@@ -0,0 +1,221 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Buses } from "../Data/fixtures";
import { ValidationError } from "../Data/validate";
import { UnitCommitmentScenario } from "../Data/types";
export const changeTimeHorizon = (
scenario: UnitCommitmentScenario,
newTimeHorizonStr: string,
): [UnitCommitmentScenario, ValidationError | null] => {
// Parse string
const newTimeHorizon = parseInt(newTimeHorizonStr);
if (isNaN(newTimeHorizon) || newTimeHorizon <= 0) {
return [scenario, { message: `Invalid value: ${newTimeHorizonStr}` }];
}
const newScenario = JSON.parse(
JSON.stringify(scenario),
) as UnitCommitmentScenario;
newScenario.Parameters["Time horizon (h)"] = newTimeHorizon;
const newT = (newTimeHorizon * 60) / scenario.Parameters["Time step (min)"];
const oldT =
(scenario.Parameters["Time horizon (h)"] * 60) /
scenario.Parameters["Time step (min)"];
if (newT < oldT) {
Object.values(newScenario.Buses).forEach((bus) => {
bus["Load (MW)"] = bus["Load (MW)"].slice(0, newT);
});
Object.values(newScenario.Generators).forEach((generator) => {
if (generator.Type === "Profiled") {
generator["Minimum power (MW)"] = generator["Minimum power (MW)"].slice(0, newT);
generator["Maximum power (MW)"] = generator["Maximum power (MW)"].slice(0, newT);
}
});
Object.values(newScenario["Price-sensitive loads"]).forEach((psLoad) => {
psLoad["Demand (MW)"] = psLoad["Demand (MW)"].slice(0, newT);
});
} else {
const padding = Array(newT - oldT).fill(0);
Object.values(newScenario.Buses).forEach((bus) => {
bus["Load (MW)"] = bus["Load (MW)"].concat(padding);
});
Object.values(newScenario.Generators).forEach((generator) => {
if (generator.Type === "Profiled") {
generator["Minimum power (MW)"] = generator["Minimum power (MW)"].concat(padding);
generator["Maximum power (MW)"] = generator["Maximum power (MW)"].concat(padding);
}
});
Object.values(newScenario["Price-sensitive loads"]).forEach((psLoad) => {
psLoad["Demand (MW)"] = psLoad["Demand (MW)"].concat(padding);
});
}
return [newScenario, null];
};
export const evaluatePwlFunction = (
data_x: number[],
data_y: number[],
x: number,
) => {
if (x < data_x[0]! || x > data_x[data_x.length - 1]!) {
throw Error("PWL interpolation: Out of bounds");
}
if (x === data_x[0]) return data_y[0];
// Binary search to find the interval containing x
let low = 0;
let high = data_x.length - 1;
while (low < high) {
let mid = Math.floor((low + high) / 2);
if (data_x[mid]! < x) low = mid + 1;
else high = mid;
}
// Linear interpolation within the found interval
const x1 = data_x[low - 1]!;
const y1 = data_y[low - 1]!;
const x2 = data_x[low]!;
const y2 = data_y[low]!;
return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1);
};
export const changeTimeStep = (
scenario: UnitCommitmentScenario,
newTimeStepStr: string,
): [UnitCommitmentScenario, ValidationError | null] => {
// Parse string and perform validation
const newTimeStep = parseFloat(newTimeStepStr);
if (isNaN(newTimeStep) || newTimeStep < 1 || newTimeStep > 60) {
return [scenario, { message: `Invalid value: ${newTimeStepStr}` }];
}
if (60 % newTimeStep !== 0) {
return [
scenario,
{ message: `Time step must be a divisor of 60: ${newTimeStepStr}` },
];
}
// Build data_x
let timeHorizon = scenario.Parameters["Time horizon (h)"];
const oldTimeStep = scenario.Parameters["Time step (min)"];
const oldT = (timeHorizon * 60) / oldTimeStep;
const newT = (timeHorizon * 60) / newTimeStep;
const data_x = Array(oldT + 1).fill(0);
for (let i = 0; i <= oldT; i++) data_x[i] = i * oldTimeStep;
const newBuses: Buses = {};
for (const busName in scenario.Buses) {
// Build data_y
const busLoad = scenario.Buses[busName]!["Load (MW)"];
const data_y = Array(oldT + 1).fill(0);
for (let i = 0; i < oldT; i++) data_y[i] = busLoad[i];
data_y[oldT] = data_y[0];
// Run interpolation
const newBusLoad = Array(newT).fill(0);
for (let i = 0; i < newT; i++) {
newBusLoad[i] = evaluatePwlFunction(data_x, data_y, newTimeStep * i);
}
newBuses[busName] = {
...scenario.Buses[busName],
"Load (MW)": newBusLoad,
};
}
const newGenerators: { [name: string]: any } = {};
for (const generatorName in scenario.Generators) {
const generator = scenario.Generators[generatorName]!;
if (generator.Type === "Profiled") {
// Build data_y for minimum power
const minPower = generator["Minimum power (MW)"];
const minData_y = Array(oldT + 1).fill(0);
for (let i = 0; i < oldT; i++) minData_y[i] = minPower[i];
minData_y[oldT] = minData_y[0];
// Build data_y for maximum power
const maxPower = generator["Maximum power (MW)"];
const maxData_y = Array(oldT + 1).fill(0);
for (let i = 0; i < oldT; i++) maxData_y[i] = maxPower[i];
maxData_y[oldT] = maxData_y[0];
// Run interpolation for both
const newMinPower = Array(newT).fill(0);
const newMaxPower = Array(newT).fill(0);
for (let i = 0; i < newT; i++) {
newMinPower[i] = evaluatePwlFunction(data_x, minData_y, newTimeStep * i);
newMaxPower[i] = evaluatePwlFunction(data_x, maxData_y, newTimeStep * i);
}
newGenerators[generatorName] = {
...generator,
"Minimum power (MW)": newMinPower,
"Maximum power (MW)": newMaxPower,
};
} else {
newGenerators[generatorName] = generator;
}
}
const newPriceSensitiveLoads: { [name: string]: any } = {};
for (const psLoadName in scenario["Price-sensitive loads"]) {
const psLoad = scenario["Price-sensitive loads"][psLoadName]!;
// Build data_y for demand
const demand = psLoad["Demand (MW)"];
const demandData_y = Array(oldT + 1).fill(0);
for (let i = 0; i < oldT; i++) demandData_y[i] = demand[i];
demandData_y[oldT] = demandData_y[0];
// Run interpolation for demand
const newDemand = Array(newT).fill(0);
for (let i = 0; i < newT; i++) {
newDemand[i] = evaluatePwlFunction(data_x, demandData_y, newTimeStep * i);
}
newPriceSensitiveLoads[psLoadName] = {
...psLoad,
"Demand (MW)": newDemand,
};
}
return [
{
...scenario,
Parameters: {
...scenario.Parameters,
"Time step (min)": newTimeStep,
},
Buses: newBuses,
Generators: newGenerators,
"Price-sensitive loads": newPriceSensitiveLoads,
},
null,
];
};
export const changeParameter = (
scenario: UnitCommitmentScenario,
key: string,
valueStr: string,
): [UnitCommitmentScenario, ValidationError | null] => {
const value = parseFloat(valueStr);
if (isNaN(value)) {
return [scenario, { message: `Invalid value: ${valueStr}` }];
}
return [
{
...scenario,
Parameters: {
...scenario.Parameters,
[key]: value,
},
},
null,
];
};

View File

@@ -0,0 +1,46 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import assert from "node:assert";
import { preprocess } from "./preprocessing";
export const PREPROCESSING_TEST_DATA_1: any = {
Parameters: {
Version: "0.4",
"Time horizon (h)": 5,
},
Buses: {
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
b2: { "Load (MW)": 10 },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
},
};
test("preprocess", () => {
const [newScenario, err] = preprocess(PREPROCESSING_TEST_DATA_1);
assert(err === null);
assert.deepEqual(newScenario, {
Parameters: {
Version: "0.4",
"Time horizon (h)": 5,
"Power balance penalty ($/MW)": 1000,
"Scenario name": "s1",
"Scenario weight": 1,
"Time step (min)": 60,
},
Buses: {
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
b2: { "Load (MW)": [10, 10, 10, 10, 10] },
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
},
"Price-sensitive loads": {},
"Storage units": {},
"Transmission lines": {},
Contingencies: {},
Generators: {},
Reserves: {},
});
});

View File

@@ -0,0 +1,70 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { validate, ValidationError } from "../Data/validate";
import { UnitCommitmentScenario } from "../Data/types";
import { migrate } from "../Data/migrate";
import {
getContingencyTransmissionLines,
rebuildContingencies,
} from "./transmissionOps";
export const preprocess = (
data: any,
): [UnitCommitmentScenario | null, ValidationError | null] => {
// Make a copy of the original data
let result = JSON.parse(JSON.stringify(data));
// Run migration
migrate(result);
// Run JSON validation and assign default values
if (!validate(result)) {
console.error(validate.errors);
return [
null,
{ message: "Invalid JSON file. See console for more details." },
];
}
// Expand scalars into arrays
// @ts-ignore
const timeHorizon = result["Parameters"]["Time horizon (h)"];
// @ts-ignore
const timeStep = result["Parameters"]["Time step (min)"];
const T = (timeHorizon * 60) / timeStep;
for (const busName in result["Buses"]) {
// @ts-ignore
const busData = result["Buses"][busName];
const busLoad = busData["Load (MW)"];
if (typeof busLoad === "number") {
busData["Load (MW)"] = Array(T).fill(busLoad);
}
}
// Add optional fields
for (let field of [
"Buses",
"Generators",
"Storage units",
"Price-sensitive loads",
"Transmission lines",
"Reserves",
"Contingencies",
]) {
if (!result[field]) {
result[field] = {};
}
}
const scenario = result as unknown as UnitCommitmentScenario;
// Rebuild contingencies
const contingencyLines = getContingencyTransmissionLines(scenario);
scenario["Contingencies"] = rebuildContingencies(contingencyLines);
return [scenario, null];
};

View File

@@ -0,0 +1,60 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { TEST_DATA_1 } from "../Data/fixtures.test";
import assert from "node:assert";
import {
changePriceSensitiveLoadData,
createPriceSensitiveLoad,
deletePriceSensitiveLoad,
renamePriceSensitiveLoad,
} from "./psloadOps";
import { ValidationError } from "../Data/validate";
test("createPriceSensitiveLoad", () => {
const [newScenario, err] = createPriceSensitiveLoad(TEST_DATA_1);
assert(err === null);
assert.equal(Object.keys(newScenario["Price-sensitive loads"]).length, 2);
assert("ps2" in newScenario["Price-sensitive loads"]);
});
test("renamePriceSensitiveLoad", () => {
const [newScenario, err] = renamePriceSensitiveLoad(
"ps1",
"ps2",
TEST_DATA_1,
);
assert(err === null);
assert.deepEqual(
newScenario["Price-sensitive loads"]["ps2"],
TEST_DATA_1["Price-sensitive loads"]["ps1"],
);
assert.equal(Object.keys(newScenario["Price-sensitive loads"]).length, 1);
});
test("changePriceSensitiveLoadData", () => {
let scenario = TEST_DATA_1;
let err: ValidationError | null;
[scenario, err] = changePriceSensitiveLoadData("ps1", "Bus", "b3", scenario);
assert.equal(err, null);
[scenario, err] = changePriceSensitiveLoadData(
"ps1",
"Demand (MW) 00:00",
"99",
scenario,
);
assert.equal(err, null);
assert.deepEqual(scenario["Price-sensitive loads"]["ps1"], {
Bus: "b3",
"Revenue ($/MW)": 23,
"Demand (MW)": [99, 50, 50, 50, 50],
});
});
test("deletePriceSensitiveLoad", () => {
const newScenario = deletePriceSensitiveLoad("ps1", TEST_DATA_1);
assert.equal(Object.keys(newScenario["Price-sensitive loads"]).length, 0);
});

View File

@@ -0,0 +1,88 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ValidationError } from "../Data/validate";
import { PriceSensitiveLoad, UnitCommitmentScenario } from "../Data/types";
import {
assertBusesNotEmpty,
changeData,
generateUniqueName,
renameItemInObject,
} from "./commonOps";
import { PriceSensitiveLoadsColumnSpec } from "../../components/CaseBuilder/Psload";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
export const createPriceSensitiveLoad = (
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const err = assertBusesNotEmpty(scenario);
if (err) return [scenario, err];
const busName = Object.keys(scenario.Buses)[0]!;
const timeslots = generateTimeslots(scenario);
const name = generateUniqueName(scenario["Price-sensitive loads"], "ps");
return [
{
...scenario,
"Price-sensitive loads": {
...scenario["Price-sensitive loads"],
[name]: {
Bus: busName,
"Revenue ($/MW)": 0,
"Demand (MW)": Array(timeslots.length).fill(0),
},
},
},
null,
];
};
export const renamePriceSensitiveLoad = (
oldName: string,
newName: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newObj, err] = renameItemInObject(
oldName,
newName,
scenario["Price-sensitive loads"],
);
if (err) return [scenario, err];
return [{ ...scenario, "Price-sensitive loads": newObj }, null];
};
export const changePriceSensitiveLoadData = (
name: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newObj, err] = changeData(
field,
newValueStr,
scenario["Price-sensitive loads"][name]!,
PriceSensitiveLoadsColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
"Price-sensitive loads": {
...scenario["Price-sensitive loads"],
[name]: newObj as PriceSensitiveLoad,
},
},
null,
];
};
export const deletePriceSensitiveLoad = (
name: string,
scenario: UnitCommitmentScenario,
): UnitCommitmentScenario => {
const { [name]: _, ...newContainer } = scenario["Price-sensitive loads"];
return { ...scenario, "Price-sensitive loads": newContainer };
};

View File

@@ -0,0 +1,75 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { TEST_DATA_1 } from "../Data/fixtures.test";
import assert from "node:assert";
import {
changeStorageUnitData,
createStorageUnit,
deleteStorageUnit,
renameStorageUnit,
} from "./storageOps";
import { ValidationError } from "../Data/validate";
test("createStorageUnit", () => {
const [newScenario, err] = createStorageUnit(TEST_DATA_1);
assert(err === null);
assert.equal(Object.keys(newScenario["Storage units"]).length, 2);
assert("su2" in newScenario["Storage units"]);
});
test("renameStorageUnit", () => {
const [newScenario, err] = renameStorageUnit("su1", "su2", TEST_DATA_1);
assert(err === null);
assert.deepEqual(
newScenario["Storage units"]["su2"],
TEST_DATA_1["Storage units"]["su1"],
);
assert.equal(Object.keys(newScenario["Storage units"]).length, 1);
});
test("changeStorageUnitData", () => {
let scenario = TEST_DATA_1;
let err: ValidationError | null;
[scenario, err] = changeStorageUnitData("su1", "Bus", "b3", scenario);
assert.equal(err, null);
[scenario, err] = changeStorageUnitData(
"su1",
"Minimum level (MWh)",
"99",
scenario,
);
assert.equal(err, null);
[scenario, err] = changeStorageUnitData(
"su1",
"Maximum discharge rate (MW)",
"99",
scenario,
);
assert.equal(err, null);
assert.deepEqual(scenario["Storage units"]["su1"], {
Bus: "b3",
"Minimum level (MWh)": 99.0,
"Maximum level (MWh)": 100.0,
"Charge cost ($/MW)": 2.0,
"Discharge cost ($/MW)": 1.0,
"Charge efficiency": 0.8,
"Discharge efficiency": 0.85,
"Loss factor": 0.01,
"Minimum charge rate (MW)": 5.0,
"Maximum charge rate (MW)": 10.0,
"Minimum discharge rate (MW)": 4.0,
"Maximum discharge rate (MW)": 99.0,
"Initial level (MWh)": 20.0,
"Last period minimum level (MWh)": 21.0,
"Last period maximum level (MWh)": 22.0,
});
});
test("deleteStorageUnit", () => {
const newScenario = deleteStorageUnit("su1", TEST_DATA_1);
assert.equal(Object.keys(newScenario["Storage units"]).length, 0);
});

View File

@@ -0,0 +1,98 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ValidationError } from "../Data/validate";
import { StorageUnit, UnitCommitmentScenario } from "../Data/types";
import {
assertBusesNotEmpty,
changeData,
generateUniqueName,
renameItemInObject,
} from "./commonOps";
import { StorageUnitsColumnSpec } from "../../components/CaseBuilder/StorageUnits";
export const createStorageUnit = (
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const err = assertBusesNotEmpty(scenario);
if (err) return [scenario, err];
const busName = Object.keys(scenario.Buses)[0]!;
const name = generateUniqueName(scenario["Storage units"], "su");
return [
{
...scenario,
"Storage units": {
...scenario["Storage units"],
[name]: {
Bus: busName,
"Minimum level (MWh)": 0,
"Maximum level (MWh)": 1,
"Charge cost ($/MW)": 0.0,
"Discharge cost ($/MW)": 0.0,
"Charge efficiency": 1,
"Discharge efficiency": 1,
"Loss factor": 0,
"Minimum charge rate (MW)": 1,
"Maximum charge rate (MW)": 1,
"Minimum discharge rate (MW)": 1,
"Maximum discharge rate (MW)": 1,
"Initial level (MWh)": 0,
"Last period minimum level (MWh)": 0,
"Last period maximum level (MWh)": 1,
},
},
},
null,
];
};
export const renameStorageUnit = (
oldName: string,
newName: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newObj, err] = renameItemInObject(
oldName,
newName,
scenario["Storage units"],
);
if (err) return [scenario, err];
return [{ ...scenario, "Storage units": newObj }, null];
};
export const changeStorageUnitData = (
name: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newObj, err] = changeData(
field,
newValueStr,
scenario["Storage units"][name]!,
StorageUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
"Storage units": {
...scenario["Storage units"],
[name]: newObj as StorageUnit,
},
},
null,
];
};
export const deleteStorageUnit = (
name: string,
scenario: UnitCommitmentScenario,
): UnitCommitmentScenario => {
const { [name]: _, ...newContainer } = scenario["Storage units"];
return { ...scenario, "Storage units": newContainer };
};

View File

@@ -0,0 +1,102 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { TEST_DATA_1 } from "../Data/fixtures.test";
import assert from "node:assert";
import {
changeTransmissionLineData,
createTransmissionLine,
deleteTransmissionLine,
getContingencyTransmissionLines,
rebuildContingencies,
renameTransmissionLine,
} from "./transmissionOps";
import { ValidationError } from "../Data/validate";
test("createTransmissionLine", () => {
const [newScenario, err] = createTransmissionLine(TEST_DATA_1);
assert(err === null);
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 2);
assert("l2" in newScenario["Transmission lines"]);
});
test("renameTransmissionLine", () => {
const [newScenario, err] = renameTransmissionLine("l1", "l3", TEST_DATA_1);
assert(err === null);
assert.deepEqual(newScenario["Transmission lines"]["l3"], {
"Source bus": "b1",
"Target bus": "b2",
"Susceptance (S)": 29.49686,
"Normal flow limit (MW)": 15000.0,
"Emergency flow limit (MW)": 20000.0,
"Flow limit penalty ($/MW)": 5000.0,
});
assert.deepEqual(newScenario["Contingencies"], {
l3: {
"Affected lines": ["l3"],
"Affected generators": [],
},
});
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 1);
});
test("changeTransmissionLineData", () => {
let scenario = TEST_DATA_1;
let err: ValidationError | null;
[scenario, err] = changeTransmissionLineData(
"l1",
"Source bus",
"b3",
scenario,
);
assert.equal(err, null);
[scenario, err] = changeTransmissionLineData(
"l1",
"Normal flow limit (MW)",
"99",
scenario,
);
assert.equal(err, null);
[scenario, err] = changeTransmissionLineData(
"l1",
"Target bus",
"b1",
scenario,
);
assert.equal(err, null);
assert.deepEqual(scenario["Transmission lines"]["l1"], {
"Source bus": "b3",
"Target bus": "b1",
"Susceptance (S)": 29.49686,
"Normal flow limit (MW)": 99,
"Emergency flow limit (MW)": 20000.0,
"Flow limit penalty ($/MW)": 5000.0,
});
});
test("deleteTransmissionLine", () => {
const newScenario = deleteTransmissionLine("l1", TEST_DATA_1);
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 0);
assert.equal(Object.keys(newScenario["Contingencies"]).length, 0);
});
test("getContingencyTransmissionLines", () => {
const contLines = getContingencyTransmissionLines(TEST_DATA_1);
assert.deepEqual(contLines, new Set(["l1"]));
});
test("rebuildContingencies", () => {
assert.deepEqual(rebuildContingencies(new Set(["l1", "l2"])), {
l1: {
"Affected lines": ["l1"],
"Affected generators": [],
},
l2: {
"Affected lines": ["l2"],
"Affected generators": [],
},
});
});

View File

@@ -0,0 +1,163 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import {
assertBusesNotEmpty,
changeData,
generateUniqueName,
parseBool,
renameItemInObject,
} from "./commonOps";
import { ValidationError } from "../Data/validate";
import { TransmissionLinesColumnSpec } from "../../components/CaseBuilder/TransmissionLines";
import {
Contingency,
TransmissionLine,
UnitCommitmentScenario,
} from "../Data/types";
export const createTransmissionLine = (
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const err = assertBusesNotEmpty(scenario);
if (err) return [scenario, err];
const busName = Object.keys(scenario.Buses)[0]!;
const name = generateUniqueName(scenario["Transmission lines"], "l");
return [
{
...scenario,
"Transmission lines": {
...scenario["Transmission lines"],
[name]: {
"Source bus": busName,
"Target bus": busName,
"Susceptance (S)": 1.0,
"Normal flow limit (MW)": 1000,
"Emergency flow limit (MW)": 1500,
"Flow limit penalty ($/MW)": 5000.0,
},
},
},
null,
];
};
export const renameTransmissionLine = (
oldName: string,
newName: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [newLine, err] = renameItemInObject(
oldName,
newName,
scenario["Transmission lines"],
);
if (err) return [scenario, err];
// Update transmission line contingencies
let newContingencies = scenario["Contingencies"];
const contingencyLines = getContingencyTransmissionLines(scenario);
if (contingencyLines.has(oldName)) {
contingencyLines.delete(oldName);
contingencyLines.add(newName);
newContingencies = rebuildContingencies(contingencyLines);
}
return [
{
...scenario,
"Transmission lines": newLine,
Contingencies: newContingencies,
},
null,
];
};
export const changeTransmissionLineData = (
line: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
if (field === "Contingency?") {
// Parse boolean value
const [newValue, err] = parseBool(newValueStr);
if (err) return [scenario, err];
// Rebuild contingencies
const contLines = getContingencyTransmissionLines(scenario);
if (newValue) contLines.add(line);
else contLines.delete(line);
const newContingencies = rebuildContingencies(contLines);
return [{ ...scenario, Contingencies: newContingencies }, null];
} else {
const [newLine, err] = changeData(
field,
newValueStr,
scenario["Transmission lines"][line]!,
TransmissionLinesColumnSpec,
scenario,
);
if (err) return [scenario, err];
return [
{
...scenario,
"Transmission lines": {
...scenario["Transmission lines"],
[line]: newLine as TransmissionLine,
},
},
null,
];
}
};
export const deleteTransmissionLine = (
name: string,
scenario: UnitCommitmentScenario,
): UnitCommitmentScenario => {
const { [name]: _, ...newLines } = scenario["Transmission lines"];
// Update transmission line contingencies
let newContingencies = scenario["Contingencies"];
const contingencyLines = getContingencyTransmissionLines(scenario);
if (contingencyLines.has(name)) {
contingencyLines.delete(name);
newContingencies = rebuildContingencies(contingencyLines);
}
return {
...scenario,
"Transmission lines": newLines,
Contingencies: newContingencies,
};
};
export const getContingencyTransmissionLines = (
scenario: UnitCommitmentScenario,
): Set<String> => {
let result: Set<String> = new Set();
Object.entries(scenario.Contingencies).forEach(([name, contingency]) => {
if (contingency["Affected lines"].length !== 1)
throw Error("not implemented");
result.add(contingency["Affected lines"][0]!!);
});
return result;
};
export const rebuildContingencies = (
contingencyLines: Set<String>,
): { [name: string]: Contingency } => {
const result: { [name: string]: Contingency } = {};
contingencyLines.forEach((lineName) => {
result[lineName as string] = {
"Affected lines": [lineName as string],
"Affected generators": [],
};
});
return result;
};

View File

@@ -0,0 +1,22 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import React from "react";
import ReactDOM from "react-dom/client";
import reportWebVitals from "./reportWebVitals";
import CaseBuilder from "./components/CaseBuilder/CaseBuilder";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render(
<React.StrictMode>
<CaseBuilder />
</React.StrictMode>,
);
reportWebVitals();

13
web/frontend/src/logo.svg Normal file
View File

@@ -0,0 +1,13 @@
<!--
- UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
- Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
- Released under the modified BSD license. See COPYING.md for more details.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

7
web/frontend/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,21 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,7 @@
/*
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
* Released under the modified BSD license. See COPYING.md for more details.
*/
import "@testing-library/jest-dom";

View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"checkJs": true
},
"include": ["src"]
}