From eb862e5701c962f53b88dac337965362a602dd3b Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Mon, 23 Jun 2025 10:57:46 -0500 Subject: [PATCH] web: Update busOperations to support time-indexed loads --- .../components/Common/Forms/DataTable.test.ts | 10 ++-- web/src/core/Operations/busOperations.test.ts | 49 ++++--------------- web/src/core/Operations/busOperations.ts | 9 +++- .../Operations/parameterOperations.test.ts | 24 ++++----- web/src/core/fixtures.test.ts | 30 ++++++++++++ 5 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 web/src/core/fixtures.test.ts diff --git a/web/src/components/Common/Forms/DataTable.test.ts b/web/src/components/Common/Forms/DataTable.test.ts index b3cdcb8..e4c2e1d 100644 --- a/web/src/components/Common/Forms/DataTable.test.ts +++ b/web/src/components/Common/Forms/DataTable.test.ts @@ -5,13 +5,13 @@ */ import assert from "node:assert"; -import { BUS_TEST_DATA_1 } from "../../../core/Operations/busOperations.test"; import { parseBusesCsv } from "../../CaseBuilder/Buses/BusesCsv"; import { generateBusesData } from "../../CaseBuilder/Buses/Buses"; import { generateCsv } from "./DataTable"; +import { TEST_DATA_1 } from "../../../core/fixtures.test"; test("generate CSV", () => { - const [data, columns] = generateBusesData(BUS_TEST_DATA_1); + const [data, columns] = generateBusesData(TEST_DATA_1); const actualCsv = generateCsv(data, columns); const expectedCsv = "Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" + @@ -26,7 +26,7 @@ test("parse valid CSV", () => { // "Name,Load 0,Load 1,Load 2,Load 3,Load 4\n" + // "b1,0,1,2,3,4\n" + // "b3,27.3729,26.29698,25.58005,25.15675,25.4268"; - // const newScenario = parseBusesCsv(BUS_TEST_DATA_1, csvContents); + // const newScenario = parseBusesCsv(FixturesTest, csvContents); // assert.deepEqual(newScenario.Buses, { // b1: { // "Load (MW)": [0, 1, 2, 3, 4], @@ -43,7 +43,7 @@ test("parse invalid CSV (wrong headers)", () => { "b1,0,1,2,3,4\n" + "b3,27.3729,26.29698,25.58005,25.15675,25.4268"; expect(() => { - parseBusesCsv(BUS_TEST_DATA_1, csvContents); + parseBusesCsv(TEST_DATA_1, csvContents); }).toThrow(Error); }); @@ -53,6 +53,6 @@ test("parse invalid CSV (wrong data length)", () => { "b1,0,1,2,3\n" + "b3,27.3729,26.29698,25.58005,25.15675,25.4268"; expect(() => { - parseBusesCsv(BUS_TEST_DATA_1, csvContents); + parseBusesCsv(TEST_DATA_1, csvContents); }).toThrow(Error); }); diff --git a/web/src/core/Operations/busOperations.test.ts b/web/src/core/Operations/busOperations.test.ts index 9563674..20263e6 100644 --- a/web/src/core/Operations/busOperations.test.ts +++ b/web/src/core/Operations/busOperations.test.ts @@ -4,7 +4,6 @@ * Released under the modified BSD license. See COPYING.md for more details. */ -import { UnitCommitmentScenario } from "../fixtures"; import { changeBusData, createBus, @@ -12,51 +11,23 @@ import { renameBus, } from "./busOperations"; import assert from "node:assert"; - -export const BUS_TEST_DATA_1: UnitCommitmentScenario = { - Parameters: { - Version: "0.4", - "Power balance penalty ($/MW)": 1000.0, - "Time horizon (h)": 5, - "Time step (min)": 60, - }, - Buses: { - b1: { "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] }, - }, -}; - -export const BUS_TEST_DATA_2: UnitCommitmentScenario = { - Parameters: { - Version: "0.4", - "Power balance penalty ($/MW)": 1000.0, - "Time horizon (h)": 2, - "Time step (min)": 30, - }, - Buses: { - b1: { "Load (MW)": [30, 30, 30, 30] }, - b2: { "Load (MW)": [10, 20, 30, 40] }, - b3: { "Load (MW)": [0, 30, 0, 40] }, - }, -}; +import { TEST_DATA_1 } from "../fixtures.test"; test("createBus", () => { - const newScenario = createBus(BUS_TEST_DATA_1); + const newScenario = createBus(TEST_DATA_1); assert.deepEqual(Object.keys(newScenario.Buses), ["b1", "b2", "b3", "b4"]); }); test("changeBusData", () => { - let scenario = BUS_TEST_DATA_1; + let scenario = TEST_DATA_1; let err = null; - [scenario, err] = changeBusData("b1", "Load 0", "99", scenario); + [scenario, err] = changeBusData("b1", "Load (MW) 00:00", "99", scenario); assert.equal(err, null); - - [scenario, err] = changeBusData("b1", "Load 3", "99", scenario); + [scenario, err] = changeBusData("b1", "Load (MW) 03:00", "99", scenario); assert.equal(err, null); - [scenario, err] = changeBusData("b3", "Load 4", "99", scenario); + [scenario, err] = changeBusData("b3", "Load (MW) 04:00", "99", scenario); assert.equal(err, null); assert.deepEqual(scenario, { @@ -75,13 +46,13 @@ test("changeBusData", () => { }); test("changeBusData with invalid numbers", () => { - let [, err] = changeBusData("b1", "Load 0", "xx", BUS_TEST_DATA_1); + let [, err] = changeBusData("b1", "Load (MW) 00:00", "xx", TEST_DATA_1); assert(err !== null); assert.equal(err.message, "Invalid value: xx"); }); test("deleteBus", () => { - let scenario = BUS_TEST_DATA_1; + let scenario = TEST_DATA_1; scenario = deleteBus("b2", scenario); assert.deepEqual(scenario, { Parameters: { @@ -98,7 +69,7 @@ test("deleteBus", () => { }); test("renameBus", () => { - let [scenario, err] = renameBus("b2", "b99", BUS_TEST_DATA_1); + let [scenario, err] = renameBus("b2", "b99", TEST_DATA_1); assert(err === null); assert.deepEqual(scenario, { Parameters: { @@ -116,7 +87,7 @@ test("renameBus", () => { }); test("renameBus with duplicated name", () => { - let [, err] = renameBus("b3", "b1", BUS_TEST_DATA_1); + let [, err] = renameBus("b3", "b1", TEST_DATA_1); assert(err != null); assert.equal(err.message, `Bus b1 already exists`); }); diff --git a/web/src/core/Operations/busOperations.ts b/web/src/core/Operations/busOperations.ts index ffccc0c..74b0bf8 100644 --- a/web/src/core/Operations/busOperations.ts +++ b/web/src/core/Operations/busOperations.ts @@ -46,13 +46,18 @@ export const changeBusData = ( scenario: UnitCommitmentScenario, ): [UnitCommitmentScenario, ValidationError | null] => { // Load (MW) - const match = field.match(/Load (\d+)/); + const match = field.match(/Load \(MW\) (\d+):(\d+)/); if (match) { const newValueFloat = parseFloat(newValueStr); if (isNaN(newValueFloat)) { return [scenario, { message: `Invalid value: ${newValueStr}` }]; } - const idx = parseInt(match[1]!, 10); + + // 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 [ diff --git a/web/src/core/Operations/parameterOperations.test.ts b/web/src/core/Operations/parameterOperations.test.ts index 29f82b7..c7ae767 100644 --- a/web/src/core/Operations/parameterOperations.test.ts +++ b/web/src/core/Operations/parameterOperations.test.ts @@ -9,11 +9,11 @@ import { changeTimeStep, evaluatePwlFunction, } from "./parameterOperations"; -import { BUS_TEST_DATA_1, BUS_TEST_DATA_2 } from "./busOperations.test"; import assert from "node:assert"; +import { TEST_DATA_1, TEST_DATA_2 } from "../fixtures.test"; test("changeTimeHorizon: Shrink 1", () => { - const [newScenario, err] = changeTimeHorizon(BUS_TEST_DATA_1, "3"); + const [newScenario, err] = changeTimeHorizon(TEST_DATA_1, "3"); assert(err === null); assert.deepEqual(newScenario, { Parameters: { @@ -31,7 +31,7 @@ test("changeTimeHorizon: Shrink 1", () => { }); test("changeTimeHorizon: Shrink 2", () => { - const [newScenario, err] = changeTimeHorizon(BUS_TEST_DATA_2, "1"); + const [newScenario, err] = changeTimeHorizon(TEST_DATA_2, "1"); assert(err === null); assert.deepEqual(newScenario, { Parameters: { @@ -49,7 +49,7 @@ test("changeTimeHorizon: Shrink 2", () => { }); test("changeTimeHorizon grow", () => { - const [newScenario, err] = changeTimeHorizon(BUS_TEST_DATA_1, "7"); + const [newScenario, err] = changeTimeHorizon(TEST_DATA_1, "7"); assert(err === null); assert.deepEqual(newScenario, { Parameters: { @@ -73,11 +73,11 @@ test("changeTimeHorizon grow", () => { }); test("changeTimeHorizon invalid", () => { - let [, err] = changeTimeHorizon(BUS_TEST_DATA_1, "x"); + let [, err] = changeTimeHorizon(TEST_DATA_1, "x"); assert(err !== null); assert.equal(err.message, "Invalid value: x"); - [, err] = changeTimeHorizon(BUS_TEST_DATA_1, "-3"); + [, err] = changeTimeHorizon(TEST_DATA_1, "-3"); assert(err !== null); assert.equal(err.message, "Invalid value: -3"); }); @@ -93,7 +93,7 @@ test("evaluatePwlFunction", () => { }); test("changeTimeStep", () => { - let [scenario, err] = changeTimeStep(BUS_TEST_DATA_2, "15"); + let [scenario, err] = changeTimeStep(TEST_DATA_2, "15"); assert(err === null); assert.deepEqual(scenario, { Parameters: { @@ -109,7 +109,7 @@ test("changeTimeStep", () => { }, }); - [scenario, err] = changeTimeStep(BUS_TEST_DATA_2, "60"); + [scenario, err] = changeTimeStep(TEST_DATA_2, "60"); assert(err === null); assert.deepEqual(scenario, { Parameters: { @@ -127,19 +127,19 @@ test("changeTimeStep", () => { }); test("changeTimeStep invalid", () => { - let [, err] = changeTimeStep(BUS_TEST_DATA_2, "x"); + let [, err] = changeTimeStep(TEST_DATA_2, "x"); assert(err !== null); assert.equal(err.message, "Invalid value: x"); - [, err] = changeTimeStep(BUS_TEST_DATA_2, "-10"); + [, err] = changeTimeStep(TEST_DATA_2, "-10"); assert(err !== null); assert.equal(err.message, "Invalid value: -10"); - [, err] = changeTimeStep(BUS_TEST_DATA_2, "120"); + [, err] = changeTimeStep(TEST_DATA_2, "120"); assert(err !== null); assert.equal(err.message, "Invalid value: 120"); - [, err] = changeTimeStep(BUS_TEST_DATA_2, "7"); + [, err] = changeTimeStep(TEST_DATA_2, "7"); assert(err !== null); assert.equal(err.message, "Time step must be a divisor of 60: 7"); }); diff --git a/web/src/core/fixtures.test.ts b/web/src/core/fixtures.test.ts new file mode 100644 index 0000000..e0cc41d --- /dev/null +++ b/web/src/core/fixtures.test.ts @@ -0,0 +1,30 @@ +import { UnitCommitmentScenario } from "./fixtures"; + +export const TEST_DATA_1: UnitCommitmentScenario = { + Parameters: { + Version: "0.4", + "Power balance penalty ($/MW)": 1000.0, + "Time horizon (h)": 5, + "Time step (min)": 60, + }, + Buses: { + b1: { "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] }, + }, +}; +export const TEST_DATA_2: UnitCommitmentScenario = { + Parameters: { + Version: "0.4", + "Power balance penalty ($/MW)": 1000.0, + "Time horizon (h)": 2, + "Time step (min)": 30, + }, + Buses: { + b1: { "Load (MW)": [30, 30, 30, 30] }, + b2: { "Load (MW)": [10, 20, 30, 40] }, + b3: { "Load (MW)": [0, 30, 0, 40] }, + }, +}; + +test("fixtures", () => {});