mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
web: ThermalUnits: CSV upload
This commit is contained in:
48
web/src/components/CaseBuilder/Buses.test.ts
Normal file
48
web/src/components/CaseBuilder/Buses.test.ts
Normal file
@@ -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)`);
|
||||||
|
});
|
||||||
209
web/src/components/CaseBuilder/ThermalUnits.test.ts
Normal file
209
web/src/components/CaseBuilder/ThermalUnits.test.ts
Normal file
@@ -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,
|
generateCsv,
|
||||||
generateTableColumns,
|
generateTableColumns,
|
||||||
generateTableData,
|
generateTableData,
|
||||||
|
parseCsv,
|
||||||
} from "../Common/Forms/DataTable";
|
} from "../Common/Forms/DataTable";
|
||||||
import { CaseBuilderSectionProps } from "./CaseBuilder";
|
import { CaseBuilderSectionProps } from "./CaseBuilder";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
faUpload,
|
faUpload,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {
|
import {
|
||||||
|
getProfiledGenerators,
|
||||||
getThermalGenerators,
|
getThermalGenerators,
|
||||||
UnitCommitmentScenario,
|
UnitCommitmentScenario,
|
||||||
} from "../../core/fixtures";
|
} from "../../core/fixtures";
|
||||||
@@ -115,7 +117,7 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateThermalUnitsData = (
|
export const generateThermalUnitsData = (
|
||||||
scenario: UnitCommitmentScenario,
|
scenario: UnitCommitmentScenario,
|
||||||
): [any[], ColumnDefinition[]] => {
|
): [any[], ColumnDefinition[]] => {
|
||||||
const columns = generateTableColumns(scenario, ThermalUnitsColumnSpec);
|
const columns = generateTableColumns(scenario, ThermalUnitsColumnSpec);
|
||||||
@@ -127,6 +129,31 @@ const generateThermalUnitsData = (
|
|||||||
return [data, columns];
|
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 ThermalUnitsComponent = (props: CaseBuilderSectionProps) => {
|
||||||
const fileUploadElem = useRef<FileUploadElement>(null);
|
const fileUploadElem = useRef<FileUploadElement>(null);
|
||||||
|
|
||||||
@@ -137,26 +164,14 @@ const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onUpload = () => {
|
const onUpload = () => {
|
||||||
// fileUploadElem.current!.showFilePicker((csvContents: any) => {
|
fileUploadElem.current!.showFilePicker((csv: any) => {
|
||||||
// const [newGenerators, err] = parseCsv(
|
const [newScenario, err] = parseThermalUnitsCsv(csv, props.scenario);
|
||||||
// csvContents,
|
if (err) {
|
||||||
// ThermalUnitsColumnSpec,
|
props.onError(err.message);
|
||||||
// props.scenario,
|
return;
|
||||||
// );
|
}
|
||||||
// if (err) {
|
props.onDataChanged(newScenario);
|
||||||
// props.onError(err.message);
|
});
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// for (const gen in newGenerators) {
|
|
||||||
// newGenerators[gen]["Type"] = "Thermal";
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const newScenario = {
|
|
||||||
// ...props.scenario,
|
|
||||||
// Generators: newGenerators,
|
|
||||||
// };
|
|
||||||
// props.onDataChanged(newScenario);
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAdd = () => {
|
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 { ValidationError } from "../../../core/Validation/validate";
|
||||||
import { UnitCommitmentScenario } from "../../../core/fixtures";
|
import { UnitCommitmentScenario } from "../../../core/fixtures";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
import { parseNumber } from "../../../core/Operations/commonOps";
|
import { parseBool, parseNumber } from "../../../core/Operations/commonOps";
|
||||||
|
|
||||||
export interface ColumnSpec {
|
export interface ColumnSpec {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -228,11 +228,12 @@ export const parseCsv = (
|
|||||||
case "string":
|
case "string":
|
||||||
data[name][spec.title] = row[spec.title];
|
data[name][spec.title] = row[spec.title];
|
||||||
break;
|
break;
|
||||||
case "number":
|
case "number": {
|
||||||
const [val, err] = parseNumber(row[spec.title]);
|
const [val, err] = parseNumber(row[spec.title]);
|
||||||
if (err) return [null, { message: err.message + rowRef }];
|
if (err) return [null, { message: err.message + rowRef }];
|
||||||
data[name][spec.title] = val;
|
data[name][spec.title] = val;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "busRef":
|
case "busRef":
|
||||||
const busName = row[spec.title];
|
const busName = row[spec.title];
|
||||||
if (!(busName in scenario.Buses)) {
|
if (!(busName in scenario.Buses)) {
|
||||||
@@ -243,15 +244,36 @@ export const parseCsv = (
|
|||||||
}
|
}
|
||||||
data[name][spec.title] = row[spec.title];
|
data[name][spec.title] = row[spec.title];
|
||||||
break;
|
break;
|
||||||
case "number[T]":
|
case "number[T]": {
|
||||||
data[name][spec.title] = Array(timeslots.length);
|
data[name][spec.title] = Array(timeslots.length);
|
||||||
|
|
||||||
for (let i = 0; i < timeslots.length; i++) {
|
for (let i = 0; i < timeslots.length; i++) {
|
||||||
data[name][spec.title][i] = parseFloat(
|
const [vf, err] = parseNumber(row[`${spec.title} ${timeslots[i]}`]);
|
||||||
row[`${spec.title} ${timeslots[i]}`],
|
if (err) return [data, { message: err.message + rowRef }];
|
||||||
);
|
data[name][spec.title][i] = vf;
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
throw Error(`Unknown type: ${spec.type}`);
|
throw Error(`Unknown type: ${spec.type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
30
web/src/core/Operations/commonOps.test.ts
Normal file
30
web/src/core/Operations/commonOps.test.ts
Normal file
@@ -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 = (
|
export const changeStringData = (
|
||||||
field: string,
|
field: string,
|
||||||
newValue: string,
|
newValue: string,
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ export const createThermalUnit = (
|
|||||||
[name]: {
|
[name]: {
|
||||||
Bus: busName,
|
Bus: busName,
|
||||||
Type: "Thermal",
|
Type: "Thermal",
|
||||||
"Production cost curve (MW)": [0],
|
"Production cost curve (MW)": [0, 100],
|
||||||
"Production cost curve ($)": [0],
|
"Production cost curve ($)": [0, 10],
|
||||||
"Startup costs ($)": [0],
|
"Startup costs ($)": [0],
|
||||||
"Startup delays (h)": [1],
|
"Startup delays (h)": [1],
|
||||||
"Ramp up limit (MW)": "",
|
"Ramp up limit (MW)": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user