180 lines
11 KiB
C++
180 lines
11 KiB
C++
#include "FluidSim/Turbocharger.hpp"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
|
|
namespace fluidsim::port {
|
|
namespace {
|
|
constexpr double cp = double(gamma)*gas_constant/(gamma-1);
|
|
constexpr double exponent = double(gamma-1)/gamma;
|
|
TurboConfig checked(TurboConfig c) {
|
|
for (double x : {c.shaft_inertia, c.compressor_radius, c.turbine_radius,
|
|
c.compressor_head_coefficient, c.turbine_optimum_speed_ratio,
|
|
c.compressor_effective_area, c.turbine_effective_area, c.gate_diaphragm_area,
|
|
c.gate_spring_stiffness, c.gate_damping, c.gate_diameter, c.reference_pressure})
|
|
if (!std::isfinite(x) || x <= 0) throw std::invalid_argument("invalid turbo geometry/inertia");
|
|
for (double x : {c.compressor_efficiency, c.turbine_peak_efficiency})
|
|
if (!std::isfinite(x) || x <= 0 || x > 1) throw std::invalid_argument("invalid turbo efficiency");
|
|
for (double x : {c.bearing_viscous, c.windage, c.gate_spring_preload, c.gate_maximum_lift})
|
|
if (!std::isfinite(x) || x < 0) throw std::invalid_argument("invalid turbo loss coefficient");
|
|
for (float x : c.connection_diameters)
|
|
if (!std::isfinite(x) || x <= 0) throw std::invalid_argument("invalid turbo connection diameter");
|
|
return c;
|
|
}
|
|
}
|
|
Turbocharger::Turbocharger(TurboConfig configuration)
|
|
: config(checked(configuration)),
|
|
compressor_inlet(config.compressor_inlet_volume), compressor_outlet(config.compressor_outlet_volume),
|
|
turbine_inlet(config.turbine_inlet_volume), turbine_outlet(config.turbine_outlet_volume),
|
|
compressor_inlet_port(compressor_inlet.create_port()),
|
|
compressor_outlet_port(compressor_outlet.create_port()),
|
|
turbine_inlet_port(turbine_inlet.create_port()), turbine_outlet_port(turbine_outlet.create_port()),
|
|
internal_{&compressor_inlet.create_port(), &compressor_outlet.create_port(),
|
|
&turbine_inlet.create_port(), &turbine_outlet.create_port()} {}
|
|
|
|
void Turbocharger::connect(BoundarySystem& b, const std::vector<Pipe1D>& pipes,
|
|
int ci, bool cil, int co, bool col, int ti, bool til, int to, bool tol) {
|
|
if (connected_) throw std::logic_error("turbo already connected");
|
|
const std::array<int, 4> indices{ci, co, ti, to};
|
|
const std::array<bool, 4> left{cil, col, til, tol};
|
|
auto external = ports();
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
const auto& p = pipes.at(indices[i]);
|
|
const float diameter = config.connection_diameters[i];
|
|
connection_areas_[i] = std::min(p.cell_area(left[i] ? 0 : int(p.cells.size())-1), pi*diameter*diameter*.25f);
|
|
b.add_orifice(*external[i], indices[i], left[i], connection_areas_[i]);
|
|
}
|
|
connected_ = true;
|
|
}
|
|
std::vector<Port*> Turbocharger::ports() {
|
|
return {&compressor_inlet_port, &compressor_outlet_port, &turbine_inlet_port, &turbine_outlet_port};
|
|
}
|
|
double Turbocharger::fluid_energy() const {
|
|
return double(compressor_inlet.internal_energy()) + compressor_outlet.internal_energy() +
|
|
turbine_inlet.internal_energy() + turbine_outlet.internal_energy();
|
|
}
|
|
void Turbocharger::set_initial_rpm(double rpm) {
|
|
if (!std::isfinite(rpm) || rpm < 0) throw std::invalid_argument("invalid initial turbo speed");
|
|
omega_ = rpm*2*pi/60;
|
|
}
|
|
double Turbocharger::nozzle(double p, double t, double back, double area) {
|
|
if (back >= p) return 0;
|
|
const double critical = std::pow(2/(gamma+1), gamma/(gamma-1));
|
|
const double ratio = std::max(back/p, critical);
|
|
const double factor = 2*gamma/(gas_constant*(gamma-1)) *
|
|
(std::pow(ratio, 2/gamma)-std::pow(ratio, (gamma+1)/gamma));
|
|
return area*p/std::sqrt(t)*std::sqrt(std::max(0., factor));
|
|
}
|
|
double Turbocharger::transfer_flow(Volume0D& in, Volume0D& out, Port& in_ext,
|
|
Port& out_ext, double requested, double dt) {
|
|
const bool forward = requested >= 0;
|
|
const auto& donor = forward ? in : out;
|
|
const auto& external = forward ? in_ext : out_ext;
|
|
const double h = cp*donor.temperature();
|
|
// Reserve the mass/energy already promised to the external pipe boundary.
|
|
const double mass = donor.mass()+std::min(0.f, external.mass_flow)*dt;
|
|
const double energy = donor.internal_energy()+std::min(0.f, external.mass_flow)*external.enthalpy*dt;
|
|
const double bound = .25*std::max(0., std::min(mass, energy/h))/dt;
|
|
const double actual = std::clamp(requested, -bound, bound);
|
|
if (actual != requested) ++limited_transfers_;
|
|
return actual;
|
|
}
|
|
void Turbocharger::set_transfer(Volume0D& in, Volume0D& out, Port& ip, Port& op,
|
|
double flow, double work, double dt) {
|
|
const auto& donor = flow >= 0 ? in : out;
|
|
const double h = cp*donor.temperature();
|
|
ip.mass_flow = float(-flow);
|
|
op.mass_flow = float(flow);
|
|
ip.air_fraction = op.air_fraction = donor.air_fraction();
|
|
ip.enthalpy = op.enthalpy = float(h);
|
|
// Positive work means energy delivered to the shaft. Only the receiving
|
|
// fluid enthalpy changes; the donor loses precisely its stagnation enthalpy.
|
|
if (std::abs(flow) > 0) {
|
|
auto& receiver = flow >= 0 ? op : ip;
|
|
receiver.enthalpy = float(h-work/(std::abs(flow)*dt));
|
|
}
|
|
}
|
|
void Turbocharger::update_state(float timestep) {
|
|
if (!std::isfinite(timestep) || timestep < 0) throw std::invalid_argument("invalid turbo timestep");
|
|
if (timestep == 0) return;
|
|
const double dt = timestep, initial_fluid = fluid_energy(), initial_shaft = shaft_energy();
|
|
const std::array<const Port*, 4> external{&compressor_inlet_port, &compressor_outlet_port,
|
|
&turbine_inlet_port, &turbine_outlet_port};
|
|
double external_work = 0;
|
|
for (const auto* p : external) external_work += double(p->mass_flow)*p->enthalpy*dt;
|
|
|
|
const double pin = compressor_inlet.pressure(), tin = compressor_inlet.temperature();
|
|
const double head = config.compressor_head_coefficient*std::pow(omega_*config.compressor_radius, 2);
|
|
const double pump_pressure = pin*std::pow(1+config.compressor_efficiency*head/(cp*tin), 1/exponent);
|
|
double mc = compressor_outlet.pressure() <= pump_pressure
|
|
? nozzle(pump_pressure, tin+head/cp, compressor_outlet.pressure(), config.compressor_effective_area)
|
|
: -nozzle(compressor_outlet.pressure(), compressor_outlet.temperature(), pump_pressure,
|
|
config.compressor_effective_area);
|
|
mc = transfer_flow(compressor_inlet, compressor_outlet, compressor_inlet_port, compressor_outlet_port, mc, dt);
|
|
|
|
const double tp = turbine_inlet.pressure(), tout = turbine_outlet.pressure();
|
|
const double can_force = std::max(0., double(compressor_outlet.pressure())-config.reference_pressure)*
|
|
config.gate_diaphragm_area;
|
|
const double equilibrium_lift = (can_force-config.gate_spring_preload)/config.gate_spring_stiffness;
|
|
// Overdamped spring-can linkage: b*dx/dt + k*x = F-Fpreload, with seats.
|
|
wastegate_lift_ = std::clamp(equilibrium_lift+(wastegate_lift_-equilibrium_lift)*
|
|
std::exp(-dt*config.gate_spring_stiffness/config.gate_damping), 0., config.gate_maximum_lift);
|
|
const double gate_area = pi*config.gate_diameter*wastegate_lift_;
|
|
double mt = tp >= tout ? nozzle(tp, turbine_inlet.temperature(), tout, config.turbine_effective_area)
|
|
: -nozzle(tout, turbine_outlet.temperature(), tp, config.turbine_effective_area);
|
|
double mw = tp >= tout ? nozzle(tp, turbine_inlet.temperature(), tout, gate_area)
|
|
: -nozzle(tout, turbine_outlet.temperature(), tp, gate_area);
|
|
const double requested = mt+mw;
|
|
const double actual = transfer_flow(turbine_inlet, turbine_outlet, turbine_inlet_port, turbine_outlet_port, requested, dt);
|
|
if (requested != 0) { mt *= actual/requested; mw *= actual/requested; }
|
|
const double ideal_drop = tp > tout ? cp*turbine_inlet.temperature()*(1-std::pow(tout/tp, exponent)) : 0;
|
|
const double jet = std::sqrt(2*ideal_drop), radius = config.turbine_radius;
|
|
const double ratio = config.turbine_optimum_speed_ratio, eta = config.turbine_peak_efficiency;
|
|
const double drive = std::max(0., mt)*eta*jet*radius/ratio;
|
|
const double turbine_drag = std::max(0., mt)*eta*.5*radius*radius/(ratio*ratio);
|
|
const double compressor_drag = std::max(0., mc)*config.compressor_head_coefficient*
|
|
config.compressor_radius*config.compressor_radius;
|
|
// Implicit midpoint solves I*domega/dt = drive - b*omega - c*omega^2.
|
|
// Evaluating work at midpoint conserves rotor kinetic energy, even at rest.
|
|
double wt = 0, wc = 0, loss = 0, remaining = dt;
|
|
int steps = 0;
|
|
while (remaining > dt*1e-12) {
|
|
const double drag = turbine_drag+compressor_drag+config.bearing_viscous+config.windage*omega_;
|
|
const double h = std::min(remaining, config.shaft_inertia/std::max(drag, 1e-300));
|
|
if (!(h > 0) || ++steps > 1000000) throw std::runtime_error("turbo shaft timestep failed");
|
|
const double b = 2*config.shaft_inertia/h+turbine_drag+compressor_drag+config.bearing_viscous;
|
|
const double rhs = 2*config.shaft_inertia/h*omega_+drive;
|
|
const double mid = 2*rhs/(b+std::sqrt(b*b+4*config.windage*rhs));
|
|
const double next = 2*mid-omega_;
|
|
if (next < 0) throw std::runtime_error("turbo shaft integration reversed unexpectedly");
|
|
wt += (drive-turbine_drag*mid)*mid*h;
|
|
wc += compressor_drag*mid*mid*h;
|
|
loss += (config.bearing_viscous+config.windage*mid)*mid*mid*h;
|
|
omega_ = next;
|
|
angle_ = std::fmod(angle_+mid*h, 2*double(pi));
|
|
remaining -= h;
|
|
}
|
|
bearing_heat_ += loss;
|
|
turbine_power_ = wt/dt;
|
|
compressor_power_ = wc/dt;
|
|
bearing_power_ = loss/dt;
|
|
turbine_flow_ = mt;
|
|
wastegate_flow_ = mw;
|
|
compressor_flow_ = mc;
|
|
set_transfer(compressor_inlet, compressor_outlet, *internal_[0], *internal_[1], mc, -wc, dt);
|
|
set_transfer(turbine_inlet, turbine_outlet, *internal_[2], *internal_[3], mt+mw, wt, dt);
|
|
compressor_inlet.update_state(timestep);
|
|
compressor_outlet.update_state(timestep);
|
|
turbine_inlet.update_state(timestep);
|
|
turbine_outlet.update_state(timestep);
|
|
energy_residual_ += fluid_energy()-initial_fluid + shaft_energy()-initial_shaft+loss-external_work;
|
|
}
|
|
TurboSnapshot Turbocharger::snapshot() const {
|
|
return {omega_*60/(2*pi), angle_, shaft_energy(), turbine_power_, compressor_power_, bearing_power_,
|
|
compressor_flow_, turbine_flow_, compressor_outlet.pressure()/compressor_inlet.pressure(),
|
|
turbine_inlet.pressure()/turbine_outlet.pressure(), turbine_inlet.pressure(),
|
|
turbine_outlet.pressure(), compressor_outlet.pressure(), compressor_outlet.temperature(),
|
|
bearing_heat_, energy_residual_, limited_transfers_, wastegate_flow_, wastegate_lift_};
|
|
}
|
|
} // namespace fluidsim::port
|