(null);
+
+ const onDownload = () => {
+ const [data, columns] = generateTransmissionLinesData(props.scenario);
+ const csvContents = generateCsv(data, columns);
+ offerDownload(csvContents, "text/csv", "transmission.csv");
+ };
+
+ const onUpload = () => {
+ fileUploadElem.current!.showFilePicker((csv: any) => {
+ const [newLines, err] = parseCsv(
+ csv,
+ TransmissionLinesColumnSpec,
+ props.scenario,
+ );
+ if (err) {
+ props.onError(err.message);
+ return;
+ }
+ const newScenario = {
+ ...props.scenario,
+ "Transmission lines": newLines,
+ };
+ props.onDataChanged(newScenario);
+ });
+ };
+
+ const onAdd = () => {
+ const [newScenario, err] = createTransmissionLine(props.scenario);
+ if (err) {
+ props.onError(err.message);
+ return;
+ }
+ props.onDataChanged(newScenario);
+ };
+
+ const onDelete = (name: string): ValidationError | null => {
+ const newScenario = deleteTransmissionLine(name, props.scenario);
+ props.onDataChanged(newScenario);
+ return null;
+ };
+
+ const onDataChanged = (
+ name: string,
+ field: string,
+ newValue: string,
+ ): ValidationError | null => {
+ const [newScenario, err] = changeTransmissionLineData(
+ name,
+ field,
+ newValue,
+ props.scenario,
+ );
+ if (err) {
+ props.onError(err.message);
+ return err;
+ }
+ props.onDataChanged(newScenario);
+ return null;
+ };
+
+ const onRename = (
+ oldName: string,
+ newName: string,
+ ): ValidationError | null => {
+ const [newScenario, err] = renameTransmissionLine(
+ oldName,
+ newName,
+ props.scenario,
+ );
+ if (err) {
+ props.onError(err.message);
+ return err;
+ }
+ props.onDataChanged(newScenario);
+ return null;
+ };
+
+ return (
+
+
+
+
+
+
+ generateTransmissionLinesData(props.scenario)}
+ />
+
+
+ );
+};
+
+export default TransmissionLinesComponent;
diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx
index 992ebac..67b652c 100644
--- a/web/src/components/Common/Forms/DataTable.tsx
+++ b/web/src/components/Common/Forms/DataTable.tsx
@@ -288,7 +288,10 @@ export const floatFormatter = (cell: CellComponent) => {
if (v === "") {
return "—";
} else {
- return parseFloat(cell.getValue()).toFixed(1);
+ return parseFloat(cell.getValue()).toLocaleString("en-US", {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1,
+ });
}
};
diff --git a/web/src/core/Operations/commonOps.ts b/web/src/core/Operations/commonOps.ts
index e0cf187..e3af76c 100644
--- a/web/src/core/Operations/commonOps.ts
+++ b/web/src/core/Operations/commonOps.ts
@@ -234,3 +234,10 @@ export const changeData = (
}
throw Error(`Unknown field: ${fieldName}`);
};
+export const assertBusesNotEmpty = (
+ scenario: UnitCommitmentScenario,
+): ValidationError | null => {
+ if (Object.keys(scenario.Buses).length === 0)
+ return { message: "Profiled unit requires an existing bus." };
+ return null;
+};
diff --git a/web/src/core/Operations/generatorOps.ts b/web/src/core/Operations/generatorOps.ts
index 10ee096..56fef26 100644
--- a/web/src/core/Operations/generatorOps.ts
+++ b/web/src/core/Operations/generatorOps.ts
@@ -8,6 +8,7 @@ import { Generators, UnitCommitmentScenario } from "../fixtures";
import { generateTimeslots } from "../../components/Common/Forms/DataTable";
import { ValidationError } from "../Validation/validate";
import {
+ assertBusesNotEmpty,
changeData,
generateUniqueName,
renameItemInObject,
@@ -15,14 +16,6 @@ import {
import { ProfiledUnitsColumnSpec } from "../../components/CaseBuilder/ProfiledUnits";
import { ThermalUnitsColumnSpec } from "../../components/CaseBuilder/ThermalUnits";
-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] => {
diff --git a/web/src/core/Operations/transmissionOps.test.ts b/web/src/core/Operations/transmissionOps.test.ts
new file mode 100644
index 0000000..1300e2c
--- /dev/null
+++ b/web/src/core/Operations/transmissionOps.test.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { TEST_DATA_1 } from "../fixtures.test";
+import assert from "node:assert";
+import {
+ changeTransmissionLineData,
+ createTransmissionLine,
+ deleteTransmissionLine,
+ renameTransmissionLine,
+} from "./transmissionOps";
+import { ValidationError } from "../Validation/validate";
+
+test("createTransmissionLine", () => {
+ const [newScenario, err] = createTransmissionLine(TEST_DATA_1);
+ assert(err === null);
+ assert.equal(Object.keys(newScenario["Transmission lines"]).length, 2);
+ assert("l2" in newScenario["Transmission lines"]);
+});
+
+test("renameTransmissionLine", () => {
+ const [newScenario, err] = renameTransmissionLine("l1", "l3", TEST_DATA_1);
+ assert(err === null);
+ assert.deepEqual(newScenario["Transmission lines"]["l3"], {
+ "Source bus": "b1",
+ "Target bus": "b2",
+ "Susceptance (S)": 29.49686,
+ "Normal flow limit (MW)": 15000.0,
+ "Emergency flow limit (MW)": 20000.0,
+ "Flow limit penalty ($/MW)": 5000.0,
+ });
+ assert.equal(Object.keys(newScenario["Transmission lines"]).length, 1);
+});
+
+test("changeTransmissionLineData", () => {
+ let scenario = TEST_DATA_1;
+ let err: ValidationError | null;
+ [scenario, err] = changeTransmissionLineData(
+ "l1",
+ "Source bus",
+ "b3",
+ scenario,
+ );
+ assert.equal(err, null);
+ [scenario, err] = changeTransmissionLineData(
+ "l1",
+ "Normal flow limit (MW)",
+ "99",
+ scenario,
+ );
+ assert.equal(err, null);
+ [scenario, err] = changeTransmissionLineData(
+ "l1",
+ "Target bus",
+ "b1",
+ scenario,
+ );
+ assert.equal(err, null);
+ assert.deepEqual(scenario["Transmission lines"]["l1"], {
+ "Source bus": "b3",
+ "Target bus": "b1",
+ "Susceptance (S)": 29.49686,
+ "Normal flow limit (MW)": 99,
+ "Emergency flow limit (MW)": 20000.0,
+ "Flow limit penalty ($/MW)": 5000.0,
+ });
+});
+
+test("deleteTransmissionLine", () => {
+ const newScenario = deleteTransmissionLine("l1", TEST_DATA_1);
+ assert.equal(Object.keys(newScenario["Transmission lines"]).length, 0);
+});
diff --git a/web/src/core/Operations/transmissionOps.ts b/web/src/core/Operations/transmissionOps.ts
new file mode 100644
index 0000000..6e90f21
--- /dev/null
+++ b/web/src/core/Operations/transmissionOps.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { TransmissionLine, UnitCommitmentScenario } from "../fixtures";
+import {
+ assertBusesNotEmpty,
+ changeData,
+ generateUniqueName,
+ renameItemInObject,
+} from "./commonOps";
+import { ValidationError } from "../Validation/validate";
+import { TransmissionLinesColumnSpec } from "../../components/CaseBuilder/TransmissionLines";
+
+export const createTransmissionLine = (
+ 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["Transmission lines"], "l");
+ return [
+ {
+ ...scenario,
+ "Transmission lines": {
+ ...scenario["Transmission lines"],
+ [name]: {
+ "Source bus": busName,
+ "Target bus": busName,
+ "Susceptance (S)": 1.0,
+ "Normal flow limit (MW)": 1000,
+ "Emergency flow limit (MW)": 1500,
+ "Flow limit penalty ($/MW)": 5000.0,
+ },
+ },
+ },
+ null,
+ ];
+};
+
+export const renameTransmissionLine = (
+ oldName: string,
+ newName: string,
+ scenario: UnitCommitmentScenario,
+): [UnitCommitmentScenario, ValidationError | null] => {
+ const [newLine, err] = renameItemInObject(
+ oldName,
+ newName,
+ scenario["Transmission lines"],
+ );
+ if (err) return [scenario, err];
+ return [{ ...scenario, "Transmission lines": newLine }, null];
+};
+
+export const changeTransmissionLineData = (
+ line: string,
+ field: string,
+ newValueStr: string,
+ scenario: UnitCommitmentScenario,
+): [UnitCommitmentScenario, ValidationError | null] => {
+ const [newLine, err] = changeData(
+ field,
+ newValueStr,
+ scenario["Transmission lines"][line]!,
+ TransmissionLinesColumnSpec,
+ scenario,
+ );
+ if (err) return [scenario, err];
+ return [
+ {
+ ...scenario,
+ "Transmission lines": {
+ ...scenario["Transmission lines"],
+ [line]: newLine as TransmissionLine,
+ },
+ },
+ null,
+ ];
+};
+
+export const deleteTransmissionLine = (
+ name: string,
+ scenario: UnitCommitmentScenario,
+): UnitCommitmentScenario => {
+ const { [name]: _, ...newLines } = scenario["Transmission lines"];
+ return { ...scenario, "Transmission lines": newLines };
+};
diff --git a/web/src/core/fixtures.test.ts b/web/src/core/fixtures.test.ts
index 7c049a9..916ae9a 100644
--- a/web/src/core/fixtures.test.ts
+++ b/web/src/core/fixtures.test.ts
@@ -45,6 +45,16 @@ export const TEST_DATA_1: UnitCommitmentScenario = {
"Minimum power (MW)": [0, 0, 0, 0, 0],
},
},
+ "Transmission lines": {
+ l1: {
+ "Source bus": "b1",
+ "Target bus": "b2",
+ "Susceptance (S)": 29.49686,
+ "Normal flow limit (MW)": 15000.0,
+ "Emergency flow limit (MW)": 20000.0,
+ "Flow limit penalty ($/MW)": 5000.0,
+ },
+ },
};
export const TEST_DATA_2: UnitCommitmentScenario = {
@@ -60,6 +70,7 @@ export const TEST_DATA_2: UnitCommitmentScenario = {
b3: { "Load (MW)": [0, 30, 0, 40] },
},
Generators: {},
+ "Transmission lines": {},
};
export const TEST_DATA_BLANK: UnitCommitmentScenario = {
@@ -71,6 +82,7 @@ export const TEST_DATA_BLANK: UnitCommitmentScenario = {
},
Buses: {},
Generators: {},
+ "Transmission lines": {},
};
test("fixtures", () => {});
diff --git a/web/src/core/fixtures.tsx b/web/src/core/fixtures.tsx
index 9130245..dcd3b66 100644
--- a/web/src/core/fixtures.tsx
+++ b/web/src/core/fixtures.tsx
@@ -38,6 +38,15 @@ export interface ThermalUnit {
"Must run?": boolean;
}
+export interface TransmissionLine {
+ "Source bus": string;
+ "Target bus": string;
+ "Susceptance (S)": number;
+ "Normal flow limit (MW)": number;
+ "Emergency flow limit (MW)": number;
+ "Flow limit penalty ($/MW)": number;
+}
+
export interface UnitCommitmentScenario {
Parameters: {
Version: string;
@@ -47,6 +56,9 @@ export interface UnitCommitmentScenario {
};
Buses: Buses;
Generators: Generators;
+ "Transmission lines": {
+ [name: string]: TransmissionLine;
+ };
}
const getTypedGenerators = (
@@ -81,6 +93,7 @@ export const BLANK_SCENARIO: UnitCommitmentScenario = {
},
Buses: {},
Generators: {},
+ "Transmission lines": {},
};
export const TEST_SCENARIO: UnitCommitmentScenario = {
@@ -176,4 +189,14 @@ export const TEST_SCENARIO: UnitCommitmentScenario = {
"Must run?": false,
},
},
+ "Transmission lines": {
+ l1: {
+ "Source bus": "b1",
+ "Target bus": "b2",
+ "Susceptance (S)": 29.49686,
+ "Normal flow limit (MW)": 15000.0,
+ "Emergency flow limit (MW)": 20000.0,
+ "Flow limit penalty ($/MW)": 5000.0,
+ },
+ },
};