Improve physical geometry, fluid solvers, audio, and profiling
This commit is contained in:
@@ -1,10 +1,29 @@
|
||||
#include "FluidSim/Desktop/DesktopUi.hpp"
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include "FluidSim/DesktopScenario.hpp"
|
||||
#include "FluidSim/Desktop/PlaybackControl.hpp"
|
||||
#include "FluidSim/Desktop/PipePath.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port::desktop;
|
||||
void check(bool condition,const char* reason) {if(!condition)throw std::runtime_error(reason);}
|
||||
int main() {
|
||||
int main(int argc,char** argv) {
|
||||
PlaybackControl playback;
|
||||
sf::Event space=sf::Event::KeyPressed{.code=sf::Keyboard::Key::Space};
|
||||
sf::Event wheel=sf::Event::MouseWheelScrolled{sf::Mouse::Wheel::Vertical,-1.f,{400,300}};
|
||||
check(playback.realtime()&&!playback.handle_event(wheel,true),"realtime mode intercepted camera scrolling");
|
||||
check(playback.handle_event(space)&&!playback.realtime(),"Space did not disable realtime");
|
||||
check(!playback.handle_event(wheel)&&playback.target_speed()==1,"regular scroll changed manual speed");
|
||||
check(playback.handle_event(wheel,true)&&playback.target_speed()<1,"Shift+scroll did not change manual speed");
|
||||
const auto manual=playback.target_speed();playback.handle_event(space);check(playback.target_speed()==1,"Space did not restore realtime");
|
||||
playback.handle_event(space);check(playback.target_speed()==manual,"manual speed was lost");
|
||||
PipePath corner({{0,{0,0,0}},{1,{1,0,0}},{2,{1,1,0}}});
|
||||
check(corner.position(.5f)==fluidsim::port::Vec3{.5f,0,0}&&corner.position(1.5f)==fluidsim::port::Vec3{1,.5f,0},"sharp pipe bend was rounded into a spline");
|
||||
const auto miter=corner.miter_offset(1,{.70710678f,-.70710678f,0});
|
||||
check(std::abs(miter.x-1)<1e-5f&&std::abs(miter.y+1)<1e-5f,"90 degree pipe joint pinches the cross-section");
|
||||
for(auto size : {sf::Vector2u{933,1010},sf::Vector2u{1920,1080},sf::Vector2u{1040,800},sf::Vector2u{800,700}}) {
|
||||
const auto l=WorkspaceLayout::calculate(size);
|
||||
for(auto r : {l.sidebar,l.viewport,l.chart,l.inspector,l.pedal}) {
|
||||
@@ -46,5 +65,31 @@ int main() {
|
||||
check(std::abs(std::cos(y)*listener.right.x+std::sin(y)*listener.right.z-1)<1e-6f,
|
||||
"audio ears must rotate with the camera right axis");
|
||||
}
|
||||
std::cout<<"Desktop layout, spring pedal, keyboard travel and orbit camera checks passed\n";
|
||||
ui.configure_resolution(20);
|
||||
const auto buttons=DesktopUi::resolution_buttons(size);
|
||||
const auto plus=buttons[1].position+sf::Vector2f{10,8};
|
||||
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,{int(plus.x),int(plus.y)}},size);
|
||||
check(ui.take_resolution_request()==25,"resolution plus button failed");
|
||||
ui.resolution_started();
|
||||
ui.handle_event(sf::Event::MouseButtonPressed{sf::Mouse::Button::Left,{int(plus.x),int(plus.y)}},size);
|
||||
check(!ui.take_resolution_request(),"resolution changed while rebuilding");
|
||||
ui.resolution_finished(25,"Applied");
|
||||
if(argc==3 && std::string(argv[1])=="--preview") {
|
||||
const std::filesystem::path directory=argv[2];std::filesystem::create_directories(directory);
|
||||
sf::ContextSettings context;context.depthBits=24;context.majorVersion=2;context.minorVersion=1;
|
||||
sf::RenderWindow window(sf::VideoMode({1440,900}),"Geometry preview",sf::Style::Default,sf::State::Windowed,context);
|
||||
window.setVisible(false);sf::Font font("C:/Windows/Fonts/consola.ttf");
|
||||
const auto& catalog=fluidsim::port::scenario_examples();
|
||||
sf::RenderTexture contact({1280,static_cast<unsigned>((catalog.size()+3)/4)*200});contact.clear(sf::Color::Black);
|
||||
unsigned index=0;
|
||||
for(const auto& example:catalog) {
|
||||
fluidsim::port::DesktopScenario simulation(example);for(int i=0;i<512;++i)simulation.process();
|
||||
const auto state=simulation.snapshot();DesktopUi preview;preview.configure_resolution(state.cells_per_meter);preview.draw(window,&font,state,{});
|
||||
sf::Texture texture(window.getSize());texture.update(window);
|
||||
check(texture.copyToImage().saveToFile(directory/(example.id+".png")),"could not save geometry preview");
|
||||
sf::Sprite thumbnail(texture);thumbnail.setScale({320.f/window.getSize().x,200.f/window.getSize().y});thumbnail.setPosition({float(index%4)*320,float(index/4)*200});contact.draw(thumbnail);++index;
|
||||
}
|
||||
contact.display();check(contact.getTexture().copyToImage().saveToFile(directory/"catalog.png"),"could not save catalog preview");
|
||||
}
|
||||
std::cout<<"Desktop controls, resolution stepper, and camera checks passed\n";
|
||||
}
|
||||
|
||||
@@ -56,14 +56,29 @@ int main() {
|
||||
}
|
||||
check(rejected,"invalid config accepted: "+expected);
|
||||
};
|
||||
auto changed=original;replace(changed,"schema_version = 1","schema_version = 99");reject(changed,"schema_version");
|
||||
auto changed=original;replace(changed,"schema_version = 2","schema_version = 99");reject(changed,"schema_version");
|
||||
changed=original;replace(changed,"bore_mm = 75.0","bore_mn = 75.0");reject(changed,"bore_mm");
|
||||
changed=original;replace(changed,"bore_mm = 75.0","bore_mm = -75.0");reject(changed,"cylinder");
|
||||
changed=original;replace(changed,"cylinders = 2","cylinders = 2.5");reject(changed,"integer");
|
||||
changed=original;replace(changed,"layout = \"inline\"","layout = \"flatbread\"");reject(changed,"layout");
|
||||
changed=original;replace(changed,"topology = \"collector\"","topology = \"american_v8\"");reject(changed,"V8");
|
||||
changed=original;replace(changed,"cells = 10","cells = 1000000");reject(changed,"cell");
|
||||
changed=original;replace(changed,"cells_per_meter = 50.0","cells_per_meter = 1000000");reject(changed,"cells_per_meter");
|
||||
changed=original;replace(changed,"phase_degrees = [0.0, 180.0]","phase_degrees = [0.0]");reject(changed,"phase_degrees");
|
||||
reject(original+"\n[junctions]\nmodel = \"mystery\"\n","model must be");
|
||||
reject(original+"\n[junctions]\nturning_loss = -1\n","turning_loss");
|
||||
reject(original+"\n[[junctions.branches]]\npipe=3\nleft=false\ndirection=[0,0,0]\n","nonzero");
|
||||
{
|
||||
std::ofstream output(file);
|
||||
output<<original<<"\n[junctions]\nmodel=\"characteristic\"\nturning_loss=0.5\n"
|
||||
<<"[[junctions.branches]]\npipe=3\nleft=false\ndirection=[-1,0,0]\n";
|
||||
}
|
||||
const auto directed=load_engine_file(file);
|
||||
check(directed.junctions.model=="characteristic" && directed.junctions.branches.size()==1,"junction configuration was not loaded");
|
||||
{
|
||||
DesktopScenario scenario(directed);
|
||||
for(int i=0;i<2048;++i) scenario.process();
|
||||
check(scenario.snapshot().pipe_positivity_repairs==0,"configured characteristic engine needs positivity repairs");
|
||||
}
|
||||
reject(original+"\n[unknown_section]\nvalue = 1\n","unknown field");
|
||||
reject("schema_version = [","Error");
|
||||
|
||||
|
||||
@@ -794,6 +794,38 @@ int main(int argc,char** argv) {
|
||||
"thermal relaxation differs from exact exponential");
|
||||
});
|
||||
|
||||
test("pipe solver scratch follows runtime solver changes", [] {
|
||||
Pipe1D pipe(.001f,.2f,16);
|
||||
pipe.left_ghost=pipe.state(0);
|
||||
pipe.right_ghost=pipe.state(15);
|
||||
pipe.solver=PipeSolver::Rusanov;
|
||||
pipe.prepare_step();
|
||||
const auto rusanov_bytes=pipe.scratch_bytes();
|
||||
pipe.solver=PipeSolver::Hlle;
|
||||
pipe.prepare_step();
|
||||
const auto hlle_bytes=pipe.scratch_bytes();
|
||||
pipe.solver=PipeSolver::Muscl;
|
||||
pipe.prepare_step();
|
||||
const auto muscl_bytes=pipe.scratch_bytes();
|
||||
check(rusanov_bytes<hlle_bytes && hlle_bytes<muscl_bytes,
|
||||
"solver-specific work buffers were not allocated on demand");
|
||||
for(auto solver : {PipeSolver::Rusanov,PipeSolver::Hlle,PipeSolver::Hllc,
|
||||
PipeSolver::Muscl,PipeSolver::Rusanov}) {
|
||||
pipe.solver=solver;
|
||||
for(int step=0;step<8;++step) {
|
||||
pipe.left_ghost=pipe.state(0);
|
||||
pipe.right_ghost=pipe.state(15);
|
||||
const float dt=.5f*pipe.prepare_step();
|
||||
pipe.advance(dt);
|
||||
}
|
||||
}
|
||||
for(int i=0;i<16;++i) {
|
||||
const auto state=pipe.state(i);
|
||||
check(std::isfinite(state.rho)&&std::isfinite(state.pressure)&&
|
||||
state.rho>0&&state.pressure>0,"solver switch produced a nonphysical pipe state");
|
||||
}
|
||||
});
|
||||
|
||||
test("sealed crankcase compression conserves mass above five bar", [] {
|
||||
Crankshaft crank;
|
||||
crank.angle = 0;
|
||||
|
||||
79
tests/junction_tests.cpp
Normal file
79
tests/junction_tests.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool condition,const char* message) { if(!condition) throw std::runtime_error(message); }
|
||||
struct Outcome { std::array<double,3> mass{}, pressure{}; std::uint64_t fallbacks{}; };
|
||||
Outcome flow(bool reverse,bool rotate,float loss,bool mixed=false,bool swap_donors=false) {
|
||||
std::vector<Pipe1D> pipes;
|
||||
for(int j=0;j<3;++j) {
|
||||
pipes.emplace_back(.001f*(mixed?j+1:1),.2f,8);
|
||||
for(auto& cell:pipes.back().cells) {
|
||||
const double rho=mixed?(j==0?1.4:1.1):ambient_density;
|
||||
const double p=mixed?(j==0?120000:101325):101325;
|
||||
const double u=(swap_donors?(j==0?0:-50):(j==0?70:0))*(reverse?-1:1);
|
||||
cell={float(rho),float(rho*u),float(p/pipe_gm1+.5*rho*u*u),j==0?.7f:1.f,j==0?.1f:0.f};
|
||||
}
|
||||
}
|
||||
BoundarySystem boundaries(pipes);
|
||||
std::array<std::array<float,3>,3> axes{{{-1,0,0},{1,0,0},{0,1,0}}};
|
||||
if(rotate) for(auto& axis:axes) axis={-axis[1],axis[0],axis[2]};
|
||||
boundaries.add_junction({{0,reverse,axes[0]},{1,!reverse,axes[1]},{2,!reverse,axes[2]}},BoundarySystem::JunctionMethod::Characteristic,loss);
|
||||
boundaries.add_closed_end(0,!reverse);boundaries.add_closed_end(1,reverse);boundaries.add_closed_end(2,reverse);
|
||||
const auto totals=[&] {
|
||||
std::array<double,4> t{};
|
||||
for(auto& p:pipes) for(int i=0;i<int(p.cells.size());++i) {
|
||||
const double m=p.cell_mass(i);
|
||||
t[0]+=m;t[1]+=double(p.cells[i].energy)*p.cell_area(i)*p.cell_length(i);
|
||||
t[2]+=m*p.cells[i].air;t[3]+=m*p.cells[i].fuel;
|
||||
}
|
||||
return t;
|
||||
};
|
||||
const auto initial=totals();Solver solver(pipes,boundaries);solver.minimum_substeps=1;
|
||||
// End before waves reflected by the sealed far ends reach the fitting.
|
||||
for(int i=0;i<30;++i) solver.step(1e-5f);
|
||||
const auto final=totals();
|
||||
for(int j=0;j<4;++j) check(std::abs(final[j]-initial[j])<std::max(1e-10,std::abs(initial[j])*1e-5),"junction lost mass, energy, or species");
|
||||
Outcome result;
|
||||
for(int j=0;j<3;++j) {
|
||||
const int endpoint=(j==0?reverse:!reverse)?0:7;
|
||||
result.pressure[j]=pipes[j].state(endpoint).pressure;
|
||||
for(int i=0;i<8;++i) result.mass[j]+=pipes[j].cell_mass(i);
|
||||
check(pipes[j].positivity_repairs==0,"junction needed positivity repair");
|
||||
}
|
||||
result.fallbacks=boundaries.junction_fallbacks;
|
||||
check(result.fallbacks==0,"subsonic junction unexpectedly fell back");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
try {
|
||||
const auto ideal=flow(false,false,0);
|
||||
check(std::abs(ideal.mass[1]-ideal.mass[2])<1e-10,"ideal equal-area outlets are asymmetric");
|
||||
const auto directed=flow(false,false,1);
|
||||
check(directed.mass[1]>directed.mass[2]+1e-8,"turning loss does not favor straight-through flow");
|
||||
const auto rotated=flow(false,true,1), reversed=flow(true,false,1);
|
||||
for(int j=0;j<3;++j) {
|
||||
check(std::abs(rotated.mass[j]-directed.mass[j])<1e-10,"rotating junction changes flow");
|
||||
check(std::abs(reversed.mass[j]-directed.mass[j])<1e-9,"pipe coordinate reversal changes flow");
|
||||
}
|
||||
flow(false,false,.5f,true);flow(true,true,.5f,true);
|
||||
flow(false,false,.5f,true,true);flow(true,true,.5f,true,true);
|
||||
// Supersonic endpoint data must explicitly select the conservative fallback.
|
||||
std::vector<Pipe1D> pipes;for(int j=0;j<2;++j) pipes.emplace_back(.001f,.1f,4);
|
||||
for(auto& cell:pipes[0].cells) { const float u=500;cell.momentum=cell.rho*u;cell.energy+=.5f*cell.rho*u*u; }
|
||||
BoundarySystem boundaries(pipes);
|
||||
boundaries.add_junction({{0,false},{1,true}});
|
||||
boundaries.add_closed_end(0,true);boundaries.add_closed_end(1,false);
|
||||
Solver solver(pipes,boundaries);solver.minimum_substeps=1;solver.step(1e-6f);
|
||||
check(boundaries.junction_fallbacks==1,"supersonic fallback was not reported");
|
||||
bool rejected=false;
|
||||
try { BoundarySystem invalid(pipes);invalid.add_junction({{0,false},{1,true}},BoundarySystem::JunctionMethod::Characteristic,1); }
|
||||
catch(const std::invalid_argument&) {rejected=true;}
|
||||
check(rejected,"directionless turning-loss node was accepted");
|
||||
std::cout<<"Junction conservation, species, direction, rotation, reversal, and fallback tests passed\n";
|
||||
} catch(const std::exception& e) { std::cerr<<e.what()<<'\n';return 1; }
|
||||
}
|
||||
79
tests/live_tuning_tests.cpp
Normal file
79
tests/live_tuning_tests.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "FluidSim/DesktopScenario.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool value,const char* message) {if(!value)throw std::runtime_error(message);}
|
||||
bool near(float a,float b) {return std::abs(a-b)<1e-5f;}
|
||||
struct TempDirectory {
|
||||
std::filesystem::path path=std::filesystem::temp_directory_path()/
|
||||
("fluidsim-live-tuning-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
TempDirectory(){std::filesystem::create_directory(path);}
|
||||
~TempDirectory(){std::error_code ignored;std::filesystem::remove_all(path,ignored);}
|
||||
};
|
||||
}
|
||||
int main() {
|
||||
const auto& source=scenario_example("single-450");
|
||||
FourStrokeScenario simulation(44100,source);
|
||||
for(int i=0;i<1024;++i)simulation.process();
|
||||
const float rpm=simulation.crankshaft.omega,angle=simulation.crankshaft.angle;
|
||||
const float pressure=simulation.cylinder1.pressure,lift=simulation.cylinder1.intake_lift();
|
||||
const auto samples=simulation.sample_count;
|
||||
auto tuned=source.engine;
|
||||
tuned.intake_valve.mass_kg=.047f;tuned.intake_valve.spring_rate_n_per_m=45000;
|
||||
tuned.intake_valve.damping_n_s_per_m=32;tuned.valve_sound.radiation_area=.0005f;
|
||||
tuned.ecu.idle_base_opening=.055f;tuned.fuel.pilot_jet_diameter*=1.1f;
|
||||
for(auto& p:tuned.intake_cam.points)p.lift_metres*=.8f;
|
||||
for(auto& p:tuned.ecu.ignition_map)p.degrees_btdc+=4;
|
||||
simulation.set_tuning(tuned);
|
||||
check(simulation.crankshaft.omega==rpm && simulation.crankshaft.angle==angle && simulation.sample_count==samples,"live edit restarted the engine");
|
||||
check(simulation.cylinder1.pressure==pressure && simulation.cylinder1.intake_lift()==lift,"live edit reset cylinder or valve state");
|
||||
const auto snapshot=simulation.snapshot();
|
||||
check(near(snapshot.intake_cam_lifts[120],FourStrokeCylinder::cam_lift_at(tuned.intake_cam,480)),"live cam profile was not published");
|
||||
check(near(simulation.ecu.idle_base_opening,.055f),"ECU calibration was not applied");
|
||||
for(int i=0;i<2048;++i) {
|
||||
const auto frame=simulation.process();
|
||||
check(std::isfinite(frame.left) && std::isfinite(frame.right),"live tuning produced nonfinite audio");
|
||||
}
|
||||
auto invalid=tuned;invalid.pipes[0].length*=1.5f;
|
||||
bool rejected=false;try {simulation.set_tuning(invalid);}catch(const std::invalid_argument&){rejected=true;}
|
||||
check(rejected,"mesh edits must require a rebuild");
|
||||
invalid=tuned;invalid.intake_valve.mass_kg=std::numeric_limits<float>::quiet_NaN();
|
||||
rejected=false;try {simulation.set_tuning(invalid);}catch(const std::exception&){rejected=true;}
|
||||
check(rejected,"invalid calibration accepted");
|
||||
|
||||
TempDirectory temp;
|
||||
const auto saved=save_tuned_engine(source,tuned,temp.path);
|
||||
const auto loaded=load_engine_file(saved);
|
||||
check(loaded.id=="single-450-tuned" && loaded.source_path!=source.source_path,"export replaced source identity");
|
||||
check(near(loaded.engine.intake_valve.mass_kg,tuned.intake_valve.mass_kg),"valve mass export changed units");
|
||||
check(near(loaded.engine.fuel.pilot_jet_diameter,tuned.fuel.pilot_jet_diameter),"carburetor edit was not exported");
|
||||
check(near(loaded.engine.ecu.ignition_map[0].degrees_btdc,tuned.ecu.ignition_map[0].degrees_btdc),"ignition trim was not exported");
|
||||
check(near(loaded.engine.intake_cam.points[10].lift_metres,tuned.intake_cam.points[10].lift_metres),"cam profile export changed units");
|
||||
auto at_limit=tuned;at_limit.intake_cam.points[10].lift_metres=at_limit.intake_valve.maximum_lift_m;
|
||||
check(save_tuned_engine(source,at_limit,temp.path)!=saved,"repeat export overwrote an engine");
|
||||
|
||||
DesktopScenario desktop(source);
|
||||
desktop.set_controls(.2f,.1f,.6f);desktop.set_pipe_solver(PipeSolver::Hlle);desktop.set_audio_scene(OutdoorScene::Dry);
|
||||
auto pipes=tuned;auto& route=pipes.geometry.pipes[0];const auto end=route.points.back(),start=route.points.front();
|
||||
route.points.front()={end.x+(start.x-end.x)*1.2f,end.y+(start.y-end.y)*1.2f,end.z+(start.z-end.z)*1.2f};
|
||||
pipes.pipes[0].length=route.length();
|
||||
desktop.rebuild_engine(pipes);
|
||||
const auto rebuilt=desktop.snapshot();
|
||||
const float length=std::accumulate(rebuilt.pipe_lengths[0].begin(),rebuilt.pipe_lengths[0].end(),0.f);
|
||||
check(near(length,pipes.pipes[0].length),"rebuilt mesh uses old dimensions");
|
||||
check(rebuilt.pipe_solver==PipeSolver::Hlle && rebuilt.audio_scene==OutdoorScene::Dry && near(rebuilt.clutch,.6f),"rebuild lost scene or controls");
|
||||
std::atomic<bool> finite{true};
|
||||
std::jthread worker([&]{for(int i=0;i<4096;++i) {auto f=desktop.process();if(!std::isfinite(f.left)||!std::isfinite(f.right))finite=false;}});
|
||||
for(int i=0;i<16;++i) {auto live=pipes;live.intake_valve.mass_kg+=float(i)*.0001f;desktop.set_tuning(live);desktop.snapshot();}
|
||||
desktop.rebuild_engine(pipes);
|
||||
worker.join();check(finite,"concurrent editing or rebuild corrupted the simulation");
|
||||
std::cout<<"Live tuning state, export round trip, mesh rebuild and concurrent publication passed\n";
|
||||
}
|
||||
47
tests/mesh_resolution_tests.cpp
Normal file
47
tests/mesh_resolution_tests.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "FluidSim/DesktopScenario.hpp"
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
using namespace fluidsim::port;
|
||||
void check(bool value,const char* reason) {if(!value)throw std::runtime_error(reason);}
|
||||
double volume(const ScenarioSnapshot& s) {
|
||||
double sum=0;
|
||||
for(int p=0;p<32;++p) for(std::size_t i=0;i<s.pipe_areas[p].size();++i)
|
||||
sum+=double(s.pipe_areas[p][i])*s.pipe_lengths[p][i];
|
||||
return sum;
|
||||
}
|
||||
int main() {
|
||||
try {
|
||||
check(mesh_cell_count(.1f,100)==10,"decimal cell count rounded upward");
|
||||
for(const auto& example:scenario_examples())
|
||||
check((example.kind==ScenarioKind::TwoStroke?example.two.cells_per_meter:example.engine.cells_per_meter)>0,"scenario lacks mesh density");
|
||||
for(const auto* id:{"single-450","yz-racing","saab-chambered"}) {
|
||||
DesktopScenario scenario(scenario_example(id));
|
||||
scenario.set_controls(.3f,.1f,.6f);scenario.set_pipe_solver(PipeSolver::Hllc);scenario.set_audio_scene(OutdoorScene::Dry);
|
||||
const double original_volume=volume(scenario.snapshot());
|
||||
std::atomic<bool> finite{true};
|
||||
std::jthread worker([&]{for(int i=0;i<2048;++i) {const auto f=scenario.process();if(!std::isfinite(f.left)||!std::isfinite(f.right))finite=false;}});
|
||||
scenario.set_cells_per_meter(35);
|
||||
worker.join();check(finite,"concurrent resolution rebuild corrupted audio");
|
||||
auto state=scenario.snapshot();
|
||||
check(state.cells_per_meter==35 && state.pipe_solver==PipeSolver::Hllc && state.audio_scene==OutdoorScene::Dry,"resolution rebuild lost options");
|
||||
check(std::abs(volume(state)-original_volume)<original_volume*1e-5,"remeshing changed pipe volume");
|
||||
for(const auto& lengths:state.pipe_lengths) for(float length:lengths)
|
||||
check(length<=1.f/35+1e-6f,"profile or uniform pipe ignored cells/m");
|
||||
check(std::abs(state.clutch-.6f)<1e-5f,"resolution rebuild lost controls");
|
||||
const auto old_count=state.pipe_states[0].size();
|
||||
for(float invalid:{0.f,-1.f,501.f,std::numeric_limits<float>::quiet_NaN()}) {
|
||||
bool rejected=false;try{scenario.set_cells_per_meter(invalid);}catch(const std::exception&){rejected=true;}
|
||||
check(rejected,"invalid density accepted");
|
||||
check(scenario.snapshot().pipe_states[0].size()==old_count,"failed rebuild changed live mesh");
|
||||
}
|
||||
}
|
||||
DesktopScenario a(scenario_example("single-450")),b(scenario_example("single-650"));
|
||||
const float density=b.snapshot().cells_per_meter;a.set_cells_per_meter(50);
|
||||
check(b.snapshot().cells_per_meter==density,"resolution leaked between scenarios");
|
||||
std::cout<<"Uniform/profile mesh resolution, volume, rebuild, isolation and validation checks passed\n";
|
||||
} catch(const std::exception& e) {std::cerr<<e.what()<<'\n';return 1;}
|
||||
}
|
||||
@@ -181,6 +181,26 @@ int main() {
|
||||
check(std::isfinite(frame.left) && std::isfinite(frame.right),"catalog listener wiring failed");
|
||||
}
|
||||
|
||||
// Source stems reconstruct the mix, and muting leaves diagnostic stems alive.
|
||||
auto stem_config=config();
|
||||
stem_config.dual_exhaust=true; stem_config.valve_contact_enabled=true;
|
||||
OutdoorAudio stems(44100,stem_config), muted(44100,stem_config);
|
||||
for(std::size_t j=0;j<4;++j) muted.set_source_enabled(j,false);
|
||||
for(int i=0;i<12000;++i) {
|
||||
const float x=.01f*std::sin(i*.07f);
|
||||
const auto mix=stems.process(x,x*.4f,-x*.2f,x*.1f);
|
||||
auto sum=stems.tail_frame();
|
||||
for(const auto& source:stems.source_frames()) {
|
||||
sum.left+=source.rendered.left; sum.right+=source.rendered.right;
|
||||
}
|
||||
check(std::abs(sum.left-mix.left)<1e-6f && std::abs(sum.right-mix.right)<1e-6f,
|
||||
"source stems do not reconstruct rendered mix");
|
||||
const auto silent=muted.process(x,x*.4f,-x*.2f,x*.1f);
|
||||
check(silent.left==0 && silent.right==0,"muted sources feed listening mix or reverb");
|
||||
check(muted.source_frames()[0].rendered.left==stems.source_frames()[0].rendered.left,
|
||||
"muting changes diagnostic source history");
|
||||
}
|
||||
|
||||
ReedValve reed;
|
||||
for (int i = 0; i < 10000; i++)
|
||||
reed.step(1.f / 44100, 100);
|
||||
|
||||
67
tests/physical_geometry_tests.cpp
Normal file
67
tests/physical_geometry_tests.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "FluidSim/DesktopScenario.hpp"
|
||||
#include "FluidSim/ScenarioGeometry.hpp"
|
||||
#include <toml.hpp>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
void check(bool value,const char* message){if(!value)throw std::runtime_error(message);}
|
||||
int main() try {
|
||||
for(const auto& e:scenario_examples()) {
|
||||
DesktopScenario scenario(e);const auto state=scenario.snapshot();
|
||||
check(bool(state.geometry),"scenario lacks physical geometry");const auto routes=scenario_pipe_routes(state);
|
||||
for(std::size_t p=0;p<state.geometry->pipes.size();++p) {
|
||||
const float mesh=std::accumulate(state.pipe_lengths[p].begin(),state.pipe_lengths[p].end(),0.f);
|
||||
check(std::abs(mesh-state.geometry->pipes[p].length())<1e-5f*std::max(1.f,mesh),"fluid length differs from render centerline");
|
||||
check(routes[p].count==state.geometry->pipes[p].points.size(),"renderer lost a bend");
|
||||
for(unsigned k=0;k<routes[p].count;++k)check(geometry_distance(routes[p].points[k],state.geometry->pipes[p].points[k])<1e-7f,"render route differs from input geometry");
|
||||
}
|
||||
check(!state.storage_volumes.empty(),"storage volumes missing from rendering snapshot");
|
||||
for(const auto& v:state.storage_volumes) {
|
||||
check(v.box.volume()>0&&std::isfinite(v.pressure),"invalid physical storage volume");
|
||||
const auto lo=v.box.minimum(),hi=v.box.maximum();int ports=0;
|
||||
for(const auto& p:state.geometry->pipes)for(auto endpoint:{p.points.front(),p.points.back()}) {
|
||||
constexpr float tolerance=2e-6f;
|
||||
const bool inside=endpoint.x>=lo.x-tolerance&&endpoint.x<=hi.x+tolerance&&endpoint.y>=lo.y-tolerance&&endpoint.y<=hi.y+tolerance&&endpoint.z>=lo.z-tolerance&&endpoint.z<=hi.z+tolerance;
|
||||
const bool surface=std::min({std::abs(endpoint.x-lo.x),std::abs(endpoint.x-hi.x),std::abs(endpoint.y-lo.y),std::abs(endpoint.y-hi.y),std::abs(endpoint.z-lo.z),std::abs(endpoint.z-hi.z)})<tolerance;
|
||||
if(inside&&surface)++ports;
|
||||
}
|
||||
if(ports<2)throw std::runtime_error(e.id+": storage volume has disconnected pipe ports: "+v.id);
|
||||
}
|
||||
for(int i=0;i<state.cylinder_count;++i) {
|
||||
const auto& c=state.geometry->cylinders[i];const float angle=-c.tilt_degrees*pi/180;
|
||||
const auto attached=[&](Vec3 p) {
|
||||
p={p.x-c.origin.x,p.y-c.origin.y,p.z-c.origin.z};
|
||||
const float y=p.y*std::cos(angle)-p.z*std::sin(angle),z=p.y*std::sin(angle)+p.z*std::cos(angle);
|
||||
return std::abs(p.x)<=state.cylinder_bore*.65f&&std::abs(z)<=state.cylinder_bore*.65f&&y>=state.cylinder_rod-state.cylinder_stroke*.5f-.005f&&y<=state.cylinder_rod+state.cylinder_stroke*.5f+state.cylinder_clearance/(pi*state.cylinder_bore*state.cylinder_bore*.25f)+.005f;
|
||||
};
|
||||
const auto& intake=state.geometry->pipes[state.two_stroke?2:1+i];
|
||||
const auto& exhaust=state.geometry->pipes[state.two_stroke?3:1+state.cylinder_count+i];
|
||||
if(!attached(intake.points.back())||!attached(exhaust.points.front()))throw std::runtime_error(e.id+": cylinder port is disconnected from its geometry");
|
||||
}
|
||||
if(e.kind!=ScenarioKind::TwoStroke){FourStrokeScenario four(44100,e);check(std::abs(four.intake_plenum.volume()-e.engine.geometry.volume("intake_plenum")->volume())<1e-8f,"plenum simulation ignores geometry");}
|
||||
}
|
||||
const auto average_primary=[](const ScenarioExample& e){float sum=0;for(int i=0;i<e.engine.cylinders;++i)sum+=e.engine.geometry.pipes[1+e.engine.cylinders+i].length();return sum/e.engine.cylinders;};
|
||||
check(average_primary(scenario_example("v8-short"))<average_primary(scenario_example("v8-h"))&&average_primary(scenario_example("v8-h"))<average_primary(scenario_example("v8-long")),"short and long header variants lost their physical distinction");
|
||||
auto table=toml::parse_file(scenario_example("single-450").source_path.string());
|
||||
auto& volumes=*table["geometry"]["volumes"].as_array();
|
||||
toml::table* plenum_table=nullptr;
|
||||
for(auto& v:volumes)if((*v.as_table())["id"].value_or<std::string>("")=="intake_plenum")plenum_table=v.as_table();
|
||||
check(plenum_table!=nullptr,"test volume not found");auto& plenum=*plenum_table;
|
||||
plenum.erase("size_mm");plenum.erase("center_mm");
|
||||
plenum.insert_or_assign("corners_mm",toml::array{toml::array{100.,200.,300.},toml::array{0.,0.,0.}});
|
||||
auto& pipe=*table["geometry"]["pipes"].as_array()->get(0)->as_table();
|
||||
pipe.insert_or_assign("points_mm",toml::array{toml::array{0.,0.,0.},toml::array{30.,40.,0.},toml::array{30.,40.,120.}});
|
||||
const auto file=std::filesystem::temp_directory_path()/("fluidsim-geometry-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".toml");
|
||||
const auto write=[&]{{std::ofstream out(file);out<<table;}};
|
||||
write();const auto loaded=load_engine_file(file);FourStrokeScenario changed(44100,loaded);
|
||||
check(std::abs(changed.intake_plenum.volume()-.006f)<1e-8f,"corner-defined volume conversion is wrong");
|
||||
check(geometry_distance(loaded.engine.geometry.volume("intake_plenum")->center,{.05f,.1f,.15f})<1e-7f,"corner center is wrong");
|
||||
check(std::abs(std::accumulate(changed.pipes[0].cell_lengths.begin(),changed.pipes[0].cell_lengths.end(),0.f)-.17f)<1e-6f,"path length does not drive simulation");
|
||||
pipe.insert_or_assign("points_mm",toml::array{toml::array{0.,0.,0.},toml::array{0.,0.,0.}});write();
|
||||
bool rejected=false;try{load_engine_file(file);}catch(const std::exception& e){rejected=std::string(e.what()).find(file.filename().string())!=std::string::npos;}
|
||||
std::filesystem::remove(file);check(rejected,"degenerate geometry accepted or missing file diagnostic");
|
||||
std::cout<<"All scenario centerlines, storage dimensions, corner volumes and invalid geometry passed\n";
|
||||
}catch(const std::exception& e){std::cerr<<e.what()<<'\n';return 1;}
|
||||
@@ -77,6 +77,14 @@ int main() {
|
||||
require(std::abs(direction.x*direction.x+direction.y*direction.y+direction.z*direction.z-1)<1e-4f,"pipe path tangent must be normalized");
|
||||
auto atm=desktop::pressure_tint(101325),vac=desktop::pressure_tint(0),peak=desktop::pressure_tint(1000000);
|
||||
require(atm.y>.8f && vac.z>.99f && peak.x>.99f,"pressure palette must show atmosphere green, vacuum blue, spikes red");
|
||||
const auto vacuum_before=desktop::pressure_tint(40000),vacuum_after=desktop::pressure_tint(40000,true);
|
||||
const auto small_intake_pulse=desktop::pressure_tint(104325),small_exhaust_pulse=desktop::pressure_tint(104325,true);
|
||||
require(vacuum_before.x==vacuum_after.x&&vacuum_before.y==vacuum_after.y&&vacuum_before.z==vacuum_after.z,
|
||||
"exhaust highlighting must not change the established underpressure palette");
|
||||
require(small_intake_pulse.x>.9f&&small_intake_pulse.y>.8f&&small_intake_pulse.z<.1f,
|
||||
"a small positive intake pulse must be visible as yellow");
|
||||
require(small_exhaust_pulse.x>.95f&&small_exhaust_pulse.y<.4f,
|
||||
"a small positive exhaust pulse must be visible as orange-red");
|
||||
Ecu fast_actuator;
|
||||
for(int i=0;i<230;++i)fast_actuator.step(1.f/44100,1,5000);
|
||||
require(fast_actuator.throttle_opening()>.99f,"throttle actuator must settle within about 5 ms");
|
||||
|
||||
Reference in New Issue
Block a user