diff --git a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx index a016f85..3351305 100644 --- a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx +++ b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx @@ -23,6 +23,7 @@ import { ColumnDefinition } from "tabulator-tables"; import { offerDownload } from "../../Common/io"; import FileUploadElement from "../../Common/Buttons/FileUploadElement"; import { useRef } from "react"; +import { createProfiledUnit } from "../../../core/Operations/profiledUnitOps"; interface ProfiledUnitsProps { scenario: UnitCommitmentScenario; @@ -98,7 +99,14 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => { }); }; - const onAdd = () => {}; + const onAdd = () => { + const [newScenario, err] = createProfiledUnit(props.scenario); + if (err) { + props.onError(err.message); + return; + } + props.onDataChanged(newScenario); + }; return (
diff --git a/web/src/core/Operations/busOperations.test.ts b/web/src/core/Operations/busOperations.test.ts index 20263e6..14c18c5 100644 --- a/web/src/core/Operations/busOperations.test.ts +++ b/web/src/core/Operations/busOperations.test.ts @@ -30,18 +30,10 @@ test("changeBusData", () => { [scenario, err] = changeBusData("b3", "Load (MW) 04:00", "99", scenario); assert.equal(err, null); - assert.deepEqual(scenario, { - Parameters: { - Version: "0.4", - "Power balance penalty ($/MW)": 1000.0, - "Time horizon (h)": 5, - "Time step (min)": 60, - }, - 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] }, - }, + 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] }, }); }); @@ -54,35 +46,19 @@ test("changeBusData with invalid numbers", () => { test("deleteBus", () => { let scenario = TEST_DATA_1; scenario = deleteBus("b2", scenario); - assert.deepEqual(scenario, { - 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] }, - b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] }, - }, + 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("b2", "b99", TEST_DATA_1); assert(err === null); - assert.deepEqual(scenario, { - 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] }, - b99: { "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.Buses, { + b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] }, + b99: { "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] }, }); }); diff --git a/web/src/core/Operations/busOperations.ts b/web/src/core/Operations/busOperations.ts index 74b0bf8..cc80c9e 100644 --- a/web/src/core/Operations/busOperations.ts +++ b/web/src/core/Operations/busOperations.ts @@ -6,34 +6,27 @@ import { Buses, UnitCommitmentScenario } from "../fixtures"; import { ValidationError } from "../Validation/validate"; +import { generateTimeslots } from "../../components/Common/Forms/DataTable"; -const generateUniqueBusName = (scenario: UnitCommitmentScenario) => { - let newBusName = "b"; +export const generateUniqueName = (container: any, prefix: string): string => { let counter = 1; - let name = `${newBusName}${counter}`; - while (name in scenario.Buses) { + let name = `${prefix}${counter}`; + while (name in container) { counter++; - name = `${newBusName}${counter}`; + name = `${prefix}${counter}`; } return name; }; -const generateDefaultBusLoad = (scenario: UnitCommitmentScenario) => { - const T = - scenario.Parameters["Time horizon (h)"] * - (60 / scenario.Parameters["Time step (min)"]); - return new Array(T).fill(0); -}; - export const createBus = (scenario: UnitCommitmentScenario) => { - const load = generateDefaultBusLoad(scenario); - let name = generateUniqueBusName(scenario); + const name = generateUniqueName(scenario.Buses, "b"); + const timeslots = generateTimeslots(scenario); return { ...scenario, Buses: { ...scenario.Buses, [name]: { - "Load (MW)": load, + "Load (MW)": Array(timeslots.length).fill(0), }, }, }; diff --git a/web/src/core/Operations/parameterOperations.test.ts b/web/src/core/Operations/parameterOperations.test.ts index c7ae767..5127369 100644 --- a/web/src/core/Operations/parameterOperations.test.ts +++ b/web/src/core/Operations/parameterOperations.test.ts @@ -15,18 +15,16 @@ import { TEST_DATA_1, TEST_DATA_2 } from "../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, - }, - 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] }, - }, + 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] }, }); }); @@ -51,23 +49,21 @@ test("changeTimeHorizon: Shrink 2", () => { 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.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], }, - 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], - }, + 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], }, }); }); diff --git a/web/src/core/Operations/profiledUnitOps.test.ts b/web/src/core/Operations/profiledUnitOps.test.ts new file mode 100644 index 0000000..76dd431 --- /dev/null +++ b/web/src/core/Operations/profiledUnitOps.test.ts @@ -0,0 +1,36 @@ +/* + * 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 "../fixtures.test"; +import assert from "node:assert"; +import { createProfiledUnit } from "./profiledUnitOps"; + +test("createUnit", () => { + const [newScenario, err] = createProfiledUnit(TEST_DATA_1); + assert(err === null); + assert.deepEqual(newScenario.Generators, { + 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)": 0, + "Maximum power (MW)": [0, 0, 0, 0, 0], + "Minimum power (MW)": [0, 0, 0, 0, 0], + }, + }); +}); + +test("createUnit with blank file", () => { + const [newScenario, err] = createProfiledUnit(TEST_DATA_BLANK); + assert(err !== null); + assert.equal(err.message, "Profiled unit requires an existing bus."); +}); diff --git a/web/src/core/Operations/profiledUnitOps.ts b/web/src/core/Operations/profiledUnitOps.ts new file mode 100644 index 0000000..6075002 --- /dev/null +++ b/web/src/core/Operations/profiledUnitOps.ts @@ -0,0 +1,37 @@ +/* + * 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 "../fixtures"; +import { generateTimeslots } from "../../components/Common/Forms/DataTable"; +import { generateUniqueName } from "./busOperations"; +import { ValidationError } from "../Validation/validate"; + +export const createProfiledUnit = ( + scenario: UnitCommitmentScenario, +): [UnitCommitmentScenario, ValidationError | null] => { + const busNames = Object.keys(scenario.Buses); + if (busNames.length === 0) { + return [scenario, { message: "Profiled unit requires an existing bus." }]; + } + const timeslots = generateTimeslots(scenario); + const name = generateUniqueName(scenario.Generators, "pu"); + return [ + { + ...scenario, + Generators: { + ...scenario.Generators, + [name]: { + Bus: busNames[0]!, + Type: "Profiled", + "Cost ($/MW)": 0, + "Minimum power (MW)": Array(timeslots.length).fill(0), + "Maximum power (MW)": Array(timeslots.length).fill(0), + }, + }, + }, + null, + ]; +}; diff --git a/web/src/core/fixtures.test.ts b/web/src/core/fixtures.test.ts index e0cc41d..47d069c 100644 --- a/web/src/core/fixtures.test.ts +++ b/web/src/core/fixtures.test.ts @@ -12,7 +12,17 @@ export const TEST_DATA_1: UnitCommitmentScenario = { 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: { + 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], + }, + }, }; + export const TEST_DATA_2: UnitCommitmentScenario = { Parameters: { Version: "0.4", @@ -27,4 +37,15 @@ export const TEST_DATA_2: UnitCommitmentScenario = { }, }; +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: {}, + Generators: {}, +}; + test("fixtures", () => {});