diff --git a/web/src/components/CaseBuilder/ThermalUnits.tsx b/web/src/components/CaseBuilder/ThermalUnits.tsx index acde078..94fdd24 100644 --- a/web/src/components/CaseBuilder/ThermalUnits.tsx +++ b/web/src/components/CaseBuilder/ThermalUnits.tsx @@ -27,6 +27,11 @@ import { } from "../../core/fixtures"; import { ColumnDefinition } from "tabulator-tables"; import { offerDownload } from "../Common/io"; +import { + createThermalUnit, + deleteGenerator, + renameGenerator, +} from "../../core/Operations/generatorOps"; export const ThermalUnitsColumnSpec: ColumnSpec[] = [ { @@ -155,17 +160,17 @@ const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => { }; const onAdd = () => { - // const [newScenario, err] = createThermalUnit(props.scenario); - // if (err) { - // props.onError(err.message); - // return; - // } - // props.onDataChanged(newScenario); + const [newScenario, err] = createThermalUnit(props.scenario); + if (err) { + props.onError(err.message); + return; + } + props.onDataChanged(newScenario); }; const onDelete = (name: string): ValidationError | null => { - // const newScenario = deleteGenerator(name, props.scenario); - // props.onDataChanged(newScenario); + const newScenario = deleteGenerator(name, props.scenario); + props.onDataChanged(newScenario); return null; }; @@ -192,16 +197,16 @@ const ThermalUnitsComponent = (props: CaseBuilderSectionProps) => { oldName: string, newName: string, ): ValidationError | null => { - // const [newScenario, err] = renameGenerator( - // oldName, - // newName, - // props.scenario, - // ); - // if (err) { - // props.onError(err.message); - // return err; - // } - // props.onDataChanged(newScenario); + const [newScenario, err] = renameGenerator( + oldName, + newName, + props.scenario, + ); + if (err) { + props.onError(err.message); + return err; + } + props.onDataChanged(newScenario); return null; }; diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx index 8bf78af..9c816c0 100644 --- a/web/src/components/Common/Forms/DataTable.tsx +++ b/web/src/components/Common/Forms/DataTable.tsx @@ -129,7 +129,9 @@ export const generateTableData = ( break; case "number[N]": for (let i = 0; i < spec.length!; i++) { - entry[`${spec.title} ${i + 1}`] = entryData[spec.title][i] || ""; + let v = entryData[spec.title][i]; + if (v === undefined || v === null) v = ""; + entry[`${spec.title} ${i + 1}`] = v; } break; default: diff --git a/web/src/core/Operations/generatorOps.test.ts b/web/src/core/Operations/generatorOps.test.ts index a2616ba..1badef1 100644 --- a/web/src/core/Operations/generatorOps.test.ts +++ b/web/src/core/Operations/generatorOps.test.ts @@ -9,21 +9,24 @@ import assert from "node:assert"; import { changeProfiledUnitData, createProfiledUnit, + createThermalUnit, deleteGenerator, renameGenerator, } from "./generatorOps"; +import { ValidationError } from "../Validation/validate"; test("createProfiledUnit", () => { const [newScenario, err] = createProfiledUnit(TEST_DATA_1); assert(err === null); assert.equal(Object.keys(newScenario.Generators).length, 4); - assert.deepEqual(newScenario.Generators["pu3"], { - Bus: "b1", - Type: "Profiled", - "Cost ($/MW)": 0, - "Maximum power (MW)": [0, 0, 0, 0, 0], - "Minimum power (MW)": [0, 0, 0, 0, 0], - }); + assert("pu3" in newScenario.Generators); +}); + +test("createThermalUnit", () => { + const [newScenario, err] = createThermalUnit(TEST_DATA_1); + assert(err === null); + assert.equal(Object.keys(newScenario.Generators).length, 4); + assert("g2" in newScenario.Generators); }); test("createProfiledUnit with blank file", () => { @@ -34,7 +37,7 @@ test("createProfiledUnit with blank file", () => { test("changeProfiledUnitData", () => { let scenario = TEST_DATA_1; - let err = null; + let err: ValidationError | null; [scenario, err] = changeProfiledUnitData( "pu1", "Cost ($/MW)", @@ -71,7 +74,7 @@ test("changeProfiledUnitData with invalid bus", () => { test("deleteGenerator", () => { const newScenario = deleteGenerator("pu1", TEST_DATA_1); assert.equal(Object.keys(newScenario.Generators).length, 2); - assert("gen1" in newScenario.Generators); + assert("g1" in newScenario.Generators); assert("pu2" in newScenario.Generators); }); diff --git a/web/src/core/Operations/generatorOps.ts b/web/src/core/Operations/generatorOps.ts index bac9607..8968aa5 100644 --- a/web/src/core/Operations/generatorOps.ts +++ b/web/src/core/Operations/generatorOps.ts @@ -14,13 +14,20 @@ import { } from "./commonOps"; import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits"; +const assertBusesNotEmpty = ( + scenario: UnitCommitmentScenario, +): ValidationError | null => { + if (Object.keys(scenario.Buses).length === 0) + return { message: "Profiled unit requires an existing bus." }; + return null; +}; + export const createProfiledUnit = ( scenario: UnitCommitmentScenario, ): [UnitCommitmentScenario, ValidationError | null] => { - const busNames = Object.keys(scenario.Buses); - if (busNames.length === 0) { - return [scenario, { message: "Profiled unit requires an existing bus." }]; - } + const err = assertBusesNotEmpty(scenario); + if (err) return [scenario, err]; + const busName = Object.keys(scenario.Buses)[0]!; const timeslots = generateTimeslots(scenario); const name = generateUniqueName(scenario.Generators, "pu"); return [ @@ -29,7 +36,7 @@ export const createProfiledUnit = ( Generators: { ...scenario.Generators, [name]: { - Bus: busNames[0]!, + Bus: busName, Type: "Profiled", "Cost ($/MW)": 0, "Minimum power (MW)": Array(timeslots.length).fill(0), @@ -41,6 +48,41 @@ export const createProfiledUnit = ( ]; }; +export const createThermalUnit = ( + scenario: UnitCommitmentScenario, +): [UnitCommitmentScenario, ValidationError | null] => { + const err = assertBusesNotEmpty(scenario); + if (err) return [scenario, err]; + const busName = Object.keys(scenario.Buses)[0]!; + const name = generateUniqueName(scenario.Generators, "g"); + return [ + { + ...scenario, + Generators: { + ...scenario.Generators, + [name]: { + Bus: busName, + Type: "Thermal", + "Production cost curve (MW)": [0], + "Production cost curve ($)": [0], + "Startup costs ($)": [0], + "Startup delays (h)": [1], + "Ramp up limit (MW)": "", + "Ramp down limit (MW)": "", + "Startup limit (MW)": "", + "Shutdown limit (MW)": "", + "Minimum downtime (h)": 1, + "Minimum uptime (h)": 1, + "Initial status (h)": -24, + "Initial power (MW)": 0, + "Must run?": false, + }, + }, + }, + null, + ]; +}; + export const changeProfiledUnitData = ( generator: string, field: string, diff --git a/web/src/core/fixtures.test.ts b/web/src/core/fixtures.test.ts index cee79b1..7c049a9 100644 --- a/web/src/core/fixtures.test.ts +++ b/web/src/core/fixtures.test.ts @@ -13,7 +13,7 @@ export const TEST_DATA_1: UnitCommitmentScenario = { b3: { "Load (MW)": [27.3729, 26.29698, 25.58005, 25.15675, 25.4268] }, }, Generators: { - gen1: { + g1: { Bus: "b1", Type: "Thermal", "Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0], diff --git a/web/src/core/fixtures.tsx b/web/src/core/fixtures.tsx index 2061556..9130245 100644 --- a/web/src/core/fixtures.tsx +++ b/web/src/core/fixtures.tsx @@ -27,10 +27,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 | ""; + "Ramp down limit (MW)": number | ""; + "Startup limit (MW)": number | ""; + "Shutdown limit (MW)": number | ""; "Minimum downtime (h)": number; "Minimum uptime (h)": number; "Initial status (h)": number; @@ -158,7 +158,7 @@ export const TEST_SCENARIO: UnitCommitmentScenario = { ], "Cost ($/MW)": 50.0, }, - gen1: { + g1: { Bus: "b1", Type: "Thermal", "Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],