From eb3d39b1abc1db6682c92da38532f2244672fe21 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 25 Jun 2025 13:59:07 -0500 Subject: [PATCH] web: ThermalUnits: onDataChanged --- .../components/CaseBuilder/ThermalUnits.tsx | 23 +++--- web/src/components/Common/Forms/DataTable.tsx | 7 +- web/src/core/Operations/commonOps.ts | 75 +++++++++++++++++-- web/src/core/Operations/generatorOps.test.ts | 53 +++++++++++++ web/src/core/Operations/generatorOps.ts | 27 +++++++ 5 files changed, 165 insertions(+), 20 deletions(-) diff --git a/web/src/components/CaseBuilder/ThermalUnits.tsx b/web/src/components/CaseBuilder/ThermalUnits.tsx index 77dcc10..eb5286d 100644 --- a/web/src/components/CaseBuilder/ThermalUnits.tsx +++ b/web/src/components/CaseBuilder/ThermalUnits.tsx @@ -30,6 +30,7 @@ import { import { ColumnDefinition } from "tabulator-tables"; import { offerDownload } from "../Common/io"; import { + changeThermalUnitData, createThermalUnit, deleteGenerator, renameGenerator, @@ -194,17 +195,17 @@ const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => { field: string, newValue: string, ): ValidationError | null => { - // const [newScenario, err] = changeThermalUnitData( - // name, - // field, - // newValue, - // props.scenario, - // ); - // if (err) { - // props.onError(err.message); - // return err; - // } - // props.onDataChanged(newScenario); + const [newScenario, err] = changeThermalUnitData( + name, + field, + newValue, + props.scenario, + ); + if (err) { + props.onError(err.message); + return err; + } + props.onDataChanged(newScenario); return null; }; diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx index a2e6a54..992ebac 100644 --- a/web/src/components/Common/Forms/DataTable.tsx +++ b/web/src/components/Common/Forms/DataTable.tsx @@ -350,10 +350,9 @@ const DataTable = (props: DataTableProps) => { useEffect(() => { const onCellEdited = (cell: CellComponent) => { - let newValue = cell.getValue(); - let oldValue = cell.getOldValue(); - // eslint-disable-next-line eqeqeq - if (newValue == oldValue) return; + let newValue = `${cell.getValue()}`; + let oldValue = `${cell.getOldValue()}`; + if (newValue === oldValue) return; if (cell.getField() === "Name") { if (newValue === "") { const err = props.onRowDeleted(oldValue); diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts index e8854ac..e0cf187 100644 --- a/web/src/core/Operations/commonOps.ts +++ b/web/src/core/Operations/commonOps.ts @@ -108,7 +108,26 @@ export const changeNumberData = ( ]; }; -export const changeNumberVecData = ( +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, @@ -136,6 +155,43 @@ export const changeNumberVecData = ( ]; }; +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, @@ -143,9 +199,9 @@ export const changeData = ( colSpecs: ColumnSpec[], scenario: UnitCommitmentScenario, ): [{ [key: string]: any }, ValidationError | null] => { - const match = field.match(/^([^0-9]+)(\d+:\d+)?$/); + const match = field.match(/^([^0-9]+)([0-9:]+)?$/); const fieldName = match![1]!.trim(); - const fieldTime = match![2]; + const fieldOffset = match![2]; for (const spec of colSpecs) { if (spec.title !== fieldName) continue; switch (spec.type) { @@ -156,13 +212,22 @@ export const changeData = ( case "number": return changeNumberData(fieldName, newValueStr, container); case "number[T]": - return changeNumberVecData( + return changeNumberVecTData( fieldName, - fieldTime!, + 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}`); } diff --git a/web/src/core/Operations/generatorOps.test.ts b/web/src/core/Operations/generatorOps.test.ts index 1badef1..5664518 100644 --- a/web/src/core/Operations/generatorOps.test.ts +++ b/web/src/core/Operations/generatorOps.test.ts @@ -8,6 +8,7 @@ import { TEST_DATA_1, TEST_DATA_BLANK } from "../fixtures.test"; import assert from "node:assert"; import { changeProfiledUnitData, + changeThermalUnitData, createProfiledUnit, createThermalUnit, deleteGenerator, @@ -63,6 +64,58 @@ test("changeProfiledUnitData", () => { }); }); +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; diff --git a/web/src/core/Operations/generatorOps.ts b/web/src/core/Operations/generatorOps.ts index d8ceda0..10ee096 100644 --- a/web/src/core/Operations/generatorOps.ts +++ b/web/src/core/Operations/generatorOps.ts @@ -13,6 +13,7 @@ import { renameItemInObject, } from "./commonOps"; import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits"; +import { ThermalUnitsColumnSpec } from "../../components/CaseBuilder/ThermalUnits"; const assertBusesNotEmpty = ( scenario: UnitCommitmentScenario, @@ -109,6 +110,32 @@ export const changeProfiledUnitData = ( ]; }; +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,