Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
@@ -24,6 +24,8 @@ public:
|
||||
|
||||
private:
|
||||
void update_port() {
|
||||
port.gamma_value = gamma_value;
|
||||
port.gas_constant_value = gas_constant_value;
|
||||
port.pressure = pressure;
|
||||
port.density = density();
|
||||
port.temperature = temperature;
|
||||
|
||||
@@ -12,6 +12,7 @@ class AudioOutputStream final : public sf::SoundStream {
|
||||
public:
|
||||
explicit AudioOutputStream(SimulationRingBuffer& ring, unsigned int sample_rate = 44100);
|
||||
void set_speed(double speed) { speed_.store(speed, std::memory_order_relaxed); }
|
||||
std::uint64_t underrun_blocks() const { return underrun_blocks_.load(std::memory_order_relaxed); }
|
||||
double speed() const { return speed_.load(std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
@@ -19,10 +20,16 @@ private:
|
||||
SimulationRingBuffer& ring_;
|
||||
std::array<float, block_size> temp_left_{}, temp_right_{};
|
||||
std::array<std::int16_t, block_size * 2> output_{};
|
||||
std::atomic<std::uint64_t> underrun_blocks_{};
|
||||
std::atomic<double> speed_{1.0};
|
||||
float previous_input_left_{}, previous_output_left_{};
|
||||
float previous_input_right_{}, previous_output_right_{};
|
||||
|
||||
static constexpr std::size_t transition_samples = 64;
|
||||
std::size_t transition_remaining_{};
|
||||
float last_left_{}, last_right_{}, transition_left_{}, transition_right_{};
|
||||
bool starving_{true};
|
||||
|
||||
bool onGetData(Chunk& data) override;
|
||||
void onSeek(sf::Time time_offset) override;
|
||||
};
|
||||
|
||||
@@ -10,21 +10,21 @@ struct Vec3 { float x{}, y{}, z{}; };
|
||||
// Flow-derivative sound source used by the C# two-stroke and pulse scenarios.
|
||||
class SoundProcessor {
|
||||
public:
|
||||
float gain{1.0f};
|
||||
float gain{0.005f};
|
||||
explicit SoundProcessor(int sample_rate = 44100, float listener_distance = 1.0f);
|
||||
float process(float mass_flow_out);
|
||||
private:
|
||||
float dt_{}, scale_factor_{}, lowpass_alpha_{}, derivative_alpha_{};
|
||||
float flow_lowpass_{}, previous_flow_{}, smooth_derivative_{};
|
||||
float dt_{}, scale_factor_{}, previous_flow_{};
|
||||
};
|
||||
|
||||
class RadiationSource {
|
||||
public:
|
||||
float flow_tau{0.005f}, smooth_tau{0.020f}, gain{15.0f};
|
||||
// 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{4000.0f}, speed_of_sound{343.0f};
|
||||
float directivity_balance{0.30f}, air_absorption_exponent{0.002f}, min_air_cutoff{1500.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);
|
||||
|
||||
@@ -10,7 +10,9 @@ struct Port {
|
||||
float pressure{101325.0f};
|
||||
float density{1.1768f};
|
||||
float temperature{300.0f};
|
||||
float gamma_value{1.4f}, gas_constant_value{287.0f};
|
||||
float air_fraction{1.0f};
|
||||
float* owner_energy{};
|
||||
float* owner_air_mass{};
|
||||
float* owner_exhaust_mass{};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
float temperature() const { return temperature_; }
|
||||
float density() const { return mass_ / (volume_ > 1e-12f ? volume_ : 1e-12f); }
|
||||
float volume() const { return volume_; }
|
||||
float geometry_work() const { return geometry_work_; } // J delivered by gas during the latest geometry step.
|
||||
float mass() const { return mass_; }
|
||||
float air_fraction() const { return air_fraction_; }
|
||||
|
||||
@@ -28,6 +29,7 @@ private:
|
||||
Crankshaft& crankshaft_;
|
||||
float crank_radius_{}, connecting_rod_length_{}, piston_area_{};
|
||||
float clearance_volume_{}, obliquity_{};
|
||||
float geometry_work_{};
|
||||
float mass_{}, energy_{}, air_fraction_{1.0f};
|
||||
float pressure_{101325.0f}, temperature_{300.0f}, volume_{}, previous_volume_{};
|
||||
|
||||
|
||||
87
include/FluidSim/DesktopScenario.hpp
Normal file
87
include/FluidSim/DesktopScenario.hpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Shared desktop controls/snapshot contract. Two-stroke access is serialized;
|
||||
// the twin already protects its own simulation state.
|
||||
class DesktopScenario {
|
||||
public:
|
||||
explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}) {
|
||||
if (kind == ScenarioKind::TwoStroke)
|
||||
two_ = std::make_unique<TwoStrokeScenario>(44100, config);
|
||||
else
|
||||
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
|
||||
FourStrokePreset::SaabB205 : FourStrokePreset::Twin, kind == ScenarioKind::TurboTwin);
|
||||
}
|
||||
AudioFrame process() {
|
||||
if (twin_)
|
||||
return twin_->process();
|
||||
const std::lock_guard lock(mutex_);
|
||||
return two_->process();
|
||||
}
|
||||
void set_controls(float pedal, float load, float clutch) {
|
||||
visit([&](auto& scenario) { scenario.set_controls(pedal, load, clutch); });
|
||||
}
|
||||
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_audio_scene(OutdoorScene scene) {
|
||||
visit([&](auto& scenario) { scenario.set_audio_scene(scene); });
|
||||
}
|
||||
ScenarioSnapshot snapshot() const {
|
||||
if (twin_)
|
||||
return twin_->snapshot();
|
||||
const std::lock_guard lock(mutex_);
|
||||
const auto source = two_->snapshot();
|
||||
ScenarioSnapshot s;
|
||||
s.two_stroke = true;
|
||||
s.cylinder_count = 1;
|
||||
s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing;
|
||||
s.rpm = source.rpm;
|
||||
s.speed_kmh = two_->vehicle.speed * 3.6f;
|
||||
s.kinetic_energy = two_->vehicle.kinetic_energy();
|
||||
s.gear = two_->vehicle.current_gear;
|
||||
s.clutch = two_->clutch;
|
||||
s.average_power = two_->crankshaft.average_power;
|
||||
s.average_torque = two_->crankshaft.average_torque;
|
||||
s.cylinder_pressure1 = source.cylinder_pressure;
|
||||
s.cylinder_temperature1 = two_->cylinder.temperature();
|
||||
s.cylinder_piston_fraction1 =
|
||||
(two_->cylinder.volume - two_->cylinder.clearance_volume()) / two_->cylinder.swept_volume();
|
||||
s.crankcase_pressure = source.crankcase_pressure;
|
||||
s.intake_pressure = source.intake_pressure;
|
||||
s.exhaust_pressure = two_->pipes[3].state(0).pressure;
|
||||
s.throttle = source.throttle;
|
||||
s.pedal = source.pedal;
|
||||
s.target_afr = two_->ecu.target_afr();
|
||||
s.valve1_intake_area = source.transfer_area;
|
||||
s.valve1_exhaust_area = source.exhaust_area;
|
||||
s.reed_open = source.reed_open;
|
||||
s.fuel_cut = source.fuel_cut;
|
||||
s.idle_control = two_->ecu.idle_active();
|
||||
s.rev_limit1 = two_->ecu.rev_limited();
|
||||
s.audio_scene = source.audio_scene;
|
||||
s.intake_flow = two_->intake_flow;
|
||||
s.exhaust_flow = two_->exhaust_flow;
|
||||
s.capture_pipes(two_->pipes);
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
template<class Function> void visit(Function&& function) {
|
||||
const std::lock_guard lock(mutex_);
|
||||
if (twin_) function(*twin_);
|
||||
else function(*two_);
|
||||
}
|
||||
std::unique_ptr<FourStrokeTwinScenario> twin_;
|
||||
std::unique_ptr<TwoStrokeScenario> two_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
38
include/FluidSim/Ecu.hpp
Normal file
38
include/FluidSim/Ecu.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
||||
class Ecu {
|
||||
public:
|
||||
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 rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
bool rev_limit_enabled{true};
|
||||
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
|
||||
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
||||
float stoichiometric_afr{14.7f}, power_afr{12.8f};
|
||||
|
||||
void step(float dt, float pedal, float rpm);
|
||||
float fuel_for_air(float trapped_air_mass) const;
|
||||
float throttle_opening() const { return opening_; }
|
||||
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_ || fault_; }
|
||||
bool rev_limited() const { return limiter_; }
|
||||
bool idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
|
||||
private:
|
||||
float filtered_rpm_{};
|
||||
bool initialized_{};
|
||||
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};
|
||||
bool limiter_{}, overrun_{}, fault_{}, idle_{};
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
@@ -8,11 +8,14 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Ecu;
|
||||
|
||||
class Crankshaft {
|
||||
public:
|
||||
float omega{400.0f * 2.0f * pi / 60.0f};
|
||||
float angle{}, previous_angle{}, inertia{0.2f};
|
||||
float friction_constant{}, friction_viscous{};
|
||||
float friction_quadratic{}; // Nm / (rad/s)^2, dissipative windage term
|
||||
float load_torque{}, drivetrain_torque{};
|
||||
float average_power{}, average_torque{};
|
||||
|
||||
@@ -27,8 +30,10 @@ private:
|
||||
|
||||
class EngineCylinder : public Component {
|
||||
public:
|
||||
Ecu* ecu{}; // Optional ideal electronic fuel/ignition controller.
|
||||
float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f};
|
||||
float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f};
|
||||
int intake_valve_count{1}, exhaust_valve_count{1};
|
||||
float intake_valve_lift{0.005f}, exhaust_valve_lift{0.005f};
|
||||
float phase{};
|
||||
float spark_advance{20.0f}, wiebe_a{5.0f}, wiebe_m{2.0f}, burn_duration{60.0f}, burn_start{5.0f};
|
||||
@@ -61,6 +66,9 @@ protected:
|
||||
|
||||
float random_unit();
|
||||
static float wrap_degrees(float value, float cycle_degrees = 720.0f);
|
||||
static bool crossed_angle(float previous, float current, float event) {
|
||||
return current >= previous ? previous < event && current >= event : event > previous || event <= current;
|
||||
}
|
||||
static bool crossed_window_close(float previous, float current, float open, float close);
|
||||
void update_rev_limiter(float rpm, float dt);
|
||||
void sync_ports();
|
||||
|
||||
@@ -17,18 +17,24 @@ public:
|
||||
float gas_constant_value = 287.0f, float gamma_value = 1.4f)
|
||||
: gamma(gamma_value), gas_constant(gas_constant_value), volume_(volume_m3),
|
||||
air_mass_(pressure * volume_m3 / (gas_constant_value * temperature)),
|
||||
energy_(pressure * volume_m3 / (gamma_value - 1.0f)) {}
|
||||
energy_(pressure * volume_m3 / (gamma_value - 1.0f)) {
|
||||
if (!std::isfinite(volume_m3) || volume_m3 <= 0 || !std::isfinite(pressure) || pressure <= 0 ||
|
||||
!std::isfinite(temperature) || temperature <= 0 || !std::isfinite(gas_constant_value) ||
|
||||
gas_constant_value <= 0 || !std::isfinite(gamma_value) || gamma_value <= 1)
|
||||
throw std::invalid_argument("volume thermodynamic state must be finite and positive, gamma > 1");
|
||||
}
|
||||
|
||||
float gamma{1.4f};
|
||||
float gas_constant{287.0f};
|
||||
float ambient_pressure{101325.0f};
|
||||
float ambient_temperature{300.0f};
|
||||
float energy_relaxation_rate{10.0f};
|
||||
float energy_relaxation_rate{0.0f};
|
||||
float volume_change_rate{};
|
||||
|
||||
Port& create_port() {
|
||||
Port &create_port() {
|
||||
ports_.emplace_back();
|
||||
auto& p = ports_.back();
|
||||
auto &p = ports_.back();
|
||||
p.owner_energy = &energy_;
|
||||
p.owner_air_mass = &air_mass_;
|
||||
p.owner_exhaust_mass = &exhaust_mass_;
|
||||
sync_port(p);
|
||||
@@ -39,61 +45,93 @@ public:
|
||||
float temperature() const { return pressure() / std::max(density() * gas_constant, 1e-9f); }
|
||||
float volume() const { return volume_; }
|
||||
float mass() const { return air_mass_ + exhaust_mass_; }
|
||||
float internal_energy() const { return energy_; }
|
||||
float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); }
|
||||
void set_pressure(float value, float temp = -1.0f) {
|
||||
if (!std::isfinite(value) || value <= 0 || !std::isfinite(temp))
|
||||
throw std::invalid_argument("invalid volume pressure/temperature");
|
||||
const float target_t = temp > 0.0f ? temp : temperature();
|
||||
const float total = value * volume_ / (gas_constant * target_t);
|
||||
const float fraction = air_fraction();
|
||||
air_mass_ = total * fraction;
|
||||
exhaust_mass_ = total * (1.0f - fraction);
|
||||
energy_ = value * volume_ / (gamma - 1.0f);
|
||||
air_roundoff_ = exhaust_roundoff_ = energy_roundoff_ = 0;
|
||||
sync_ports();
|
||||
}
|
||||
void update(float dt) {
|
||||
if (dt <= 0.0f) return;
|
||||
float d_air = 0.0f, d_exhaust = 0.0f, d_energy = 0.0f;
|
||||
if (!std::isfinite(dt) || dt < 0)
|
||||
throw std::invalid_argument("invalid volume timestep");
|
||||
if (dt == 0)
|
||||
return;
|
||||
double d_air = 0, d_exhaust = 0, d_energy = 0;
|
||||
const float fraction = air_fraction();
|
||||
for (const Port& p : ports_) {
|
||||
for (const Port &p : ports_) {
|
||||
const float flow_fraction = p.mass_flow >= 0.0f ? p.air_fraction : fraction;
|
||||
d_air += p.mass_flow * flow_fraction;
|
||||
d_exhaust += p.mass_flow * (1.0f - flow_fraction);
|
||||
d_energy += p.mass_flow * p.enthalpy;
|
||||
d_air += double(p.mass_flow) * flow_fraction;
|
||||
d_exhaust += double(p.mass_flow) * (1.0f - flow_fraction);
|
||||
d_energy += double(p.mass_flow) * p.enthalpy;
|
||||
}
|
||||
air_mass_ += d_air * dt;
|
||||
exhaust_mass_ += d_exhaust * dt;
|
||||
energy_ += (d_energy - pressure() * volume_change_rate) * dt;
|
||||
accumulate(air_mass_, air_roundoff_, d_air * dt);
|
||||
accumulate(exhaust_mass_, exhaust_roundoff_, d_exhaust * dt);
|
||||
accumulate(energy_, energy_roundoff_, d_energy * dt);
|
||||
const float new_volume = volume_ + volume_change_rate * dt;
|
||||
if (!(new_volume > 0) || !std::isfinite(new_volume))
|
||||
throw std::runtime_error("invalid moving volume");
|
||||
if (new_volume != volume_) {
|
||||
const double scale = std::pow(double(volume_) / new_volume, gamma - 1.0f);
|
||||
const double precise = (double(energy_)+energy_roundoff_)*scale;
|
||||
energy_ = float(precise);
|
||||
energy_roundoff_ = precise-energy_;
|
||||
}
|
||||
volume_ = new_volume;
|
||||
if (energy_relaxation_rate > 0.0f && mass() > 1e-12f) {
|
||||
const float target = mass() * gas_constant * ambient_temperature / (gamma - 1.0f);
|
||||
energy_ = target + (energy_ - target) * std::exp(-energy_relaxation_rate * dt);
|
||||
energy_roundoff_ = 0;
|
||||
}
|
||||
if (mass() < 1e-9f || energy_ < 0.0f) {
|
||||
air_mass_ = 1e-9f;
|
||||
exhaust_mass_ = 0.0f;
|
||||
energy_ = ambient_pressure * volume_ / (gamma - 1.0f);
|
||||
}
|
||||
if (!(mass() > 0) || !(energy_ > 0) || !std::isfinite(mass()) || !std::isfinite(energy_))
|
||||
throw std::runtime_error(
|
||||
"nonphysical volume state; reduce timestep or check external port fluxes");
|
||||
if (air_mass_ < -1e-12f || exhaust_mass_ < -1e-12f)
|
||||
throw std::runtime_error("negative volume species mass");
|
||||
air_mass_ = std::max(air_mass_, 0.0f);
|
||||
exhaust_mass_ = std::max(exhaust_mass_, 0.0f);
|
||||
sync_ports();
|
||||
}
|
||||
void update_state(float dt) override { update(dt); }
|
||||
std::vector<Port*> ports() override {
|
||||
std::vector<Port*> result;
|
||||
std::vector<Port *> ports() override {
|
||||
std::vector<Port *> result;
|
||||
result.reserve(ports_.size());
|
||||
for (Port& port : ports_) result.push_back(&port);
|
||||
for (Port &port : ports_)
|
||||
result.push_back(&port);
|
||||
return result;
|
||||
}
|
||||
const std::deque<Port>& ports() const { return ports_; }
|
||||
const std::deque<Port> &ports() const { return ports_; }
|
||||
|
||||
private:
|
||||
float volume_, air_mass_{}, exhaust_mass_{}, energy_;
|
||||
// Preserve sub-ulp transfers without changing the float port/budget ABI.
|
||||
double air_roundoff_{}, exhaust_roundoff_{}, energy_roundoff_{};
|
||||
static void accumulate(float& value, double& roundoff, double increment) {
|
||||
const double precise = double(value)+roundoff+increment;
|
||||
value = float(precise);
|
||||
roundoff = precise-value;
|
||||
}
|
||||
std::deque<Port> ports_;
|
||||
void sync_port(Port& p) const {
|
||||
void sync_port(Port &p) const {
|
||||
p.gamma_value = gamma;
|
||||
p.gas_constant_value = gas_constant;
|
||||
p.pressure = pressure();
|
||||
p.density = density();
|
||||
p.temperature = temperature();
|
||||
p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f);
|
||||
p.air_fraction = air_fraction();
|
||||
}
|
||||
void sync_ports() { for (Port& p : ports_) sync_port(p); }
|
||||
void sync_ports() {
|
||||
for (Port &p : ports_)
|
||||
sync_port(p);
|
||||
}
|
||||
};
|
||||
|
||||
struct GasCell {
|
||||
@@ -101,286 +139,489 @@ struct GasCell {
|
||||
float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))};
|
||||
float air{1.0f};
|
||||
};
|
||||
struct GasState { float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f}; };
|
||||
struct GasState {
|
||||
float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f};
|
||||
};
|
||||
|
||||
class Pipe1D {
|
||||
public:
|
||||
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air, positive along +x.
|
||||
struct Flux {
|
||||
float m{}, p{}, e{}, y{};
|
||||
};
|
||||
float area{}, dx{};
|
||||
float damping_multiplier{1.0f}, energy_relaxation_rate{0.5f}, ambient_pressure{101325.0f};
|
||||
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
|
||||
std::vector<float> cell_areas, cell_lengths;
|
||||
std::vector<GasCell> cells;
|
||||
GasState left_ghost{}, right_ghost{};
|
||||
Pipe1D(float area_m2, float length_m, int cell_count)
|
||||
: area(area_m2), dx(length_m / static_cast<float>(cell_count)),
|
||||
cell_areas(static_cast<std::size_t>(cell_count), area_m2),
|
||||
cell_lengths(static_cast<std::size_t>(cell_count), dx),
|
||||
cells(static_cast<std::size_t>(cell_count)), next_cells_(static_cast<std::size_t>(cell_count)) {}
|
||||
Pipe1D(std::vector<float> areas_m2, std::vector<float> lengths_m)
|
||||
: area(areas_m2.empty() ? 0.0f : areas_m2.front()),
|
||||
dx(lengths_m.empty() ? 0.0f : lengths_m.front()),
|
||||
cell_areas(std::move(areas_m2)), cell_lengths(std::move(lengths_m)),
|
||||
cells(cell_areas.size()), next_cells_(cell_areas.size()) {
|
||||
if (cell_areas.empty() || cell_lengths.size() != cell_areas.size())
|
||||
throw std::invalid_argument("pipe area and length profiles must be non-empty and have equal length");
|
||||
for (std::size_t i = 0; i < cell_areas.size(); ++i) {
|
||||
if (cell_areas[i] <= 0.0f || cell_lengths[i] <= 0.0f)
|
||||
throw std::invalid_argument("pipe cell area and length must be positive");
|
||||
std::uint64_t positivity_repairs{};
|
||||
Pipe1D(float a, float length, int count)
|
||||
: Pipe1D(uniform(a, count), uniform(length / std::max(count, 1), count)) {}
|
||||
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) {
|
||||
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)
|
||||
if (!std::isfinite(cell_areas[i]) || !std::isfinite(cell_lengths[i]) || cell_areas[i] <= 0 ||
|
||||
cell_lengths[i] <= 0)
|
||||
throw std::invalid_argument("pipe geometry must be finite and positive");
|
||||
}
|
||||
GasState state(int i) const { return primitive(cells.at(static_cast<std::size_t>(i))); }
|
||||
float cell_mass(int i) const { return cells.at(i).rho * cell_areas.at(i) * cell_lengths.at(i); }
|
||||
float cell_area(int i) const { return cell_areas.at(i); }
|
||||
float cell_length(int i) const { return cell_lengths.at(i); }
|
||||
float minimum_cell_length() const { return *std::min_element(cell_lengths.begin(), cell_lengths.end()); }
|
||||
float stable_timestep(float cfl = 0.8f) const {
|
||||
float dt = std::numeric_limits<float>::max();
|
||||
for (std::size_t i = 0; i < cells.size(); ++i) {
|
||||
auto s = primitive(cells[i]);
|
||||
if (!(s.rho > 0) || !(s.pressure > 0) || !std::isfinite(s.velocity) || !std::isfinite(s.pressure))
|
||||
throw std::runtime_error("nonphysical pipe state in CFL calculation");
|
||||
dt = std::min(dt, cfl * cell_lengths[i] / wave(s));
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
// Solver preparation is valid until advance; boundary resolution changes
|
||||
// fluxes/ghosts, not cells. Standalone advance still prepares its own cache.
|
||||
float prepare_step(float cfl = 0.8f) {
|
||||
float dt = std::numeric_limits<float>::max();
|
||||
for (std::size_t i = 0; i < cells.size(); ++i) {
|
||||
states_[i] = primitive(cells[i]);
|
||||
const auto &s = states_[i];
|
||||
if (!(s.rho > 0) || !(s.pressure > 0) || !std::isfinite(s.velocity) || !std::isfinite(s.pressure))
|
||||
throw std::runtime_error("nonphysical pipe state in preparation");
|
||||
waves_[i] = wave(s);
|
||||
dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]);
|
||||
}
|
||||
prepared_ = true;
|
||||
return dt;
|
||||
}
|
||||
int required_substeps(float dt, float cfl = 0.8f) const {
|
||||
if (!std::isfinite(dt) || dt < 0 || !std::isfinite(cfl) || cfl <= 0 || cfl > 1)
|
||||
throw std::invalid_argument("invalid timestep/CFL");
|
||||
const double n = std::ceil(double(dt) / stable_timestep(cfl));
|
||||
if (n > 1000000)
|
||||
throw std::runtime_error("excessive pipe substeps");
|
||||
return std::max(1, static_cast<int>(n));
|
||||
}
|
||||
void set_boundary_flux(bool left, Flux f) {
|
||||
if (left) {
|
||||
left_flux_ = f;
|
||||
left_override_ = true;
|
||||
} else {
|
||||
right_flux_ = f;
|
||||
right_override_ = true;
|
||||
}
|
||||
}
|
||||
GasState state(int idx) const {
|
||||
const auto& c = cells.at(static_cast<std::size_t>(idx));
|
||||
const float rho = c.rho;
|
||||
const float u = c.momentum / std::max(rho, 1e-12f);
|
||||
const float p = pipe_gm1 * (c.energy - 0.5f * c.momentum * u);
|
||||
return {rho, u, p, std::clamp(c.air, 0.0f, 1.0f)};
|
||||
static GasState primitive(const GasCell &c) {
|
||||
const float u = c.momentum / std::max(c.rho, 1e-12f);
|
||||
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), std::clamp(c.air, 0.0f, 1.0f)};
|
||||
}
|
||||
float cell_mass(int idx) const {
|
||||
const auto cell = static_cast<std::size_t>(idx);
|
||||
return cells.at(cell).rho * cell_areas.at(cell) * cell_lengths.at(cell);
|
||||
static float wave(const GasState &s) {
|
||||
return std::abs(s.velocity) + std::sqrt(gamma * s.pressure / s.rho);
|
||||
}
|
||||
float cell_area(int idx) const { return cell_areas.at(static_cast<std::size_t>(idx)); }
|
||||
float cell_length(int idx) const { return cell_lengths.at(static_cast<std::size_t>(idx)); }
|
||||
float minimum_cell_length() const {
|
||||
return *std::min_element(cell_lengths.begin(), cell_lengths.end());
|
||||
}
|
||||
int required_substeps(float dt, float target_cfl = 0.8f) const {
|
||||
float maximum_wave_speed = 0.0f;
|
||||
for (int i = 0; i < static_cast<int>(cells.size()); ++i) {
|
||||
const GasState s = state(i);
|
||||
maximum_wave_speed = std::max(maximum_wave_speed,
|
||||
std::abs(s.velocity) + std::sqrt(gamma * s.pressure / std::max(s.rho, 1e-12f)));
|
||||
}
|
||||
return std::max(1, static_cast<int>(std::ceil(dt * maximum_wave_speed /
|
||||
(target_cfl * std::max(minimum_cell_length(), 1e-12f)))));
|
||||
static Flux riemann(const GasState &a, const GasState &b) {
|
||||
return riemann(a, b, std::max(wave(a), wave(b)));
|
||||
}
|
||||
void advance(float dt) {
|
||||
const int n = static_cast<int>(cells.size());
|
||||
if (n == 0 || dt <= 0.0f) return;
|
||||
const float ambient_energy = ambient_pressure * pipe_gm1_inverse;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const GasState l = i == 0 ? left_ghost : flux_state(i - 1), r = flux_state(i);
|
||||
const GasState a = flux_state(i), b = i == n - 1 ? right_ghost : flux_state(i + 1);
|
||||
const Flux fl = flux(l, r), fr = flux(a, b);
|
||||
const float factor = dt / cell_lengths[static_cast<std::size_t>(i)];
|
||||
if (dt <= 0)
|
||||
return;
|
||||
if (!std::isfinite(dt))
|
||||
throw std::invalid_argument("nonfinite pipe timestep");
|
||||
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_[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]));
|
||||
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];
|
||||
const float factor = dt / (cell_areas[i] * cell_lengths[i]);
|
||||
const float al = i ? .5f * (cell_areas[i - 1] + cell_areas[i]) : cell_areas[i];
|
||||
const float ar = i + 1 < n ? .5f * (cell_areas[i] + cell_areas[i + 1]) : cell_areas[i];
|
||||
float rho = cells[i].rho - factor * (fr.m - fl.m);
|
||||
float mom = cells[i].momentum - factor * (fr.p - fl.p);
|
||||
float mom = cells[i].momentum - factor * (fr.p - fl.p - states_[i].pressure * (ar - al));
|
||||
float en = cells[i].energy - factor * (fr.e - fl.e);
|
||||
const float rho_air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(rho_air)) {
|
||||
next_cells_[i] = cells[i];
|
||||
continue;
|
||||
const float air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(air))
|
||||
throw std::runtime_error("nonfinite finite-volume update");
|
||||
if (rho < 1e-12f) {
|
||||
rho = 1e-12f;
|
||||
++positivity_repairs;
|
||||
}
|
||||
rho = std::max(rho, 1e-12f);
|
||||
const float damping = std::exp(-0.1f * damping_multiplier * dt /
|
||||
std::max(cells[i].rho, 1e-12f));
|
||||
mom *= damping;
|
||||
if (energy_relaxation_rate > 0.0f) {
|
||||
const float relax = std::exp(-energy_relaxation_rate * dt);
|
||||
en = ambient_energy + (en - ambient_energy) * relax;
|
||||
if (damping_multiplier > 0)
|
||||
mom *= std::exp(-.1f * damping_multiplier * dt / cells[i].rho);
|
||||
if (energy_relaxation_rate > 0)
|
||||
en = ambient_pressure * pipe_gm1_inverse +
|
||||
(en - ambient_pressure * pipe_gm1_inverse) * relaxation;
|
||||
const float minimum = 1e-6f * pipe_gm1_inverse + .5f * mom * mom / rho;
|
||||
if (en < minimum) {
|
||||
en = minimum;
|
||||
++positivity_repairs;
|
||||
}
|
||||
en = std::max(en, 100.0f * pipe_gm1_inverse + 0.5f * mom * mom / rho);
|
||||
next_cells_[i] = {rho, mom, en, std::clamp(rho_air / rho, 0.0f, 1.0f)};
|
||||
next_cells_[i] = {rho, mom, en, std::clamp(air / rho, 0.0f, 1.0f)};
|
||||
}
|
||||
cells.swap(next_cells_);
|
||||
prepared_ = false;
|
||||
left_override_ = right_override_ = false;
|
||||
}
|
||||
static Flux scaled(Flux f, float a) { return {f.m * a, f.p * a, f.e * a, f.y * a}; }
|
||||
|
||||
private:
|
||||
struct Flux { float m{}, p{}, e{}, y{}; };
|
||||
std::vector<GasCell> next_cells_;
|
||||
GasState flux_state(int idx) const {
|
||||
const auto& c = cells[static_cast<std::size_t>(idx)];
|
||||
const float rho = c.rho;
|
||||
const float inverse_rho = 1.0f / std::max(rho, 1e-12f);
|
||||
const float u = c.momentum * inverse_rho;
|
||||
const float p = pipe_gm1 * (c.energy - 0.5f * c.momentum * u);
|
||||
return {rho, u, p, std::clamp(c.air, 0.0f, 1.0f)};
|
||||
std::vector<GasState> states_;
|
||||
std::vector<float> waves_;
|
||||
std::vector<Flux> faces_;
|
||||
Flux left_flux_{}, right_flux_{};
|
||||
bool left_override_{}, right_override_{}, prepared_{};
|
||||
static std::vector<float> uniform(float v, int n) {
|
||||
if (n <= 0)
|
||||
throw std::invalid_argument("pipe count must be positive");
|
||||
return std::vector<float>(n, v);
|
||||
}
|
||||
static Flux flux(const GasState& a, const GasState& b) {
|
||||
const float ua = a.velocity, ub = b.velocity;
|
||||
const float inv_rho_a = 1.0f / std::max(a.rho, 1e-12f);
|
||||
const float inv_rho_b = 1.0f / std::max(b.rho, 1e-12f);
|
||||
const float ea = a.pressure * pipe_gm1_inverse * inv_rho_a + 0.5f * ua * ua;
|
||||
const float eb = b.pressure * pipe_gm1_inverse * inv_rho_b + 0.5f * ub * ub;
|
||||
const float c = std::max(std::abs(ua) + std::sqrt(gamma * a.pressure * inv_rho_a),
|
||||
std::abs(ub) + std::sqrt(gamma * b.pressure * inv_rho_b));
|
||||
return {
|
||||
0.5f * (a.rho * ua + b.rho * ub) - 0.5f * c * (b.rho - a.rho),
|
||||
0.5f * (a.rho * ua * ua + a.pressure + b.rho * ub * ub + b.pressure) - 0.5f * c * (b.rho * ub - a.rho * ua),
|
||||
0.5f * ((a.rho * ea + a.pressure) * ua + (b.rho * eb + b.pressure) * ub) - 0.5f * c * (b.rho * eb - a.rho * ea),
|
||||
0.5f * (a.rho * ua * a.air + b.rho * ub * b.air) - 0.5f * c * (b.rho * b.air - a.rho * a.air)
|
||||
};
|
||||
static Flux riemann(const GasState &a, const GasState &b, float c) {
|
||||
const float ma = a.rho * a.velocity, mb = b.rho * b.velocity;
|
||||
const float ea = a.pressure * pipe_gm1_inverse + .5f * ma * a.velocity;
|
||||
const float eb = b.pressure * pipe_gm1_inverse + .5f * mb * b.velocity;
|
||||
return {.5f * (ma + mb - c * (b.rho - a.rho)),
|
||||
.5f * (ma * a.velocity + a.pressure + mb * b.velocity + b.pressure - c * (mb - ma)),
|
||||
.5f * ((ea + a.pressure) * a.velocity + (eb + b.pressure) * b.velocity - c * (eb - ea)),
|
||||
.5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air))};
|
||||
}
|
||||
};
|
||||
|
||||
class BoundarySystem {
|
||||
public:
|
||||
explicit BoundarySystem(std::vector<Pipe1D>& pipes) : pipes_(pipes) {}
|
||||
void add_orifice(Port& port, int pipe, bool left, float& area, float cd = 1.0f) {
|
||||
Orifice desc;
|
||||
desc.port = &port;
|
||||
desc.pipe = pipe;
|
||||
desc.left = left;
|
||||
desc.area = &area;
|
||||
desc.cd = cd;
|
||||
orifices_.push_back(desc);
|
||||
explicit BoundarySystem(std::vector<Pipe1D> &pipes) : pipes_(pipes) {}
|
||||
void add_orifice(Port &port, int pipe, bool left, float &area, float cd = 1.0f) {
|
||||
if (std::abs(port.gamma_value - gamma) > 1e-6f ||
|
||||
std::abs(port.gas_constant_value - gas_constant) > .001f)
|
||||
throw std::invalid_argument("pipe coupling requires the same calorically perfect gas");
|
||||
if (!std::isfinite(cd) || cd < 0 || cd > 1)
|
||||
throw std::invalid_argument("orifice Cd must be in [0,1]");
|
||||
for (auto &o : orifices_)
|
||||
if (o.port == &port)
|
||||
throw std::invalid_argument("each connection requires a distinct component port");
|
||||
claim(pipe, left);
|
||||
Orifice o;
|
||||
o.port = &port;
|
||||
o.pipe = pipe;
|
||||
o.left = left;
|
||||
o.area = &area;
|
||||
o.cd = cd;
|
||||
if (port.owner_air_mass) {
|
||||
auto it = std::find_if(budgets_.begin(), budgets_.end(),
|
||||
[&](const Budget &b) { return b.air == port.owner_air_mass; });
|
||||
if (it == budgets_.end()) {
|
||||
o.budget = budgets_.size();
|
||||
budgets_.push_back({port.owner_air_mass, port.owner_exhaust_mass, port.owner_energy});
|
||||
} else
|
||||
o.budget = static_cast<std::size_t>(it - budgets_.begin());
|
||||
}
|
||||
orifices_.push_back(o);
|
||||
}
|
||||
void add_orifice_with_inertance(Port& port, int pipe, bool left, float& area,
|
||||
float cd, float effective_length, float loss_coefficient = 0.0f) {
|
||||
add_orifice(port, pipe, left, area, cd);
|
||||
auto& desc = orifices_.back();
|
||||
desc.use_inertance = true;
|
||||
desc.effective_length = effective_length;
|
||||
desc.loss_coefficient = loss_coefficient;
|
||||
void add_orifice_with_inertance(Port &p, int pipe, bool left, float &a, float cd, float length,
|
||||
float loss = 0) {
|
||||
if (!std::isfinite(length) || length <= 0 || !std::isfinite(loss) || loss < 0)
|
||||
throw std::invalid_argument("invalid inertance");
|
||||
add_orifice(p, pipe, left, a, cd);
|
||||
auto &o = orifices_.back();
|
||||
o.length = length;
|
||||
o.loss = loss;
|
||||
}
|
||||
void add_closed_end(int pipe, bool left) {
|
||||
Orifice desc;
|
||||
desc.pipe = pipe;
|
||||
desc.left = left;
|
||||
desc.closed_end = true;
|
||||
orifices_.push_back(desc);
|
||||
claim(pipe, left);
|
||||
Orifice o;
|
||||
o.pipe = pipe;
|
||||
o.left = left;
|
||||
orifices_.push_back(o);
|
||||
}
|
||||
void add_open_end(int pipe, bool left, float p, float a) {
|
||||
if (!std::isfinite(p) || p <= 0 || !std::isfinite(a) || a <= 0)
|
||||
throw std::invalid_argument("invalid open boundary");
|
||||
claim(pipe, left);
|
||||
opens_.push_back({pipe, left, p, a});
|
||||
}
|
||||
// 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() ||
|
||||
static_cast<std::size_t>(b) >= pipes_.size())
|
||||
throw std::invalid_argument("invalid connection pipe index");
|
||||
const float aa = pipes_[a].cell_area(end(pipes_[a], left_a));
|
||||
const float ab = pipes_[b].cell_area(end(pipes_[b], left_b));
|
||||
if (std::abs(aa - ab) > 1e-6f * std::max(aa, ab))
|
||||
throw std::invalid_argument("unequal-area junction needs a profiled pipe transition");
|
||||
if (a == b && left_a == left_b)
|
||||
throw std::invalid_argument("cannot join a face to itself");
|
||||
check_face(a, left_a);
|
||||
check_face(b, left_b);
|
||||
claim(a, left_a);
|
||||
claim(b, left_b);
|
||||
connections_.push_back({a, b, left_a, left_b});
|
||||
}
|
||||
float open_end_mass_flow(std::size_t i) const { return opens_.at(i).last_mass_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 {
|
||||
if (claims_.size() != 2 * pipes_.size())
|
||||
throw std::runtime_error("every pipe endpoint needs an explicit boundary or connection");
|
||||
}
|
||||
void add_open_end(int pipe, bool left, float pressure, float area) { opens_.push_back({pipe, left, pressure, area}); }
|
||||
float open_end_mass_flow(std::size_t index) const { return opens_.at(index).last_mass_flow; }
|
||||
float open_end_pressure(std::size_t index) const { return opens_.at(index).last_face_pressure; }
|
||||
void resolve(float dt) {
|
||||
for (auto& o : orifices_) resolve_orifice(o, dt);
|
||||
for (auto& o : opens_) resolve_open_end(o, dt);
|
||||
if (!std::isfinite(dt) || dt <= 0)
|
||||
throw std::invalid_argument("boundary timestep must be positive");
|
||||
for (auto &b : budgets_) {
|
||||
b.mass = .25f * (*b.air + (b.exhaust ? *b.exhaust : 0));
|
||||
b.heat = b.energy ? .25f * (*b.energy) : std::numeric_limits<float>::max();
|
||||
}
|
||||
for (auto &o : orifices_)
|
||||
resolve_orifice(o, dt);
|
||||
for (auto &o : opens_)
|
||||
resolve_open(o);
|
||||
for (auto &o : connections_)
|
||||
resolve_connection(o);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Budget {
|
||||
float *air{}, *exhaust{}, *energy{};
|
||||
float mass{}, heat{};
|
||||
};
|
||||
struct Orifice {
|
||||
Port* port{};
|
||||
Port *port{};
|
||||
int pipe{};
|
||||
bool left{};
|
||||
float* area{};
|
||||
float cd{1.0f};
|
||||
bool closed_end{}, use_inertance{};
|
||||
float effective_length{}, loss_coefficient{}, current_mass_flow{};
|
||||
float *area{};
|
||||
float cd{1}, length{}, loss{}, mdot{};
|
||||
std::size_t budget{std::numeric_limits<std::size_t>::max()};
|
||||
};
|
||||
struct Open { int pipe; bool left; float pressure, area; float last_mass_flow{}, last_face_pressure{}; };
|
||||
struct OrificeSolution { float mass_flow{}, rho{}, velocity{}, pressure{}; };
|
||||
std::vector<Pipe1D>& pipes_;
|
||||
struct Open {
|
||||
int pipe;
|
||||
bool left;
|
||||
float pressure, area, last_mass_flow{}, last_face_pressure{};
|
||||
};
|
||||
struct Connection {
|
||||
int a, b;
|
||||
bool left_a, left_b;
|
||||
};
|
||||
struct Claim {
|
||||
int pipe;
|
||||
bool left;
|
||||
};
|
||||
std::vector<Pipe1D> &pipes_;
|
||||
std::vector<Orifice> orifices_;
|
||||
std::vector<Open> opens_;
|
||||
|
||||
static OrificeSolution isentropic(float pup, float rho_up, float t_up, float pdown, float area, float cd) {
|
||||
OrificeSolution out{0.0f, rho_up, 0.0f, pup};
|
||||
if (area <= 0.0f || pup <= 0.0f || rho_up <= 0.0f || t_up <= 0.0f) return out;
|
||||
float ratio = std::clamp(pdown / pup, 1e-6f, 1.0f);
|
||||
const float critical = std::pow(2.0f / (gamma + 1.0f), gamma / (gamma - 1.0f));
|
||||
ratio = std::max(ratio, critical);
|
||||
const float mach = std::sqrt(std::max(0.0f, 2.0f / (gamma - 1.0f) *
|
||||
(std::pow(ratio, -(gamma - 1.0f) / gamma) - 1.0f)));
|
||||
const float a_up = std::sqrt(gamma * gas_constant * t_up);
|
||||
out.velocity = mach * a_up;
|
||||
out.rho = rho_up * std::pow(ratio, 1.0f / gamma);
|
||||
out.pressure = pup * ratio;
|
||||
out.mass_flow = out.rho * out.velocity * area * cd;
|
||||
return out;
|
||||
std::vector<Connection> connections_;
|
||||
std::vector<Budget> budgets_;
|
||||
std::vector<Claim> claims_;
|
||||
void check_face(int pipe, bool left) const {
|
||||
if (pipe < 0 || static_cast<std::size_t>(pipe) >= pipes_.size())
|
||||
throw std::invalid_argument("invalid boundary pipe index");
|
||||
for (auto c : claims_)
|
||||
if (c.pipe == pipe && c.left == left)
|
||||
throw std::invalid_argument("pipe face already connected");
|
||||
}
|
||||
static float available_mass(const Port& port) {
|
||||
return (port.owner_air_mass ? *port.owner_air_mass : 0.0f) +
|
||||
(port.owner_exhaust_mass ? *port.owner_exhaust_mass : 0.0f);
|
||||
void claim(int pipe, bool left) {
|
||||
check_face(pipe, left);
|
||||
claims_.push_back({pipe, left});
|
||||
}
|
||||
void resolve_orifice(Orifice& o, float dt) {
|
||||
Pipe1D& pipe = pipes_.at(static_cast<std::size_t>(o.pipe));
|
||||
const int idx = o.left ? 0 : static_cast<int>(pipe.cells.size()) - 1;
|
||||
const GasState interior = pipe.state(idx);
|
||||
const float area = o.area ? std::max(*o.area, 0.0f) : 0.0f;
|
||||
if (o.closed_end || !o.port || area <= 1e-12f) {
|
||||
GasState wall = interior;
|
||||
wall.velocity = -interior.velocity;
|
||||
if (o.left) pipe.left_ghost = wall; else pipe.right_ghost = wall;
|
||||
if (o.port) o.port->mass_flow = 0.0f;
|
||||
static int end(const Pipe1D &p, bool left) { return left ? 0 : static_cast<int>(p.cells.size()) - 1; }
|
||||
static void ghost(Pipe1D &p, bool left, GasState s) {
|
||||
if (left)
|
||||
p.left_ghost = s;
|
||||
else
|
||||
p.right_ghost = s;
|
||||
}
|
||||
void resolve_orifice(Orifice &o, float dt) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
int i = end(p, o.left);
|
||||
auto s = p.state(i);
|
||||
float face_area = p.cell_area(i);
|
||||
if (o.area && (!std::isfinite(*o.area) || *o.area < 0))
|
||||
throw std::invalid_argument("invalid dynamic orifice area");
|
||||
if (o.port && (!(o.port->pressure > 0) || !(o.port->density > 0) ||
|
||||
!std::isfinite(o.port->pressure) || !std::isfinite(o.port->density)))
|
||||
throw std::runtime_error("nonphysical component port");
|
||||
const float area = o.area ? std::min(std::max(*o.area, 0.f), face_area) : 0.f;
|
||||
if (!o.port || area <= 0 || o.cd == 0) {
|
||||
auto g = s;
|
||||
g.velocity = -s.velocity;
|
||||
ghost(p, o.left, g);
|
||||
auto wall_flux =
|
||||
Pipe1D::scaled(o.left ? Pipe1D::riemann(g, s) : Pipe1D::riemann(s, g), face_area);
|
||||
// A stationary adiabatic wall transfers momentum, never mass or energy.
|
||||
p.set_boundary_flux(o.left, {0, wall_flux.p, 0, 0});
|
||||
if (o.port)
|
||||
o.port->mass_flow = 0;
|
||||
o.mdot = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const float dp = o.port->pressure - interior.pressure;
|
||||
OrificeSolution solution{0.0f, interior.rho, 0.0f, interior.pressure};
|
||||
// Preserve the C# tie-break: at equal pressure it treats the volume as
|
||||
// upstream, retaining the volume-side density in the ghost state. That
|
||||
// matters when equal-pressure volume and pipe states have different
|
||||
// temperatures/densities, as on the initialized intake plenum.
|
||||
if (o.port->pressure >= interior.pressure) {
|
||||
solution = isentropic(o.port->pressure, o.port->density, o.port->temperature, interior.pressure, area, o.cd);
|
||||
} else {
|
||||
solution = isentropic(interior.pressure, interior.rho,
|
||||
interior.pressure / std::max(interior.rho * gas_constant, 1e-12f), o.port->pressure, area, o.cd);
|
||||
solution.mass_flow = -solution.mass_flow;
|
||||
}
|
||||
float mdot = solution.mass_flow; // positive means port component -> pipe
|
||||
const float adjacent_mass = pipe.cell_mass(idx);
|
||||
if (o.use_inertance) {
|
||||
const float rho_up = o.current_mass_flow >= 0.0f ? o.port->density : interior.rho;
|
||||
const float inertance = rho_up * o.effective_length / area;
|
||||
const float derivative = (dp - o.loss_coefficient * o.current_mass_flow) /
|
||||
std::max(inertance, 1e-12f);
|
||||
mdot = o.current_mass_flow + derivative * dt;
|
||||
}
|
||||
if (mdot > 0.0f && (o.port->owner_air_mass || o.port->owner_exhaust_mass))
|
||||
mdot = std::min(mdot, available_mass(*o.port) / std::max(dt, 1e-12f));
|
||||
if (mdot < 0.0f) mdot = std::max(mdot, -adjacent_mass / std::max(dt, 1e-12f));
|
||||
|
||||
if (o.use_inertance) {
|
||||
o.current_mass_flow = mdot;
|
||||
const bool leaves_port = mdot >= 0.0f;
|
||||
const float rho_up = leaves_port ? o.port->density : interior.rho;
|
||||
const float temperature_up = leaves_port ? o.port->temperature :
|
||||
interior.pressure / std::max(interior.rho * gas_constant, 1e-12f);
|
||||
const float max_velocity = 0.9f * std::sqrt(gamma * gas_constant * temperature_up);
|
||||
const float max_flow = rho_up * max_velocity * area;
|
||||
mdot = std::clamp(mdot, -max_flow, max_flow);
|
||||
o.current_mass_flow = mdot;
|
||||
if (mdot >= 0.0f) {
|
||||
solution = isentropic(o.port->pressure, o.port->density, o.port->temperature,
|
||||
interior.pressure, area, o.cd);
|
||||
} else {
|
||||
solution = isentropic(interior.pressure, interior.rho, temperature_up,
|
||||
o.port->pressure, area, o.cd);
|
||||
solution.mass_flow = -solution.mass_flow;
|
||||
auto &v = *o.port;
|
||||
if (std::abs(v.gamma_value - gamma) > 1e-6f || std::abs(v.gas_constant_value - gas_constant) > .001f)
|
||||
throw std::invalid_argument("pipe coupling requires the same calorically perfect gas");
|
||||
const float dp = v.pressure - s.pressure;
|
||||
const double g = gamma;
|
||||
const double critical = std::pow(2.0 / (g + 1), g / (g - 1));
|
||||
const double c = std::sqrt(g * s.pressure / s.rho);
|
||||
const double ui = s.velocity * (o.left ? 1 : -1);
|
||||
const auto face_density = [&](double pressure) {
|
||||
return s.rho * std::pow(pressure / s.pressure, 1 / g);
|
||||
};
|
||||
const auto nozzle_flow = [&](double pressure) {
|
||||
const bool from = v.pressure >= pressure;
|
||||
const double up = from ? v.pressure : pressure;
|
||||
const double down = from ? pressure : v.pressure;
|
||||
const double density = from ? v.density : face_density(pressure);
|
||||
const double ratio = std::max(down / up, critical);
|
||||
const double r = std::pow(ratio, 1 / g);
|
||||
const double speed2 = 2 * g / (g - 1) * up / density * (1 - std::pow(ratio, (g - 1) / g));
|
||||
return (from ? 1 : -1) * density * r * std::sqrt(std::max(0.0, speed2)) * area * o.cd;
|
||||
};
|
||||
// Match nozzle discharge to the outgoing duct characteristic. Imposing
|
||||
// sqrt(dp) against the *cell-centre* pressure is an unstable explicit
|
||||
// boundary near equilibrium and ignores the duct's acoustic impedance.
|
||||
const double zero_sound = c - .5 * (g - 1) * ui;
|
||||
if (zero_sound <= 0)
|
||||
throw std::runtime_error("orifice characteristic outside subsonic model");
|
||||
const double zero_pressure = s.pressure * std::pow(zero_sound / c, 2 * g / (g - 1));
|
||||
double low = std::min(zero_pressure, double(v.pressure));
|
||||
double high = std::max(zero_pressure, double(v.pressure));
|
||||
double duct_pressure = .5 * (low + high);
|
||||
const double flow_sign = v.pressure >= zero_pressure ? 1 : -1;
|
||||
const double flow_scale = area * o.cd;
|
||||
const double density_ratio_scale = std::pow(double(s.pressure) / v.pressure, 1 / g);
|
||||
const double critical_density_ratio = std::pow(critical, 1 / g);
|
||||
// Squared-flow residual avoids the nozzle sqrt singularity at dp=0.
|
||||
// Safeguarded analytic Newton; bisection remains the fallback.
|
||||
if (o.length == 0 && high > low)
|
||||
for (int iteration = 0; iteration < 40; ++iteration) {
|
||||
++nonlinear_iterations;
|
||||
const double density = face_density(duct_pressure);
|
||||
const double sound = std::sqrt(g * duct_pressure / density);
|
||||
const double velocity = ui + 2 * (sound - c) / (g - 1);
|
||||
const double q = density * p.cell_area(i) * velocity;
|
||||
const double dq = p.cell_area(i) * density / (g * duct_pressure) * (velocity + sound);
|
||||
const bool from = v.pressure >= duct_pressure;
|
||||
const double up = from ? v.pressure : duct_pressure;
|
||||
const double down = from ? duct_pressure : v.pressure;
|
||||
const double upstream_density = from ? v.density : density;
|
||||
const double raw_ratio = down / up;
|
||||
const double ratio = std::max(raw_ratio, critical);
|
||||
const double root_ratio = raw_ratio <= critical
|
||||
? critical_density_ratio
|
||||
: (from ? density / s.rho * density_ratio_scale
|
||||
: s.rho / (density * density_ratio_scale));
|
||||
const double r1 = root_ratio * root_ratio, r2 = root_ratio * ratio;
|
||||
const double coefficient = flow_scale * flow_scale * 2 * g / (g - 1) * upstream_density * up;
|
||||
const double q2 = coefficient * (r1 - r2);
|
||||
const double dr = raw_ratio > critical ? (from ? 1 / up : -ratio / up) : 0;
|
||||
const double derivative = coefficient * ((2 / g * r1 - (g + 1) / g * r2) / ratio) * dr +
|
||||
(from ? 0 : q2 * (1 + 1 / g) / duct_pressure);
|
||||
const double residual = flow_sign * (q * q - q2);
|
||||
if (residual > 0)
|
||||
high = duct_pressure;
|
||||
else
|
||||
low = duct_pressure;
|
||||
if (high - low < std::max(1e-9, duct_pressure * 1e-12))
|
||||
break;
|
||||
const double slope = flow_sign * (2 * q * dq - derivative);
|
||||
const double candidate = duct_pressure - residual / slope;
|
||||
if (std::abs(residual) < 4e-7 * std::max(q * q + q2, 1e-24))
|
||||
break;
|
||||
duct_pressure = (std::isfinite(candidate) && candidate > low && candidate < high)
|
||||
? candidate
|
||||
: .5 * (low + high);
|
||||
}
|
||||
solution.mass_flow = mdot;
|
||||
solution.rho = rho_up;
|
||||
float mdot = static_cast<float>(nozzle_flow(duct_pressure));
|
||||
const bool upstream_port = mdot >= 0;
|
||||
const float pup = upstream_port ? v.pressure : static_cast<float>(duct_pressure);
|
||||
const float ratio =
|
||||
static_cast<float>(std::max((upstream_port ? duct_pressure : v.pressure) / pup, critical));
|
||||
const float throat_rho =
|
||||
(upstream_port ? v.density : static_cast<float>(face_density(duct_pressure))) *
|
||||
std::pow(ratio, 1 / gamma);
|
||||
if (o.length > 0) {
|
||||
// d(mdot)/dt = A/L dp - K mdot |mdot|/(2 rho A L).
|
||||
const float donor_rho = o.mdot >= 0 ? v.density : s.rho;
|
||||
// Semi-implicit resistance remains dissipative for large dt.
|
||||
mdot = (o.mdot + area / o.length * dp * dt) /
|
||||
(1 + dt * o.loss * std::abs(o.mdot) / (2 * donor_rho * area * o.length));
|
||||
}
|
||||
|
||||
const bool from_port = mdot >= 0.0f;
|
||||
const float rho_face = std::max(solution.rho, 1e-9f);
|
||||
const float u = std::abs(mdot) / std::max(rho_face * area, 1e-12f);
|
||||
GasState ghost{rho_face, 0.0f, std::max(solution.pressure, 1.0f), from_port ? o.port->air_fraction : interior.air};
|
||||
ghost.velocity = ((from_port == o.left) ? u : -u);
|
||||
if (o.left) pipe.left_ghost = ghost; else pipe.right_ghost = ghost;
|
||||
o.port->mass_flow = -mdot;
|
||||
if (!from_port) o.port->air_fraction = interior.air;
|
||||
o.port->enthalpy = from_port
|
||||
? o.port->enthalpy
|
||||
: gamma / (gamma - 1.0f) * interior.pressure / std::max(interior.rho, 1e-12f);
|
||||
(void)dp;
|
||||
const bool from = mdot >= 0;
|
||||
const float h = from ? gamma / (gamma - 1) * v.pressure / v.density
|
||||
: gamma / (gamma - 1) * s.pressure / s.rho + .5f * s.velocity * s.velocity;
|
||||
const float requested_mdot = mdot;
|
||||
if (from && o.budget < budgets_.size()) {
|
||||
auto &b = budgets_[o.budget];
|
||||
mdot = std::min(mdot, std::max(0.f, std::min(b.mass, b.heat / std::max(h, 1.f))) / dt);
|
||||
b.mass -= mdot * dt;
|
||||
b.heat -= mdot * h * dt;
|
||||
} else if (!from) {
|
||||
const float cell_energy = p.cells[i].energy * face_area * p.cell_length(i);
|
||||
mdot = std::max(mdot, -.25f * std::min(p.cell_mass(i), cell_energy / std::max(h, 1.f)) / dt);
|
||||
}
|
||||
if (mdot != requested_mdot)
|
||||
++limited_fluxes;
|
||||
o.mdot = mdot;
|
||||
const float donor_rho = from ? v.density : s.rho;
|
||||
const float density = o.length > 0 ? donor_rho : throat_rho;
|
||||
const float velocity = mdot / (density * area);
|
||||
const float face_pressure =
|
||||
o.length > 0 ? .5f * (v.pressure + s.pressure) : static_cast<float>(duct_pressure);
|
||||
const float direction = o.left ? 1.f : -1.f;
|
||||
const float species = from ? v.air_fraction : s.air;
|
||||
const float signed_mass = direction * mdot;
|
||||
p.set_boundary_flux(o.left, {signed_mass,
|
||||
mdot * velocity + face_pressure * area + s.pressure * (face_area - area),
|
||||
signed_mass * h, signed_mass * species});
|
||||
ghost(p, o.left, {density, direction * velocity, face_pressure, species});
|
||||
v.mass_flow = -mdot;
|
||||
v.enthalpy = h;
|
||||
if (!from)
|
||||
v.air_fraction = s.air;
|
||||
}
|
||||
void resolve_open_end(Open& o, float dt) {
|
||||
Pipe1D& pipe = pipes_.at(static_cast<std::size_t>(o.pipe));
|
||||
const int idx = o.left ? 0 : static_cast<int>(pipe.cells.size()) - 1;
|
||||
const GasState interior = pipe.state(idx);
|
||||
const float c = std::sqrt(gamma * interior.pressure / std::max(interior.rho, 1e-12f));
|
||||
const float gm1 = gamma - 1.0f;
|
||||
const float j_plus = interior.velocity + 2.0f * c / gm1;
|
||||
const float j_minus = interior.velocity - 2.0f * c / gm1;
|
||||
const float entropy = interior.pressure / std::pow(std::max(interior.rho, 1e-12f), gamma);
|
||||
const float rho_iso = std::pow(std::max(o.pressure / entropy, 1e-12f), 1.0f / gamma);
|
||||
const float c_iso = std::sqrt(gamma * o.pressure / std::max(rho_iso, 1e-12f));
|
||||
const float u_iso = o.left ? j_minus + 2.0f * c_iso / gm1 : j_plus - 2.0f * c_iso / gm1;
|
||||
const bool supersonic = o.left ? (interior.velocity <= -c || u_iso <= -c_iso)
|
||||
: (interior.velocity >= c || u_iso >= c_iso);
|
||||
GasState ghost = supersonic ? interior : GasState{rho_iso, u_iso, o.pressure,
|
||||
((o.left ? u_iso >= 0.0f : u_iso <= 0.0f) ? 1.0f : interior.air)};
|
||||
|
||||
const float cell_mass = pipe.cell_mass(idx);
|
||||
float mdot_out = ghost.rho * ghost.velocity * o.area * (o.left ? -1.0f : 1.0f);
|
||||
if (mdot_out > 0.0f) mdot_out = std::min(mdot_out, cell_mass / std::max(dt, 1e-12f));
|
||||
else mdot_out = std::max(mdot_out, -10.0f * cell_mass / std::max(dt, 1e-12f));
|
||||
const float speed = std::abs(mdot_out) / std::max(ghost.rho * o.area, 1e-12f);
|
||||
ghost.velocity = o.left ? (mdot_out >= 0.0f ? -speed : speed)
|
||||
: (mdot_out >= 0.0f ? speed : -speed);
|
||||
if (o.left) pipe.left_ghost = ghost; else pipe.right_ghost = ghost;
|
||||
o.last_mass_flow = mdot_out;
|
||||
o.last_face_pressure = ghost.pressure;
|
||||
void resolve_open(Open &o) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
int i = end(p, o.left);
|
||||
auto s = p.state(i);
|
||||
const float c = std::sqrt(gamma * s.pressure / s.rho), gm1 = gamma - 1;
|
||||
if (o.left ? s.velocity >= c : s.velocity <= -c)
|
||||
throw std::runtime_error(
|
||||
"supersonic inlet needs full upstream state; pressure-only boundary is insufficient");
|
||||
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);
|
||||
const float u =
|
||||
o.left ? s.velocity - 2 * c / gm1 + 2 * cb / gm1 : s.velocity + 2 * c / gm1 - 2 * cb / gm1;
|
||||
const bool out = o.left ? s.velocity <= -c : s.velocity >= c;
|
||||
GasState g = out ? s : GasState{rho, u, o.pressure, (o.left ? u >= 0 : u <= 0) ? 1.f : s.air};
|
||||
ghost(p, o.left, g);
|
||||
const float a = std::min(o.area, p.cell_area(i));
|
||||
auto f = Pipe1D::scaled(o.left ? Pipe1D::riemann(g, s) : Pipe1D::riemann(s, g), a);
|
||||
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_face_pressure = f.p / p.cell_area(i);
|
||||
}
|
||||
void resolve_connection(const Connection &o) {
|
||||
auto &a = pipes_[o.a];
|
||||
auto &b = pipes_[o.b];
|
||||
int ia = end(a, o.left_a), ib = end(b, o.left_b);
|
||||
auto sa = a.state(ia), sb = b.state(ib);
|
||||
const float da = o.left_a ? -1.f : 1.f, db = o.left_b ? 1.f : -1.f;
|
||||
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);
|
||||
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});
|
||||
b.set_boundary_flux(o.left_b,
|
||||
{db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e, db * f.y});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim\AudioProcessing.hpp"
|
||||
#include "FluidSim\Fluid.hpp"
|
||||
#include "FluidSim\Solver.hpp"
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
22
include/FluidSim/MotorcycleTest.hpp
Normal file
22
include/FluidSim/MotorcycleTest.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Shared longitudinal load and clutch integration for motorcycle/car presets.
|
||||
class MotorcycleTest {
|
||||
public:
|
||||
Vehicle vehicle{};
|
||||
float load{}, clutch{1.0f};
|
||||
|
||||
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);
|
||||
crank.step(dt);
|
||||
crank.drivetrain_torque = vehicle.update(crank.omega, crank.inertia, dt);
|
||||
}
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
72
include/FluidSim/OutdoorAudio.hpp
Normal file
72
include/FluidSim/OutdoorAudio.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class OutdoorScene { Dry, Field, Wall, Woodland };
|
||||
const char *scene_name(OutdoorScene scene);
|
||||
|
||||
struct OutdoorAudioConfig {
|
||||
Vec3 listener{1.6f, 1.0f, 2.0f};
|
||||
Vec3 listener_right{1.0f, 0.0f, 0.0f};
|
||||
Vec3 intake_position{-0.2f, 0.7f, 0.0f};
|
||||
Vec3 exhaust_position{0.55f, 0.5f, 0.0f};
|
||||
float ear_spacing{0.18f}, sound_speed{343.0f};
|
||||
float ground_y{0.0f}, wall_x{-1.5f};
|
||||
float ground_reflection{0.35f}, wall_reflection{0.65f};
|
||||
float pressure_gain{0.005f}; // 200 Pa per device full scale, not speaker SPL.
|
||||
float monitor_cutoff_hz{6000.0f}; // Second-order acquisition/rendering filter.
|
||||
};
|
||||
|
||||
// Static geometric direct/first-order reflection paths plus an explicitly
|
||||
// approximate diffuse tail. No feedback into the fluid/combustion simulation.
|
||||
class OutdoorAudio {
|
||||
public:
|
||||
explicit OutdoorAudio(int sample_rate = 44100, OutdoorAudioConfig config = {});
|
||||
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow);
|
||||
void set_scene(OutdoorScene scene) { scene_ = scene; }
|
||||
OutdoorScene scene() const { return scene_; }
|
||||
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_; }
|
||||
|
||||
private:
|
||||
struct LowPass {
|
||||
float b0{}, b1{}, b2{}, a1{}, a2{}, z1{}, z2{};
|
||||
float process(float x);
|
||||
};
|
||||
struct Path {
|
||||
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
|
||||
};
|
||||
struct Emitter {
|
||||
std::vector<float> filtered, raw;
|
||||
std::vector<Path> paths;
|
||||
std::size_t position{};
|
||||
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};
|
||||
bool monitor_filter_{true};
|
||||
std::array<Emitter, 2> emitters_;
|
||||
std::array<TailLine, 4> tail_;
|
||||
std::array<float, 4> weights_{1, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_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
|
||||
34
include/FluidSim/PipeProfile.hpp
Normal file
34
include/FluidSim/PipeProfile.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include <span>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct PipeSection {
|
||||
float length, inlet_diameter, outlet_diameter;
|
||||
};
|
||||
|
||||
// Linear diameter within each section. Cell area is the exact frustum-volume
|
||||
// average, so refining the mesh preserves the specified chamber volume.
|
||||
inline Pipe1D make_profiled_pipe(std::span<const PipeSection> sections, float maximum_cell_length = .01f) {
|
||||
if (!std::isfinite(maximum_cell_length) || maximum_cell_length <= 0 || sections.empty())
|
||||
throw std::invalid_argument("invalid pipe profile mesh");
|
||||
std::vector<float> areas, lengths;
|
||||
float previous_diameter = sections.front().inlet_diameter;
|
||||
for (const auto& section : sections) {
|
||||
if (!std::isfinite(section.length) || !std::isfinite(section.inlet_diameter) ||
|
||||
!std::isfinite(section.outlet_diameter) || section.length <= 0 ||
|
||||
section.inlet_diameter <= 0 || section.outlet_diameter <= 0 ||
|
||||
std::abs(section.inlet_diameter - previous_diameter) > 1e-6f)
|
||||
throw std::invalid_argument("invalid/discontinuous pipe profile");
|
||||
const int count = static_cast<int>(std::ceil(section.length / maximum_cell_length));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const float d0 = std::lerp(section.inlet_diameter, section.outlet_diameter, float(i) / count);
|
||||
const float d1 = std::lerp(section.inlet_diameter, section.outlet_diameter, float(i + 1) / count);
|
||||
areas.push_back(pi / 12 * (d0*d0 + d0*d1 + d1*d1));
|
||||
lengths.push_back(section.length / count);
|
||||
}
|
||||
previous_diameter = section.outlet_diameter;
|
||||
}
|
||||
return Pipe1D(std::move(areas), std::move(lengths));
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
47
include/FluidSim/ReedValve.hpp
Normal file
47
include/FluidSim/ReedValve.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Lumped cantilever-petal model; geometry/material values require calibration.
|
||||
class ReedValve {
|
||||
public:
|
||||
float length{0.030f}, width{0.016f}, thickness{0.00035f};
|
||||
float youngs_modulus{70e9f}, material_density{1600.0f};
|
||||
float damping_ratio{0.30f}, maximum_lift{0.004f};
|
||||
int petals{4};
|
||||
float lift() const { return lift_; }
|
||||
float step(float dt, float pressure_difference) {
|
||||
if (!std::isfinite(dt) || dt <= 0 || !std::isfinite(pressure_difference))
|
||||
throw std::invalid_argument("invalid reed timestep or pressure");
|
||||
if (!(length > 0 && width > 0 && thickness > 0 && youngs_modulus > 0 && material_density > 0 &&
|
||||
maximum_lift > 0 && petals > 0 && damping_ratio >= 0))
|
||||
throw std::invalid_argument("invalid reed geometry/material");
|
||||
const float mass = .236f * material_density * length * width * thickness * petals;
|
||||
const float stiffness = petals * youngs_modulus * width * thickness * thickness * thickness /
|
||||
(4 * length * length * length);
|
||||
const float damping = 2 * damping_ratio * std::sqrt(mass * stiffness);
|
||||
const float force = pressure_difference * (3.0f / 8.0f) * length * width * petals;
|
||||
const int steps = std::max(1, int(std::ceil(dt * std::sqrt(stiffness / mass) / .15f)));
|
||||
const float h = dt / steps;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
velocity_ += (force - stiffness * lift_ - damping * velocity_) / mass * h;
|
||||
lift_ += velocity_ * h;
|
||||
if (lift_ < 0) {
|
||||
lift_ = 0;
|
||||
velocity_ = 0;
|
||||
}
|
||||
if (lift_ > maximum_lift) {
|
||||
lift_ = maximum_lift;
|
||||
velocity_ = 0;
|
||||
}
|
||||
}
|
||||
// Front-edge plus triangular side-gap areas for all petals.
|
||||
return petals * (width + length) * lift_;
|
||||
}
|
||||
|
||||
private:
|
||||
float lift_{}, velocity_{};
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
@@ -1,12 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#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/OutdoorExhaustReverb.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>
|
||||
#include <mutex>
|
||||
@@ -14,27 +19,58 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, TurboTwin };
|
||||
enum class FourStrokePreset { Twin, SaabB205 };
|
||||
|
||||
struct ScenarioSnapshot {
|
||||
bool turbo_enabled{};
|
||||
TurboSnapshot turbo{};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 4> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{};
|
||||
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 throttle{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
float throttle{}, pedal{}, target_afr{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
bool idle_control{}, fuel_cut{}, two_stroke{}, reed_open{};
|
||||
bool racing_exhaust{};
|
||||
float crankcase_pressure{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{};
|
||||
std::array<std::vector<GasState>, 6> pipe_states;
|
||||
std::array<std::vector<GasState>, 12> pipe_states;
|
||||
std::array<std::vector<float>, 12> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
for (std::size_t k = 0; k < pipes.size(); ++k) {
|
||||
pipe_areas[k] = pipes[k].cell_areas;
|
||||
pipe_lengths[k] = pipes[k].cell_lengths;
|
||||
pipe_states[k].reserve(pipes[k].cells.size());
|
||||
for (std::size_t i = 0; i < pipes[k].cells.size(); ++i)
|
||||
pipe_states[k].push_back(pipes[k].state(static_cast<int>(i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Active C# SingleCylScenario topology: a 600 cc parallel twin, independent
|
||||
// intake runners, separate short exhaust headers, shared collector and tailpipe.
|
||||
class FourStrokeTwinScenario {
|
||||
// Shared four-stroke topology: independent runners/headers and shared plenums.
|
||||
// Presets retain the original 600 cc twin and add a B205-sized inline-four.
|
||||
class FourStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit FourStrokeTwinScenario(int sample_rate = 44100);
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin,
|
||||
bool turbo_enabled = false);
|
||||
const FourStrokePreset preset;
|
||||
int cylinder_count() const { return preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 4> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4}; }
|
||||
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_monitor_filter(bool enabled);
|
||||
AudioFrame dry_audio() const;
|
||||
AudioFrame unfiltered_audio() const;
|
||||
void shift_up();
|
||||
void shift_down();
|
||||
void set_neutral();
|
||||
@@ -42,14 +78,13 @@ public:
|
||||
ScenarioSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
float load{};
|
||||
float clutch{1.0f};
|
||||
bool mechanical_sound_enabled{};
|
||||
float mechanical_mix{0.02f}, reverb_send_gain{1.0f};
|
||||
float exhaust_send{0.5f}, intake_send{0.5f}, mechanical_send{};
|
||||
float mechanical_mix{0.02f};
|
||||
Ecu ecu{};
|
||||
std::unique_ptr<Turbocharger> turbo;
|
||||
Crankshaft crankshaft{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{};
|
||||
Vehicle vehicle{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
@@ -58,25 +93,27 @@ public:
|
||||
std::uint64_t sample_count{};
|
||||
|
||||
private:
|
||||
Port* throttle_port_{};
|
||||
Port* runner_feed1_{};
|
||||
Port* runner_feed2_{};
|
||||
Port* collector_in1_{};
|
||||
Port* collector_in2_{};
|
||||
Port* collector_out_{};
|
||||
float throttle_area_{}, runner_area_{}, valve1_in_{}, valve2_in_{};
|
||||
float valve1_out_{}, valve2_out_{}, header_area_{}, tail_area_{};
|
||||
float intake_boundary_area_{}, throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 4> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float dt_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
RadiationSource intake_radiation_;
|
||||
RadiationSource exhaust_radiation_;
|
||||
OutdoorExhaustReverb reverb_;
|
||||
OutdoorAudio audio_;
|
||||
MechanicalSoundProcessor mechanical_sound_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes();
|
||||
static std::vector<Pipe1D> create_pipes(FourStrokePreset preset, bool turbo_enabled);
|
||||
};
|
||||
|
||||
using FourStrokeTwinScenario = FourStrokeScenario;
|
||||
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) {}
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace fluidsim::port {
|
||||
class Solver {
|
||||
public:
|
||||
Solver(std::vector<Pipe1D>& pipes, BoundarySystem& boundaries);
|
||||
std::uint64_t substep_count() const { return substep_count_; }
|
||||
int minimum_substeps{4};
|
||||
void add_component(Component& component);
|
||||
void step(float dt);
|
||||
@@ -18,7 +19,7 @@ private:
|
||||
std::vector<Pipe1D>& pipes_;
|
||||
BoundarySystem& boundaries_;
|
||||
std::vector<Component*> components_;
|
||||
std::uint64_t step_count_{};
|
||||
std::uint64_t step_count_{}, substep_count_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
double speed() const;
|
||||
void set_volume(float volume);
|
||||
float volume() const;
|
||||
std::uint64_t underrun_blocks() const { return stream_.underrun_blocks(); }
|
||||
bool is_playing() const { return is_playing_; }
|
||||
|
||||
private:
|
||||
|
||||
78
include/FluidSim/Turbocharger.hpp
Normal file
78
include/FluidSim/Turbocharger.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#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 {
|
||||
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);
|
||||
|
||||
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_{};
|
||||
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
|
||||
38
include/FluidSim/TwoStrokeConfig.hpp
Normal file
38
include/FluidSim/TwoStrokeConfig.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "FluidSim/PipeProfile.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class TwoStrokeExhaust { Racing, Straight };
|
||||
|
||||
// YZ125-sized baseline. Port/chamber/combustion dimensions are explicit design
|
||||
// estimates, not a measured Yamaha cylinder or factory exhaust/ignition map.
|
||||
struct TwoStrokeConfig {
|
||||
TwoStrokeExhaust exhaust{TwoStrokeExhaust::Racing};
|
||||
float bore{.054f}, stroke{.0545f}, rod{.110f};
|
||||
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 carburetor_diameter{.0381f};
|
||||
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.
|
||||
float friction_bar{.4f}, friction_bar_per_mps{.09f}, friction_bar_per_mps2{.0009f};
|
||||
float exhaust_cell_length{.01f};
|
||||
std::array<PipeSection, 6> racing_pipe{{
|
||||
{.160f, .036f, .036f}, // header
|
||||
{.180f, .036f, .060f}, // shallow first diffuser
|
||||
{.190f, .060f, .105f}, // second diffuser
|
||||
{.130f, .105f, .105f}, // belly
|
||||
{.210f, .105f, .022f}, // convergent / baffle cone
|
||||
{.240f, .022f, .022f} // stinger
|
||||
}};
|
||||
float geometric_compression() const {
|
||||
const float theta = (180 + exhaust_duration * .5f) * pi / 180;
|
||||
const float radius = stroke * .5f, sine = std::sin(theta);
|
||||
const float travel = radius * (1 - std::cos(theta)) + rod -
|
||||
std::sqrt(rod*rod - radius*radius*sine*sine);
|
||||
return 1 + (trapped_compression - 1) * stroke / travel;
|
||||
}
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
@@ -18,6 +19,9 @@ public:
|
||||
float exhaust_open() const { return 180.0f - exhaust_duration_ * 0.5f; }
|
||||
float exhaust_close() const { return 180.0f + exhaust_duration_ * 0.5f; }
|
||||
void set_crankcase(Crankcase& crankcase) { crankcase_ = &crankcase; }
|
||||
float transfer_width{.050f}, exhaust_width{.036f};
|
||||
float minimum_burn_seconds{.0008f};
|
||||
std::uint64_t combustion_count() const { return combustion_count_; }
|
||||
|
||||
float valve_area(float cycle_degrees, bool inlet) const override;
|
||||
void pre_step(Crankshaft& crank, float dt, float throttle) override;
|
||||
@@ -26,7 +30,10 @@ private:
|
||||
float transfer_duration_{}, exhaust_duration_{};
|
||||
Crankcase* crankcase_{};
|
||||
|
||||
static float lift(float angle, float opens, float closes, float peak_lift);
|
||||
float active_burn_duration_{60};
|
||||
float current_rpm_{};
|
||||
float active_spark_angle_{};
|
||||
std::uint64_t combustion_count_{};
|
||||
void handle_cycle_events(float previous_degrees, float current_degrees);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/ReedValve.hpp"
|
||||
#include "FluidSim/MotorcycleTest.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include "FluidSim/TwoStrokeCylinder.hpp"
|
||||
#include "FluidSim/TwoStrokeConfig.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -14,19 +19,34 @@ namespace fluidsim::port {
|
||||
struct TwoStrokeSnapshot {
|
||||
float rpm{}, cylinder_pressure{}, crankcase_pressure{}, intake_pressure{};
|
||||
float throttle{}, transfer_area{}, exhaust_area{};
|
||||
bool reed_open{};
|
||||
bool reed_open{}, fuel_cut{};
|
||||
float pedal{};
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
};
|
||||
|
||||
// Port of the C# 125 cc crankcase-scavenged two-stroke scenario. It retains
|
||||
// the plenum / reed / crankcase / transfer / cylinder / exhaust topology.
|
||||
class TwoStrokeScenario {
|
||||
class TwoStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit TwoStrokeScenario(int sample_rate = 44100);
|
||||
explicit TwoStrokeScenario(int sample_rate = 44100, TwoStrokeConfig config = {});
|
||||
AudioFrame process();
|
||||
void set_throttle(float value);
|
||||
void set_controls(float pedal, float load_value, float clutch_value);
|
||||
void shift_up() { vehicle.shift_up(); }
|
||||
void shift_down() { vehicle.shift_down(); }
|
||||
void set_neutral() { vehicle.set_neutral(); }
|
||||
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(); }
|
||||
AudioFrame unfiltered_audio() const { return audio_.unfiltered_frame(); }
|
||||
TwoStrokeSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
const TwoStrokeConfig configuration;
|
||||
Ecu ecu{};
|
||||
ReedValve reed{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
Crankshaft crankshaft{};
|
||||
TwoStrokeCylinder cylinder;
|
||||
Crankcase crankcase;
|
||||
@@ -34,7 +54,7 @@ public:
|
||||
std::vector<Pipe1D> pipes;
|
||||
|
||||
private:
|
||||
float dt_{}, max_throttle_area_{};
|
||||
float dt_{}, max_throttle_area_{}, intake_boundary_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_{};
|
||||
@@ -42,12 +62,10 @@ private:
|
||||
Port* plenum_outlet_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
SoundProcessor intake_sound_;
|
||||
SoundProcessor exhaust_sound_;
|
||||
OutdoorExhaustReverb reverb_;
|
||||
OutdoorAudio audio_;
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes();
|
||||
static std::vector<Pipe1D> create_pipes(const TwoStrokeConfig& config);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Longitudinal motorcycle model and the friction clutch coupling it to the crank.
|
||||
// Longitudinal vehicle model and the friction clutch coupling it to the crank.
|
||||
class Vehicle {
|
||||
public:
|
||||
int current_gear{};
|
||||
std::array<float, 6> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f};
|
||||
std::vector<float> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f};
|
||||
float final_drive_ratio{2.7f}, primary_reduction{1.8f};
|
||||
float clutch_input{1.0f}, clutch_capacity{80.0f}, clutch_engagement_rate{8.0f};
|
||||
float mass{160.0f}, wheel_radius{0.32f}, rotational_inertia{1.2f};
|
||||
|
||||
Reference in New Issue
Block a user