/** * Mock flow-distribution solver. * * Not a hydraulic simulation — a plausible mass-balance mock: * - sources offer `supply`, consumers request `demand` (per relation network); * - each consumer's demand is split across reachable sources proportionally * to their supply; * - each (source, consumer) portion is pushed along the shortest path * (BFS, edge count), accumulating a signed flow per edge; * - positive flow runs source→target of the edge as drawn, negative runs * against it. Demands are scaled down if total supply is insufficient. */ import type { EdgeData, PipelineGraph } from '../model/graph'; import { getNodeType } from '../model/nodeTypes'; export interface EdgeFlow { edgeId: string; /** Signed flow, positive along the drawn source→target direction, m³/h. */ flow: number; } export interface FlowResult { /** edgeId → signed flow. Every edge of the graph is present. */ flows: Map; maxAbsFlow: number; totalSupplied: number; totalDemand: number; /** Consumer node ids that no source can reach. */ unreachedConsumers: string[]; } function numProp(props: Record, key: string): number { const v = props[key]; return typeof v === 'number' && Number.isFinite(v) ? v : 0; } interface Adj { edge: EdgeData; other: string; } const FLUID_RELATIONS = new Set(['water', 'heat', 'gas', 'sewage']); /** Undirected adjacency restricted to fluid-carrying relations. */ function buildAdjacency(graph: PipelineGraph): Map { const adj = new Map(); for (const edge of graph.edges.values()) { if (!FLUID_RELATIONS.has(edge.relation)) continue; // power/signal lines carry no fluid const a = adj.get(edge.sourceNodeId) ?? []; a.push({ edge, other: edge.targetNodeId }); adj.set(edge.sourceNodeId, a); const b = adj.get(edge.targetNodeId) ?? []; b.push({ edge, other: edge.sourceNodeId }); adj.set(edge.targetNodeId, b); } return adj; } /** BFS shortest path from `from` to `to`; returns traversed adjacency steps. */ function shortestPath(adj: Map, from: string, to: string): { edge: EdgeData; forward: boolean }[] | null { if (from === to) return []; const prev = new Map(); const queue = [from]; const seen = new Set([from]); while (queue.length > 0) { const node = queue.shift()!; for (const { edge, other } of adj.get(node) ?? []) { if (seen.has(other)) continue; seen.add(other); prev.set(other, { node, edge }); if (other === to) { const path: { edge: EdgeData; forward: boolean }[] = []; let cur = to; while (cur !== from) { const step = prev.get(cur)!; path.unshift({ edge: step.edge, forward: step.edge.sourceNodeId === step.node }); cur = step.node; } return path; } queue.push(other); } } return null; } export function solveFlow(graph: PipelineGraph): FlowResult { const adj = buildAdjacency(graph); const flows = new Map(); for (const edge of graph.edges.values()) flows.set(edge.id, 0); const sources: { id: string; supply: number }[] = []; const consumers: { id: string; demand: number }[] = []; for (const node of graph.nodes.values()) { const role = getNodeType(node.typeId)?.simRole; if (role === 'source') { const supply = numProp(node.props, 'supply'); if (supply > 0) sources.push({ id: node.id, supply }); } else if (role === 'consumer') { const demand = numProp(node.props, 'demand'); if (demand > 0) consumers.push({ id: node.id, demand }); } } const totalDemand = consumers.reduce((s, c) => s + c.demand, 0); let totalSupplied = 0; const unreachedConsumers: string[] = []; for (const consumer of consumers) { // Which sources can reach this consumer, and by what path? const routes: { path: { edge: EdgeData; forward: boolean }[]; supply: number }[] = []; for (const source of sources) { const path = shortestPath(adj, source.id, consumer.id); if (path) routes.push({ path, supply: source.supply }); } if (routes.length === 0) { unreachedConsumers.push(consumer.id); continue; } const reachableSupply = routes.reduce((s, r) => s + r.supply, 0); // Scale demand down when reachable supply cannot cover it. const served = Math.min(consumer.demand, reachableSupply); totalSupplied += served; for (const route of routes) { const portion = served * (route.supply / reachableSupply); for (const step of route.path) { flows.set(step.edge.id, (flows.get(step.edge.id) ?? 0) + (step.forward ? portion : -portion)); } } } let maxAbsFlow = 0; for (const f of flows.values()) maxAbsFlow = Math.max(maxAbsFlow, Math.abs(f)); return { flows, maxAbsFlow, totalSupplied, totalDemand, unreachedConsumers }; }