diff --git a/web/src/components/CaseBuilder/ProfiledUnits.test.ts b/web/src/components/CaseBuilder/ProfiledUnits.test.ts new file mode 100644 index 0000000..1939219 --- /dev/null +++ b/web/src/components/CaseBuilder/ProfiledUnits.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { + floatFormatter, + generateTableColumns, + parseCsv, +} from "../Common/Forms/DataTable"; +import { + parseProfiledUnitsCsv, + ProfiledUnitsColumnSpec, +} from "./ProfiledUnits"; +import { TEST_DATA_1 } from "../../core/fixtures.test"; +import assert from "node:assert"; +import { + getProfiledGenerators, + getThermalGenerators, +} from "../../core/fixtures"; + +test("parse CSV", () => { + const csvContents = + "Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," + + "Maximum power (MW) 02:00,Maximum power (MW) 03:00," + + "Maximum power (MW) 04:00,Minimum power (MW) 00:00," + + "Minimum power (MW) 01:00,Minimum power (MW) 02:00," + + "Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" + + "pu1,b1,50,260.25384545,72.89148068,377.17886108,336.66732361," + + "376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" + + "pu2,b1,0,0,0,0,0,0,0,0,0,0,0"; + const [scenario, err] = parseProfiledUnitsCsv(csvContents, TEST_DATA_1); + assert(err === null); + const thermalGens = getThermalGenerators(scenario); + const profGens = getProfiledGenerators(scenario); + assert.equal(Object.keys(thermalGens).length, 1); + assert.equal(Object.keys(profGens).length, 2); + + assert.deepEqual(profGens, { + pu1: { + Bus: "b1", + "Minimum power (MW)": [ + 52.05076909, 14.57829614, 75.43577222, 67.33346472, 75.36556352, + ], + "Maximum power (MW)": [ + 260.25384545, 72.89148068, 377.17886108, 336.66732361, 376.82781758, + ], + "Cost ($/MW)": 50.0, + Type: "Profiled", + }, + pu2: { + Bus: "b1", + "Minimum power (MW)": [0, 0, 0, 0, 0], + "Maximum power (MW)": [0, 0, 0, 0, 0], + "Cost ($/MW)": 0.0, + Type: "Profiled", + }, + }); +}); + +test("parse CSV with invalid bus", () => { + const csvContents = + "Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," + + "Maximum power (MW) 02:00,Maximum power (MW) 03:00," + + "Maximum power (MW) 04:00,Minimum power (MW) 00:00," + + "Minimum power (MW) 01:00,Minimum power (MW) 02:00," + + "Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" + + "pu1,b99,50,260.25384545,72.89148068,377.17886108,336.66732361," + + "376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" + + "pu2,b1,0,0,0,0,0,0,0,0,0,0,0"; + const [, err] = parseCsv(csvContents, ProfiledUnitsColumnSpec, TEST_DATA_1); + assert(err !== null); + assert.equal(err.message, 'Bus "b99" does not exist (row 1)'); +}); + +test("generateTableColumns", () => { + const columns = generateTableColumns(TEST_DATA_1, ProfiledUnitsColumnSpec); + assert.equal(columns.length, 5); + assert.deepEqual(columns[0], { + editor: "input", + editorParams: { + selectContents: true, + }, + field: "Name", + formatter: "plaintext", + headerHozAlign: "left", + headerSort: false, + headerWordWrap: true, + hozAlign: "left", + minWidth: 100, + resizable: false, + title: "Name", + }); + assert.equal(columns[3]!["columns"]!.length, 5); + assert.deepEqual(columns[3]!["columns"]![0], { + editor: "input", + editorParams: { + selectContents: true, + }, + field: "Maximum power (MW) 00:00", + formatter: floatFormatter, + headerHozAlign: "left", + headerSort: false, + headerWordWrap: true, + hozAlign: "left", + minWidth: 60, + resizable: false, + title: "00:00", + }); +}); diff --git a/web/src/components/CaseBuilder/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits.tsx index 361c9ae..162b66e 100644 --- a/web/src/components/CaseBuilder/ProfiledUnits.tsx +++ b/web/src/components/CaseBuilder/ProfiledUnits.tsx @@ -20,6 +20,7 @@ import DataTable, { } from "../Common/Forms/DataTable"; import { getProfiledGenerators, + getThermalGenerators, UnitCommitmentScenario, } from "../../core/fixtures"; import { ColumnDefinition } from "tabulator-tables"; @@ -75,6 +76,31 @@ const generateProfiledUnitsData = ( return [data, columns]; }; +export const parseProfiledUnitsCsv = ( + csvContents: string, + scenario: UnitCommitmentScenario, +): [UnitCommitmentScenario, ValidationError | null] => { + const [profGens, err] = parseCsv( + csvContents, + ProfiledUnitsColumnSpec, + scenario, + ); + if (err) return [scenario, err]; + + // Process imported generators + for (const gen in profGens) { + profGens[gen]["Type"] = "Profiled"; + } + + // Merge with existing data + const thermalGens = getThermalGenerators(scenario); + const newScenario = { + ...scenario, + Generators: { ...thermalGens, ...profGens }, + }; + return [newScenario, null]; +}; + const ProfiledUnitsComponent = (props: CaseBuilderSectionProps) => { const fileUploadElem = useRef(null); @@ -85,24 +111,12 @@ const ProfiledUnitsComponent = (props: CaseBuilderSectionProps) => { }; const onUpload = () => { - fileUploadElem.current!.showFilePicker((csvContents: any) => { - const [newGenerators, err] = parseCsv( - csvContents, - ProfiledUnitsColumnSpec, - props.scenario, - ); + fileUploadElem.current!.showFilePicker((csv: any) => { + const [newScenario, err] = parseProfiledUnitsCsv(csv, props.scenario); if (err) { props.onError(err.message); return; } - for (const gen in newGenerators) { - newGenerators[gen]["Type"] = "Profiled"; - } - - const newScenario = { - ...props.scenario, - Generators: newGenerators, - }; props.onDataChanged(newScenario); }); }; diff --git a/web/src/components/Common/Forms/DataTable.test.ts b/web/src/components/Common/Forms/DataTable.test.ts index 1c29976..ccc92b7 100644 --- a/web/src/components/Common/Forms/DataTable.test.ts +++ b/web/src/components/Common/Forms/DataTable.test.ts @@ -14,46 +14,9 @@ import { parseCsv, } from "./DataTable"; import { TEST_DATA_1 } from "../../../core/fixtures.test"; -import { ProfiledUnitsColumnSpec } from "../../CaseBuilder/ProfiledUnits"; import { ThermalUnitsColumnSpec } from "../../CaseBuilder/ThermalUnits"; import { getThermalGenerators } from "../../../core/fixtures"; -test("generateTableColumns (ProfiledUnits)", () => { - const columns = generateTableColumns(TEST_DATA_1, ProfiledUnitsColumnSpec); - assert.equal(columns.length, 5); - assert.deepEqual(columns[0], { - editor: "input", - editorParams: { - selectContents: true, - }, - field: "Name", - formatter: "plaintext", - headerHozAlign: "left", - headerSort: false, - headerWordWrap: true, - hozAlign: "left", - minWidth: 100, - resizable: false, - title: "Name", - }); - assert.equal(columns[3]!["columns"]!.length, 5); - assert.deepEqual(columns[3]!["columns"]![0], { - editor: "input", - editorParams: { - selectContents: true, - }, - field: "Maximum power (MW) 00:00", - formatter: floatFormatter, - headerHozAlign: "left", - headerSort: false, - headerWordWrap: true, - hozAlign: "left", - minWidth: 60, - resizable: false, - title: "00:00", - }); -}); - test("generateTableColumns (ThermalUnits)", () => { const columns = generateTableColumns(TEST_DATA_1, ThermalUnitsColumnSpec); assert.equal(columns[2]!["columns"]!.length, 10); @@ -81,7 +44,7 @@ test("generateTableData (ThermalUnits)", () => { TEST_DATA_1, ); assert.deepEqual(data[0], { - Name: "gen1", + Name: "g1", Bus: "b1", "Initial power (MW)": 115, "Initial status (h)": 12, @@ -121,6 +84,7 @@ test("generateTableData (ThermalUnits)", () => { "Startup delays (h) 3": "", "Startup delays (h) 4": "", "Startup delays (h) 5": "", + "Must run?": false, }); }); @@ -161,54 +125,3 @@ test("parse CSV with duplicated names", () => { assert(err !== null); assert.equal(err.message, `Name "b1" is duplicated (row 2)`); }); - -test("parse CSV (Profiled Units)", () => { - const csvContents = - "Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," + - "Maximum power (MW) 02:00,Maximum power (MW) 03:00," + - "Maximum power (MW) 04:00,Minimum power (MW) 00:00," + - "Minimum power (MW) 01:00,Minimum power (MW) 02:00," + - "Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" + - "pu1,b1,50,260.25384545,72.89148068,377.17886108,336.66732361," + - "376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" + - "pu2,b1,0,0,0,0,0,0,0,0,0,0,0"; - const [newGenerators, err] = parseCsv( - csvContents, - ProfiledUnitsColumnSpec, - TEST_DATA_1, - ); - assert(err === null); - assert.deepEqual(newGenerators, { - pu1: { - Bus: "b1", - "Minimum power (MW)": [ - 52.05076909, 14.57829614, 75.43577222, 67.33346472, 75.36556352, - ], - "Maximum power (MW)": [ - 260.25384545, 72.89148068, 377.17886108, 336.66732361, 376.82781758, - ], - "Cost ($/MW)": 50.0, - }, - pu2: { - Bus: "b1", - "Minimum power (MW)": [0, 0, 0, 0, 0], - "Maximum power (MW)": [0, 0, 0, 0, 0], - "Cost ($/MW)": 0.0, - }, - }); -}); - -test("parse CSV with invalid bus", () => { - const csvContents = - "Name,Bus,Cost ($/MW),Maximum power (MW) 00:00,Maximum power (MW) 01:00," + - "Maximum power (MW) 02:00,Maximum power (MW) 03:00," + - "Maximum power (MW) 04:00,Minimum power (MW) 00:00," + - "Minimum power (MW) 01:00,Minimum power (MW) 02:00," + - "Minimum power (MW) 03:00,Minimum power (MW) 04:00\n" + - "pu1,b99,50,260.25384545,72.89148068,377.17886108,336.66732361," + - "376.82781758,52.05076909,14.57829614,75.43577222,67.33346472,75.36556352\n" + - "pu2,b1,0,0,0,0,0,0,0,0,0,0,0"; - const [, err] = parseCsv(csvContents, ProfiledUnitsColumnSpec, TEST_DATA_1); - assert(err !== null); - assert.equal(err.message, 'Bus "b99" does not exist (row 1)'); -});