(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 (
+
+
+
+
+
+
+ generatePriceSensitiveLoadsData(props.scenario)}
+ />
+
+
+ );
+};
+
+export default PriceSensitiveLoadsComponent;
diff --git a/web/src/core/Data/fixtures.test.ts b/web/src/core/Data/fixtures.test.ts
index 350fe77..2a331b3 100644
--- a/web/src/core/Data/fixtures.test.ts
+++ b/web/src/core/Data/fixtures.test.ts
@@ -80,6 +80,13 @@ export const TEST_DATA_1: UnitCommitmentScenario = {
"Last period maximum level (MWh)": 22.0,
},
},
+ "Price-sensitive loads": {
+ ps1: {
+ Bus: "b3",
+ "Revenue ($/MW)": 23.0,
+ "Demand (MW)": [50, 50, 50, 50, 50],
+ },
+ },
};
export const TEST_DATA_2: UnitCommitmentScenario = {
@@ -97,6 +104,7 @@ export const TEST_DATA_2: UnitCommitmentScenario = {
Generators: {},
"Transmission lines": {},
"Storage units": {},
+ "Price-sensitive loads": {},
};
export const TEST_DATA_BLANK: UnitCommitmentScenario = {
@@ -110,6 +118,7 @@ export const TEST_DATA_BLANK: UnitCommitmentScenario = {
Generators: {},
"Transmission lines": {},
"Storage units": {},
+ "Price-sensitive loads": {},
};
test("fixtures", () => {});
diff --git a/web/src/core/Data/fixtures.tsx b/web/src/core/Data/fixtures.tsx
index 3b2cf73..9b6a870 100644
--- a/web/src/core/Data/fixtures.tsx
+++ b/web/src/core/Data/fixtures.tsx
@@ -21,4 +21,5 @@ export const BLANK_SCENARIO: UnitCommitmentScenario = {
Generators: {},
"Transmission lines": {},
"Storage units": {},
+ "Price-sensitive loads": {},
};
diff --git a/web/src/core/Data/types.tsx b/web/src/core/Data/types.tsx
index c49433f..3d11661 100644
--- a/web/src/core/Data/types.tsx
+++ b/web/src/core/Data/types.tsx
@@ -63,6 +63,12 @@ export interface StorageUnit {
"Last period maximum level (MWh)": number;
}
+export interface PriceSensitiveLoad {
+ Bus: string;
+ "Revenue ($/MW)": number;
+ "Demand (MW)": number[];
+}
+
export interface UnitCommitmentScenario {
Parameters: {
Version: string;
@@ -78,6 +84,9 @@ export interface UnitCommitmentScenario {
"Storage units": {
[name: string]: StorageUnit;
};
+ "Price-sensitive loads": {
+ [name: string]: PriceSensitiveLoad;
+ };
}
const getTypedGenerators = (
diff --git a/web/src/core/Operations/generatorOps.test.ts b/web/src/core/Operations/generatorOps.test.ts
index ecb1f12..fd5a44b 100644
--- a/web/src/core/Operations/generatorOps.test.ts
+++ b/web/src/core/Operations/generatorOps.test.ts
@@ -33,7 +33,7 @@ test("createThermalUnit", () => {
test("createProfiledUnit with blank file", () => {
const [, err] = createProfiledUnit(TEST_DATA_BLANK);
assert(err !== null);
- assert.equal(err.message, "Profiled unit requires an existing bus.");
+ assert.equal(err.message, "This component requires an existing bus.");
});
test("changeProfiledUnitData", () => {
diff --git a/web/src/core/Operations/parameterOps.ts b/web/src/core/Operations/parameterOps.ts
index 175b626..7a14890 100644
--- a/web/src/core/Operations/parameterOps.ts
+++ b/web/src/core/Operations/parameterOps.ts
@@ -35,6 +35,9 @@ export const changeTimeHorizon = (
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) => {
@@ -46,6 +49,9 @@ export const changeTimeHorizon = (
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];
};
@@ -156,6 +162,28 @@ export const changeTimeStep = (
}
}
+ 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,
@@ -165,6 +193,7 @@ export const changeTimeStep = (
},
Buses: newBuses,
Generators: newGenerators,
+ "Price-sensitive loads": newPriceSensitiveLoads,
},
null,
];
diff --git a/web/src/core/Operations/psloadOps.test.ts b/web/src/core/Operations/psloadOps.test.ts
new file mode 100644
index 0000000..8efd344
--- /dev/null
+++ b/web/src/core/Operations/psloadOps.test.ts
@@ -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);
+});
diff --git a/web/src/core/Operations/psloadOps.ts b/web/src/core/Operations/psloadOps.ts
new file mode 100644
index 0000000..62bc170
--- /dev/null
+++ b/web/src/core/Operations/psloadOps.ts
@@ -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 };
+};