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 = {