mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
web: Update nullable number handling
This commit is contained in:
@@ -13,7 +13,6 @@ import "tabulator-tables/dist/css/tabulator.min.css";
|
|||||||
import "../Common/Forms/Tables.css";
|
import "../Common/Forms/Tables.css";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { validate } from "../../core/Data/validate";
|
|
||||||
import { offerDownload } from "../Common/io";
|
import { offerDownload } from "../Common/io";
|
||||||
import { preprocess } from "../../core/Operations/preprocessing";
|
import { preprocess } from "../../core/Operations/preprocessing";
|
||||||
import Toast from "../Common/Forms/Toast";
|
import Toast from "../Common/Forms/Toast";
|
||||||
@@ -68,19 +67,14 @@ const CaseBuilder = () => {
|
|||||||
setAndSaveScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoad = (scenario: UnitCommitmentScenario) => {
|
const onLoad = (data: any) => {
|
||||||
const preprocessed = preprocess(
|
const json = JSON.parse(data);
|
||||||
scenario,
|
const [scenario, err] = preprocess(json);
|
||||||
) as unknown as UnitCommitmentScenario;
|
if (err) {
|
||||||
|
setToastMessage(err.message);
|
||||||
// Validate and assign default values
|
|
||||||
if (!validate(preprocessed)) {
|
|
||||||
setToastMessage("Error loading JSON file");
|
|
||||||
console.error(validate.errors);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setAndSaveScenario(scenario!);
|
||||||
setAndSaveScenario(preprocessed);
|
|
||||||
setToastMessage("Data loaded successfully");
|
setToastMessage("Data loaded successfully");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ function Header(props: HeaderProps) {
|
|||||||
|
|
||||||
function onLoad() {
|
function onLoad() {
|
||||||
fileElem.current!.showFilePicker((data: any) => {
|
fileElem.current!.showFilePicker((data: any) => {
|
||||||
const scenario = JSON.parse(data) as UnitCommitmentScenario;
|
props.onLoad(data);
|
||||||
props.onLoad(scenario);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
|
|||||||
title: "Production cost curve (MW)",
|
title: "Production cost curve (MW)",
|
||||||
type: "number[N]",
|
type: "number[N]",
|
||||||
length: 10,
|
length: 10,
|
||||||
width: 75,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Production cost curve ($)",
|
title: "Production cost curve ($)",
|
||||||
type: "number[N]",
|
type: "number[N]",
|
||||||
length: 10,
|
length: 10,
|
||||||
width: 75,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Startup costs ($)",
|
title: "Startup costs ($)",
|
||||||
@@ -83,22 +83,22 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Ramp up limit (MW)",
|
title: "Ramp up limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Ramp down limit (MW)",
|
title: "Ramp down limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Startup limit (MW)",
|
title: "Startup limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Shutdown limit (MW)",
|
title: "Shutdown limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export const TransmissionLinesColumnSpec: ColumnSpec[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Normal flow limit (MW)",
|
title: "Normal flow limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Emergency flow limit (MW)",
|
title: "Emergency flow limit (MW)",
|
||||||
type: "number",
|
type: "number?",
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ import { UnitCommitmentScenario } from "../../../core/Data/types";
|
|||||||
|
|
||||||
export interface ColumnSpec {
|
export interface ColumnSpec {
|
||||||
title: string;
|
title: string;
|
||||||
type: "string" | "number" | "number[N]" | "number[T]" | "busRef" | "boolean";
|
type:
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "number?"
|
||||||
|
| "number[N]"
|
||||||
|
| "number[T]"
|
||||||
|
| "busRef"
|
||||||
|
| "boolean";
|
||||||
length?: number;
|
length?: number;
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
@@ -53,6 +60,7 @@ export const generateTableColumns = (
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "number":
|
case "number":
|
||||||
|
case "number?":
|
||||||
columns.push({
|
columns.push({
|
||||||
...columnsCommonAttrs,
|
...columnsCommonAttrs,
|
||||||
title: spec.title,
|
title: spec.title,
|
||||||
@@ -118,6 +126,7 @@ export const generateTableData = (
|
|||||||
switch (spec.type) {
|
switch (spec.type) {
|
||||||
case "string":
|
case "string":
|
||||||
case "number":
|
case "number":
|
||||||
|
case "number?":
|
||||||
case "boolean":
|
case "boolean":
|
||||||
case "busRef":
|
case "busRef":
|
||||||
entry[spec.title] = entryData[spec.title];
|
entry[spec.title] = entryData[spec.title];
|
||||||
@@ -285,7 +294,7 @@ export const parseCsv = (
|
|||||||
|
|
||||||
export const floatFormatter = (cell: CellComponent) => {
|
export const floatFormatter = (cell: CellComponent) => {
|
||||||
const v = cell.getValue();
|
const v = cell.getValue();
|
||||||
if (v === "") {
|
if (v === "" || v === null) {
|
||||||
return "—";
|
return "—";
|
||||||
} else {
|
} else {
|
||||||
return parseFloat(cell.getValue()).toLocaleString("en-US", {
|
return parseFloat(cell.getValue()).toLocaleString("en-US", {
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ export const TEST_SCENARIO: UnitCommitmentScenario = {
|
|||||||
"Source bus": "b1",
|
"Source bus": "b1",
|
||||||
"Target bus": "b2",
|
"Target bus": "b2",
|
||||||
"Susceptance (S)": 29.49686,
|
"Susceptance (S)": 29.49686,
|
||||||
"Normal flow limit (MW)": 15000.0,
|
"Normal flow limit (MW)": null,
|
||||||
"Emergency flow limit (MW)": 20000.0,
|
"Emergency flow limit (MW)": null,
|
||||||
"Flow limit penalty ($/MW)": 5000.0,
|
"Flow limit penalty ($/MW)": 5000.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,15 +97,18 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
"Susceptance (S)": {
|
"Susceptance (S)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
|
||||||
},
|
},
|
||||||
"Normal flow limit (MW)": {
|
"Normal flow limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Emergency flow limit (MW)": {
|
"Emergency flow limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Flow limit penalty ($/MW)": {
|
"Flow limit penalty ($/MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
@@ -254,18 +257,26 @@ export const schema = {
|
|||||||
"Ramp up limit (MW)": {
|
"Ramp up limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Ramp down limit (MW)": {
|
"Ramp down limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Startup limit (MW)": {
|
"Startup limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Shutdown limit (MW)": {
|
"Shutdown limit (MW)": {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
"Initial status (h)": {
|
"Initial status (h)": {
|
||||||
type: "integer",
|
type: "integer",
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export interface ThermalUnit {
|
|||||||
"Production cost curve ($)": number[];
|
"Production cost curve ($)": number[];
|
||||||
"Startup costs ($)": number[];
|
"Startup costs ($)": number[];
|
||||||
"Startup delays (h)": number[];
|
"Startup delays (h)": number[];
|
||||||
"Ramp up limit (MW)": number | "";
|
"Ramp up limit (MW)": number | null;
|
||||||
"Ramp down limit (MW)": number | "";
|
"Ramp down limit (MW)": number | null;
|
||||||
"Startup limit (MW)": number | "";
|
"Startup limit (MW)": number | null;
|
||||||
"Shutdown limit (MW)": number | "";
|
"Shutdown limit (MW)": number | null;
|
||||||
"Minimum downtime (h)": number;
|
"Minimum downtime (h)": number;
|
||||||
"Minimum uptime (h)": number;
|
"Minimum uptime (h)": number;
|
||||||
"Initial status (h)": number;
|
"Initial status (h)": number;
|
||||||
@@ -40,8 +40,8 @@ export interface TransmissionLine {
|
|||||||
"Source bus": string;
|
"Source bus": string;
|
||||||
"Target bus": string;
|
"Target bus": string;
|
||||||
"Susceptance (S)": number;
|
"Susceptance (S)": number;
|
||||||
"Normal flow limit (MW)": number;
|
"Normal flow limit (MW)": number | null;
|
||||||
"Emergency flow limit (MW)": number;
|
"Emergency flow limit (MW)": number | null;
|
||||||
"Flow limit penalty ($/MW)": number;
|
"Flow limit penalty ($/MW)": number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export const generateUniqueName = (container: any, prefix: string): string => {
|
|||||||
export const parseNumber = (
|
export const parseNumber = (
|
||||||
valueStr: string,
|
valueStr: string,
|
||||||
): [number, ValidationError | null] => {
|
): [number, ValidationError | null] => {
|
||||||
|
if (valueStr === "") {
|
||||||
|
return [0, { message: "Field must not be blank" }];
|
||||||
|
}
|
||||||
const valueFloat = parseFloat(valueStr);
|
const valueFloat = parseFloat(valueStr);
|
||||||
if (isNaN(valueFloat)) {
|
if (isNaN(valueFloat)) {
|
||||||
return [0, { message: `"${valueStr}" is not a valid number` }];
|
return [0, { message: `"${valueStr}" is not a valid number` }];
|
||||||
@@ -51,6 +54,13 @@ export const parseNumber = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseNullableNumber = (
|
||||||
|
valueStr: string,
|
||||||
|
): [number | null, ValidationError | null] => {
|
||||||
|
if (valueStr === "") return [null, null];
|
||||||
|
return parseNumber(valueStr);
|
||||||
|
};
|
||||||
|
|
||||||
export const parseBool = (
|
export const parseBool = (
|
||||||
valueStr: string,
|
valueStr: string,
|
||||||
): [boolean, ValidationError | null] => {
|
): [boolean, ValidationError | null] => {
|
||||||
@@ -93,9 +103,12 @@ export const changeNumberData = (
|
|||||||
field: string,
|
field: string,
|
||||||
newValueStr: string,
|
newValueStr: string,
|
||||||
container: { [key: string]: any },
|
container: { [key: string]: any },
|
||||||
|
nullable: boolean = false,
|
||||||
): [{ [key: string]: any }, ValidationError | null] => {
|
): [{ [key: string]: any }, ValidationError | null] => {
|
||||||
// Parse value
|
// Parse value
|
||||||
const [newValueFloat, err] = parseNumber(newValueStr);
|
const [newValueFloat, err] = nullable
|
||||||
|
? parseNullableNumber(newValueStr)
|
||||||
|
: parseNumber(newValueStr);
|
||||||
if (err) return [container, err];
|
if (err) return [container, err];
|
||||||
|
|
||||||
// Build the new object
|
// Build the new object
|
||||||
@@ -211,6 +224,8 @@ export const changeData = (
|
|||||||
return changeBusRefData(fieldName, newValueStr, container, scenario);
|
return changeBusRefData(fieldName, newValueStr, container, scenario);
|
||||||
case "number":
|
case "number":
|
||||||
return changeNumberData(fieldName, newValueStr, container);
|
return changeNumberData(fieldName, newValueStr, container);
|
||||||
|
case "number?":
|
||||||
|
return changeNumberData(fieldName, newValueStr, container, true);
|
||||||
case "number[T]":
|
case "number[T]":
|
||||||
return changeNumberVecTData(
|
return changeNumberVecTData(
|
||||||
fieldName,
|
fieldName,
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ export const createThermalUnit = (
|
|||||||
"Production cost curve ($)": [0, 10],
|
"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)": null,
|
||||||
"Ramp down limit (MW)": "",
|
"Ramp down limit (MW)": null,
|
||||||
"Startup limit (MW)": "",
|
"Startup limit (MW)": null,
|
||||||
"Shutdown limit (MW)": "",
|
"Shutdown limit (MW)": null,
|
||||||
"Minimum downtime (h)": 1,
|
"Minimum downtime (h)": 1,
|
||||||
"Minimum uptime (h)": 1,
|
"Minimum uptime (h)": 1,
|
||||||
"Initial status (h)": -24,
|
"Initial status (h)": -24,
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
* UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
|
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
|
||||||
* Released under the modified BSD license. See COPYING.md for more details.
|
* Released under the modified BSD license. See COPYING.md for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { validate } from "../Data/validate";
|
import { validate, ValidationError } from "../Data/validate";
|
||||||
|
import { UnitCommitmentScenario } from "../Data/types";
|
||||||
|
import { migrate } from "../Data/migrate";
|
||||||
|
|
||||||
export const preprocess = (data) => {
|
export const preprocess = (
|
||||||
|
data: any,
|
||||||
|
): [UnitCommitmentScenario | null, ValidationError | null] => {
|
||||||
// Make a copy of the original data
|
// Make a copy of the original data
|
||||||
let result = JSON.parse(JSON.stringify(data));
|
let result = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
migrate(result);
|
||||||
|
|
||||||
// Run JSON validation and assign default values
|
// Run JSON validation and assign default values
|
||||||
if (!validate(result)) {
|
if (!validate(result)) {
|
||||||
console.error(validate.errors);
|
console.error(validate.errors);
|
||||||
throw Error("Invalid JSON");
|
return [
|
||||||
|
null,
|
||||||
|
{ message: "Invalid JSON file. See console for more details." },
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand scalars into arrays
|
// Expand scalars into arrays
|
||||||
|
// @ts-ignore
|
||||||
const timeHorizon = result["Parameters"]["Time horizon (h)"];
|
const timeHorizon = result["Parameters"]["Time horizon (h)"];
|
||||||
|
// @ts-ignore
|
||||||
const timeStep = result["Parameters"]["Time step (min)"];
|
const timeStep = result["Parameters"]["Time step (min)"];
|
||||||
const T = (timeHorizon * 60) / timeStep;
|
const T = (timeHorizon * 60) / timeStep;
|
||||||
for (const busName in result["Buses"]) {
|
for (const busName in result["Buses"]) {
|
||||||
@@ -30,5 +40,7 @@ export const preprocess = (data) => {
|
|||||||
busData["Load (MW)"] = Array(T).fill(busLoad);
|
busData["Load (MW)"] = Array(T).fill(busLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
const scenario = result as unknown as UnitCommitmentScenario;
|
||||||
|
return [scenario, null];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user