export interface DistributionBoardLoadBoardInput { id: string; name: string; boardNumber: string | null; simultaneityFactor: number; feedingDistributionBoardId: string | null; feedingCircuitId: string | null; } export interface DistributionBoardLoadCircuitInput { id: string; distributionBoardId: string; rows: Array<{ activePowerKw: number; cosPhi: number | null; }>; } export interface DownstreamDistributionBoardLoad { distributionBoardId: string; name: string; boardNumber: string | null; activePowerKw: number; apparentPowerKva: number; cosPhi: number; } export interface CalculatedCircuitLoad { circuitId: string; deviceActivePowerKw: number; deviceApparentPowerKva: number; activePowerKw: number; apparentPowerKva: number; cosPhi: number; downstreamBoards: DownstreamDistributionBoardLoad[]; } export interface CalculatedDistributionBoardLoad { distributionBoardId: string; activePowerBeforeSimultaneityKw: number; apparentPowerBeforeSimultaneityKva: number; demandActivePowerKw: number; demandApparentPowerKva: number; cosPhi: number; } export interface DistributionBoardLoadCalculation { boards: Map; circuits: Map; } /** * Calculates one project graph. A child board enters exactly one feeder * circuit with its demand after the child's own board simultaneity factor. * Board-only relations form a hierarchy but intentionally add no load. */ export function calculateDistributionBoardLoads(input: { boards: DistributionBoardLoadBoardInput[]; circuits: DistributionBoardLoadCircuitInput[]; }): DistributionBoardLoadCalculation { const boardById = new Map(input.boards.map((board) => [board.id, board])); const circuitById = new Map(input.circuits.map((circuit) => [circuit.id, circuit])); const circuitsByBoardId = groupBy( input.circuits, (circuit) => circuit.distributionBoardId ); const childrenByCircuitId = new Map(); for (const board of input.boards) { assertBoardInput(board, boardById, circuitById); if (board.feedingCircuitId === null) continue; const children = childrenByCircuitId.get(board.feedingCircuitId) ?? []; children.push(board); childrenByCircuitId.set(board.feedingCircuitId, children); } const boards = new Map(); const circuits = new Map(); const activePath = new Set(); const calculateBoard = (boardId: string): CalculatedDistributionBoardLoad => { const cached = boards.get(boardId); if (cached) return cached; if (activePath.has(boardId)) { throw new Error("Distribution-board feeding graph contains a cycle."); } const board = boardById.get(boardId); if (!board) throw new Error("Distribution board is missing from load graph."); activePath.add(boardId); let activePowerBeforeSimultaneityKw = 0; let apparentPowerBeforeSimultaneityKva = 0; for (const circuit of circuitsByBoardId.get(boardId) ?? []) { let deviceActivePowerKw = 0; let deviceApparentPowerKva = 0; for (const row of circuit.rows) { assertNonNegativeFinite(row.activePowerKw, "row active power"); const cosPhi = row.cosPhi ?? 1; if (!Number.isFinite(cosPhi) || cosPhi <= 0 || cosPhi > 1) { throw new Error("Row cos phi must be greater than zero and at most one."); } deviceActivePowerKw += row.activePowerKw; deviceApparentPowerKva += row.activePowerKw / cosPhi; } const downstreamBoards = (childrenByCircuitId.get(circuit.id) ?? []) .map((child) => { const childLoad = calculateBoard(child.id); return { distributionBoardId: child.id, name: child.name, boardNumber: child.boardNumber, activePowerKw: childLoad.demandActivePowerKw, apparentPowerKva: childLoad.demandApparentPowerKva, cosPhi: effectiveCosPhi( childLoad.demandActivePowerKw, childLoad.demandApparentPowerKva ), }; }); const activePowerKw = deviceActivePowerKw + downstreamBoards.reduce((sum, child) => sum + child.activePowerKw, 0); const apparentPowerKva = deviceApparentPowerKva + downstreamBoards.reduce((sum, child) => sum + child.apparentPowerKva, 0); circuits.set(circuit.id, { circuitId: circuit.id, deviceActivePowerKw, deviceApparentPowerKva, activePowerKw, apparentPowerKva, cosPhi: effectiveCosPhi(activePowerKw, apparentPowerKva), downstreamBoards, }); activePowerBeforeSimultaneityKw += activePowerKw; apparentPowerBeforeSimultaneityKva += apparentPowerKva; } activePath.delete(boardId); const result = { distributionBoardId: boardId, activePowerBeforeSimultaneityKw, apparentPowerBeforeSimultaneityKva, demandActivePowerKw: activePowerBeforeSimultaneityKw * board.simultaneityFactor, demandApparentPowerKva: apparentPowerBeforeSimultaneityKva * board.simultaneityFactor, cosPhi: effectiveCosPhi( activePowerBeforeSimultaneityKw, apparentPowerBeforeSimultaneityKva ), }; boards.set(boardId, result); return result; }; for (const board of input.boards) calculateBoard(board.id); return { boards, circuits }; } function assertBoardInput( board: DistributionBoardLoadBoardInput, boardById: Map, circuitById: Map ) { if ( !Number.isFinite(board.simultaneityFactor) || board.simultaneityFactor < 0 || board.simultaneityFactor > 1 ) { throw new Error("Board simultaneity factor must be between zero and one."); } if (board.feedingDistributionBoardId === board.id) { throw new Error("Distribution board cannot feed itself."); } if ( board.feedingDistributionBoardId !== null && !boardById.has(board.feedingDistributionBoardId) ) { throw new Error("Feeding distribution board is missing from load graph."); } if ( board.feedingCircuitId !== null && board.feedingDistributionBoardId === null ) { throw new Error("Feeding circuit requires a feeding distribution board."); } if ( board.feedingCircuitId !== null && circuitById.get(board.feedingCircuitId)?.distributionBoardId !== board.feedingDistributionBoardId ) { throw new Error("Feeding circuit belongs to another distribution board."); } } function effectiveCosPhi(activePowerKw: number, apparentPowerKva: number) { return activePowerKw > 0 && apparentPowerKva > 0 ? activePowerKw / apparentPowerKva : 1; } function assertNonNegativeFinite(value: number, label: string) { if (!Number.isFinite(value) || value < 0) { throw new Error(`${label} must be a non-negative finite number.`); } } function groupBy(values: T[], keyOf: (value: T) => string) { const groups = new Map(); for (const value of values) { const key = keyOf(value); const group = groups.get(key) ?? []; group.push(value); groups.set(key, group); } return groups; }