web: ThermalUnits: CSV upload

web
Alinson S. Xavier 3 months ago
parent 3f10ad23ca
commit 3bf028577e

@ -0,0 +1,48 @@
/*
* 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 assert from "node:assert";
import { BusesColumnSpec, generateBusesData } from "./Buses";
import { generateCsv, parseCsv } from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/fixtures.test";
test("generate CSV", () => {
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" +
"b1,35.79534,34.38835,33.45083,32.89729,33.25044\n" +
"b2,14.03739,13.48563,13.11797,12.9009,13.03939\n" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
assert.strictEqual(actualCsv, expectedCsv);
});
test("parse CSV", () => {
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" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
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],
},
});
});
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)`);
});

@ -0,0 +1,209 @@
/*
* 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,
generateCsv,
generateTableColumns,
generateTableData,
} from "../Common/Forms/DataTable";
import { TEST_DATA_1 } from "../../core/fixtures.test";
import {
generateThermalUnitsData,
parseThermalUnitsCsv,
ThermalUnitsColumnSpec,
} from "./ThermalUnits";
import assert from "node:assert";
import {
getProfiledGenerators,
getThermalGenerators,
} from "../../core/fixtures";
test("generateTableColumns", () => {
const columns = generateTableColumns(TEST_DATA_1, ThermalUnitsColumnSpec);
assert.equal(columns[2]!["columns"]!.length, 10);
assert.deepEqual(columns[2]!["columns"]![0], {
editor: "input",
editorParams: {
selectContents: true,
},
field: "Production cost curve (MW) 1",
formatter: floatFormatter,
headerHozAlign: "left",
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 60,
resizable: false,
title: "1",
});
});
test("generateTableData", () => {
const data = generateTableData(
getThermalGenerators(TEST_DATA_1),
ThermalUnitsColumnSpec,
TEST_DATA_1,
);
assert.deepEqual(data[0], {
Name: "g1",
Bus: "b1",
"Initial power (MW)": 115,
"Initial status (h)": 12,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Ramp down limit (MW)": 232.68,
"Ramp up limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Production cost curve ($) 1": 1400,
"Production cost curve ($) 2": 1600,
"Production cost curve ($) 3": 2200,
"Production cost curve ($) 4": 2400,
"Production cost curve ($) 5": "",
"Production cost curve ($) 6": "",
"Production cost curve ($) 7": "",
"Production cost curve ($) 8": "",
"Production cost curve ($) 9": "",
"Production cost curve ($) 10": "",
"Production cost curve (MW) 1": 100,
"Production cost curve (MW) 2": 110,
"Production cost curve (MW) 3": 130,
"Production cost curve (MW) 4": 135,
"Production cost curve (MW) 5": "",
"Production cost curve (MW) 6": "",
"Production cost curve (MW) 7": "",
"Production cost curve (MW) 8": "",
"Production cost curve (MW) 9": "",
"Production cost curve (MW) 10": "",
"Startup costs ($) 1": 300,
"Startup costs ($) 2": 400,
"Startup costs ($) 3": "",
"Startup costs ($) 4": "",
"Startup costs ($) 5": "",
"Startup delays (h) 1": 1,
"Startup delays (h) 2": 4,
"Startup delays (h) 3": "",
"Startup delays (h) 4": "",
"Startup delays (h) 5": "",
"Must run?": false,
});
});
const expectedCsvContents =
"Name,Bus," +
"Production cost curve (MW) 1," +
"Production cost curve (MW) 2," +
"Production cost curve (MW) 3," +
"Production cost curve (MW) 4," +
"Production cost curve (MW) 5," +
"Production cost curve (MW) 6," +
"Production cost curve (MW) 7," +
"Production cost curve (MW) 8," +
"Production cost curve (MW) 9," +
"Production cost curve (MW) 10," +
"Production cost curve ($) 1," +
"Production cost curve ($) 2," +
"Production cost curve ($) 3," +
"Production cost curve ($) 4," +
"Production cost curve ($) 5," +
"Production cost curve ($) 6," +
"Production cost curve ($) 7," +
"Production cost curve ($) 8," +
"Production cost curve ($) 9," +
"Production cost curve ($) 10," +
"Startup costs ($) 1," +
"Startup costs ($) 2," +
"Startup costs ($) 3," +
"Startup costs ($) 4," +
"Startup costs ($) 5," +
"Startup delays (h) 1," +
"Startup delays (h) 2," +
"Startup delays (h) 3," +
"Startup delays (h) 4," +
"Startup delays (h) 5," +
"Minimum uptime (h),Minimum downtime (h),Ramp up limit (MW)," +
"Ramp down limit (MW),Startup limit (MW),Shutdown limit (MW)," +
"Initial status (h),Initial power (MW),Must run?\n" +
"g1,b1,100,110,130,135,,,,,,,1400,1600,2200,2400,,,,,,,300,400,,,,1,4,,,,4,4,232.68,232.68,232.68,232.68,12,115,false";
const invalidCsv =
"Name,Bus," +
"Production cost curve (MW) 1," +
"Production cost curve (MW) 2," +
"Production cost curve (MW) 3," +
"Production cost curve (MW) 4," +
"Production cost curve (MW) 5," +
"Production cost curve (MW) 6," +
"Production cost curve (MW) 7," +
"Production cost curve (MW) 8," +
"Production cost curve (MW) 9," +
"Production cost curve (MW) 10," +
"Production cost curve ($) 1," +
"Production cost curve ($) 2," +
"Production cost curve ($) 3," +
"Production cost curve ($) 4," +
"Production cost curve ($) 5," +
"Production cost curve ($) 6," +
"Production cost curve ($) 7," +
"Production cost curve ($) 8," +
"Production cost curve ($) 9," +
"Production cost curve ($) 10," +
"Startup costs ($) 1," +
"Startup costs ($) 2," +
"Startup costs ($) 3," +
"Startup costs ($) 4," +
"Startup costs ($) 5," +
"Startup delays (h) 1," +
"Startup delays (h) 2," +
"Startup delays (h) 3," +
"Startup delays (h) 4," +
"Startup delays (h) 5," +
"Minimum uptime (h),Minimum downtime (h),Ramp up limit (MW)," +
"Ramp down limit (MW),Startup limit (MW),Shutdown limit (MW)," +
"Initial status (h),Initial power (MW),Must run?\n" +
"g1,b1,100,110,130,x,,,,,,,1400,1600,2200,2400,,,,,,,300,400,,,,1,4,,,,4,4,232.68,232.68,232.68,232.68,12,115,false";
test("generateCSV", () => {
const [data, columns] = generateThermalUnitsData(TEST_DATA_1);
const actualCsvContents = generateCsv(data, columns);
assert.equal(actualCsvContents, expectedCsvContents);
});
test("parseCSV", () => {
const [scenario, err] = parseThermalUnitsCsv(
expectedCsvContents,
TEST_DATA_1,
);
assert(!err);
const thermalGens = getThermalGenerators(scenario);
const profGens = getProfiledGenerators(scenario);
assert.equal(Object.keys(thermalGens).length, 1);
assert.equal(Object.keys(profGens).length, 2);
assert.deepEqual(thermalGens["g1"], {
Bus: "b1",
Type: "Thermal",
"Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0],
"Startup costs ($)": [300.0, 400.0],
"Startup delays (h)": [1, 4],
"Ramp up limit (MW)": 232.68,
"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?": false,
});
});
test("parseCSV with invalid number[T]", () => {
const [, err] = parseThermalUnitsCsv(invalidCsv, TEST_DATA_1);
assert(err);
assert.equal(err.message, '"x" is not a valid number (row 1)');
});

@ -9,6 +9,7 @@ import DataTable, {
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "../Common/Forms/DataTable";
import { CaseBuilderSectionProps } from "./CaseBuilder";
import { useRef } from "react";
@ -22,6 +23,7 @@ import {
faUpload,
} from "@fortawesome/free-solid-svg-icons";
import {
getProfiledGenerators,
getThermalGenerators,
UnitCommitmentScenario,
} from "../../core/fixtures";
@ -115,7 +117,7 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
},
];
const generateThermalUnitsData = (
export const generateThermalUnitsData = (
scenario: UnitCommitmentScenario,
): [any[], ColumnDefinition[]] => {
const columns = generateTableColumns(scenario, ThermalUnitsColumnSpec);
@ -127,6 +129,31 @@ const generateThermalUnitsData = (
return [data, columns];
};
export const parseThermalUnitsCsv = (
csvContents: string,
scenario: UnitCommitmentScenario,
): [UnitCommitmentScenario, ValidationError | null] => {
const [thermalGens, err] = parseCsv(
csvContents,
ThermalUnitsColumnSpec,
scenario,
);
if (err) return [scenario, err];
// Process imported generators
for (const gen in thermalGens) {
thermalGens[gen]["Type"] = "Thermal";
}
// Merge with existing data
const profGens = getProfiledGenerators(scenario);
const newScenario = {
...scenario,
Generators: { ...thermalGens, ...profGens },
};
return [newScenario, null];
};
const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => {
const fileUploadElem = useRef<FileUploadElement>(null);
@ -137,26 +164,14 @@ const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => {
};
const onUpload = () => {
// fileUploadElem.current!.showFilePicker((csvContents: any) => {
// const [newGenerators, err] = parseCsv(
// csvContents,
// ThermalUnitsColumnSpec,
// props.scenario,
// );
// if (err) {
// props.onError(err.message);
// return;
// }
// for (const gen in newGenerators) {
// newGenerators[gen]["Type"] = "Thermal";
// }
//
// const newScenario = {
// ...props.scenario,
// Generators: newGenerators,
// };
// props.onDataChanged(newScenario);
// });
fileUploadElem.current!.showFilePicker((csv: any) => {
const [newScenario, err] = parseThermalUnitsCsv(csv, props.scenario);
if (err) {
props.onError(err.message);
return;
}
props.onDataChanged(newScenario);
});
};
const onAdd = () => {

@ -1,127 +0,0 @@
/*
* 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 assert from "node:assert";
import { BusesColumnSpec, generateBusesData } from "../../CaseBuilder/Buses";
import {
floatFormatter,
generateCsv,
generateTableColumns,
generateTableData,
parseCsv,
} from "./DataTable";
import { TEST_DATA_1 } from "../../../core/fixtures.test";
import { ThermalUnitsColumnSpec } from "../../CaseBuilder/ThermalUnits";
import { getThermalGenerators } from "../../../core/fixtures";
test("generateTableColumns (ThermalUnits)", () => {
const columns = generateTableColumns(TEST_DATA_1, ThermalUnitsColumnSpec);
assert.equal(columns[2]!["columns"]!.length, 10);
assert.deepEqual(columns[2]!["columns"]![0], {
editor: "input",
editorParams: {
selectContents: true,
},
field: "Production cost curve (MW) 1",
formatter: floatFormatter,
headerHozAlign: "left",
headerSort: false,
headerWordWrap: true,
hozAlign: "left",
minWidth: 60,
resizable: false,
title: "1",
});
});
test("generateTableData (ThermalUnits)", () => {
const data = generateTableData(
getThermalGenerators(TEST_DATA_1),
ThermalUnitsColumnSpec,
TEST_DATA_1,
);
assert.deepEqual(data[0], {
Name: "g1",
Bus: "b1",
"Initial power (MW)": 115,
"Initial status (h)": 12,
"Minimum downtime (h)": 4,
"Minimum uptime (h)": 4,
"Ramp down limit (MW)": 232.68,
"Ramp up limit (MW)": 232.68,
"Shutdown limit (MW)": 232.68,
"Startup limit (MW)": 232.68,
"Production cost curve ($) 1": 1400,
"Production cost curve ($) 2": 1600,
"Production cost curve ($) 3": 2200,
"Production cost curve ($) 4": 2400,
"Production cost curve ($) 5": "",
"Production cost curve ($) 6": "",
"Production cost curve ($) 7": "",
"Production cost curve ($) 8": "",
"Production cost curve ($) 9": "",
"Production cost curve ($) 10": "",
"Production cost curve (MW) 1": 100,
"Production cost curve (MW) 2": 110,
"Production cost curve (MW) 3": 130,
"Production cost curve (MW) 4": 135,
"Production cost curve (MW) 5": "",
"Production cost curve (MW) 6": "",
"Production cost curve (MW) 7": "",
"Production cost curve (MW) 8": "",
"Production cost curve (MW) 9": "",
"Production cost curve (MW) 10": "",
"Startup costs ($) 1": 300,
"Startup costs ($) 2": 400,
"Startup costs ($) 3": "",
"Startup costs ($) 4": "",
"Startup costs ($) 5": "",
"Startup delays (h) 1": 1,
"Startup delays (h) 2": 4,
"Startup delays (h) 3": "",
"Startup delays (h) 4": "",
"Startup delays (h) 5": "",
"Must run?": false,
});
});
test("generate CSV", () => {
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" +
"b1,35.79534,34.38835,33.45083,32.89729,33.25044\n" +
"b2,14.03739,13.48563,13.11797,12.9009,13.03939\n" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
assert.strictEqual(actualCsv, expectedCsv);
});
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" +
"b3,27.3729,26.29698,25.58005,25.15675,25.4268";
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],
},
});
});
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)`);
});

@ -13,7 +13,7 @@ import {
import { ValidationError } from "../../../core/Validation/validate";
import { UnitCommitmentScenario } from "../../../core/fixtures";
import Papa from "papaparse";
import { parseNumber } from "../../../core/Operations/commonOps";
import { parseBool, parseNumber } from "../../../core/Operations/commonOps";
export interface ColumnSpec {
title: string;
@ -228,11 +228,12 @@ export const parseCsv = (
case "string":
data[name][spec.title] = row[spec.title];
break;
case "number":
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)) {
@ -243,15 +244,36 @@ export const parseCsv = (
}
data[name][spec.title] = row[spec.title];
break;
case "number[T]":
case "number[T]": {
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]}`],
);
const [vf, err] = parseNumber(row[`${spec.title} ${timeslots[i]}`]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title][i] = vf;
}
break;
}
case "number[N]": {
data[name][spec.title] = Array(spec.length).fill(0);
for (let i = 0; i < spec.length!; i++) {
let v = row[`${spec.title} ${i + 1}`];
if (v.trim() === "") {
data[name][spec.title].splice(i, spec.length! - i);
break;
} else {
const [vf, err] = parseNumber(row[`${spec.title} ${i + 1}`]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title][i] = vf;
}
}
break;
}
case "boolean": {
const [val, err] = parseBool(row[spec.title]);
if (err) return [data, { message: err.message + rowRef }];
data[name][spec.title] = val;
break;
}
default:
throw Error(`Unknown type: ${spec.type}`);
}

@ -0,0 +1,30 @@
/*
* 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 { parseBool } from "./commonOps";
import assert from "node:assert";
test("parseBool", () => {
// True values
for (const str of ["true", "TRUE", "1"]) {
let [v, err] = parseBool(str);
assert(!err);
assert.equal(v, true);
}
// False values
for (const str of ["false", "FALSE", "0"]) {
let [v, err] = parseBool(str);
assert(!err);
assert.equal(v, false);
}
// Invalid values
for (const str of ["qwe", ""]) {
let [, err] = parseBool(str);
assert(err);
}
});

@ -51,6 +51,18 @@ export const parseNumber = (
}
};
export const parseBool = (
valueStr: string,
): [boolean, ValidationError | null] => {
if (["true", "1"].includes(valueStr.toLowerCase())) {
return [true, null];
}
if (["false", "0"].includes(valueStr.toLowerCase())) {
return [false, null];
}
return [true, { message: `"${valueStr}" is not a valid boolean value` }];
};
export const changeStringData = (
field: string,
newValue: string,

@ -63,8 +63,8 @@ export const createThermalUnit = (
[name]: {
Bus: busName,
Type: "Thermal",
"Production cost curve (MW)": [0],
"Production cost curve ($)": [0],
"Production cost curve (MW)": [0, 100],
"Production cost curve ($)": [0, 10],
"Startup costs ($)": [0],
"Startup delays (h)": [1],
"Ramp up limit (MW)": "",

Loading…
Cancel
Save