diagrams-fable-qt/tests/tst_solver.cpp
Ilya Ashikhmin c2501ff04e feat(core): add mock flow solver with conservation-based distribution
Treats each relation network as a conductance network (diameter^4 / length),
solves nodal balance per connected component, so flow is conserved at every
junction and wider pipes carry proportionally more flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:14:11 +02:00

124 lines
5.0 KiB
C++

#include "TestFixtures.h"
#include "core/FlowSolver.h"
#include "core/NetworkModel.h"
#include <QSignalSpy>
#include <QTest>
using namespace diag;
class TestSolver : public QObject {
Q_OBJECT
private slots:
void init()
{
fixtures::fillRegistry(m_reg);
delete m_model;
m_model = new NetworkModel(&m_reg, this);
}
void chain_carriesSupplyThrough()
{
// source(10) -> pump -> consumer(10)
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {200, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {400, 0});
const QUuid e1 = m_model->addEdge(src, "out", pump, "in");
const QUuid e2 = m_model->addEdge(pump, "out", c, "in");
const FlowResult r = FlowSolver::solve(*m_model, QStringLiteral("water"));
QVERIFY(r.ok);
QVERIFY(qAbs(r.edgeFlow.value(e1) - 10.0) < 1e-6);
QVERIFY(qAbs(r.edgeFlow.value(e2) - 10.0) < 1e-6);
}
void junction_conservesFlow()
{
// source(10) -> junction -> two consumers (arbitrary demands).
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid j = m_model->addNode(QStringLiteral("junction"), {200, 0});
const QUuid c1 = m_model->addNode(QStringLiteral("consumer"), {400, -100});
const QUuid c2 = m_model->addNode(QStringLiteral("consumer"), {400, 100});
m_model->setNodeProperty(c1, "demand", 4.0);
m_model->setNodeProperty(c2, "demand", 6.0);
const QUuid eIn = m_model->addEdge(src, "out", j, "w");
const QUuid eOut1 = m_model->addEdge(j, "n", c1, "in");
const QUuid eOut2 = m_model->addEdge(j, "s", c2, "in");
const FlowResult r = FlowSolver::solve(*m_model, QStringLiteral("water"));
QVERIFY(r.ok);
const double in = r.edgeFlow.value(eIn);
const double out = r.edgeFlow.value(eOut1) + r.edgeFlow.value(eOut2);
QVERIFY(qAbs(in - out) < 1e-6); // Kirchhoff at the junction
QVERIFY(qAbs(in - 10.0) < 1e-6); // all supply enters
QVERIFY(qAbs(r.edgeFlow.value(eOut1) - 4.0) < 1e-6);
QVERIFY(qAbs(r.edgeFlow.value(eOut2) - 6.0) < 1e-6);
}
void parallelPipes_widerCarriesMore()
{
// Two parallel pipes between two junctions; DN150 vs DN75.
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid j1 = m_model->addNode(QStringLiteral("junction"), {200, 0});
const QUuid j2 = m_model->addNode(QStringLiteral("junction"), {400, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {600, 0});
m_model->addEdge(src, "out", j1, "w");
const QUuid wide = m_model->addEdge(j1, "n", j2, "n");
const QUuid narrow = m_model->addEdge(j1, "s", j2, "s");
m_model->addEdge(j2, "e", c, "in");
m_model->setEdgeProperty(wide, "diameter", 150.0);
m_model->setEdgeProperty(narrow, "diameter", 75.0);
const FlowResult r = FlowSolver::solve(*m_model, QStringLiteral("water"));
QVERIFY(r.ok);
const double fWide = r.edgeFlow.value(wide);
const double fNarrow = r.edgeFlow.value(narrow);
QVERIFY(fWide > 0 && fNarrow > 0);
QVERIFY(qAbs(fWide + fNarrow - 10.0) < 1e-6);
// Conductance ~ d^4: ratio (150/75)^4 = 16.
QVERIFY(qAbs(fWide / fNarrow - 16.0) < 1e-6);
}
void unbalancedInjections_areNormalized()
{
// Supply 10 vs demand 4: imbalance is spread, but flow stays conserved.
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {200, 0});
m_model->setNodeProperty(c, "demand", 4.0);
const QUuid e = m_model->addEdge(src, "out", c, "in");
const FlowResult r = FlowSolver::solve(*m_model, QStringLiteral("water"));
QVERIFY(r.ok);
QVERIFY(qAbs(r.edgeFlow.value(e) - 7.0) < 1e-6); // (10 + 4) / 2
}
void otherRelation_untouched()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {200, 0});
m_model->addEdge(src, "out", c, "in");
const FlowResult r = FlowSolver::solve(*m_model, QStringLiteral("power"));
QVERIFY(r.ok);
QVERIFY(r.edgeFlow.isEmpty());
}
void apply_writesFlowsAndSignals()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {200, 0});
const QUuid e = m_model->addEdge(src, "out", c, "in");
QSignalSpy spy(m_model, &NetworkModel::flowChanged);
FlowSolver::apply(*m_model, FlowSolver::solve(*m_model, QStringLiteral("water")));
QCOMPARE(spy.count(), 1);
QVERIFY(m_model->edge(e)->flowRate > 0);
}
private:
TypeRegistry m_reg;
NetworkModel* m_model = nullptr;
};
QTEST_GUILESS_MAIN(TestSolver)
#include "tst_solver.moc"