Compare commits

...

6 Commits

Binary file not shown.

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

@ -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;"]

@ -7,7 +7,7 @@
import assert from "node:assert";
import { BusesColumnSpec, generateBusesData } from "./Buses";
import { generateCsv, parseCsv } from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/fixtures.test";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
test("generate CSV", () => {
const [data, columns] = generateBusesData(TEST_DATA_1);

@ -14,7 +14,7 @@ import {
import { offerDownload } from "../Common/io";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { useRef } from "react";
import { ValidationError } from "../../core/Validation/validate";
import { ValidationError } from "../../core/Data/validate";
import DataTable, {
ColumnSpec,
generateCsv,
@ -23,7 +23,6 @@ import DataTable, {
parseCsv,
} from "../Common/Forms/DataTable";
import { UnitCommitmentScenario } from "../../core/fixtures";
import { ColumnDefinition } from "tabulator-tables";
import {
changeBusData,
@ -32,6 +31,7 @@ import {
renameBus,
} from "../../core/Operations/busOps";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { UnitCommitmentScenario } from "../../core/Data/types";
export const BusesColumnSpec: ColumnSpec[] = [
{

@ -7,22 +7,19 @@
import Header from "./Header";
import Parameters from "./Parameters";
import BusesComponent from "./Buses";
import {
BLANK_SCENARIO,
TEST_SCENARIO,
UnitCommitmentScenario,
} from "../../core/fixtures";
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 { validate } from "../../core/Validation/validate";
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";
export interface CaseBuilderSectionProps {
scenario: UnitCommitmentScenario;
@ -32,9 +29,8 @@ export interface CaseBuilderSectionProps {
const CaseBuilder = () => {
const [scenario, setScenario] = useState(() => {
// const savedScenario = localStorage.getItem("scenario");
// return savedScenario ? JSON.parse(savedScenario) : TEST_SCENARIO;
return TEST_SCENARIO;
const savedScenario = localStorage.getItem("scenario");
return savedScenario ? JSON.parse(savedScenario) : BLANK_SCENARIO;
});
const [undoStack, setUndoStack] = useState<UnitCommitmentScenario[]>([]);
const [toastMessage, setToastMessage] = useState<string>("");
@ -70,19 +66,14 @@ const CaseBuilder = () => {
setAndSaveScenario(newScenario);
};
const onLoad = (scenario: UnitCommitmentScenario) => {
const preprocessed = preprocess(
scenario,
) as unknown as UnitCommitmentScenario;
// Validate and assign default values
if (!validate(preprocessed)) {
setToastMessage("Error loading JSON file");
console.error(validate.errors);
const onLoad = (data: any) => {
const json = JSON.parse(data);
const [scenario, err] = preprocess(json);
if (err) {
setToastMessage(err.message);
return;
}
setAndSaveScenario(preprocessed);
setAndSaveScenario(scenario!);
setToastMessage("Data loaded successfully");
};
@ -121,6 +112,11 @@ const CaseBuilder = () => {
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<TransmissionLinesComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<Toast message={toastMessage} />
</div>
<Footer />

@ -6,9 +6,9 @@
import styles from "./Header.module.css";
import SiteHeaderButton from "../Common/Buttons/SiteHeaderButton";
import { UnitCommitmentScenario } from "../../core/fixtures";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { UnitCommitmentScenario } from "../../core/Data/types";
interface HeaderProps {
onClear: () => void;
@ -22,8 +22,7 @@ function Header(props: HeaderProps) {
function onLoad() {
fileElem.current!.showFilePicker((data: any) => {
const scenario = JSON.parse(data) as UnitCommitmentScenario;
props.onLoad(scenario);
props.onLoad(data);
});
}

@ -7,12 +7,12 @@
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import Form from "../Common/Forms/Form";
import TextInputRow from "../Common/Forms/TextInputRow";
import { UnitCommitmentScenario } from "../../core/fixtures";
import {
changeParameter,
changeTimeHorizon,
changeTimeStep,
} from "../../core/Operations/parameterOps";
import { UnitCommitmentScenario } from "../../core/Data/types";
interface ParametersProps {
scenario: UnitCommitmentScenario;

@ -13,12 +13,12 @@ import {
parseProfiledUnitsCsv,
ProfiledUnitsColumnSpec,
} from "./ProfiledUnits";
import { TEST_DATA_1 } from "../../core/fixtures.test";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
import assert from "node:assert";
import {
getProfiledGenerators,
getThermalGenerators,
} from "../../core/fixtures";
} from "../../core/Data/types";
test("parse CSV", () => {
const csvContents =
@ -104,7 +104,7 @@ test("generateTableColumns", () => {
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 60,
minWidth: 75,
resizable: false,
title: "00:00",
});

@ -18,11 +18,6 @@ import DataTable, {
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/fixtures";
import { ColumnDefinition } from "tabulator-tables";
import { offerDownload } from "../Common/io";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
@ -33,8 +28,13 @@ import {
deleteGenerator,
renameGenerator,
} from "../../core/Operations/generatorOps";
import { ValidationError } from "../../core/Validation/validate";
import { ValidationError } from "../../core/Data/validate";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/Data/types";
export const ProfiledUnitsColumnSpec: ColumnSpec[] = [
{
@ -55,12 +55,12 @@ export const ProfiledUnitsColumnSpec: ColumnSpec[] = [
{
title: "Maximum power (MW)",
type: "number[T]",
width: 60,
width: 75,
},
{
title: "Minimum power (MW)",
type: "number[T]",
width: 60,
width: 75,
},
];

@ -10,7 +10,7 @@ import {
generateTableColumns,
generateTableData,
} from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/fixtures.test";
import { TEST_DATA_1 } from "../../core/Data/fixtures.test";
import {
generateThermalUnitsData,
parseThermalUnitsCsv,
@ -20,7 +20,7 @@ import assert from "node:assert";
import {
getProfiledGenerators,
getThermalGenerators,
} from "../../core/fixtures";
} from "../../core/Data/types";
test("generateTableColumns", () => {
const columns = generateTableColumns(TEST_DATA_1, ThermalUnitsColumnSpec);
@ -36,7 +36,7 @@ test("generateTableColumns", () => {
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 60,
minWidth: 75,
resizable: false,
title: "1",
});

@ -14,7 +14,7 @@ import DataTable, {
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { ValidationError } from "../../core/Validation/validate";
import { ValidationError } from "../../core/Data/validate";
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
@ -22,11 +22,6 @@ import {
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/fixtures";
import { ColumnDefinition } from "tabulator-tables";
import { offerDownload } from "../Common/io";
import {
@ -35,6 +30,11 @@ import {
deleteGenerator,
renameGenerator,
} from "../../core/Operations/generatorOps";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/Data/types";
export const ThermalUnitsColumnSpec: ColumnSpec[] = [
{
@ -51,19 +51,19 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
title: "Production cost curve (MW)",
type: "number[N]",
length: 10,
width: 60,
width: 80,
},
{
title: "Production cost curve ($)",
type: "number[N]",
length: 10,
width: 60,
width: 80,
},
{
title: "Startup costs ($)",
type: "number[N]",
length: 5,
width: 60,
width: 75,
},
{
title: "Startup delays (h)",
@ -83,22 +83,22 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
},
{
title: "Ramp up limit (MW)",
type: "number",
type: "number?",
width: 100,
},
{
title: "Ramp down limit (MW)",
type: "number",
type: "number?",
width: 100,
},
{
title: "Startup limit (MW)",
type: "number",
type: "number?",
width: 80,
},
{
title: "Shutdown limit (MW)",
type: "number",
type: "number?",
width: 100,
},
{

@ -0,0 +1,186 @@
/*
* 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,
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,
},
];
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) => {
const [newLines, err] = parseCsv(
csv,
TransmissionLinesColumnSpec,
props.scenario,
);
if (err) {
props.onError(err.message);
return;
}
const newScenario = {
...props.scenario,
"Transmission lines": newLines,
};
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;

@ -10,14 +10,21 @@ import {
ColumnDefinition,
TabulatorFull as Tabulator,
} from "tabulator-tables";
import { ValidationError } from "../../../core/Validation/validate";
import { UnitCommitmentScenario } from "../../../core/fixtures";
import { ValidationError } from "../../../core/Data/validate";
import Papa from "papaparse";
import { parseBool, parseNumber } from "../../../core/Operations/commonOps";
import { UnitCommitmentScenario } from "../../../core/Data/types";
export interface ColumnSpec {
title: string;
type: "string" | "number" | "number[N]" | "number[T]" | "busRef" | "boolean";
type:
| "string"
| "number"
| "number?"
| "number[N]"
| "number[T]"
| "busRef"
| "boolean";
length?: number;
width: number;
}
@ -53,6 +60,7 @@ export const generateTableColumns = (
});
break;
case "number":
case "number?":
columns.push({
...columnsCommonAttrs,
title: spec.title,
@ -118,6 +126,7 @@ export const generateTableData = (
switch (spec.type) {
case "string":
case "number":
case "number?":
case "boolean":
case "busRef":
entry[spec.title] = entryData[spec.title];
@ -285,10 +294,13 @@ export const parseCsv = (
export const floatFormatter = (cell: CellComponent) => {
const v = cell.getValue();
if (v === "") {
if (v === "" || v === null) {
return "&mdash;";
} else {
return parseFloat(cell.getValue()).toFixed(1);
return parseFloat(cell.getValue()).toLocaleString("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
}
};

@ -7,7 +7,7 @@
import formStyles from "./Form.module.css";
import HelpButton from "../Buttons/HelpButton";
import React, { useRef, useState } from "react";
import { ValidationError } from "../../../core/Validation/validate";
import { ValidationError } from "../../../core/Data/validate";
interface TextInputRowProps {
label: string;

@ -1,4 +1,10 @@
import { UnitCommitmentScenario } from "./fixtures";
/*
* 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: {
@ -45,6 +51,16 @@ export const TEST_DATA_1: UnitCommitmentScenario = {
"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,
},
},
};
export const TEST_DATA_2: UnitCommitmentScenario = {
@ -60,6 +76,7 @@ export const TEST_DATA_2: UnitCommitmentScenario = {
b3: { "Load (MW)": [0, 30, 0, 40] },
},
Generators: {},
"Transmission lines": {},
};
export const TEST_DATA_BLANK: UnitCommitmentScenario = {
@ -71,6 +88,7 @@ export const TEST_DATA_BLANK: UnitCommitmentScenario = {
},
Buses: {},
Generators: {},
"Transmission lines": {},
};
test("fixtures", () => {});

@ -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 { 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": {},
};

@ -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");
});

@ -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";
}
}
}
};

@ -97,15 +97,18 @@ export const schema = {
},
"Susceptance (S)": {
type: "number",
minimum: 0,
},
"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",
@ -254,18 +257,26 @@ export const schema = {
"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",

@ -0,0 +1,81 @@
/*
* 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 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;
};
}
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");

@ -6,7 +6,7 @@
import { changeBusData, createBus, deleteBus, renameBus } from "./busOps";
import assert from "node:assert";
import { TEST_DATA_1 } from "../fixtures.test";
import { TEST_DATA_1 } from "../Data/fixtures.test";
test("createBus", () => {
const newScenario = createBus(TEST_DATA_1);

@ -4,8 +4,8 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Buses, UnitCommitmentScenario } from "../fixtures";
import { ValidationError } from "../Validation/validate";
import { Buses } from "../Data/fixtures";
import { ValidationError } from "../Data/validate";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import {
changeData,
@ -13,6 +13,7 @@ import {
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");

@ -4,9 +4,9 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { ValidationError } from "../Validation/validate";
import { UnitCommitmentScenario } from "../fixtures";
import { ValidationError } from "../Data/validate";
import { ColumnSpec } from "../../components/Common/Forms/DataTable";
import { UnitCommitmentScenario } from "../Data/types";
export const renameItemInObject = <T>(
oldName: string,
@ -43,6 +43,9 @@ export const generateUniqueName = (container: any, prefix: string): string => {
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` }];
@ -51,6 +54,13 @@ export const parseNumber = (
}
};
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] => {
@ -93,9 +103,12 @@ export const changeNumberData = (
field: string,
newValueStr: string,
container: { [key: string]: any },
nullable: boolean = false,
): [{ [key: string]: any }, ValidationError | null] => {
// Parse value
const [newValueFloat, err] = parseNumber(newValueStr);
const [newValueFloat, err] = nullable
? parseNullableNumber(newValueStr)
: parseNumber(newValueStr);
if (err) return [container, err];
// Build the new object
@ -211,6 +224,8 @@ export const changeData = (
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,
@ -234,3 +249,10 @@ export const changeData = (
}
throw Error(`Unknown field: ${fieldName}`);
};
export const assertBusesNotEmpty = (
scenario: UnitCommitmentScenario,
): ValidationError | null => {
if (Object.keys(scenario.Buses).length === 0)
return { message: "Profiled unit requires an existing bus." };
return null;
};

@ -4,7 +4,7 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { TEST_DATA_1, TEST_DATA_BLANK } from "../fixtures.test";
import { TEST_DATA_1, TEST_DATA_BLANK } from "../Data/fixtures.test";
import assert from "node:assert";
import {
changeProfiledUnitData,
@ -14,7 +14,7 @@ import {
deleteGenerator,
renameGenerator,
} from "./generatorOps";
import { ValidationError } from "../Validation/validate";
import { ValidationError } from "../Data/validate";
test("createProfiledUnit", () => {
const [newScenario, err] = createProfiledUnit(TEST_DATA_1);

@ -4,24 +4,17 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Generators, UnitCommitmentScenario } from "../fixtures";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import { ValidationError } from "../Validation/validate";
import { ValidationError } from "../Data/validate";
import {
assertBusesNotEmpty,
changeData,
generateUniqueName,
renameItemInObject,
} from "./commonOps";
import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits";
import { ThermalUnitsColumnSpec } from "../../components/CaseBuilder/ThermalUnits";
const assertBusesNotEmpty = (
scenario: UnitCommitmentScenario,
): ValidationError | null => {
if (Object.keys(scenario.Buses).length === 0)
return { message: "Profiled unit requires an existing bus." };
return null;
};
import { Generators, UnitCommitmentScenario } from "../Data/types";
export const createProfiledUnit = (
scenario: UnitCommitmentScenario,
@ -68,10 +61,10 @@ export const createThermalUnit = (
"Production cost curve ($)": [0, 10],
"Startup costs ($)": [0],
"Startup delays (h)": [1],
"Ramp up limit (MW)": "",
"Ramp down limit (MW)": "",
"Startup limit (MW)": "",
"Shutdown limit (MW)": "",
"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,

@ -10,7 +10,7 @@ import {
evaluatePwlFunction,
} from "./parameterOps";
import assert from "node:assert";
import { TEST_DATA_1, TEST_DATA_2 } from "../fixtures.test";
import { TEST_DATA_1, TEST_DATA_2 } from "../Data/fixtures.test";
test("changeTimeHorizon: Shrink 1", () => {
const [newScenario, err] = changeTimeHorizon(TEST_DATA_1, "3");

@ -4,8 +4,9 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
import { Buses, UnitCommitmentScenario } from "../fixtures";
import { ValidationError } from "../Validation/validate";
import { Buses } from "../Data/fixtures";
import { ValidationError } from "../Data/validate";
import { UnitCommitmentScenario } from "../Data/types";
export const changeTimeHorizon = (
scenario: UnitCommitmentScenario,

@ -1,25 +1,35 @@
// @ts-nocheck
/*
* 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 } from "../Validation/validate";
import { validate, ValidationError } from "../Data/validate";
import { UnitCommitmentScenario } from "../Data/types";
import { migrate } from "../Data/migrate";
export const preprocess = (data) => {
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);
throw Error("Invalid JSON");
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"]) {
@ -30,5 +40,7 @@ export const preprocess = (data) => {
busData["Load (MW)"] = Array(T).fill(busLoad);
}
}
return result;
const scenario = result as unknown as UnitCommitmentScenario;
return [scenario, null];
};

@ -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 {
changeTransmissionLineData,
createTransmissionLine,
deleteTransmissionLine,
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.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);
});

@ -0,0 +1,89 @@
/*
* 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,
renameItemInObject,
} from "./commonOps";
import { ValidationError } from "../Data/validate";
import { TransmissionLinesColumnSpec } from "../../components/CaseBuilder/TransmissionLines";
import { 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];
return [{ ...scenario, "Transmission lines": newLine }, null];
};
export const changeTransmissionLineData = (
line: string,
field: string,
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
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"];
return { ...scenario, "Transmission lines": newLines };
};

@ -1,179 +0,0 @@
/*
* 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 interface Buses {
[busName: string]: { "Load (MW)": number[] };
}
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 | "";
"Ramp down limit (MW)": number | "";
"Startup limit (MW)": number | "";
"Shutdown limit (MW)": number | "";
"Minimum downtime (h)": number;
"Minimum uptime (h)": number;
"Initial status (h)": number;
"Initial power (MW)": number;
"Must run?": boolean;
}
export interface UnitCommitmentScenario {
Parameters: {
Version: string;
"Power balance penalty ($/MW)": number;
"Time horizon (h)": number;
"Time step (min)": number;
};
Buses: Buses;
Generators: Generators;
}
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");
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: {},
};
export const TEST_SCENARIO: UnitCommitmentScenario = {
Parameters: {
Version: "0.4",
"Power balance penalty ($/MW)": 1000.0,
"Time horizon (h)": 36,
"Time step (min)": 60,
},
Buses: {
b1: {
"Load (MW)": [
35.79534, 34.38835, 33.45083, 32.89729, 33.25044, 33.93851, 35.8654,
37.27098, 38.08378, 38.99327, 38.65134, 38.83212, 37.60031, 37.27939,
37.11823, 37.73063, 40.951, 44.77115, 43.67527, 44.40959, 44.33812,
42.29071, 40.07654, 37.42093, 35.61175, 34.28185, 32.74174, 33.17336,
33.5181, 35.63558, 38.12722, 39.61689, 40.80105, 42.55277, 42.76017,
42.12535,
],
},
b2: {
"Load (MW)": [
14.03739, 13.48563, 13.11797, 12.9009, 13.03939, 13.30922, 14.06486,
14.61607, 14.93482, 15.29148, 15.15739, 15.22828, 14.74522, 14.61937,
14.55617, 14.79633, 16.05921, 17.55731, 17.12756, 17.41553, 17.3875,
16.58459, 15.71629, 14.67487, 13.96539, 13.44386, 12.8399, 13.00916,
13.14435, 13.97474, 14.95185, 15.53603, 16.00041, 16.68736, 16.76869,
16.51974,
],
},
b3: {
"Load (MW)": [
27.3729, 26.29698, 25.58005, 25.15675, 25.4268, 25.95298, 27.42649,
28.50134, 29.12289, 29.81839, 29.55691, 29.69515, 28.75318, 28.50777,
28.38453, 28.85284, 31.31547, 34.23676, 33.39874, 33.96028, 33.90562,
32.33996, 30.64676, 28.61601, 27.23252, 26.21553, 25.0378, 25.36786,
25.63149, 27.25074, 29.15611, 30.29527, 31.2008, 32.54035, 32.69895,
32.2135,
],
},
b4: {
"Load (MW)": [
27.5, 26.29698, 25.58005, 25.15675, 25.4268, 25.95298, 27.42649,
28.50134, 29.12289, 29.81839, 29.55691, 29.69515, 28.75318, 28.50777,
28.38453, 28.85284, 31.31547, 34.23676, 33.39874, 33.96028, 33.90562,
32.33996, 30.64676, 28.61601, 27.23252, 26.21553, 25.0378, 25.36786,
25.63149, 27.25074, 29.15611, 30.29527, 31.2008, 32.54035, 32.69895,
32.2135,
],
},
},
Generators: {
pu1: {
Bus: "b1",
Type: "Profiled",
"Minimum power (MW)": [
52.05076909, 14.57829614, 75.43577222, 67.33346472, 75.36556352,
21.57017795, 38.57431892, 46.71083643, 97.87434963, 95.12592361,
82.00040834, 25.97388027, 14.87169082, 8.68106053, 43.67452089,
18.95280541, 85.59390327, 59.62398136, 81.30530633, 83.61173632,
10.07929569, 87.96736565, 84.65719304, 30.57367207, 27.39181212,
78.27367461, 6.81518238, 68.40723311, 19.616812, 60.20940984,
58.57199889, 89.50587265, 65.26434981, 78.57656542, 52.20154156,
42.79584818,
],
"Maximum power (MW)": [
260.25384545, 72.89148068, 377.17886108, 336.66732361, 376.82781758,
107.85088974, 192.8715946, 233.55418217, 489.37174815, 475.62961804,
410.00204171, 129.86940137, 74.35845412, 43.40530267, 218.37260447,
94.76402706, 427.96951634, 298.11990681, 406.52653167, 418.05868161,
50.39647843, 439.83682824, 423.28596522, 152.86836035, 136.95906058,
391.36837307, 34.0759119, 342.03616557, 98.08406001, 301.04704921,
292.85999447, 447.52936326, 326.32174903, 392.88282708, 261.0077078,
213.9792409,
],
"Cost ($/MW)": 50.0,
},
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,
},
},
};
Loading…
Cancel
Save