import { describe, it, expect } from 'vitest'; import { PipelineGraph } from '../src/model/graph'; import { solveFlow } from '../src/sim/flow'; function chain(): PipelineGraph { // source --e1--> pump --e2--> consumer(demand 5) const g = new PipelineGraph(); g.addNode({ id: 'src', typeId: 'waterSource', x: 0, y: 0, angle: 0, props: { supply: 50 } }); g.addNode({ id: 'pump', typeId: 'pump', x: 1, y: 0, angle: 0 }); g.addNode({ id: 'c1', typeId: 'consumer', x: 2, y: 0, angle: 0, props: { demand: 5 } }); g.addEdge({ id: 'e1', sourceNodeId: 'src', sourcePortId: 'out', targetNodeId: 'pump', targetPortId: 'in' }); g.addEdge({ id: 'e2', sourceNodeId: 'pump', sourcePortId: 'out', targetNodeId: 'c1', targetPortId: 'waterIn' }); return g; } describe('mock flow solver', () => { it('pushes consumer demand along a simple chain', () => { const r = solveFlow(chain()); expect(r.flows.get('e1')).toBeCloseTo(5); expect(r.flows.get('e2')).toBeCloseTo(5); expect(r.totalDemand).toBe(5); expect(r.totalSupplied).toBe(5); expect(r.unreachedConsumers).toEqual([]); }); it('reports negative flow when an edge is drawn against the flow direction', () => { const g = new PipelineGraph(); g.addNode({ id: 'src', typeId: 'waterSource', x: 0, y: 0, angle: 0, props: { supply: 50 } }); g.addNode({ id: 'tee', typeId: 'tee', x: 1, y: 0, angle: 0 }); g.addNode({ id: 'c1', typeId: 'consumer', x: 2, y: 0, angle: 0, props: { demand: 8 } }); // Drawn from the tee back to the source: flow should be negative. g.addEdge({ id: 'back', sourceNodeId: 'tee', sourcePortId: 'a', targetNodeId: 'src', targetPortId: 'out' }); g.addEdge({ id: 'fwd', sourceNodeId: 'tee', sourcePortId: 'b', targetNodeId: 'c1', targetPortId: 'waterIn' }); const r = solveFlow(g); expect(r.flows.get('back')).toBeCloseTo(-8); expect(r.flows.get('fwd')).toBeCloseTo(8); }); it('splits demand across sources proportionally to their supply', () => { const g = new PipelineGraph(); g.addNode({ id: 's1', typeId: 'waterSource', x: 0, y: 0, angle: 0, props: { supply: 30 } }); g.addNode({ id: 's2', typeId: 'waterSource', x: 0, y: 2, angle: 0, props: { supply: 10 } }); g.addNode({ id: 'tee', typeId: 'tee', x: 1, y: 1, angle: 0 }); g.addNode({ id: 'c1', typeId: 'consumer', x: 2, y: 1, angle: 0, props: { demand: 20 } }); g.addEdge({ id: 'a1', sourceNodeId: 's1', sourcePortId: 'out', targetNodeId: 'tee', targetPortId: 'a' }); g.addEdge({ id: 'a2', sourceNodeId: 's2', sourcePortId: 'out', targetNodeId: 'tee', targetPortId: 'c' }); g.addEdge({ id: 'out', sourceNodeId: 'tee', sourcePortId: 'b', targetNodeId: 'c1', targetPortId: 'waterIn' }); const r = solveFlow(g); expect(r.flows.get('a1')).toBeCloseTo(15); // 20 * 30/40 expect(r.flows.get('a2')).toBeCloseTo(5); // 20 * 10/40 expect(r.flows.get('out')).toBeCloseTo(20); }); it('conserves mass at pass-through junctions', () => { const g = new PipelineGraph(); g.addNode({ id: 'src', typeId: 'waterSource', x: 0, y: 0, angle: 0, props: { supply: 100 } }); g.addNode({ id: 'tee', typeId: 'tee', x: 1, y: 0, angle: 0 }); g.addNode({ id: 'c1', typeId: 'consumer', x: 2, y: 0, angle: 0, props: { demand: 7 } }); g.addNode({ id: 'c2', typeId: 'consumer', x: 2, y: 1, angle: 0, props: { demand: 3 } }); g.addEdge({ id: 'in', sourceNodeId: 'src', sourcePortId: 'out', targetNodeId: 'tee', targetPortId: 'a' }); g.addEdge({ id: 'o1', sourceNodeId: 'tee', sourcePortId: 'b', targetNodeId: 'c1', targetPortId: 'waterIn' }); g.addEdge({ id: 'o2', sourceNodeId: 'tee', sourcePortId: 'c', targetNodeId: 'c2', targetPortId: 'waterIn' }); const r = solveFlow(g); // Inflow to tee equals sum of outflows. expect(r.flows.get('in')).toBeCloseTo(r.flows.get('o1')! + r.flows.get('o2')!); expect(r.flows.get('in')).toBeCloseTo(10); }); it('scales demand down when supply is insufficient', () => { const g = chain(); g.nodes.get('src')!.props.supply = 2; // demand is 5 const r = solveFlow(g); expect(r.flows.get('e2')).toBeCloseTo(2); expect(r.totalSupplied).toBeCloseTo(2); expect(r.totalDemand).toBe(5); }); it('flags consumers with no path to any source', () => { const g = chain(); g.addNode({ id: 'lonely', typeId: 'consumer', x: 9, y: 9, angle: 0, props: { demand: 4 } }); const r = solveFlow(g); expect(r.unreachedConsumers).toEqual(['lonely']); expect(r.totalSupplied).toBeCloseTo(5); }); it('ignores signal lines when routing flow', () => { const g = new PipelineGraph(); g.addNode({ id: 'src', typeId: 'waterSource', x: 0, y: 0, angle: 0, props: { supply: 50 } }); g.addNode({ id: 'sensor', typeId: 'sensor', x: 1, y: 0, angle: 0 }); g.addNode({ id: 'meter', typeId: 'meter', x: 1, y: 1, angle: 0 }); g.addNode({ id: 'c1', typeId: 'consumer', x: 2, y: 0, angle: 0, props: { demand: 5 } }); // Water path: src → meter → consumer. Signal line meter → nothing useful. g.addEdge({ id: 'w1', sourceNodeId: 'src', sourcePortId: 'out', targetNodeId: 'meter', targetPortId: 'in' }); g.addEdge({ id: 'w2', sourceNodeId: 'meter', sourcePortId: 'out', targetNodeId: 'c1', targetPortId: 'waterIn' }); const r = solveFlow(g); expect(r.flows.get('w1')).toBeCloseTo(5); expect(r.flows.get('w2')).toBeCloseTo(5); void g.nodes.get('sensor'); }); it('does not route fluid through power lines', () => { const g = chain(); // Wire a power supply to the pump: must stay at zero flow and must not // become a path or a fluid source. g.addNode({ id: 'mcc', typeId: 'powerSupply', x: 1, y: 1, angle: 0, props: { supply: 100 } }); g.addEdge({ id: 'pw', sourceNodeId: 'mcc', sourcePortId: 'out', targetNodeId: 'pump', targetPortId: 'powerIn' }); const r = solveFlow(g); expect(r.flows.get('pw')).toBe(0); expect(r.flows.get('e2')).toBeCloseTo(5); expect(r.totalSupplied).toBeCloseTo(5); }); it('handles an empty graph', () => { const r = solveFlow(new PipelineGraph()); expect(r.maxAbsFlow).toBe(0); expect(r.totalDemand).toBe(0); expect(r.flows.size).toBe(0); }); });