web: Add support for price-sensitive loads

dev
Alinson S. Xavier 2 weeks ago
parent 356046be7b
commit 9f560df4f5

@ -21,6 +21,7 @@ import ThermalUnitsComponent from "./ThermalUnits";
import TransmissionLinesComponent from "./TransmissionLines";
import { UnitCommitmentScenario } from "../../core/Data/types";
import StorageComponent from "./StorageUnits";
import PriceSensitiveLoadsComponent from "./Psload";
export interface CaseBuilderSectionProps {
scenario: UnitCommitmentScenario;
@ -124,6 +125,11 @@ const CaseBuilder = () => {
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<PriceSensitiveLoadsComponent
scenario={scenario}
onDataChanged={onDataChanged}
onError={setToastMessage}
/>
<TransmissionLinesComponent
scenario={scenario}
onDataChanged={onDataChanged}

@ -0,0 +1,175 @@
/*
* 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 DataTable, {
ColumnSpec,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
import FileUploadElement from "../Common/Buttons/FileUploadElement";
import { ValidationError } from "../../core/Data/validate";
import SectionHeader from "../Common/SectionHeader/SectionHeader";
import SectionButton from "../Common/Buttons/SectionButton";
import {
faDownload,
faPlus,
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import { UnitCommitmentScenario } from "../../core/Data/types";
import { ColumnDefinition } from "tabulator-tables";
import {
changePriceSensitiveLoadData,
createPriceSensitiveLoad,
deletePriceSensitiveLoad,
renamePriceSensitiveLoad,
} from "../../core/Operations/psloadOps";
import { offerDownload } from "../Common/io";
export const PriceSensitiveLoadsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
width: 100,
},
{
title: "Bus",
type: "busRef",
width: 100,
},
{
title: "Revenue ($/MW)",
type: "number",
width: 100,
},
{
title: "Demand (MW)",
type: "number[T]",
width: 70,
},
];
export const generatePriceSensitiveLoadsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, PriceSensitiveLoadsColumnSpec);
const data = generateTableData(
scenario["Price-sensitive loads"],
PriceSensitiveLoadsColumnSpec,
scenario,
);
return [data, columns];
};
const PriceSensitiveLoadsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(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 (
<div>
<SectionHeader title="Price-sensitive loads">
<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={() => generatePriceSensitiveLoadsData(props.scenario)}
/>
<FileUploadElement ref={fileUploadElem} accept=".csv" />
</div>
);
};
export default PriceSensitiveLoadsComponent;

@ -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", () => {});

@ -21,4 +21,5 @@ export const BLANK_SCENARIO: UnitCommitmentScenario = {
Generators: {},
"Transmission lines": {},
"Storage units": {},
"Price-sensitive loads": {},
};

@ -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 = <T extends any>(

@ -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", () => {

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

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

@ -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 };
};
Loading…
Cancel
Save