diff --git a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx index fa18bb5..24db9e3 100644 --- a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx +++ b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx @@ -97,6 +97,10 @@ const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => { props.onError(err.message); return; } + for (const gen in newGenerators) { + newGenerators[gen]["Type"] = "Profiled"; + } + const newScenario = { ...props.scenario, Generators: newGenerators, diff --git a/web/src/components/Common/Forms/DataTable.test.ts b/web/src/components/Common/Forms/DataTable.test.ts index 08f1618..4060908 100644 --- a/web/src/components/Common/Forms/DataTable.test.ts +++ b/web/src/components/Common/Forms/DataTable.test.ts @@ -11,6 +11,7 @@ import { } from "../../CaseBuilder/Buses/Buses"; import { generateCsv, parseCsv } from "./DataTable"; import { TEST_DATA_1 } from "../../../core/fixtures.test"; +import { ProfiledUnitsColumnSpec } from "../../CaseBuilder/ProfiledUnits/ProfiledUnits"; test("generate CSV", () => { const [data, columns] = generateBusesData(TEST_DATA_1); @@ -23,7 +24,7 @@ test("generate CSV", () => { assert.strictEqual(actualCsv, expectedCsv); }); -test("parse CSV", () => { +test("parse CSV (Buses)", () => { const csvContents = "Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" + "b1,0,1,2,3,4\n" + @@ -39,3 +40,64 @@ test("parse CSV", () => { }, }); }); + +test("parse CSV with duplicated names", () => { + const csvContents = + "Name,Load (MW) 00:00,Load (MW) 01:00,Load (MW) 02:00,Load (MW) 03:00,Load (MW) 04:00\n" + + "b1,0,0,0,0,0\n" + + "b1,0,0,0,0,0"; + const [, err] = parseCsv(csvContents, BusesColumnSpec, TEST_DATA_1); + 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)'); +}); diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx index 541b8cd..b38b5a3 100644 --- a/web/src/components/Common/Forms/DataTable.tsx +++ b/web/src/components/Common/Forms/DataTable.tsx @@ -13,6 +13,7 @@ import { import { ValidationError } from "../../../core/Validation/validate"; import { UnitCommitmentScenario } from "../../../core/fixtures"; import Papa from "papaparse"; +import { parseNumber } from "../../../core/Operations/commonOps"; export interface ColumnSpec { title: string; @@ -178,14 +179,32 @@ export const parseCsv = ( const data: { [key: string]: any } = {}; for (let i = 0; i < csv.data.length; i++) { const row = csv.data[i] as { [key: string]: any }; + const rowRef = ` (row ${i + 1})`; const name = row["Name"] as string; + if (name in data) { + return [null, { message: `Name "${name}" is duplicated` + rowRef }]; + } data[name] = {}; for (const spec of colSpecs) { if (spec.title === "Name") continue; switch (spec.type) { case "string": + data[name][spec.title] = row[spec.title]; + break; case "number": + const [val, err] = parseNumber(row[spec.title]); + if (err) return [null, { message: err.message + rowRef }]; + data[name][spec.title] = val; + break; + case "busRef": + const busName = row[spec.title]; + if (!(busName in scenario.Buses)) { + return [ + null, + { message: `Bus "${busName}" does not exist` + rowRef }, + ]; + } data[name][spec.title] = row[spec.title]; break; case "number[]": @@ -198,7 +217,7 @@ export const parseCsv = ( } break; default: - console.error(`Unknown type: ${spec.type}`); + throw Error(`Unknown type: ${spec.type}`); } } } diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts index eb580da..da17787 100644 --- a/web/src/core/Operations/commonOps.ts +++ b/web/src/core/Operations/commonOps.ts @@ -40,7 +40,9 @@ export const generateUniqueName = (container: any, prefix: string): string => { return name; }; -const parseNumber = (valueStr: string): [number, ValidationError | null] => { +export const parseNumber = ( + valueStr: string, +): [number, ValidationError | null] => { const valueFloat = parseFloat(valueStr); if (isNaN(valueFloat)) { return [0, { message: `"${valueStr}" is not a valid number` }];