forked from jappel/leistungsbilanz-ts
217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
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<string, CalculatedDistributionBoardLoad>;
|
|
circuits: Map<string, CalculatedCircuitLoad>;
|
|
}
|
|
|
|
/**
|
|
* 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<string, DistributionBoardLoadBoardInput[]>();
|
|
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<string, CalculatedDistributionBoardLoad>();
|
|
const circuits = new Map<string, CalculatedCircuitLoad>();
|
|
const activePath = new Set<string>();
|
|
|
|
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<string, DistributionBoardLoadBoardInput>,
|
|
circuitById: Map<string, DistributionBoardLoadCircuitInput>
|
|
) {
|
|
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<T>(values: T[], keyOf: (value: T) => string) {
|
|
const groups = new Map<string, T[]>();
|
|
for (const value of values) {
|
|
const key = keyOf(value);
|
|
const group = groups.get(key) ?? [];
|
|
group.push(value);
|
|
groups.set(key, group);
|
|
}
|
|
return groups;
|
|
}
|