From 53489c1638fa6e795715a203aebe99f8c5932efb Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 27 Jun 2025 11:37:50 -0500 Subject: [PATCH] web: Update nullable number handling --- .../components/CaseBuilder/CaseBuilder.tsx | 18 +++++--------- web/src/components/CaseBuilder/Header.tsx | 3 +-- .../components/CaseBuilder/ThermalUnits.tsx | 12 +++++----- .../CaseBuilder/TransmissionLines.tsx | 4 ++-- web/src/components/Common/Forms/DataTable.tsx | 13 ++++++++-- web/src/core/Data/fixtures.tsx | 4 ++-- web/src/core/Data/schema.ts | 13 +++++++++- web/src/core/Data/types.tsx | 12 +++++----- web/src/core/Operations/commonOps.ts | 17 ++++++++++++- web/src/core/Operations/generatorOps.ts | 8 +++---- web/src/core/Operations/preprocessing.ts | 24 ++++++++++++++----- 11 files changed, 84 insertions(+), 44 deletions(-) diff --git a/web/src/components/CaseBuilder/CaseBuilder.tsx b/web/src/components/CaseBuilder/CaseBuilder.tsx index 188982a..0c5a01c 100644 --- a/web/src/components/CaseBuilder/CaseBuilder.tsx +++ b/web/src/components/CaseBuilder/CaseBuilder.tsx @@ -13,7 +13,6 @@ import "tabulator-tables/dist/css/tabulator.min.css"; import "../Common/Forms/Tables.css"; import { useState } from "react"; import Footer from "./Footer"; -import { validate } from "../../core/Data/validate"; import { offerDownload } from "../Common/io"; import { preprocess } from "../../core/Operations/preprocessing"; import Toast from "../Common/Forms/Toast"; @@ -68,19 +67,14 @@ const CaseBuilder = () => { setAndSaveScenario(newScenario); }; - const onLoad = (scenario: UnitCommitmentScenario) => { - const preprocessed = preprocess( - scenario, - ) as unknown as UnitCommitmentScenario; - - // Validate and assign default values - if (!validate(preprocessed)) { - setToastMessage("Error loading JSON file"); - console.error(validate.errors); + const onLoad = (data: any) => { + const json = JSON.parse(data); + const [scenario, err] = preprocess(json); + if (err) { + setToastMessage(err.message); return; } - - setAndSaveScenario(preprocessed); + setAndSaveScenario(scenario!); setToastMessage("Data loaded successfully"); }; diff --git a/web/src/components/CaseBuilder/Header.tsx b/web/src/components/CaseBuilder/Header.tsx index 9573edf..1c151cd 100644 --- a/web/src/components/CaseBuilder/Header.tsx +++ b/web/src/components/CaseBuilder/Header.tsx @@ -22,8 +22,7 @@ function Header(props: HeaderProps) { function onLoad() { fileElem.current!.showFilePicker((data: any) => { - const scenario = JSON.parse(data) as UnitCommitmentScenario; - props.onLoad(scenario); + props.onLoad(data); }); } diff --git a/web/src/components/CaseBuilder/ThermalUnits.tsx b/web/src/components/CaseBuilder/ThermalUnits.tsx index a1aff04..9a26cc8 100644 --- a/web/src/components/CaseBuilder/ThermalUnits.tsx +++ b/web/src/components/CaseBuilder/ThermalUnits.tsx @@ -51,13 +51,13 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [ title: "Production cost curve (MW)", type: "number[N]", length: 10, - width: 75, + width: 80, }, { title: "Production cost curve ($)", type: "number[N]", length: 10, - width: 75, + width: 80, }, { title: "Startup costs ($)", @@ -83,22 +83,22 @@ export const ThermalUnitsColumnSpec: ColumnSpec[] = [ }, { title: "Ramp up limit (MW)", - type: "number", + type: "number?", width: 100, }, { title: "Ramp down limit (MW)", - type: "number", + type: "number?", width: 100, }, { title: "Startup limit (MW)", - type: "number", + type: "number?", width: 80, }, { title: "Shutdown limit (MW)", - type: "number", + type: "number?", width: 100, }, { diff --git a/web/src/components/CaseBuilder/TransmissionLines.tsx b/web/src/components/CaseBuilder/TransmissionLines.tsx index 39c1300..aeab1c9 100644 --- a/web/src/components/CaseBuilder/TransmissionLines.tsx +++ b/web/src/components/CaseBuilder/TransmissionLines.tsx @@ -55,12 +55,12 @@ export const TransmissionLinesColumnSpec: ColumnSpec[] = [ }, { title: "Normal flow limit (MW)", - type: "number", + type: "number?", width: 60, }, { title: "Emergency flow limit (MW)", - type: "number", + type: "number?", width: 60, }, { diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx index 962fa36..b9f3306 100644 --- a/web/src/components/Common/Forms/DataTable.tsx +++ b/web/src/components/Common/Forms/DataTable.tsx @@ -17,7 +17,14 @@ import { UnitCommitmentScenario } from "../../../core/Data/types"; export interface ColumnSpec { title: string; - type: "string" | "number" | "number[N]" | "number[T]" | "busRef" | "boolean"; + type: + | "string" + | "number" + | "number?" + | "number[N]" + | "number[T]" + | "busRef" + | "boolean"; length?: number; width: number; } @@ -53,6 +60,7 @@ export const generateTableColumns = ( }); break; case "number": + case "number?": columns.push({ ...columnsCommonAttrs, title: spec.title, @@ -118,6 +126,7 @@ export const generateTableData = ( switch (spec.type) { case "string": case "number": + case "number?": case "boolean": case "busRef": entry[spec.title] = entryData[spec.title]; @@ -285,7 +294,7 @@ export const parseCsv = ( export const floatFormatter = (cell: CellComponent) => { const v = cell.getValue(); - if (v === "") { + if (v === "" || v === null) { return "—"; } else { return parseFloat(cell.getValue()).toLocaleString("en-US", { diff --git a/web/src/core/Data/fixtures.tsx b/web/src/core/Data/fixtures.tsx index ba2e00b..f6c399a 100644 --- a/web/src/core/Data/fixtures.tsx +++ b/web/src/core/Data/fixtures.tsx @@ -120,8 +120,8 @@ export const TEST_SCENARIO: UnitCommitmentScenario = { "Source bus": "b1", "Target bus": "b2", "Susceptance (S)": 29.49686, - "Normal flow limit (MW)": 15000.0, - "Emergency flow limit (MW)": 20000.0, + "Normal flow limit (MW)": null, + "Emergency flow limit (MW)": null, "Flow limit penalty ($/MW)": 5000.0, }, }, diff --git a/web/src/core/Data/schema.ts b/web/src/core/Data/schema.ts index d1d8550..8f9c67e 100644 --- a/web/src/core/Data/schema.ts +++ b/web/src/core/Data/schema.ts @@ -97,15 +97,18 @@ export const schema = { }, "Susceptance (S)": { type: "number", - minimum: 0, }, "Normal flow limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Emergency flow limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Flow limit penalty ($/MW)": { type: "number", @@ -254,18 +257,26 @@ export const schema = { "Ramp up limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Ramp down limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Startup limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Shutdown limit (MW)": { type: "number", minimum: 0, + nullable: true, + default: null, }, "Initial status (h)": { type: "integer", diff --git a/web/src/core/Data/types.tsx b/web/src/core/Data/types.tsx index ad3a237..435b71b 100644 --- a/web/src/core/Data/types.tsx +++ b/web/src/core/Data/types.tsx @@ -25,10 +25,10 @@ export interface ThermalUnit { "Production cost curve ($)": number[]; "Startup costs ($)": number[]; "Startup delays (h)": number[]; - "Ramp up limit (MW)": number | ""; - "Ramp down limit (MW)": number | ""; - "Startup limit (MW)": number | ""; - "Shutdown limit (MW)": number | ""; + "Ramp up limit (MW)": number | null; + "Ramp down limit (MW)": number | null; + "Startup limit (MW)": number | null; + "Shutdown limit (MW)": number | null; "Minimum downtime (h)": number; "Minimum uptime (h)": number; "Initial status (h)": number; @@ -40,8 +40,8 @@ export interface TransmissionLine { "Source bus": string; "Target bus": string; "Susceptance (S)": number; - "Normal flow limit (MW)": number; - "Emergency flow limit (MW)": number; + "Normal flow limit (MW)": number | null; + "Emergency flow limit (MW)": number | null; "Flow limit penalty ($/MW)": number; } diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts index ff0f99e..e69ca72 100644 --- a/web/src/core/Operations/commonOps.ts +++ b/web/src/core/Operations/commonOps.ts @@ -43,6 +43,9 @@ export const generateUniqueName = (container: any, prefix: string): string => { export const parseNumber = ( valueStr: string, ): [number, ValidationError | null] => { + if (valueStr === "") { + return [0, { message: "Field must not be blank" }]; + } const valueFloat = parseFloat(valueStr); if (isNaN(valueFloat)) { 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 = ( valueStr: string, ): [boolean, ValidationError | null] => { @@ -93,9 +103,12 @@ export const changeNumberData = ( field: string, newValueStr: string, container: { [key: string]: any }, + nullable: boolean = false, ): [{ [key: string]: any }, ValidationError | null] => { // Parse value - const [newValueFloat, err] = parseNumber(newValueStr); + const [newValueFloat, err] = nullable + ? parseNullableNumber(newValueStr) + : parseNumber(newValueStr); if (err) return [container, err]; // Build the new object @@ -211,6 +224,8 @@ export const changeData = ( return changeBusRefData(fieldName, newValueStr, container, scenario); case "number": return changeNumberData(fieldName, newValueStr, container); + case "number?": + return changeNumberData(fieldName, newValueStr, container, true); case "number[T]": return changeNumberVecTData( fieldName, diff --git a/web/src/core/Operations/generatorOps.ts b/web/src/core/Operations/generatorOps.ts index 1e828cd..9e017e7 100644 --- a/web/src/core/Operations/generatorOps.ts +++ b/web/src/core/Operations/generatorOps.ts @@ -61,10 +61,10 @@ export const createThermalUnit = ( "Production cost curve ($)": [0, 10], "Startup costs ($)": [0], "Startup delays (h)": [1], - "Ramp up limit (MW)": "", - "Ramp down limit (MW)": "", - "Startup limit (MW)": "", - "Shutdown limit (MW)": "", + "Ramp up limit (MW)": null, + "Ramp down limit (MW)": null, + "Startup limit (MW)": null, + "Shutdown limit (MW)": null, "Minimum downtime (h)": 1, "Minimum uptime (h)": 1, "Initial status (h)": -24, diff --git a/web/src/core/Operations/preprocessing.ts b/web/src/core/Operations/preprocessing.ts index e66d571..78cc4c4 100644 --- a/web/src/core/Operations/preprocessing.ts +++ b/web/src/core/Operations/preprocessing.ts @@ -1,25 +1,35 @@ -// @ts-nocheck - /* * 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 { 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 let result = JSON.parse(JSON.stringify(data)); + // Run migration + migrate(result); + // Run JSON validation and assign default values if (!validate(result)) { console.error(validate.errors); - throw Error("Invalid JSON"); + return [ + null, + { message: "Invalid JSON file. See console for more details." }, + ]; } // Expand scalars into arrays + // @ts-ignore const timeHorizon = result["Parameters"]["Time horizon (h)"]; + // @ts-ignore const timeStep = result["Parameters"]["Time step (min)"]; const T = (timeHorizon * 60) / timeStep; for (const busName in result["Buses"]) { @@ -30,5 +40,7 @@ export const preprocess = (data) => { busData["Load (MW)"] = Array(T).fill(busLoad); } } - return result; + + const scenario = result as unknown as UnitCommitmentScenario; + return [scenario, null]; };