Improve physical geometry, fluid solvers, audio, and profiling
This commit is contained in:
162
src/scenario.cpp
162
src/scenario.cpp
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fluidsim::port {
|
||||
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset preset)
|
||||
@@ -14,17 +15,22 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate,const ScenarioExample& de
|
||||
: FourStrokeScenario(sample_rate,definition.engine,definition.exhaust,definition.junctions) {}
|
||||
|
||||
FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configuration, ExhaustConfig exhaust, JunctionConfig fittings)
|
||||
: exhaust_config(exhaust), junction_config(fittings), engine_config(std::move(configuration)),
|
||||
: exhaust_config(exhaust), junction_config(fittings), engine_config(resolve_engine_geometry(std::move(configuration),exhaust,fittings)),
|
||||
intake_plenum(engine_config.plenum_volume,101325,300),
|
||||
exhaust_collector(engine_config.collector_volume,101325,700),
|
||||
pipes(create_pipes(engine_config,exhaust)), dt_(1.f/std::max(sample_rate,1)),
|
||||
boundaries_(pipes), solver_(pipes,boundaries_), audio_(sample_rate,[&] {
|
||||
OutdoorAudioConfig config;
|
||||
config.dual_exhaust=engine_config.dual_outlets();
|
||||
config.valve_contact_enabled=true;
|
||||
return config;
|
||||
}()) {
|
||||
const auto& engine=engine_config;
|
||||
geometry_=std::make_shared<PhysicalGeometry>(engine.geometry);
|
||||
active_tuning_=std::make_shared<EngineConfig>(engine_config);
|
||||
const auto& junctions=junction_config;
|
||||
if(junctions.model!="volume" && junctions.model!="characteristic" && junctions.model!="legacy")
|
||||
throw std::invalid_argument("invalid junction model");
|
||||
const bool v8=engine.american_headers(),staged=engine.staged_headers();
|
||||
const int count=cylinder_count();
|
||||
static_cast<CrankshaftConfig&>(crankshaft)=engine.crankshaft;
|
||||
@@ -48,6 +54,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
for (int i=0;i<count;++i) {
|
||||
auto& c=*engines[i];
|
||||
static_cast<CylinderConfig&>(c)=engine.cylinder;
|
||||
c.set_valve_profiles(active_tuning_->intake_cam,engine.intake_valve,active_tuning_->exhaust_cam,engine.exhaust_valve);
|
||||
c.phase=engine.phase_degrees.at(i)*pi/180;
|
||||
c.transported_fuel=engine.carbureted;
|
||||
c.rev_limit_enabled = ecu.rev_limit_enabled;
|
||||
@@ -75,7 +82,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
auto& throttle_port = intake_plenum.create_port();
|
||||
throttle_port_=&throttle_port;
|
||||
solver_.add_component(intake_plenum);
|
||||
if(!v8 && !staged) solver_.add_component(exhaust_collector);
|
||||
if(!v8 && !staged && junctions.model=="volume") solver_.add_component(exhaust_collector);
|
||||
boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto& feed = intake_plenum.create_port();
|
||||
@@ -85,24 +92,53 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
boundaries_.add_orifice(feed, 1+i, true, runner_area_);
|
||||
boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]);
|
||||
boundaries_.add_orifice(engines[i]->exhaust, 1+count+i, true, exhaust_valve_areas_[i]);
|
||||
if (!v8 && !staged) boundaries_.add_orifice(exhaust_collector.create_port(), 1+count+i, false, header_area_);
|
||||
if (!v8 && !staged && junctions.model=="volume") boundaries_.add_orifice(exhaust_collector.create_port(), 1+count+i, false, header_area_);
|
||||
}
|
||||
if(v8) {
|
||||
auto junction=[&](std::initializer_list<JunctionEnd> ends) {
|
||||
auto fitting=std::make_unique<Junction>(junctions.junction_volume,101325,700);
|
||||
fitting->connect(boundaries_,pipes,std::span(ends.begin(),ends.size()));
|
||||
// Node axes follow the last/first route segment, unless explicitly supplied.
|
||||
ScenarioSnapshot geometry;
|
||||
geometry.geometry=geometry_;
|
||||
geometry.cylinder_count=count;geometry.layout=engine.layout;geometry.bank_angle=engine.bank_angle;
|
||||
geometry.v8_headers=v8;geometry.staged_headers=staged;geometry.dual_exhaust=engine.dual_outlets();
|
||||
geometry.h_pipe=junctions.h_pipe;geometry.silencers=engine.has_silenced_exhaust() && exhaust.enabled;geometry.exhaust_config=exhaust;
|
||||
const auto routes=scenario_pipe_routes(geometry);
|
||||
std::vector<bool> branch_used(junctions.branches.size(),false);
|
||||
int junction_index=0;
|
||||
auto junction=[&](std::span<const JunctionEnd> ends) {
|
||||
if(junctions.model=="volume") {
|
||||
const auto* box=engine.geometry.volume("junction_"+std::to_string(junction_index));
|
||||
if(!box)throw std::invalid_argument("geometry missing junction_"+std::to_string(junction_index));
|
||||
auto fitting=std::make_unique<Junction>(box->volume(),101325,700);
|
||||
fitting->connect(boundaries_,pipes,ends);
|
||||
solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
|
||||
};
|
||||
} else {
|
||||
std::vector<BoundarySystem::JunctionFace> faces;
|
||||
for(const auto& e:ends) {
|
||||
const auto& route=routes.at(e.pipe);
|
||||
if(route.count<2) throw std::invalid_argument("junction branch has no route direction");
|
||||
const auto a=route.points[e.left?0:route.count-1], b=route.points[e.left?1:route.count-2];
|
||||
std::array<float,3> direction{b.x-a.x,b.y-a.y,b.z-a.z};
|
||||
for(std::size_t j=0;j<junctions.branches.size();++j) {
|
||||
const auto& override=junctions.branches[j];
|
||||
if(override.pipe==e.pipe && override.left==e.left) { direction=override.direction;branch_used[j]=true; }
|
||||
}
|
||||
faces.push_back({e.pipe,e.left,direction});
|
||||
}
|
||||
boundaries_.add_junction(faces,junctions.model=="legacy"?BoundarySystem::JunctionMethod::LegacyRusanov:BoundarySystem::JunctionMethod::Characteristic,junctions.turning_loss);
|
||||
}
|
||||
++junction_index;
|
||||
};
|
||||
auto join=[&](std::initializer_list<JunctionEnd> ends) { junction(std::span(ends.begin(),ends.size())); };
|
||||
if(v8) {
|
||||
// Within each bank pair cylinders with the widest available firing gaps.
|
||||
junction({{9,false},{10,false},{17,true}});
|
||||
junction({{11,false},{12,false},{18,true}});
|
||||
junction({{13,false},{15,false},{19,true}});
|
||||
junction({{14,false},{16,false},{20,true}});
|
||||
junction({{17,false},{18,false},{21,true}});
|
||||
junction({{19,false},{20,false},{22,true}});
|
||||
join({{9,false},{10,false},{17,true}});
|
||||
join({{11,false},{12,false},{18,true}});
|
||||
join({{13,false},{15,false},{19,true}});
|
||||
join({{14,false},{16,false},{20,true}});
|
||||
join({{17,false},{18,false},{21,true}});
|
||||
join({{19,false},{20,false},{22,true}});
|
||||
if(junctions.h_pipe) {
|
||||
junction({{21,false},{23,true},{25,true}});
|
||||
junction({{22,false},{24,true},{25,false}});
|
||||
join({{21,false},{23,true},{25,true}});
|
||||
join({{22,false},{24,true},{25,false}});
|
||||
} else {
|
||||
boundaries_.add_connection(21,false,23,true);
|
||||
boundaries_.add_connection(22,false,24,true);
|
||||
@@ -114,11 +150,6 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
const int exit=1+2*count;
|
||||
const bool dual=engine.dual_outlets();
|
||||
const int groups=engine.header_groups(), secondary=exit+(dual?2:1);
|
||||
auto junction=[&](const std::vector<JunctionEnd>& ends) {
|
||||
auto fitting=std::make_unique<Junction>(junctions.junction_volume,101325,700);
|
||||
fitting->connect(boundaries_,pipes,ends);
|
||||
solver_.add_component(*fitting);exhaust_junctions_.push_back(std::move(fitting));
|
||||
};
|
||||
for(int group=0;group<groups;++group) {
|
||||
std::vector<JunctionEnd> ends;
|
||||
for(int i=0;i<count;++i)
|
||||
@@ -128,7 +159,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
if(dual) {
|
||||
// Preserve independent bank pulse trains all the way to their mouths.
|
||||
for(int bank=0;bank<2;++bank) {
|
||||
junction({{secondary+bank,false},{exit+bank,true}});
|
||||
join({{secondary+bank,false},{exit+bank,true}});
|
||||
boundaries_.add_radiating_open_end(exit+bank,false,101325,pipes[exit+bank].area);
|
||||
}
|
||||
} else {
|
||||
@@ -138,23 +169,37 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, EngineConfig configurati
|
||||
boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area);
|
||||
}
|
||||
} else {
|
||||
auto& collector_out = exhaust_collector.create_port();
|
||||
const int exit=1+2*count;
|
||||
boundaries_.add_orifice(collector_out,exit,true,tail_area_);
|
||||
if(junctions.model=="volume") {
|
||||
auto& collector_out = exhaust_collector.create_port();
|
||||
boundaries_.add_orifice(collector_out,exit,true,tail_area_);
|
||||
} else {
|
||||
std::vector<JunctionEnd> ends{{exit,true}};
|
||||
for(int i=0;i<count;++i) ends.push_back({1+count+i,false});
|
||||
junction(ends);
|
||||
}
|
||||
boundaries_.add_radiating_open_end(exit,false,101325,pipes[exit].area);
|
||||
}
|
||||
for(std::size_t i=0;i<branch_used.size();++i)
|
||||
if(junctions.model!="volume" && !branch_used[i]) throw std::invalid_argument("junction direction override does not name an active characteristic/legacy junction branch");
|
||||
boundaries_.add_radiating_open_end(0, true, 101325, pipes[0].area);
|
||||
solver_.set_profiler(&profiler_);
|
||||
audio_.set_source_positions(scenario_sound_sources(snapshot()));
|
||||
}
|
||||
|
||||
AudioFrame FourStrokeScenario::process() {
|
||||
const std::lock_guard guard(mutex_);
|
||||
const bool profile=profiler_.begin_step();
|
||||
ProfileScope total(&profiler_,profile,ProfileStage::Total);
|
||||
float contact_impulse=0;
|
||||
{ ProfileScope controls(&profiler_,profile,ProfileStage::Controls);
|
||||
step_motorcycle(crankshaft, dt_, engine_config.maximum_load_torque);
|
||||
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi));
|
||||
const auto engines = cylinders();
|
||||
for (int i = 0; i < cylinder_count(); ++i) {
|
||||
auto& c = *engines[i];
|
||||
c.pre_step(crankshaft, dt_, throttle);
|
||||
contact_impulse+=c.take_contact_impulse();
|
||||
const float degrees = std::fmod((crankshaft.angle+c.phase)*180/pi, 720.f);
|
||||
intake_valve_areas_[i] = c.valve_area(degrees, true);
|
||||
exhaust_valve_areas_[i] = c.valve_area(degrees, false);
|
||||
@@ -167,7 +212,8 @@ AudioFrame FourStrokeScenario::process() {
|
||||
carb_->step(dt_,throttle,opening,crankshaft.omega*60/(2*pi),std::max(throttle_port_->mass_flow,0.f),inlet.rho,inlet.pressure,intake_plenum.pressure());
|
||||
intake_plenum.add_fuel_mass(carb_->fuel_flow()*dt_);
|
||||
}
|
||||
solver_.step(dt_);
|
||||
}
|
||||
solver_.step(dt_,profile);
|
||||
intake_flow = boundaries_.open_end_mass_flow(engine_config.dual_outlets() ? 2 : 1);
|
||||
exhaust_flow = boundaries_.open_end_mass_flow(0);
|
||||
const float other_exhaust = engine_config.dual_outlets() ? boundaries_.open_end_mass_flow(1) : 0.f;
|
||||
@@ -176,12 +222,57 @@ AudioFrame FourStrokeScenario::process() {
|
||||
const float intake_source = ambient_density * boundaries_.open_end_volume_flow(engine_config.dual_outlets() ? 2 : 1);
|
||||
const float exhaust_source = ambient_density * boundaries_.open_end_volume_flow(0);
|
||||
const float other_source = engine_config.dual_outlets() ? ambient_density * boundaries_.open_end_volume_flow(1) : 0.f;
|
||||
const auto rendered = audio_.process(intake_source, exhaust_source, other_source);
|
||||
const auto& sound=active_tuning_->valve_sound;
|
||||
const float mode_omega=2*pi*sound.mode_hz;
|
||||
valve_sound_velocity_+=contact_impulse/sound.effective_mass_kg;
|
||||
valve_sound_velocity_+=(-mode_omega*mode_omega*valve_sound_displacement_-2*sound.damping_ratio*mode_omega*valve_sound_velocity_)*dt_;
|
||||
valve_sound_displacement_+=valve_sound_velocity_*dt_;
|
||||
const float valve_contact_source=ambient_density*sound.radiation_area*valve_sound_velocity_;
|
||||
ProfileScope audio_profile(&profiler_,profile,ProfileStage::Audio);
|
||||
const auto rendered = audio_.process(intake_source, exhaust_source, other_source,valve_contact_source);
|
||||
// Count sampled frames only after the complete process call has finished.
|
||||
if(profile) profiler_.finish_step(total.elapsed_ns());
|
||||
exhaust_flow += other_exhaust;
|
||||
++sample_count;
|
||||
const auto bin=std::clamp(int(std::fmod((crankshaft.angle+cylinder1.phase)*180/pi,720.f)/4),0,179);
|
||||
intake_lift_trace_[bin]=cylinder1.intake_lift();exhaust_lift_trace_[bin]=cylinder1.exhaust_lift();
|
||||
intake_lift_trace_[180]=intake_lift_trace_[0];exhaust_lift_trace_[180]=exhaust_lift_trace_[0];
|
||||
last_contact_impulse_=contact_impulse;
|
||||
return rendered;
|
||||
}
|
||||
|
||||
void FourStrokeScenario::set_tuning(const EngineConfig& tuning) {
|
||||
validate_engine_config(tuning,"live tuning");
|
||||
// The live editor only changes calibration. Geometry is rebuilt separately.
|
||||
const auto& a=tuning.cylinder;const auto& b=engine_config.cylinder;
|
||||
if(tuning.geometry!=engine_config.geometry || tuning.cylinders!=engine_config.cylinders || tuning.layout!=engine_config.layout ||
|
||||
tuning.topology!=engine_config.topology || tuning.phase_degrees!=engine_config.phase_degrees ||
|
||||
tuning.carbureted!=engine_config.carbureted || tuning.throttle_diameter!=engine_config.throttle_diameter ||
|
||||
a.bore!=b.bore || a.stroke!=b.stroke || a.rod!=b.rod || a.compression!=b.compression ||
|
||||
tuning.cells_per_meter!=engine_config.cells_per_meter || tuning.pipes!=engine_config.pipes || tuning.plenum_volume!=engine_config.plenum_volume ||
|
||||
tuning.collector_volume!=engine_config.collector_volume ||
|
||||
a.intake_valve_count!=b.intake_valve_count || a.exhaust_valve_count!=b.exhaust_valve_count ||
|
||||
a.intake_valve_diameter!=b.intake_valve_diameter || a.exhaust_valve_diameter!=b.exhaust_valve_diameter)
|
||||
throw std::invalid_argument("Live tuning cannot change engine geometry; rebuild the engine instead");
|
||||
auto next=std::make_shared<const EngineConfig>(tuning);
|
||||
auto calibration=tuning.ecu;
|
||||
// The scenario lock spans exactly one process() call. Copy/validation and
|
||||
// retirement of the old profile happen on the editing thread outside it.
|
||||
std::shared_ptr<const EngineConfig> retired;
|
||||
{
|
||||
const std::lock_guard guard(mutex_);
|
||||
retired=std::move(active_tuning_);active_tuning_=std::move(next);
|
||||
std::swap(static_cast<EcuConfig&>(ecu),calibration);
|
||||
if(carb_) {
|
||||
static_cast<CarburetorConfig&>(*carb_)=tuning.fuel;
|
||||
ecu.rev_limit_fuel_cut_enabled=false;ecu.overrun_fuel_cut_enabled=false;ecu.overrun_ignition_cut_enabled=true;
|
||||
}
|
||||
const auto engines=cylinders();
|
||||
for(int i=0;i<cylinder_count();++i)
|
||||
engines[i]->set_valve_profiles(active_tuning_->intake_cam,tuning.intake_valve,active_tuning_->exhaust_cam,tuning.exhaust_valve);
|
||||
}
|
||||
}
|
||||
|
||||
void FourStrokeScenario::set_pipe_solver(PipeSolver solver) {
|
||||
const std::lock_guard guard(mutex_);
|
||||
for(auto& pipe : pipes) pipe.solver=solver;
|
||||
@@ -242,10 +333,25 @@ float FourStrokeScenario::area_for_diameter(float diameter) {
|
||||
std::vector<Pipe1D> FourStrokeScenario::create_pipes(const EngineConfig& engine,const ExhaustConfig& exhaust) {
|
||||
validate_engine_config(engine);
|
||||
std::vector<Pipe1D> pipes;
|
||||
auto mesh_exhaust=exhaust;
|
||||
if(engine.cells_per_meter>0) mesh_exhaust.cell_length=1/engine.cells_per_meter;
|
||||
std::size_t index=0;
|
||||
for(const auto& pipe:engine.pipes)for(int i=0;i<pipe.count;++i) {
|
||||
if(pipe.silenced)pipes.push_back(make_silenced_exhaust(exhaust));
|
||||
else pipes.emplace_back(area_for_diameter(pipe.diameter),pipe.length,pipe.cells);
|
||||
const float length=engine.geometry.pipes.at(index++).length();
|
||||
if(pipe.silenced) {
|
||||
auto layout=mesh_exhaust;
|
||||
const float original=layout.inlet_length+layout.outlet_length+(layout.enabled?layout.connecting_length+layout.resonator.length+layout.muffler.length:0);
|
||||
const float scale=length/original;
|
||||
layout.inlet_length*=scale;layout.outlet_length*=scale;layout.connecting_length*=scale;
|
||||
layout.resonator.length*=scale;layout.resonator.volume*=scale;layout.muffler.length*=scale;layout.muffler.volume*=scale;
|
||||
auto profile=make_silenced_exhaust(layout);
|
||||
if(profile.cells.size()>4096) throw std::invalid_argument("silencer mesh exceeds 4096 cells");
|
||||
pipes.push_back(std::move(profile));
|
||||
}
|
||||
else pipes.emplace_back(area_for_diameter(pipe.diameter),length,engine.cells_per_meter>0?mesh_cell_count(length,engine.cells_per_meter):pipe.cells);
|
||||
}
|
||||
std::size_t total=0;for(const auto& pipe:pipes)total+=pipe.cells.size();
|
||||
if(total>32768) throw std::invalid_argument("network mesh exceeds 32768 cells");
|
||||
return pipes;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user