diff --git a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
index de885af..fa18bb5 100644
--- a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
+++ b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
@@ -24,8 +24,10 @@ import { offerDownload } from "../../Common/io";
import FileUploadElement from "../../Common/Buttons/FileUploadElement";
import { useRef } from "react";
import {
+ changeProfiledUnitData,
createProfiledUnit,
deleteGenerator,
+ renameGenerator,
} from "../../../core/Operations/generatorOps";
import { ValidationError } from "../../../core/Validation/validate";
@@ -35,7 +37,7 @@ interface ProfiledUnitsProps {
onError: (msg: string) => void;
}
-const ProfiledUnitsColumnSpec: ColumnSpec[] = [
+export const ProfiledUnitsColumnSpec: ColumnSpec[] = [
{
title: "Name",
type: "string",
@@ -43,7 +45,7 @@ const ProfiledUnitsColumnSpec: ColumnSpec[] = [
},
{
title: "Bus",
- type: "string",
+ type: "busRef",
width: 150,
},
{
@@ -118,6 +120,42 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => {
return null;
};
+ const onDataChanged = (
+ name: string,
+ field: string,
+ newValue: string,
+ ): ValidationError | null => {
+ const [newScenario, err] = changeProfiledUnitData(
+ 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] = renameGenerator(
+ oldName,
+ newName,
+ props.scenario,
+ );
+ if (err) {
+ props.onError(err.message);
+ return err;
+ }
+ props.onDataChanged(newScenario);
+ return null;
+ };
+
return (
@@ -131,12 +169,8 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => {
{
- return null;
- }}
- onDataChanged={() => {
- return null;
- }}
+ onRowRenamed={onRename}
+ onDataChanged={onDataChanged}
generateData={() => generateProfiledUnitsData(props.scenario)}
/>
diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx
index 5134aaf..541b8cd 100644
--- a/web/src/components/Common/Forms/DataTable.tsx
+++ b/web/src/components/Common/Forms/DataTable.tsx
@@ -16,7 +16,7 @@ import Papa from "papaparse";
export interface ColumnSpec {
title: string;
- type: "string" | "number" | "number[]";
+ type: "string" | "number" | "number[]" | "busRef";
width: number;
}
@@ -29,6 +29,7 @@ export const generateTableColumns = (
colSpecs.forEach((spec) => {
switch (spec.type) {
case "string":
+ case "busRef":
columns.push({
...columnsCommonAttrs,
title: spec.title,
@@ -62,7 +63,7 @@ export const generateTableColumns = (
});
break;
default:
- console.error(`Unknown type: ${spec.type}`);
+ throw Error(`Unknown type: ${spec.type}`);
}
});
return columns;
@@ -88,6 +89,7 @@ export const generateTableData = (
switch (spec.type) {
case "string":
case "number":
+ case "busRef":
entry[spec.title] = entryData[spec.title];
break;
case "number[]":
diff --git a/web/src/core/Operations/busOps.test.ts b/web/src/core/Operations/busOps.test.ts
index 8a6c0ae..efb2008 100644
--- a/web/src/core/Operations/busOps.test.ts
+++ b/web/src/core/Operations/busOps.test.ts
@@ -35,7 +35,7 @@ test("changeBusData", () => {
test("changeBusData with invalid numbers", () => {
let [, err] = changeBusData("b1", "Load (MW) 00:00", "xx", TEST_DATA_1);
assert(err !== null);
- assert.equal(err.message, "Invalid value: xx");
+ assert.equal(err.message, '"xx" is not a valid number');
});
test("deleteBus", () => {
diff --git a/web/src/core/Operations/busOps.ts b/web/src/core/Operations/busOps.ts
index 175eba6..d2ebb3b 100644
--- a/web/src/core/Operations/busOps.ts
+++ b/web/src/core/Operations/busOps.ts
@@ -4,10 +4,15 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
-import { UnitCommitmentScenario } from "../fixtures";
+import { Buses, UnitCommitmentScenario } from "../fixtures";
import { ValidationError } from "../Validation/validate";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
-import { generateUniqueName, renameItemInObject } from "./commonOps";
+import {
+ changeData,
+ generateUniqueName,
+ renameItemInObject,
+} from "./commonOps";
+import { BusesColumnSpec } from "../../components/CaseBuilder/Buses/Buses";
export const createBus = (scenario: UnitCommitmentScenario) => {
const name = generateUniqueName(scenario.Buses, "b");
@@ -29,36 +34,24 @@ export const changeBusData = (
newValueStr: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
- // Load (MW)
- const match = field.match(/Load \(MW\) (\d+):(\d+)/);
- if (match) {
- const newValueFloat = parseFloat(newValueStr);
- if (isNaN(newValueFloat)) {
- return [scenario, { message: `Invalid value: ${newValueStr}` }];
- }
-
- // Convert HH:MM to offset
- const hours = parseInt(match[1]!, 10);
- const min = parseInt(match[2]!, 10);
- const idx = (hours * 60 + min) / scenario.Parameters["Time step (min)"];
-
- const newLoad = [...scenario.Buses[bus]!["Load (MW)"]];
- newLoad[idx] = newValueFloat;
- return [
- {
- ...scenario,
- Buses: {
- ...scenario.Buses,
- [bus]: {
- "Load (MW)": newLoad,
- },
- },
- },
- null,
- ];
- }
-
- throw Error(`Unknown field: ${field}`);
+ 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) => {
diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts
index 804800e..eb580da 100644
--- a/web/src/core/Operations/commonOps.ts
+++ b/web/src/core/Operations/commonOps.ts
@@ -5,6 +5,8 @@
*/
import { ValidationError } from "../Validation/validate";
+import { UnitCommitmentScenario } from "../fixtures";
+import { ColumnSpec } from "../../components/Common/Forms/DataTable";
export const renameItemInObject = (
oldName: string,
@@ -37,3 +39,119 @@ export const generateUniqueName = (container: any, prefix: string): string => {
}
return name;
};
+
+const parseNumber = (valueStr: string): [number, ValidationError | null] => {
+ const valueFloat = parseFloat(valueStr);
+ if (isNaN(valueFloat)) {
+ return [0, { message: `"${valueStr}" is not a valid number` }];
+ } else {
+ return [valueFloat, null];
+ }
+};
+
+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 },
+): [{ [key: string]: any }, ValidationError | null] => {
+ // Parse value
+ const [newValueFloat, err] = parseNumber(newValueStr);
+ if (err) return [container, err];
+
+ // Build the new object
+ return [
+ {
+ ...container,
+ [field]: newValueFloat,
+ },
+ null,
+ ];
+};
+
+export const changeNumberVecData = (
+ 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 changeData = (
+ field: string,
+ newValueStr: string,
+ container: { [key: string]: any },
+ colSpecs: ColumnSpec[],
+ scenario: UnitCommitmentScenario,
+): [{ [key: string]: any }, ValidationError | null] => {
+ const match = field.match(/^([^0-9]+)(\d+:\d+)?$/);
+ const fieldName = match![1]!.trim();
+ const fieldTime = 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 changeNumberVecData(
+ fieldName,
+ fieldTime!,
+ newValueStr,
+ container,
+ scenario,
+ );
+ default:
+ throw Error(`Unknown type: ${spec.type}`);
+ }
+ }
+ throw Error(`Unknown field: ${fieldName}`);
+};
diff --git a/web/src/core/Operations/generatorOps.test.ts b/web/src/core/Operations/generatorOps.test.ts
index 1b8a3e7..4302ce3 100644
--- a/web/src/core/Operations/generatorOps.test.ts
+++ b/web/src/core/Operations/generatorOps.test.ts
@@ -7,6 +7,7 @@
import { TEST_DATA_1, TEST_DATA_BLANK } from "../fixtures.test";
import assert from "node:assert";
import {
+ changeProfiledUnitData,
createProfiledUnit,
deleteGenerator,
renameGenerator,
@@ -41,11 +42,56 @@ test("createProfiledUnit", () => {
});
test("createProfiledUnit with blank file", () => {
- const [_, err] = createProfiledUnit(TEST_DATA_BLANK);
+ const [, err] = createProfiledUnit(TEST_DATA_BLANK);
assert(err !== null);
assert.equal(err.message, "Profiled unit requires an existing bus.");
});
+test("changeProfiledUnitData", () => {
+ let scenario = TEST_DATA_1;
+ let err = 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, {
+ pu1: {
+ Bus: "b1",
+ Type: "Profiled",
+ "Cost ($/MW)": 99,
+ "Maximum power (MW)": [10, 12, 13, 99, 20],
+ "Minimum power (MW)": [0, 0, 0, 0, 0],
+ },
+ 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("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.deepEqual(newScenario.Generators, {
diff --git a/web/src/core/Operations/generatorOps.ts b/web/src/core/Operations/generatorOps.ts
index ecac5a4..e99535a 100644
--- a/web/src/core/Operations/generatorOps.ts
+++ b/web/src/core/Operations/generatorOps.ts
@@ -4,10 +4,15 @@
* Released under the modified BSD license. See COPYING.md for more details.
*/
-import { UnitCommitmentScenario } from "../fixtures";
+import { Generators, UnitCommitmentScenario } from "../fixtures";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import { ValidationError } from "../Validation/validate";
-import { generateUniqueName, renameItemInObject } from "./commonOps";
+import {
+ changeData,
+ generateUniqueName,
+ renameItemInObject,
+} from "./commonOps";
+import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits/ProfiledUnits";
export const createProfiledUnit = (
scenario: UnitCommitmentScenario,
@@ -36,6 +41,32 @@ export const createProfiledUnit = (
];
};
+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 deleteGenerator = (
name: string,
scenario: UnitCommitmentScenario,