mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-07 16:58:51 -06:00
web: Reorganize into frontend/backend
This commit is contained in:
71
web/frontend/src/core/Operations/busOps.test.ts
Normal file
71
web/frontend/src/core/Operations/busOps.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { changeBusData, createBus, deleteBus, renameBus } from "./busOps";
|
||||
import assert from "node:assert";
|
||||
import { TEST_DATA_1 } from "../Data/fixtures.test";
|
||||
|
||||
test("createBus", () => {
|
||||
const newScenario = createBus(TEST_DATA_1);
|
||||
assert.deepEqual(Object.keys(newScenario.Buses), ["b1", "b2", "b3", "b4"]);
|
||||
});
|
||||
|
||||
test("changeBusData", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err = null;
|
||||
|
||||
[scenario, err] = changeBusData("b1", "Load (MW) 00:00", "99", scenario);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeBusData("b1", "Load (MW) 03:00", "99", scenario);
|
||||
assert.equal(err, null);
|
||||
|
||||
[scenario, err] = changeBusData("b3", "Load (MW) 04:00", "99", scenario);
|
||||
assert.equal(err, null);
|
||||
|
||||
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] },
|
||||
});
|
||||
});
|
||||
|
||||
test("changeBusData with invalid numbers", () => {
|
||||
let [, err] = changeBusData("b1", "Load (MW) 00:00", "xx", TEST_DATA_1);
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, '"xx" is not a valid number');
|
||||
});
|
||||
|
||||
test("deleteBus", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
scenario = deleteBus("b2", scenario);
|
||||
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("b1", "b99", TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.deepEqual(scenario.Buses, {
|
||||
b99: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 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, 25.4268] },
|
||||
});
|
||||
assert.deepEqual(scenario.Generators["pu1"], {
|
||||
Bus: "b99",
|
||||
Type: "Profiled",
|
||||
"Cost ($/MW)": 12.5,
|
||||
"Maximum power (MW)": [10, 12, 13, 15, 20],
|
||||
"Minimum power (MW)": [0, 0, 0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test("renameBus with duplicated name", () => {
|
||||
let [, err] = renameBus("b3", "b1", TEST_DATA_1);
|
||||
assert(err != null);
|
||||
assert.equal(err.message, `b1 already exists`);
|
||||
});
|
||||
87
web/frontend/src/core/Operations/busOps.ts
Normal file
87
web/frontend/src/core/Operations/busOps.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 { Buses } from "../Data/fixtures";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
|
||||
import {
|
||||
changeData,
|
||||
generateUniqueName,
|
||||
renameItemInObject,
|
||||
} from "./commonOps";
|
||||
import { BusesColumnSpec } from "../../components/CaseBuilder/Buses";
|
||||
import { UnitCommitmentScenario } from "../Data/types";
|
||||
|
||||
export const createBus = (scenario: UnitCommitmentScenario) => {
|
||||
const name = generateUniqueName(scenario.Buses, "b");
|
||||
const timeslots = generateTimeslots(scenario);
|
||||
return {
|
||||
...scenario,
|
||||
Buses: {
|
||||
...scenario.Buses,
|
||||
[name]: {
|
||||
"Load (MW)": Array(timeslots.length).fill(0),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const changeBusData = (
|
||||
bus: string,
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newBus, err] = changeData(
|
||||
field,
|
||||
newValueStr,
|
||||
scenario.Buses[bus]!,
|
||||
BusesColumnSpec,
|
||||
scenario,
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Buses: {
|
||||
...scenario.Buses,
|
||||
[bus]: newBus,
|
||||
} as Buses,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const deleteBus = (bus: string, scenario: UnitCommitmentScenario) => {
|
||||
const { [bus]: _, ...newBuses } = scenario.Buses;
|
||||
const newGenerators = { ...scenario.Generators };
|
||||
|
||||
// Update generators
|
||||
for (const genName in scenario.Generators) {
|
||||
let gen = scenario.Generators[genName]!;
|
||||
if (gen["Bus"] === bus) delete newGenerators[genName];
|
||||
}
|
||||
return { ...scenario, Buses: newBuses, Generators: newGenerators };
|
||||
};
|
||||
|
||||
export const renameBus = (
|
||||
oldName: string,
|
||||
newName: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newBuses, err] = renameItemInObject(oldName, newName, scenario.Buses);
|
||||
if (err) return [scenario, err];
|
||||
|
||||
// Update generators
|
||||
const newGenerators = { ...scenario.Generators };
|
||||
for (const genName in scenario.Generators) {
|
||||
let gen = newGenerators[genName]!;
|
||||
if (gen["Bus"] === oldName) {
|
||||
newGenerators[genName] = { ...gen, Bus: newName };
|
||||
}
|
||||
}
|
||||
return [{ ...scenario, Buses: newBuses, Generators: newGenerators }, null];
|
||||
};
|
||||
30
web/frontend/src/core/Operations/commonOps.test.ts
Normal file
30
web/frontend/src/core/Operations/commonOps.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { parseBool } from "./commonOps";
|
||||
import assert from "node:assert";
|
||||
|
||||
test("parseBool", () => {
|
||||
// True values
|
||||
for (const str of ["true", "TRUE", "1"]) {
|
||||
let [v, err] = parseBool(str);
|
||||
assert(!err);
|
||||
assert.equal(v, true);
|
||||
}
|
||||
|
||||
// False values
|
||||
for (const str of ["false", "FALSE", "0"]) {
|
||||
let [v, err] = parseBool(str);
|
||||
assert(!err);
|
||||
assert.equal(v, false);
|
||||
}
|
||||
|
||||
// Invalid values
|
||||
for (const str of ["qwe", ""]) {
|
||||
let [, err] = parseBool(str);
|
||||
assert(err);
|
||||
}
|
||||
});
|
||||
258
web/frontend/src/core/Operations/commonOps.ts
Normal file
258
web/frontend/src/core/Operations/commonOps.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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 { ColumnSpec } from "../../components/Common/Forms/DataTable";
|
||||
import { UnitCommitmentScenario } from "../Data/types";
|
||||
|
||||
export const renameItemInObject = <T>(
|
||||
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;
|
||||
};
|
||||
|
||||
export const parseNumber = (
|
||||
valueStr: string,
|
||||
): [number, ValidationError | null] => {
|
||||
if (valueStr === "") {
|
||||
return [0, { message: "Field must not be blank" }];
|
||||
}
|
||||
const valueFloat = parseFloat(valueStr);
|
||||
if (isNaN(valueFloat)) {
|
||||
return [0, { message: `"${valueStr}" is not a valid number` }];
|
||||
} else {
|
||||
return [valueFloat, null];
|
||||
}
|
||||
};
|
||||
|
||||
export const parseNullableNumber = (
|
||||
valueStr: string,
|
||||
): [number | null, ValidationError | null] => {
|
||||
if (valueStr === "") return [null, null];
|
||||
return parseNumber(valueStr);
|
||||
};
|
||||
|
||||
export const parseBool = (
|
||||
valueStr: string,
|
||||
): [boolean, ValidationError | null] => {
|
||||
if (["true", "1"].includes(valueStr.toLowerCase())) {
|
||||
return [true, null];
|
||||
}
|
||||
if (["false", "0"].includes(valueStr.toLowerCase())) {
|
||||
return [false, null];
|
||||
}
|
||||
return [true, { message: `"${valueStr}" is not a valid boolean value` }];
|
||||
};
|
||||
|
||||
export const changeStringData = (
|
||||
field: string,
|
||||
newValue: string,
|
||||
container: { [key: string]: any },
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
return [
|
||||
{
|
||||
...container,
|
||||
[field]: newValue,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeBusRefData = (
|
||||
field: string,
|
||||
newValue: string,
|
||||
container: { [key: string]: any },
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
if (!(newValue in scenario.Buses)) {
|
||||
return [scenario, { message: `Bus "${newValue}" does not exist` }];
|
||||
}
|
||||
return changeStringData(field, newValue, container);
|
||||
};
|
||||
|
||||
export const changeNumberData = (
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
container: { [key: string]: any },
|
||||
nullable: boolean = false,
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
// Parse value
|
||||
const [newValueFloat, err] = nullable
|
||||
? parseNullableNumber(newValueStr)
|
||||
: parseNumber(newValueStr);
|
||||
if (err) return [container, err];
|
||||
|
||||
// Build the new object
|
||||
return [
|
||||
{
|
||||
...container,
|
||||
[field]: newValueFloat,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeBooleanData = (
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
container: { [key: string]: any },
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
// Parse value
|
||||
const [newValueBool, err] = parseBool(newValueStr);
|
||||
if (err) return [container, err];
|
||||
|
||||
// Build the new object
|
||||
return [
|
||||
{
|
||||
...container,
|
||||
[field]: newValueBool,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeNumberVecTData = (
|
||||
field: string,
|
||||
time: string,
|
||||
newValueStr: string,
|
||||
container: { [key: string]: any },
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
// Parse value
|
||||
const [newValueFloat, err] = parseNumber(newValueStr);
|
||||
if (err) return [container, err];
|
||||
|
||||
// Convert HH:MM to offset
|
||||
const hours = parseInt(time.split(":")[0]!, 10);
|
||||
const min = parseInt(time.split(":")[1]!, 10);
|
||||
const idx = (hours * 60 + min) / scenario.Parameters["Time step (min)"];
|
||||
|
||||
// Build the new vector
|
||||
const newVec = [...container[field]];
|
||||
newVec[idx] = newValueFloat;
|
||||
return [
|
||||
{
|
||||
...container,
|
||||
[field]: newVec,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeNumberVecNData = (
|
||||
field: string,
|
||||
offset: string,
|
||||
newValueStr: string,
|
||||
container: { [key: string]: any },
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
const oldVec = container[field];
|
||||
const newVec = [...container[field]];
|
||||
const idx = parseInt(offset) - 1;
|
||||
|
||||
if (newValueStr === "") {
|
||||
// Trim the vector
|
||||
newVec.splice(idx, oldVec.length - idx);
|
||||
} else {
|
||||
// Parse new value
|
||||
const [newValueFloat, err] = parseNumber(newValueStr);
|
||||
if (err) return [container, err];
|
||||
|
||||
// Increase the length of the vector
|
||||
if (idx >= oldVec.length) {
|
||||
for (let i = oldVec.length; i < idx; i++) {
|
||||
newVec[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign new value
|
||||
newVec[idx] = newValueFloat;
|
||||
}
|
||||
return [
|
||||
{
|
||||
...container,
|
||||
[field]: newVec,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeData = (
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
container: { [key: string]: any },
|
||||
colSpecs: ColumnSpec[],
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [{ [key: string]: any }, ValidationError | null] => {
|
||||
const match = field.match(/^([^0-9]+)([0-9:]+)?$/);
|
||||
const fieldName = match![1]!.trim();
|
||||
const fieldOffset = match![2];
|
||||
for (const spec of colSpecs) {
|
||||
if (spec.title !== fieldName) continue;
|
||||
switch (spec.type) {
|
||||
case "string":
|
||||
return changeStringData(fieldName, newValueStr, container);
|
||||
case "busRef":
|
||||
return changeBusRefData(fieldName, newValueStr, container, scenario);
|
||||
case "number":
|
||||
return changeNumberData(fieldName, newValueStr, container);
|
||||
case "number?":
|
||||
return changeNumberData(fieldName, newValueStr, container, true);
|
||||
case "number[T]":
|
||||
return changeNumberVecTData(
|
||||
fieldName,
|
||||
fieldOffset!,
|
||||
newValueStr,
|
||||
container,
|
||||
scenario,
|
||||
);
|
||||
case "number[N]":
|
||||
return changeNumberVecNData(
|
||||
fieldName,
|
||||
fieldOffset!,
|
||||
newValueStr,
|
||||
container,
|
||||
);
|
||||
case "boolean":
|
||||
return changeBooleanData(fieldName, newValueStr, container);
|
||||
default:
|
||||
throw Error(`Unknown type: ${spec.type}`);
|
||||
}
|
||||
}
|
||||
throw Error(`Unknown field: ${fieldName}`);
|
||||
};
|
||||
export const assertBusesNotEmpty = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
): ValidationError | null => {
|
||||
if (Object.keys(scenario.Buses).length === 0)
|
||||
return { message: "This component requires an existing bus." };
|
||||
return null;
|
||||
};
|
||||
151
web/frontend/src/core/Operations/generatorOps.test.ts
Normal file
151
web/frontend/src/core/Operations/generatorOps.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 "../Data/fixtures.test";
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
changeProfiledUnitData,
|
||||
changeThermalUnitData,
|
||||
createProfiledUnit,
|
||||
createThermalUnit,
|
||||
deleteGenerator,
|
||||
renameGenerator,
|
||||
} from "./generatorOps";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
|
||||
test("createProfiledUnit", () => {
|
||||
const [newScenario, err] = createProfiledUnit(TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.equal(Object.keys(newScenario.Generators).length, 4);
|
||||
assert("pu3" in newScenario.Generators);
|
||||
});
|
||||
|
||||
test("createThermalUnit", () => {
|
||||
const [newScenario, err] = createThermalUnit(TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.equal(Object.keys(newScenario.Generators).length, 4);
|
||||
assert("g2" in newScenario.Generators);
|
||||
});
|
||||
|
||||
test("createProfiledUnit with blank file", () => {
|
||||
const [, err] = createProfiledUnit(TEST_DATA_BLANK);
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "This component requires an existing bus.");
|
||||
});
|
||||
|
||||
test("changeProfiledUnitData", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err: ValidationError | null;
|
||||
[scenario, err] = changeProfiledUnitData(
|
||||
"pu1",
|
||||
"Cost ($/MW)",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeProfiledUnitData(
|
||||
"pu1",
|
||||
"Maximum power (MW) 03:00",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeProfiledUnitData("pu2", "Bus", "b3", scenario);
|
||||
assert.equal(err, null);
|
||||
assert.deepEqual(scenario.Generators["pu2"], {
|
||||
Bus: "b3",
|
||||
Type: "Profiled",
|
||||
"Cost ($/MW)": 120,
|
||||
"Maximum power (MW)": [50, 50, 50, 50, 50],
|
||||
"Minimum power (MW)": [0, 0, 0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test("changeThermalUnitData", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err: ValidationError | null;
|
||||
[scenario, err] = changeThermalUnitData(
|
||||
"g1",
|
||||
"Ramp up limit (MW)",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert(!err);
|
||||
[scenario, err] = changeThermalUnitData(
|
||||
"g1",
|
||||
"Startup costs ($) 2",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert(!err);
|
||||
[scenario, err] = changeThermalUnitData(
|
||||
"g1",
|
||||
"Production cost curve ($) 7",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert(!err);
|
||||
[scenario, err] = changeThermalUnitData(
|
||||
"g1",
|
||||
"Production cost curve (MW) 3",
|
||||
"",
|
||||
scenario,
|
||||
);
|
||||
assert(!err);
|
||||
[scenario, err] = changeThermalUnitData("g1", "Must run?", "true", scenario);
|
||||
assert(!err);
|
||||
assert.deepEqual(scenario.Generators["g1"], {
|
||||
Bus: "b1",
|
||||
Type: "Thermal",
|
||||
"Production cost curve (MW)": [100.0, 110],
|
||||
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0, 0, 0, 99],
|
||||
"Startup costs ($)": [300.0, 99.0],
|
||||
"Startup delays (h)": [1, 4],
|
||||
"Ramp up limit (MW)": 99,
|
||||
"Ramp down limit (MW)": 232.68,
|
||||
"Startup limit (MW)": 232.68,
|
||||
"Shutdown limit (MW)": 232.68,
|
||||
"Minimum downtime (h)": 4,
|
||||
"Minimum uptime (h)": 4,
|
||||
"Initial status (h)": 12,
|
||||
"Initial power (MW)": 115,
|
||||
"Must run?": true,
|
||||
});
|
||||
});
|
||||
|
||||
test("changeProfiledUnitData with invalid bus", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err = null;
|
||||
[scenario, err] = changeProfiledUnitData("pu1", "Bus", "b99", scenario);
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, 'Bus "b99" does not exist');
|
||||
});
|
||||
|
||||
test("deleteGenerator", () => {
|
||||
const newScenario = deleteGenerator("pu1", TEST_DATA_1);
|
||||
assert.equal(Object.keys(newScenario.Generators).length, 2);
|
||||
assert("g1" in newScenario.Generators);
|
||||
assert("pu2" in newScenario.Generators);
|
||||
});
|
||||
|
||||
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],
|
||||
});
|
||||
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],
|
||||
});
|
||||
});
|
||||
152
web/frontend/src/core/Operations/generatorOps.ts
Normal file
152
web/frontend/src/core/Operations/generatorOps.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 { generateTimeslots } from "../../components/Common/Forms/DataTable";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
import {
|
||||
assertBusesNotEmpty,
|
||||
changeData,
|
||||
generateUniqueName,
|
||||
renameItemInObject,
|
||||
} from "./commonOps";
|
||||
import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits";
|
||||
import { ThermalUnitsColumnSpec } from "../../components/CaseBuilder/ThermalUnits";
|
||||
import { Generators, UnitCommitmentScenario } from "../Data/types";
|
||||
|
||||
export const createProfiledUnit = (
|
||||
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.Generators, "pu");
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Generators: {
|
||||
...scenario.Generators,
|
||||
[name]: {
|
||||
Bus: busName,
|
||||
Type: "Profiled",
|
||||
"Cost ($/MW)": 0,
|
||||
"Minimum power (MW)": Array(timeslots.length).fill(0),
|
||||
"Maximum power (MW)": Array(timeslots.length).fill(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const createThermalUnit = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const err = assertBusesNotEmpty(scenario);
|
||||
if (err) return [scenario, err];
|
||||
const busName = Object.keys(scenario.Buses)[0]!;
|
||||
const name = generateUniqueName(scenario.Generators, "g");
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Generators: {
|
||||
...scenario.Generators,
|
||||
[name]: {
|
||||
Bus: busName,
|
||||
Type: "Thermal",
|
||||
"Production cost curve (MW)": [0, 100],
|
||||
"Production cost curve ($)": [0, 10],
|
||||
"Startup costs ($)": [0],
|
||||
"Startup delays (h)": [1],
|
||||
"Ramp up limit (MW)": null,
|
||||
"Ramp down limit (MW)": null,
|
||||
"Startup limit (MW)": null,
|
||||
"Shutdown limit (MW)": null,
|
||||
"Minimum downtime (h)": 1,
|
||||
"Minimum uptime (h)": 1,
|
||||
"Initial status (h)": -24,
|
||||
"Initial power (MW)": 0,
|
||||
"Must run?": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeProfiledUnitData = (
|
||||
generator: string,
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newGen, err] = changeData(
|
||||
field,
|
||||
newValueStr,
|
||||
scenario.Generators[generator]!,
|
||||
ProfiledUnitsColumnSpec,
|
||||
scenario,
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Generators: {
|
||||
...scenario.Generators,
|
||||
[generator]: newGen,
|
||||
} as Generators,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeThermalUnitData = (
|
||||
generator: string,
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newGen, err] = changeData(
|
||||
field,
|
||||
newValueStr,
|
||||
scenario.Generators[generator]!,
|
||||
ThermalUnitsColumnSpec,
|
||||
scenario,
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Generators: {
|
||||
...scenario.Generators,
|
||||
[generator]: newGen,
|
||||
} as Generators,
|
||||
},
|
||||
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];
|
||||
};
|
||||
137
web/frontend/src/core/Operations/parameterOps.test.ts
Normal file
137
web/frontend/src/core/Operations/parameterOps.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 {
|
||||
changeTimeHorizon,
|
||||
changeTimeStep,
|
||||
evaluatePwlFunction,
|
||||
} from "./parameterOps";
|
||||
import assert from "node:assert";
|
||||
import { TEST_DATA_1, TEST_DATA_2 } from "../Data/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,
|
||||
});
|
||||
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] },
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
assert.deepEqual(newScenario.Buses, {
|
||||
b1: { "Load (MW)": [30, 30] },
|
||||
b2: { "Load (MW)": [10, 20] },
|
||||
b3: { "Load (MW)": [0, 30] },
|
||||
});
|
||||
});
|
||||
|
||||
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.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],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("changeTimeHorizon invalid", () => {
|
||||
let [, err] = changeTimeHorizon(TEST_DATA_1, "x");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Invalid value: x");
|
||||
|
||||
[, err] = changeTimeHorizon(TEST_DATA_1, "-3");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Invalid value: -3");
|
||||
});
|
||||
|
||||
test("evaluatePwlFunction", () => {
|
||||
const data_x = [0, 60, 120, 180];
|
||||
const data_y = [100, 200, 250, 100];
|
||||
assert.equal(evaluatePwlFunction(data_x, data_y, 0), 100);
|
||||
assert.equal(evaluatePwlFunction(data_x, data_y, 15), 125);
|
||||
assert.equal(evaluatePwlFunction(data_x, data_y, 30), 150);
|
||||
assert.equal(evaluatePwlFunction(data_x, data_y, 60), 200);
|
||||
assert.equal(evaluatePwlFunction(data_x, data_y, 180), 100);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
assert.deepEqual(scenario.Buses, {
|
||||
b1: { "Load (MW)": [30, 30] },
|
||||
b2: { "Load (MW)": [10, 30] },
|
||||
b3: { "Load (MW)": [0, 0] },
|
||||
});
|
||||
});
|
||||
|
||||
test("changeTimeStep invalid", () => {
|
||||
let [, err] = changeTimeStep(TEST_DATA_2, "x");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Invalid value: x");
|
||||
|
||||
[, err] = changeTimeStep(TEST_DATA_2, "-10");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Invalid value: -10");
|
||||
|
||||
[, err] = changeTimeStep(TEST_DATA_2, "120");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Invalid value: 120");
|
||||
|
||||
[, err] = changeTimeStep(TEST_DATA_2, "7");
|
||||
assert(err !== null);
|
||||
assert.equal(err.message, "Time step must be a divisor of 60: 7");
|
||||
});
|
||||
|
||||
export {};
|
||||
221
web/frontend/src/core/Operations/parameterOps.ts
Normal file
221
web/frontend/src/core/Operations/parameterOps.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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 { Buses } from "../Data/fixtures";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
import { UnitCommitmentScenario } from "../Data/types";
|
||||
|
||||
export const changeTimeHorizon = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
newTimeHorizonStr: string,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
// Parse string
|
||||
const newTimeHorizon = parseInt(newTimeHorizonStr);
|
||||
if (isNaN(newTimeHorizon) || newTimeHorizon <= 0) {
|
||||
return [scenario, { message: `Invalid value: ${newTimeHorizonStr}` }];
|
||||
}
|
||||
const newScenario = JSON.parse(
|
||||
JSON.stringify(scenario),
|
||||
) as UnitCommitmentScenario;
|
||||
newScenario.Parameters["Time horizon (h)"] = newTimeHorizon;
|
||||
const newT = (newTimeHorizon * 60) / scenario.Parameters["Time step (min)"];
|
||||
const oldT =
|
||||
(scenario.Parameters["Time horizon (h)"] * 60) /
|
||||
scenario.Parameters["Time step (min)"];
|
||||
if (newT < oldT) {
|
||||
Object.values(newScenario.Buses).forEach((bus) => {
|
||||
bus["Load (MW)"] = bus["Load (MW)"].slice(0, newT);
|
||||
});
|
||||
Object.values(newScenario.Generators).forEach((generator) => {
|
||||
if (generator.Type === "Profiled") {
|
||||
generator["Minimum power (MW)"] = generator["Minimum power (MW)"].slice(0, newT);
|
||||
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) => {
|
||||
bus["Load (MW)"] = bus["Load (MW)"].concat(padding);
|
||||
});
|
||||
Object.values(newScenario.Generators).forEach((generator) => {
|
||||
if (generator.Type === "Profiled") {
|
||||
generator["Minimum power (MW)"] = generator["Minimum power (MW)"].concat(padding);
|
||||
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];
|
||||
};
|
||||
|
||||
export const evaluatePwlFunction = (
|
||||
data_x: number[],
|
||||
data_y: number[],
|
||||
x: number,
|
||||
) => {
|
||||
if (x < data_x[0]! || x > data_x[data_x.length - 1]!) {
|
||||
throw Error("PWL interpolation: Out of bounds");
|
||||
}
|
||||
|
||||
if (x === data_x[0]) return data_y[0];
|
||||
|
||||
// Binary search to find the interval containing x
|
||||
let low = 0;
|
||||
let high = data_x.length - 1;
|
||||
while (low < high) {
|
||||
let mid = Math.floor((low + high) / 2);
|
||||
if (data_x[mid]! < x) low = mid + 1;
|
||||
else high = mid;
|
||||
}
|
||||
|
||||
// Linear interpolation within the found interval
|
||||
const x1 = data_x[low - 1]!;
|
||||
const y1 = data_y[low - 1]!;
|
||||
const x2 = data_x[low]!;
|
||||
const y2 = data_y[low]!;
|
||||
|
||||
return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1);
|
||||
};
|
||||
|
||||
export const changeTimeStep = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
newTimeStepStr: string,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
// Parse string and perform validation
|
||||
const newTimeStep = parseFloat(newTimeStepStr);
|
||||
if (isNaN(newTimeStep) || newTimeStep < 1 || newTimeStep > 60) {
|
||||
return [scenario, { message: `Invalid value: ${newTimeStepStr}` }];
|
||||
}
|
||||
if (60 % newTimeStep !== 0) {
|
||||
return [
|
||||
scenario,
|
||||
{ message: `Time step must be a divisor of 60: ${newTimeStepStr}` },
|
||||
];
|
||||
}
|
||||
|
||||
// Build data_x
|
||||
let timeHorizon = scenario.Parameters["Time horizon (h)"];
|
||||
const oldTimeStep = scenario.Parameters["Time step (min)"];
|
||||
const oldT = (timeHorizon * 60) / oldTimeStep;
|
||||
const newT = (timeHorizon * 60) / newTimeStep;
|
||||
const data_x = Array(oldT + 1).fill(0);
|
||||
for (let i = 0; i <= oldT; i++) data_x[i] = i * oldTimeStep;
|
||||
|
||||
const newBuses: Buses = {};
|
||||
for (const busName in scenario.Buses) {
|
||||
// Build data_y
|
||||
const busLoad = scenario.Buses[busName]!["Load (MW)"];
|
||||
const data_y = Array(oldT + 1).fill(0);
|
||||
for (let i = 0; i < oldT; i++) data_y[i] = busLoad[i];
|
||||
data_y[oldT] = data_y[0];
|
||||
|
||||
// Run interpolation
|
||||
const newBusLoad = Array(newT).fill(0);
|
||||
for (let i = 0; i < newT; i++) {
|
||||
newBusLoad[i] = evaluatePwlFunction(data_x, data_y, newTimeStep * i);
|
||||
}
|
||||
newBuses[busName] = {
|
||||
...scenario.Buses[busName],
|
||||
"Load (MW)": newBusLoad,
|
||||
};
|
||||
}
|
||||
|
||||
const newGenerators: { [name: string]: any } = {};
|
||||
for (const generatorName in scenario.Generators) {
|
||||
const generator = scenario.Generators[generatorName]!;
|
||||
if (generator.Type === "Profiled") {
|
||||
// Build data_y for minimum power
|
||||
const minPower = generator["Minimum power (MW)"];
|
||||
const minData_y = Array(oldT + 1).fill(0);
|
||||
for (let i = 0; i < oldT; i++) minData_y[i] = minPower[i];
|
||||
minData_y[oldT] = minData_y[0];
|
||||
|
||||
// Build data_y for maximum power
|
||||
const maxPower = generator["Maximum power (MW)"];
|
||||
const maxData_y = Array(oldT + 1).fill(0);
|
||||
for (let i = 0; i < oldT; i++) maxData_y[i] = maxPower[i];
|
||||
maxData_y[oldT] = maxData_y[0];
|
||||
|
||||
// Run interpolation for both
|
||||
const newMinPower = Array(newT).fill(0);
|
||||
const newMaxPower = Array(newT).fill(0);
|
||||
for (let i = 0; i < newT; i++) {
|
||||
newMinPower[i] = evaluatePwlFunction(data_x, minData_y, newTimeStep * i);
|
||||
newMaxPower[i] = evaluatePwlFunction(data_x, maxData_y, newTimeStep * i);
|
||||
}
|
||||
|
||||
newGenerators[generatorName] = {
|
||||
...generator,
|
||||
"Minimum power (MW)": newMinPower,
|
||||
"Maximum power (MW)": newMaxPower,
|
||||
};
|
||||
} else {
|
||||
newGenerators[generatorName] = generator;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Parameters: {
|
||||
...scenario.Parameters,
|
||||
"Time step (min)": newTimeStep,
|
||||
},
|
||||
Buses: newBuses,
|
||||
Generators: newGenerators,
|
||||
"Price-sensitive loads": newPriceSensitiveLoads,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeParameter = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
key: string,
|
||||
valueStr: string,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const value = parseFloat(valueStr);
|
||||
if (isNaN(value)) {
|
||||
return [scenario, { message: `Invalid value: ${valueStr}` }];
|
||||
}
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
Parameters: {
|
||||
...scenario.Parameters,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
46
web/frontend/src/core/Operations/preprocessing.test.ts
Normal file
46
web/frontend/src/core/Operations/preprocessing.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 assert from "node:assert";
|
||||
import { preprocess } from "./preprocessing";
|
||||
|
||||
export const PREPROCESSING_TEST_DATA_1: any = {
|
||||
Parameters: {
|
||||
Version: "0.4",
|
||||
"Time horizon (h)": 5,
|
||||
},
|
||||
Buses: {
|
||||
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
|
||||
b2: { "Load (MW)": 10 },
|
||||
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
|
||||
},
|
||||
};
|
||||
|
||||
test("preprocess", () => {
|
||||
const [newScenario, err] = preprocess(PREPROCESSING_TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.deepEqual(newScenario, {
|
||||
Parameters: {
|
||||
Version: "0.4",
|
||||
"Time horizon (h)": 5,
|
||||
"Power balance penalty ($/MW)": 1000,
|
||||
"Scenario name": "s1",
|
||||
"Scenario weight": 1,
|
||||
"Time step (min)": 60,
|
||||
},
|
||||
Buses: {
|
||||
b1: { "Load (MW)": [35.79534, 34.38835, 33.45083, 32.89729, 33.25044] },
|
||||
b2: { "Load (MW)": [10, 10, 10, 10, 10] },
|
||||
b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] },
|
||||
},
|
||||
"Price-sensitive loads": {},
|
||||
"Storage units": {},
|
||||
"Transmission lines": {},
|
||||
Contingencies: {},
|
||||
Generators: {},
|
||||
Reserves: {},
|
||||
});
|
||||
});
|
||||
70
web/frontend/src/core/Operations/preprocessing.ts
Normal file
70
web/frontend/src/core/Operations/preprocessing.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { validate, ValidationError } from "../Data/validate";
|
||||
import { UnitCommitmentScenario } from "../Data/types";
|
||||
import { migrate } from "../Data/migrate";
|
||||
import {
|
||||
getContingencyTransmissionLines,
|
||||
rebuildContingencies,
|
||||
} from "./transmissionOps";
|
||||
|
||||
export const preprocess = (
|
||||
data: any,
|
||||
): [UnitCommitmentScenario | null, ValidationError | null] => {
|
||||
// Make a copy of the original data
|
||||
let result = JSON.parse(JSON.stringify(data));
|
||||
|
||||
// Run migration
|
||||
migrate(result);
|
||||
|
||||
// Run JSON validation and assign default values
|
||||
if (!validate(result)) {
|
||||
console.error(validate.errors);
|
||||
return [
|
||||
null,
|
||||
{ message: "Invalid JSON file. See console for more details." },
|
||||
];
|
||||
}
|
||||
|
||||
// Expand scalars into arrays
|
||||
// @ts-ignore
|
||||
const timeHorizon = result["Parameters"]["Time horizon (h)"];
|
||||
// @ts-ignore
|
||||
const timeStep = result["Parameters"]["Time step (min)"];
|
||||
const T = (timeHorizon * 60) / timeStep;
|
||||
for (const busName in result["Buses"]) {
|
||||
// @ts-ignore
|
||||
const busData = result["Buses"][busName];
|
||||
const busLoad = busData["Load (MW)"];
|
||||
if (typeof busLoad === "number") {
|
||||
busData["Load (MW)"] = Array(T).fill(busLoad);
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional fields
|
||||
for (let field of [
|
||||
"Buses",
|
||||
"Generators",
|
||||
"Storage units",
|
||||
"Price-sensitive loads",
|
||||
"Transmission lines",
|
||||
"Reserves",
|
||||
"Contingencies",
|
||||
]) {
|
||||
if (!result[field]) {
|
||||
result[field] = {};
|
||||
}
|
||||
}
|
||||
|
||||
const scenario = result as unknown as UnitCommitmentScenario;
|
||||
|
||||
// Rebuild contingencies
|
||||
const contingencyLines = getContingencyTransmissionLines(scenario);
|
||||
scenario["Contingencies"] = rebuildContingencies(contingencyLines);
|
||||
|
||||
return [scenario, null];
|
||||
};
|
||||
60
web/frontend/src/core/Operations/psloadOps.test.ts
Normal file
60
web/frontend/src/core/Operations/psloadOps.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
88
web/frontend/src/core/Operations/psloadOps.ts
Normal file
88
web/frontend/src/core/Operations/psloadOps.ts
Normal file
@@ -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 };
|
||||
};
|
||||
75
web/frontend/src/core/Operations/storageOps.test.ts
Normal file
75
web/frontend/src/core/Operations/storageOps.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 {
|
||||
changeStorageUnitData,
|
||||
createStorageUnit,
|
||||
deleteStorageUnit,
|
||||
renameStorageUnit,
|
||||
} from "./storageOps";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
|
||||
test("createStorageUnit", () => {
|
||||
const [newScenario, err] = createStorageUnit(TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.equal(Object.keys(newScenario["Storage units"]).length, 2);
|
||||
assert("su2" in newScenario["Storage units"]);
|
||||
});
|
||||
|
||||
test("renameStorageUnit", () => {
|
||||
const [newScenario, err] = renameStorageUnit("su1", "su2", TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.deepEqual(
|
||||
newScenario["Storage units"]["su2"],
|
||||
TEST_DATA_1["Storage units"]["su1"],
|
||||
);
|
||||
assert.equal(Object.keys(newScenario["Storage units"]).length, 1);
|
||||
});
|
||||
|
||||
test("changeStorageUnitData", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err: ValidationError | null;
|
||||
[scenario, err] = changeStorageUnitData("su1", "Bus", "b3", scenario);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeStorageUnitData(
|
||||
"su1",
|
||||
"Minimum level (MWh)",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeStorageUnitData(
|
||||
"su1",
|
||||
"Maximum discharge rate (MW)",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
assert.deepEqual(scenario["Storage units"]["su1"], {
|
||||
Bus: "b3",
|
||||
"Minimum level (MWh)": 99.0,
|
||||
"Maximum level (MWh)": 100.0,
|
||||
"Charge cost ($/MW)": 2.0,
|
||||
"Discharge cost ($/MW)": 1.0,
|
||||
"Charge efficiency": 0.8,
|
||||
"Discharge efficiency": 0.85,
|
||||
"Loss factor": 0.01,
|
||||
"Minimum charge rate (MW)": 5.0,
|
||||
"Maximum charge rate (MW)": 10.0,
|
||||
"Minimum discharge rate (MW)": 4.0,
|
||||
"Maximum discharge rate (MW)": 99.0,
|
||||
"Initial level (MWh)": 20.0,
|
||||
"Last period minimum level (MWh)": 21.0,
|
||||
"Last period maximum level (MWh)": 22.0,
|
||||
});
|
||||
});
|
||||
|
||||
test("deleteStorageUnit", () => {
|
||||
const newScenario = deleteStorageUnit("su1", TEST_DATA_1);
|
||||
assert.equal(Object.keys(newScenario["Storage units"]).length, 0);
|
||||
});
|
||||
98
web/frontend/src/core/Operations/storageOps.ts
Normal file
98
web/frontend/src/core/Operations/storageOps.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { StorageUnit, UnitCommitmentScenario } from "../Data/types";
|
||||
import {
|
||||
assertBusesNotEmpty,
|
||||
changeData,
|
||||
generateUniqueName,
|
||||
renameItemInObject,
|
||||
} from "./commonOps";
|
||||
import { StorageUnitsColumnSpec } from "../../components/CaseBuilder/StorageUnits";
|
||||
|
||||
export const createStorageUnit = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const err = assertBusesNotEmpty(scenario);
|
||||
if (err) return [scenario, err];
|
||||
const busName = Object.keys(scenario.Buses)[0]!;
|
||||
const name = generateUniqueName(scenario["Storage units"], "su");
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
"Storage units": {
|
||||
...scenario["Storage units"],
|
||||
[name]: {
|
||||
Bus: busName,
|
||||
"Minimum level (MWh)": 0,
|
||||
"Maximum level (MWh)": 1,
|
||||
"Charge cost ($/MW)": 0.0,
|
||||
"Discharge cost ($/MW)": 0.0,
|
||||
"Charge efficiency": 1,
|
||||
"Discharge efficiency": 1,
|
||||
"Loss factor": 0,
|
||||
"Minimum charge rate (MW)": 1,
|
||||
"Maximum charge rate (MW)": 1,
|
||||
"Minimum discharge rate (MW)": 1,
|
||||
"Maximum discharge rate (MW)": 1,
|
||||
"Initial level (MWh)": 0,
|
||||
"Last period minimum level (MWh)": 0,
|
||||
"Last period maximum level (MWh)": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const renameStorageUnit = (
|
||||
oldName: string,
|
||||
newName: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newObj, err] = renameItemInObject(
|
||||
oldName,
|
||||
newName,
|
||||
scenario["Storage units"],
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [{ ...scenario, "Storage units": newObj }, null];
|
||||
};
|
||||
|
||||
export const changeStorageUnitData = (
|
||||
name: string,
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newObj, err] = changeData(
|
||||
field,
|
||||
newValueStr,
|
||||
scenario["Storage units"][name]!,
|
||||
StorageUnitsColumnSpec,
|
||||
scenario,
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
"Storage units": {
|
||||
...scenario["Storage units"],
|
||||
[name]: newObj as StorageUnit,
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const deleteStorageUnit = (
|
||||
name: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): UnitCommitmentScenario => {
|
||||
const { [name]: _, ...newContainer } = scenario["Storage units"];
|
||||
return { ...scenario, "Storage units": newContainer };
|
||||
};
|
||||
102
web/frontend/src/core/Operations/transmissionOps.test.ts
Normal file
102
web/frontend/src/core/Operations/transmissionOps.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 {
|
||||
changeTransmissionLineData,
|
||||
createTransmissionLine,
|
||||
deleteTransmissionLine,
|
||||
getContingencyTransmissionLines,
|
||||
rebuildContingencies,
|
||||
renameTransmissionLine,
|
||||
} from "./transmissionOps";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
|
||||
test("createTransmissionLine", () => {
|
||||
const [newScenario, err] = createTransmissionLine(TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 2);
|
||||
assert("l2" in newScenario["Transmission lines"]);
|
||||
});
|
||||
|
||||
test("renameTransmissionLine", () => {
|
||||
const [newScenario, err] = renameTransmissionLine("l1", "l3", TEST_DATA_1);
|
||||
assert(err === null);
|
||||
assert.deepEqual(newScenario["Transmission lines"]["l3"], {
|
||||
"Source bus": "b1",
|
||||
"Target bus": "b2",
|
||||
"Susceptance (S)": 29.49686,
|
||||
"Normal flow limit (MW)": 15000.0,
|
||||
"Emergency flow limit (MW)": 20000.0,
|
||||
"Flow limit penalty ($/MW)": 5000.0,
|
||||
});
|
||||
assert.deepEqual(newScenario["Contingencies"], {
|
||||
l3: {
|
||||
"Affected lines": ["l3"],
|
||||
"Affected generators": [],
|
||||
},
|
||||
});
|
||||
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 1);
|
||||
});
|
||||
|
||||
test("changeTransmissionLineData", () => {
|
||||
let scenario = TEST_DATA_1;
|
||||
let err: ValidationError | null;
|
||||
[scenario, err] = changeTransmissionLineData(
|
||||
"l1",
|
||||
"Source bus",
|
||||
"b3",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeTransmissionLineData(
|
||||
"l1",
|
||||
"Normal flow limit (MW)",
|
||||
"99",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
[scenario, err] = changeTransmissionLineData(
|
||||
"l1",
|
||||
"Target bus",
|
||||
"b1",
|
||||
scenario,
|
||||
);
|
||||
assert.equal(err, null);
|
||||
assert.deepEqual(scenario["Transmission lines"]["l1"], {
|
||||
"Source bus": "b3",
|
||||
"Target bus": "b1",
|
||||
"Susceptance (S)": 29.49686,
|
||||
"Normal flow limit (MW)": 99,
|
||||
"Emergency flow limit (MW)": 20000.0,
|
||||
"Flow limit penalty ($/MW)": 5000.0,
|
||||
});
|
||||
});
|
||||
|
||||
test("deleteTransmissionLine", () => {
|
||||
const newScenario = deleteTransmissionLine("l1", TEST_DATA_1);
|
||||
assert.equal(Object.keys(newScenario["Transmission lines"]).length, 0);
|
||||
assert.equal(Object.keys(newScenario["Contingencies"]).length, 0);
|
||||
});
|
||||
|
||||
test("getContingencyTransmissionLines", () => {
|
||||
const contLines = getContingencyTransmissionLines(TEST_DATA_1);
|
||||
assert.deepEqual(contLines, new Set(["l1"]));
|
||||
});
|
||||
|
||||
test("rebuildContingencies", () => {
|
||||
assert.deepEqual(rebuildContingencies(new Set(["l1", "l2"])), {
|
||||
l1: {
|
||||
"Affected lines": ["l1"],
|
||||
"Affected generators": [],
|
||||
},
|
||||
l2: {
|
||||
"Affected lines": ["l2"],
|
||||
"Affected generators": [],
|
||||
},
|
||||
});
|
||||
});
|
||||
163
web/frontend/src/core/Operations/transmissionOps.ts
Normal file
163
web/frontend/src/core/Operations/transmissionOps.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 {
|
||||
assertBusesNotEmpty,
|
||||
changeData,
|
||||
generateUniqueName,
|
||||
parseBool,
|
||||
renameItemInObject,
|
||||
} from "./commonOps";
|
||||
import { ValidationError } from "../Data/validate";
|
||||
import { TransmissionLinesColumnSpec } from "../../components/CaseBuilder/TransmissionLines";
|
||||
import {
|
||||
Contingency,
|
||||
TransmissionLine,
|
||||
UnitCommitmentScenario,
|
||||
} from "../Data/types";
|
||||
|
||||
export const createTransmissionLine = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const err = assertBusesNotEmpty(scenario);
|
||||
if (err) return [scenario, err];
|
||||
const busName = Object.keys(scenario.Buses)[0]!;
|
||||
const name = generateUniqueName(scenario["Transmission lines"], "l");
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
"Transmission lines": {
|
||||
...scenario["Transmission lines"],
|
||||
[name]: {
|
||||
"Source bus": busName,
|
||||
"Target bus": busName,
|
||||
"Susceptance (S)": 1.0,
|
||||
"Normal flow limit (MW)": 1000,
|
||||
"Emergency flow limit (MW)": 1500,
|
||||
"Flow limit penalty ($/MW)": 5000.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const renameTransmissionLine = (
|
||||
oldName: string,
|
||||
newName: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
const [newLine, err] = renameItemInObject(
|
||||
oldName,
|
||||
newName,
|
||||
scenario["Transmission lines"],
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
|
||||
// Update transmission line contingencies
|
||||
let newContingencies = scenario["Contingencies"];
|
||||
const contingencyLines = getContingencyTransmissionLines(scenario);
|
||||
if (contingencyLines.has(oldName)) {
|
||||
contingencyLines.delete(oldName);
|
||||
contingencyLines.add(newName);
|
||||
newContingencies = rebuildContingencies(contingencyLines);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
"Transmission lines": newLine,
|
||||
Contingencies: newContingencies,
|
||||
},
|
||||
null,
|
||||
];
|
||||
};
|
||||
|
||||
export const changeTransmissionLineData = (
|
||||
line: string,
|
||||
field: string,
|
||||
newValueStr: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||
if (field === "Contingency?") {
|
||||
// Parse boolean value
|
||||
const [newValue, err] = parseBool(newValueStr);
|
||||
if (err) return [scenario, err];
|
||||
|
||||
// Rebuild contingencies
|
||||
const contLines = getContingencyTransmissionLines(scenario);
|
||||
if (newValue) contLines.add(line);
|
||||
else contLines.delete(line);
|
||||
const newContingencies = rebuildContingencies(contLines);
|
||||
|
||||
return [{ ...scenario, Contingencies: newContingencies }, null];
|
||||
} else {
|
||||
const [newLine, err] = changeData(
|
||||
field,
|
||||
newValueStr,
|
||||
scenario["Transmission lines"][line]!,
|
||||
TransmissionLinesColumnSpec,
|
||||
scenario,
|
||||
);
|
||||
if (err) return [scenario, err];
|
||||
return [
|
||||
{
|
||||
...scenario,
|
||||
"Transmission lines": {
|
||||
...scenario["Transmission lines"],
|
||||
[line]: newLine as TransmissionLine,
|
||||
},
|
||||
},
|
||||
null,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTransmissionLine = (
|
||||
name: string,
|
||||
scenario: UnitCommitmentScenario,
|
||||
): UnitCommitmentScenario => {
|
||||
const { [name]: _, ...newLines } = scenario["Transmission lines"];
|
||||
|
||||
// Update transmission line contingencies
|
||||
let newContingencies = scenario["Contingencies"];
|
||||
const contingencyLines = getContingencyTransmissionLines(scenario);
|
||||
if (contingencyLines.has(name)) {
|
||||
contingencyLines.delete(name);
|
||||
newContingencies = rebuildContingencies(contingencyLines);
|
||||
}
|
||||
|
||||
return {
|
||||
...scenario,
|
||||
"Transmission lines": newLines,
|
||||
Contingencies: newContingencies,
|
||||
};
|
||||
};
|
||||
|
||||
export const getContingencyTransmissionLines = (
|
||||
scenario: UnitCommitmentScenario,
|
||||
): Set<String> => {
|
||||
let result: Set<String> = new Set();
|
||||
Object.entries(scenario.Contingencies).forEach(([name, contingency]) => {
|
||||
if (contingency["Affected lines"].length !== 1)
|
||||
throw Error("not implemented");
|
||||
result.add(contingency["Affected lines"][0]!!);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const rebuildContingencies = (
|
||||
contingencyLines: Set<String>,
|
||||
): { [name: string]: Contingency } => {
|
||||
const result: { [name: string]: Contingency } = {};
|
||||
contingencyLines.forEach((lineName) => {
|
||||
result[lineName as string] = {
|
||||
"Affected lines": [lineName as string],
|
||||
"Affected generators": [],
|
||||
};
|
||||
});
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user