Add machine workshop, typed logic, mechanics and sensor-driven ECUs
This commit is contained in:
@@ -29,7 +29,7 @@ struct CarburetorConfig {
|
||||
|
||||
class Carburetor : public CarburetorConfig {
|
||||
public:
|
||||
explicit Carburetor(float throat_diameter = 0.0381f);
|
||||
explicit Carburetor(float throat_diameter = 0.0381f,const CarburetorConfig& config={});
|
||||
|
||||
// air_mass_flow is positive from atmosphere toward the engine.
|
||||
void step(float dt, float pedal, float slide, float rpm, float air_mass_flow,
|
||||
|
||||
@@ -13,6 +13,7 @@ struct Port {
|
||||
float gamma_value{1.4f}, gas_constant_value{287.0f};
|
||||
float air_fraction{1.0f};
|
||||
float fuel_fraction{};
|
||||
float fuel_equivalent_fraction{}; // fuel origin, including fuel already burned
|
||||
float* owner_energy{};
|
||||
float* owner_air_mass{};
|
||||
float* owner_exhaust_mass{};
|
||||
@@ -25,6 +26,8 @@ class Component {
|
||||
public:
|
||||
virtual ~Component() = default;
|
||||
virtual std::vector<Port*> ports() = 0;
|
||||
// Optional actuation at each CFL substep, before boundary fluxes are resolved.
|
||||
virtual void prepare_step(float /*dt*/) {}
|
||||
virtual void update_state(float dt) = 0;
|
||||
};
|
||||
|
||||
|
||||
45
include/FluidSim/Desktop/LogicEditor.hpp
Normal file
45
include/FluidSim/Desktop/LogicEditor.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include "FluidSim/Desktop/WorkshopLayout.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace fluidsim::port::desktop {
|
||||
struct LogicEditorHooks {
|
||||
MachineDocument& document;
|
||||
const MachineSnapshot* live{};
|
||||
bool shift{};
|
||||
std::function<bool(const std::function<void()>&)> edit;
|
||||
std::function<void(std::uint32_t,bool)> boolean_control;
|
||||
std::function<void(std::uint32_t,double)> number_control;
|
||||
std::function<void()> release_buttons;
|
||||
std::function<void(std::uint32_t)> select_physical,focus_physical;
|
||||
std::function<void(const std::string&)> status;
|
||||
std::function<void()> example;
|
||||
};
|
||||
class LogicEditor {
|
||||
public:
|
||||
LogicEditor();
|
||||
~LogicEditor();
|
||||
void reset_document();
|
||||
void deactivate(const LogicEditorHooks&);
|
||||
bool editing_text()const;
|
||||
bool active_gesture()const;
|
||||
std::uint32_t selected_node()const;
|
||||
void select_node(std::uint32_t,const MachineDocument&);
|
||||
void select_part_reference(std::uint32_t,const MachineDocument&);
|
||||
void fit(const LogicGraph&,sf::FloatRect,bool selection_only=false);
|
||||
void reveal(std::uint32_t,const MachineDocument&,sf::FloatRect);
|
||||
sf::Vector2f project(LogicPosition,sf::FloatRect)const;
|
||||
LogicPosition unproject(sf::Vector2f,sf::FloatRect)const;
|
||||
sf::FloatRect node_rect(const LogicNode&,sf::FloatRect)const;
|
||||
sf::Vector2f pin_position(const LogicNode&,const std::string&,sf::FloatRect)const;
|
||||
// Panels can be owned independently in split view; the canvas is always live.
|
||||
bool event(const sf::Event&,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels);
|
||||
void draw(sf::RenderWindow&,const sf::Font*,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
57
include/FluidSim/Desktop/MachineBuilder.hpp
Normal file
57
include/FluidSim/Desktop/MachineBuilder.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include "FluidSim/Desktop/OrbitCamera.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace fluidsim::port::desktop {
|
||||
struct BuilderRay {Vec3 origin,direction;};
|
||||
BuilderRay builder_ray(const OrbitCamera&,sf::FloatRect,sf::Vector2f);
|
||||
std::optional<sf::Vector2f> builder_project(const OrbitCamera&,sf::FloatRect,Vec3);
|
||||
// Returns metres along the ray; OBBs and duct centerlines use the rendered dimensions.
|
||||
std::optional<float> builder_pick_part(const MachinePart&,BuilderRay,float tolerance=0);
|
||||
Vec3 builder_rotate_world(Vec3 euler,unsigned axis,float degrees);
|
||||
struct BuilderPipeLocation {std::size_t segment{};Vec3 local_point;float screen_distance{};};
|
||||
std::optional<BuilderPipeLocation> builder_pipe_location(const OrbitCamera&,sf::FloatRect,const MachinePart&,sf::Vector2f);
|
||||
struct MachinePlaybackStats {double simulation_seconds{};std::uint64_t underruns{};std::size_t buffered{};double speed{1};bool paused{};};
|
||||
enum class WorkshopView {ThreeD,Logic,Split};
|
||||
class MachineBuilder {
|
||||
public:
|
||||
MachineBuilder();
|
||||
~MachineBuilder();
|
||||
const MachineDocument& document()const;
|
||||
std::vector<std::uint32_t> selected_parts()const;
|
||||
void set_document(MachineDocument,std::filesystem::path source={});
|
||||
bool load_file(const std::filesystem::path&);
|
||||
void start_test();
|
||||
void stop_test();
|
||||
bool testing()const;
|
||||
MachinePlaybackStats playback_stats()const;
|
||||
void set_test_speed(double);
|
||||
void pause_test();
|
||||
void step_test();
|
||||
std::vector<MachineMeasurement> measurements()const;
|
||||
bool export_measurements(const std::filesystem::path&);
|
||||
bool finished()const;
|
||||
bool wants_close_window()const;
|
||||
bool handle_event(const sf::Event&,sf::Vector2u);
|
||||
void update();
|
||||
void draw(sf::RenderWindow&,const sf::Font*);
|
||||
static sf::FloatRect viewport(sf::Vector2u);
|
||||
sf::FloatRect workspace_viewport(sf::Vector2u)const;
|
||||
sf::FloatRect logic_viewport(sf::Vector2u)const;
|
||||
WorkshopView workspace_view()const;
|
||||
void set_workspace_view(WorkshopView);
|
||||
std::uint32_t selected_logic_node()const;
|
||||
std::vector<LogicNodeState> logic_values()const;
|
||||
sf::FloatRect logic_node_rectangle(std::uint32_t,sf::Vector2u)const;
|
||||
sf::Vector2f logic_pin_position(std::uint32_t,const std::string&,sf::Vector2u)const;
|
||||
void restore_workspace_preferences();
|
||||
const OrbitCamera& camera()const;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
void run_machine_builder(sf::RenderWindow&,const sf::Font*,const std::filesystem::path& file={},bool play=false);
|
||||
}
|
||||
21
include/FluidSim/Desktop/MachineRenderer3D.hpp
Normal file
21
include/FluidSim/Desktop/MachineRenderer3D.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Machine.hpp"
|
||||
#include "FluidSim/Desktop/OrbitCamera.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
namespace fluidsim::port::desktop {
|
||||
struct MachineRenderStats {std::uint64_t pipe_mesh_builds{},icon_atlas_builds{};std::size_t cached_pipes{};};
|
||||
class MachineRenderer3D {
|
||||
public:
|
||||
MachineRenderer3D();
|
||||
~MachineRenderer3D();
|
||||
MachineRenderer3D(const MachineRenderer3D&)=delete;
|
||||
MachineRenderer3D& operator=(const MachineRenderer3D&)=delete;
|
||||
void draw(sf::RenderWindow&,const MachineDocument&,const MachineSnapshot*,sf::FloatRect,const OrbitCamera&,std::uint32_t selected,const MachinePart* ghost=nullptr,const std::vector<std::uint32_t>& selection={});
|
||||
void draw_icon(sf::RenderWindow&,MachinePartKind,sf::FloatRect);
|
||||
MachineRenderStats stats()const;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
10
include/FluidSim/Desktop/PortAppearance.hpp
Normal file
10
include/FluidSim/Desktop/PortAppearance.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "FluidSim/MachinePorts.hpp"
|
||||
#include <SFML/Graphics/Color.hpp>
|
||||
namespace fluidsim::port::desktop {
|
||||
inline sf::Color port_color(PortDomain domain){
|
||||
switch(domain){case PortDomain::Boolean:return {235,92,102};case PortDomain::Number:return {87,201,128};case PortDomain::Gas:return {189,200,214};case PortDomain::Rotational:return {229,184,91};case PortDomain::Translational:return {177,139,220};case PortDomain::EcuAssignment:return {91,161,242};}
|
||||
return sf::Color::White;
|
||||
}
|
||||
inline bool filled_port(const MachinePortDescriptor& descriptor){return descriptor.direction==PortDirection::Output;}
|
||||
}
|
||||
@@ -2,6 +2,26 @@
|
||||
#include "FluidSim/ScenarioCatalog.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
namespace fluidsim::port::desktop {
|
||||
std::optional<std::size_t> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
|
||||
struct ScenarioMenuChoice {
|
||||
std::optional<std::size_t> example;
|
||||
std::filesystem::path machine;
|
||||
bool play{};
|
||||
};
|
||||
enum class MenuScreen {Home,Workshop,Scenarios};
|
||||
class ScenarioMenu {
|
||||
public:
|
||||
ScenarioMenu(std::vector<ScenarioExample>,std::vector<std::filesystem::path> machines={});
|
||||
~ScenarioMenu();
|
||||
bool handle_event(const sf::Event&,sf::Vector2u);
|
||||
void draw(sf::RenderWindow&,const sf::Font*);
|
||||
bool finished()const;
|
||||
MenuScreen screen()const;
|
||||
std::optional<ScenarioMenuChoice> take_choice();
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
std::optional<ScenarioMenuChoice> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
|
||||
}
|
||||
|
||||
8
include/FluidSim/Desktop/WindowPresentation.hpp
Normal file
8
include/FluidSim/Desktop/WindowPresentation.hpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <SFML/Graphics/RenderWindow.hpp>
|
||||
|
||||
namespace fluidsim::port::desktop {
|
||||
// Maximize the decorated OS window; retain normal restore/minimize controls.
|
||||
void maximize_window(sf::RenderWindow&);
|
||||
bool window_is_maximized(const sf::RenderWindow&);
|
||||
}
|
||||
30
include/FluidSim/Desktop/WorkshopLayout.hpp
Normal file
30
include/FluidSim/Desktop/WorkshopLayout.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <SFML/Graphics/Rect.hpp>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fluidsim::port::desktop {
|
||||
// One geometry source for drawing, picking and viewport projection.
|
||||
struct WorkshopLayout {
|
||||
sf::FloatRect left, right, viewport, library, search;
|
||||
float inspector_x{}, inspector_width{}, height{}, width{};
|
||||
static constexpr float field_top=302, field_height=52;
|
||||
int field_rows()const{return std::max(1,int((height-164-field_top)/field_height));}
|
||||
int library_rows()const{return std::max(1,int(library.size.y/36));}
|
||||
sf::FloatRect field_row(int row)const{return {{inspector_x+12,field_top+row*field_height},{inspector_width-24,48}};}
|
||||
sf::FloatRect field_value(int row)const{auto r=field_row(row);return {{r.position.x,r.position.y+21},{r.size.x-68,26}};}
|
||||
static WorkshopLayout calculate(sf::Vector2u size,float left_width=260,float right_width=340,bool left_closed=false,bool right_closed=false){
|
||||
WorkshopLayout l;l.width=float(size.x);l.height=float(size.y);
|
||||
const float available=std::max(480.f,l.width-440);
|
||||
float lw=left_closed?28.f:std::clamp(left_width,220.f,420.f),rw=right_closed?28.f:std::clamp(right_width,280.f,520.f);
|
||||
if(lw+rw>available){const float excess=lw+rw-available;rw=std::max(right_closed?28.f:280.f,rw-excess);lw=std::max(left_closed?28.f:180.f,std::min(lw,available-rw));}
|
||||
l.left={{8,96},{lw,std::max(100.f,l.height-154)}};
|
||||
l.right={{l.width-rw-8,96},{rw,std::max(100.f,l.height-154)}};
|
||||
l.inspector_x=l.right.position.x;l.inspector_width=rw;
|
||||
l.viewport={{l.left.position.x+lw+8,96},{std::max(100.f,l.right.position.x-lw-24),std::max(100.f,l.height-154)}};
|
||||
l.search={{20,152},{lw-24,30}};
|
||||
l.library={{20,230},{lw-24,std::max(36.f,l.height-474)}};
|
||||
return l;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,29 @@
|
||||
#pragma once
|
||||
#include "FluidSim/MachinePorts.hpp"
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Accepts only coherent, current channels from one physical crank pickup.
|
||||
// Signed reverse measurements remain valid sensors, but cannot drive this
|
||||
// forward-only ignition controller. A stationary crank is valid.
|
||||
std::optional<SignalTiming> ecu_crank_sample(const SignalValue& speed,
|
||||
const SignalValue& angle,double time);
|
||||
|
||||
class CrankIgnitionClock {
|
||||
public:
|
||||
struct Result {bool valid{};unsigned sparks{};};
|
||||
// Crank-only wasted spark: one event per revolution at the configured
|
||||
// pickup angle, without consulting a cylinder's hidden 720-degree phase.
|
||||
Result sample(const SignalValue& speed,const SignalValue& angle,
|
||||
double time,double advance_degrees,double tdc_degrees=0);
|
||||
void reset(){previous_.reset();last_event_cycle_.reset();}
|
||||
private:
|
||||
std::optional<SignalTiming> previous_;
|
||||
std::optional<double> last_event_cycle_;
|
||||
};
|
||||
|
||||
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
||||
struct EcuConfig {
|
||||
enum class IdleControl { Feedback, FixedOpening };
|
||||
@@ -18,6 +39,8 @@ struct EcuConfig {
|
||||
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
bool rev_limit_enabled{true};
|
||||
bool rev_limit_fuel_cut_enabled{true};
|
||||
bool rev_limit_ignition_cut_enabled{true};
|
||||
float rev_limit_cut_time{}; // Minimum cut duration in seconds, followed by RPM hysteresis.
|
||||
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
|
||||
bool overrun_fuel_cut_enabled{true};
|
||||
bool overrun_ignition_cut_enabled{false};
|
||||
@@ -35,7 +58,7 @@ public:
|
||||
float target_afr() const { return afr_; }
|
||||
float spark_advance() const { return spark_; }
|
||||
bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
|
||||
bool ignition_cut() const { return limiter_ || (overrun_ && overrun_ignition_cut_enabled) || fault_; }
|
||||
bool ignition_cut() const { return (limiter_ && rev_limit_ignition_cut_enabled) || (overrun_ && overrun_ignition_cut_enabled) || fault_; }
|
||||
bool rev_limited() const { return limiter_; }
|
||||
bool idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
@@ -45,5 +68,51 @@ private:
|
||||
bool initialized_{};
|
||||
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};
|
||||
bool limiter_{}, overrun_{}, fault_{}, idle_{};
|
||||
float limiter_time_{};
|
||||
};
|
||||
|
||||
// Separate from the legacy ECU: measurements are supplied through typed wires.
|
||||
// Alpha-N cells are calibrated kg per cylinder per four-stroke cycle, indexed
|
||||
// by RPM rows and measured TPS columns. No trapped-air-mass input is available.
|
||||
struct SensorEcuConfig {
|
||||
enum class Strategy {Ignition,AlphaN,SpeedDensity};
|
||||
Strategy strategy{Strategy::Ignition};
|
||||
std::vector<EcuConfig::IgnitionPoint> ignition_map{{0,10},{6000,30}};
|
||||
std::vector<double> fuel_rpm{0,6000},fuel_tps{0,1};
|
||||
std::vector<double> fuel_per_cycle{0.000002,0.00002,0.000002,0.00002};
|
||||
// Open-loop dry ideal-gas estimate per cylinder. Geometry, VE and fuel AFR
|
||||
// are calibration; MAP and IAT must arrive as current wired measurements.
|
||||
double displacement{.0003},air_gas_constant{287},target_afr{14.7};
|
||||
std::vector<double> ve_rpm{0,6000},ve_pressure{20000,100000};
|
||||
std::vector<double> volumetric_efficiency{.9,.9,.9,.9};
|
||||
bool rev_limit_enabled{true};
|
||||
double rev_limit_rpm{7000},rev_resume_rpm{6800};
|
||||
};
|
||||
inline constexpr int sensor_ecu_strategy_count=3;
|
||||
const char* sensor_ecu_strategy_key(SensorEcuConfig::Strategy);
|
||||
const char* sensor_ecu_strategy_name(SensorEcuConfig::Strategy);
|
||||
SensorEcuConfig::Strategy sensor_ecu_strategy_from_key(std::string_view);
|
||||
SensorEcuConfig::Strategy next_sensor_ecu_strategy(SensorEcuConfig::Strategy);
|
||||
struct SensorEcuMeasurements {SignalValue speed,angle,tps,map,iat;};
|
||||
struct SensorEcuState {
|
||||
bool sensor_fault{true},rev_limited{};
|
||||
double spark_advance{},fuel_per_cycle{},fuel_mass_flow{};
|
||||
double estimated_air_per_cycle{},volumetric_efficiency{};
|
||||
unsigned spark_events{};
|
||||
};
|
||||
class SensorEcu {
|
||||
public:
|
||||
explicit SensorEcu(SensorEcuConfig config);
|
||||
void apply_calibration(SensorEcuConfig config);
|
||||
const SensorEcuState& sample(const SignalValue& speed,const SignalValue& angle,
|
||||
const SignalValue& tps,double time);
|
||||
const SensorEcuState& sample(const SensorEcuMeasurements&,double time);
|
||||
const SensorEcuState& state()const{return state_;}
|
||||
void reset(){clock_.reset();state_={};limiter_=false;}
|
||||
private:
|
||||
SensorEcuConfig config_;
|
||||
CrankIgnitionClock clock_;
|
||||
SensorEcuState state_;
|
||||
bool limiter_{};
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "FluidSim/Constants.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
@@ -25,6 +27,8 @@ public:
|
||||
float average_power{}, average_torque{};
|
||||
|
||||
void add_torque(float torque);
|
||||
float take_torque(); // Consume gas torque when an external shaft integrator owns motion.
|
||||
void record_output(float torque,float power);
|
||||
void step(float dt);
|
||||
private:
|
||||
float external_torque_{};
|
||||
@@ -51,6 +55,7 @@ public:
|
||||
float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f};
|
||||
float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f};
|
||||
float fuel_mass{}, burn_fraction{};
|
||||
float fuel_equivalent_mass{}; // passive fuel-origin tracer, unaffected by combustion
|
||||
bool combustion_active{}, fuel_injected{}, rev_limit_active{};
|
||||
Port intake{}, exhaust{};
|
||||
|
||||
@@ -84,6 +89,14 @@ protected:
|
||||
|
||||
class FourStrokeCylinder final : public EngineCylinder {
|
||||
public:
|
||||
struct IgnitionCommand {bool fire{},cut{true};};
|
||||
// Present selects external timing exclusively. A fire pulse is consumed
|
||||
// once; missing subsequent commands cannot recreate internal timed sparks.
|
||||
// Requires physically transported fuel, including carburetor operation.
|
||||
std::optional<IgnitionCommand> ignition_command;
|
||||
// Once per cycle, before ignition consumes fuel. Independent of spark cut.
|
||||
float charge_afr{std::numeric_limits<float>::quiet_NaN()};
|
||||
bool charge_sampled{};
|
||||
struct CamPoint { float angle_degrees{}, lift_metres{}; };
|
||||
struct ValveDynamics {
|
||||
float mass_kg{0.035f};
|
||||
@@ -106,6 +119,8 @@ public:
|
||||
static float cam_lift_at(const CamProfile& profile,float degrees) { return sample_cam(profile,degrees).lift_metres; }
|
||||
|
||||
private:
|
||||
float external_burn_angle_{};
|
||||
bool external_charge_ready_{};
|
||||
struct ValveState { float lift{},velocity{};bool floating{}; };
|
||||
const CamProfile* intake_cam_{};
|
||||
const CamProfile* exhaust_cam_{};
|
||||
|
||||
@@ -54,10 +54,12 @@ public:
|
||||
float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); }
|
||||
float fuel_fraction() const { return fuel_mass_ / std::max(mass(), 1e-12f); }
|
||||
float fuel_mass() const { return fuel_mass_; }
|
||||
float fuel_equivalent_fraction()const{return fuel_equivalent_mass_/std::max(mass(),1e-12f);}
|
||||
void add_fuel_mass(float value, float temperature = 300.0f) {
|
||||
if (!std::isfinite(value) || value < 0.0f || !std::isfinite(temperature) || temperature <= 0.0f)
|
||||
throw std::invalid_argument("invalid fuel addition");
|
||||
fuel_mass_ += value;
|
||||
fuel_equivalent_mass_ += value;
|
||||
energy_ += value * gas_constant / (gamma - 1.0f) * temperature;
|
||||
sync_ports();
|
||||
}
|
||||
@@ -68,9 +70,11 @@ public:
|
||||
const float total = value * volume_ / (gas_constant * target_t);
|
||||
const float fraction = air_fraction();
|
||||
const float fuel = fuel_fraction();
|
||||
const float equivalent=fuel_equivalent_fraction();
|
||||
air_mass_ = total * fraction;
|
||||
fuel_mass_ = total * fuel;
|
||||
exhaust_mass_ = total * std::max(1.0f - fraction - fuel, 0.0f);
|
||||
fuel_equivalent_mass_=total*equivalent;equivalent_roundoff_=0;
|
||||
energy_ = value * volume_ / (gamma - 1.0f);
|
||||
air_roundoff_ = exhaust_roundoff_ = fuel_roundoff_ = energy_roundoff_ = 0;
|
||||
sync_ports();
|
||||
@@ -80,7 +84,7 @@ public:
|
||||
throw std::invalid_argument("invalid volume timestep");
|
||||
if (dt == 0)
|
||||
return;
|
||||
double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0;
|
||||
double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0, d_equivalent=0;
|
||||
const float fraction = air_fraction();
|
||||
const float fuel = fuel_fraction();
|
||||
for (const Port &p : ports_) {
|
||||
@@ -90,11 +94,13 @@ public:
|
||||
d_fuel += double(p.mass_flow) * flow_fuel;
|
||||
d_exhaust += double(p.mass_flow) * std::max(1.0f - flow_fraction - flow_fuel, 0.0f);
|
||||
d_energy += double(p.mass_flow) * p.enthalpy;
|
||||
d_equivalent+=double(p.mass_flow)*(p.mass_flow>=0?std::max(p.fuel_equivalent_fraction,p.fuel_fraction):fuel_equivalent_fraction());
|
||||
}
|
||||
accumulate(air_mass_, air_roundoff_, d_air * dt);
|
||||
accumulate(exhaust_mass_, exhaust_roundoff_, d_exhaust * dt);
|
||||
accumulate(fuel_mass_, fuel_roundoff_, d_fuel * dt);
|
||||
accumulate(energy_, energy_roundoff_, d_energy * dt);
|
||||
accumulate(fuel_equivalent_mass_,equivalent_roundoff_,d_equivalent*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");
|
||||
@@ -118,6 +124,8 @@ public:
|
||||
air_mass_ = std::max(air_mass_, 0.0f);
|
||||
exhaust_mass_ = std::max(exhaust_mass_, 0.0f);
|
||||
fuel_mass_ = std::max(fuel_mass_, 0.0f);
|
||||
fuel_equivalent_mass_=std::clamp(fuel_equivalent_mass_,fuel_mass_,mass());
|
||||
if(fuel_equivalent_mass_<std::numeric_limits<float>::min()){fuel_equivalent_mass_=0;equivalent_roundoff_=0;}
|
||||
sync_ports();
|
||||
}
|
||||
void update_state(float dt) override { update(dt); }
|
||||
@@ -132,6 +140,7 @@ public:
|
||||
|
||||
private:
|
||||
float volume_, air_mass_{}, exhaust_mass_{}, fuel_mass_{}, energy_;
|
||||
float fuel_equivalent_mass_{};double equivalent_roundoff_{};
|
||||
// Preserve sub-ulp transfers without changing the float port/budget ABI.
|
||||
double air_roundoff_{}, exhaust_roundoff_{}, fuel_roundoff_{}, energy_roundoff_{};
|
||||
static void accumulate(float& value, double& roundoff, double increment) {
|
||||
@@ -149,6 +158,7 @@ private:
|
||||
p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f);
|
||||
p.air_fraction = air_fraction();
|
||||
p.fuel_fraction = fuel_fraction();
|
||||
p.fuel_equivalent_fraction=fuel_equivalent_fraction();
|
||||
}
|
||||
void sync_ports() {
|
||||
for (Port &p : ports_)
|
||||
@@ -161,10 +171,12 @@ struct GasCell {
|
||||
float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))};
|
||||
float air{1.0f};
|
||||
float fuel{};
|
||||
float fuel_equivalent{};
|
||||
};
|
||||
struct GasState {
|
||||
float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f};
|
||||
float fuel{};
|
||||
float fuel_equivalent{};
|
||||
};
|
||||
|
||||
class Pipe1D {
|
||||
@@ -172,7 +184,7 @@ 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{};
|
||||
float m{}, p{}, e{}, y{}, fuel{}, fuel_equivalent{};
|
||||
};
|
||||
struct RoeState { double root_density{}, enthalpy{}; };
|
||||
PipeSolver solver{PipeSolver::Rusanov};
|
||||
@@ -200,6 +212,11 @@ public:
|
||||
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");
|
||||
if(std::any_of(cell_areas.begin(),cell_areas.end(),[&](float a){return a!=cell_areas.front();})){
|
||||
geometry_cfl_lengths_.resize(cells.size());
|
||||
for(std::size_t i=0;i<cells.size();++i){const float a=cell_areas[i],left=i?.5f*(cell_areas[i-1]+a):a,right=i+1<cells.size()?.5f*(a+cell_areas[i+1]):a;
|
||||
geometry_cfl_lengths_[i]=cell_lengths[i]*(a/std::max({a,left,right}));}
|
||||
}
|
||||
}
|
||||
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); }
|
||||
@@ -212,7 +229,9 @@ public:
|
||||
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));
|
||||
float speed=wave(s),length=cell_lengths[i];
|
||||
if(!geometry_cfl_lengths_.empty()){length=geometry_cfl_lengths_[i];if(i)speed=std::max(speed,wave(primitive(cells[i-1])));if(i+1<cells.size())speed=std::max(speed,wave(primitive(cells[i+1])));}
|
||||
dt = std::min(dt, cfl * length / speed);
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
@@ -242,6 +261,13 @@ public:
|
||||
throw std::runtime_error("nonphysical pipe state in preparation");
|
||||
dt = std::min(dt, cfl * cell_lengths[i] / waves_[i]);
|
||||
}
|
||||
// A narrow cell exchanges flux through faces larger than its own area.
|
||||
// Limit that face/volume ratio and use each face's fastest adjacent wave.
|
||||
// Uniform pipes retain the original fast preparation path.
|
||||
if(!geometry_cfl_lengths_.empty())for(std::size_t i=0;i<cells.size();++i){
|
||||
float speed=waves_[i];if(i)speed=std::max(speed,waves_[i-1]);if(i+1<cells.size())speed=std::max(speed,waves_[i+1]);
|
||||
dt=std::min(dt,cfl*geometry_cfl_lengths_[i]/speed);
|
||||
}
|
||||
if (solver == PipeSolver::Hlle || solver == PipeSolver::Hllc)
|
||||
for (std::size_t i=0; i<cells.size(); ++i) {
|
||||
const auto& s=states_[i];
|
||||
@@ -272,7 +298,7 @@ public:
|
||||
const float u = c.momentum / std::max(c.rho, 1e-12f);
|
||||
const float air = std::clamp(c.air, 0.0f, 1.0f);
|
||||
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), air,
|
||||
std::clamp(c.fuel, 0.0f, 1.0f - air)};
|
||||
std::clamp(c.fuel, 0.0f, 1.0f - air),std::clamp(std::max(c.fuel,c.fuel_equivalent),0.f,1.f)};
|
||||
}
|
||||
static float wave(const GasState &s) {
|
||||
return std::abs(s.velocity) + std::sqrt(gamma * s.pressure / s.rho);
|
||||
@@ -301,7 +327,7 @@ public:
|
||||
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};
|
||||
return Flux{m,m*q.velocity+q.pressure,float((energy+q.pressure)*q.velocity),m*q.air,m*q.fuel,m*q.fuel_equivalent};
|
||||
};
|
||||
const Flux fa=physical(a,ea), fb=physical(b,eb);
|
||||
if(sl>=0) return fa;
|
||||
@@ -312,7 +338,7 @@ public:
|
||||
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)};
|
||||
float((sr*fa.fuel-sl*fb.fuel+diff*(b.rho*b.fuel-a.rho*a.fuel))*inv),float((sr*fa.fuel_equivalent-sl*fb.fuel_equivalent+diff*(b.rho*b.fuel_equivalent-a.rho*a.fuel_equivalent))*inv)};
|
||||
};
|
||||
if constexpr(method==PipeSolver::Hlle) return hlle();
|
||||
const double dl=a.rho*(sl-ua), dr=b.rho*(sr-ub);
|
||||
@@ -333,7 +359,7 @@ public:
|
||||
}
|
||||
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)};
|
||||
float(f.e+speed*(energy-(left?ea:eb))),float(mass*q.air),float(mass*q.fuel),float(mass*q.fuel_equivalent)};
|
||||
}
|
||||
static Flux numerical_flux(const GasState& a,const GasState& b,PipeSolver solver,
|
||||
float ca,float cb,bool* fallback=nullptr) {
|
||||
@@ -407,6 +433,7 @@ public:
|
||||
float en = cells[i].energy - factor * (fr.e - fl.e);
|
||||
const float air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
|
||||
const float fuel = cells[i].rho * cells[i].fuel - factor * (fr.fuel - fl.fuel);
|
||||
const float equivalent=cells[i].rho*std::max(cells[i].fuel_equivalent,cells[i].fuel)-factor*(fr.fuel_equivalent-fl.fuel_equivalent);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) ||
|
||||
!std::isfinite(air) || !std::isfinite(fuel))
|
||||
throw std::runtime_error("nonfinite finite-volume update");
|
||||
@@ -434,9 +461,10 @@ public:
|
||||
// subnormal concentrations; total gas mass and energy are unchanged.
|
||||
const float fraction = std::clamp(air / rho, 0.0f, 1.0f);
|
||||
const float fuel_fraction = std::clamp(fuel / rho, 0.0f, 1.0f - fraction);
|
||||
const float equivalent_fraction=std::clamp(equivalent/rho,fuel_fraction,1.f);
|
||||
next_cells_[i] = {rho, mom, en,
|
||||
fraction < std::numeric_limits<float>::min() ? 0.0f : fraction,
|
||||
fuel_fraction < std::numeric_limits<float>::min() ? 0.0f : fuel_fraction};
|
||||
fuel_fraction < std::numeric_limits<float>::min() ? 0.0f : fuel_fraction,equivalent_fraction<std::numeric_limits<float>::min()?0.f:equivalent_fraction};
|
||||
}
|
||||
cells.swap(next_cells_);
|
||||
prepared_ = false;
|
||||
@@ -444,11 +472,12 @@ public:
|
||||
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, f.fuel * a};
|
||||
return {f.m * a, f.p * a, f.e * a, f.y * a, f.fuel * a,f.fuel_equivalent*a};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<GasCell> next_cells_;
|
||||
std::vector<float> geometry_cfl_lengths_;
|
||||
std::vector<GasState> states_;
|
||||
std::vector<GasState> reconstructed_left_, reconstructed_right_;
|
||||
std::vector<float> waves_;
|
||||
@@ -480,13 +509,14 @@ private:
|
||||
const float dp=minmod(q.pressure-a.pressure,b.pressure-q.pressure);
|
||||
const float da=minmod(q.air-a.air,b.air-q.air);
|
||||
const float df=minmod(q.fuel-a.fuel,b.fuel-q.fuel);
|
||||
const float de=minmod(q.fuel_equivalent-a.fuel_equivalent,b.fuel_equivalent-q.fuel_equivalent);
|
||||
const float h=.5f*dt/cell_lengths[i];
|
||||
const GasState half{q.rho-h*(q.velocity*dr+q.rho*du),
|
||||
q.velocity-h*(q.velocity*du+dp/q.rho),
|
||||
q.pressure-h*(q.velocity*dp+gamma*q.pressure*du),
|
||||
q.air-h*q.velocity*da,q.fuel-h*q.velocity*df};
|
||||
q.air-h*q.velocity*da,q.fuel-h*q.velocity*df,q.fuel_equivalent-h*q.velocity*de};
|
||||
const auto edge=[&](float sign) { return GasState{half.rho+sign*dr,
|
||||
half.velocity+sign*du,half.pressure+sign*dp,half.air+sign*da,half.fuel+sign*df}; };
|
||||
half.velocity+sign*du,half.pressure+sign*dp,half.air+sign*da,half.fuel+sign*df,half.fuel_equivalent+sign*de}; };
|
||||
const auto left=edge(-.5f), right=edge(.5f);
|
||||
const auto physical=[](const GasState& v) {
|
||||
return v.rho>1e-12f && v.pressure>1e-6f && std::isfinite(v.pressure) &&
|
||||
@@ -510,7 +540,7 @@ private:
|
||||
.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)),
|
||||
.5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel))};
|
||||
.5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel)),.5f*(ma*a.fuel_equivalent+mb*b.fuel_equivalent-c*(b.rho*b.fuel_equivalent-a.rho*a.fuel_equivalent))};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -668,7 +698,7 @@ private:
|
||||
struct JunctionNode {
|
||||
std::vector<JunctionFace> ends;
|
||||
std::vector<GasState> states;
|
||||
struct Face { double pressure{},rho{},velocity{},mass{},enthalpy{},air{},fuel{},loss{}; };
|
||||
struct Face { double pressure{},rho{},velocity{},mass{},enthalpy{},air{},fuel{},loss{},equivalent{}; };
|
||||
std::vector<Face> faces;
|
||||
JunctionMethod method{JunctionMethod::Characteristic};
|
||||
double turning_loss{};
|
||||
@@ -689,7 +719,7 @@ private:
|
||||
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;
|
||||
double area_sum=0,rho=0,energy=0,air=0,fuel=0,equivalent=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;
|
||||
@@ -697,8 +727,10 @@ private:
|
||||
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);
|
||||
equivalent+=a*s.rho*s.fuel_equivalent*(1+u/speed);
|
||||
}
|
||||
rho/=area_sum;energy/=area_sum;air/=area_sum;fuel/=area_sum;
|
||||
equivalent/=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];
|
||||
@@ -710,7 +742,8 @@ private:
|
||||
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)});
|
||||
const double marker=.5*a*(s.rho*s.fuel_equivalent*u-speed*(equivalent-s.rho*s.fuel_equivalent));
|
||||
p.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*heat),float(orientation*y),float(orientation*f),float(orientation*marker)});
|
||||
}
|
||||
}
|
||||
// Nonlinear outgoing acoustic-wave curves (shock or rarefaction), with
|
||||
@@ -743,7 +776,7 @@ private:
|
||||
};
|
||||
// Each pressure evaluation writes scratch face states only, never pipes.
|
||||
const auto residual=[&](double pressure) {
|
||||
double incoming=0, heat=0, air=0, fuel=0;
|
||||
double incoming=0, heat=0, air=0, fuel=0,equivalent=0;
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
const auto q=node.states[k]; auto& f=node.faces[k];
|
||||
double p=pressure;
|
||||
@@ -757,6 +790,7 @@ private:
|
||||
f.mass=pipe.cell_area(end(pipe,e.left))*f.rho*f.velocity;
|
||||
f.enthalpy=gamma/(gamma-1)*p/f.rho+.5*f.velocity*f.velocity;
|
||||
f.air=q.air; f.fuel=q.fuel;
|
||||
f.equivalent=q.fuel_equivalent;if(f.mass>0)equivalent+=f.mass*f.equivalent;
|
||||
if(f.mass>0) { incoming+=f.mass; heat+=f.mass*f.enthalpy; air+=f.mass*q.air; fuel+=f.mass*q.fuel; }
|
||||
}
|
||||
double balance=0;
|
||||
@@ -769,6 +803,7 @@ private:
|
||||
const auto e=node.ends[k]; const auto& pipe=pipes_[e.pipe];
|
||||
f.mass=pipe.cell_area(end(pipe,e.left))*f.rho*f.velocity;
|
||||
f.enthalpy=h; f.air=air/incoming; f.fuel=fuel/incoming;
|
||||
f.equivalent=equivalent/incoming;
|
||||
}
|
||||
balance+=f.mass;
|
||||
}
|
||||
@@ -808,12 +843,13 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
double incoming=0, outgoing=0, heat=0, air=0, fuel=0, scale=1;
|
||||
double incoming=0, outgoing=0, heat=0, air=0, fuel=0, scale=1,equivalent=0;
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
const auto& f=node.faces[k];
|
||||
if(!std::isfinite(f.mass) || !std::isfinite(f.enthalpy)) return false;
|
||||
if(std::abs(f.velocity)>=.98*std::sqrt(gamma*f.pressure/f.rho)) return false;
|
||||
if(f.mass>0) { incoming+=f.mass;heat+=f.mass*f.enthalpy;air+=f.mass*f.air;fuel+=f.mass*f.fuel; }
|
||||
if(f.mass>0)equivalent+=f.mass*f.equivalent;
|
||||
else outgoing-=f.mass;
|
||||
}
|
||||
if(incoming>1e-16 && outgoing<=1e-16) {
|
||||
@@ -821,12 +857,14 @@ private:
|
||||
// Residual tolerance at rest must not create a one-sided mass source.
|
||||
for(auto& face:node.faces) face.mass=0;
|
||||
incoming=heat=air=fuel=0;
|
||||
equivalent=0;
|
||||
}
|
||||
// Remove root roundoff by scaling all outflows together. A shared donor
|
||||
// budget scales advective fluxes together, preserving node conservation.
|
||||
for(std::size_t k=0;k<node.ends.size();++k) {
|
||||
auto& f=node.faces[k];
|
||||
if(f.mass<0) { f.mass*=outgoing>0?incoming/outgoing:0; f.enthalpy=incoming>0?heat/incoming:0; f.air=incoming>0?air/incoming:0; f.fuel=incoming>0?fuel/incoming:0; }
|
||||
if(f.mass<0)f.equivalent=incoming>0?equivalent/incoming:0;
|
||||
if(f.mass>0) {
|
||||
const auto e=node.ends[k]; const auto& pipe=pipes_[e.pipe];const int i=end(pipe,e.left);
|
||||
scale=std::min(scale,.25*pipe.cell_mass(i)/(f.mass*dt));
|
||||
@@ -838,7 +876,7 @@ private:
|
||||
const auto e=node.ends[k]; auto& pipe=pipes_[e.pipe]; const auto& f=node.faces[k];
|
||||
const double orientation=e.left?-1:1, m=f.mass*scale;
|
||||
const double momentum=m*f.velocity+f.pressure*pipe.cell_area(end(pipe,e.left));
|
||||
pipe.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*m*f.enthalpy),float(orientation*m*f.air),float(orientation*m*f.fuel)});
|
||||
pipe.set_boundary_flux(e.left,{float(orientation*m),float(momentum),float(orientation*m*f.enthalpy),float(orientation*m*f.air),float(orientation*m*f.fuel),float(orientation*m*f.equivalent)});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1036,17 +1074,19 @@ private:
|
||||
const float direction = o.left ? 1.f : -1.f;
|
||||
const float species = from ? v.air_fraction : s.air;
|
||||
const float fuel = from ? v.fuel_fraction : s.fuel;
|
||||
const float equivalent=from?std::max(v.fuel_equivalent_fraction,v.fuel_fraction):s.fuel_equivalent;
|
||||
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, signed_mass * fuel});
|
||||
ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel});
|
||||
signed_mass * h, signed_mass * species, signed_mass * fuel,signed_mass*equivalent});
|
||||
ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel,equivalent});
|
||||
v.mass_flow = -mdot;
|
||||
v.enthalpy = h;
|
||||
if (!from)
|
||||
v.air_fraction = s.air;
|
||||
if (!from)
|
||||
v.fuel_fraction = s.fuel;
|
||||
if(!from)v.fuel_equivalent_fraction=s.fuel_equivalent;
|
||||
}
|
||||
void resolve_open(Open &o, float dt) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
@@ -1078,13 +1118,14 @@ private:
|
||||
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;
|
||||
face.fuel_equivalent=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});
|
||||
mass_flux*enthalpy, mass_flux*face.air, mass_flux*face.fuel,mass_flux*face.fuel_equivalent});
|
||||
o.last_volume_flow = direction*face.velocity*o.area;
|
||||
o.last_mass_flow = direction*mass_flux;
|
||||
o.last_face_pressure = face.pressure;
|
||||
@@ -1098,7 +1139,7 @@ private:
|
||||
const bool out = o.left ? s.velocity <= -c : s.velocity >= c;
|
||||
const bool ambient_inflow = o.left ? u >= 0 : u <= 0;
|
||||
GasState g = out ? s : GasState{rho, u, o.pressure, ambient_inflow ? 1.f : s.air,
|
||||
ambient_inflow ? 0.f : s.fuel};
|
||||
ambient_inflow ? 0.f : s.fuel,ambient_inflow?0.f:s.fuel_equivalent};
|
||||
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);
|
||||
@@ -1123,10 +1164,10 @@ private:
|
||||
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});
|
||||
da * f.y, da * f.fuel,da*f.fuel_equivalent});
|
||||
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, db * f.fuel});
|
||||
db * f.y, db * f.fuel,db*f.fuel_equivalent});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
31
include/FluidSim/FuelInjector.hpp
Normal file
31
include/FluidSim/FuelInjector.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "FluidSim/MachinePorts.hpp"
|
||||
|
||||
namespace fluidsim::port {
|
||||
class Volume0D;
|
||||
// Initial delivery model: calibrated regulated rail, with declared ideal or
|
||||
// finite supply. Rail pressure dynamics and liquid plumbing are separate work.
|
||||
struct FuelInjectorConfig {
|
||||
enum class Supply {IdealRail,FiniteRail};
|
||||
Supply supply{Supply::FiniteRail};
|
||||
double maximum_mass_flow{.001}; // kg/s at the calibrated rail pressure
|
||||
double initial_fuel_mass{.1}; // kg, used only by FiniteRail
|
||||
float fuel_temperature{300}; // sensible energy of the vaporized fuel
|
||||
};
|
||||
struct FuelInjectorState {
|
||||
bool command_fault{},saturated{},empty{};
|
||||
double mass_flow{},delivered_mass{},remaining_mass{};
|
||||
};
|
||||
class FuelInjector {
|
||||
public:
|
||||
explicit FuelInjector(FuelInjectorConfig config={});
|
||||
// Continuous/batch-average delivery, not a sequential pulse-width model.
|
||||
// Adds conserved fuel species, tracer and sensible energy to its host.
|
||||
const FuelInjectorState& deliver(Volume0D& host,const SignalValue& command,
|
||||
double time,double dt);
|
||||
const FuelInjectorState& state()const{return state_;}
|
||||
private:
|
||||
FuelInjectorConfig config_;
|
||||
FuelInjectorState state_;
|
||||
};
|
||||
}
|
||||
288
include/FluidSim/Machine.hpp
Normal file
288
include/FluidSim/Machine.hpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#pragma once
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/PhysicalGeometry.hpp"
|
||||
#include "FluidSim/PipeSolver.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include "FluidSim/FuelInjector.hpp"
|
||||
#include "FluidSim/PipeProfile.hpp"
|
||||
#include "FluidSim/MachinePorts.hpp"
|
||||
#include "FluidSim/Mechanics.hpp"
|
||||
#include "FluidSim/MachineSensors.hpp"
|
||||
#include "FluidSim/MachineLogic.hpp"
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
namespace machine_math {
|
||||
Vec3 add(Vec3,Vec3);
|
||||
Vec3 subtract(Vec3,Vec3);
|
||||
Vec3 multiply(Vec3,float);
|
||||
float dot(Vec3,Vec3);
|
||||
Vec3 cross(Vec3,Vec3);
|
||||
float length(Vec3);
|
||||
Vec3 unit(Vec3);
|
||||
// Intrinsic XYZ Euler angles, in degrees. Rendering and picking use this transform.
|
||||
Vec3 rotate(Vec3,Vec3 degrees);
|
||||
Vec3 inverse_rotate(Vec3,Vec3 degrees);
|
||||
Vec3 rotate_euler_world(Vec3,unsigned axis,float degrees);
|
||||
}
|
||||
enum class MachinePartKind { Pipe, Chamber, Junction, Throttle, CheckValve, Cylinder, PistonPump, Reservoir, Cap, Carburetor, Ecu, Shaft, Flywheel, DriveMotor, Brake };
|
||||
inline constexpr int machine_part_kind_count=15;
|
||||
enum class MachineConnectionKind { Orifice, Inertance, Characteristic, Legacy, Control, Mechanical };
|
||||
inline constexpr int machine_connection_kind_count=6;
|
||||
const char* machine_part_name(MachinePartKind);
|
||||
const char* machine_part_key(MachinePartKind);
|
||||
const char* machine_connection_name(MachineConnectionKind);
|
||||
const char* machine_connection_key(MachineConnectionKind);
|
||||
struct MachineShaftConfig {
|
||||
float inertia{.15f},initial_speed{},initial_angle{};
|
||||
float friction_constant{},friction_viscous{},friction_quadratic{},starter_torque{};
|
||||
};
|
||||
struct MachineDriveConfig {float target_speed{100},torque{1};bool speed_control{true},enabled{true};};
|
||||
struct MachineBrakeConfig {float constant{1},viscous{},quadratic{};bool enabled{true};};
|
||||
struct MachineSensorEcuConfig {
|
||||
bool enabled{}; // False preserves the explicitly labeled legacy ECU.
|
||||
SensorEcuConfig::Strategy strategy{SensorEcuConfig::Strategy::Ignition};
|
||||
std::vector<float> fuel_rpm{0,6000},fuel_tps{0,1};
|
||||
std::vector<float> fuel_per_cycle{.000002f,.00002f,.000002f,.00002f};
|
||||
float displacement{.0003f},air_gas_constant{287},target_afr{14.7f};
|
||||
std::vector<float> ve_rpm{0,6000},ve_pressure{20000,100000},volumetric_efficiency{.9f,.9f,.9f,.9f};
|
||||
bool operator==(const MachineSensorEcuConfig&)const=default;
|
||||
};
|
||||
struct MachineInjectorConfig {
|
||||
std::uint32_t ecu{};
|
||||
std::string ecu_label;
|
||||
int cylinders{1}; // Number of calibrated cylinder doses delivered here.
|
||||
float maximum_flow{.001f},initial_mass{.1f},temperature{300};
|
||||
bool finite_supply{true};
|
||||
bool operator==(const MachineInjectorConfig&)const=default;
|
||||
};
|
||||
|
||||
struct MachinePart {
|
||||
std::uint32_t id{};
|
||||
MachinePartKind kind{MachinePartKind::Pipe};
|
||||
std::string name;
|
||||
Vec3 position{0,.25f,0}, rotation{}, size{.12f,.12f,.12f};
|
||||
// Pipe centerline in part-local metres. Length is always derived from it.
|
||||
std::vector<Vec3> points{{-.15f,0,0},{.15f,0,0}};
|
||||
// One diameter per route point, linearly interpolated along each segment.
|
||||
// Empty profiles are legacy/programmatic pipes using the two scalar values.
|
||||
std::vector<float> point_diameters;
|
||||
float diameter{.04f}, outlet_diameter{.04f};
|
||||
float pressure{101325}, temperature{300}, opening{1}, discharge{.8f};
|
||||
bool pedal_control{true};
|
||||
float cracking_pressure{100}, phase_degrees{}, maximum_lift{.008f};
|
||||
float intake_open{350}, intake_close{580}, exhaust_open{130}, exhaust_close{370};
|
||||
CylinderConfig cylinder{};
|
||||
FourStrokeCylinder::ValveDynamics valve{};
|
||||
FourStrokeCylinder::ValveDynamics exhaust_valve{};
|
||||
FourStrokeCylinder::CamProfile intake_profile,exhaust_profile;
|
||||
// Normalized body coordinates preserve imported port locations when resized.
|
||||
std::vector<Vec3> port_coordinates;
|
||||
// Stable identities paired with custom chamber/junction port coordinates.
|
||||
std::vector<std::string> port_keys;
|
||||
EcuConfig ecu;
|
||||
MachineSensorEcuConfig sensor_ecu;
|
||||
std::optional<MachineInjectorConfig> injector; // Explicit chamber attachment.
|
||||
float ignition_tdc_degrees{}; // Calibrated crank pickup angle, 0..360.
|
||||
CarburetorConfig carburetor;
|
||||
bool injection_enabled{true};
|
||||
// Junction method 0 stores gas in its visible box; 1/2 are zero-volume nodes.
|
||||
int junction_method{};
|
||||
float turning_loss{};
|
||||
MachineShaftConfig shaft;
|
||||
MachineDriveConfig drive;
|
||||
MachineBrakeConfig brake;
|
||||
};
|
||||
struct MachinePort {
|
||||
std::uint32_t part{};
|
||||
unsigned port{};
|
||||
bool operator==(const MachinePort&) const = default;
|
||||
};
|
||||
SensorEcuConfig machine_sensor_ecu_config(const MachinePart&);
|
||||
struct MachineLink {
|
||||
MachinePort a,b;
|
||||
MachineConnectionKind kind{MachineConnectionKind::Orifice};
|
||||
float discharge{1}, inertance_length{.01f}, loss{};
|
||||
};
|
||||
enum class MachineQuantity {Pressure,Temperature,MassFlow,AirFuelRatio,Lambda,ChargeAfr,Rpm,Torque,Power};
|
||||
inline constexpr std::size_t machine_probe_limit=8;
|
||||
inline constexpr float machine_afr_reading_limit=1000.f;
|
||||
enum class MachineReadingState {Valid,NoFuel,LeanLimit,FuelCut,AwaitingCharge};
|
||||
const char* machine_reading_state_name(MachineReadingState);
|
||||
const char* machine_quantity_name(MachineQuantity);
|
||||
const char* machine_quantity_unit(MachineQuantity);
|
||||
float machine_quantity_scale(MachineQuantity);
|
||||
bool machine_quantity_supported(const MachinePart&,MachineQuantity);
|
||||
struct MachineProbe {
|
||||
std::uint32_t id{},part{};
|
||||
std::string name;
|
||||
MachineQuantity quantity{MachineQuantity::Pressure};
|
||||
float position{.5f}; // fraction of pipe arclength; lumped parts ignore it
|
||||
float reference_afr{14.7f}; // stoichiometric AFR for the selected fuel
|
||||
bool generated_output{}; // Migrated legacy shaft torque/power excludes bench motor/load.
|
||||
bool operator==(const MachineProbe&)const=default;
|
||||
};
|
||||
struct MachineMeasurement {
|
||||
double time{};
|
||||
std::array<float,machine_probe_limit> values{}; // SI; undefined readings are NaN
|
||||
std::array<MachineReadingState,machine_probe_limit> states{};
|
||||
std::size_t count{};
|
||||
};
|
||||
struct MachineDocument {
|
||||
std::string name{"Untitled machine"};
|
||||
float cells_per_meter{40}, drive_rpm{}, shaft_inertia{.15f}, load_torque{};
|
||||
bool driven{};
|
||||
// Deprecated calibration fields above are read only by legacy migration.
|
||||
std::uint32_t primary_shaft{};
|
||||
float friction_constant{.15f},friction_viscous{.002f},friction_quadratic{},starter_torque{15};
|
||||
PipeSolver solver{PipeSolver::Rusanov};
|
||||
OutdoorScene scene{OutdoorScene::Wall};
|
||||
std::uint32_t next_id{1};
|
||||
std::vector<MachinePart> parts;
|
||||
std::vector<MachineLink> links;
|
||||
std::vector<MachineProbe> probes;
|
||||
std::vector<MachineSensor> sensors;
|
||||
std::uint32_t next_sensor_id{1};
|
||||
LogicGraph logic;
|
||||
MachinePart* find(std::uint32_t);
|
||||
const MachinePart* find(std::uint32_t) const;
|
||||
MachinePart& add(MachinePartKind,Vec3 position={0,.25f,0});
|
||||
MachineSensor& add_sensor(MachineSensorKind,std::uint32_t host,float position=.5f);
|
||||
MachineSensor* find_sensor(std::uint32_t);
|
||||
const MachineSensor* find_sensor(std::uint32_t)const;
|
||||
void remove_sensor(std::uint32_t);
|
||||
void remove(std::uint32_t);
|
||||
void disconnect(MachinePort);
|
||||
// At least one endpoint must be a pipe. Its centerline snaps to the other port.
|
||||
void connect(MachinePort,MachinePort,MachineConnectionKind=MachineConnectionKind::Orifice);
|
||||
void sync_connections(std::uint32_t moved_part=0);
|
||||
void transform_selection(const std::vector<std::uint32_t>& ids,Vec3 pivot,Vec3 translation,unsigned rotation_axis=0,float degrees=0);
|
||||
};
|
||||
bool machine_sensor_supported(const MachinePart&,MachineSensorKind,bool reference=false);
|
||||
MachineSensorStatus machine_sensor_binding(const MachineDocument&,const MachineSensor&);
|
||||
std::optional<Vec3> machine_sensor_position(const MachineDocument&,const MachineSensorTap&);
|
||||
void validate_machine_sensors(const MachineDocument&);
|
||||
bool machine_control_port(const MachinePart&,unsigned);
|
||||
MachinePortDescriptor machine_port_descriptor(const MachinePart&,unsigned);
|
||||
unsigned machine_port_index(const MachinePart&,const std::string& key);
|
||||
PortDomain machine_link_domain(const MachineDocument&,const MachineLink&);
|
||||
bool machine_rotational_part(MachinePartKind);
|
||||
std::uint32_t machine_shaft_for(const MachineDocument&,std::uint32_t part);
|
||||
std::uint32_t machine_primary_shaft(const MachineDocument&);
|
||||
bool machine_shaft_driven(const MachineDocument&,std::uint32_t shaft);
|
||||
// Convert an old shared-shaft document to visible mechanical parts once.
|
||||
void upgrade_machine_mechanics(MachineDocument&);
|
||||
bool machine_port_available(const MachineDocument&,MachinePort);
|
||||
unsigned machine_port_count(const MachinePart&);
|
||||
std::string machine_port_name(const MachinePart&,unsigned);
|
||||
Vec3 machine_local_port(const MachinePart&,unsigned);
|
||||
Vec3 machine_world_point(const MachinePart&,Vec3);
|
||||
Vec3 machine_local_point(const MachinePart&,Vec3);
|
||||
Vec3 machine_port_position(const MachinePart&,unsigned);
|
||||
std::vector<Vec3> machine_pipe_path(const MachinePart&);
|
||||
float machine_pipe_length(const MachinePart&);
|
||||
std::vector<float> machine_pipe_diameters(const MachinePart&);
|
||||
std::vector<PipeSection> machine_pipe_sections(const MachinePart&);
|
||||
float machine_pipe_diameter(const MachinePart&,float distance);
|
||||
void machine_insert_pipe_point(MachinePart&,std::size_t segment,Vec3 local_point);
|
||||
void machine_remove_pipe_point(MachinePart&,std::size_t point);
|
||||
float machine_part_volume(const MachinePart&);
|
||||
Vec3 machine_part_size(const MachinePart&);
|
||||
// Drafts may contain unconnected ports. Unconnected duct ends are explicitly capped at test time.
|
||||
void validate_machine(const MachineDocument&);
|
||||
std::vector<std::string> machine_diagnostics(const MachineDocument&);
|
||||
struct MachineConnectionPreview {
|
||||
bool allowed{};
|
||||
std::string message;
|
||||
std::optional<MachinePart> pipe;
|
||||
};
|
||||
// Dry-run the actual connection and validation without changing the design.
|
||||
MachineConnectionPreview machine_connection_preview(const MachineDocument&,MachinePort,MachinePort);
|
||||
bool machine_has_fuel_source(const MachineDocument&,std::uint32_t cylinder);
|
||||
struct MachineCheck {
|
||||
std::uint32_t part{};
|
||||
bool warning{};
|
||||
std::string title,advice;
|
||||
std::uint32_t sensor{};
|
||||
std::uint32_t logic_node{};
|
||||
};
|
||||
std::vector<MachineCheck> machine_checks(const MachineDocument&);
|
||||
std::filesystem::path machine_directory();
|
||||
std::vector<std::filesystem::path> machine_files();
|
||||
std::filesystem::path machine_assembly_directory();
|
||||
std::vector<std::filesystem::path> machine_assembly_files();
|
||||
// Center of the selected world geometry, including routed pipes.
|
||||
Vec3 machine_selection_center(const MachineDocument&,const std::vector<std::uint32_t>&);
|
||||
// Stores only selected parts, internal links and their probes, relative to the center.
|
||||
MachineDocument machine_extract_assembly(const MachineDocument&,const std::vector<std::uint32_t>&,std::string name={});
|
||||
// Inserts an independent copy atomically; returned IDs follow assembly.parts order.
|
||||
std::vector<std::uint32_t> machine_insert_assembly(MachineDocument&,const MachineDocument&,Vec3 center);
|
||||
MachineDocument load_machine(const std::filesystem::path&);
|
||||
void save_machine(const MachineDocument&,const std::filesystem::path&);
|
||||
MachineDocument machine_pump_example();
|
||||
MachineDocument machine_engine_example();
|
||||
MachineDocument machine_logic_example();
|
||||
MachineDocument machine_sensor_engine_example(bool injection=false);
|
||||
MachineDocument machine_speed_density_example();
|
||||
struct ScenarioExample;
|
||||
MachineDocument machine_from_engine(const ScenarioExample&);
|
||||
|
||||
struct MachinePartState {
|
||||
std::uint32_t id{};
|
||||
float pressure{101325}, temperature{300}, volume{}, flow{}, piston_fraction{}, intake_lift{}, exhaust_lift{}, opening{};
|
||||
std::vector<float> pipe_pressures;
|
||||
float fuel_mass{},fuel_flow{},afr{},burn_fraction{};
|
||||
bool combustion{},fuel_cut{},ignition_cut{},idle_control{},valve_float{},fuel_source{};
|
||||
bool actuator_enabled{},signal_controlled{};
|
||||
double command_target{};
|
||||
double injector_remaining{},injector_delivered{};
|
||||
bool sensor_fault{},rev_limited{},injector_fault{},injector_empty{};
|
||||
float spark_advance{};
|
||||
std::uint32_t shaft{};
|
||||
double shaft_angle{},shaft_speed{},rotational_inertia{},torque{},power{},kinetic_energy{},mechanical_work{},mechanical_loss{};
|
||||
};
|
||||
struct MachineSnapshot {
|
||||
double time{};
|
||||
float rpm{}, torque{}, power{};
|
||||
std::uint64_t cells{},substeps{},positivity_repairs{};
|
||||
float minimum_cell_length{};
|
||||
std::uint32_t minimum_cell_part{};
|
||||
std::vector<MachinePartState> parts;
|
||||
std::uint32_t primary_shaft{};
|
||||
std::vector<ShaftState> shafts;
|
||||
std::vector<MachineSensorState> sensors;
|
||||
std::vector<LogicNodeState> logic;
|
||||
};
|
||||
// Owns the compiled graph. Editing never mutates a running solver; Test compiles a fresh graph.
|
||||
class MachineSimulation {
|
||||
public:
|
||||
explicit MachineSimulation(const MachineDocument&,int sample_rate=44100);
|
||||
~MachineSimulation();
|
||||
MachineSimulation(const MachineSimulation&)=delete;
|
||||
MachineSimulation& operator=(const MachineSimulation&)=delete;
|
||||
AudioFrame process();
|
||||
void set_controls(float pedal,bool starter=false);
|
||||
void set_listener(AudioListener);
|
||||
MachineSnapshot snapshot() const;
|
||||
MachineMeasurement measure() const;
|
||||
// Latest sample-boundary values, independent of recorded instrument channels.
|
||||
MachineSensorState sense(std::uint32_t sensor)const;
|
||||
void set_logic_control(std::uint32_t node,bool);
|
||||
void set_logic_control(std::uint32_t node,double);
|
||||
void release_logic_buttons();
|
||||
// Applies safe tuning and instrument changes between samples, preserving state.
|
||||
// Geometry, topology, initial conditions and compiled boundary settings must match.
|
||||
void apply_tuning(const MachineDocument&);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
111
include/FluidSim/MachineLogic.hpp
Normal file
111
include/FluidSim/MachineLogic.hpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
#include "FluidSim/MachineSensors.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
struct MachineDocument;
|
||||
enum class LogicNodeKind {
|
||||
BooleanConstant,Switch,Button,NumberConstant,Slider,NumberInput,
|
||||
And,Or,Not,Xor,Nand,Nor,Greater,Less,Equal,Window,Hysteresis,
|
||||
Add,Subtract,Multiply,Divide,Minimum,Maximum,Absolute,Clamp,GainOffset,MapRange,Select,BooleanToNumber,
|
||||
Sensor,Command,Lamp,Readout,BooleanDelay,NumberDelay,Ecu
|
||||
};
|
||||
inline constexpr int logic_node_kind_count=36;
|
||||
enum class LogicCommand {MotorEnable,MotorSpeed,MotorTorque,BrakeEnable,ThrottleOpening,Starter};
|
||||
inline constexpr int logic_command_count=6;
|
||||
struct LogicPosition {float x{},y{};bool operator==(const LogicPosition&)const=default;};
|
||||
struct LogicNode {
|
||||
std::uint32_t id{};
|
||||
LogicNodeKind kind{LogicNodeKind::Switch};
|
||||
std::string name;
|
||||
LogicPosition position;
|
||||
SignalQuantity quantity{SignalQuantity::Scalar},output_quantity{SignalQuantity::Scalar};
|
||||
PressureReference pressure_reference{PressureReference::None},output_pressure_reference{PressureReference::None};
|
||||
double value{},minimum{},maximum{1},low{.25},high{.75},tolerance{1e-6};
|
||||
double gain{1},offset{},false_value{},true_value{1},input_low{},input_high{1},output_low{},output_high{1};
|
||||
bool boolean{},initial{},below{},clamp{true};
|
||||
unsigned input_count{2};
|
||||
std::uint32_t sensor{},part{};
|
||||
MachineSensorKind sensor_kind{MachineSensorKind::Pressure}; // Schema survives an unresolved reference.
|
||||
SensorEcuConfig::Strategy ecu_strategy{SensorEcuConfig::Strategy::Ignition};
|
||||
LogicCommand command{LogicCommand::MotorEnable};
|
||||
std::string binding_label;
|
||||
bool operator==(const LogicNode&)const=default;
|
||||
};
|
||||
struct LogicEndpoint {
|
||||
std::uint32_t node{};
|
||||
std::string port;
|
||||
bool operator==(const LogicEndpoint&)const=default;
|
||||
};
|
||||
struct LogicLink {
|
||||
LogicEndpoint from,to;
|
||||
bool operator==(const LogicLink&)const=default;
|
||||
};
|
||||
struct LogicGraph {
|
||||
std::uint32_t next_id{1};
|
||||
std::vector<LogicNode> nodes;
|
||||
std::vector<LogicLink> links;
|
||||
LogicNode* find(std::uint32_t);
|
||||
const LogicNode* find(std::uint32_t)const;
|
||||
LogicNode& add(LogicNodeKind,LogicPosition={});
|
||||
void remove(std::uint32_t);
|
||||
void disconnect(const LogicEndpoint&);
|
||||
void connect(LogicEndpoint,LogicEndpoint); // Canonical output -> input; validates without mutation on failure.
|
||||
bool operator==(const LogicGraph&)const=default;
|
||||
};
|
||||
const char* logic_node_key(LogicNodeKind);
|
||||
const char* logic_node_name(LogicNodeKind);
|
||||
const char* logic_command_key(LogicCommand);
|
||||
const char* logic_command_name(LogicCommand);
|
||||
bool logic_memory_node(LogicNodeKind);
|
||||
std::vector<MachinePortDescriptor> logic_node_ports(const LogicNode&,const MachineDocument* context=nullptr);
|
||||
MachinePortDescriptor logic_port(const LogicGraph&,const LogicEndpoint&);
|
||||
PortCompatibility logic_connection_preview(const LogicGraph&,LogicEndpoint,LogicEndpoint);
|
||||
void validate_logic_graph(const LogicGraph&); // Drafts may have required unconnected inputs.
|
||||
std::vector<std::uint32_t> logic_evaluation_order(const LogicGraph&,std::vector<std::uint32_t>* cycle_path=nullptr);
|
||||
std::vector<std::string> logic_required_inputs(const LogicGraph&);
|
||||
bool logic_topology_equal(const LogicGraph&,const LogicGraph&);
|
||||
// Empty means a compatible physical binding. Missing targets remain unresolved.
|
||||
std::string logic_binding_problem(const MachineDocument&,const LogicNode&);
|
||||
const LogicNode* logic_command_owner(const MachineDocument&,std::uint32_t part,LogicCommand);
|
||||
void validate_machine_logic(const MachineDocument&);
|
||||
struct LogicNodeState {
|
||||
std::uint32_t id{};
|
||||
std::vector<SignalValue> values; // Same order as logic_node_ports, including inputs.
|
||||
std::string problem;
|
||||
};
|
||||
struct LogicCommandState {
|
||||
std::uint32_t node{},part{};
|
||||
LogicCommand command{};
|
||||
SignalValue value; // Invalid values require the declared inactive consumer fallback.
|
||||
};
|
||||
struct LogicEcuInputs {
|
||||
std::uint32_t node{},part{};
|
||||
SignalValue speed,angle,tps,map,iat;
|
||||
};
|
||||
class MachineLogicRuntime {
|
||||
public:
|
||||
using SensorReader=std::function<MachineSensorState(std::uint32_t)>;
|
||||
explicit MachineLogicRuntime(const MachineDocument&,SensorReader={});
|
||||
~MachineLogicRuntime();
|
||||
MachineLogicRuntime(const MachineLogicRuntime&)=delete;
|
||||
MachineLogicRuntime& operator=(const MachineLogicRuntime&)=delete;
|
||||
void evaluate(double time,double dt);
|
||||
SignalValue value(const LogicEndpoint&)const;
|
||||
std::vector<LogicNodeState> snapshot()const;
|
||||
const std::vector<LogicCommandState>& commands()const;
|
||||
const std::vector<LogicEcuInputs>& ecu_inputs()const;
|
||||
void set_manual(std::uint32_t,bool);
|
||||
void set_manual(std::uint32_t,double);
|
||||
void release_buttons();
|
||||
void apply_tuning(const MachineDocument&);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
51
include/FluidSim/MachinePorts.hpp
Normal file
51
include/FluidSim/MachinePorts.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class PortDomain {Gas,Rotational,Translational,Boolean,Number,EcuAssignment};
|
||||
enum class PortDirection {Bidirectional,Input,Output};
|
||||
enum class PortCardinality {Single,Multiple};
|
||||
// Quantities carry meaning as well as dimensions. Opening and scalar, or crank
|
||||
// angle and cycle phase, require an explicit conversion even when units match.
|
||||
enum class SignalQuantity {None,Scalar,Fraction,Pressure,Temperature,MassFlow,FreshAirMassFlow,AngularSpeed,Angle,CyclePhase,Torque,Power,Position,Velocity,Force,Time,Inertia};
|
||||
enum class PressureReference {None,Absolute,Gauge,Differential};
|
||||
enum class SignalValidity {Valid,Missing,Fault};
|
||||
// Runtime-only pickup evidence. Arithmetic creates new values without timing
|
||||
// authority; routing retains the original acquisition time, including delays.
|
||||
struct SignalTiming {
|
||||
std::uint32_t sensor{},shaft{};
|
||||
double sampled_at{},unwrapped_angle{},angular_speed{};
|
||||
bool operator==(const SignalTiming&)const=default;
|
||||
};
|
||||
struct SignalValue {
|
||||
std::variant<bool,double> value{false};
|
||||
SignalValidity validity{SignalValidity::Missing};
|
||||
double timestamp{};
|
||||
std::optional<SignalTiming> timing;
|
||||
static SignalValue boolean(bool,double timestamp=0);
|
||||
static SignalValue number(double,double timestamp=0);
|
||||
};
|
||||
struct MachinePortDescriptor {
|
||||
std::string key,name;
|
||||
PortDomain domain{PortDomain::Gas};
|
||||
PortDirection direction{PortDirection::Bidirectional};
|
||||
PortCardinality cardinality{PortCardinality::Single};
|
||||
SignalQuantity quantity{SignalQuantity::None};
|
||||
PressureReference pressure_reference{PressureReference::None};
|
||||
bool required{};
|
||||
std::variant<std::monostate,bool,double> default_value;
|
||||
std::optional<double> minimum,maximum;
|
||||
bool operator==(const MachinePortDescriptor&)const=default;
|
||||
};
|
||||
struct PortCompatibility {bool allowed{};std::string message;};
|
||||
const char* port_domain_key(PortDomain);
|
||||
const char* signal_quantity_key(SignalQuantity);
|
||||
const char* signal_quantity_unit(SignalQuantity); // Internal SI units.
|
||||
bool signal_domain(PortDomain);
|
||||
void validate_port_descriptor(const MachinePortDescriptor&);
|
||||
PortCompatibility port_compatibility(const MachinePortDescriptor&,const MachinePortDescriptor&);
|
||||
bool signal_value_valid_for(const SignalValue&,const MachinePortDescriptor&);
|
||||
}
|
||||
40
include/FluidSim/MachineSensors.hpp
Normal file
40
include/FluidSim/MachineSensors.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "FluidSim/MachinePorts.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace fluidsim::port {
|
||||
enum class MachineSensorKind {Pressure,DifferentialPressure,GasTemperature,MassFlow,Crank,ThrottlePosition};
|
||||
inline constexpr int machine_sensor_kind_count=6;
|
||||
struct MachineSensorTap {
|
||||
std::uint32_t part{};
|
||||
float position{.5f}; // Normalized pipe arclength, independent of fluid cells.
|
||||
std::string label; // Retains the intended host when a binding is unresolved.
|
||||
bool operator==(const MachineSensorTap&)const=default;
|
||||
};
|
||||
struct MachineSensor {
|
||||
std::uint32_t id{};
|
||||
MachineSensorKind kind{MachineSensorKind::Pressure};
|
||||
std::string name;
|
||||
MachineSensorTap target,reference;
|
||||
float angle_offset{}; // Radians, added to the physical shaft angle.
|
||||
bool operator==(const MachineSensor&)const=default;
|
||||
};
|
||||
enum class MachineSensorStatus {Valid,MissingHost,IncompatibleHost,Fault};
|
||||
struct MachineSensorState {
|
||||
std::uint32_t id{};
|
||||
std::array<SignalValue,3> outputs;
|
||||
unsigned count{};
|
||||
MachineSensorStatus status{MachineSensorStatus::MissingHost};
|
||||
// An ideal crank pickup preserves provenance and unwrapped crossings. An
|
||||
// arbitrary numeric RPM signal must never acquire timing authority from it.
|
||||
std::uint32_t timing_shaft{};
|
||||
double unwrapped_angle{};
|
||||
};
|
||||
const char* machine_sensor_key(MachineSensorKind);
|
||||
const char* machine_sensor_name(MachineSensorKind);
|
||||
const char* machine_sensor_status_name(MachineSensorStatus);
|
||||
unsigned machine_sensor_output_count(MachineSensorKind);
|
||||
MachinePortDescriptor machine_sensor_output(const MachineSensor&,unsigned);
|
||||
}
|
||||
54
include/FluidSim/Mechanics.hpp
Normal file
54
include/FluidSim/Mechanics.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace fluidsim::port {
|
||||
// Rotational quantities use SI: rad, rad/s, N m, kg m2, J and W.
|
||||
enum class DriveMode {Torque,Speed};
|
||||
struct ShaftParameters {
|
||||
double inertia{.15},initial_speed{},initial_angle{};
|
||||
double friction_constant{},friction_viscous{},friction_quadratic{};
|
||||
bool forward_only{};
|
||||
};
|
||||
struct DriveParameters {DriveMode mode{DriveMode::Torque};double command{};bool enabled{true};};
|
||||
struct BrakeParameters {double constant{},viscous{},quadratic{};bool enabled{true};};
|
||||
struct ShaftState {
|
||||
std::uint32_t id{};
|
||||
double angle{},previous_angle{},speed{},inertia{},kinetic_energy{};
|
||||
double net_torque{},source_torque{},external_torque{},friction_torque{},brake_torque{},constraint_torque{};
|
||||
double external_work{},drive_work{},friction_loss{},brake_loss{},initial_energy{};
|
||||
// initial_energy + external_work + drive_work = kinetic_energy + losses.
|
||||
};
|
||||
struct DriveState {
|
||||
std::uint32_t id{},shaft{};
|
||||
double torque{},power{},work{};
|
||||
};
|
||||
struct BrakeState {
|
||||
std::uint32_t id{},shaft{};
|
||||
double torque{},power{},loss{}; // Torque on shaft signed; absorbed power >= 0.
|
||||
};
|
||||
// Independent rigid shaft nodes. Gears and clutches will couple these nodes;
|
||||
// direct attachment adds torque or inertia to one named node exactly once.
|
||||
class MechanicalSystem {
|
||||
public:
|
||||
MechanicalSystem();
|
||||
~MechanicalSystem();
|
||||
MechanicalSystem(const MechanicalSystem&)=delete;
|
||||
MechanicalSystem& operator=(const MechanicalSystem&)=delete;
|
||||
void add_shaft(std::uint32_t,const ShaftParameters&);
|
||||
void add_flywheel(std::uint32_t id,std::uint32_t shaft,double inertia);
|
||||
void add_drive(std::uint32_t id,std::uint32_t shaft,const DriveParameters&);
|
||||
void add_brake(std::uint32_t id,std::uint32_t shaft,const BrakeParameters&);
|
||||
void set_drive(std::uint32_t,const DriveParameters&);
|
||||
void set_brake(std::uint32_t,const BrakeParameters&);
|
||||
void set_friction(std::uint32_t shaft,double constant,double viscous,double quadratic);
|
||||
void add_torque(std::uint32_t shaft,double torque);
|
||||
void step(double dt);
|
||||
const ShaftState& shaft(std::uint32_t)const;
|
||||
const DriveState& drive(std::uint32_t)const;
|
||||
const BrakeState& brake(std::uint32_t)const;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
@@ -35,4 +35,31 @@ inline Pipe1D make_profiled_pipe(std::span<const PipeSection> sections, float ma
|
||||
}
|
||||
return Pipe1D(std::move(areas), std::move(lengths));
|
||||
}
|
||||
|
||||
// Uniform cells over the full arclength, integrating every profile section that
|
||||
// a cell crosses. Route/profile knots do not force short cells; gas volume is
|
||||
// still the exact sum of the piecewise frustums at any mesh density.
|
||||
inline Pipe1D make_uniform_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");
|
||||
double total=0;float previous=sections.front().inlet_diameter;
|
||||
for(const auto& s:sections){
|
||||
if(!std::isfinite(s.length)||!std::isfinite(s.inlet_diameter)||!std::isfinite(s.outlet_diameter)||s.length<=0||s.inlet_diameter<=0||s.outlet_diameter<=0||std::abs(s.inlet_diameter-previous)>1e-6f)throw std::invalid_argument("invalid/discontinuous pipe profile");
|
||||
total+=s.length;previous=s.outlet_diameter;
|
||||
}
|
||||
const int count=profile_section_cells(float(total),maximum_cell_length);const double dx=total/count;
|
||||
std::vector<float> areas;areas.reserve(count);std::vector<float> lengths(count,float(dx));
|
||||
std::size_t section=0;double section_start=0,section_end=sections.front().length;
|
||||
for(int i=0;i<count;++i){
|
||||
double cursor=i*dx,volume=0;const double end=i+1==count?total:(i+1)*dx;
|
||||
while(cursor<end){
|
||||
while(section+1<sections.size()&&cursor>=section_end){section_start=section_end;section_end+=sections[++section].length;}
|
||||
const auto& s=sections[section];const double stop=std::min(end,section_end);
|
||||
const double d0=std::lerp(double(s.inlet_diameter),double(s.outlet_diameter),std::clamp((cursor-section_start)/s.length,0.,1.));
|
||||
const double d1=std::lerp(double(s.inlet_diameter),double(s.outlet_diameter),std::clamp((stop-section_start)/s.length,0.,1.));
|
||||
volume+=double(pi)/12*(stop-cursor)*(d0*d0+d0*d1+d1*d1);cursor=stop;
|
||||
}
|
||||
areas.push_back(float(volume/dx));
|
||||
}
|
||||
return Pipe1D(std::move(areas),std::move(lengths));
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
bool write(float left, float right, std::stop_token stop = {});
|
||||
bool wait_for_space(std::size_t target_fill, std::stop_token stop);
|
||||
void reset_consumer();
|
||||
// Only while both producer and audio consumer are stopped.
|
||||
void discard();
|
||||
std::size_t read_interpolated(std::span<float> left, std::span<float> right, double speed);
|
||||
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user