diff --git a/web/src/components/CaseBuilder/Parameters/Parameters.tsx b/web/src/components/CaseBuilder/Parameters/Parameters.tsx
index 16b19ee..d4331cc 100644
--- a/web/src/components/CaseBuilder/Parameters/Parameters.tsx
+++ b/web/src/components/CaseBuilder/Parameters/Parameters.tsx
@@ -8,14 +8,36 @@ import SectionHeader from "../../Common/SectionHeader/SectionHeader";
import Form from "../../Common/Forms/Form";
import TextInputRow from "../../Common/Forms/TextInputRow";
import { UnitCommitmentScenario } from "../../../core/fixtures";
-import { ValidationError } from "../../../core/Validation/validate";
+import {
+ changeParameter,
+ changeTimeHorizon,
+ changeTimeStep,
+} from "../../../core/Operations/parameterOperations";
interface ParametersProps {
scenario: UnitCommitmentScenario;
- onParameterChanged: (key: string, value: string) => ValidationError | null;
+ onError: (msg: string) => void;
+ onDataChanged: (scenario: UnitCommitmentScenario) => void;
}
function Parameters(props: ParametersProps) {
+ const onDataChanged = (key: string, value: string) => {
+ let newScenario, err;
+ if (key === "Time horizon (h)") {
+ [newScenario, err] = changeTimeHorizon(props.scenario, value);
+ } else if (key === "Time step (min)") {
+ [newScenario, err] = changeTimeStep(props.scenario, value);
+ } else {
+ [newScenario, err] = changeParameter(props.scenario, key, value);
+ }
+ if (err) {
+ props.onError(err.message);
+ return err;
+ }
+ props.onDataChanged(newScenario);
+ return null;
+ };
+
return (
@@ -25,23 +47,21 @@ function Parameters(props: ParametersProps) {
unit="h"
tooltip="Length of the planning horizon (in hours)."
initialValue={`${props.scenario.Parameters["Time horizon (h)"]}`}
- onChange={(v) => props.onParameterChanged("Time horizon (h)", v)}
+ onChange={(v) => onDataChanged("Time horizon (h)", v)}
/>
props.onParameterChanged("Time step (min)", v)}
+ onChange={(v) => onDataChanged("Time step (min)", v)}
/>
- props.onParameterChanged("Power balance penalty ($/MW)", v)
- }
+ onChange={(v) => onDataChanged("Power balance penalty ($/MW)", v)}
/>
diff --git a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
index 3dd3df3..a016f85 100644
--- a/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
+++ b/web/src/components/CaseBuilder/ProfiledUnits/ProfiledUnits.tsx
@@ -16,66 +16,94 @@ import DataTable, {
generateCsv,
generateTableColumns,
generateTableData,
+ parseCsv,
} from "../../Common/Forms/DataTable";
import { UnitCommitmentScenario } from "../../../core/fixtures";
import { ColumnDefinition } from "tabulator-tables";
import { offerDownload } from "../../Common/io";
+import FileUploadElement from "../../Common/Buttons/FileUploadElement";
+import { useRef } from "react";
interface ProfiledUnitsProps {
scenario: UnitCommitmentScenario;
- onProfiledUnitCreated: () => void;
+ onDataChanged: (scenario: UnitCommitmentScenario) => void;
+ onError: (msg: string) => void;
}
+const ProfiledUnitsColumnSpec: ColumnSpec[] = [
+ {
+ title: "Name",
+ type: "string",
+ width: 150,
+ },
+ {
+ title: "Bus",
+ type: "string",
+ width: 150,
+ },
+ {
+ title: "Cost ($/MW)",
+ type: "number",
+ width: 100,
+ },
+ {
+ title: "Maximum power (MW)",
+ type: "number[]",
+ width: 60,
+ },
+ {
+ title: "Minimum power (MW)",
+ type: "number[]",
+ width: 60,
+ },
+];
+
const generateProfiledUnitsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
- const colSpecs: ColumnSpec[] = [
- {
- title: "Name",
- type: "string",
- width: 150,
- },
- {
- title: "Bus",
- type: "string",
- width: 150,
- },
- {
- title: "Cost ($/MW)",
- type: "number",
- width: 100,
- },
- {
- title: "Maximum power (MW)",
- type: "number[]",
- width: 60,
- },
- {
- title: "Minimum power (MW)",
- type: "number[]",
- width: 60,
- },
- ];
- const columns = generateTableColumns(scenario, colSpecs);
- const data = generateTableData(scenario.Generators, colSpecs, scenario);
+ const columns = generateTableColumns(scenario, ProfiledUnitsColumnSpec);
+ const data = generateTableData(
+ scenario.Generators,
+ ProfiledUnitsColumnSpec,
+ scenario,
+ );
return [data, columns];
};
const ProfiledUnitsComponent = (props: ProfiledUnitsProps) => {
+ const fileUploadElem = useRef
(null);
+
const onDownload = () => {
const [data, columns] = generateProfiledUnitsData(props.scenario);
const csvContents = generateCsv(data, columns);
offerDownload(csvContents, "text/csv", "profiled_units.csv");
};
- const onUpload = () => {};
+
+ const onUpload = () => {
+ fileUploadElem.current!.showFilePicker((csvContents: any) => {
+ const [newGenerators, err] = parseCsv(
+ csvContents,
+ ProfiledUnitsColumnSpec,
+ props.scenario,
+ );
+ if (err) {
+ props.onError(err.message);
+ return;
+ }
+ const newScenario = {
+ ...props.scenario,
+ Generators: newGenerators,
+ };
+ props.onDataChanged(newScenario);
+ });
+ };
+
+ const onAdd = () => {};
+
return (
-
+
{
}}
generateData={() => generateProfiledUnitsData(props.scenario)}
/>
+
);
};
diff --git a/web/src/components/Common/Forms/DataTable.test.ts b/web/src/components/Common/Forms/DataTable.test.ts
index e4c2e1d..08f1618 100644
--- a/web/src/components/Common/Forms/DataTable.test.ts
+++ b/web/src/components/Common/Forms/DataTable.test.ts
@@ -5,9 +5,11 @@
*/
import assert from "node:assert";
-import { parseBusesCsv } from "../../CaseBuilder/Buses/BusesCsv";
-import { generateBusesData } from "../../CaseBuilder/Buses/Buses";
-import { generateCsv } from "./DataTable";
+import {
+ BusesColumnSpec,
+ generateBusesData,
+} from "../../CaseBuilder/Buses/Buses";
+import { generateCsv, parseCsv } from "./DataTable";
import { TEST_DATA_1 } from "../../../core/fixtures.test";
test("generate CSV", () => {
@@ -21,38 +23,19 @@ test("generate CSV", () => {
assert.strictEqual(actualCsv, expectedCsv);
});
-test("parse valid CSV", () => {
- // const csvContents =
- // "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(FixturesTest, csvContents);
- // assert.deepEqual(newScenario.Buses, {
- // b1: {
- // "Load (MW)": [0, 1, 2, 3, 4],
- // },
- // b3: {
- // "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268],
- // },
- // });
-});
-
-test("parse invalid CSV (wrong headers)", () => {
+test("parse CSV", () => {
const csvContents =
- "Name,Load 5,Load 7,Load 23,Load 3,Load 4\n" +
+ "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" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
- expect(() => {
- parseBusesCsv(TEST_DATA_1, csvContents);
- }).toThrow(Error);
-});
-
-test("parse invalid CSV (wrong data length)", () => {
- const csvContents =
- "Name,Load 0,Load 1,Load 2,Load 3,Load 4\n" +
- "b1,0,1,2,3\n" +
- "b3,27.3729,26.29698,25.58005,25.15675,25.4268";
- expect(() => {
- parseBusesCsv(TEST_DATA_1, csvContents);
- }).toThrow(Error);
+ const [newBuses, err] = parseCsv(csvContents, BusesColumnSpec, TEST_DATA_1);
+ assert(err === null);
+ assert.deepEqual(newBuses, {
+ b1: {
+ "Load (MW)": [0, 1, 2, 3, 4],
+ },
+ b3: {
+ "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268],
+ },
+ });
});
diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx
index 56eb92f..5134aaf 100644
--- a/web/src/components/Common/Forms/DataTable.tsx
+++ b/web/src/components/Common/Forms/DataTable.tsx
@@ -12,6 +12,7 @@ import {
} from "tabulator-tables";
import { ValidationError } from "../../../core/Validation/validate";
import { UnitCommitmentScenario } from "../../../core/fixtures";
+import Papa from "papaparse";
export interface ColumnSpec {
title: string;
@@ -126,17 +127,85 @@ export const generateCsv = (data: any[], columns: ColumnDefinition[]) => {
return `${csvHeader}\n${csvBody}`;
};
-export const floatFormatter = (cell: CellComponent) => {
- return parseFloat(cell.getValue()).toFixed(1);
-};
+export const parseCsv = (
+ csvContents: string,
+ colSpecs: ColumnSpec[],
+ scenario: UnitCommitmentScenario,
+): [any, ValidationError | null] => {
+ // Parse contents
+ const csv = Papa.parse(csvContents, {
+ header: true,
+ skipEmptyLines: true,
+ transformHeader: (header) => header.trim(),
+ transform: (value) => value.trim(),
+ });
-export const addNameColumn = (columns: ColumnDefinition[]) => {
- columns.push({
- ...columnsCommonAttrs,
- title: "Name",
- field: "Name",
- minWidth: 150,
+ // Check for parsing errors
+ if (csv.errors.length > 0) {
+ console.error(csv.errors);
+ return [null, { message: "Could not parse CSV file" }];
+ }
+
+ // Check CSV headers
+ const columns = generateTableColumns(scenario, colSpecs);
+ const expectedHeader: string[] = [];
+ columns.forEach((column) => {
+ if (column.columns) {
+ column.columns.forEach((subcolumn) => {
+ expectedHeader.push(subcolumn.field!);
+ });
+ } else {
+ expectedHeader.push(column.field!);
+ }
});
+ const actualHeader = csv.meta.fields!;
+ for (let i = 0; i < expectedHeader.length; i++) {
+ if (expectedHeader[i] !== actualHeader[i]) {
+ return [
+ null,
+ {
+ message: `Invalid CSV: Header mismatch at column ${i + 1}.
+ Expected "${expectedHeader[i]}", found "${actualHeader[i]}"`,
+ },
+ ];
+ }
+ }
+
+ // Parse each row
+ const timeslots = generateTimeslots(scenario);
+ const data: { [key: string]: any } = {};
+ for (let i = 0; i < csv.data.length; i++) {
+ const row = csv.data[i] as { [key: string]: any };
+ const name = row["Name"] as string;
+ data[name] = {};
+
+ for (const spec of colSpecs) {
+ if (spec.title === "Name") continue;
+ switch (spec.type) {
+ case "string":
+ case "number":
+ data[name][spec.title] = row[spec.title];
+ break;
+ case "number[]":
+ data[name][spec.title] = Array(timeslots.length);
+
+ for (let i = 0; i < timeslots.length; i++) {
+ data[name][spec.title][i] = parseFloat(
+ row[`${spec.title} ${timeslots[i]}`],
+ );
+ }
+ break;
+ default:
+ console.error(`Unknown type: ${spec.type}`);
+ }
+ }
+ }
+
+ return [data, null];
+};
+
+export const floatFormatter = (cell: CellComponent) => {
+ return parseFloat(cell.getValue()).toFixed(1);
};
export const generateTimeslots = (scenario: UnitCommitmentScenario) => {
@@ -156,29 +225,6 @@ export const generateTimeslots = (scenario: UnitCommitmentScenario) => {
return timeslots;
};
-export const addTimeseriesColumn = (
- scenario: UnitCommitmentScenario,
- title: string,
- columns: ColumnDefinition[],
- minWidth: number = 65,
-) => {
- const timeSlots = generateTimeslots(scenario);
- const subColumns: ColumnDefinition[] = [];
- timeSlots.forEach((t) => {
- subColumns.push({
- ...columnsCommonAttrs,
- title: `${t}`,
- field: `${title} ${t}`,
- minWidth: minWidth,
- formatter: floatFormatter,
- });
- });
- columns.push({
- title: title,
- columns: subColumns,
- });
-};
-
export const columnsCommonAttrs: ColumnDefinition = {
headerHozAlign: "left",
hozAlign: "left",