all of the changes
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct AudioFrame { float left{}, right{}; };
|
||||
struct Vec3 { float x{}, y{}, z{}; };
|
||||
|
||||
// Flow-derivative sound source used by the C# two-stroke and pulse scenarios.
|
||||
// Simple flow-derivative source used by the Helmholtz and pulse scenarios.
|
||||
class SoundProcessor {
|
||||
public:
|
||||
float gain{0.005f};
|
||||
@@ -17,34 +15,4 @@ private:
|
||||
float dt_{}, scale_factor_{}, previous_flow_{};
|
||||
};
|
||||
|
||||
class RadiationSource {
|
||||
public:
|
||||
// Optional diagnostic filters; disabled for the linear radiation path.
|
||||
float flow_tau{0.0f}, smooth_tau{0.0f}, gain{0.005f};
|
||||
Vec3 pipe_end{}, pipe_axis{1.0f, 0.0f, 0.0f};
|
||||
Vec3 listener{0.0f, 0.0f, 1.5f}, listener_right{1.0f, 0.0f, 0.0f};
|
||||
float ear_spacing{0.18f}, head_shadow_cutoff{0.0f}, speed_of_sound{343.0f};
|
||||
float directivity_balance{0.0f}, air_absorption_exponent{0.0f}, min_air_cutoff{1500.0f};
|
||||
|
||||
explicit RadiationSource(int sample_rate = 44100);
|
||||
AudioFrame process(float mass_flow);
|
||||
float last_mono_pressure() const { return last_mono_pressure_; }
|
||||
|
||||
private:
|
||||
class DelayLine;
|
||||
class OnePole;
|
||||
int sample_rate_{};
|
||||
float dt_{}, flow_lp_{}, previous_flow_lp_{}, smooth_derivative_{};
|
||||
float last_mono_pressure_{};
|
||||
std::vector<float> left_delay_, right_delay_;
|
||||
std::size_t write_position_{}, left_offset_{}, right_offset_{};
|
||||
float air_filter_{}, shadow_left_{}, shadow_right_{};
|
||||
|
||||
static float dot(Vec3 a, Vec3 b);
|
||||
static float length(Vec3 value);
|
||||
static Vec3 subtract(Vec3 a, Vec3 b);
|
||||
float one_pole(float input, float cutoff, float& state) const;
|
||||
float delayed(float input, std::vector<float>& buffer, std::size_t offset);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
|
||||
37
include/FluidSim/Desktop/DesktopUi.hpp
Normal file
37
include/FluidSim/Desktop/DesktopUi.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/DynoCurve.hpp"
|
||||
#include "FluidSim/Desktop/PedalControl.hpp"
|
||||
#include "FluidSim/Desktop/EngineRenderer3D.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
namespace fluidsim::port::desktop {
|
||||
struct PlaybackInfo {
|
||||
float pedal_target{},load{};
|
||||
double speed{1};
|
||||
std::size_t buffered{},capacity{};
|
||||
std::uint64_t underruns{};
|
||||
bool auto_gain{},time_warp{};
|
||||
};
|
||||
struct WorkspaceLayout {
|
||||
sf::FloatRect sidebar,viewport,chart,inspector,pedal;
|
||||
static WorkspaceLayout calculate(sf::Vector2u size);
|
||||
};
|
||||
class DesktopUi {
|
||||
public:
|
||||
float pedal() const {return pedal_.value();}
|
||||
void nudge_pedal(float amount) {pedal_.set(pedal_.value()+amount);}
|
||||
void update_pedal(float dt,bool raise,bool lower) {if(!pedal_drag_)pedal_.advance(dt,raise,lower);}
|
||||
bool handle_event(const sf::Event&,sf::Vector2u);
|
||||
void draw(sf::RenderWindow&,const sf::Font*,const ScenarioSnapshot&,const PlaybackInfo&);
|
||||
private:
|
||||
DynoCurve dyno_;
|
||||
EngineRenderer3D renderer_;
|
||||
OrbitCamera camera_;
|
||||
bool three_d_{true},camera_fitted_{},pan_drag_{},pedal_drag_{};
|
||||
PedalControl pedal_;
|
||||
sf::Vector2f pan_{};
|
||||
sf::Vector2i previous_mouse_{};
|
||||
bool dragging_{};
|
||||
float zoom_{1};
|
||||
};
|
||||
}
|
||||
6
include/FluidSim/Desktop/DynoRenderer.hpp
Normal file
6
include/FluidSim/Desktop/DynoRenderer.hpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include "FluidSim/DynoCurve.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
namespace fluidsim::port::desktop {
|
||||
void draw_dyno_curve(sf::RenderWindow&,const sf::Font*,const DynoCurve&,float,float,float,float,float,float);
|
||||
}
|
||||
19
include/FluidSim/Desktop/EngineRenderer3D.hpp
Normal file
19
include/FluidSim/Desktop/EngineRenderer3D.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/Desktop/OrbitCamera.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
namespace fluidsim::port::desktop {
|
||||
// Visualization consumes snapshots only; it never accesses mutable solver state.
|
||||
class EngineRenderer3D {
|
||||
public:
|
||||
EngineRenderer3D();
|
||||
~EngineRenderer3D();
|
||||
EngineRenderer3D(const EngineRenderer3D&)=delete;
|
||||
EngineRenderer3D& operator=(const EngineRenderer3D&)=delete;
|
||||
void draw(sf::RenderWindow&,const ScenarioSnapshot&,sf::FloatRect,const OrbitCamera&);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
6
include/FluidSim/Desktop/EngineSchematic.hpp
Normal file
6
include/FluidSim/Desktop/EngineSchematic.hpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
namespace fluidsim::port::desktop {
|
||||
void draw_schematic(sf::RenderWindow&, const sf::Font*, const ScenarioSnapshot&);
|
||||
}
|
||||
20
include/FluidSim/Desktop/OrbitCamera.hpp
Normal file
20
include/FluidSim/Desktop/OrbitCamera.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
namespace fluidsim::port::desktop {
|
||||
struct OrbitCamera {
|
||||
Vec3 target{.12f,.25f,0};
|
||||
float yaw{-28},pitch{24},distance{2.2f};
|
||||
void reset(float aspect) {target={.12f,.25f,0};yaw=-28;pitch=24;distance=std::max(.65f,1.6f/aspect)/.414214f;}
|
||||
void orbit(sf::Vector2i delta) {yaw+=delta.x*.35f;pitch=std::clamp(pitch+delta.y*.3f,-75.f,80.f);}
|
||||
void pan(sf::Vector2i delta,float height) {
|
||||
const float y=yaw*.017453293f,p=pitch*.017453293f,k=distance*.828427f/std::max(height,1.f);
|
||||
target.x+=k*(-delta.x*std::cos(y)+delta.y*std::sin(p)*std::sin(y));
|
||||
target.y+=k*delta.y*std::cos(p);
|
||||
target.z+=k*(-delta.x*std::sin(y)-delta.y*std::sin(p)*std::cos(y));
|
||||
}
|
||||
void zoom(float delta) {distance=std::clamp(distance*std::exp(-delta*.12f),.35f,12.f);}
|
||||
};
|
||||
}
|
||||
15
include/FluidSim/Desktop/PedalControl.hpp
Normal file
15
include/FluidSim/Desktop/PedalControl.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
namespace fluidsim::port::desktop {
|
||||
// Keyboard input travel is separate from the ECU actuator response.
|
||||
class PedalControl {
|
||||
public:
|
||||
float value() const {return value_;}
|
||||
void set(float value) {value_=std::clamp(value,0.f,1.f);}
|
||||
void advance(float dt,bool raise,bool lower) {
|
||||
if(dt>0) set(value_+(raise && !lower?2.f:-2.f)*dt);
|
||||
}
|
||||
private:
|
||||
float value_{};
|
||||
};
|
||||
}
|
||||
46
include/FluidSim/Desktop/PipePath.hpp
Normal file
46
include/FluidSim/Desktop/PipePath.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
namespace fluidsim::port::desktop {
|
||||
struct PipeAnchor {float distance;Vec3 position;};
|
||||
// Knots are distances along the simulated pipe, in metres. Display positions
|
||||
// define its schematic route; changing them never changes fluid cell lengths.
|
||||
class PipePath {
|
||||
public:
|
||||
explicit PipePath(std::vector<PipeAnchor> anchors):anchors_(std::move(anchors)) {
|
||||
if(anchors_.size()<2 || anchors_.front().distance!=0)throw std::invalid_argument("pipe path needs origin and endpoint");
|
||||
for(std::size_t i=0;i<anchors_.size();++i) {
|
||||
const auto& p=anchors_[i];
|
||||
if(!std::isfinite(p.distance) || !std::isfinite(p.position.x) || !std::isfinite(p.position.y) || !std::isfinite(p.position.z) ||
|
||||
(i && !(p.distance>anchors_[i-1].distance))) throw std::invalid_argument("invalid distance-referenced pipe path");
|
||||
}
|
||||
}
|
||||
Vec3 position(float distance) const {
|
||||
distance=std::clamp(distance,0.f,anchors_.back().distance);
|
||||
auto it=std::upper_bound(anchors_.begin(),anchors_.end(),distance,[](float s,const PipeAnchor& a){return s<a.distance;});
|
||||
std::size_t i=std::clamp<std::size_t>(it-anchors_.begin(),1,anchors_.size()-1)-1;
|
||||
const auto& a=anchors_[i];const auto& b=anchors_[i+1];
|
||||
const float span=b.distance-a.distance,t=(distance-a.distance)/span,t2=t*t,t3=t2*t;
|
||||
const auto ma=tangent_at(i),mb=tangent_at(i+1);
|
||||
auto coordinate=[&](float pa,float pb,float ta,float tb){return (2*t3-3*t2+1)*pa+(t3-2*t2+t)*span*ta+(-2*t3+3*t2)*pb+(t3-t2)*span*tb;};
|
||||
return {coordinate(a.position.x,b.position.x,ma.x,mb.x),coordinate(a.position.y,b.position.y,ma.y,mb.y),coordinate(a.position.z,b.position.z,ma.z,mb.z)};
|
||||
}
|
||||
Vec3 tangent(float distance) const {
|
||||
const float epsilon=anchors_.back().distance*.0001f;
|
||||
const auto a=position(distance-epsilon),b=position(distance+epsilon);
|
||||
const float x=b.x-a.x,y=b.y-a.y,z=b.z-a.z,n=std::sqrt(x*x+y*y+z*z);
|
||||
return n>1e-8f?Vec3{x/n,y/n,z/n}:Vec3{1,0,0};
|
||||
}
|
||||
private:
|
||||
Vec3 tangent_at(std::size_t i) const {
|
||||
const auto& a=anchors_[i?i-1:0];const auto& b=anchors_[std::min(i+1,anchors_.size()-1)];
|
||||
const float d=b.distance-a.distance;
|
||||
return {(b.position.x-a.position.x)/d,(b.position.y-a.position.y)/d,(b.position.z-a.position.z)/d};
|
||||
}
|
||||
std::vector<PipeAnchor> anchors_;
|
||||
};
|
||||
}
|
||||
12
include/FluidSim/Desktop/PressurePalette.hpp
Normal file
12
include/FluidSim/Desktop/PressurePalette.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
namespace fluidsim::port::desktop {
|
||||
// Fixed absolute scale: vacuum blue, atmosphere green, >=1 MPa red.
|
||||
inline Vec3 pressure_tint(float pressure) {
|
||||
if(pressure<=101325) {float t=std::clamp(pressure/101325,0.f,1.f);return {.05f,.15f+.70f*t,1-.95f*t};}
|
||||
float t=std::sqrt(std::clamp((pressure-101325)/(1000000-101325),0.f,1.f));
|
||||
return {.05f+.95f*t,.85f-.80f*t,.05f-.03f*t};
|
||||
}
|
||||
}
|
||||
7
include/FluidSim/Desktop/ScenarioMenu.hpp
Normal file
7
include/FluidSim/Desktop/ScenarioMenu.hpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <optional>
|
||||
namespace fluidsim::port::desktop {
|
||||
std::optional<std::size_t> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
|
||||
}
|
||||
@@ -9,13 +9,12 @@ namespace fluidsim::port {
|
||||
// the twin already protects its own simulation state.
|
||||
class DesktopScenario {
|
||||
public:
|
||||
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, bool saab_turbo = true) {
|
||||
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, V8ExhaustConfig v8 = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)) {
|
||||
if (kind == ScenarioKind::TwoStroke)
|
||||
two_ = std::make_unique<TwoStrokeScenario>(44100, config);
|
||||
else
|
||||
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin,
|
||||
kind == ScenarioKind::TurboTwin || (kind == ScenarioKind::SaabB205 && saab_turbo));
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin, exhaust, v8,engine);
|
||||
}
|
||||
AudioFrame process() {
|
||||
if (twin_)
|
||||
@@ -26,23 +25,23 @@ public:
|
||||
void set_controls(float pedal, float load, float clutch) {
|
||||
visit([&](auto& scenario) { scenario.set_controls(pedal, load, clutch); });
|
||||
}
|
||||
void set_starter(bool engaged) { visit([&](auto& scenario) { scenario.set_starter(engaged); }); }
|
||||
void shift_up() { visit([](auto& scenario) { scenario.shift_up(); }); }
|
||||
void shift_down() { visit([](auto& scenario) { scenario.shift_down(); }); }
|
||||
void set_neutral() { visit([](auto& scenario) { scenario.set_neutral(); }); }
|
||||
void toggle_mechanical_sound() {
|
||||
if (twin_)
|
||||
twin_->toggle_mechanical_sound();
|
||||
}
|
||||
void set_pipe_solver(PipeSolver solver) { visit([&](auto& scenario) { scenario.set_pipe_solver(solver); }); }
|
||||
void set_audio_scene(OutdoorScene scene) {
|
||||
visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
|
||||
}
|
||||
ScenarioSnapshot snapshot() const {
|
||||
if (twin_)
|
||||
return twin_->snapshot();
|
||||
if (twin_) { auto s=twin_->snapshot();s.example_name=name_;return s; }
|
||||
const std::lock_guard lock(mutex_);
|
||||
const auto source = two_->snapshot();
|
||||
ScenarioSnapshot s;
|
||||
s.example_name=name_;
|
||||
s.pipe_solver=two_->pipes.front().solver;
|
||||
s.two_stroke = true;
|
||||
s.cylinder_angles[0]=two_->crankshaft.angle;
|
||||
s.cylinder_count = 1;
|
||||
s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing;
|
||||
s.rpm = source.rpm;
|
||||
@@ -81,6 +80,7 @@ private:
|
||||
if (twin_) function(*twin_);
|
||||
else function(*two_);
|
||||
}
|
||||
std::string name_;
|
||||
std::unique_ptr<FourStrokeTwinScenario> twin_;
|
||||
std::unique_ptr<TwoStrokeScenario> two_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
@@ -6,12 +6,14 @@ namespace fluidsim::port {
|
||||
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
||||
class Ecu {
|
||||
public:
|
||||
enum class IdleControl { Feedback, FixedOpening };
|
||||
IdleControl idle_control{IdleControl::Feedback};
|
||||
struct IgnitionPoint { float rpm, degrees_btdc; };
|
||||
std::vector<IgnitionPoint> ignition_map{{0, 10}, {15000, 40}};
|
||||
float idle_rpm{1800.0f};
|
||||
float idle_base_opening{0.08f}, idle_max_opening{0.25f};
|
||||
float idle_kp{0.0001f}, idle_ki{0.00004f};
|
||||
float throttle_time_constant{0.08f}, throttle_rate{2.0f}; // seconds, full travel/second
|
||||
float throttle_time_constant{0.001f}, throttle_rate{1000.0f}; // seconds, full travel/second
|
||||
// Zero uses the opening calibration, preserving symmetric response.
|
||||
float throttle_closing_time_constant{}, throttle_closing_rate{};
|
||||
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
@@ -20,12 +22,6 @@ public:
|
||||
bool overrun_fuel_cut_enabled{true};
|
||||
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
||||
float stoichiometric_afr{14.7f}, power_afr{12.8f};
|
||||
bool boost_control_enabled{false};
|
||||
float maximum_boost{45000}; // Pa gauge, estimated low-pressure B205 calibration
|
||||
float boost_kp{1e-5f}, boost_ki{4e-6f};
|
||||
void control_boost(float dt, float pedal, float rpm, float manifold, float charge, float ambient = 101325);
|
||||
float boost_target() const { return boost_target_; }
|
||||
float wastegate_duty() const { return boost_duty_; }
|
||||
|
||||
void step(float dt, float pedal, float rpm);
|
||||
float fuel_for_air(float trapped_air_mass) const;
|
||||
@@ -33,14 +29,12 @@ public:
|
||||
float throttle_target() const { return target_; }
|
||||
float target_afr() const { return afr_; }
|
||||
float spark_advance() const { return spark_; }
|
||||
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_ || boost_cut_; }
|
||||
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
|
||||
bool rev_limited() const { return limiter_; }
|
||||
bool idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
|
||||
private:
|
||||
float boost_target_{}, boost_duty_{}, boost_integral_{}, filtered_boost_{};
|
||||
bool boost_cut_{};
|
||||
float filtered_rpm_{};
|
||||
bool initialized_{};
|
||||
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/OpenEndRadiation.hpp"
|
||||
#include "FluidSim/PipeSolver.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
@@ -165,10 +167,14 @@ struct GasState {
|
||||
|
||||
class Pipe1D {
|
||||
public:
|
||||
std::vector<float> linear_resistance, quadratic_resistance;
|
||||
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x.
|
||||
struct Flux {
|
||||
float m{}, p{}, e{}, y{}, fuel{};
|
||||
};
|
||||
struct RoeState { double root_density{}, enthalpy{}; };
|
||||
PipeSolver solver{PipeSolver::Rusanov};
|
||||
std::uint64_t hllc_fallbacks{};
|
||||
float area{}, dx{};
|
||||
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
|
||||
std::vector<float> cell_areas, cell_lengths;
|
||||
@@ -180,7 +186,7 @@ public:
|
||||
Pipe1D(std::vector<float> areas, std::vector<float> lengths)
|
||||
: area(areas.empty() ? 0 : areas.front()), dx(lengths.empty() ? 0 : lengths.front()),
|
||||
cell_areas(std::move(areas)), cell_lengths(std::move(lengths)), cells(cell_areas.size()),
|
||||
next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1) {
|
||||
next_cells_(cells.size()), states_(cells.size()), waves_(cells.size()), faces_(cells.size() + 1), roe_states_(cells.size()) {
|
||||
if (cells.empty() || cell_lengths.size() != cells.size())
|
||||
throw std::invalid_argument("pipe profiles must be nonempty and have equal length");
|
||||
for (std::size_t i = 0; i < cells.size(); ++i)
|
||||
@@ -219,6 +225,12 @@ public:
|
||||
throw std::runtime_error("nonphysical pipe state in preparation");
|
||||
dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]);
|
||||
}
|
||||
if (solver != PipeSolver::Rusanov)
|
||||
for (std::size_t i=0; i<cells.size(); ++i) {
|
||||
const auto& s=states_[i];
|
||||
const double energy=s.pressure*pipe_gm1_inverse+.5*double(s.rho)*s.velocity*s.velocity;
|
||||
roe_states_[i]={std::sqrt(s.rho),(energy+s.pressure)/s.rho};
|
||||
}
|
||||
prepared_ = true;
|
||||
return dt;
|
||||
}
|
||||
@@ -251,6 +263,70 @@ public:
|
||||
static Flux riemann(const GasState &a, const GasState &b) {
|
||||
return riemann(a, b, std::max(wave(a), wave(b)));
|
||||
}
|
||||
// HLLE/HLLC use bounds enclosing endpoint and Roe acoustic speeds.
|
||||
template<PipeSolver method>
|
||||
static Flux flux_kernel(const GasState& a, const GasState& b,
|
||||
float ca, float cb, bool* fallback = nullptr,
|
||||
const RoeState* left_roe = nullptr, const RoeState* right_roe = nullptr) {
|
||||
if constexpr (method == PipeSolver::Rusanov)
|
||||
return riemann(a,b,std::max(std::abs(a.velocity)+ca,std::abs(b.velocity)+cb));
|
||||
const double ra=left_roe?left_roe->root_density:std::sqrt(a.rho);
|
||||
const double rb=right_roe?right_roe->root_density:std::sqrt(b.rho), sum=ra+rb;
|
||||
const double ea=a.pressure*pipe_gm1_inverse+.5*double(a.rho)*a.velocity*a.velocity;
|
||||
const double eb=b.pressure*pipe_gm1_inverse+.5*double(b.rho)*b.velocity*b.velocity;
|
||||
const double ua=a.velocity, ub=b.velocity;
|
||||
const double uroe=(ra*ua+rb*ub)/sum;
|
||||
const double ha=left_roe?left_roe->enthalpy:(ea+a.pressure)/a.rho;
|
||||
const double hb=right_roe?right_roe->enthalpy:(eb+b.pressure)/b.rho;
|
||||
const double hroe=(ra*ha+rb*hb)/sum;
|
||||
const double croe=std::sqrt(std::max(0.,double(gamma-1)*(hroe-.5*uroe*uroe)));
|
||||
const double sl=std::min({ua-ca,ub-cb,uroe-croe});
|
||||
const double sr=std::max({ua+ca,ub+cb,uroe+croe});
|
||||
const auto physical=[](const GasState& q,double energy) {
|
||||
const float m=q.rho*q.velocity;
|
||||
return Flux{m,m*q.velocity+q.pressure,float((energy+q.pressure)*q.velocity),m*q.air,m*q.fuel};
|
||||
};
|
||||
const Flux fa=physical(a,ea), fb=physical(b,eb);
|
||||
if(sl>=0) return fa;
|
||||
if(sr<=0) return fb;
|
||||
const auto hlle=[&] {
|
||||
const double inv=1/(sr-sl), diff=sl*sr;
|
||||
return Flux{float((sr*fa.m-sl*fb.m+diff*(b.rho-a.rho))*inv),
|
||||
float((sr*fa.p-sl*fb.p+diff*(b.rho*ub-a.rho*ua))*inv),
|
||||
float((sr*fa.e-sl*fb.e+diff*(eb-ea))*inv),
|
||||
float((sr*fa.y-sl*fb.y+diff*(b.rho*b.air-a.rho*a.air))*inv),
|
||||
float((sr*fa.fuel-sl*fb.fuel+diff*(b.rho*b.fuel-a.rho*a.fuel))*inv)};
|
||||
};
|
||||
if constexpr(method==PipeSolver::Hlle) return hlle();
|
||||
const double dl=a.rho*(sl-ua), dr=b.rho*(sr-ub);
|
||||
const double sm=(b.pressure-a.pressure+dl*ua-dr*ub)/(dl-dr);
|
||||
const double ps=a.pressure+dl*(sm-ua);
|
||||
// Reject nonphysical HLLC intermediate states without modifying conserved cells.
|
||||
const bool left=sm>=0;
|
||||
const auto& q=left?a:b;
|
||||
const auto f=left?fa:fb;
|
||||
const double speed=left?sl:sr;
|
||||
const double inv=1/(speed-sm);
|
||||
const double density=(left?dl:dr)*inv;
|
||||
const double energy=((speed-q.velocity)*(left?ea:eb)-q.pressure*q.velocity+ps*sm)*inv;
|
||||
if(!std::isfinite(sm) || !(sl<sm && sm<sr) || !(ps>0) || !(density>0) ||
|
||||
!(energy>.5*density*sm*sm)) {
|
||||
if(fallback) *fallback=true;
|
||||
return hlle();
|
||||
}
|
||||
const double mass=f.m+speed*(density-q.rho);
|
||||
return {float(mass),float(f.p+speed*(density*sm-q.rho*q.velocity)),
|
||||
float(f.e+speed*(energy-(left?ea:eb))),float(mass*q.air),float(mass*q.fuel)};
|
||||
}
|
||||
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver,
|
||||
float ca,float cb,bool* fallback=nullptr) {
|
||||
if(solver==PipeSolver::Rusanov) return flux_kernel<PipeSolver::Rusanov>(a,b,ca,cb,fallback);
|
||||
if(solver==PipeSolver::Hlle) return flux_kernel<PipeSolver::Hlle>(a,b,ca,cb,fallback);
|
||||
return flux_kernel<PipeSolver::Hllc>(a,b,ca,cb,fallback);
|
||||
}
|
||||
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver) {
|
||||
return numerical_flux(a,b,solver,std::sqrt(gamma*a.pressure/a.rho),std::sqrt(gamma*b.pressure/b.rho));
|
||||
}
|
||||
void advance(float dt) {
|
||||
if (dt <= 0)
|
||||
return;
|
||||
@@ -259,12 +335,27 @@ public:
|
||||
const std::size_t n = cells.size();
|
||||
if (!prepared_)
|
||||
prepare_step();
|
||||
faces_[0] = left_override_ ? left_flux_ : scaled(riemann(left_ghost, states_[0]), cell_areas[0]);
|
||||
faces_[0] = left_override_ ? left_flux_ : scaled(numerical_flux(left_ghost, states_[0], solver), cell_areas[0]);
|
||||
faces_[n] =
|
||||
right_override_ ? right_flux_ : scaled(riemann(states_[n - 1], right_ghost), cell_areas[n - 1]);
|
||||
for (std::size_t i = 1; i < n; ++i)
|
||||
faces_[i] = scaled(riemann(states_[i - 1], states_[i], std::max(waves_[i - 1], waves_[i])),
|
||||
.5f * (cell_areas[i - 1] + cell_areas[i]));
|
||||
right_override_ ? right_flux_ : scaled(numerical_flux(states_[n - 1], right_ghost, solver), cell_areas[n - 1]);
|
||||
if (solver == PipeSolver::Rusanov) {
|
||||
for (std::size_t i = 1; i < n; ++i)
|
||||
faces_[i] = scaled(riemann(states_[i-1], states_[i], std::max(waves_[i-1], waves_[i])),
|
||||
.5f*(cell_areas[i-1]+cell_areas[i]));
|
||||
} else {
|
||||
const auto fill_faces=[&]<PipeSolver method>() {
|
||||
for (std::size_t i = 1; i < n; ++i) {
|
||||
bool fallback=false;
|
||||
faces_[i] = scaled(flux_kernel<method>(states_[i-1], states_[i],
|
||||
waves_[i-1]-std::abs(states_[i-1].velocity), waves_[i]-std::abs(states_[i].velocity),
|
||||
&fallback, &roe_states_[i-1], &roe_states_[i]),
|
||||
.5f*(cell_areas[i-1]+cell_areas[i]));
|
||||
hllc_fallbacks += fallback;
|
||||
}
|
||||
};
|
||||
if(solver==PipeSolver::Hlle) fill_faces.template operator()<PipeSolver::Hlle>();
|
||||
else fill_faces.template operator()<PipeSolver::Hllc>();
|
||||
}
|
||||
const float relaxation = energy_relaxation_rate > 0 ? std::exp(-energy_relaxation_rate * dt) : 1.0f;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const auto fl = faces_[i], fr = faces_[i + 1];
|
||||
@@ -283,6 +374,11 @@ public:
|
||||
rho = 1e-12f;
|
||||
++positivity_repairs;
|
||||
}
|
||||
if (!linear_resistance.empty()) {
|
||||
const float speed = std::abs(mom/rho);
|
||||
mom /= 1 + dt*(linear_resistance[i] + .5f*quadratic_resistance[i]*speed);
|
||||
// Retain total energy: dissipated kinetic energy becomes heat.
|
||||
}
|
||||
if (damping_multiplier > 0)
|
||||
mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho);
|
||||
if (energy_relaxation_rate > 0)
|
||||
@@ -315,6 +411,7 @@ private:
|
||||
std::vector<GasState> states_;
|
||||
std::vector<float> waves_;
|
||||
std::vector<Flux> faces_;
|
||||
std::vector<RoeState> roe_states_;
|
||||
Flux left_flux_{}, right_flux_{};
|
||||
bool left_override_{}, right_override_{}, prepared_{};
|
||||
static std::vector<float> uniform(float v, int n) {
|
||||
@@ -387,6 +484,19 @@ public:
|
||||
claim(pipe, left);
|
||||
opens_.push_back({pipe, left, p, a});
|
||||
}
|
||||
// Full circular mouth radiating into free space; no flange or mean-flow correction.
|
||||
void add_radiating_open_end(int pipe, bool left, float pressure, float area, float temperature = 300) {
|
||||
if (!std::isfinite(temperature) || temperature <= 0)
|
||||
throw std::invalid_argument("invalid ambient temperature");
|
||||
if (pipe < 0 || static_cast<std::size_t>(pipe) >= pipes_.size())
|
||||
throw std::invalid_argument("invalid boundary pipe index");
|
||||
const float face_area = pipes_[pipe].cell_area(end(pipes_[pipe], left));
|
||||
if (std::abs(area-face_area) > 1e-6f*face_area)
|
||||
throw std::invalid_argument("radiating mouth must match pipe face area");
|
||||
add_open_end(pipe, left, pressure, area);
|
||||
opens_.back().radiating = true;
|
||||
opens_.back().temperature = temperature;
|
||||
}
|
||||
// An ideal, zero-volume junction between two duct faces. Orientation is explicit.
|
||||
void add_connection(int a, bool left_a, int b, bool left_b) {
|
||||
if (a < 0 || b < 0 || static_cast<std::size_t>(a) >= pipes_.size() ||
|
||||
@@ -404,7 +514,23 @@ public:
|
||||
claim(b, left_b);
|
||||
connections_.push_back({a, b, left_a, left_b});
|
||||
}
|
||||
struct JunctionFace { int pipe; bool left; };
|
||||
void add_junction(std::initializer_list<JunctionFace> ends) {
|
||||
if(ends.size()<2) throw std::invalid_argument("junction needs at least two faces");
|
||||
JunctionNode node;
|
||||
for(auto e:ends) {
|
||||
check_face(e.pipe,e.left);
|
||||
for(auto previous:node.ends) {
|
||||
if(previous.pipe==e.pipe && previous.left==e.left) throw std::invalid_argument("duplicate junction face");
|
||||
}
|
||||
node.ends.push_back(e);
|
||||
}
|
||||
for(auto e:ends) claim(e.pipe,e.left);
|
||||
node.states.resize(ends.size());
|
||||
nodes_.push_back(std::move(node));
|
||||
}
|
||||
float open_end_mass_flow(std::size_t i) const { return opens_.at(i).last_mass_flow; }
|
||||
float open_end_volume_flow(std::size_t i) const { return opens_.at(i).last_volume_flow; }
|
||||
float open_end_pressure(std::size_t i) const { return opens_.at(i).last_face_pressure; }
|
||||
std::uint64_t limited_fluxes{}, nonlinear_iterations{};
|
||||
void validate_topology() const {
|
||||
@@ -421,12 +547,52 @@ public:
|
||||
for (auto &o : orifices_)
|
||||
resolve_orifice(o, dt);
|
||||
for (auto &o : opens_)
|
||||
resolve_open(o);
|
||||
resolve_open(o, dt);
|
||||
for (auto &o : connections_)
|
||||
resolve_connection(o);
|
||||
for(auto& node:nodes_) resolve_junction(node,dt);
|
||||
}
|
||||
|
||||
private:
|
||||
struct JunctionNode {
|
||||
std::vector<JunctionFace> ends;
|
||||
std::vector<GasState> states;
|
||||
};
|
||||
std::vector<JunctionNode> nodes_;
|
||||
// Zero-storage Rusanov star state. Integrated mass, total energy and
|
||||
// species fluxes sum to zero algebraically across every connected face.
|
||||
// The node has no conserved state, timestep or storage volume of its own.
|
||||
void resolve_junction(JunctionNode& node,float) {
|
||||
double speed=0;
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
auto e=node.ends[k];auto& p=pipes_[e.pipe];auto s=p.state(end(p,e.left));
|
||||
s.velocity*=e.left?-1:1;node.states[k]=s;
|
||||
speed=std::max(speed,2*(std::sqrt(gamma*s.pressure/s.rho)+std::abs(double(s.velocity))));
|
||||
}
|
||||
double area_sum=0,rho=0,energy=0,air=0,fuel=0;
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
|
||||
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
|
||||
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
|
||||
area_sum+=a;rho+=a*s.rho*(1+u/speed);
|
||||
energy+=a*(en+(en+s.pressure)*u/speed);
|
||||
air+=a*s.rho*s.air*(1+u/speed);fuel+=a*s.rho*s.fuel*(1+u/speed);
|
||||
}
|
||||
rho/=area_sum;energy/=area_sum;air/=area_sum;fuel/=area_sum;
|
||||
const double pressure=energy*pipe_gm1;
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
auto e=node.ends[k];auto& p=pipes_[e.pipe];const auto s=node.states[k];
|
||||
const double a=p.cell_area(end(p,e.left)), u=s.velocity;
|
||||
const double en=s.pressure/pipe_gm1+.5*s.rho*u*u;
|
||||
const double orientation=e.left?-1:1;
|
||||
const double m=.5*a*(s.rho*u-speed*(rho-s.rho));
|
||||
const double heat=.5*a*((en+s.pressure)*u-speed*(energy-en));
|
||||
const double y=.5*a*(s.rho*s.air*u-speed*(air-s.rho*s.air));
|
||||
const double f=.5*a*(s.rho*s.fuel*u-speed*(fuel-s.rho*s.fuel));
|
||||
const double momentum=.5*a*(s.rho*u*u+s.pressure+pressure+speed*s.rho*u);
|
||||
p.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*heat),float(orientation*y),float(orientation*f)});
|
||||
}
|
||||
}
|
||||
struct Budget {
|
||||
float *air{}, *exhaust{}, *fuel{}, *energy{};
|
||||
float mass{}, heat{};
|
||||
@@ -443,7 +609,10 @@ private:
|
||||
struct Open {
|
||||
int pipe;
|
||||
bool left;
|
||||
float pressure, area, last_mass_flow{}, last_face_pressure{};
|
||||
float pressure, area, last_mass_flow{}, last_face_pressure{}, last_volume_flow{};
|
||||
bool radiating{};
|
||||
float temperature{300};
|
||||
OpenEndRadiation reflection{};
|
||||
};
|
||||
struct Connection {
|
||||
int a, b;
|
||||
@@ -630,7 +799,7 @@ private:
|
||||
if (!from)
|
||||
v.fuel_fraction = s.fuel;
|
||||
}
|
||||
void resolve_open(Open &o) {
|
||||
void resolve_open(Open &o, float dt) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
int i = end(p, o.left);
|
||||
auto s = p.state(i);
|
||||
@@ -638,6 +807,40 @@ private:
|
||||
if (o.left ? s.velocity >= c : s.velocity <= -c)
|
||||
throw std::runtime_error(
|
||||
"supersonic inlet needs full upstream state; pressure-only boundary is insufficient");
|
||||
if (o.radiating) {
|
||||
const float direction = o.left ? -1.f : 1.f;
|
||||
const double outward = direction*s.velocity;
|
||||
GasState face = s;
|
||||
if (outward < c) {
|
||||
const double reference_c = c*std::pow(double(o.pressure)/s.pressure, (gamma-1)/(2*gamma));
|
||||
const double incident = outward + 2*(c-reference_c)/(gamma-1);
|
||||
const double radius = std::sqrt(o.area/pi);
|
||||
const double ambient_c = std::sqrt(gamma*gas_constant*o.temperature);
|
||||
const double reflected = o.reflection.process(incident, dt*ambient_c/radius);
|
||||
const double sound = reference_c + (gamma-1)*.25*(incident+reflected);
|
||||
if (!(sound > 0)) throw std::runtime_error("radiation characteristic outside subsonic model");
|
||||
face.pressure = o.pressure*std::pow(sound/reference_c, 2*gamma/(gamma-1));
|
||||
face.velocity = direction*.5*(incident-reflected);
|
||||
face.rho = s.rho*std::pow(face.pressure/s.pressure, 1/gamma);
|
||||
if (direction*face.velocity < 0) {
|
||||
// Fresh ambient gas enters with ambient stagnation enthalpy.
|
||||
const float temperature = o.temperature - .5f*face.velocity*face.velocity /
|
||||
(gamma/(gamma-1)*gas_constant);
|
||||
if (!(temperature > 0)) throw std::runtime_error("ambient inflow exceeds stagnation enthalpy");
|
||||
face.rho = face.pressure/(gas_constant*temperature);
|
||||
face.air = 1; face.fuel = 0;
|
||||
}
|
||||
}
|
||||
ghost(p, o.left, face);
|
||||
const float mass_flux = face.rho*face.velocity*o.area;
|
||||
const float enthalpy = gamma/(gamma-1)*face.pressure/face.rho + .5f*face.velocity*face.velocity;
|
||||
p.set_boundary_flux(o.left, {mass_flux, mass_flux*face.velocity+face.pressure*o.area,
|
||||
mass_flux*enthalpy, mass_flux*face.air, mass_flux*face.fuel});
|
||||
o.last_volume_flow = direction*face.velocity*o.area;
|
||||
o.last_mass_flow = direction*mass_flux;
|
||||
o.last_face_pressure = face.pressure;
|
||||
return;
|
||||
}
|
||||
const float entropy = s.pressure / std::pow(s.rho, gamma);
|
||||
const float rho = std::pow(o.pressure / entropy, 1 / gamma);
|
||||
const float cb = std::sqrt(gamma * o.pressure / rho);
|
||||
@@ -653,6 +856,7 @@ private:
|
||||
f.p += s.pressure * (p.cell_area(i) - a);
|
||||
p.set_boundary_flux(o.left, f);
|
||||
o.last_mass_flow = f.m * (o.left ? -1 : 1);
|
||||
o.last_volume_flow = o.last_mass_flow / (ambient_inflow ? rho : s.rho);
|
||||
o.last_face_pressure = f.p / p.cell_area(i);
|
||||
}
|
||||
void resolve_connection(const Connection &o) {
|
||||
@@ -664,7 +868,7 @@ private:
|
||||
sa.velocity *= da;
|
||||
sb.velocity *= db;
|
||||
const float area = std::min(a.cell_area(ia), b.cell_area(ib));
|
||||
auto f = Pipe1D::scaled(Pipe1D::riemann(sa, sb), area);
|
||||
auto f = Pipe1D::scaled(Pipe1D::numerical_flux(sa, sb, a.solver == b.solver ? a.solver : PipeSolver::Rusanov), area);
|
||||
a.set_boundary_flux(o.left_a,
|
||||
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e,
|
||||
da * f.y, da * f.fuel});
|
||||
|
||||
34
include/FluidSim/Junction.hpp
Normal file
34
include/FluidSim/Junction.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include <span>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct JunctionEnd { int pipe; bool left; float discharge{1.f}; };
|
||||
// Finite-volume N-port fitting: its specified physical volume stores gas and
|
||||
// energy. All ports share pressure and mixture, with bidirectional nozzle flow.
|
||||
// T, Y, cross and collectors share this model; angles require calibrated losses.
|
||||
class Junction : public Volume0D {
|
||||
public:
|
||||
using Volume0D::Volume0D;
|
||||
void connect(BoundarySystem& boundaries, const std::vector<Pipe1D>& pipes,
|
||||
std::span<const JunctionEnd> ends) {
|
||||
if(connected_ || ends.size()<2) throw std::invalid_argument("junction needs at least two distinct ends and one connection call");
|
||||
for(std::size_t i=0;i<ends.size();++i) {
|
||||
const auto& e=ends[i];
|
||||
if(e.pipe<0 || std::size_t(e.pipe)>=pipes.size() || !std::isfinite(e.discharge) || e.discharge<=0 || e.discharge>1)
|
||||
throw std::invalid_argument("invalid junction endpoint");
|
||||
for(std::size_t j=0;j<i;++j) if(e.pipe==ends[j].pipe && e.left==ends[j].left)
|
||||
throw std::invalid_argument("duplicate junction endpoint");
|
||||
}
|
||||
for(const auto& e:ends) {
|
||||
const auto& pipe=pipes[e.pipe];
|
||||
areas_.push_back(pipe.cell_area(e.left?0:int(pipe.cells.size())-1));
|
||||
boundaries.add_orifice(create_port(),e.pipe,e.left,areas_.back(),e.discharge);
|
||||
}
|
||||
connected_=true;
|
||||
}
|
||||
private:
|
||||
std::deque<float> areas_; // stable addresses for boundary area references
|
||||
bool connected_{};
|
||||
};
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include <random>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Synthetic valve, piston, combustion, chain, and gear sounds driven by live
|
||||
// engine state; direct port of Core/MechanicalSoundProcessor.cs.
|
||||
class MechanicalSoundProcessor {
|
||||
public:
|
||||
float master_gain{0.6f}, valve_tick_gain{0.9f}, piston_slap_gain{1.2f};
|
||||
float combustion_gain{0.5f}, gear_whine_gain{0.12f}, cam_chain_gain{0.25f};
|
||||
|
||||
explicit MechanicalSoundProcessor(int sample_rate = 44100);
|
||||
float process(const Crankshaft& crank, const EngineCylinder& cylinder, float load);
|
||||
|
||||
private:
|
||||
class Resonator {
|
||||
public:
|
||||
Resonator(float frequency_hz, float q, float sample_rate);
|
||||
float process(float input);
|
||||
private:
|
||||
float b1_{}, b2_{}, gain_{}, y1_{}, y2_{};
|
||||
};
|
||||
class OnePoleLowPass {
|
||||
public:
|
||||
OnePoleLowPass(float cutoff_hz, float sample_rate);
|
||||
float process(float input);
|
||||
private:
|
||||
float alpha_{}, state_{};
|
||||
};
|
||||
|
||||
float dt_{};
|
||||
Resonator intake_tick_, exhaust_tick_;
|
||||
OnePoleLowPass slap_filter_, combustion_filter_, chain_filter_;
|
||||
float slap_envelope_{}, combustion_envelope_{}, chain_envelope_{};
|
||||
float slap_decay_{}, combustion_decay_{}, chain_decay_{};
|
||||
float gear_phase1_{}, gear_phase2_{}, gear_phase3_{};
|
||||
float previous_intake_area_{}, previous_exhaust_area_{}, previous_pressure_{};
|
||||
float previous_cycle_position_{-1.0f}, previous_crank_fraction_{-1.0f};
|
||||
std::mt19937 random_{0x5EEDu};
|
||||
std::uniform_real_distribution<float> unit_{-1.0f, 1.0f};
|
||||
|
||||
float random_sample();
|
||||
static bool crossed_fraction(float previous, float current, float target);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
@@ -10,11 +10,15 @@ class MotorcycleTest {
|
||||
public:
|
||||
Vehicle vehicle{};
|
||||
float load{}, clutch{1.0f};
|
||||
float starter_torque{18.f}; // N m at the crankshaft while held
|
||||
bool starter_engaged{};
|
||||
void set_starter(bool engaged) { starter_engaged=engaged; }
|
||||
|
||||
protected:
|
||||
void step_motorcycle(Crankshaft& crank, float dt, float maximum_load_torque) {
|
||||
crank.load_torque = std::clamp(load, 0.0f, 1.0f) * maximum_load_torque;
|
||||
vehicle.clutch_input = std::clamp(clutch, 0.0f, 1.0f);
|
||||
if(starter_engaged) crank.add_torque(std::max(starter_torque,0.f));
|
||||
crank.step(dt);
|
||||
crank.drivetrain_torque = vehicle.update(crank.omega, crank.inertia, dt);
|
||||
}
|
||||
|
||||
27
include/FluidSim/OpenEndRadiation.hpp
Normal file
27
include/FluidSim/OpenEndRadiation.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Causal unflanged circular-pipe reflection fit, Silva et al. (2009), Eq. 16.
|
||||
// R(s)=-(1+n*a/c*s)/(1+d*a/c*s+e*(a/c*s)^2).
|
||||
// Trapezoidal state integration supports the solver's variable CFL timestep.
|
||||
class OpenEndRadiation {
|
||||
public:
|
||||
double process(double incident, double scaled_dt) {
|
||||
if (!initialized_) { position_ = incident; previous_ = incident; initialized_ = true; }
|
||||
constexpr double n = .167, d = n + 2*.6133;
|
||||
constexpr double e = (d*d-n*n-1)/2;
|
||||
const double h = scaled_dt*.5;
|
||||
const double rhs = velocity_ + h/e*(previous_ + incident - position_ - d*velocity_);
|
||||
const double next_velocity = (rhs - h/e*(position_ + h*velocity_)) /
|
||||
(1 + h*d/e + h*h/e);
|
||||
position_ += h*(velocity_ + next_velocity);
|
||||
velocity_ = next_velocity;
|
||||
previous_ = incident;
|
||||
return -(position_ + n*velocity_);
|
||||
}
|
||||
private:
|
||||
double position_{}, velocity_{}, previous_{};
|
||||
bool initialized_{};
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
@@ -1,12 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class OutdoorScene { Dry, Field, Wall, Woodland };
|
||||
enum class OutdoorScene { Dry, Field, Wall, Woodland, Tunnel };
|
||||
const char *scene_name(OutdoorScene scene);
|
||||
|
||||
struct OutdoorAudioConfig {
|
||||
@@ -28,9 +30,11 @@ struct OutdoorAudioConfig {
|
||||
class OutdoorAudio {
|
||||
public:
|
||||
explicit OutdoorAudio(int sample_rate = 44100, OutdoorAudioConfig config = {});
|
||||
// Acoustic source inputs are ambient-density times mouth volume flow, kg/s.
|
||||
// Fluid transport still uses actual donor-density mass flux independently.
|
||||
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0);
|
||||
void set_scene(OutdoorScene scene) { scene_ = scene; }
|
||||
OutdoorScene scene() const { return scene_; }
|
||||
void set_scene(OutdoorScene scene) { requested_scene_.store(scene, std::memory_order_relaxed); }
|
||||
OutdoorScene scene() const { return requested_scene_.load(std::memory_order_relaxed); }
|
||||
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
|
||||
AudioFrame dry_frame() const { return last_dry_; }
|
||||
AudioFrame unfiltered_frame() const { return last_raw_; }
|
||||
@@ -44,7 +48,7 @@ private:
|
||||
std::size_t delay_left{}, delay_right{};
|
||||
float fraction_left{}, fraction_right{}, gain_left{}, gain_right{}, alpha{1};
|
||||
float state_left{}, state_right{};
|
||||
int group{}; // direct, ground, wall, trees
|
||||
int group{}; // direct, ground, wall, trees, tunnel
|
||||
};
|
||||
struct Emitter {
|
||||
std::vector<float> filtered, raw;
|
||||
@@ -53,22 +57,17 @@ private:
|
||||
float previous_flow{};
|
||||
LowPass filter;
|
||||
};
|
||||
struct TailLine {
|
||||
std::vector<float> buffer;
|
||||
std::size_t position{};
|
||||
float gain{}, state{};
|
||||
};
|
||||
int rate_{};
|
||||
OutdoorAudioConfig config_;
|
||||
OutdoorScene scene_{OutdoorScene::Wall};
|
||||
std::atomic<OutdoorScene> requested_scene_{OutdoorScene::Wall};
|
||||
OutdoorScene active_scene_{OutdoorScene::Wall};
|
||||
bool monitor_filter_{true};
|
||||
std::array<Emitter, 3> emitters_;
|
||||
std::array<TailLine, 4> tail_;
|
||||
std::array<float, 4> weights_{1, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_alpha_{}, tail_mix_{};
|
||||
OutdoorExhaustReverb late_reverb_;
|
||||
std::array<float, 5> weights_{1, 0, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_mix_{};
|
||||
AudioFrame last_dry_{}, last_raw_{};
|
||||
void configure_emitter(Emitter &emitter, Vec3 position);
|
||||
AudioFrame render(Emitter &emitter, float flow, AudioFrame &dry, AudioFrame &raw);
|
||||
AudioFrame diffuse(float input);
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
explicit OutdoorExhaustReverb(int sample_rate = 44100);
|
||||
AudioFrame process_stereo(float input);
|
||||
float process(float input);
|
||||
void set_decay_times(float low_seconds, float mid_seconds, float high_seconds);
|
||||
|
||||
private:
|
||||
class DelayLine {
|
||||
@@ -40,7 +41,7 @@ private:
|
||||
};
|
||||
|
||||
int sample_rate_{};
|
||||
float dt_{}, gain_low_{}, gain_mid_{}, gain_high_{};
|
||||
float dt_{}, average_delay_seconds_{}, gain_low_{}, gain_mid_{}, gain_high_{};
|
||||
std::vector<DelayLine> fdn_;
|
||||
std::vector<OnePoleLowPass> low_filters_, mid_filters_;
|
||||
std::vector<float> read_buffer_, mix_buffer_;
|
||||
@@ -49,6 +50,7 @@ private:
|
||||
float fast_envelope_{}, slow_envelope_{}, bloom_{};
|
||||
float fast_attack_{}, fast_release_{}, slow_attack_{}, slow_release_{}, bloom_decay_{};
|
||||
|
||||
void update_feedback_gains();
|
||||
static std::size_t delay_samples(int sample_rate, float milliseconds);
|
||||
};
|
||||
|
||||
|
||||
20
include/FluidSim/PipeSolver.hpp
Normal file
20
include/FluidSim/PipeSolver.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
namespace fluidsim::port {
|
||||
enum class PipeSolver { Rusanov, Hlle, Hllc };
|
||||
inline const char* pipe_solver_name(PipeSolver solver) {
|
||||
switch(solver) {
|
||||
case PipeSolver::Rusanov: return "rusanov";
|
||||
case PipeSolver::Hlle: return "hlle";
|
||||
case PipeSolver::Hllc: return "hllc";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
inline PipeSolver parse_pipe_solver(std::string_view name) {
|
||||
if(name=="rusanov") return PipeSolver::Rusanov;
|
||||
if(name=="hlle") return PipeSolver::Hlle;
|
||||
if(name=="hllc") return PipeSolver::Hllc;
|
||||
throw std::invalid_argument("choose rusanov, hlle or hllc pipe solver");
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
@@ -1,16 +1,17 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Silencer.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include <string>
|
||||
#include "FluidSim/Junction.hpp"
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/MechanicalSoundProcessor.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include "FluidSim/MotorcycleTest.hpp"
|
||||
#include "FluidSim/Turbocharger.hpp"
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -19,34 +20,60 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, TurboTwin, AmericanV8 };
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, AmericanV8 };
|
||||
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
|
||||
|
||||
enum class EngineLayout { Inline, V, Boxer };
|
||||
struct EngineConfig {
|
||||
int cylinders{}; // zero selects the existing preset
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{60}, bore{.086f}, stroke{.086f}, rod{.145f}, compression{10};
|
||||
float idle_rpm{1000}, rev_limit{7000}, idle_opening{.045f};
|
||||
bool carbureted{};
|
||||
float carb_pilot_scale{1.f};
|
||||
float carb_jet_scale{1.f}; // multiplier on displacement-scaled effective jet diameters
|
||||
std::array<float,12> phase_degrees{}; // used when custom_phases is true
|
||||
bool custom_phases{};
|
||||
};
|
||||
|
||||
struct V8ExhaustConfig {
|
||||
float primary_length{.75f}, primary_diameter{.038f};
|
||||
bool h_pipe{true};
|
||||
float junction_volume{50e-6f}; // m3, 50 cc per fitting
|
||||
};
|
||||
|
||||
struct ScenarioSnapshot {
|
||||
bool turbo_enabled{};
|
||||
float boost_target{}, wastegate_duty{};
|
||||
TurboSnapshot turbo{};
|
||||
std::string example_name;
|
||||
EngineLayout layout{EngineLayout::Inline};
|
||||
float bank_angle{};
|
||||
bool v8_headers{};
|
||||
PipeSolver pipe_solver{PipeSolver::Rusanov};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 8> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{};
|
||||
std::array<float, 12> cylinder_angles{};
|
||||
std::array<float, 12> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{}, pipe_positivity_repairs{};
|
||||
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
|
||||
float cylinder_pressure1{}, cylinder_pressure2{};
|
||||
float cylinder_temperature1{}, cylinder_temperature2{};
|
||||
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
|
||||
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
|
||||
float intake_pressure{}, exhaust_pressure{};
|
||||
float intake_pressure{}, exhaust_pressure{}, second_bank_pressure{};
|
||||
std::array<float,2> crossover_pressures{};
|
||||
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
|
||||
bool racing_exhaust{};
|
||||
bool silencers{}, h_pipe{};
|
||||
ExhaustConfig exhaust_config{};
|
||||
float crankcase_pressure{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{};
|
||||
std::array<std::vector<GasState>, 22> pipe_states;
|
||||
std::array<std::vector<float>, 22> pipe_areas, pipe_lengths;
|
||||
bool rev_limit1{}, rev_limit2{};
|
||||
std::array<std::vector<GasState>, 32> pipe_states;
|
||||
std::array<std::vector<float>, 32> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
for (std::size_t k = 0; k < pipes.size(); ++k) {
|
||||
pipe_positivity_repairs += pipes[k].positivity_repairs;
|
||||
pipe_areas[k] = pipes[k].cell_areas;
|
||||
pipe_lengths[k] = pipes[k].cell_lengths;
|
||||
pipe_states[k].reserve(pipes[k].cells.size());
|
||||
@@ -60,17 +87,20 @@ struct ScenarioSnapshot {
|
||||
// Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8.
|
||||
class FourStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin,
|
||||
bool turbo_enabled = false);
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin, ExhaustConfig exhaust = {}, V8ExhaustConfig v8_exhaust = {}, EngineConfig engine = {});
|
||||
const FourStrokePreset preset;
|
||||
int cylinder_count() const { return preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 8> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8}; }
|
||||
const ExhaustConfig exhaust_config;
|
||||
const V8ExhaustConfig v8_exhaust_config;
|
||||
const EngineConfig engine_config;
|
||||
int cylinder_count() const { return engine_config.cylinders ? engine_config.cylinders : preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 12> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8,&cylinder9,&cylinder10,&cylinder11,&cylinder12}; }
|
||||
AudioFrame process();
|
||||
void set_controls(float throttle_value, float load_value, float clutch_value);
|
||||
void toggle_mechanical_sound();
|
||||
void set_audio_scene(OutdoorScene scene);
|
||||
void set_pipe_solver(PipeSolver solver);
|
||||
void set_monitor_filter(bool enabled);
|
||||
void set_starter(bool engaged);
|
||||
AudioFrame dry_audio() const;
|
||||
AudioFrame unfiltered_audio() const;
|
||||
void shift_up();
|
||||
@@ -80,34 +110,31 @@ public:
|
||||
ScenarioSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
bool mechanical_sound_enabled{};
|
||||
float mechanical_mix{0.02f};
|
||||
Ecu ecu{};
|
||||
std::unique_ptr<Turbocharger> turbo;
|
||||
Crankshaft crankshaft{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
|
||||
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
FourStrokeCylinder cylinder9{}, cylinder10{}, cylinder11{}, cylinder12{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
Volume0D second_bank_collector{1.5e-3f, 101325.0f, 700.0f};
|
||||
Junction exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
float last_mix_l{}, last_mix_r{};
|
||||
std::uint64_t sample_count{};
|
||||
|
||||
private:
|
||||
float intake_boundary_area_{}, throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 8> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 12> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float dt_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
OutdoorAudio audio_;
|
||||
MechanicalSoundProcessor mechanical_sound_;
|
||||
mutable std::mutex mutex_;
|
||||
std::vector<std::unique_ptr<Junction>> exhaust_junctions_;
|
||||
std::unique_ptr<Carburetor> carb_;
|
||||
Port* throttle_port_{};
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, bool turbo_enabled);
|
||||
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, const ExhaustConfig& exhaust, const V8ExhaustConfig& v8_exhaust, const EngineConfig& engine);
|
||||
};
|
||||
|
||||
using FourStrokeTwinScenario = FourStrokeScenario;
|
||||
@@ -116,17 +143,10 @@ public:
|
||||
explicit AmericanV8Scenario(int sample_rate = 44100)
|
||||
: FourStrokeScenario(sample_rate, FourStrokePreset::AmericanV8) {}
|
||||
};
|
||||
class TurboTwinScenario : public FourStrokeScenario {
|
||||
public:
|
||||
explicit TurboTwinScenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::Twin, true) {}
|
||||
};
|
||||
class SaabB205Scenario : public FourStrokeScenario {
|
||||
public:
|
||||
explicit SaabB205Scenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::SaabB205) {}
|
||||
};
|
||||
class SaabTurboScenario : public FourStrokeScenario {
|
||||
public:
|
||||
explicit SaabTurboScenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::SaabB205, true) {}
|
||||
};
|
||||
|
||||
|
||||
} // namespace fluidsim::port
|
||||
|
||||
83
include/FluidSim/ScenarioCatalog.hpp
Normal file
83
include/FluidSim/ScenarioCatalog.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/TwoStrokeConfig.hpp"
|
||||
#include <string>
|
||||
namespace fluidsim::port {
|
||||
struct ScenarioExample {
|
||||
std::string id,title,description;
|
||||
ScenarioKind kind;
|
||||
TwoStrokeConfig two{};
|
||||
ExhaustConfig exhaust{};
|
||||
V8ExhaustConfig v8{};
|
||||
EngineConfig engine{};
|
||||
std::string category;
|
||||
};
|
||||
inline std::vector<ScenarioExample> scenario_examples() {
|
||||
std::vector<ScenarioExample> examples;
|
||||
auto add=[&](const char* id,const char* title,const char* description,ScenarioKind kind)->ScenarioExample& {
|
||||
examples.push_back({id,title,description,kind,{},{},{},{},{}});return examples.back();
|
||||
};
|
||||
add("twin","600 cc parallel twin","EFI motorcycle baseline",ScenarioKind::Twin);
|
||||
add("yz-racing","YZ125 / expansion chamber","Racing pipe, fixed 4.5% carb idle",ScenarioKind::TwoStroke);
|
||||
add("yz-straight","YZ125 / straight exhaust","Same engine and carb, straight exhaust comparison",ScenarioKind::TwoStroke).two.exhaust=TwoStrokeExhaust::Straight;
|
||||
add("saab-straight","Saab B205 / straight-through","6 L resonator and 18 L straight-through rear muffler",ScenarioKind::SaabB205);
|
||||
auto& chambered=add("saab-chambered","Saab B205 / chambered","Expansion resonator and two-chamber rear muffler",ScenarioKind::SaabB205);
|
||||
chambered.exhaust.resonator.type=SilencerType::ExpansionResonator;
|
||||
chambered.exhaust.muffler.type=SilencerType::Chambered;chambered.exhaust.muffler.chambers=2;
|
||||
auto& resonator=add("saab-expansion","Saab B205 / expansion silencers","Two single expansion chambers, same body volumes",ScenarioKind::SaabB205);
|
||||
resonator.exhaust.resonator.type=SilencerType::ExpansionResonator;resonator.exhaust.muffler.type=SilencerType::ExpansionResonator;
|
||||
add("saab-open","Saab B205 / open exhaust","Silencers bypassed; naturally aspirated baseline",ScenarioKind::SaabB205).exhaust.enabled=false;
|
||||
add("v8-h","American 5.0 / 4-2-1 + H","750 mm primaries, 50 cc junctions, H crossover",ScenarioKind::AmericanV8);
|
||||
add("v8-dual","American 5.0 / independent banks","Same 4-2-1 headers with H crossover disconnected",ScenarioKind::AmericanV8).v8.h_pipe=false;
|
||||
add("v8-short","American 5.0 / short headers","600 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.60f;
|
||||
add("v8-long","American 5.0 / long headers","900 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_length=.90f;
|
||||
add("v8-large","American 5.0 / larger primaries","42 mm x 750 mm primaries with H crossover",ScenarioKind::AmericanV8).v8.primary_diameter=.042f;
|
||||
for(auto& e:examples) e.category=e.kind==ScenarioKind::TwoStroke?"Two-stroke / 125 cc":e.kind==ScenarioKind::SaabB205?"Inline four / Saab":e.kind==ScenarioKind::AmericanV8?"V8 / American 5.0":"Twins / parallel";
|
||||
auto chamber=[&](const char* id,const char* title,float length,float width) {
|
||||
auto& e=add(id,title,"Estimated chamber geometry / same 125 cc engine",ScenarioKind::TwoStroke);
|
||||
e.category="Two-stroke / 125 cc";
|
||||
for(auto& section:e.two.racing_pipe) {section.length*=length;section.inlet_diameter*=width;section.outlet_diameter*=width;}
|
||||
};
|
||||
chamber("yz-torque","125 / long torque chamber",1.18f,1.f);
|
||||
chamber("yz-short","125 / short high-RPM chamber",.82f,1.f);
|
||||
chamber("yz-fat","125 / wide belly chamber",1.f,1.18f);
|
||||
auto two=[&](const char* id,const char* title,const char* category,float bore,float stroke,float carb) {
|
||||
auto& e=add(id,title,"Concept single / transported carb fuel",ScenarioKind::TwoStroke);e.category=category;
|
||||
const float scale=std::cbrt((bore*bore*stroke)/(.054f*.054f*.0545f));
|
||||
e.two.jet_scale=bore<.05f?.5f:1.f;
|
||||
e.two.bore=bore;e.two.stroke=stroke;e.two.rod*=scale;e.two.carburetor_diameter=carb;
|
||||
e.two.transfer_width*=scale;e.two.exhaust_width*=scale;e.two.crankcase_clearance*=scale*scale*scale;
|
||||
e.two.minimum_burn_seconds*=scale;
|
||||
for(auto& section:e.two.racing_pipe) {section.length*=scale;section.inlet_diameter*=scale;section.outlet_diameter*=scale;}
|
||||
e.two.exhaust_cell_length=.015f;
|
||||
};
|
||||
two("fifty","50 cc / little screamer","Two-stroke / 50 cc",.040f,.0398f,.019f);
|
||||
two("fivehundred","500 cc / big single","Two-stroke / 500 cc",.086f,.086f,.041f);
|
||||
auto custom=[&](const char* id,const char* title,const char* category,int count,EngineLayout layout,float bore,float stroke)->ScenarioExample& {
|
||||
auto& e=add(id,title,"Estimated concept calibration / EFI",ScenarioKind::Twin);e.category=category;
|
||||
e.engine.cylinders=count;e.engine.layout=layout;e.engine.bore=bore;e.engine.stroke=stroke;
|
||||
e.engine.rod=std::max(.11f,stroke*1.8f);return e;
|
||||
};
|
||||
custom("single-450","450 cc / four-stroke thumper","Singles / four-stroke",1,EngineLayout::Inline,.096f,.062f);
|
||||
custom("single-650","650 cc / big thumper","Singles / four-stroke",1,EngineLayout::Inline,.100f,.083f).engine.rev_limit=6500;
|
||||
auto& twin270=custom("twin-270","700 cc / 270-degree parallel twin","Twins / parallel",2,EngineLayout::Inline,.080f,.068f);
|
||||
twin270.engine.custom_phases=true;twin270.engine.phase_degrees[1]=450;
|
||||
auto& ducati=custom("ltwin","916-inspired / 90-degree V twin","Twins / V",2,EngineLayout::V,.094f,.066f);
|
||||
ducati.engine.bank_angle=90;ducati.engine.custom_phases=true;ducati.engine.phase_degrees[1]=450;
|
||||
custom("triple","1.0 litre / inline triple","Inline three",3,EngineLayout::Inline,.079f,.068f);
|
||||
auto& four=custom("f20","F20C-inspired / 2.0 high-rev four","Inline four / specials",4,EngineLayout::Inline,.087f,.084f);four.engine.rev_limit=9000;
|
||||
custom("five","Audi-inspired / 2.2 inline five","Inline five",5,EngineLayout::Inline,.081f,.0864f);
|
||||
custom("rb26","RB26-inspired / 2.6 inline six NA","Inline six",6,EngineLayout::Inline,.086f,.0737f);
|
||||
custom("six-long","3.0 litre / long-stroke inline six","Inline six",6,EngineLayout::Inline,.086f,.086f);
|
||||
custom("busso","Busso-inspired / 3.0 V6","V6",6,EngineLayout::V,.093f,.0726f);
|
||||
auto& flat4=custom("ej20","EJ20-inspired / 2.0 flat four NA","Boxer / flat four",4,EngineLayout::Boxer,.092f,.075f);
|
||||
flat4.engine.bank_angle=180;
|
||||
custom("flat6","911-inspired / 3.0 flat six","Boxer / flat six",6,EngineLayout::Boxer,.095f,.0704f).engine.bank_angle=180;
|
||||
auto& big=custom("big-carb","8.0 litre / carbureted big-block","V8 / big-block",8,EngineLayout::V,.1143f,.098f);
|
||||
big.engine.carb_pilot_scale=.4f;big.engine.carb_jet_scale=.47f;big.engine.bank_angle=90;big.engine.compression=9;big.engine.carbureted=true;big.engine.idle_rpm=800;big.engine.idle_opening=.08f;
|
||||
big.description="Carb fuel transport / fixed configurable idle stop";
|
||||
custom("v10","5.0 litre / even-fire V10","V10",10,EngineLayout::V,.092f,.0752f).engine.bank_angle=72;
|
||||
custom("v12","250-inspired / 3.0 V12","V12",12,EngineLayout::V,.073f,.0588f).engine.bank_angle=60;
|
||||
return examples;
|
||||
}
|
||||
}
|
||||
54
include/FluidSim/Silencer.hpp
Normal file
54
include/FluidSim/Silencer.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include "FluidSim/PipeProfile.hpp"
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class SilencerType { StraightThrough, ExpansionResonator, Chambered };
|
||||
struct SilencerConfig {
|
||||
SilencerType type{SilencerType::Chambered};
|
||||
float volume{.018f}, length{.65f}, core_diameter{.055f};
|
||||
int chambers{2};
|
||||
float passage_diameter{.058f};
|
||||
float resistance{2.f}; // linear momentum decay, 1/s (packing surrogate)
|
||||
float loss_coefficient{.35f}; // total dimensionless quadratic flow resistance
|
||||
float diameter() const { return std::sqrt(4*volume/(pi*length)); }
|
||||
};
|
||||
struct ExhaustConfig {
|
||||
bool enabled{true};
|
||||
float inlet_length{1.4f}, outlet_length{.35f}, connecting_length{.8f}, diameter{.060f}, cell_length{.035f};
|
||||
SilencerConfig resonator{SilencerType::StraightThrough,.006f,.40f,.060f,1,.060f,2, .1f};
|
||||
SilencerConfig muffler{SilencerType::StraightThrough,.018f,.65f,.060f,1,.060f,4, .35f};
|
||||
};
|
||||
inline Pipe1D make_silenced_exhaust(const ExhaustConfig& c) {
|
||||
std::vector<PipeSection> sections;
|
||||
std::vector<float> linear, quadratic;
|
||||
auto add=[&](float length,float a,float b,float r=0,float k=0) {
|
||||
if(!std::isfinite(length)||length<=0||!std::isfinite(r)||r<0||!std::isfinite(k)||k<0)
|
||||
throw std::invalid_argument("invalid silencer resistance or length");
|
||||
sections.push_back({length,a,b}); linear.push_back(r); quadratic.push_back(k/length);
|
||||
};
|
||||
add(c.inlet_length,c.diameter,c.diameter);
|
||||
if(c.enabled) for(int index=0;index<2;++index) {
|
||||
if(index) add(c.connecting_length,c.diameter,c.diameter);
|
||||
const auto& s=index?c.muffler:c.resonator;
|
||||
if(!std::isfinite(s.volume)||s.volume<=0||!std::isfinite(s.length)||s.length<=0||s.chambers<1||s.chambers>16 || s.core_diameter<=0 || s.passage_diameter<=0)
|
||||
throw std::invalid_argument("invalid silencer geometry");
|
||||
const float d=s.type==SilencerType::StraightThrough?s.core_diameter:s.diameter();
|
||||
const int n=s.type==SilencerType::Chambered?s.chambers:1;
|
||||
float previous=c.diameter;
|
||||
for(int i=0;i<n;++i) {
|
||||
add(s.length*.1f/n,previous,d,s.resistance,s.loss_coefficient*.5f/n);
|
||||
add(s.length*.8f/n,d,d,s.resistance,0);
|
||||
const float end=i+1==n?c.diameter:s.passage_diameter;
|
||||
add(s.length*.1f/n,d,end,s.resistance,s.loss_coefficient*.5f/n);
|
||||
previous=end;
|
||||
}
|
||||
}
|
||||
add(c.outlet_length,c.diameter,c.diameter);
|
||||
auto pipe=make_profiled_pipe(sections,c.cell_length);
|
||||
for(std::size_t i=0;i<sections.size();++i) {
|
||||
const int n=int(std::ceil(sections[i].length/c.cell_length));
|
||||
for(int j=0;j<n;++j) { pipe.linear_resistance.push_back(linear[i]);pipe.quadratic_resistance.push_back(quadratic[i]); }
|
||||
}
|
||||
return pipe;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class ThreadLoadTracker {
|
||||
public:
|
||||
void record(double busy_milliseconds, double budget_milliseconds) {
|
||||
if (budget_milliseconds <= 0.0) return;
|
||||
constexpr double alpha = 0.05;
|
||||
const double instant = busy_milliseconds / budget_milliseconds * 100.0;
|
||||
const double previous = load_percent_.load(std::memory_order_relaxed);
|
||||
load_percent_.store(previous + alpha * (instant - previous), std::memory_order_relaxed);
|
||||
last_busy_ms_.store(busy_milliseconds, std::memory_order_relaxed);
|
||||
}
|
||||
double load_percent() const { return load_percent_.load(std::memory_order_relaxed); }
|
||||
double last_step_microseconds() const { return last_busy_ms_.load(std::memory_order_relaxed) * 1000.0; }
|
||||
private:
|
||||
std::atomic<double> load_percent_{};
|
||||
std::atomic<double> last_busy_ms_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
@@ -1,81 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// RHB31/VZ21-size mean-line approximation. Geometry, losses and coefficients
|
||||
// are estimates, not a digitized or validated manufacturer performance map.
|
||||
struct TurboConfig {
|
||||
static TurboConfig gt17(); // estimated Saab GT1752-size calibration
|
||||
double shaft_inertia{4e-6}; // kg m^2, compressor+turbine+shaft rigid rotor
|
||||
double compressor_radius{.018}, turbine_radius{.0135};
|
||||
double compressor_head_coefficient{.60}, compressor_efficiency{.65};
|
||||
double turbine_peak_efficiency{.65}, turbine_optimum_speed_ratio{.70};
|
||||
double compressor_effective_area{110e-6}, turbine_effective_area{100e-6};
|
||||
double bearing_viscous{1e-7}, windage{2e-11}; // Nm/(rad/s), Nm/(rad/s)^2
|
||||
// Estimated pressure-can/spring wastegate. Set maximum lift to zero for a
|
||||
// pure ungated turbo bench test. This changes bypass AREA, never boost.
|
||||
double gate_diaphragm_area{8e-4}, gate_spring_preload{40}, gate_spring_stiffness{6000};
|
||||
double gate_damping{120}, gate_diameter{.014}, gate_maximum_lift{.004};
|
||||
double reference_pressure{101325};
|
||||
float compressor_inlet_volume{20e-6f}, compressor_outlet_volume{30e-6f};
|
||||
float turbine_inlet_volume{30e-6f}, turbine_outlet_volume{20e-6f};
|
||||
std::array<float, 4> connection_diameters{.024f, .024f, .025f, .026f};
|
||||
};
|
||||
|
||||
struct TurboSnapshot {
|
||||
double rpm{}, angle{}, shaft_energy{}, turbine_power{}, compressor_power{}, bearing_power{};
|
||||
double compressor_flow{}, turbine_flow{}, compressor_pressure_ratio{}, turbine_pressure_ratio{};
|
||||
double turbine_inlet_pressure{}, turbine_outlet_pressure{}, charge_pressure{}, charge_temperature{};
|
||||
double bearing_heat{}, energy_residual{};
|
||||
std::uint64_t limited_transfers{};
|
||||
double wastegate_flow{}, wastegate_lift{};
|
||||
};
|
||||
|
||||
// Four pressure-storage nodes joined by turbine/nozzle and centrifugal-pump
|
||||
// characteristics. Existing pipe orifices exchange conserved mass/enthalpy.
|
||||
// The whole turbo is ONE solver component; do not also register its volumes.
|
||||
class Turbocharger final : public Component {
|
||||
public:
|
||||
explicit Turbocharger(TurboConfig config = {});
|
||||
const TurboConfig config;
|
||||
Volume0D compressor_inlet, compressor_outlet, turbine_inlet, turbine_outlet;
|
||||
Port& compressor_inlet_port;
|
||||
Port& compressor_outlet_port;
|
||||
Port& turbine_inlet_port;
|
||||
Port& turbine_outlet_port;
|
||||
|
||||
void connect(BoundarySystem& boundaries, const std::vector<Pipe1D>& pipes,
|
||||
int compressor_in, bool compressor_in_left, int compressor_out, bool compressor_out_left,
|
||||
int turbine_in, bool turbine_in_left, int turbine_out, bool turbine_out_left);
|
||||
std::vector<Port*> ports() override;
|
||||
void update_state(float dt) override;
|
||||
TurboSnapshot snapshot() const;
|
||||
double omega() const { return omega_; }
|
||||
double shaft_energy() const { return .5*config.shaft_inertia*omega_*omega_; }
|
||||
double fluid_energy() const;
|
||||
// Intended for standalone initialization/tests, never an imposed running speed.
|
||||
void set_initial_rpm(double rpm);
|
||||
void set_wastegate_duty(double duty) { gate_duty_ = std::isfinite(duty) ? std::clamp(duty, 0., .85) : 0.; }
|
||||
|
||||
private:
|
||||
std::array<Port*, 4> internal_;
|
||||
std::array<float, 4> connection_areas_{};
|
||||
double omega_{}, angle_{}, bearing_heat_{};
|
||||
double turbine_power_{}, compressor_power_{}, bearing_power_{}, energy_residual_{};
|
||||
double compressor_flow_{}, turbine_flow_{};
|
||||
double wastegate_flow_{}, wastegate_lift_{};
|
||||
std::uint64_t limited_transfers_{};
|
||||
bool connected_{};
|
||||
double gate_duty_{}; // pneumatic bleed fraction; zero restores spring boost
|
||||
static double nozzle(double upstream_pressure, double upstream_temperature,
|
||||
double downstream_pressure, double area);
|
||||
double transfer_flow(Volume0D& inlet, Volume0D& outlet, Port& inlet_external,
|
||||
Port& outlet_external, double requested, double dt);
|
||||
static void set_transfer(Volume0D& inlet, Volume0D& outlet, Port& inlet_internal,
|
||||
Port& outlet_internal, double flow, double shaft_work, double dt);
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
@@ -13,7 +13,10 @@ struct TwoStrokeConfig {
|
||||
float trapped_compression{8.2f}; // exhaust-closure volume / clearance volume
|
||||
float transfer_duration{130}, exhaust_duration{190};
|
||||
float transfer_width{.050f}, exhaust_width{.036f}; // summed effective window widths
|
||||
float jet_scale{1.f}; // multiplier on effective jet diameters
|
||||
float crankcase_clearance{150e-6f};
|
||||
float carburetor_diameter{.0381f};
|
||||
float idle_slide_opening{.045f}; // fixed mechanical stop, normalized 0..1 (4.5%)
|
||||
float minimum_burn_seconds{.0008f};
|
||||
// Empirical mechanical FMEP = A + B*mean_piston_speed + C*speed^2.
|
||||
// Generic correlation estimates, not a measured YZ125 coast-down fit.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/ReedValve.hpp"
|
||||
#include "FluidSim/MotorcycleTest.hpp"
|
||||
@@ -36,6 +35,7 @@ public:
|
||||
void shift_up() { vehicle.shift_up(); }
|
||||
void shift_down() { vehicle.shift_down(); }
|
||||
void set_neutral() { vehicle.set_neutral(); }
|
||||
void set_pipe_solver(PipeSolver solver) { for(auto& pipe : pipes) pipe.solver=solver; }
|
||||
void set_audio_scene(OutdoorScene scene) { audio_.set_scene(scene); }
|
||||
void set_monitor_filter(bool enabled) { audio_.set_monitor_filter(enabled); }
|
||||
AudioFrame dry_audio() const { return audio_.dry_frame(); }
|
||||
@@ -47,7 +47,6 @@ public:
|
||||
Ecu ecu{};
|
||||
Carburetor carburetor;
|
||||
ReedValve reed{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
Crankshaft crankshaft{};
|
||||
TwoStrokeCylinder cylinder;
|
||||
@@ -56,7 +55,7 @@ public:
|
||||
std::vector<Pipe1D> pipes;
|
||||
|
||||
private:
|
||||
float dt_{}, max_throttle_area_{}, intake_boundary_area_{};
|
||||
float dt_{}, max_throttle_area_{};
|
||||
float reed_in_area_{}, reed_out_area_{}, transfer_pipe_area_{}, transfer_valve_area_{}, exhaust_valve_area_{};
|
||||
bool reed_open_{};
|
||||
std::uint64_t step_count_{};
|
||||
|
||||
Reference in New Issue
Block a user