Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
625
tests/fluid_validation.cpp
Normal file
625
tests/fluid_validation.cpp
Normal file
@@ -0,0 +1,625 @@
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool b, const std::string &s) {
|
||||
if (!b)
|
||||
throw std::runtime_error(s);
|
||||
}
|
||||
void near(double a, double b, double tol, const char *s) {
|
||||
check(std::isfinite(a) && std::abs(a - b) <= tol, s);
|
||||
}
|
||||
void set(Pipe1D &p, int i, float pressure, float rho, float u = 0, float air = 1) {
|
||||
p.cells[i] = {rho, rho * u, pressure / pipe_gm1 + .5f * rho * u * u, air};
|
||||
}
|
||||
void lossless(Pipe1D &p) {
|
||||
p.damping_multiplier = 0;
|
||||
p.energy_relaxation_rate = 0;
|
||||
}
|
||||
double mass(const Pipe1D &p) {
|
||||
double v = 0;
|
||||
for (int i = 0; i < int(p.cells.size()); i++)
|
||||
v += p.cell_mass(i);
|
||||
return v;
|
||||
}
|
||||
double energy(const Pipe1D &p) {
|
||||
double v = 0;
|
||||
for (int i = 0; i < int(p.cells.size()); i++)
|
||||
v += double(p.cells[i].energy) * p.cell_area(i) * p.cell_length(i);
|
||||
return v;
|
||||
}
|
||||
double vol_energy(const Volume0D &v) { return v.pressure() * v.volume() / (v.gamma - 1); }
|
||||
} // namespace
|
||||
int main() {
|
||||
int failed = 0, total = 0;
|
||||
auto test = [&](const char *n, std::function<void()> fn) {
|
||||
++total;
|
||||
try {
|
||||
fn();
|
||||
std::cout << "PASS " << n << '\n';
|
||||
} catch (const std::exception &e) {
|
||||
++failed;
|
||||
std::cout << "FAIL " << n << ": " << e.what() << '\n';
|
||||
}
|
||||
};
|
||||
test("invalid uniform geometry", [] {
|
||||
bool caught = false;
|
||||
try {
|
||||
Pipe1D p(0, .1f, 4);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "zero area accepted");
|
||||
});
|
||||
test("closed pipe conservation", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 50);
|
||||
lossless(p[0]);
|
||||
for (int i = 0; i < 50; i++)
|
||||
set(p[0], i, i < 25 ? 120000 : 100000, 1.2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
auto m = mass(p[0]), e = energy(p[0]);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]), m, m * 2e-5, "closed mass drift");
|
||||
near(energy(p[0]), e, e * 2e-5, "closed energy drift");
|
||||
});
|
||||
test("profiled pipe conservation", [] {
|
||||
std::vector<float> a(40), l(40, .025f);
|
||||
for (int i = 0; i < 40; i++)
|
||||
a[i] = .001f * (1 + .5f * std::sin(i * .1f));
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(a, l);
|
||||
lossless(p[0]);
|
||||
for (int i = 0; i < 40; i++)
|
||||
set(p[0], i, i < 20 ? 120000 : 100000, 1.2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
auto m = mass(p[0]), e = energy(p[0]);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]), m, m * 3e-5, "profile mass drift");
|
||||
near(energy(p[0]), e, e * 3e-5, "profile energy drift");
|
||||
});
|
||||
test("volume pipe sealed conservation", [] {
|
||||
Volume0D v(.001f, 180000, 400);
|
||||
v.energy_relaxation_rate = 0;
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.0003f, .5f, 25);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice(port, 0, true, a);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
s.add_component(v);
|
||||
auto m = mass(p[0]) + v.mass(), e = energy(p[0]) + vol_energy(v);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + v.mass(), m, m * 3e-5, "coupling creates/deletes mass");
|
||||
near(energy(p[0]) + vol_energy(v), e, e * 3e-5, "coupling creates/deletes energy");
|
||||
});
|
||||
test("equal pressure hot contact", [] {
|
||||
Volume0D v(.001f, 101325, 600);
|
||||
v.energy_relaxation_rate = 0;
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .2f, 20);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
float a = .001f;
|
||||
b.add_orifice(port, 0, true, a);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
s.add_component(v);
|
||||
double m = mass(p[0]) + v.mass();
|
||||
for (int i = 0; i < 500; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + v.mass(), m, m * 2e-5, "contact produces mass");
|
||||
near(v.pressure(), 101325, 5, "stationary contact pressure changes");
|
||||
});
|
||||
test("NASA choked nozzle mass flow", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 200000;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .2f, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
set(p[0], i, 10000, 10000 / (gas_constant * 300));
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice(v.port, 0, true, a, .8f);
|
||||
b.resolve(1e-8f);
|
||||
double expected = .8 * a * 200000 / std::sqrt(300.) *
|
||||
std::sqrt(fluidsim::port::gamma / gas_constant) *
|
||||
std::pow(2 / (fluidsim::port::gamma + 1),
|
||||
(fluidsim::port::gamma + 1) / (2 * (fluidsim::port::gamma - 1)));
|
||||
near(-v.port.mass_flow, expected, expected * 2e-5,
|
||||
"choked mdot differs from isentropic sonic-throat formula");
|
||||
});
|
||||
test("inertance units", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 102325;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float a = .0001f;
|
||||
b.add_orifice_with_inertance(v.port, 0, true, a, 1, .05f);
|
||||
b.resolve(1e-6f);
|
||||
double expected = a / .05 * 1000 * 1e-6;
|
||||
near(-v.port.mass_flow, expected, expected * 1e-4,
|
||||
"mass-flow inertance must satisfy d(mdot)/dt=A*dp/L");
|
||||
});
|
||||
test("moving volume adiabatic law", [] {
|
||||
Volume0D v(.001f);
|
||||
v.energy_relaxation_rate = 0;
|
||||
v.volume_change_rate = .0001f;
|
||||
for (int i = 0; i < 1000; i++)
|
||||
v.update(1e-3f);
|
||||
near(v.volume(), .0011, 1e-8, "volume work did not change geometric volume");
|
||||
near(v.pressure(), 101325 * std::pow(.001 / .0011, 1.4), 3, "p V^gamma not conserved");
|
||||
});
|
||||
test("profile at rest is well balanced", [] {
|
||||
std::vector<float> a(80), l(80, .01f);
|
||||
for (int i = 0; i < 80; i++)
|
||||
a[i] = .001f * (1 + .7f * std::sin(.1f * i));
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(a, l);
|
||||
lossless(p[0]);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver s(p, b);
|
||||
for (int j = 0; j < 1000; j++)
|
||||
s.step(1.f / 44100);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
near(p[0].state(i).velocity, 0, 1e-3, "stationary nozzle accelerates");
|
||||
near(p[0].state(i).pressure, 101325, 2, "stationary nozzle pressure drifts");
|
||||
}
|
||||
});
|
||||
test("pipe connection all four orientations", [] {
|
||||
for (bool la : {false, true})
|
||||
for (bool lb : {false, true}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .3f, 20);
|
||||
p.emplace_back(.001f, .3f, 20);
|
||||
for (auto &q : p)
|
||||
lossless(q);
|
||||
for (int i = 0; i < 20; i++)
|
||||
set(p[0], i, 130000, 1.4f, 0, .2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_connection(0, la, 1, lb);
|
||||
b.add_closed_end(0, !la);
|
||||
b.add_closed_end(1, !lb);
|
||||
Solver s(p, b);
|
||||
double m = mass(p[0]) + mass(p[1]), e = energy(p[0]) + energy(p[1]);
|
||||
for (int i = 0; i < 2000; i++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + mass(p[1]), m, m * 4e-5, "junction mass drift");
|
||||
near(energy(p[0]) + energy(p[1]), e, e * 4e-5, "junction energy drift");
|
||||
for (auto &q : p)
|
||||
check(q.positivity_repairs == 0, "junction positivity repair");
|
||||
}
|
||||
});
|
||||
test("split pipe matches unsplit pipe", [] {
|
||||
std::vector<Pipe1D> a, b;
|
||||
a.emplace_back(.001f, 1, 80);
|
||||
b.emplace_back(.001f, .5f, 40);
|
||||
b.emplace_back(.001f, .5f, 40);
|
||||
lossless(a[0]);
|
||||
lossless(b[0]);
|
||||
lossless(b[1]);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
float pr = 101325 + 100 * std::exp(-std::pow((i - 30) / 5.f, 2));
|
||||
set(a[0], i, pr, 1.2f);
|
||||
set(b[i / 40], i % 40, pr, 1.2f);
|
||||
}
|
||||
BoundarySystem ba(a), bb(b);
|
||||
ba.add_closed_end(0, true);
|
||||
ba.add_closed_end(0, false);
|
||||
bb.add_closed_end(0, true);
|
||||
bb.add_closed_end(1, false);
|
||||
bb.add_connection(0, false, 1, true);
|
||||
Solver sa(a, ba), sb(b, bb);
|
||||
for (int j = 0; j < 300; j++) {
|
||||
sa.step(1.f / 44100);
|
||||
sb.step(1.f / 44100);
|
||||
}
|
||||
for (int i = 0; i < 80; i++)
|
||||
near(a[0].state(i).pressure, b[i / 40].state(i % 40).pressure, .1,
|
||||
"split junction changes solution");
|
||||
});
|
||||
test("three volumes shared donor conservation", [] {
|
||||
Volume0D a(.000001f, 1e6f, 500), b(.001f), c(.001f);
|
||||
a.energy_relaxation_rate = b.energy_relaxation_rate = c.energy_relaxation_rate = 0;
|
||||
auto &ap = a.create_port();
|
||||
auto &ap2 = a.create_port();
|
||||
auto &bp = b.create_port();
|
||||
auto &cp = c.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
for (auto &q : p)
|
||||
lossless(q);
|
||||
BoundarySystem bs(p);
|
||||
float ar = .001f;
|
||||
bs.add_orifice(ap, 0, true, ar);
|
||||
bs.add_orifice(ap2, 1, true, ar);
|
||||
bs.add_orifice(bp, 0, false, ar);
|
||||
bs.add_orifice(cp, 1, false, ar);
|
||||
Solver s(p, bs);
|
||||
s.add_component(a);
|
||||
s.add_component(b);
|
||||
s.add_component(c);
|
||||
double m = mass(p[0]) + mass(p[1]) + a.mass() + b.mass() + c.mass();
|
||||
double e = energy(p[0]) + energy(p[1]) + vol_energy(a) + vol_energy(b) + vol_energy(c);
|
||||
for (int j = 0; j < 3000; j++)
|
||||
s.step(1.f / 44100);
|
||||
near(mass(p[0]) + mass(p[1]) + a.mass() + b.mass() + c.mass(), m, m * 5e-5,
|
||||
"shared donor mass drift");
|
||||
near(energy(p[0]) + energy(p[1]) + vol_energy(a) + vol_energy(b) + vol_energy(c), e, e * 5e-5,
|
||||
"shared donor energy drift");
|
||||
check(a.pressure() > 0, "donor vacuum");
|
||||
});
|
||||
test("open boundary reports actual conserved flux both ends", [] {
|
||||
for (bool left : {false, true}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
lossless(p[0]);
|
||||
for (int i = 0; i < 10; i++)
|
||||
set(p[0], i, 105000, 1.2f);
|
||||
BoundarySystem b(p);
|
||||
b.add_open_end(0, left, 101325, .001f);
|
||||
b.add_closed_end(0, !left);
|
||||
double m = mass(p[0]);
|
||||
b.resolve(1e-6f);
|
||||
double dm = -b.open_end_mass_flow(0) * 1e-6;
|
||||
p[0].advance(1e-6f);
|
||||
near(mass(p[0]) - m, dm, 1e-11,
|
||||
"reported radiation flow differs from finite-volume mass exchange");
|
||||
}
|
||||
});
|
||||
test("duplicate and invalid boundary rejection", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_open_end(0, true, 101325, .001f);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "duplicate face accepted");
|
||||
caught = false;
|
||||
try {
|
||||
b.add_closed_end(9, true);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "invalid pipe accepted");
|
||||
});
|
||||
test("supersonic outlet extrapolates", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
set(p[0], i, 101325, 1.2f, 700);
|
||||
BoundarySystem b(p);
|
||||
b.add_open_end(0, false, 50000, .001f);
|
||||
b.resolve(1e-6f);
|
||||
near(b.open_end_mass_flow(0), .001 * 1.2 * 700, 1e-5,
|
||||
"supersonic outlet responds to downstream pressure");
|
||||
});
|
||||
test("underdefined supersonic inlet rejected", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 10);
|
||||
set(p[0], 0, 101325, 1.2f, 700);
|
||||
BoundarySystem b(p);
|
||||
b.add_open_end(0, true, 101325, .001f);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.resolve(1e-6f);
|
||||
} catch (const std::runtime_error &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "supersonic inlet silently accepted pressure-only data");
|
||||
});
|
||||
test("closed and open acoustic reflection signs", [] {
|
||||
for (bool closed : {false, true}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, 400);
|
||||
lossless(p[0]);
|
||||
const double c = std::sqrt(1.4 * 101325 / 1.2);
|
||||
for (int i = 0; i < 400; i++) {
|
||||
double x = (i + .5) / 400.;
|
||||
float dp = 100 * std::exp(-std::pow((x - .3) / .04, 2));
|
||||
set(p[0], i, 101325 + dp, 1.2f + dp / (c * c), dp / (1.2 * c));
|
||||
}
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
if (closed)
|
||||
b.add_closed_end(0, false);
|
||||
else
|
||||
b.add_open_end(0, false, 101325, .001f);
|
||||
Solver s(p, b);
|
||||
s.minimum_substeps = 1;
|
||||
const int steps = int((1.4 / c) * 44100);
|
||||
for (int j = 0; j < steps; j++)
|
||||
s.step(1.f / 44100);
|
||||
float dp = p[0].state(119).pressure - 101325;
|
||||
check(closed ? dp > 30 : dp < -30, "wrong reflected pressure sign/amplitude");
|
||||
check(p[0].positivity_repairs == 0, "acoustic positivity repair");
|
||||
}
|
||||
});
|
||||
test("Sod shock tube convergence", [] {
|
||||
double previous = 1e9;
|
||||
for (int n : {100, 200, 400}) {
|
||||
Pipe1D p(1, 1, n);
|
||||
lossless(p);
|
||||
for (int i = 0; i < n; i++)
|
||||
set(p, i, i < n / 2 ? 1 : .1f, i < n / 2 ? 1 : .125f);
|
||||
double t = 0;
|
||||
while (t < .2) {
|
||||
p.left_ghost = p.state(0);
|
||||
p.right_ghost = p.state(n - 1);
|
||||
float dt = std::min(.2 - t, double(p.stable_timestep(.7f)));
|
||||
p.advance(dt);
|
||||
t += dt;
|
||||
}
|
||||
double error = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
double xi = ((i + .5) / n - .5) / .2;
|
||||
double rho;
|
||||
if (xi < -std::sqrt(1.4))
|
||||
rho = 1;
|
||||
else if (xi < -.0702728)
|
||||
rho = std::pow((2 / (1.4 + 1)) + (1.4 - 1) / (1.4 + 1) * (-xi / std::sqrt(1.4)),
|
||||
2 / (1.4 - 1));
|
||||
else if (xi < .9274526)
|
||||
rho = .4263194;
|
||||
else if (xi < 1.7521557)
|
||||
rho = .2655737;
|
||||
else
|
||||
rho = .125;
|
||||
error += std::abs(p.state(i).rho - rho) / n;
|
||||
}
|
||||
std::cout << " Sod n=" << n << " density L1=" << error << '\n';
|
||||
check(error < previous * .9, "shock density error does not decrease on refinement");
|
||||
previous = error;
|
||||
check(p.positivity_repairs == 0, "Sod positivity repair");
|
||||
}
|
||||
check(previous < .025, "Sod density error excessive");
|
||||
});
|
||||
test("strong pulse and near-vacuum rarefaction", [] {
|
||||
for (bool vacuum : {false, true}) {
|
||||
Pipe1D p(1, 1, 200);
|
||||
lossless(p);
|
||||
for (int i = 0; i < 200; i++)
|
||||
set(p, i, vacuum ? .4f : (i < 100 ? 1000 : .01f), 1, vacuum ? (i < 100 ? -2 : 2) : 0);
|
||||
double t = 0;
|
||||
while (t < .005) {
|
||||
p.left_ghost = p.state(0);
|
||||
p.right_ghost = p.state(199);
|
||||
float dt = std::min(.005 - t, double(p.stable_timestep(.5f)));
|
||||
p.advance(dt);
|
||||
t += dt;
|
||||
}
|
||||
for (int i = 0; i < 200; i++)
|
||||
check(p.state(i).rho > 0 && p.state(i).pressure > 0, "negative shock/vacuum state");
|
||||
check(p.positivity_repairs == 0, "stress case required conservation-breaking positivity repair");
|
||||
}
|
||||
});
|
||||
|
||||
test("small-signal Helmholtz resonance", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
float area = .000314159f;
|
||||
p.emplace_back(area, .05f, 40);
|
||||
Volume0D v(.001f, 101335, 300);
|
||||
auto &port = v.create_port();
|
||||
BoundarySystem b(p);
|
||||
b.add_orifice(port, 0, true, area, 1);
|
||||
b.add_open_end(0, false, 101325, area);
|
||||
Solver solver(p, b);
|
||||
solver.minimum_substeps = 1;
|
||||
solver.add_component(v);
|
||||
double prev = v.pressure() - 101325, first = 0, last = 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < 4410; i++) {
|
||||
solver.step(1.f / 44100);
|
||||
double now = v.pressure() - 101325;
|
||||
check(std::abs(now) < 15, "small-signal cavity grows instead of oscillating/decaying");
|
||||
if (prev > 0 && now <= 0 && i > 441) {
|
||||
double t = (i + prev / (prev - now)) / 44100;
|
||||
if (!count)
|
||||
first = t;
|
||||
last = t;
|
||||
count++;
|
||||
}
|
||||
prev = now;
|
||||
}
|
||||
double frequency = (count - 1) / (last - first);
|
||||
double expected =
|
||||
std::sqrt(1.4 * 287 * 300) / (2 * 3.141592653589793) * std::sqrt(area / (.001 * .05));
|
||||
std::cout << " Helmholtz frequency=" << frequency << " analytic lumped=" << expected << '\n';
|
||||
check(count > 5, "not enough cavity oscillations");
|
||||
near(frequency, expected, expected * .05, "Helmholtz mode frequency error exceeds 5%");
|
||||
check(p[0].positivity_repairs == 0, "cavity requires positivity repairs");
|
||||
});
|
||||
test("invalid volume and timestep rejection", [] {
|
||||
bool caught = false;
|
||||
try {
|
||||
Volume0D v(-1);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "negative volume accepted");
|
||||
Pipe1D p(.001f, .1f, 10);
|
||||
caught = false;
|
||||
try {
|
||||
p.required_substeps(std::nanf(""));
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "NaN timestep accepted");
|
||||
});
|
||||
test("fresh-air species conservation across connection", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 40);
|
||||
p.emplace_back(.001f, .5f, 40);
|
||||
for (int k = 0; k < 2; k++) {
|
||||
lossless(p[k]);
|
||||
for (int i = 0; i < 40; i++)
|
||||
set(p[k], i, k ? 100000 : 120000, 1.2f, 0, k ? .9f : .1f);
|
||||
}
|
||||
auto air_mass = [&] {
|
||||
double sum = 0;
|
||||
for (auto &q : p)
|
||||
for (int i = 0; i < 40; i++)
|
||||
sum += q.cell_mass(i) * q.cells[i].air;
|
||||
return sum;
|
||||
};
|
||||
double before = air_mass();
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(1, false);
|
||||
b.add_connection(0, false, 1, true);
|
||||
Solver solver(p, b);
|
||||
for (int i = 0; i < 3000; i++)
|
||||
solver.step(1.f / 44100);
|
||||
near(air_mass(), before, before * 4e-5, "species created at shared face");
|
||||
});
|
||||
test("unsupported unequal junction rejected", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 20);
|
||||
p.emplace_back(.002f, .5f, 20);
|
||||
BoundarySystem b(p);
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_connection(0, false, 1, true);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "unvalidated sudden area junction silently accepted");
|
||||
});
|
||||
test("unconnected endpoint rejected by solver", [] {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .5f, 20);
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
Solver solver(p, b);
|
||||
bool caught = false;
|
||||
try {
|
||||
solver.step(1.f / 44100);
|
||||
} catch (const std::runtime_error &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "unconnected face silently used default ghost");
|
||||
});
|
||||
test("closed duct acoustic eigenfrequency", [] {
|
||||
for (int n : {50, 100, 200}) {
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, 1, n);
|
||||
double c = std::sqrt(1.4 * 101325 / 1.2);
|
||||
for (int i = 0; i < n; i++) {
|
||||
double dp = 100 * std::cos(3.141592653589793 * (i + .5) / n);
|
||||
set(p[0], i, 101325 + dp, 1.2f + dp / (c * c));
|
||||
}
|
||||
BoundarySystem b(p);
|
||||
b.add_closed_end(0, true);
|
||||
b.add_closed_end(0, false);
|
||||
Solver solver(p, b);
|
||||
solver.minimum_substeps = 1;
|
||||
double prev = 1, first = 0, last = 0;
|
||||
int count = 0;
|
||||
for (int j = 0; j < 1764; j++) {
|
||||
solver.step(1.f / 44100);
|
||||
double projection = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
projection +=
|
||||
(p[0].state(i).pressure - 101325) * std::cos(3.141592653589793 * (i + .5) / n);
|
||||
if (prev > 0 && projection <= 0) {
|
||||
double t = (j + prev / (prev - projection)) / 44100;
|
||||
if (!count)
|
||||
first = t;
|
||||
last = t;
|
||||
count++;
|
||||
}
|
||||
prev = projection;
|
||||
}
|
||||
double f = (count - 1) / (last - first);
|
||||
std::cout << " closed mode n=" << n << " f=" << f << " analytic=" << c / 2 << '\n';
|
||||
check(count > 4, "insufficient acoustic cycles");
|
||||
near(f, c / 2, c / 2 * .01, "closed duct frequency exceeds 1% error");
|
||||
}
|
||||
});
|
||||
test("inertance impulse and dissipative resistance", [] {
|
||||
Atmosphere v;
|
||||
v.pressure = 102325;
|
||||
v.update_state(0);
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float area = .0001f;
|
||||
b.add_orifice_with_inertance(v.port, 0, true, area, 1, .05f, 1);
|
||||
for (int j = 0; j < 10000; j++)
|
||||
b.resolve(.0001f);
|
||||
double equilibrium = std::sqrt(2 * v.density() * area * area * 1000);
|
||||
near(-v.port.mass_flow, equilibrium, equilibrium * .001,
|
||||
"inertance resistance steady flow has wrong units or sign");
|
||||
});
|
||||
test("incompatible gas coupling rejected", [] {
|
||||
Volume0D v(.001f, 101325, 300, 300, 1.3f);
|
||||
auto &port = v.create_port();
|
||||
std::vector<Pipe1D> p;
|
||||
p.emplace_back(.001f, .1f, 10);
|
||||
BoundarySystem b(p);
|
||||
float area = .0001f;
|
||||
bool caught = false;
|
||||
try {
|
||||
b.add_orifice(port, 0, true, area);
|
||||
} catch (const std::invalid_argument &) {
|
||||
caught = true;
|
||||
}
|
||||
check(caught, "different gamma gases silently coupled");
|
||||
});
|
||||
test("thermal relaxation matches analytic energy decay", [] {
|
||||
Volume0D v(.001f, 101325, 600);
|
||||
v.energy_relaxation_rate = 2;
|
||||
double initial = vol_energy(v), target = v.mass() * 287 * 300 / (1.4 - 1);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
v.update(.001f);
|
||||
near(vol_energy(v), target + (initial - target) * std::exp(-2), initial * 3e-5,
|
||||
"thermal relaxation differs from exact exponential");
|
||||
});
|
||||
|
||||
test("sealed crankcase compression conserves mass above five bar", [] {
|
||||
Crankshaft crank;
|
||||
crank.angle = 0;
|
||||
Crankcase v(crank, .02725f, .11f, .054f, 1e-5f);
|
||||
double m = v.mass(), invariant = v.pressure() * std::pow(v.volume(), 1.4);
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
crank.angle = pi * i / 1000;
|
||||
v.pre_step(1e-5f);
|
||||
v.update_state(1e-5f);
|
||||
}
|
||||
near(v.mass(), m, m * 1e-6, "crankcase pressure cap deletes mass");
|
||||
near(v.pressure() * std::pow(v.volume(), 1.4), invariant, invariant * 1e-4,
|
||||
"crankcase compression violates adiabatic invariant");
|
||||
check(v.pressure() > 5e5, "compression did not exercise old hard pressure cap");
|
||||
});
|
||||
std::cout << total - failed << "/" << total << " passed\n";
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user