Add machine workshop, typed logic, mechanics and sensor-driven ECUs

This commit is contained in:
max
2026-11-02 07:27:13 +01:00
parent eb2b6366c2
commit abfcce0f46
99 changed files with 28835 additions and 145 deletions

View 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_;
};
}