diff --git a/web/src/components/CaseBuilder/TransmissionLines.tsx b/web/src/components/CaseBuilder/TransmissionLines.tsx index c95e1f9..f3b2d89 100644 --- a/web/src/components/CaseBuilder/TransmissionLines.tsx +++ b/web/src/components/CaseBuilder/TransmissionLines.tsx @@ -27,6 +27,7 @@ import { changeTransmissionLineData, createTransmissionLine, deleteTransmissionLine, + rebuildContingencies, renameTransmissionLine, } from "../../core/Operations/transmissionOps"; import { offerDownload } from "../Common/io"; @@ -68,6 +69,11 @@ export const TransmissionLinesColumnSpec: ColumnSpec[] = [ type: "number", width: 60, }, + { + title: "Contingency?", + type: "lineContingency", + width: 50, + }, ]; const generateTransmissionLinesData = ( @@ -93,6 +99,7 @@ const TransmissionLinesComponent = (props: CaseBuilderSectionProps) => { const onUpload = () => { fileUploadElem.current!.showFilePicker((csv: any) => { + // Parse the CSV data const [newLines, err] = parseCsv( csv, TransmissionLinesColumnSpec, @@ -102,9 +109,19 @@ const TransmissionLinesComponent = (props: CaseBuilderSectionProps) => { props.onError(err.message); return; } + + // Remove contingency field from line and rebuild the contingencies section + const lineContingencies = new Set(); + Object.entries(newLines).forEach(([lineName, line]: [string, any]) => { + if (line["Contingency?"]) lineContingencies.add(lineName); + delete line["Contingency?"]; + }); + const contingencies = rebuildContingencies(lineContingencies); + const newScenario = { ...props.scenario, "Transmission lines": newLines, + Contingencies: contingencies, }; props.onDataChanged(newScenario); }); diff --git a/web/src/components/Common/Forms/DataTable.tsx b/web/src/components/Common/Forms/DataTable.tsx index 4ff9cf7..cbf5c5b 100644 --- a/web/src/components/Common/Forms/DataTable.tsx +++ b/web/src/components/Common/Forms/DataTable.tsx @@ -18,6 +18,7 @@ import { parseNumber, } from "../../../core/Operations/commonOps"; import { UnitCommitmentScenario } from "../../../core/Data/types"; +import { getContingencyTransmissionLines } from "../../../core/Operations/transmissionOps"; export interface ColumnSpec { title: string; @@ -28,7 +29,8 @@ export interface ColumnSpec { | "number[N]" | "number[T]" | "busRef" - | "boolean"; + | "boolean" + | "lineContingency"; length?: number; width: number; } @@ -52,6 +54,7 @@ export const generateTableColumns = ( }); break; case "boolean": + case "lineContingency": columns.push({ ...columnsCommonAttrs, title: spec.title, @@ -117,6 +120,7 @@ export const generateTableData = ( ): any[] => { const data: any[] = []; const timeslots = generateTimeslots(scenario); + let contingencyLines = null; for (const [entryName, entryData] of Object.entries(container) as [ string, any, @@ -135,6 +139,13 @@ export const generateTableData = ( case "busRef": entry[spec.title] = entryData[spec.title]; break; + case "lineContingency": + if (contingencyLines === null) { + contingencyLines = getContingencyTransmissionLines(scenario); + console.log(contingencyLines); + } + entry[spec.title] = contingencyLines.has(entryName); + break; case "number[T]": for (let i = 0; i < timeslots.length; i++) { entry[`${spec.title} ${timeslots[i]}`] = entryData[spec.title][i]; @@ -287,12 +298,12 @@ export const parseCsv = ( } break; } - case "boolean": { + case "boolean": + case "lineContingency": const [val, err] = parseBool(row[spec.title]); if (err) return [data, { message: err.message + rowRef }]; data[name][spec.title] = val; break; - } default: throw Error(`Unknown type: ${spec.type}`); } diff --git a/web/src/core/Data/fixtures.test.ts b/web/src/core/Data/fixtures.test.ts index 2a331b3..433386f 100644 --- a/web/src/core/Data/fixtures.test.ts +++ b/web/src/core/Data/fixtures.test.ts @@ -87,6 +87,12 @@ export const TEST_DATA_1: UnitCommitmentScenario = { "Demand (MW)": [50, 50, 50, 50, 50], }, }, + Contingencies: { + l1: { + "Affected generators": [], + "Affected lines": ["l1"], + }, + }, }; export const TEST_DATA_2: UnitCommitmentScenario = { @@ -101,6 +107,7 @@ export const TEST_DATA_2: UnitCommitmentScenario = { b2: { "Load (MW)": [10, 20, 30, 40] }, b3: { "Load (MW)": [0, 30, 0, 40] }, }, + Contingencies: {}, Generators: {}, "Transmission lines": {}, "Storage units": {}, @@ -115,6 +122,7 @@ export const TEST_DATA_BLANK: UnitCommitmentScenario = { "Time step (min)": 60, }, Buses: {}, + Contingencies: {}, Generators: {}, "Transmission lines": {}, "Storage units": {}, diff --git a/web/src/core/Data/fixtures.tsx b/web/src/core/Data/fixtures.tsx index 9b6a870..8b46ce2 100644 --- a/web/src/core/Data/fixtures.tsx +++ b/web/src/core/Data/fixtures.tsx @@ -22,4 +22,5 @@ export const BLANK_SCENARIO: UnitCommitmentScenario = { "Transmission lines": {}, "Storage units": {}, "Price-sensitive loads": {}, + Contingencies: {}, }; diff --git a/web/src/core/Data/types.tsx b/web/src/core/Data/types.tsx index 3d11661..ce8c6e5 100644 --- a/web/src/core/Data/types.tsx +++ b/web/src/core/Data/types.tsx @@ -69,6 +69,11 @@ export interface PriceSensitiveLoad { "Demand (MW)": number[]; } +export interface Contingency { + "Affected lines": string[]; + "Affected generators": string[]; +} + export interface UnitCommitmentScenario { Parameters: { Version: string; @@ -87,6 +92,9 @@ export interface UnitCommitmentScenario { "Price-sensitive loads": { [name: string]: PriceSensitiveLoad; }; + Contingencies: { + [name: string]: Contingency; + }; } const getTypedGenerators = ( diff --git a/web/src/core/Operations/preprocessing.ts b/web/src/core/Operations/preprocessing.ts index 8aaad31..b779aff 100644 --- a/web/src/core/Operations/preprocessing.ts +++ b/web/src/core/Operations/preprocessing.ts @@ -7,6 +7,10 @@ import { validate, ValidationError } from "../Data/validate"; import { UnitCommitmentScenario } from "../Data/types"; import { migrate } from "../Data/migrate"; +import { + getContingencyTransmissionLines, + rebuildContingencies, +} from "./transmissionOps"; export const preprocess = ( data: any, @@ -57,5 +61,10 @@ export const preprocess = ( } const scenario = result as unknown as UnitCommitmentScenario; + + // Rebuild contingencies + const contingencyLines = getContingencyTransmissionLines(scenario); + scenario["Contingencies"] = rebuildContingencies(contingencyLines); + return [scenario, null]; }; diff --git a/web/src/core/Operations/transmissionOps.test.ts b/web/src/core/Operations/transmissionOps.test.ts index 65abdb5..d05252e 100644 --- a/web/src/core/Operations/transmissionOps.test.ts +++ b/web/src/core/Operations/transmissionOps.test.ts @@ -10,6 +10,8 @@ import { changeTransmissionLineData, createTransmissionLine, deleteTransmissionLine, + getContingencyTransmissionLines, + rebuildContingencies, renameTransmissionLine, } from "./transmissionOps"; import { ValidationError } from "../Data/validate"; @@ -32,6 +34,12 @@ test("renameTransmissionLine", () => { "Emergency flow limit (MW)": 20000.0, "Flow limit penalty ($/MW)": 5000.0, }); + assert.deepEqual(newScenario["Contingencies"], { + l3: { + "Affected lines": ["l3"], + "Affected generators": [], + }, + }); assert.equal(Object.keys(newScenario["Transmission lines"]).length, 1); }); @@ -72,4 +80,23 @@ test("changeTransmissionLineData", () => { test("deleteTransmissionLine", () => { const newScenario = deleteTransmissionLine("l1", TEST_DATA_1); assert.equal(Object.keys(newScenario["Transmission lines"]).length, 0); + assert.equal(Object.keys(newScenario["Contingencies"]).length, 0); +}); + +test("getContingencyTransmissionLines", () => { + const contLines = getContingencyTransmissionLines(TEST_DATA_1); + assert.deepEqual(contLines, new Set(["l1"])); +}); + +test("rebuildContingencies", () => { + assert.deepEqual(rebuildContingencies(new Set(["l1", "l2"])), { + l1: { + "Affected lines": ["l1"], + "Affected generators": [], + }, + l2: { + "Affected lines": ["l2"], + "Affected generators": [], + }, + }); }); diff --git a/web/src/core/Operations/transmissionOps.ts b/web/src/core/Operations/transmissionOps.ts index 0e55707..6cf6351 100644 --- a/web/src/core/Operations/transmissionOps.ts +++ b/web/src/core/Operations/transmissionOps.ts @@ -8,11 +8,16 @@ import { assertBusesNotEmpty, changeData, generateUniqueName, + parseBool, renameItemInObject, } from "./commonOps"; import { ValidationError } from "../Data/validate"; import { TransmissionLinesColumnSpec } from "../../components/CaseBuilder/TransmissionLines"; -import { TransmissionLine, UnitCommitmentScenario } from "../Data/types"; +import { + Contingency, + TransmissionLine, + UnitCommitmentScenario, +} from "../Data/types"; export const createTransmissionLine = ( scenario: UnitCommitmentScenario, @@ -51,7 +56,24 @@ export const renameTransmissionLine = ( scenario["Transmission lines"], ); if (err) return [scenario, err]; - return [{ ...scenario, "Transmission lines": newLine }, null]; + + // Update transmission line contingencies + let newContingencies = scenario["Contingencies"]; + const contingencyLines = getContingencyTransmissionLines(scenario); + if (contingencyLines.has(oldName)) { + contingencyLines.delete(oldName); + contingencyLines.add(newName); + newContingencies = rebuildContingencies(contingencyLines); + } + + return [ + { + ...scenario, + "Transmission lines": newLine, + Contingencies: newContingencies, + }, + null, + ]; }; export const changeTransmissionLineData = ( @@ -60,24 +82,38 @@ export const changeTransmissionLineData = ( 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, + if (field === "Contingency?") { + // Parse boolean value + const [newValue, err] = parseBool(newValueStr); + if (err) return [scenario, err]; + + // Rebuild contingencies + const contLines = getContingencyTransmissionLines(scenario); + if (newValue) contLines.add(line); + else contLines.delete(line); + const newContingencies = rebuildContingencies(contLines); + + return [{ ...scenario, Contingencies: newContingencies }, null]; + } else { + 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, - ]; + null, + ]; + } }; export const deleteTransmissionLine = ( @@ -85,5 +121,43 @@ export const deleteTransmissionLine = ( scenario: UnitCommitmentScenario, ): UnitCommitmentScenario => { const { [name]: _, ...newLines } = scenario["Transmission lines"]; - return { ...scenario, "Transmission lines": newLines }; + + // Update transmission line contingencies + let newContingencies = scenario["Contingencies"]; + const contingencyLines = getContingencyTransmissionLines(scenario); + if (contingencyLines.has(name)) { + contingencyLines.delete(name); + newContingencies = rebuildContingencies(contingencyLines); + } + + return { + ...scenario, + "Transmission lines": newLines, + Contingencies: newContingencies, + }; +}; + +export const getContingencyTransmissionLines = ( + scenario: UnitCommitmentScenario, +): Set => { + let result: Set = new Set(); + Object.entries(scenario.Contingencies).forEach(([name, contingency]) => { + if (contingency["Affected lines"].length !== 1) + throw Error("not implemented"); + result.add(contingency["Affected lines"][0]!!); + }); + return result; +}; + +export const rebuildContingencies = ( + contingencyLines: Set, +): { [name: string]: Contingency } => { + const result: { [name: string]: Contingency } = {}; + contingencyLines.forEach((lineName) => { + result[lineName as string] = { + "Affected lines": [lineName as string], + "Affected generators": [], + }; + }); + return result; };