Add recursive distribution board load projection

This commit is contained in:
Julian Appel 2026-09-27 22:31:56 +02:00
parent 855a04e655
commit dfeb0e3cb6
11 changed files with 701 additions and 138 deletions

View file

@ -38,13 +38,15 @@ describe("circuit tree controller", () => {
it("returns header, grouped and footer components with both protection layers", async () => {
const originals = {
findList: circuitListRepository.findById,
listLists: circuitListRepository.listByProject,
findProject: projectRepository.findById,
findBoard: distributionBoardRepository.findById,
listSections: circuitSectionRepository.listByCircuitList,
listCircuits: circuitRepository.listByCircuitList,
listBoards: distributionBoardRepository.listByProject,
listSections: circuitSectionRepository.listByCircuitLists,
listCircuits: circuitRepository.listByCircuitLists,
listRows: circuitDeviceRowRepository.listByCircuitList,
listComponents:
distributionBoardComponentRepository.listByCircuitList,
distributionBoardComponentRepository.listByCircuitLists,
listComponentProtection:
distributionBoardComponentRepository.listProtectionByComponentIds,
listCircuitProtection:
@ -57,6 +59,13 @@ describe("circuit tree controller", () => {
distributionBoardId: "board-1",
name: "UV-01",
}) as never;
circuitListRepository.listByProject = async () =>
[{
id: "list-1",
projectId: "project-1",
distributionBoardId: "board-1",
name: "UV-01",
}] as never;
projectRepository.findById = async () =>
({
id: "project-1",
@ -66,8 +75,19 @@ describe("circuit tree controller", () => {
threePhaseVoltageV: 400,
}) as never;
distributionBoardRepository.findById = async () =>
({ id: "board-1", simultaneityFactor: 0.8 }) as never;
circuitSectionRepository.listByCircuitList = async () =>
({ id: "board-1", name: "UV-01", simultaneityFactor: 0.8 }) as never;
distributionBoardRepository.listByProject = async () =>
[{
id: "board-1",
projectId: "project-1",
name: "UV-01",
boardNumber: "UV-01",
supplyType: "AV",
simultaneityFactor: 0.8,
feedingDistributionBoardId: null,
feedingCircuitId: null,
}] as never;
circuitSectionRepository.listByCircuitLists = async () =>
[
{
id: "group-1",
@ -80,7 +100,7 @@ describe("circuit tree controller", () => {
groupNumber: 1,
},
] as never;
circuitRepository.listByCircuitList = async () =>
circuitRepository.listByCircuitLists = async () =>
[
{
id: "circuit-1",
@ -90,10 +110,19 @@ describe("circuit tree controller", () => {
displayName: "Licht",
sortOrder: 10,
isReserve: 0,
cableType: null,
cableCrossSection: null,
cableLength: null,
rcdAssignment: null,
terminalDesignation: null,
voltage: 230,
controlRequirement: null,
status: null,
remark: null,
},
] as never;
circuitDeviceRowRepository.listByCircuitList = async () => [];
distributionBoardComponentRepository.listByCircuitList =
distributionBoardComponentRepository.listByCircuitLists =
async () =>
[
{
@ -170,13 +199,15 @@ describe("circuit tree controller", () => {
);
} finally {
circuitListRepository.findById = originals.findList;
circuitListRepository.listByProject = originals.listLists;
projectRepository.findById = originals.findProject;
distributionBoardRepository.findById = originals.findBoard;
circuitSectionRepository.listByCircuitList =
distributionBoardRepository.listByProject = originals.listBoards;
circuitSectionRepository.listByCircuitLists =
originals.listSections;
circuitRepository.listByCircuitList = originals.listCircuits;
circuitRepository.listByCircuitLists = originals.listCircuits;
circuitDeviceRowRepository.listByCircuitList = originals.listRows;
distributionBoardComponentRepository.listByCircuitList =
distributionBoardComponentRepository.listByCircuitLists =
originals.listComponents;
distributionBoardComponentRepository.listProtectionByComponentIds =
originals.listComponentProtection;

View file

@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { calculateDistributionBoardLoads } from "../src/domain/calculations/distribution-board-load-calculation.js";
const board = (
id: string,
simultaneityFactor: number,
feedingDistributionBoardId: string | null = null,
feedingCircuitId: string | null = null
) => ({
id,
name: id,
boardNumber: id.toUpperCase(),
simultaneityFactor,
feedingDistributionBoardId,
feedingCircuitId,
});
describe("distribution-board load calculation", () => {
it("rolls recursive demand into the selected feeder circuit exactly once", () => {
const result = calculateDistributionBoardLoads({
boards: [
board("nshv", 0.8),
board("uv1", 0.5, "nshv", "nshv-out"),
board("uv2", 0.25, "uv1", "uv1-out"),
],
circuits: [
{ id: "nshv-out", distributionBoardId: "nshv", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
{ id: "uv1-out", distributionBoardId: "uv1", rows: [{ activePowerKw: 4, cosPhi: 0.8 }] },
{ id: "uv2-load", distributionBoardId: "uv2", rows: [{ activePowerKw: 8, cosPhi: 0.8 }] },
],
});
assert.deepEqual(result.boards.get("uv2"), {
distributionBoardId: "uv2",
activePowerBeforeSimultaneityKw: 8,
apparentPowerBeforeSimultaneityKva: 10,
demandActivePowerKw: 2,
demandApparentPowerKva: 2.5,
cosPhi: 0.8,
});
assert.equal(result.circuits.get("uv1-out")?.activePowerKw, 6);
assert.equal(result.boards.get("uv1")?.demandActivePowerKw, 3);
assert.equal(result.circuits.get("nshv-out")?.activePowerKw, 5);
assert.equal(result.boards.get("nshv")?.demandActivePowerKw, 4);
assert.equal(result.circuits.get("nshv-out")?.downstreamBoards.length, 1);
});
it("combines apparent power for multiple child boards on one circuit", () => {
const result = calculateDistributionBoardLoads({
boards: [board("main", 1), board("a", 1, "main", "out"), board("b", 1, "main", "out")],
circuits: [
{ id: "out", distributionBoardId: "main", rows: [] },
{ id: "a-load", distributionBoardId: "a", rows: [{ activePowerKw: 2, cosPhi: 0.5 }] },
{ id: "b-load", distributionBoardId: "b", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
],
});
const feeder = result.circuits.get("out")!;
assert.equal(feeder.activePowerKw, 4);
assert.equal(feeder.apparentPowerKva, 6);
assert.equal(feeder.cosPhi, 2 / 3);
assert.deepEqual(feeder.downstreamBoards.map((entry) => entry.distributionBoardId), ["a", "b"]);
});
it("keeps a board-only hierarchy relation out of load rollup", () => {
const result = calculateDistributionBoardLoads({
boards: [board("main", 1), board("child", 1, "main", null)],
circuits: [
{ id: "main-load", distributionBoardId: "main", rows: [{ activePowerKw: 1, cosPhi: 1 }] },
{ id: "child-load", distributionBoardId: "child", rows: [{ activePowerKw: 5, cosPhi: 1 }] },
],
});
assert.equal(result.boards.get("main")?.demandActivePowerKw, 1);
assert.equal(result.boards.get("child")?.demandActivePowerKw, 5);
});
it("rejects cycles defensively", () => {
assert.throws(
() => calculateDistributionBoardLoads({
boards: [board("a", 1, "b", "b-out"), board("b", 1, "a", "a-out")],
circuits: [
{ id: "a-out", distributionBoardId: "a", rows: [] },
{ id: "b-out", distributionBoardId: "b", rows: [] },
],
}),
/cycle/
);
});
});