From 86aababf33724fe65c7fb17060fe1fd3cd81cd3c Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 24 Jun 2025 09:21:54 -0500 Subject: [PATCH] web: ProfiledUnits: Rename and delete --- .../components/CaseBuilder/Buses/Buses.tsx | 2 +- .../CaseBuilder/Parameters/Parameters.tsx | 2 +- .../ProfiledUnits/ProfiledUnits.tsx | 16 +++- .../{busOperations.test.ts => busOps.test.ts} | 9 +-- .../{busOperations.ts => busOps.ts} | 39 ++------- web/src/core/Operations/commonOps.ts | 39 +++++++++ web/src/core/Operations/generatorOps.test.ts | 81 +++++++++++++++++++ .../{profiledUnitOps.ts => generatorOps.ts} | 24 +++++- ...perations.test.ts => parameterOps.test.ts} | 68 +++++++--------- ...parameterOperations.ts => parameterOps.ts} | 0 .../core/Operations/profiledUnitOps.test.ts | 36 --------- web/src/core/fixtures.test.ts | 8 ++ web/src/core/fixtures.tsx | 2 +- 13 files changed, 205 insertions(+), 121 deletions(-) rename web/src/core/Operations/{busOperations.test.ts => busOps.test.ts} (93%) rename web/src/core/Operations/{busOperations.ts => busOps.ts} (71%) create mode 100644 web/src/core/Operations/commonOps.ts create mode 100644 web/src/core/Operations/generatorOps.test.ts rename web/src/core/Operations/{profiledUnitOps.ts => generatorOps.ts} (64%) rename web/src/core/Operations/{parameterOperations.test.ts => parameterOps.test.ts} (75%) rename web/src/core/Operations/{parameterOperations.ts => parameterOps.ts} (100%) delete mode 100644 web/src/core/Operations/profiledUnitOps.test.ts diff --git a/web/src/components/CaseBuilder/Buses/Buses.tsx b/web/src/components/CaseBuilder/Buses/Buses.tsx index fb81a0a..ea93362 100644 --- a/web/src/components/CaseBuilder/Buses/Buses.tsx +++ b/web/src/components/CaseBuilder/Buses/Buses.tsx @@ -30,7 +30,7 @@ import { createBus, deleteBus, renameBus, -} from "../../../core/Operations/busOperations"; +} from "../../../core/Operations/busOps"; export const BusesColumnSpec: ColumnSpec[] = [ { diff --git a/web/src/components/CaseBuilder/Parameters/Parameters.tsx b/web/src/components/CaseBuilder/Parameters/Parameters.tsx index d4331cc..c4e031c 100644 --- a/web/src/components/CaseBuilder/Parameters/Parameters.tsx +++ b/web/src/components/CaseBuilder/Parameters/Parameters.tsx @@ -12,7 +12,7 @@ import { changeParameter, changeTimeHorizon, changeTimeStep, -} from "../../../core/Operations/parameterOperations"; +} from "../../../core/Operations/parameterOps"; interface ParametersProps { scenario: UnitCommitmentScenario; diff --git a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx index 3351305..de885af 100644 --- a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx +++ b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx @@ -23,7 +23,11 @@ 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"; +import { + createProfiledUnit, + deleteGenerator, +} from "../../../core/Operations/generatorOps"; +import { ValidationError } from "../../../core/Validation/validate"; interface ProfiledUnitsProps { scenario: UnitCommitmentScenario; @@ -108,6 +112,12 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => { props.onDataChanged(newScenario); }; + const onDelete = (name: string): ValidationError | null => { + const newScenario = deleteGenerator(name, props.scenario); + props.onDataChanged(newScenario); + return null; + }; + return (
@@ -120,9 +130,7 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => { { - return null; - }} + onRowDeleted={onDelete} onRowRenamed={() => { return null; }} diff --git a/web/src/core/Operations/busOperations.test.ts b/web/src/core/Operations/busOps.test.ts similarity index 93% rename from web/src/core/Operations/busOperations.test.ts rename to web/src/core/Operations/busOps.test.ts index 14c18c5..8a6c0ae 100644 --- a/web/src/core/Operations/busOperations.test.ts +++ b/web/src/core/Operations/busOps.test.ts @@ -4,12 +4,7 @@ * Released under the modified BSD license. See COPYING.md for more details. */ -import { - changeBusData, - createBus, - deleteBus, - renameBus, -} from "./busOperations"; +import { changeBusData, createBus, deleteBus, renameBus } from "./busOps"; import assert from "node:assert"; import { TEST_DATA_1 } from "../fixtures.test"; @@ -65,5 +60,5 @@ test("renameBus", () => { test("renameBus with duplicated name", () => { let [, err] = renameBus("b3", "b1", TEST_DATA_1); assert(err != null); - assert.equal(err.message, `Bus b1 already exists`); + assert.equal(err.message, `b1 already exists`); }); diff --git a/web/src/core/Operations/busOperations.ts b/web/src/core/Operations/busOps.ts similarity index 71% rename from web/src/core/Operations/busOperations.ts rename to web/src/core/Operations/busOps.ts index cc80c9e..175eba6 100644 --- a/web/src/core/Operations/busOperations.ts +++ b/web/src/core/Operations/busOps.ts @@ -4,19 +4,10 @@ * Released under the modified BSD license. See COPYING.md for more details. */ -import { Buses, UnitCommitmentScenario } from "../fixtures"; +import { UnitCommitmentScenario } from "../fixtures"; import { ValidationError } from "../Validation/validate"; import { generateTimeslots } from "../../components/Common/Forms/DataTable"; - -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; -}; +import { generateUniqueName, renameItemInObject } from "./commonOps"; export const createBus = (scenario: UnitCommitmentScenario) => { const name = generateUniqueName(scenario.Buses, "b"); @@ -72,10 +63,7 @@ export const changeBusData = ( export const deleteBus = (bus: string, scenario: UnitCommitmentScenario) => { const { [bus]: _, ...newBuses } = scenario.Buses; - return { - ...scenario, - Buses: newBuses, - }; + return { ...scenario, Buses: newBuses }; }; export const renameBus = ( @@ -83,22 +71,7 @@ export const renameBus = ( newName: string, scenario: UnitCommitmentScenario, ): [UnitCommitmentScenario, ValidationError | null] => { - if (newName in scenario.Buses) { - return [scenario, { message: `Bus ${newName} already exists` }]; - } - const newBuses: Buses = Object.keys(scenario.Buses).reduce((acc, val) => { - if (val === oldName) { - acc[newName] = scenario.Buses[val]!; - } else { - acc[val] = scenario.Buses[val]!; - } - return acc; - }, {} as Buses); - return [ - { - ...scenario, - Buses: newBuses, - }, - null, - ]; + const [newBuses, err] = renameItemInObject(oldName, newName, scenario.Buses); + if (err) return [scenario, err]; + return [{ ...scenario, Buses: newBuses }, null]; }; diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts new file mode 100644 index 0000000..804800e --- /dev/null +++ b/web/src/core/Operations/commonOps.ts @@ -0,0 +1,39 @@ +/* + * 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 "../Validation/validate"; + +export const renameItemInObject = ( + 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; +}; diff --git a/web/src/core/Operations/generatorOps.test.ts b/web/src/core/Operations/generatorOps.test.ts new file mode 100644 index 0000000..1b8a3e7 --- /dev/null +++ b/web/src/core/Operations/generatorOps.test.ts @@ -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 { TEST_DATA_1, TEST_DATA_BLANK } from "../fixtures.test"; +import assert from "node:assert"; +import { + createProfiledUnit, + deleteGenerator, + renameGenerator, +} from "./generatorOps"; + +test("createProfiledUnit", () => { + 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)": 120, + "Maximum power (MW)": [50, 50, 50, 50, 50], + "Minimum power (MW)": [0, 0, 0, 0, 0], + }, + pu3: { + Bus: "b1", + Type: "Profiled", + "Cost ($/MW)": 0, + "Maximum power (MW)": [0, 0, 0, 0, 0], + "Minimum power (MW)": [0, 0, 0, 0, 0], + }, + }); +}); + +test("createProfiledUnit with blank file", () => { + const [_, err] = createProfiledUnit(TEST_DATA_BLANK); + assert(err !== null); + assert.equal(err.message, "Profiled unit requires an existing bus."); +}); + +test("deleteGenerator", () => { + const newScenario = deleteGenerator("pu1", TEST_DATA_1); + 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], + }, + }); +}); + +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], + }, + pu2: { + Bus: "b1", + Type: "Profiled", + "Cost ($/MW)": 120, + "Maximum power (MW)": [50, 50, 50, 50, 50], + "Minimum power (MW)": [0, 0, 0, 0, 0], + }, + }); +}); diff --git a/web/src/core/Operations/profiledUnitOps.ts b/web/src/core/Operations/generatorOps.ts similarity index 64% rename from web/src/core/Operations/profiledUnitOps.ts rename to web/src/core/Operations/generatorOps.ts index 6075002..ecac5a4 100644 --- a/web/src/core/Operations/profiledUnitOps.ts +++ b/web/src/core/Operations/generatorOps.ts @@ -6,8 +6,8 @@ import { UnitCommitmentScenario } from "../fixtures"; import { generateTimeslots } from "../../components/Common/Forms/DataTable"; -import { generateUniqueName } from "./busOperations"; import { ValidationError } from "../Validation/validate"; +import { generateUniqueName, renameItemInObject } from "./commonOps"; export const createProfiledUnit = ( scenario: UnitCommitmentScenario, @@ -35,3 +35,25 @@ export const createProfiledUnit = ( 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]; +}; diff --git a/web/src/core/Operations/parameterOperations.test.ts b/web/src/core/Operations/parameterOps.test.ts similarity index 75% rename from web/src/core/Operations/parameterOperations.test.ts rename to web/src/core/Operations/parameterOps.test.ts index 5127369..098007c 100644 --- a/web/src/core/Operations/parameterOperations.test.ts +++ b/web/src/core/Operations/parameterOps.test.ts @@ -8,7 +8,7 @@ import { changeTimeHorizon, changeTimeStep, evaluatePwlFunction, -} from "./parameterOperations"; +} from "./parameterOps"; import assert from "node:assert"; import { TEST_DATA_1, TEST_DATA_2 } from "../fixtures.test"; @@ -31,18 +31,16 @@ test("changeTimeHorizon: Shrink 1", () => { 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, - }, - Buses: { - b1: { "Load (MW)": [30, 30] }, - b2: { "Load (MW)": [10, 20] }, - b3: { "Load (MW)": [0, 30] }, - }, + 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] }, }); }); @@ -91,34 +89,30 @@ test("evaluatePwlFunction", () => { 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, - }, - 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] }, - }, + 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, - }, - Buses: { - b1: { "Load (MW)": [30, 30] }, - b2: { "Load (MW)": [10, 30] }, - b3: { "Load (MW)": [0, 0] }, - }, + 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] }, }); }); diff --git a/web/src/core/Operations/parameterOperations.ts b/web/src/core/Operations/parameterOps.ts similarity index 100% rename from web/src/core/Operations/parameterOperations.ts rename to web/src/core/Operations/parameterOps.ts diff --git a/web/src/core/Operations/profiledUnitOps.test.ts b/web/src/core/Operations/profiledUnitOps.test.ts deleted file mode 100644 index 76dd431..0000000 --- a/web/src/core/Operations/profiledUnitOps.test.ts +++ /dev/null @@ -1,36 +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. - */ - -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/fixtures.test.ts b/web/src/core/fixtures.test.ts index 47d069c..f4714d6 100644 --- a/web/src/core/fixtures.test.ts +++ b/web/src/core/fixtures.test.ts @@ -20,6 +20,13 @@ export const TEST_DATA_1: UnitCommitmentScenario = { "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], + }, }, }; @@ -35,6 +42,7 @@ export const TEST_DATA_2: UnitCommitmentScenario = { b2: { "Load (MW)": [10, 20, 30, 40] }, b3: { "Load (MW)": [0, 30, 0, 40] }, }, + Generators: {}, }; export const TEST_DATA_BLANK: UnitCommitmentScenario = { diff --git a/web/src/core/fixtures.tsx b/web/src/core/fixtures.tsx index 7e5513b..038c91d 100644 --- a/web/src/core/fixtures.tsx +++ b/web/src/core/fixtures.tsx @@ -28,7 +28,7 @@ export interface UnitCommitmentScenario { "Time step (min)": number; }; Buses: Buses; - Generators?: Generators; + Generators: Generators; } export const BLANK_SCENARIO: UnitCommitmentScenario = {