17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-06 05:48:35 +01:00

First implementation of graph vehicles

This commit is contained in:
2026-10-14 12:20:49 +02:00
parent 70abf879b3
commit 1ef465264c
121 changed files with 12292 additions and 729 deletions

View File

@@ -43,6 +43,18 @@ std::vector<std::string> split_labels(std::string const &Text)
} // namespace
int cab_of(block_setup const &Setup)
{
auto const index{find_param(Setup.param_defs, "cab")};
if (index < 0)
{
return 0;
}
// "any", "1", "2", "0" (the engine room)
auto const &value{Setup.params[index].text};
return value == "1" ? 1 : value == "2" ? 2 : value == "0" ? -1 : 0;
}
// cab_switch
cab_switch::cab_switch(block_setup const &Setup)
@@ -50,7 +62,10 @@ cab_switch::cab_switch(block_setup const &Setup)
m_positioncount(std::max(1, static_cast<int>(std::lround(number(Setup, param::positions))))), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)),
m_spring(boolean(Setup, param::spring)), m_labels(split_labels(text(Setup, param::labels))), m_animation(static_cast<animation_type>(choice(Setup, param::animation))),
m_step(number(Setup, param::step)), m_offset(number(Setup, param::offset)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_keyincrease(text(Setup, param::key_inc)), m_keydecrease(text(Setup, param::key_dec)), m_command(text(Setup, param::command)), m_hardwarename(text(Setup, param::hw_name))
m_soundincrease(text(Setup, param::sound_inc)), m_sounddecrease(text(Setup, param::sound_dec)),
m_soundpositions(text(Setup, param::sound_positions)),
m_keyincrease(text(Setup, param::key_inc)), m_keydecrease(text(Setup, param::key_dec)), m_command(text(Setup, param::command)), m_hardwarename(text(Setup, param::hw_name)),
m_cab(cab_of(Setup))
{
// the default is given as a value, as the author sees it; the switch starts at the position closest to it
auto const defaultvalue{number(Setup, param::default_value)};
@@ -59,6 +74,7 @@ cab_switch::cab_switch(block_setup const &Setup)
m_defaultposition = clamp_position(static_cast<int>(std::lround((defaultvalue - m_min) / (m_max - m_min) * (m_positioncount - 1))));
}
m_position = m_defaultposition;
m_display = static_cast<float>(m_position);
}
int cab_switch::clamp_position(int const Position) const
@@ -84,6 +100,7 @@ void cab_switch::update(frame &Frame)
{
set(Frame, port::value, value_at(m_position));
set(Frame, port::index, m_position);
m_display = static_cast<float>(m_position);
}
void cab_switch::increase()
@@ -181,10 +198,14 @@ block_type cab_switch::describe()
number_param("offset", 0.0, "deg/mm"),
number_param("smoothing", 0.2, "s"),
text_param("submodel"),
text_param("sound_inc"),
text_param("sound_dec"),
text_param("sound_positions"),
text_param("key_inc"),
text_param("key_dec"),
text_param("command"),
text_param("hw_name"),
cab_param(),
};
type.create = make_block<cab_switch>;
return type;
@@ -194,7 +215,8 @@ block_type cab_switch::describe()
cab_lamp::cab_lamp(block_setup const &Setup)
: block(Setup), m_supplied(is_connected(Setup, port::supply)), m_blink(boolean(Setup, param::blink)), m_blinkperiod(std::max(0.01, number(Setup, param::blink_period))),
m_minvoltage(number(Setup, param::min_voltage)), m_power(std::max(0.0, number(Setup, param::power))), m_submodel(text(Setup, param::submodel))
m_minvoltage(number(Setup, param::min_voltage)), m_power(std::max(0.0, number(Setup, param::power))), m_submodel(text(Setup, param::submodel)),
m_soundon(text(Setup, param::sound_on)), m_soundoff(text(Setup, param::sound_off)), m_cab(cab_of(Setup))
{
}
@@ -233,6 +255,9 @@ block_type cab_lamp::describe()
number_param("min_voltage", 70.0, "V"),
number_param("power", 2.0, "W"),
text_param("submodel"),
text_param("sound_on"),
text_param("sound_off"),
cab_param(),
};
type.create = make_block<cab_lamp>;
return type;
@@ -243,7 +268,7 @@ block_type cab_lamp::describe()
cab_gauge::cab_gauge(block_setup const &Setup, bool const Twoneedles)
: block(Setup), m_twoneedles(Twoneedles), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)), m_anglemin(number(Setup, param::angle_min)),
m_anglemax(number(Setup, param::angle_max)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_submodel2(Twoneedles ? text(Setup, param::submodel2) : std::string{})
m_submodel2(Twoneedles ? text(Setup, param::submodel2) : std::string{}), m_cab(cab_of(Setup))
{
m_values[0] = m_values[1] = static_cast<float>(std::min(m_min, m_max));
}
@@ -292,6 +317,7 @@ block_type cab_gauge::describe(bool const Twoneedles)
{
type.params.push_back(text_param("submodel2"));
}
type.params.push_back(cab_param());
if (Twoneedles)
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, true); };
@@ -308,7 +334,7 @@ block_type cab_gauge::describe(bool const Twoneedles)
vehicle_sound::vehicle_sound(block_setup const &Setup)
: block(Setup), m_file(text(Setup, param::file)), m_basevolume(number(Setup, param::volume)), m_basepitch(number(Setup, param::pitch)), m_loop(boolean(Setup, param::loop)),
m_placement(static_cast<placement_type>(choice(Setup, param::placement))), m_range(number(Setup, param::range)),
m_offset{number(Setup, param::offset_x), number(Setup, param::offset_y), number(Setup, param::offset_z)}
m_offset{number(Setup, param::offset_x), number(Setup, param::offset_y), number(Setup, param::offset_z)}, m_cab(cab_of(Setup))
{
}
@@ -364,11 +390,35 @@ block_type vehicle_sound::describe()
number_param("offset_x", 0.0, "m"),
number_param("offset_y", 0.0, "m"),
number_param("offset_z", 0.0, "m"),
cab_param(),
};
type.create = make_block<vehicle_sound>;
return type;
}
// cab_export
cab_export::cab_export(block_setup const &Setup) : block(Setup), m_name(text(Setup, 0)), m_kind(static_cast<kind_type>(choice(Setup, 1))) {}
void cab_export::update(frame &Frame)
{
m_value = get(Frame, port::value);
}
block_type cab_export::describe()
{
block_type type;
type.id = "fb.export";
type.category = "indicators";
type.name = "Screen value";
type.description = "Publishes the value for the python screens of the cab, as an entry of the train state with the given name.";
type.origin = block_origin::cab;
type.ports = {signal_in("value", value_type::real)};
type.params = {text_param("name"), choice_param("kind", {"float", "int", "bool"}, "float")};
type.create = make_block<cab_export>;
return type;
}
void register_cab_blocks(catalog &Catalog)
{
Catalog.add(cab_switch::describe());
@@ -376,6 +426,7 @@ void register_cab_blocks(catalog &Catalog)
Catalog.add(cab_gauge::describe(false));
Catalog.add(cab_gauge::describe(true));
Catalog.add(vehicle_sound::describe());
Catalog.add(cab_export::describe());
}
} // namespace vgraph

View File

@@ -48,10 +48,14 @@ public:
offset,
smoothing,
submodel,
sound_inc,
sound_dec,
sound_positions,
key_inc,
key_dec,
command,
hw_name
hw_name,
cab
};
};
enum class behaviour_type
@@ -136,6 +140,21 @@ public:
{
return m_submodel;
}
// sounds played by the animated submodel when the position increases or decreases, in the format of
// soundinc: and sounddec: of the .mmd controls
std::string const &sound_increase() const
{
return m_soundincrease;
}
std::string const &sound_decrease() const
{
return m_sounddecrease;
}
// sounds of single positions, as soundN: entries of the .mmd controls with N the number of the position
std::string const &sound_positions() const
{
return m_soundpositions;
}
// bindings: keys, existing simulator command, name for the hardware desk
std::string const &key_increase() const
{
@@ -153,6 +172,16 @@ public:
{
return m_hardwarename;
}
// cab whose model holds the submodel: 0 any, 1 or 2
int cab() const
{
return m_cab;
}
// stable address of the value animating the submodel (the position), for TGauge::AssignFloat
float *display_state()
{
return &m_display;
}
static block_type describe();
@@ -171,11 +200,16 @@ private:
double const m_offset;
double const m_smoothing;
std::string const m_submodel;
std::string const m_soundincrease;
std::string const m_sounddecrease;
std::string const m_soundpositions;
std::string const m_keyincrease;
std::string const m_keydecrease;
std::string const m_command;
std::string const m_hardwarename;
int const m_cab;
int m_position{0};
float m_display{0.f};
};
// indicator lamp. when the graph simulates low voltage circuits and the supply input is connected, the lamp lights
@@ -200,7 +234,10 @@ public:
blink_period,
min_voltage,
power,
submodel
submodel,
sound_on,
sound_off,
cab
};
};
@@ -229,6 +266,19 @@ public:
{
return m_submodel;
}
// sounds played when the lamp lights and goes out, as soundinc: and sounddec: of the .mmd lamps
std::string const &sound_on() const
{
return m_soundon;
}
std::string const &sound_off() const
{
return m_soundoff;
}
int cab() const
{
return m_cab;
}
static block_type describe();
@@ -239,9 +289,15 @@ private:
double const m_minvoltage;
double const m_power;
std::string const m_submodel;
std::string const m_soundon;
std::string const m_soundoff;
int const m_cab;
bool m_lit{false};
};
// cab given by the "cab" parameter of a cab block: 0 any, 1 or 2, -1 the engine room
int cab_of(block_setup const &Setup);
// needle gauge with one or two needles: voltmeter, ammeter, speedometer, pressure gauge
class cab_gauge final : public block
{
@@ -318,6 +374,10 @@ public:
{
return Needle == 0 ? m_submodel : m_submodel2;
}
int cab() const
{
return m_cab;
}
static block_type describe(bool Twoneedles);
@@ -330,6 +390,7 @@ private:
double const m_smoothing;
std::string const m_submodel;
std::string const m_submodel2;
int const m_cab;
float m_values[2]{0.f, 0.f};
};
@@ -358,7 +419,8 @@ public:
range,
offset_x,
offset_y,
offset_z
offset_z,
cab
};
};
// the same placements as sound_placement of the audio subsystem
@@ -422,6 +484,11 @@ public:
{
return m_offset[2];
}
// for the sounds placed in the cab: the cab they belong to, 0 any
int cab() const
{
return m_cab;
}
static block_type describe();
@@ -433,9 +500,53 @@ private:
placement_type const m_placement;
double const m_range;
double const m_offset[3];
int const m_cab;
bool m_playing{false};
float m_volume{0.f};
float m_pitch{1.f};
};
// value published for the python screens of the cab (TTrain::GetTrainState), under the specified name
class cab_export final : public block
{
public:
struct port
{
enum : std::size_t
{
value
};
};
enum class kind_type
{
real,
integer,
boolean
};
explicit cab_export(block_setup const &Setup);
void update(frame &Frame) override;
std::string const &name() const
{
return m_name;
}
kind_type kind() const
{
return m_kind;
}
double value() const
{
return m_value;
}
static block_type describe();
private:
std::string const m_name;
kind_type const m_kind;
double m_value{0.0};
};
} // namespace vgraph

View File

@@ -0,0 +1,920 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// combustion engine drive: engine -> clutch and gearbox / hydraulic transmission / main generator.
//
// the mechanical connections keep the convention of the electric drives: torque forward, rotational speed back. the
// engine is a torque source like the electric motors: it reads the speed of its shaft from the back channel and gives the
// torque. the speed of the engine shaft is a state of the block connected to the engine (drive.gearbox, drive.hydro or
// drive.generator), the same way the speed of the wheels is a state of the vehicle body behind drive.axle: that block
// holds the inertia of the engine side (crankshaft, flywheel, generator rotor), integrates the engine speed with the engine
// torque and the torque of its coupling (slipping clutch, torque converter, generator load), and returns it to the engine.
// when the coupling is locked (clutch engaged without slip, lockup clutch of the converter) the engine follows the speed
// of the wheels, as in TMoverParameters::dizel_Momentum, so the chain is rigid and stays stable at any step.
// the engine shaft has to be connected to exactly one such block; its speed is in rad/s, positive, in the frame of the
// engine. the direction of the vehicle is set after the coupling (dir inputs of the gearbox and the hydraulic transmission)
namespace vgraph
{
namespace
{
double constexpr two_pi{2.0 * M_PI};
double constexpr rpm_to_rad{two_pi / 60.0};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "drive";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// direction of the drive in the frame of the vehicle: -1, 0 or 1 (as for the electric motors)
int absolute_direction(frame const &Frame, double const Dir)
{
auto const dir{Dir > 0.5 ? 1 : Dir < -0.5 ? -1 : 0};
auto const cab{Frame.body->cab};
return dir * (cab != 0 ? cab : 1);
}
double sign(double const Value)
{
return Value > 0.0 ? 1.0 : Value < 0.0 ? -1.0 : 0.0;
}
// clamp which accepts the bounds in any order (safe_clamp of the Mover)
double clamp_any(double const Value, double const Bound1, double const Bound2)
{
return std::clamp(Value, std::min(Bound1, Bound2), std::max(Bound1, Bound2));
}
// linear interpolation in the rows of a table (first column the argument, sorted); the end rows outside of it
double interpolate(std::vector<std::vector<double>> const &Rows, std::size_t const Column, double const Argument)
{
if (Rows.empty())
{
return 0.0;
}
if (Argument <= Rows.front()[0])
{
return Rows.front()[Column];
}
for (std::size_t idx = 1; idx < Rows.size(); ++idx)
{
auto const &upper{Rows[idx]};
if (Argument <= upper[0])
{
auto const &lower{Rows[idx - 1]};
auto const span{upper[0] - lower[0]};
return span > 0.0 ? lower[Column] + (upper[Column] - lower[Column]) * (Argument - lower[0]) / span : upper[Column];
}
}
return Rows.back()[Column];
}
// rows of a table parameter with at least the specified number of columns, sorted by the first one
std::vector<std::vector<double>> sorted_rows(std::vector<std::vector<double>> const &Rows, std::size_t const Columns)
{
std::vector<std::vector<double>> result;
for (auto const &row : Rows)
{
if (row.size() >= Columns)
{
result.push_back(row);
}
}
std::stable_sort(std::begin(result), std::end(result), [](auto const &Left, auto const &Right) { return Left[0] < Right[0]; });
return result;
}
// combustion engine with a speed governor and a starter (adapted from TMoverParameters::dizel_Momentum, dizel_fillcheck
// and dizel_Update)
class drive_diesel final : public block
{
public:
struct port
{
enum : std::size_t
{
start,
stop,
fuel,
demand,
shaft,
running,
starting,
rpm,
torque,
fill,
power,
fuel_rate
};
};
struct param
{
enum : std::size_t
{
idle_rpm,
max_rpm,
governor,
droop,
fill_rate,
curve,
curve_type,
m_max,
rpm_m_max,
m_nmax,
friction,
starter_torque,
start_time,
fuel_consumption,
main_engine
};
};
explicit drive_diesel(block_setup const &Setup)
: block(Setup), m_idle(std::max(1.0, number(Setup, param::idle_rpm))), m_max(std::max(m_idle + 1.0, number(Setup, param::max_rpm))),
m_allspeed(choice(Setup, param::governor) == 0), m_band(std::max(1.0, std::max(0.001, number(Setup, param::droop)) * m_max)),
m_fillrate(std::max(0.0, number(Setup, param::fill_rate))), m_curve(sorted_rows(table(Setup, param::curve), 2)), m_curvepower(choice(Setup, param::curve_type) == 1),
m_mmax(number(Setup, param::m_max)), m_nmmax(number(Setup, param::rpm_m_max)), m_mnmax(number(Setup, param::m_nmax)), m_friction(std::max(0.0, number(Setup, param::friction))),
m_startertorque(std::max(0.0, number(Setup, param::starter_torque))), m_starttime(std::max(0.0, number(Setup, param::start_time))),
m_mainengine(boolean(Setup, param::main_engine))
{
if (m_curve.size() < 2)
{
m_curve.clear();
}
if (m_nmmax == m_max)
{
// as in LoadFIZ_DList: guards the division in the torque parabola
m_nmmax = m_max - 1.0;
}
// fuel used per hour at the full power, as dizel_FuelConsumption of LoadFIZ_DList: g/kWh * kW / (850 g/l)
auto const ratedpower{std::max(0.0, full_torque(m_max) - m_friction) * m_max * rpm_to_rad / 1000.0};
m_fuelperhour = std::max(0.0, number(Setup, param::fuel_consumption)) * ratedpower / 850.0;
}
void update(frame &Frame) override
{
auto const omega{back_out(Frame, port::shaft)};
auto const rpm{omega / rpm_to_rad};
auto const start{get_bool(Frame, port::start)};
auto const stop{get_bool(Frame, port::stop)};
auto const fuel{get_bool(Frame, port::fuel)};
auto const startpressed{start && false == m_startprevious};
m_startprevious = start;
// start procedure (dizel_Update): the starter cranks the engine for start_time, then the ignition; the starter
// keeps helping until the engine reaches its idle speed (dizel_spinup)
if (false == m_running && false == m_cranking && startpressed && fuel && false == stop)
{
m_cranking = true;
m_cranktime = 0.0;
}
if (m_cranking)
{
if (stop || false == fuel)
{
m_cranking = false;
}
else
{
m_cranktime += Frame.dt;
if (m_cranktime >= m_starttime)
{
m_cranking = false;
m_running = true;
m_spinup = true;
}
}
}
if (m_running && (stop || false == fuel))
{
// knocked out without fuel, as dizel_Update does when the fuel pump stops
m_running = false;
}
m_spinup = m_spinup && m_running && rpm < 0.95 * m_idle;
if (m_running && false == m_spinup && rpm < 0.25 * m_idle)
{
// stalled, e.g. by engaging the clutch at standstill
m_running = false;
}
// fuel injection set by the governor (dizel_fillcheck), reached at fill_rate (dizel_Update: fillspeed)
auto target{0.0};
if (m_running)
{
auto const demand{std::clamp(get(Frame, port::demand), 0.0, 1.0)};
if (m_allspeed)
{
// all speed governor: the demand sets the speed between the idle and the maximum one
auto const setspeed{m_idle + demand * (m_max - m_idle)};
target = std::clamp((setspeed - rpm) / m_band, 0.0, 1.0);
}
else
{
// the demand is the injection; the governor keeps the idle speed and cuts the fuel at the maximum one
target = std::max(demand, std::clamp((m_idle - rpm) / m_band, 0.0, 1.0));
target = std::min(target, std::clamp((m_max - rpm) / m_band, 0.0, 1.0));
}
}
m_fill += (target - m_fill) * std::min(1.0, m_fillrate * Frame.dt);
// torque (dizel_Momentum): the curve at full injection times the injection, less the motion resistance of
// the engine (dizel_Mstand), which acts against the rotation and fades out at standstill
m_combustion = rpm > 0.0 ? full_torque(rpm) * m_fill : 0.0;
m_resistance = m_friction * std::clamp(rpm / (0.1 * m_idle), -1.0, 1.0);
auto starter{0.0};
if (m_cranking || m_spinup)
{
// starter, falling with the speed as in dizel_Momentum
starter = m_startertorque * 0.3 / (0.3 + std::max(0.0, rpm / m_idle));
}
m_torque = m_combustion - m_resistance + starter;
m_omega = omega;
m_netpower = (m_combustion - m_resistance) * omega / 1000.0;
m_fuelrate = m_running ? m_fuelperhour * std::max(0.0, rpm / m_max) * m_fill : 0.0;
m_fuelused += m_fuelrate * Frame.dt / 3600.0;
set_forward(Frame, port::shaft, m_torque);
set_bool(Frame, port::running, m_running);
set_bool(Frame, port::starting, m_cranking || m_spinup);
set(Frame, port::rpm, rpm);
set(Frame, port::torque, m_torque);
set(Frame, port::fill, m_fill);
set(Frame, port::power, m_netpower);
set(Frame, port::fuel_rate, m_fuelrate);
}
// the fields of the vehicle body are written after all blocks were updated, so the engine speed replaces the
// revolutions written by the traction motors of a diesel-electric vehicle (for the diesel vehicles enrot is the
// engine speed: engine sounds, cab indicators)
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// for the diesel vehicles Mains is the running engine; with more engines (or line breakers) any of them
body.Mains = Frame.body->mains.any(Frame.step, m_running);
if (false == m_mainengine)
{
return;
}
body.enrot = m_omega / two_pi;
body.dizel_fill = m_fill;
body.Mm = m_combustion - m_resistance;
body.dizel_Torque = m_torque;
body.dizel_Power = m_netpower;
// as in TractionForce: (dizel_Mstand + Mm) * enrot * 2pi / 1000
body.EnginePower = m_combustion * m_omega / 1000.0;
body.dizel_startup = m_cranking;
body.dizel_spinup = m_spinup;
body.dizel_FuelConsumptionActual = m_fuelrate;
body.dizel_FuelConsumptedTotal = m_fuelused;
// speeds the sounds and indicators scale with (EngineIdleRPM, EngineMaxRPM, EngineRPMRatio), rev/s
body.dizel_nmin = m_idle / 60.0;
body.dizel_nmax = m_max / 60.0;
body.nmax = m_max / 60.0;
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "fuel_used";
}
double state_value(std::size_t) const override
{
return m_fuelused;
}
static block_type describe()
{
auto type{make_type("drive.diesel", "Diesel engine",
"Combustion engine with a speed governor and a starter (dizel_Momentum). start begins the start procedure: the starter cranks the engine for "
"start_time, then the fuel is injected. demand 0..1 sets the speed (all_speed governor) or the injection (min_max governor, idle and maximum "
"speed kept). The torque is the curve at full injection (table rpm -> Nm or kW, or the Mover parabola m_max, rpm_m_max, m_nmax) times the "
"injection, less the friction. The shaft has to go to one drive.gearbox, drive.hydro or drive.generator, which holds the engine speed.",
block_origin::extracted)};
type.ports = {signal_in("start", value_type::boolean),
signal_in("stop", value_type::boolean, 0.0, "", true),
signal_in("fuel", value_type::boolean, 1.0, "", true),
signal_in("demand", value_type::real),
physical_out("shaft", domain::mechanical),
signal_out("running", value_type::boolean),
signal_out("starting", value_type::boolean),
signal_out("rpm", value_type::real, "rpm"),
signal_out("torque", value_type::real, "Nm"),
signal_out("fill", value_type::real),
signal_out("power", value_type::real, "kW"),
signal_out("fuel_rate", value_type::real, "l/h")};
type.params = {number_param("idle_rpm", 600.0, "rpm"),
number_param("max_rpm", 1500.0, "rpm"),
choice_param("governor", {"all_speed", "min_max"}, "all_speed"),
number_param("droop", 0.05),
number_param("fill_rate", 10.0, "1/s"),
table_param("curve", {"rpm", "torque"}),
choice_param("curve_type", {"torque", "power"}, "torque"),
number_param("m_max", 3000.0, "Nm"),
number_param("rpm_m_max", 1100.0, "rpm"),
number_param("m_nmax", 2600.0, "Nm"),
number_param("friction", 300.0, "Nm"),
number_param("starter_torque", 600.0, "Nm"),
number_param("start_time", 2.0, "s"),
number_param("fuel_consumption", 220.0, "g/kWh"),
boolean_param("main_engine", true)};
type.late = true;
type.create = make_block<drive_diesel>;
return type;
}
private:
// torque at full injection, Nm
double full_torque(double const Rpm) const
{
if (false == m_curve.empty())
{
auto const value{interpolate(m_curve, 1, Rpm)};
// power curve in kW: torque = P / omega, the speed held at the first row to keep it finite
return m_curvepower ? value * 1000.0 / (std::max(Rpm, std::max(1.0, m_curve.front()[0])) * rpm_to_rad) : value;
}
// parabola of dizel_Momentum: Mmax at nMmax, Mnmax at nmax
auto const x{(Rpm - m_nmmax) / (m_nmmax - m_max)};
return std::max(0.0, m_mmax - (m_mmax - m_mnmax) * x * x);
}
double const m_idle; // rpm
double const m_max; // rpm
bool const m_allspeed;
double const m_band; // rpm
double const m_fillrate;
std::vector<std::vector<double>> m_curve;
bool const m_curvepower;
double const m_mmax;
double m_nmmax;
double const m_mnmax;
double const m_friction;
double const m_startertorque;
double const m_starttime;
bool const m_mainengine;
double m_fuelperhour{0.0}; // l/h at full power and maximum speed
bool m_startprevious{false};
bool m_cranking{false};
bool m_running{false};
bool m_spinup{false};
double m_cranktime{0.0};
double m_fill{0.0};
double m_combustion{0.0};
double m_resistance{0.0};
double m_torque{0.0};
double m_omega{0.0};
double m_netpower{0.0};
double m_fuelrate{0.0};
double m_fuelused{0.0};
};
// friction clutch and mechanical gearbox (adapted from TMoverParameters::dizel_Momentum without the converter,
// dizel_EngageChange and dizel_AutoGearCheck). holds the speed of the engine: slipping, the clutch passes its friction
// torque and the engine speed follows its own torque; synchronous, the engine follows the gearbox input and its whole
// torque passes on
class drive_gearbox final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
gear,
clutch,
dir,
out,
actual,
engage,
slip,
shifting
};
};
struct param
{
enum : std::size_t
{
gears,
clutch_torque,
engage_rate,
release_rate,
auto_clutch,
inertia,
eff
};
};
explicit drive_gearbox(block_setup const &Setup)
: block(Setup), m_clutchtorque(std::max(0.0, number(Setup, param::clutch_torque))), m_engagerate(std::max(0.0, number(Setup, param::engage_rate))),
m_releaserate(std::max(0.0, number(Setup, param::release_rate))), m_autoclutch(boolean(Setup, param::auto_clutch)),
m_inertia(std::max(0.01, number(Setup, param::inertia))), m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0))
{
for (auto const &row : table(Setup, param::gears))
{
if (row.size() >= 2)
{
m_gears.emplace_back(static_cast<int>(std::lround(row[0])), row[1]);
}
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const moment{forward_in(Frame, port::in)};
auto const requestedgear{static_cast<int>(std::lround(get(Frame, port::gear)))};
auto const requesteddir{absolute_direction(Frame, get(Frame, port::dir))};
// a change of the gear or the direction waits for the released clutch; with auto_clutch the gearbox releases it
m_shifting = (requestedgear != m_gear || requesteddir != m_dir);
auto const demand{std::clamp(get(Frame, port::clutch), 0.0, 1.0)};
m_engagestate = (m_shifting && m_autoclutch) ? 0.0 : demand;
// dizel_EngageChange: the pressure approaches the set one, and jumps to it when close
if (std::abs(m_engage - m_engagestate) < 0.11)
{
m_engage = m_engagestate;
}
else
{
m_engage += (m_engage > m_engagestate ? m_releaserate : m_engagerate) * Frame.dt * (m_engagestate - m_engage);
}
if (m_shifting && m_engage < 0.1)
{
m_gear = requestedgear;
m_dir = requesteddir;
m_shifting = false;
}
// ratio from the engine to the output shaft, in the frame of the vehicle; 0 in neutral
auto ratio{0.0};
for (auto const &gear : m_gears)
{
if (gear.first == m_gear && m_gear != 0)
{
ratio = gear.second * m_dir;
break;
}
}
auto gearmoment{0.0};
m_deltaomega = 0.0;
m_slipping = false;
if (ratio == 0.0)
{
// neutral: the input shaft turns with the engine, which runs free
auto const newomega{m_omega + moment / m_inertia * Frame.dt};
m_omega = (newomega < 0.0 && m_omega >= 0.0) ? 0.0 : newomega;
m_previous = m_omega;
}
else
{
// dizel_Momentum, in rad/s and kg m2 (the .fiz AIM is the inertia times 2pi)
auto const n{back_out(Frame, port::out) * ratio}; // speed of the gearbox input
auto const torquec{m_clutchtorque * m_engage};
if (std::abs(moment) > torquec || std::abs(m_previous - m_omega) > 0.1 * two_pi)
{
// slipping, because of the speed difference or the torque above the friction of the clutch
m_slipping = true;
m_deltaomega = m_omega - m_previous;
gearmoment = torquec * sign(m_deltaomega);
auto const eps{(moment - gearmoment) / m_inertia};
auto newomega{m_omega + eps * Frame.dt};
if ((newomega - n) * (m_omega - m_previous) < 0.0 && torquec > 0.1)
{
// the slip changed its sign: the clutch grips
newomega = n;
}
if (newomega * m_omega <= 0.0 && eps * m_omega < 0.0)
{
// no passing through zero speed
newomega = 0.0;
}
m_omega = newomega;
}
else
{
// synchronous: the engine follows the gearbox, within what the clutch can hold
gearmoment = moment;
m_omega = clamp_any(n, m_omega + (moment - torquec) / m_inertia * Frame.dt, m_omega + (moment + torquec) / m_inertia * Frame.dt);
}
m_previous = n;
}
set_forward(Frame, port::out, gearmoment * ratio * m_efficiency);
add_back(Frame, port::in, m_omega);
set(Frame, port::actual, m_gear);
set(Frame, port::engage, m_engage);
set_bool(Frame, port::slip, m_slipping && m_engage > 0.1 && std::abs(m_deltaomega) > 0.2 * two_pi);
set_bool(Frame, port::shifting, m_shifting);
// clutch state for the sounds and the cab indicators (rsEngageSlippery, clutch gauges)
body.dizel_engage = m_engage;
body.dizel_engagestate = m_engagestate;
body.dizel_engagedeltaomega = m_deltaomega / two_pi;
body.dizel_automaticgearstatus = m_shifting ? sign(requestedgear - m_gear) : 0.0;
}
static block_type describe()
{
auto type{make_type("drive.gearbox", "Gearbox with clutch",
"Friction clutch and mechanical gearbox (dizel_Momentum, dizel_EngageChange). gear selects the row of the gears table (gear number, ratio; "
"0 or a missing number is neutral, reverse gears can have negative ratios), dir reverses. A change of the gear or the direction waits until "
"the clutch is released; with auto_clutch the gearbox releases it itself. Holds the speed of the engine (inertia of the engine side).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical),
signal_in("gear", value_type::integer, 1.0, "", true),
signal_in("clutch", value_type::real, 1.0, "", true),
signal_in("dir", value_type::real, 1.0, "", true),
physical_out("out", domain::mechanical),
signal_out("actual", value_type::integer),
signal_out("engage", value_type::real),
signal_out("slip", value_type::boolean),
signal_out("shifting", value_type::boolean)};
type.params = {table_param("gears", {"gear", "ratio"}),
number_param("clutch_torque", 1500.0, "Nm"),
number_param("engage_rate", 0.5, "1/s"),
number_param("release_rate", 0.9, "1/s"),
boolean_param("auto_clutch", true),
number_param("inertia", 10.0, "kg m2"),
number_param("eff", 0.95)};
type.create = make_block<drive_gearbox>;
return type;
}
private:
double const m_clutchtorque;
double const m_engagerate;
double const m_releaserate;
bool const m_autoclutch;
double const m_inertia;
double const m_efficiency;
std::vector<std::pair<int, double>> m_gears;
int m_gear{0};
int m_dir{0};
bool m_shifting{false};
bool m_slipping{false};
double m_engage{0.0};
double m_engagestate{0.0};
double m_omega{0.0}; // engine speed, rad/s
double m_previous{0.0}; // gearbox input speed in the previous step (dizel_n_old)
double m_deltaomega{0.0};
};
// hydraulic transmission: torque converter with filling, fluid coupling range and lockup clutch (adapted from the hydro_TC
// part of TMoverParameters::dizel_Momentum). holds the speed of the engine like the gearbox
class drive_hydro final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
power,
running,
dir,
out,
fill,
lockup,
ratio
};
};
struct param
{
enum : std::size_t
{
tc_tmmax,
tc_cp,
tc_table,
tc_tii,
tc_tio,
tc_too,
fill_rate,
drain_rate,
lockup_torque,
lockup_speed,
unlock_speed,
idle_rpm,
ans_speed,
inertia
};
};
explicit drive_hydro(block_setup const &Setup)
: block(Setup), m_tmmax(number(Setup, param::tc_tmmax)), m_couplingpoint(std::clamp(number(Setup, param::tc_cp), 0.01, 0.99)),
m_table(sorted_rows(table(Setup, param::tc_table), 2)), m_torqueinin(number(Setup, param::tc_tii)), m_torqueinout(number(Setup, param::tc_tio)),
m_torqueoutout(number(Setup, param::tc_too)), m_fillrate(std::max(0.0, number(Setup, param::fill_rate))), m_drainrate(std::max(0.0, number(Setup, param::drain_rate))),
m_lockuptorque(std::max(0.0, number(Setup, param::lockup_torque))), m_lockupspeed(number(Setup, param::lockup_speed)), m_unlockspeed(number(Setup, param::unlock_speed)),
m_idle(std::max(1.0, number(Setup, param::idle_rpm)) * rpm_to_rad), m_ansspeed(number(Setup, param::ans_speed)),
m_inertia(std::max(0.01, number(Setup, param::inertia)))
{
if (m_table.size() < 2)
{
m_table.clear();
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const moment{forward_in(Frame, port::in)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const power{get_bool(Frame, port::power)};
// the engine running (Mains of dizel_Momentum); a stopped engine drains the converter and releases the lockup clutch
auto const running{get_bool(Frame, port::running)};
auto const vel{std::abs(Frame.body->velocity) * 3.6};
// speed of the output shaft in the frame of the engine; without a direction it turns with the engine
auto const n{dir != 0 ? back_out(Frame, port::out) * dir : m_omega};
// filling and draining of the converter, engaging and releasing of the lockup clutch
if (power && running && m_omega > 0.9 * m_idle)
{
m_fill += m_fillrate * Frame.dt;
}
if ((false == power && vel < m_ansspeed) || false == running || m_omega < 0.8 * m_idle)
{
m_fill -= m_drainrate * Frame.dt;
}
m_fill = std::clamp(m_fill, 0.0, 1.0);
if (vel > m_lockupspeed && running && m_omega > 0.9 * m_idle && power)
{
m_lockup = true;
m_lockuprate += m_fillrate * Frame.dt;
}
if (vel < (power ? m_lockupspeed : m_unlockspeed) || false == running || m_omega < 0.8 * m_idle)
{
m_lockup = false;
m_lockuprate -= m_drainrate * Frame.dt;
}
m_lockuprate = std::clamp(m_lockuprate, 0.0, 1.0);
// torques of the converter; its constants are for the speeds in rev/s, as in the .fiz files
auto const nin{m_omega / two_pi};
auto const nout{m_previous / two_pi};
auto const hydrotorque{nin * nin * m_torqueinin + (nin - nout) * m_torqueinout + nout * nout * m_torqueoutout};
auto const speedratio{nout / std::max(0.01, nin)};
double torquein;
double torqueout;
if (false == m_table.empty())
{
m_tmratio = interpolate(m_table, 1, speedratio);
torqueout = hydrotorque * m_fill * m_tmratio;
torquein = hydrotorque * m_fill * std::min(1.0, m_tmratio);
}
else if (speedratio < m_couplingpoint)
{
// converter range: the torque is multiplied
auto const x{1.0 - speedratio / m_couplingpoint};
m_tmratio = 1.0 + (m_tmmax - 1.0) * x * x;
torquein = hydrotorque * m_fill;
torqueout = hydrotorque * m_fill * m_tmratio;
}
else
{
// fluid coupling range
m_tmratio = (1.0 - speedratio) / (1.0 - m_couplingpoint);
torquein = hydrotorque * m_fill * m_tmratio;
torqueout = torquein;
}
auto const torquelock{m_lockuptorque * m_lockuprate};
// the main clutch of dizel_Momentum is taken as always engaged; the converter is drained to disconnect the engine
double gearmoment;
auto const capacity{torquelock + std::abs(torquein)};
if (std::abs(moment) > capacity || std::abs(m_previous - m_omega) > 0.1 * two_pi)
{
auto const deltaomega{m_omega - m_previous};
// NOTE: dizel_Momentum adds the lockup torque without the sign of the slip; here it acts against the slip too
gearmoment = (torquelock + std::abs(torqueout)) * sign(deltaomega);
auto const eps{(moment - capacity * sign(deltaomega)) / m_inertia};
auto newomega{m_omega + eps * Frame.dt};
if ((newomega - n) * deltaomega < 0.0 && torquelock > 0.1)
{
// the lockup clutch grips
newomega = n;
}
if (newomega * m_omega <= 0.0 && eps * m_omega < 0.0)
{
newomega = 0.0;
}
m_omega = newomega;
}
else
{
gearmoment = moment;
m_omega = clamp_any(n, m_omega + (moment - capacity) / m_inertia * Frame.dt, m_omega + (moment + capacity) / m_inertia * Frame.dt);
}
m_previous = n;
set_forward(Frame, port::out, gearmoment * dir);
add_back(Frame, port::in, m_omega);
set(Frame, port::fill, m_fill);
set_bool(Frame, port::lockup, m_lockup);
// the ratio of an empty converter means nothing (and grows without limit when the engine stops)
set(Frame, port::ratio, m_fill > 0.0 ? m_tmratio : 0.0);
body.hydro_TC_Fill = m_fill;
body.hydro_TC_Lockup = m_lockup;
body.hydro_TC_LockupRate = m_lockuprate;
body.hydro_TC_TMRatio = m_tmratio;
body.hydro_TC_TorqueIn = torquein;
body.hydro_TC_TorqueOut = gearmoment;
body.hydro_TC_nIn = nin;
body.hydro_TC_nOut = nout;
}
static block_type describe()
{
auto type{make_type("drive.hydro", "Hydraulic transmission",
"Torque converter with filling, fluid coupling range above the coupling point and a lockup clutch, as hydro_TC of the .fiz files (dizel_Momentum). "
"power fills the converter (with the engine above 0.9 of idle_rpm); without it the converter drains below ans_speed. The lockup clutch engages "
"above lockup_speed with power and releases below it (below unlock_speed without power). running (the engine's) false drains the converter and "
"releases the lockup clutch. Holds the speed of the engine (inertia of the engine side).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical),
signal_in("power", value_type::boolean),
signal_in("running", value_type::boolean, 1.0, "", true),
signal_in("dir", value_type::real, 1.0, "", true),
physical_out("out", domain::mechanical),
signal_out("fill", value_type::real),
signal_out("lockup", value_type::boolean),
signal_out("ratio", value_type::real)};
type.params = {number_param("tc_tmmax", 2.0),
number_param("tc_cp", 0.85),
table_param("tc_table", {"speed_ratio", "torque_ratio"}),
number_param("tc_tii", 4.5),
number_param("tc_tio", 0.0),
number_param("tc_too", 0.0),
number_param("fill_rate", 1.0, "1/s"),
number_param("drain_rate", 1.0, "1/s"),
number_param("lockup_torque", 3000.0, "Nm"),
number_param("lockup_speed", 40.0, "km/h"),
number_param("unlock_speed", 30.0, "km/h"),
number_param("idle_rpm", 600.0, "rpm"),
number_param("ans_speed", 3.0, "km/h"),
number_param("inertia", 10.0, "kg m2")};
type.create = make_block<drive_hydro>;
return type;
}
private:
double const m_tmmax;
double const m_couplingpoint;
std::vector<std::vector<double>> m_table;
double const m_torqueinin;
double const m_torqueinout;
double const m_torqueoutout;
double const m_fillrate;
double const m_drainrate;
double const m_lockuptorque;
double const m_lockupspeed;
double const m_unlockspeed;
double const m_idle; // rad/s
double const m_ansspeed;
double const m_inertia;
double m_fill{0.0};
bool m_lockup{false};
double m_lockuprate{0.0};
double m_tmratio{1.0};
double m_omega{0.0}; // engine speed, rad/s
double m_previous{0.0}; // output speed in the previous step, rad/s
};
// main generator of a diesel-electric vehicle: separately excited dc generator (adapted from the DieselElectric part of
// TMoverParameters::TractionForce: the ellipse of the generator characteristic and the power available at the engine
// speed). holds the speed of the engine
class drive_generator final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
excitation,
out,
u,
i,
p
};
};
struct param
{
enum : std::size_t
{
u_max,
i_max,
p_max,
nominal_rpm,
eff,
field_time,
inertia
};
};
explicit drive_generator(block_setup const &Setup)
: block(Setup), m_umax(std::max(0.0, number(Setup, param::u_max))), m_imax(std::max(1.0, number(Setup, param::i_max))),
m_pmax(std::max(0.0, number(Setup, param::p_max)) * 1000.0), m_nominal(std::max(1.0, number(Setup, param::nominal_rpm)) * rpm_to_rad),
m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0)), m_fieldtime(std::max(0.0, number(Setup, param::field_time))),
m_inertia(std::max(0.01, number(Setup, param::inertia)))
{
}
void update(frame &Frame) override
{
auto const moment{forward_in(Frame, port::in)};
auto const current{back_out(Frame, port::out)}; // drawn by the load in the previous step
auto const excitation{std::clamp(get(Frame, port::excitation), 0.0, 1.0)};
auto const speedratio{std::clamp(m_omega / m_nominal, 0.0, 1.0)};
// the voltage at no load grows with the excitation and the speed; under load it follows the ellipse of the
// separately excited generator (zero at i_max) and the hyperbola of the power available at this speed
auto const noload{m_umax * excitation * speedratio};
auto const load{std::min(1.0, std::abs(current) / m_imax)};
auto target{noload * std::sqrt(std::max(0.0, 1.0 - load * load))};
if (std::abs(current) > 1.0)
{
target = std::min(target, m_pmax * speedratio / std::abs(current));
}
// the field of the generator follows with its time constant, which also keeps the loop with the motors stable
m_voltage += (target - m_voltage) * (m_fieldtime > 0.0 ? std::min(1.0, Frame.dt / m_fieldtime) : 1.0);
m_power = m_voltage * current;
// load of the engine: the electric power and the losses
auto const loadtorque{m_power / m_efficiency / std::max(m_omega, 1.0)};
m_omega = std::max(0.0, m_omega + (moment - loadtorque) / m_inertia * Frame.dt);
set_forward(Frame, port::out, m_voltage);
add_back(Frame, port::in, m_omega);
set(Frame, port::u, m_voltage);
set(Frame, port::i, current);
set(Frame, port::p, m_power / 1000.0);
}
static block_type describe()
{
auto type{make_type("drive.generator", "Main generator",
"Main generator of a diesel-electric vehicle. The voltage at no load is u_max times the excitation (0..1), proportional to the engine speed "
"below nominal_rpm; under load it falls on an ellipse to zero at i_max, and the power is limited to p_max scaled with the engine speed "
"(DieselElectric of TractionForce). The electric power loads the engine. Holds the speed of the engine (inertia of the engine and the rotor).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical), signal_in("excitation", value_type::real), physical_out("out", domain::high_voltage),
signal_out("u", value_type::real, "V"), signal_out("i", value_type::real, "A"), signal_out("p", value_type::real, "kW")};
type.params = {number_param("u_max", 750.0, "V"),
number_param("i_max", 2400.0, "A"),
number_param("p_max", 330.0, "kW"),
number_param("nominal_rpm", 1500.0, "rpm"),
number_param("eff", 0.93),
number_param("field_time", 0.5, "s"),
number_param("inertia", 20.0, "kg m2")};
type.create = make_block<drive_generator>;
return type;
}
private:
double const m_umax;
double const m_imax;
double const m_pmax; // W
double const m_nominal; // rad/s
double const m_efficiency;
double const m_fieldtime;
double const m_inertia;
double m_voltage{0.0};
double m_power{0.0};
double m_omega{0.0}; // engine speed, rad/s
};
} // namespace
void register_diesel_blocks(catalog &Catalog)
{
Catalog.add(drive_diesel::describe());
Catalog.add(drive_gearbox::describe());
Catalog.add(drive_hydro::describe());
Catalog.add(drive_generator::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,658 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// drive chain: motor -> gear -> axle. the mechanical connections pass the torque forward and the rotational speed back.
// both are in the frame of the vehicle: positive torque pushes the vehicle towards its front (cab 1), positive speed
// is the rotation when moving that way. the dir input of the motors is the position of the reverser in the occupied cab,
// so the motors turn it to the frame of the vehicle with the occupied cab
namespace vgraph
{
namespace
{
double constexpr two_pi{2.0 * M_PI};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "drive";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// direction of the drive in the frame of the vehicle: -1, 0 or 1
int absolute_direction(frame const &Frame, double const Dir)
{
auto const dir{Dir > 0.5 ? 1 : Dir < -0.5 ? -1 : 0};
auto const cab{Frame.body->cab};
return dir * (cab != 0 ? cab : 1);
}
// generic motor with a torque and power limit; a stand-in for the drives not extracted yet
class drive_simple final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
demand,
dir,
shaft,
i,
p
};
};
struct param
{
enum : std::size_t
{
t_max,
p_max,
eff,
min_voltage
};
};
explicit drive_simple(block_setup const &Setup)
: block(Setup), m_supplied(is_connected(Setup, port::wn)), m_tmax(number(Setup, param::t_max)), m_pmax(number(Setup, param::p_max)),
m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0)), m_minvoltage(number(Setup, param::min_voltage))
{
}
void update(frame &Frame) override
{
auto const voltage{m_supplied ? forward_in(Frame, port::wn) : 0.0};
auto const powered{false == m_supplied || (voltage > 0.0 && voltage >= m_minvoltage)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const demand{std::clamp(get(Frame, port::demand), -1.0, 1.0)};
auto const speed{back_out(Frame, port::shaft)}; // rad/s
auto torque{0.0};
if (powered && dir != 0)
{
if (demand >= 0.0)
{
// traction, limited by the power above the base speed
auto const limit{std::abs(speed) > 1e-3 ? std::min(m_tmax, m_pmax / std::abs(speed)) : m_tmax};
torque = dir * demand * limit;
}
else
{
// electric brake, against the rotation
auto const limit{std::abs(speed) > 1e-3 ? std::min(m_tmax, m_pmax / std::abs(speed)) : 0.0};
torque = (speed >= 0.0 ? -1.0 : 1.0) * (-demand) * limit * std::min(1.0, std::abs(speed));
}
}
auto const mechanical{torque * speed}; // W, positive when driving
auto const electrical{mechanical >= 0.0 ? mechanical / m_efficiency : mechanical * m_efficiency};
auto const current{m_supplied && voltage > 1.0 ? electrical / voltage : 0.0};
if (m_supplied)
{
add_back(Frame, port::wn, current);
}
set_forward(Frame, port::shaft, torque);
set(Frame, port::i, current);
set(Frame, port::p, electrical);
auto &body{vehicle(Frame)};
body.Im = current;
body.EngineVoltage = voltage * dir;
body.enrot = speed / two_pi;
}
static block_type describe()
{
auto type{make_type("drive.simple", "Simple motor",
"Motor with a torque limit (t_max) and a power limit (p_max): demand 0..1 drives, -1..0 brakes electrically. A stand-in for prototypes.",
block_origin::native)};
type.ports = {physical_in("wn", domain::high_voltage, true), signal_in("demand", value_type::real), signal_in("dir", value_type::real),
physical_out("shaft", domain::mechanical), signal_out("i", value_type::real, "A"), signal_out("p", value_type::real, "W")};
type.params = {number_param("t_max", 5000.0, "Nm"), number_param("p_max", 500000.0, "W"), number_param("eff", 0.9), number_param("min_voltage", 0.0, "V")};
type.create = make_block<drive_simple>;
return type;
}
private:
bool const m_supplied;
double const m_tmax;
double const m_pmax;
double const m_efficiency;
double const m_minvoltage;
};
class drive_gear final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out
};
};
explicit drive_gear(block_setup const &Setup) : block(Setup), m_ratio(number(Setup, 0)), m_efficiency(number(Setup, 1)) {}
void update(frame &Frame) override
{
set_forward(Frame, port::out, forward_in(Frame, port::in) * m_ratio * m_efficiency);
add_back(Frame, port::in, back_out(Frame, port::out) * m_ratio);
}
static block_type describe()
{
auto type{make_type("drive.gear", "Gear", "Transmission: torque times ratio and efficiency, speed of the input times ratio. ratio = wheel teeth / motor teeth.",
block_origin::extracted)};
type.ports = {physical_in("in", domain::mechanical), physical_out("out", domain::mechanical)};
type.params = {number_param("ratio", 1.0), number_param("eff", 1.0)};
type.create = make_block<drive_gear>;
return type;
}
private:
double const m_ratio;
double const m_efficiency;
};
// wheelsets driven by one shaft: the torque becomes the tractive force of the vehicle
class drive_axle final : public block
{
public:
struct port
{
enum : std::size_t
{
shaft,
force,
slip
};
};
explicit drive_axle(block_setup const &Setup) : block(Setup), m_axles(std::max(0.0, number(Setup, 0))) {}
void update(frame &Frame) override
{
auto const &body{vehicle(Frame)};
auto const diameter{body.WheelDiameter > 0.0 ? body.WheelDiameter : 1.0};
auto const force{forward_in(Frame, port::shaft) * 2.0 / diameter * m_axles};
Frame.body->tractive_force += force;
add_back(Frame, port::shaft, Frame.body->wheel_speed * two_pi);
set(Frame, port::force, force);
set_bool(Frame, port::slip, Frame.body->slipping);
}
static block_type describe()
{
auto type{make_type("drive.axle", "Driven axles",
"Driven wheelsets: the torque of one shaft (of one motor, after the gear) times powered_axles gives the force at the wheel rim. All axle forces are "
"summed into the tractive force of the vehicle.",
block_origin::native)};
type.ports = {physical_in("shaft", domain::mechanical), signal_out("force", value_type::real, "N"), signal_out("slip", value_type::boolean)};
type.params = {number_param("powered_axles", 1.0)};
type.create = make_block<drive_axle>;
return type;
}
private:
double const m_axles;
};
// series dc traction motors with resistor starting (TMoverParameters::Current, Momentum)
class drive_dc_series final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
position,
shunt,
dir,
high,
reset,
shaft,
i,
u_motor,
overload
};
};
struct param
{
enum : std::size_t
{
rlist,
mpt,
volt,
winding_res,
circuit_res,
imax_lo,
imax_hi,
imin_lo,
imin_hi,
trip_delay
};
};
explicit drive_dc_series(block_setup const &Setup)
: block(Setup), m_windingres(number(Setup, param::winding_res)), m_circuitres(number(Setup, param::circuit_res)), m_imaxlo(number(Setup, param::imax_lo)),
m_imaxhi(number(Setup, param::imax_hi)), m_tripdelay(number(Setup, param::trip_delay))
{
for (auto const &row : table(Setup, param::rlist))
{
// pos, r, bn, mn, auto
scheme entry;
entry.r = row[1];
entry.bn = static_cast<int>(std::lround(row[2]));
entry.mn = static_cast<int>(std::lround(row[3]));
m_schemes.push_back(entry);
}
for (auto const &row : table(Setup, param::mpt))
{
// pos, mfi, mIsat, mfi0, fi, Isat, fi0
TMotorParameters motor;
motor.mfi = row[1];
motor.mIsat = row[2];
motor.mfi0 = row[3];
motor.fi = row[4];
motor.Isat = row[5];
motor.fi0 = row[6];
m_motors.push_back(motor);
}
if (m_schemes.empty())
{
m_schemes.emplace_back();
}
if (m_motors.empty())
{
m_motors.emplace_back();
}
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const voltage{forward_in(Frame, port::wn) * dir};
auto const position{std::clamp<int>(static_cast<int>(std::lround(get(Frame, port::position))), 0, static_cast<int>(m_schemes.size()) - 1)};
auto const shunt{std::clamp<int>(static_cast<int>(std::lround(get(Frame, port::shunt))), 0, static_cast<int>(m_motors.size()) - 1)};
auto const &scheme{m_schemes[position]};
auto const &motor{m_motors[shunt]};
auto const revolutions{back_out(Frame, port::shaft) / two_pi}; // rev/s
if (get_bool(Frame, port::reset))
{
m_overload = false;
}
auto current{0.0};
if (scheme.bn != 0 && std::abs(voltage) > 1.0 && false == m_overload)
{
auto const resistance{scheme.r + m_circuitres};
current = series_motor_current(voltage, revolutions, scheme.mn, scheme.mn * m_windingres + resistance, motor);
}
// overload relay, as in TractionForce: trips when the current stays above the limit
auto const imax{get_bool(Frame, port::high) ? m_imaxhi : m_imaxlo};
if (imax > 0.0 && std::abs(current) > imax)
{
m_overtime += Frame.dt;
if (m_overtime > m_tripdelay)
{
m_overload = true;
}
}
else
{
m_overtime = 0.0;
}
auto const torque{series_motor_momentum(current, motor)};
auto const linecurrent{current * scheme.bn};
add_back(Frame, port::wn, std::abs(linecurrent));
set_forward(Frame, port::shaft, torque);
set(Frame, port::i, current);
set(Frame, port::u_motor, scheme.mn > 0 ? (std::abs(voltage) - std::abs(current) * (scheme.r + m_circuitres)) / scheme.mn : 0.0);
set_bool(Frame, port::overload, m_overload);
body.Im = current;
body.EngineVoltage = voltage;
body.enrot = revolutions;
body.MainCtrlActualPos = position;
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "overload";
}
double state_value(std::size_t) const override
{
return m_overload ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("drive.dc_series", "Series DC motors",
"Series wound DC traction motors with resistor starting, computed as for the .fiz vehicles. position selects the row of rlist (resistance, "
"branches, motors in series), shunt the row of mpt (motor characteristics). The shaft carries the torque of one motor. high selects imax_hi "
"for the overload relay; reset clears it.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage),
signal_in("position", value_type::integer),
signal_in("shunt", value_type::integer, 0.0, "", true),
signal_in("dir", value_type::real),
signal_in("high", value_type::boolean, 0.0, "", true),
signal_in("reset", value_type::boolean, 0.0, "", true),
physical_out("shaft", domain::mechanical),
signal_out("i", value_type::real, "A"),
signal_out("u_motor", value_type::real, "V"),
signal_out("overload", value_type::boolean)};
type.params = {table_param("rlist", {"pos", "r", "bn", "mn", "auto"}),
table_param("mpt", {"pos", "mfi", "misat", "mfi0", "fi", "isat", "fi0"}),
number_param("volt", 1500.0, "V"),
number_param("winding_res", 0.0, "ohm"),
number_param("circuit_res", 0.0, "ohm"),
number_param("imax_lo", 0.0, "A"),
number_param("imax_hi", 0.0, "A"),
number_param("imin_lo", 0.0, "A"),
number_param("imin_hi", 0.0, "A"),
number_param("trip_delay", 0.05, "s")};
type.create = make_block<drive_dc_series>;
return type;
}
private:
struct scheme
{
double r{0.0};
int bn{0};
int mn{0};
};
double const m_windingres;
double const m_circuitres;
double const m_imaxlo;
double const m_imaxhi;
double const m_tripdelay;
std::vector<scheme> m_schemes;
std::vector<TMotorParameters> m_motors;
double m_overtime{0.0};
bool m_overload{false};
};
// induction motors fed by inverters (the ElectricInductionMotor drive of TMoverParameters::TractionForce)
class drive_async final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
demand,
dir,
shaft,
i,
f,
ed_active,
ed
};
};
// in the order of the eimc constants they're copied to
struct param
{
enum : std::size_t
{
dfic,
dfmax,
p,
cfu,
cim,
icif,
uzmax,
uzh,
du,
i0,
fcfu,
fcfuh,
f0,
a1,
pmax,
fh,
ph,
vh0,
vh1,
imax,
abed,
edep,
flat,
motors,
ratio,
eff,
log_force
};
};
explicit drive_async(block_setup const &Setup)
: block(Setup), m_flat(boolean(Setup, param::flat)), m_motors(static_cast<int>(std::lround(number(Setup, param::motors)))), m_ratio(number(Setup, param::ratio)),
m_efficiency(number(Setup, param::eff)), m_logforce(boolean(Setup, param::log_force))
{
std::fill(std::begin(m_eimc), std::end(m_eimc), 0.0);
std::fill(std::begin(m_eimv), std::end(m_eimv), 0.0);
m_eimc[eimc_s_dfic] = number(Setup, param::dfic);
m_eimc[eimc_s_dfmax] = number(Setup, param::dfmax);
m_eimc[eimc_s_p] = number(Setup, param::p);
m_eimc[eimc_s_cfu] = number(Setup, param::cfu);
m_eimc[eimc_s_cim] = number(Setup, param::cim);
m_eimc[eimc_s_icif] = number(Setup, param::icif);
m_eimc[eimc_f_Uzmax] = number(Setup, param::uzmax);
m_eimc[eimc_f_Uzh] = number(Setup, param::uzh);
m_eimc[eimc_f_DU] = number(Setup, param::du);
m_eimc[eimc_f_I0] = number(Setup, param::i0);
m_eimc[eimc_f_cfu] = number(Setup, param::fcfu);
// as in LoadFIZ_Engine: fcfuH defaults to fcfu
m_eimc[eimc_f_cfuH] = number(Setup, param::fcfuh) > 0.0 ? number(Setup, param::fcfuh) : m_eimc[eimc_f_cfu];
m_eimc[eimc_p_F0] = number(Setup, param::f0);
m_eimc[eimc_p_a1] = number(Setup, param::a1);
m_eimc[eimc_p_Pmax] = number(Setup, param::pmax);
m_eimc[eimc_p_Fh] = number(Setup, param::fh);
m_eimc[eimc_p_Ph] = number(Setup, param::ph);
m_eimc[eimc_p_Vh0] = number(Setup, param::vh0);
m_eimc[eimc_p_Vh1] = number(Setup, param::vh1);
m_eimc[eimc_p_Imax] = number(Setup, param::imax);
m_eimc[eimc_p_abed] = number(Setup, param::abed);
m_eimc[eimc_p_eped] = number(Setup, param::edep);
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const dir{absolute_direction(Frame, get(Frame, port::dir))};
auto const enrot{back_out(Frame, port::shaft) / two_pi};
auto const demand{std::clamp(get(Frame, port::demand), -1.0, 1.0)};
if (voltage < 1.0 || dir == 0)
{
// without supply the inverters stop, as for the vehicles without the main circuit (Mains)
std::fill(std::begin(m_eimv), std::end(m_eimv), 0.0);
m_eimvpr = 0.0;
m_dynamicbrake = false;
add_back(Frame, port::wn, 0.0);
publish(Frame, body, 0.0, 0.0, 0.0, voltage * dir, enrot);
return;
}
auto const vel{body.Vel};
// set value of the force and the rate of its ramp, as in TractionForce
double posratio;
double rate;
if (demand < -0.001)
{
m_dynamicbrake = true;
auto const vhramp{std::clamp((vel - m_eimc[eimc_p_Vh0]) / std::max(0.001, m_eimc[eimc_p_Vh1] - m_eimc[eimc_p_Vh0]), 0.0, 1.0)};
posratio = demand * vhramp;
rate = 5.0;
}
else
{
m_dynamicbrake = false;
posratio = std::max(demand, 0.0);
if (m_flat && m_eimc[eimc_p_F0] * m_eimv[eimv_Fful] > 0)
{
posratio = std::min(posratio * m_eimc[eimc_p_F0] / m_eimv[eimv_Fful], 1.0);
}
rate = 4.0;
}
if (body.SlippingWheels)
{
// anti-slip: the set value drops quickly
posratio = 0.0;
rate = 10.0;
}
induction_motor_inputs input;
input.demand = posratio;
input.rate = rate;
input.dynamic_brake = m_dynamicbrake;
input.engine_voltage = voltage;
input.enrot = enrot;
input.vel = vel;
input.v = body.V;
input.dir_absolute = dir;
input.dir_active = dir;
input.powered_axles = m_motors;
input.inverters_ratio = 1.0;
input.ratio = m_ratio;
input.efficiency = m_efficiency;
input.wheel_diameter = body.WheelDiameter > 0.0 ? body.WheelDiameter : 1.0;
input.log_force = m_logforce;
input.max_voltage = body.EnginePowerSource.CollectorParameters.MaxV;
input.dt = Frame.dt;
induction_motor_outputs output;
induction_motor_update(input, m_eimc, m_eimv, m_eimvpr, m_vadd, output);
add_back(Frame, port::wn, output.Itot);
// share of the requested braking force the motors deliver, for the blending with the pneumatic brake
auto edshare{0.0};
if (m_dynamicbrake && m_eimc[eimc_p_Fh] > 0.0 && demand < -0.001)
{
edshare = std::clamp(std::abs(m_eimv[eimv_Fr]) / (m_eimc[eimc_p_Fh] * -demand), 0.0, 1.0);
}
publish(Frame, body, output.Mm, output.Im, edshare, voltage, enrot);
}
std::size_t state_count() const override
{
return 2;
}
char const *state_name(std::size_t Index) const override
{
return Index == 0 ? "set_value" : "force_kn";
}
double state_value(std::size_t Index) const override
{
return Index == 0 ? m_eimvpr : m_eimv[eimv_Fr];
}
static block_type describe()
{
auto type{make_type("drive.async", "Induction motors with inverters",
"Induction traction motors fed by inverters, computed as for the .fiz vehicles (Engine: ElectricInductionMotor). demand 0..1 drives, -1..0 brakes "
"electrically (faded out between vh1 and vh0). The shaft carries the torque of one motor; ratio and eff have to match the gear, and motors the "
"driven axles. ed is the share of the requested braking force the motors deliver, for the ed input of the distributor.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("demand", value_type::real), signal_in("dir", value_type::real),
physical_out("shaft", domain::mechanical), signal_out("i", value_type::real, "A"), signal_out("f", value_type::real, "Hz"),
signal_out("ed_active", value_type::boolean), signal_out("ed", value_type::real)};
type.params = {number_param("dfic", 0.0),
number_param("dfmax", 0.0),
number_param("p", 2.0),
number_param("cfu", 0.0),
number_param("cim", 0.0),
number_param("icif", 0.0),
number_param("uzmax", 0.0, "V"),
number_param("uzh", 0.0, "V"),
number_param("du", 0.0, "V"),
number_param("i0", 0.0, "A"),
number_param("fcfu", 0.0),
number_param("fcfuh", 0.0),
number_param("f0", 0.0, "kN"),
number_param("a1", 0.0),
number_param("pmax", 0.0, "kW"),
number_param("fh", 0.0, "kN"),
number_param("ph", 0.0, "kW"),
number_param("vh0", 0.0, "km/h"),
number_param("vh1", 0.0, "km/h"),
number_param("imax", 0.0, "A"),
number_param("abed", 1.0),
number_param("edep", 1.5),
boolean_param("flat", false),
number_param("motors", 4.0),
number_param("ratio", 1.0),
number_param("eff", 1.0),
boolean_param("log_force", false)};
type.create = make_block<drive_async>;
return type;
}
private:
void publish(frame &Frame, TMoverParameters &Body, double const Torque, double const Current, double const Edshare, double const Voltage, double const Revolutions)
{
set_forward(Frame, port::shaft, Torque);
set(Frame, port::i, Current);
set(Frame, port::f, std::abs(m_eimv[eimv_fp]));
set_bool(Frame, port::ed_active, m_dynamicbrake);
set(Frame, port::ed, Edshare);
Body.Im = Current;
Body.EngineVoltage = Voltage;
Body.enrot = Revolutions;
Body.DynamicBrakeFlag = m_dynamicbrake;
Body.InverterFrequency = std::abs(m_eimv[eimv_fp]);
std::copy(std::begin(m_eimv), std::end(m_eimv), Body.eimv);
}
bool const m_flat;
int const m_motors;
double const m_ratio;
double const m_efficiency;
bool const m_logforce;
double m_eimc[26];
double m_eimv[21];
double m_eimvpr{0.0};
double m_vadd{0.0};
bool m_dynamicbrake{false};
};
} // namespace
void register_drive_blocks(catalog &Catalog)
{
Catalog.add(drive_simple::describe());
Catalog.add(drive_dc_series::describe());
Catalog.add(drive_async::describe());
Catalog.add(drive_gear::describe());
Catalog.add(drive_axle::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,658 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// high voltage circuit and auxiliary supplies. voltage goes forward through the connections, current comes back; the
// back channel lags one step, so a breaker sees the current its load drew in the previous step
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = "electric";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// current collector. in the simulator its contact with the catenary is the pantograph of the vehicle model
class el_pantograph final : public block
{
public:
struct port
{
enum : std::size_t
{
raise,
air,
wn,
up,
height,
u
};
};
struct param
{
enum : std::size_t
{
index,
raise_time,
lower_time,
min_pressure,
preview_voltage
};
};
explicit el_pantograph(block_setup const &Setup)
: block(Setup), m_air(is_connected(Setup, port::air)), m_index(std::clamp(static_cast<int>(std::lround(number(Setup, param::index))), 0, 1)),
m_raisetime(std::max(0.01, number(Setup, param::raise_time))), m_lowertime(std::max(0.01, number(Setup, param::lower_time))),
m_minpressure(number(Setup, param::min_pressure)), m_previewvoltage(number(Setup, param::preview_voltage))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const pressure{m_air ? forward_in(Frame, port::air) : body.EnginePowerSource.CollectorParameters.MaxPress};
auto const raise{get_bool(Frame, port::raise) && pressure >= m_minpressure};
auto &pantograph{body.Pantographs[m_index]};
pantograph.is_active = raise;
// the model raises the pantograph at a speed depending on this pressure
body.PantPress = pressure;
m_height = std::clamp(m_height + (raise ? Frame.dt / m_raisetime : -Frame.dt / m_lowertime), 0.0, 1.0);
double voltage;
if (Frame.body->standalone)
{
voltage = m_height >= 1.0 ? m_previewvoltage : 0.0;
}
else
{
voltage = std::abs(m_index == 0 ? body.PantFrontVolt : body.PantRearVolt);
}
pantograph.voltage = voltage;
set_forward(Frame, port::wn, voltage);
// current drawn by the vehicle through this pantograph, in the previous step
Frame.body->current += back_out(Frame, port::wn);
set_bool(Frame, port::up, m_height >= 1.0);
set(Frame, port::height, m_height);
set(Frame, port::u, voltage);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "height";
}
double state_value(std::size_t) const override
{
return m_height;
}
static block_type describe()
{
auto type{make_type("el.pantograph", "Pantograph",
"Current collector: index 0 is the front pantograph of the vehicle model, 1 the rear one. Raising needs the air pressure (when connected). "
"Without the simulator the catenary has the preview voltage.",
block_origin::extracted)};
type.ports = {signal_in("raise", value_type::boolean), physical_in("air", domain::air, true), physical_out("wn", domain::high_voltage),
signal_out("up", value_type::boolean), signal_out("height", value_type::real), signal_out("u", value_type::real, "V")};
type.params = {number_param("index", 0.0), number_param("raise_time", 4.0, "s"), number_param("lower_time", 1.0, "s"), number_param("min_pressure", 3.5, "bar"),
number_param("preview_voltage", 3000.0, "V")};
type.create = make_block<el_pantograph>;
return type;
}
private:
bool const m_air;
int const m_index;
double const m_raisetime;
double const m_lowertime;
double const m_minpressure;
double const m_previewvoltage;
double m_height{0.0};
};
// joins two supplies: the higher voltage wins, the current goes back to the source(s) supplying it
class el_junction final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
b,
out
};
};
explicit el_junction(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const a{forward_in(Frame, port::a)};
auto const b{forward_in(Frame, port::b)};
set_forward(Frame, port::out, std::max(a, b));
auto const current{back_out(Frame, port::out)};
if (std::abs(a - b) < 1.0)
{
add_back(Frame, port::a, 0.5 * current);
add_back(Frame, port::b, 0.5 * current);
}
else
{
add_back(Frame, a > b ? port::a : port::b, current);
}
}
static block_type describe()
{
auto type{make_type("el.junction", "Junction", "Joins two high voltage supplies, e.g. two pantographs.", block_origin::primitive)};
type.ports = {physical_in("a", domain::high_voltage), physical_in("b", domain::high_voltage, true), physical_out("out", domain::high_voltage)};
type.create = make_block<el_junction>;
return type;
}
};
// line breaker (WS). its state is the Mains of the vehicle body, which the sounds and the AI watch; with more breakers
// (multi-system vehicles) the Mains is closed when any of them is
class el_hsb final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
close,
open,
hold,
out,
closed,
i,
tripped
};
};
struct param
{
enum : std::size_t
{
trip_current,
close_time,
min_voltage,
init_time
};
};
explicit el_hsb(block_setup const &Setup)
: block(Setup), m_tripcurrent(number(Setup, param::trip_current)), m_closetime(number(Setup, param::close_time)), m_minvoltage(number(Setup, param::min_voltage)),
m_inittime(number(Setup, param::init_time)), m_initcountdown(m_inittime)
{
}
void update(frame &Frame) override
{
m_voltage = forward_in(Frame, port::in);
m_current = back_out(Frame, port::out);
// the current of the previous step passes to the supply
add_back(Frame, port::in, m_current);
auto const voltageok{m_voltage > 0.0 && m_voltage >= m_minvoltage};
// the breaker can be closed after the voltage was present for the init time
m_initcountdown = voltageok ? m_initcountdown - Frame.dt : m_inittime;
if (m_closed)
{
auto const overload{m_tripcurrent > 0.0 && std::abs(m_current) > m_tripcurrent};
if (overload || false == voltageok || get_bool(Frame, port::open) || false == get_bool(Frame, port::hold))
{
m_closed = false;
m_tripped = m_tripped || overload;
}
}
else if (get_bool(Frame, port::close) && voltageok && m_initcountdown <= 0.0 && get_bool(Frame, port::hold) && false == get_bool(Frame, port::open))
{
m_closing += Frame.dt;
if (m_closing >= m_closetime)
{
m_closed = true;
m_tripped = false;
m_closing = 0.0;
}
}
else
{
m_closing = 0.0;
}
// the vehicle can have more breakers, e.g. one for each supply system; its Mains is closed when any of them is
vehicle(Frame).Mains = Frame.body->mains.any(Frame.step, m_closed);
}
void commit(frame &Frame) override
{
set_forward(Frame, port::out, m_closed ? m_voltage : 0.0);
set_bool(Frame, port::closed, m_closed);
set(Frame, port::i, m_current);
set_bool(Frame, port::tripped, m_tripped);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "closed";
}
double state_value(std::size_t) const override
{
return m_closed ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("el.hsb", "Line breaker",
"Main (line) breaker. Closes after close is held for close_time, with enough voltage present for init_time; opens with open, without hold, "
"below min_voltage or above trip_current (then tripped is set until the next closing).",
block_origin::extracted)};
type.ports = {physical_in("in", domain::high_voltage),
signal_in("close", value_type::boolean),
signal_in("open", value_type::boolean, 0.0, "", true),
signal_in("hold", value_type::boolean, 1.0, "", true),
physical_out("out", domain::high_voltage),
signal_out("closed", value_type::boolean),
signal_out("i", value_type::real, "A"),
signal_out("tripped", value_type::boolean)};
type.params = {number_param("trip_current", 0.0, "A"), number_param("close_time", 0.2, "s"), number_param("min_voltage", 0.0, "V"), number_param("init_time", 0.0, "s")};
type.deferred = true;
type.create = make_block<el_hsb>;
return type;
}
private:
double const m_tripcurrent;
double const m_closetime;
double const m_minvoltage;
double const m_inittime;
double m_initcountdown;
double m_closing{0.0};
double m_voltage{0.0};
double m_current{0.0};
bool m_closed{false};
bool m_tripped{false};
};
class el_contactor final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
on,
out,
closed
};
};
explicit el_contactor(block_setup const &Setup) : block(Setup), m_delay(number(Setup, 0)) {}
void update(frame &Frame) override
{
m_voltage = forward_in(Frame, port::in);
add_back(Frame, port::in, back_out(Frame, port::out));
if (get_bool(Frame, port::on))
{
m_timer += Frame.dt;
m_closed = m_timer >= m_delay;
}
else
{
m_timer = 0.0;
m_closed = false;
}
}
void commit(frame &Frame) override
{
set_forward(Frame, port::out, m_closed ? m_voltage : 0.0);
set_bool(Frame, port::closed, m_closed);
}
static block_type describe()
{
auto type{make_type("el.contactor", "Contactor", "Connects the output to the input after on was held for the delay; disconnects at once.", block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), signal_in("on", value_type::boolean), physical_out("out", domain::high_voltage),
signal_out("closed", value_type::boolean)};
type.params = {number_param("delay", 0.0, "s")};
type.deferred = true;
type.create = make_block<el_contactor>;
return type;
}
private:
double const m_delay;
double m_timer{0.0};
double m_voltage{0.0};
bool m_closed{false};
};
// static converter or motor generator: auxiliary supply from the high voltage, and optionally the onboard low voltage
// (charger). its state is the ConverterFlag of the body; with more converters the flag is set when any of them runs
class el_converter final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
on,
aux,
run,
nn
};
};
struct param
{
enum : std::size_t
{
power,
start_time,
min_voltage,
out_voltage,
nn_voltage,
aux_kind
};
};
explicit el_converter(block_setup const &Setup)
: block(Setup), m_idlepower(number(Setup, param::power) * 0.05), m_starttime(number(Setup, param::start_time)), m_minvoltage(number(Setup, param::min_voltage)),
m_outvoltage(number(Setup, param::out_voltage)), m_nnvoltage(std::max(0.0, number(Setup, param::nn_voltage)))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const on{get_bool(Frame, port::on)};
if (on && voltage > 0.0 && voltage >= m_minvoltage)
{
m_timer += Frame.dt;
}
else
{
m_timer = 0.0;
}
auto const run{on && voltage > 0.0 && voltage >= m_minvoltage && m_timer >= m_starttime};
body.ConverterAllow = Frame.body->converter_allow.any(Frame.step, on);
body.ConverterFlag = Frame.body->converter.any(Frame.step, run);
set_forward(Frame, port::aux, run ? m_outvoltage : 0.0);
set_forward(Frame, port::nn, run ? m_nnvoltage : 0.0);
if (run)
{
// power taken by the auxiliaries and the low voltage loads in the previous step, and the losses
auto const nnpower{m_nnvoltage * std::max(0.0, back_out(Frame, port::nn))};
add_back(Frame, port::wn, (back_out(Frame, port::aux) + nnpower + m_idlepower) / voltage);
}
set_bool(Frame, port::run, run);
}
static block_type describe()
{
auto type{make_type("el.converter", "Converter",
"Converter of the auxiliary supply. Runs start_time after it was switched on with enough voltage; draws the power of its loads from the high "
"voltage. nn is the onboard low voltage made by its charger (off when nn_voltage is 0). aux_kind only names the auxiliary circuit.",
block_origin::extracted)};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("on", value_type::boolean), physical_out("aux", domain::aux_ac), signal_out("run", value_type::boolean),
physical_out("nn", domain::low_voltage)};
// aux_kind describes the auxiliary circuit for the editor (three phase, single phase or direct current); the simulator ignores it
type.params = {number_param("power", 50000.0, "W"), number_param("start_time", 2.0, "s"), number_param("min_voltage", 0.0, "V"),
number_param("out_voltage", 400.0, "V"), number_param("nn_voltage", 0.0, "V"), choice_param("aux_kind", {"ac3", "ac1", "dc"}, "ac3")};
type.create = make_block<el_converter>;
return type;
}
private:
double const m_idlepower;
double const m_starttime;
double const m_minvoltage;
double const m_outvoltage;
double const m_nnvoltage;
double m_timer{0.0};
};
// voltage relay: tells whether the supply is within a range, e.g. to recognize the supply system of a multi-system
// vehicle. the simulator gives the vehicles only the voltage of the catenary, so the systems are told apart by it
class el_voltage_relay final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
ok,
u
};
};
struct param
{
enum : std::size_t
{
min_voltage,
max_voltage,
delay
};
};
explicit el_voltage_relay(block_setup const &Setup)
: block(Setup), m_minvoltage(number(Setup, param::min_voltage)), m_maxvoltage(number(Setup, param::max_voltage)), m_delay(number(Setup, param::delay))
{
}
void update(frame &Frame) override
{
// the relay draws no current
auto const voltage{forward_in(Frame, port::in)};
auto const inrange{voltage > 0.0 && voltage >= m_minvoltage && (m_maxvoltage <= 0.0 || voltage <= m_maxvoltage)};
m_timer = inrange ? m_timer + Frame.dt : 0.0;
set_bool(Frame, port::ok, inrange && m_timer >= m_delay);
set(Frame, port::u, voltage);
}
static block_type describe()
{
auto type{make_type("el.voltage_relay", "Voltage relay",
"ok is set when the voltage was within min_voltage and max_voltage (0: no upper limit) for the delay. Draws no current; used e.g. to recognize "
"the supply system.",
block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), signal_out("ok", value_type::boolean), signal_out("u", value_type::real, "V")};
type.params = {number_param("min_voltage", 2200.0, "V"), number_param("max_voltage", 4000.0, "V"), number_param("delay", 0.5, "s")};
type.create = make_block<el_voltage_relay>;
return type;
}
private:
double const m_minvoltage;
double const m_maxvoltage;
double const m_delay;
double m_timer{0.0};
};
// main transformer of an ac vehicle, or of the ac branch of a multi-system one, with the rectifier behind it implied.
// the output voltage is the input voltage times the ratio; the current drawn from the input follows from the power
class el_transformer final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out
};
};
struct param
{
enum : std::size_t
{
ratio,
eff
};
};
explicit el_transformer(block_setup const &Setup)
: block(Setup), m_ratio(std::max(0.0, number(Setup, param::ratio))), m_efficiency(std::clamp(number(Setup, param::eff), 0.01, 1.0))
{
}
void update(frame &Frame) override
{
set_forward(Frame, port::out, forward_in(Frame, port::in) * m_ratio);
add_back(Frame, port::in, back_out(Frame, port::out) * m_ratio / m_efficiency);
}
static block_type describe()
{
auto type{make_type("el.transformer", "Transformer",
"Output voltage is the input voltage times the ratio (out/in); the input current is the output current times ratio / eff. For ac vehicles "
"and the ac branches of multi-system vehicles, with the rectifier implied.",
block_origin::primitive)};
type.ports = {physical_in("in", domain::high_voltage), physical_out("out", domain::high_voltage)};
type.params = {number_param("ratio", 0.12, ""), number_param("eff", 0.97, "")};
type.create = make_block<el_transformer>;
return type;
}
private:
double const m_ratio;
double const m_efficiency;
};
// battery of the low voltage circuits; its switch is the Battery of the body
class el_battery final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
charge,
nn,
ok,
u
};
};
struct param
{
enum : std::size_t
{
voltage,
capacity,
min_voltage
};
};
explicit el_battery(block_setup const &Setup)
: block(Setup), m_charger(is_connected(Setup, port::charge)), m_voltage(number(Setup, param::voltage)), m_capacity(std::max(0.1, number(Setup, param::capacity))),
m_minvoltage(number(Setup, param::min_voltage)), m_charge(m_capacity)
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const on{get_bool(Frame, port::on)};
if (on)
{
// current of the loads in the previous step, A
m_charge -= std::max(0.0, back_out(Frame, port::nn)) * Frame.dt / 3600.0;
}
if (m_charger && forward_in(Frame, port::charge) > 0.0)
{
// charged at a tenth of the capacity per hour
m_charge += 0.1 * m_capacity * Frame.dt / 3600.0;
}
m_charge = std::clamp(m_charge, 0.0, m_capacity);
// open circuit voltage drops with the charge
auto const voltage{on ? m_voltage * (0.85 + 0.15 * m_charge / m_capacity) : 0.0};
body.Battery = on;
body.BatteryVoltage = voltage;
set_forward(Frame, port::nn, voltage);
set_bool(Frame, port::ok, on && voltage >= m_minvoltage);
set(Frame, port::u, voltage);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "charge";
}
double state_value(std::size_t) const override
{
return m_charge;
}
static block_type describe()
{
auto type{make_type("el.battery", "Battery",
"Battery of the low voltage circuits. ok tells whether its voltage is above min_voltage; it's charged when the charge input has voltage.",
block_origin::extracted)};
type.ports = {signal_in("on", value_type::boolean), physical_in("charge", domain::aux_ac, true), physical_out("nn", domain::low_voltage),
signal_out("ok", value_type::boolean), signal_out("u", value_type::real, "V")};
type.params = {number_param("voltage", 110.0, "V"), number_param("capacity", 100.0, "Ah"), number_param("min_voltage", 80.0, "V")};
type.single = true;
type.create = make_block<el_battery>;
return type;
}
private:
bool const m_charger;
double const m_voltage;
double const m_capacity;
double const m_minvoltage;
double m_charge;
};
} // namespace
void register_electric_blocks(catalog &Catalog)
{
Catalog.add(el_pantograph::describe());
Catalog.add(el_junction::describe());
Catalog.add(el_hsb::describe());
Catalog.add(el_contactor::describe());
Catalog.add(el_converter::describe());
Catalog.add(el_battery::describe());
Catalog.add(el_voltage_relay::describe());
Catalog.add(el_transformer::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,468 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "vehicle/graph/graph_instance.h"
#include "McZapkie/MOVER.h"
#include <array>
#include <cmath>
#include <limits>
// standard interfaces: between the vehicle and its neighbours (brake pipe, feed pipe, train heating), and between the
// vehicle and the AI. the neighbours may be .fiz vehicles, so these blocks work through the couplers of the vehicle body
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description)
{
block_type type;
type.id = std::move(Id);
type.category = "interfaces";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = block_origin::interface_profile;
return type;
}
param_def side_param()
{
return choice_param("side", {"front", "rear"}, "front");
}
// end cock of the brake pipe, or of the feed pipe, at one end of the vehicle
class if_cock final : public block
{
public:
struct port
{
enum : std::size_t
{
pipe,
cock
};
};
struct param
{
enum : std::size_t
{
side,
cock
};
};
if_cock(block_setup const &Setup, bool const Brakepipe)
: block(Setup), m_brakepipe(Brakepipe), m_side(choice(Setup, param::side) == 0 ? end::front : end::rear), m_switched(is_connected(Setup, port::cock)),
m_open(boolean(Setup, param::cock))
{
}
void update(frame &Frame) override
{
auto const open{m_switched ? get_bool(Frame, port::cock) : m_open};
auto &coupler{vehicle(Frame).Couplers[m_side]};
if (m_brakepipe)
{
coupler.BrakePipeCock = open;
}
else
{
coupler.FeedPipeCock = open;
}
}
static block_type describe(bool const Brakepipe)
{
auto type{Brakepipe ? make_type("if.pg", "Brake pipe coupling",
"End cock of the brake pipe at one end of the vehicle. Open (the default), the brake pipe exchanges air with the neighbouring vehicle.")
: make_type("if.pz", "Feed pipe coupling",
"End cock (yellow) of the feed pipe at one end of the vehicle. Closed by default; open, the feed pipe exchanges air with the neighbour.")};
type.ports = {Brakepipe ? physical_in("pg", domain::brake_pipe) : physical_in("pz", domain::feed_pipe), signal_in("cock", value_type::boolean, 0.0, "", true)};
type.params = {side_param(), boolean_param("cock", Brakepipe)};
if (Brakepipe)
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<if_cock>(Setup, true); };
}
else
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<if_cock>(Setup, false); };
}
return type;
}
private:
bool const m_brakepipe;
int const m_side;
bool const m_switched;
bool const m_open;
};
// train heating supply through the couplers. the voltage comes from the main circuit of the vehicle (Mains)
class if_heat final : public block
{
public:
struct port
{
enum : std::size_t
{
wn,
on,
i
};
};
explicit if_heat(block_setup const &Setup) : block(Setup), m_side(choice(Setup, 0) == 0 ? end::front : end::rear) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const voltage{forward_in(Frame, port::wn)};
auto const on{get_bool(Frame, port::on)};
body.HeatingAllow = on;
body.Heating = on && voltage > 0.0;
auto const current{std::abs(body.Couplers[m_side].power_high.current)};
add_back(Frame, port::wn, current);
set(Frame, port::i, current);
}
static block_type describe()
{
auto type{make_type("if.heat", "Train heating",
"Train heating supply through the coupler at one end: on connects the main circuit (behind the line breaker) to the heating line. i is the "
"current the train draws.")};
type.ports = {physical_in("wn", domain::high_voltage), signal_in("on", value_type::boolean, 1.0, "", true), signal_out("i", value_type::real, "A")};
type.params = {side_param()};
type.create = make_block<if_heat>;
return type;
}
private:
int const m_side;
};
// intents of the AI driver, and the feedback it reads. graph vehicles aren't driven by the AI yet: the outputs stay
// at rest until the AI adapter of the graph vehicles is in place
class if_ai final : public block
{
public:
struct port
{
enum : std::size_t
{
ready,
i_ratio,
slip,
pg,
zg,
bc,
vmax,
dir,
traction,
ed_brake,
brake,
emergency,
local_brake,
prepare,
shutdown,
doors_left,
doors_right,
lights,
horn,
pantographs,
sand
};
};
explicit if_ai(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const &intents{Frame.body->ai};
set(Frame, port::dir, intents.dir);
set(Frame, port::traction, intents.traction);
set(Frame, port::ed_brake, intents.ed_brake);
set(Frame, port::brake, intents.brake);
set_bool(Frame, port::emergency, intents.emergency);
set(Frame, port::local_brake, intents.local_brake);
set_bool(Frame, port::prepare, intents.prepare);
set_bool(Frame, port::shutdown, intents.shutdown);
set_bool(Frame, port::doors_left, intents.doors_left);
set_bool(Frame, port::doors_right, intents.doors_right);
set(Frame, port::lights, intents.lights);
set_bool(Frame, port::horn, intents.horn);
set_bool(Frame, port::pantographs, intents.pantographs);
set_bool(Frame, port::sand, intents.sand);
auto &feedback{Frame.body->ai_reply};
feedback.ready = get_bool(Frame, port::ready);
feedback.i_ratio = get(Frame, port::i_ratio);
feedback.slip = get_bool(Frame, port::slip);
feedback.pg = get(Frame, port::pg);
feedback.zg = get(Frame, port::zg);
feedback.bc = get(Frame, port::bc);
feedback.vmax = get(Frame, port::vmax);
}
static block_type describe()
{
auto type{make_type("if.ai", "AI interface",
"What the AI driver wants (outputs) and what it needs to know (inputs). The vehicle's graph translates the intents to its controls. Not "
"used by the simulator yet: the AI doesn't drive graph vehicles.")};
type.ports = {signal_in("ready", value_type::boolean, 0.0, "", true),
signal_in("i_ratio", value_type::real, 0.0, "", true),
signal_in("slip", value_type::boolean, 0.0, "", true),
signal_in("pg", value_type::real, 0.0, "bar", true),
signal_in("zg", value_type::real, 0.0, "bar", true),
signal_in("bc", value_type::real, 0.0, "bar", true),
signal_in("vmax", value_type::real, 0.0, "km/h", true),
signal_out("dir", value_type::real),
signal_out("traction", value_type::real),
signal_out("ed_brake", value_type::real),
signal_out("brake", value_type::real),
signal_out("emergency", value_type::boolean),
signal_out("local_brake", value_type::real),
signal_out("prepare", value_type::boolean),
signal_out("shutdown", value_type::boolean),
signal_out("doors_left", value_type::boolean),
signal_out("doors_right", value_type::boolean),
signal_out("lights", value_type::integer),
signal_out("horn", value_type::boolean),
signal_out("pantographs", value_type::boolean),
signal_out("sand", value_type::boolean)};
type.single = true;
type.create = make_block<if_ai>;
return type;
}
};
// control line of the multiple unit control: a named train wire shared by the coupled graph vehicles. every vehicle
// publishes its contribution in its body state (body_state::lines); the others read what it published last, so the
// values pass between the vehicles with up to one step of delay. the line ends at a vehicle without a graph, at
// a vehicle without the line, or at a coupling without the required flag
class if_line final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
active,
out,
vehicles,
drivers
};
};
struct param
{
enum : std::size_t
{
name,
combine,
reverse,
pair,
coupling
};
};
enum class combination
{
max,
min,
sum,
maxabs,
any
};
explicit if_line(block_setup const &Setup)
: block(Setup), m_name(text(Setup, param::name)), m_pair(text(Setup, param::pair).empty() ? m_name : text(Setup, param::pair)),
m_combine(static_cast<combination>(std::max(0, choice(Setup, param::combine)))), m_reverse(boolean(Setup, param::reverse)),
m_flag(coupling_flag(choice(Setup, param::coupling))), m_drives(is_connected(Setup, port::in))
{
m_hints.fill(0);
}
void update(frame &Frame) override
{
auto &lines{Frame.body->lines};
if (m_entry < 0)
{
// the entry of the line in this vehicle, shared with the other blocks of the same line
m_entry = find(lines, m_name, 0);
if (m_entry < 0)
{
lines.push_back({m_name, 0.0, false});
m_entry = static_cast<int>(lines.size()) - 1;
}
}
auto &own{lines[m_entry]};
if (m_drives)
{
own.value = get(Frame, port::in);
own.active = get_bool(Frame, port::active);
}
m_drivers = 0;
m_vehicles = 1;
if (own.active)
{
add(own.value);
}
auto const *body{Frame.body->mover};
for (int side = 0; side < 2; ++side)
{
walk(*body, side, m_hints.data() + side * max_vehicles);
}
set(Frame, port::out, m_drivers > 0 ? m_result : 0.0);
set(Frame, port::vehicles, m_vehicles);
set(Frame, port::drivers, m_drivers);
}
static block_type describe()
{
auto type{make_type("if.line", "Train line",
"Named control line of the multiple unit control, shared by the coupled vehicles with the line (the coupling needs the selected flag). out "
"combines the values of the vehicles driving the line (in connected and active set), in the orientation of this vehicle: with reverse the "
"value of a vehicle facing the other way changes sign, and such a vehicle gives its pair line (if set) in place of this one.")};
type.ports = {signal_in("in", value_type::real, 0.0, "", true), signal_in("active", value_type::boolean, 1.0, "", true), signal_out("out", value_type::real),
signal_out("vehicles", value_type::integer), signal_out("drivers", value_type::integer)};
type.params = {text_param("name", "linia"), choice_param("combine", {"max", "min", "sum", "maxabs", "or"}, "max"), boolean_param("reverse", false), text_param("pair"),
choice_param("coupling", {"control", "permanent", "coupler", "highvoltage", "heating", "power110v", "power24v", "power3x400v"}, "control")};
type.create = make_block<if_line>;
return type;
}
private:
// longest chain of vehicles followed in each direction, also the guard against coupling loops
static constexpr int max_vehicles{64};
static int coupling_flag(int const Choice)
{
static int const flags[]{coupling::control, coupling::permanent, coupling::coupler, coupling::highvoltage, coupling::heating, coupling::power110v, coupling::power24v,
coupling::power3x400v};
return flags[std::clamp(Choice, 0, static_cast<int>(std::size(flags)) - 1)];
}
// index of the line with the specified name, checking the hint first, or -1
static int find(std::vector<train_line_value> const &Lines, std::string const &Name, int const Hint)
{
if (Hint >= 0 && Hint < static_cast<int>(Lines.size()) && Lines[Hint].name == Name)
{
return Hint;
}
for (std::size_t idx = 0; idx < Lines.size(); ++idx)
{
if (Lines[idx].name == Name)
{
return static_cast<int>(idx);
}
}
return -1;
}
void add(double const Value)
{
if (m_drivers == 0)
{
m_result = (m_combine == combination::any ? (Value != 0.0 ? 1.0 : 0.0) : Value);
}
else
{
switch (m_combine)
{
case combination::max:
m_result = std::max(m_result, Value);
break;
case combination::min:
m_result = std::min(m_result, Value);
break;
case combination::sum:
m_result += Value;
break;
case combination::maxabs:
m_result = (std::abs(Value) > std::abs(m_result) ? Value : m_result);
break;
case combination::any:
m_result = (m_result != 0.0 || Value != 0.0 ? 1.0 : 0.0);
break;
}
}
++m_drivers;
}
// follows the coupled vehicles from the specified side of the vehicle. a neighbour coupled by the same side as the one
// leading to it (front to front, rear to rear) faces the other way
void walk(TMoverParameters const &Body, int Side, int *Hints)
{
auto const *current{&Body};
auto facing{1}; // orientation of the current vehicle relative to this one
for (int count = 0; count < max_vehicles; ++count)
{
auto const &coupler{current->Couplers[Side]};
if (coupler.Connected == nullptr || (coupler.CouplingFlag & m_flag) == 0 || coupler.Connected == &Body)
{
return;
}
auto const *neighbour{coupler.Connected};
auto const connectedside{coupler.ConnectedNr};
facing = (Side != connectedside ? facing : -facing);
if (neighbour->Graph == nullptr)
{
return;
}
auto const &lines{neighbour->Graph->body().lines};
auto const index{find(lines, facing > 0 ? m_name : m_pair, Hints[count])};
if (index < 0)
{
return;
}
Hints[count] = index;
++m_vehicles;
auto const &line{lines[index]};
if (line.active)
{
add(m_reverse && facing < 0 ? -line.value : line.value);
}
current = neighbour;
Side = 1 - connectedside;
}
}
std::string const m_name;
std::string const m_pair;
combination const m_combine;
bool const m_reverse;
int const m_flag;
bool const m_drives;
int m_entry{-1};
std::array<int, 2 * max_vehicles> m_hints;
double m_result{0.0};
int m_drivers{0};
int m_vehicles{1};
};
} // namespace
void register_interface_blocks(catalog &Catalog)
{
Catalog.add(if_cock::describe(true));
Catalog.add(if_cock::describe(false));
Catalog.add(if_heat::describe());
Catalog.add(if_ai::describe());
Catalog.add(if_line::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,278 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_format.h"
#include "vehicle/graph/graph_lua.h"
#include <cmath>
#ifdef WITH_LUA
#include <lua.hpp>
#endif
// function block defined by a lua script. its ports and numeric parameters come from the script
namespace vgraph
{
namespace
{
bool resolve_lua_block(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
if (false == lua_available())
{
Error = "lua blocks need a simulator built with lua support (WITH_LUA)";
return false;
}
if (Node.script.empty())
{
Error = "lua block without a script";
return false;
}
lua_block_definition definition;
if (false == parse_lua_block(Node.script, Node.id, definition, Error))
{
Error = "lua: " + Error;
return false;
}
Ports = std::move(definition.ports);
Params = std::move(definition.params);
return true;
}
#ifdef WITH_LUA
class lua_block final : public block
{
public:
explicit lua_block(block_setup const &Setup) : block(Setup), m_host(Setup.lua), m_nodeid(Setup.node_id)
{
for (std::size_t idx = 0; idx < Setup.ports.size(); ++idx)
{
auto const &port{Setup.ports[idx]};
(port.direction == port_direction::in ? m_inputs : m_outputs).push_back({idx, port.key, port.type});
}
if (m_host == nullptr || m_host->state() == nullptr)
{
fail("no lua state");
return;
}
std::string error;
m_definition = m_host->load_block(Setup.script, Setup.node_id, error);
if (m_definition < 0)
{
fail(error);
return;
}
auto *L{m_host->state()};
auto const top{lua_gettop(L)};
lua_rawgeti(L, LUA_REGISTRYINDEX, m_definition);
auto const definition{lua_gettop(L)};
lua_getfield(L, definition, "rate");
auto const rate{lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0.0};
lua_pop(L, 1);
m_period = (rate > 0.0 ? 1.0 / rate : 0.0);
// self: copy of the state table, and the parameters with values from the graph for the numeric ones
lua_newtable(L);
auto const self{lua_gettop(L)};
lua_getfield(L, definition, "state");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, self);
}
}
lua_pop(L, 1);
lua_getfield(L, definition, "params");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TNUMBER)
{
auto const paramindex{find_param(Setup.param_defs, std::string{"lua."} + lua_tostring(L, -2))};
if (paramindex >= 0)
{
lua_pop(L, 1);
lua_pushnumber(L, Setup.params[paramindex].number);
}
}
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, self);
}
}
lua_pop(L, 1);
m_self = luaL_ref(L, LUA_REGISTRYINDEX);
lua_newtable(L);
m_in = luaL_ref(L, LUA_REGISTRYINDEX);
lua_newtable(L);
m_out = luaL_ref(L, LUA_REGISTRYINDEX);
lua_settop(L, top);
}
~lua_block() override
{
if (m_host != nullptr)
{
m_host->release(m_definition);
m_host->release(m_self);
m_host->release(m_in);
m_host->release(m_out);
}
}
void update(frame &Frame) override
{
if (m_fault)
{
// outputs keep their last values
return;
}
auto dt{Frame.dt};
if (m_period > 0.0)
{
m_accumulated += Frame.dt;
if (m_accumulated + 1e-9 < m_period)
{
return;
}
dt = m_accumulated;
m_accumulated = 0.0;
}
auto *L{m_host->state()};
auto const top{lua_gettop(L)};
lua_rawgeti(L, LUA_REGISTRYINDEX, m_in);
for (auto const &input : m_inputs)
{
auto const value{get(Frame, input.port)};
switch (input.type)
{
case value_type::boolean:
lua_pushboolean(L, value > 0.5);
break;
case value_type::integer:
lua_pushnumber(L, std::round(value));
break;
default:
lua_pushnumber(L, value);
break;
}
lua_setfield(L, -2, input.key.c_str());
}
lua_pop(L, 1);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_definition);
lua_getfield(L, -1, "update");
lua_remove(L, -2);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_self);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_in);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_out);
lua_pushnumber(L, dt);
std::string error;
if (false == m_host->call(4, error))
{
lua_settop(L, top);
fail(error);
return;
}
lua_rawgeti(L, LUA_REGISTRYINDEX, m_out);
for (auto const &output : m_outputs)
{
lua_getfield(L, -1, output.key.c_str());
auto value{0.0};
if (lua_isboolean(L, -1))
{
value = (lua_toboolean(L, -1) ? 1.0 : 0.0);
}
else if (lua_type(L, -1) == LUA_TNUMBER)
{
value = lua_tonumber(L, -1);
if (output.type == value_type::integer)
{
value = std::round(value);
}
}
lua_pop(L, 1);
set(Frame, output.port, value);
}
lua_settop(L, top);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "fault";
}
double state_value(std::size_t) const override
{
return m_fault ? 1.0 : 0.0;
}
private:
struct port_entry
{
std::size_t port;
std::string key;
value_type type;
};
void fail(std::string const &Reason)
{
// a failing block stops, and the rest of the vehicle keeps working
m_fault = true;
log_message("lua [" + m_nodeid + "]: block stopped: " + Reason);
}
lua_host *m_host;
std::string m_nodeid;
std::vector<port_entry> m_inputs;
std::vector<port_entry> m_outputs;
int m_definition{-1};
int m_self{-1};
int m_in{-1};
int m_out{-1};
double m_period{0.0};
double m_accumulated{0.0};
bool m_fault{false};
};
#endif
} // namespace
void register_script_blocks(catalog &Catalog)
{
block_type type;
type.id = "lua.block";
type.category = "script";
type.name = "Lua block";
type.description = "Function block defined by a lua 5.1 script: block \"name\" { inputs, outputs, params, state, rate, update = function(self, i, o, dt) ... end }.";
type.origin = block_origin::script;
type.dynamic_ports = true;
type.resolve = resolve_lua_block;
#ifdef WITH_LUA
type.create = make_block<lua_block>;
#else
// graphs with lua blocks are rejected when loaded, so blocks of this type are never created
type.create = nullptr;
#endif
Catalog.add(std::move(type));
}
} // namespace vgraph

View File

@@ -0,0 +1,186 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_format.h"
// modules: graphs of logic made once and used by many vehicles. a node of type "module" names the file of the module;
// when the vehicle graph is loaded, the node is replaced with the blocks of the module (expand_modules), connected
// through the module's interface blocks, module.input and module.output, one per port of the module.
// the interface blocks work also on their own, so a module file can be tried with -graph-run: an input gives its
// default value (forced values come from the script), an output shows the value it receives
namespace vgraph
{
namespace
{
std::vector<std::string> interface_types()
{
std::vector<std::string> result{"bool", "int", "float"};
for (std::size_t idx = 1; idx < static_cast<std::size_t>(domain::count_); ++idx)
{
result.emplace_back(info(static_cast<domain>(idx)).id);
}
return result;
}
std::vector<param_def> interface_params(bool const Input)
{
std::vector<param_def> params{choice_param("type", interface_types(), "float")};
if (Input)
{
params.push_back(number_param("default", 0.0));
}
params.push_back(text_param("label"));
return params;
}
// the port of an interface block, of the type chosen by its "type" parameter
bool interface_port(node_desc const &Node, bool const Input, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
std::string type{"float"};
for (auto const &param : Node.params)
{
if (param.first == "type")
{
type = param.second.text;
}
}
// the input of the module is an output of the interface block inside it, and the other way round
auto const key{Input ? "out" : "in"};
if (type == "bool" || type == "int" || type == "float")
{
auto const valuetype{type == "bool" ? value_type::boolean : type == "int" ? value_type::integer : value_type::real};
Ports = {Input ? signal_out(key, valuetype) : signal_in(key, valuetype, 0.0, "", true)};
}
else
{
domain dom;
if (false == parse_domain(type, dom) || dom == domain::signal)
{
Error = "unknown type \"" + type + "\" of the module port; expected bool, int, float or a physical domain";
return false;
}
Ports = {Input ? physical_out(key, dom) : physical_in(key, dom, true)};
}
Params = interface_params(Input);
return true;
}
bool resolve_input(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
return interface_port(Node, true, Ports, Params, Error);
}
bool resolve_output(node_desc const &Node, std::vector<port_def> &Ports, std::vector<param_def> &Params, std::string &Error)
{
return interface_port(Node, false, Ports, Params, Error);
}
bool resolve_module(node_desc const &, std::vector<port_def> &, std::vector<param_def> &, std::string &Error)
{
// expand_modules replaces these nodes when the file is loaded, so one left here means the module couldn't be read
Error = "the module wasn't expanded; graphs with modules have to be loaded from a file, next to the module files";
return false;
}
class module_input final : public block
{
public:
explicit module_input(block_setup const &Setup) : block(Setup), m_signal(Setup.ports[0].dom == domain::signal), m_value(number(Setup, 1)) {}
void update(frame &Frame) override
{
if (m_signal)
{
set(Frame, 0, m_value);
}
else
{
set_forward(Frame, 0, m_value);
}
}
private:
bool const m_signal;
double const m_value;
};
class module_output final : public block
{
public:
explicit module_output(block_setup const &Setup) : block(Setup), m_signal(Setup.ports[0].dom == domain::signal), m_connected(is_connected(Setup, 0)) {}
void update(frame &Frame) override
{
if (false == m_connected)
{
return;
}
m_value = m_signal ? get(Frame, 0) : forward_in(Frame, 0);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "value";
}
double state_value(std::size_t) const override
{
return m_value;
}
private:
bool const m_signal;
bool const m_connected;
double m_value{0.0};
};
block_type interface_type(bool const Input)
{
block_type type;
type.id = Input ? "module.input" : "module.output";
type.category = "module";
type.name = Input ? "Module input" : "Module output";
type.description = Input ? "Input of a module: the value given to the module node in the vehicle graph. On its own it gives the default value."
: "Output of a module: the value the module node gives in the vehicle graph.";
type.origin = block_origin::primitive;
type.dynamic_ports = true;
type.resolve = Input ? resolve_input : resolve_output;
type.params = interface_params(Input);
type.create = Input ? make_block<module_input> : make_block<module_output>;
return type;
}
} // namespace
void register_module_blocks(catalog &Catalog)
{
Catalog.add(interface_type(true));
Catalog.add(interface_type(false));
block_type type;
type.id = std::string{module_type};
type.category = "module";
type.name = "Module";
type.description = "Graph of logic from another file (a module), used as one block. Its ports are the module.input and module.output blocks "
"of the module; the file is given relative to the vehicle graph.";
type.origin = block_origin::primitive;
type.dynamic_ports = true;
type.resolve = resolve_module;
type.params = {text_param("file")};
type.create = nullptr;
Catalog.add(std::move(type));
}
} // namespace vgraph

View File

@@ -0,0 +1,801 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <cmath>
// air supply and brakes. the blocks drive the brake equipment of the vehicle body (TMoverParameters): its brake pipe and
// feed pipe, which exchange air with the neighbouring vehicles, and the distributor and driver's brake valves created
// by CheckLocomotiveParameters from the parameters of these blocks, with the same code as for the .fiz vehicles.
// the sequence of the operations follows TMoverParameters::UpdatePipePressure and UpdateScndPipePressure
namespace vgraph
{
namespace
{
// cross-section of the feed pipe connection, as in TMoverParameters::UpdateScndPipePressure
double constexpr feed_pipe_section{0.5067};
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Category, std::string Name, std::string Description, block_origin Origin)
{
block_type type;
type.id = std::move(Id);
type.category = std::move(Category);
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = Origin;
return type;
}
// compressor of the main reservoir
class pn_compressor final : public block
{
public:
struct port
{
enum : std::size_t
{
power,
power_hv,
on,
air,
run
};
};
struct param
{
enum : std::size_t
{
flow,
max_pressure,
min_voltage,
power
};
};
explicit pn_compressor(block_setup const &Setup)
: block(Setup), m_aux(is_connected(Setup, port::power)), m_hv(is_connected(Setup, port::power_hv)), m_flow(number(Setup, param::flow)),
m_maxpressure(number(Setup, param::max_pressure)), m_minvoltage(number(Setup, param::min_voltage)), m_power(number(Setup, param::power))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// without supply connections the compressor is assumed to be powered
auto powered{false == m_aux && false == m_hv};
auto const auxvoltage{m_aux ? forward_in(Frame, port::power) : 0.0};
auto const hvvoltage{m_hv ? forward_in(Frame, port::power_hv) : 0.0};
powered = powered || (m_aux && auxvoltage >= m_minvoltage && auxvoltage > 0.0) || (m_hv && hvvoltage >= m_minvoltage && hvvoltage > 0.0);
auto const run{powered && get_bool(Frame, port::on) && body.VeselVolume > 0.0 && m_maxpressure > 0.0};
body.CompressorAllow = get_bool(Frame, port::on);
body.CompressorFlag = run;
if (run)
{
// TMoverParameters::CompressorCheck, compressor working at steady pace
body.CompressedVolume += m_flow * (2.0 * m_maxpressure - body.Compressor) / m_maxpressure * Frame.dt;
if (m_aux && auxvoltage > 0.0)
{
add_back(Frame, port::power, m_power);
}
else if (m_hv && hvvoltage > 0.0)
{
add_back(Frame, port::power_hv, m_power / hvvoltage);
}
}
set_forward(Frame, port::air, body.Compressor);
set_bool(Frame, port::run, run);
}
static block_type describe()
{
auto type{make_type("pn.compressor", "pneumatics", "Compressor",
"Compressor of the main reservoir. Without a supply connected it's assumed to be powered; with both connected either one is enough.",
block_origin::extracted)};
type.ports = {physical_in("power", domain::aux_ac, true), physical_in("power_hv", domain::high_voltage, true), signal_in("on", value_type::boolean),
physical_out("air", domain::air), signal_out("run", value_type::boolean)};
type.params = {fiz(number_param("flow", 0.05, "m3/s"), "Brake:", "CompressorSpeed"), number_param("max_pressure", 10.0, "bar"), number_param("min_voltage", 0.0, "V"),
number_param("power", 10000.0, "W")};
type.single = true;
type.create = make_block<pn_compressor>;
return type;
}
private:
bool const m_aux;
bool const m_hv;
double const m_flow;
double const m_maxpressure;
double const m_minvoltage;
double const m_power;
};
// pressure switch of the compressor: asks for air below the lower pressure, until the upper one is reached
class pn_governor final : public block
{
public:
struct port
{
enum : std::size_t
{
p,
need
};
};
struct param
{
enum : std::size_t
{
min,
max
};
};
explicit pn_governor(block_setup const &Setup) : block(Setup), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)) {}
void update(frame &Frame) override
{
auto const pressure{get(Frame, port::p)};
if (pressure < m_min)
{
m_need = true;
}
else if (pressure > m_max)
{
m_need = false;
}
vehicle(Frame).CompressorGovernorLock = false == m_need;
}
void commit(frame &Frame) override
{
set_bool(Frame, port::need, m_need);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "need";
}
double state_value(std::size_t) const override
{
return m_need ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("pn.governor", "pneumatics", "Pressure governor", "Pressure switch of the compressor: on below min, off above max.", block_origin::primitive)};
type.ports = {signal_in("p", value_type::real, 0.0, "bar"), signal_out("need", value_type::boolean)};
type.params = {fiz(number_param("min", 7.5, "bar"), "Brake:", "MinCP"), fiz(number_param("max", 8.5, "bar"), "Brake:", "MaxCP")};
type.deferred = true;
type.create = make_block<pn_governor>;
return type;
}
private:
double const m_min;
double const m_max;
bool m_need{true};
};
// main reservoir of the vehicle, and its connection with the feed pipe
class pn_reservoir final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
out,
pz,
p
};
};
explicit pn_reservoir(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
if (body.VeselVolume > 0.0)
{
// TMoverParameters::CompressorCheck: safety valve and leaks
body.Compressor = body.CompressedVolume / body.VeselVolume;
body.EmergencyValveOpen = body.Compressor > (body.EmergencyValveOpen ? body.EmergencyValveOff : body.EmergencyValveOn);
if (body.EmergencyValveOpen)
{
body.CompressedVolume -= PF(0, body.Compressor, body.EmergencyValveArea) * dt;
}
body.CompressedVolume = std::max(0.0, body.CompressedVolume - dt * body.AirLeakRate * 0.1);
body.Compressor = body.CompressedVolume / body.VeselVolume;
}
else
{
body.Compressor = 0.0;
}
set_forward(Frame, port::out, body.Compressor);
set_forward(Frame, port::pz, body.ScndPipePress);
set(Frame, port::p, body.Compressor);
}
// the feed pipe, after the other blocks took their air from it (TMoverParameters::UpdateScndPipePressure)
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
// the end cocks (if.pz) are checked by the exchange
auto const exchange{body.ScndPipeExchange(dt)};
if (body.VeselVolume > 0.0)
{
auto const flow{PF(body.Compressor, body.ScndPipePress, feed_pipe_section) * dt};
body.CompressedVolume += flow / 1000.0;
body.Pipe2->Flow(-flow);
}
body.Pipe2->Flow(exchange);
body.Pipe2->Act();
body.ScndPipePress = body.Pipe2->P();
if (body.ScndPipePress < -1.0)
{
body.ScndPipePress = -1.0;
body.Pipe2->CreatePress(-1.0);
body.Pipe2->Act();
}
if (body.CompressedVolume < 0.0)
{
body.CompressedVolume = 0.0;
}
}
static block_type describe()
{
auto type{make_type("pn.reservoir", "pneumatics", "Main reservoir",
"The main reservoir of the vehicle. It feeds the feed pipe (pz), which the brake valves and the neighbouring vehicles take air from.",
block_origin::simulator)};
type.ports = {physical_in("in", domain::air, true), physical_out("out", domain::air), physical_out("pz", domain::feed_pipe), signal_out("p", value_type::real, "bar")};
type.params = {fiz(number_param("volume", 0.0, "m3"), "Brake:", "Vv")};
type.single = true;
type.late = true;
type.create = make_block<pn_reservoir>;
return type;
}
};
// brake pipe of the vehicle
class br_pipe final : public block
{
public:
struct port
{
enum : std::size_t
{
in,
vent,
dist,
couplers,
p
};
};
explicit br_pipe(block_setup const &Setup) : block(Setup), m_trainlength(std::max(0.0, number(Setup, 0))) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
if (Frame.body->standalone && m_trainlength > 0.0 && false == m_extended)
{
// without the simulator there are no neighbours: the pipe gets the volume of the train instead
auto const pressure{body.Pipe->P()};
body.Pipe->CreateCap((std::max(body.Dim.L, 14.0) + 0.5 + m_trainlength) * body.Spg);
body.Pipe->CreatePress(pressure);
m_extended = true;
}
// emergency valve: the vent input, and the emergency brakes the simulator applies to every vehicle
body.EmergencyValveFlow = 0.0;
if (get_bool(Frame, port::vent) || body.RadioStopFlag || body.AlarmChainFlag)
{
body.EmergencyValveFlow = PF(0, body.PipePress, 0.15) * dt;
}
body.Pipe->Flow(-body.EmergencyValveFlow);
body.Pipe->Flow(-body.PipePress * 0.001 * dt);
body.Pipe->Act();
body.PipePress = body.Pipe->P();
publish(Frame, body.PipePress);
}
// exchange with the neighbours, after the distributor took its air
void late_update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// the end cocks (if.pg) are checked by the exchange
body.Pipe->Flow(body.GetDVc(Frame.dt));
body.Pipe->Act();
body.PipePress = body.Pipe->P();
if (body.PipePress < -1.0)
{
body.PipePress = -1.0;
body.Pipe->CreatePress(-1.0);
body.Pipe->Act();
}
}
static block_type describe()
{
auto type{make_type("br.pipe", "brakes", "Brake pipe",
"The brake pipe of the vehicle, connected through the couplers with the neighbouring vehicles. vent opens the emergency valve. "
"preview_train_length adds the pipe of a train of that length when the graph runs without the simulator.",
block_origin::native)};
type.ports = {physical_in("in", domain::brake_pipe, true), signal_in("vent", value_type::boolean, 0.0, "", true), physical_out("dist", domain::brake_pipe),
physical_out("couplers", domain::brake_pipe), signal_out("p", value_type::real, "bar")};
type.params = {number_param("preview_train_length", 0.0, "m")};
type.single = true;
type.late = true;
type.create = make_block<br_pipe>;
return type;
}
private:
double const m_trainlength; // without the simulator: length of the brake pipe of the train behind the vehicle
bool m_extended{false};
void publish(frame &Frame, double const Pressure)
{
set_forward(Frame, port::dist, Pressure);
set_forward(Frame, port::couplers, Pressure);
set(Frame, port::p, Pressure);
}
};
// driver's brake valve
class br_handle final : public block
{
public:
struct port
{
enum : std::size_t
{
zg,
pos,
emerg,
reductor,
pg,
p_ctl
};
};
explicit br_handle(block_setup const &Setup) : block(Setup), m_reductor(is_connected(Setup, port::reductor)) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
auto position{get(Frame, port::pos)};
if (get_bool(Frame, port::emerg))
{
position = body.Handle->GetPos(bh_EB);
}
body.BrakeCtrlPosR = position;
body.BrakeCtrlPos = static_cast<int>(std::floor(position + 0.5));
// brake pipe lock in the emergency state, per the .fiz parameters (LPOn, LPOff, HandlePipeUnlockPos, EmergencyCutsOffHandle)
body.LockPipe = body.PipePress < (body.LockPipe ? body.LockPipeOff : body.LockPipeOn);
auto const lock{(body.LockPipe && false == body.UnlockPipe && position > body.HandleUnlock) || (body.EmergencyCutsOffHandle && body.EmergencyValveFlow > 0)};
auto const supply{lock ? body.PipePress + 0.00001 : body.ScndPipePress};
body.Handle->SetReductor(m_reductor ? get(Frame, port::reductor) : body.BrakeCtrlPos2);
auto const flow{body.Handle->GetPF(position, body.PipePress, supply, dt, body.EqvtPipePress)};
if (flow < 0)
{
if (body.Compressor > body.ScndPipePress)
{
body.CompressedVolume += flow / 1500.0;
body.Pipe2->Flow(flow / 3.0);
}
else
{
body.Pipe2->Flow(flow);
}
}
body.Pipe->Flow(-flow);
// normalised as in UpdatePipePressure, for the sounds of the valve
body.dpMainValve = flow / (std::max(body.Dim.L, 1.0) * std::max(body.Spg, 0.01) * 20) / (100.0 * dt);
set_forward(Frame, port::pg, body.PipePress);
set(Frame, port::p_ctl, body.Handle->GetCP());
}
static block_type describe()
{
auto type{make_type("br.handle", "brakes", "Driver's brake valve",
"Driver's brake valve (TDriverHandle). pos is the position of the handle in the valve's own scale, e.g. -1..6 for FV4a; emerg moves it to the "
"emergency position. It takes air from the feed pipe of the main reservoir.",
block_origin::simulator)};
type.ports = {physical_in("zg", domain::air),
signal_in("pos", value_type::real),
signal_in("emerg", value_type::boolean, 0.0, "", true),
signal_in("reductor", value_type::real, 0.0, "bar", true),
physical_out("pg", domain::brake_pipe),
signal_out("p_ctl", value_type::real, "bar")};
type.params = {
fiz(choice_param("type", {"FV4a", "MHZ_EN57", "MHZ_K5P", "MHZ_K8P", "MHZ_6P", "M394", "Knorr", "St113", "FVel6", "FVE408", "D2", "Westinghouse", "test"}, "FV4a"), "Cntrl.",
"BrakeHandle"),
fiz(boolean_param("auto_overload", false), "Cntrl.", "HAO"),
fiz(boolean_param("manual_overload", false), "Cntrl.", "HMO"),
fiz(number_param("gdp1", 0.0), "Cntrl.", "HGDP1"),
fiz(number_param("gdp2", 0.0), "Cntrl.", "HGDP2"),
fiz(number_param("overload_max", 0.0, "bar"), "Cntrl.", "OMP"),
fiz(number_param("overload_decrease", 0.0, "bar/s"), "Cntrl.", "OPD"),
fiz(number_param("lock_on", -1.0, "bar"), "Brake:", "LPOn"),
fiz(number_param("lock_off", -1.0, "bar"), "Brake:", "LPOff"),
fiz(number_param("unlock_pos", -3.0), "Brake:", "HandlePipeUnlockPos"),
fiz(boolean_param("emergency_cuts_off", false), "Brake:", "EmergencyCutsOffHandle"),
};
type.single = true;
type.create = make_block<br_handle>;
return type;
}
private:
bool const m_reductor;
};
// independent (local) brake valve
class br_local_handle final : public block
{
public:
struct port
{
enum : std::size_t
{
zg,
pos,
bc
};
};
explicit br_local_handle(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const position{std::clamp(get(Frame, port::pos), 0.0, 1.0)};
body.LocalBrakePosA = position;
body.dpLocalValve = body.LocHandle->GetPF(position, body.Hamulec->GetBCP(), body.ScndPipePress, Frame.dt, 0);
body.LocBrakePress = body.LocHandle->GetCP();
set_forward(Frame, port::bc, body.LocBrakePress);
}
static block_type describe()
{
auto type{make_type("br.local_handle", "brakes", "Independent brake valve",
"Independent (local) brake valve of the locomotive. pos 0..1 is the position of the handle; bc is the pressure it sets for the distributor.",
block_origin::simulator)};
type.ports = {physical_in("zg", domain::air), signal_in("pos", value_type::real), physical_out("bc", domain::air)};
type.params = {fiz(choice_param("type", {"FD1", "Knorr", "Westinghouse"}, "FD1"), "Cntrl.", "LocBrakeHandle")};
type.single = true;
type.create = make_block<br_local_handle>;
return type;
}
};
// distributor (brake valve) of the vehicle, with its auxiliary reservoir and brake cylinders
class br_distributor final : public block
{
public:
struct port
{
enum : std::size_t
{
pg,
feed,
local,
ed,
release,
cyl,
bc,
aux,
crp
};
};
explicit br_distributor(block_setup const &Setup)
: block(Setup), m_local(is_connected(Setup, port::local)), m_ed(is_connected(Setup, port::ed)), m_release(is_connected(Setup, port::release))
{
}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const dt{Frame.dt};
auto &brake{*body.Hamulec};
brake.ForceLeak(dt * body.AirLeakRate * 0.25);
if (m_local)
{
body.LocBrakePress = forward_in(Frame, port::local);
brake.SetLBP(body.LocBrakePress);
}
// load dependent brake force, as in TMoverParameters::UpdatePipePressure
switch (body.BrakeValve)
{
case TBrakeValve::K:
case TBrakeValve::W:
case TBrakeValve::KE:
case TBrakeValve::EStED:
case TBrakeValve::ESt3AL2:
case TBrakeValve::NESt3:
case TBrakeValve::ESt4:
case TBrakeValve::ESt3:
{
brake.PLC(body.MBPM < 2 ? body.MaxBrakePress[body.LoadFlag] : body.TotalMass - body.Mred);
break;
}
case TBrakeValve::EP2:
case TBrakeValve::EP1:
{
brake.PLC(body.TotalMass - body.Mred);
break;
}
default:
{
break;
}
}
if (m_ed)
{
brake.SetED(std::clamp(get(Frame, port::ed), 0.0, 1.0));
}
if (m_release)
{
brake.Releaser(get_bool(Frame, port::release) ? 1 : 0);
}
body.CntrlPipePress = brake.GetVRP();
// the brake pipe
auto const connected{(brake.GetBrakeStatus() & b_dmg) == b_dmg ? 0.0 : 1.0};
body.Pipe->Flow(connected * brake.GetPF(connected * body.PipePress, dt, body.Vel));
if (body.ASBType == 128)
{
brake.ASB(int(body.SlippingWheels && body.Vel > 1) * (1 + 2 * int(body.nrot_eps < -0.01)));
}
// the feed pipe
body.Pipe2->Flow(brake.GetHPFlow(body.ScndPipePress, dt));
body.BrakePress = brake.GetBCP();
body.Volume = brake.GetBRP();
set_forward(Frame, port::cyl, body.BrakePress);
set(Frame, port::bc, body.BrakePress);
set(Frame, port::aux, body.Volume);
set(Frame, port::crp, brake.GetCRP());
}
static block_type describe()
{
auto type{make_type("br.distributor", "brakes", "Distributor",
"Distributor of the automatic brake (TBrake), with its auxiliary reservoir and the brake cylinders. local is the pressure of the independent "
"brake valve; ed (0..1) is the share of the electrodynamic brake for the blending of the EStED type; release operates the releaser.",
block_origin::simulator)};
type.ports = {physical_in("pg", domain::brake_pipe),
physical_in("feed", domain::feed_pipe, true),
physical_in("local", domain::air, true),
signal_in("ed", value_type::real, 0.0, "", true),
signal_in("release", value_type::boolean, 0.0, "", true),
physical_out("cyl", domain::air),
signal_out("bc", value_type::real, "bar"),
signal_out("aux", value_type::real, "bar"),
signal_out("crp", value_type::real, "bar")};
type.params = {
fiz(choice_param("type", {"W", "W_Lu_L", "W_Lu_XR", "W_Lu_VI", "K", "Kg", "Kp", "Kss", "Kkg", "Kkp", "Kks", "Hikp1", "Hikss", "Hikg1", "KE", "SW", "EStED", "NESt3", "ESt3",
"LSt", "ESt4", "ESt3AL2", "EP1", "EP2", "M483", "CV1_L_TR", "CV1", "CV1_R"},
"ESt3"),
"Brake:", "BrakeValve"),
fiz(choice_param("mode", {"G", "P", "R", "GP", "PR", "GPR", "GPR+Mg", "PR+Mg"}, "GP"), "Cntrl.", "BrakeDelays"),
fiz(number_param("max_bp", 3.8, "bar"), "Brake:", "MaxBP"),
fiz(number_param("max_lbp", 0.0, "bar"), "Brake:", "MaxLBP"),
fiz(number_param("tare_max_bp", 0.0, "bar"), "Brake:", "TareMaxBP"),
fiz(number_param("max_asbp", 0.0, "bar"), "Brake:", "MaxASBP"),
fiz(number_param("cyl_count", 2.0), "Brake:", "BCN"),
fiz(number_param("cyl_radius", 0.178, "m"), "Brake:", "BCR"),
fiz(number_param("cyl_dist", 0.1, "m"), "Brake:", "BCD"),
fiz(number_param("volume", 0.0, "l"), "Brake:", "BVV"),
fiz(number_param("rm", 1.0), "Brake:", "RM"),
fiz(number_param("rv", 55.0, "km/h"), "Brake:", "RV"),
fiz(number_param("valve_size", 0.0), "Brake:", "Size"),
fiz(number_param("delay1", 0.0, "s"), "Cntrl.", "BDelay1"),
fiz(number_param("delay2", 0.0, "s"), "Cntrl.", "BDelay2"),
fiz(number_param("delay3", 0.0, "s"), "Cntrl.", "BDelay3"),
fiz(number_param("delay4", 0.0, "s"), "Cntrl.", "BDelay4"),
fiz(choice_param("asb", {"none", "manual", "automatic", "yes"}, "none"), "Cntrl.", "ASB"),
};
type.single = true;
type.create = make_block<br_distributor>;
return type;
}
private:
bool const m_local;
bool const m_ed;
bool const m_release;
};
// brake rigging: from the cylinder pressure to the brake force at the wheels
class br_rigging final : public block
{
public:
struct port
{
enum : std::size_t
{
bc,
spring,
force
};
};
struct param
{
enum : std::size_t
{
bcm,
bcs,
bsa,
bre,
nbpa,
material
};
};
explicit br_rigging(block_setup const &Setup) : block(Setup), m_material(make_friction_material(material_id(text(Setup, param::material)))) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
// the cylinders are described by the distributor (BCR, BCN), the rigging by this block; both are in the body
brake_rigging_setup setup;
setup.press_to_force = body.P2FTrans;
setup.cylinder_spring = body.BrakeCylSpring;
setup.cylinder_mult = body.BrakeCylMult[0];
setup.slack_adjuster = body.BrakeSlckAdj;
setup.rigging_efficiency = body.BrakeRigEff;
setup.cylinder_count = body.BrakeCylNo;
setup.axles = body.NAxles;
setup.blocks_per_axle = body.NBpA;
auto const shoeforce{brake_rigging_shoe_force(setup, std::max(0.0, get(Frame, port::spring)), get(Frame, port::bc), body.Ntotal)};
auto const friction{m_material->GetFC(shoeforce, body.Vel)};
body.UnitBrakeForce = friction * shoeforce * 1000.0;
auto const force{body.UnitBrakeForce * body.NAxles * std::max(1, body.NBpA)};
Frame.body->brake_force += force;
set(Frame, port::force, force);
}
static block_type describe()
{
auto type{make_type("br.rigging", "brakes", "Brake rigging",
"Brake rigging and shoes or discs: the brake force from the cylinder pressure (bc) and the force of the spring brake (spring, kN). The force is "
"applied at the wheels of the vehicle.",
block_origin::extracted)};
type.ports = {signal_in("bc", value_type::real, 0.0, "bar"), signal_in("spring", value_type::real, 0.0, "kN", true), signal_out("force", value_type::real, "N")};
type.params = {
fiz(number_param("bcm", 1.0), "Brake:", "BCM"),
fiz(number_param("bcs", 0.0, "kN"), "Brake:", "BCS"),
fiz(number_param("bsa", 0.0, "kN"), "Brake:", "BSA"),
fiz(number_param("bre", 1.0), "Brake:", "BRE"),
fiz(number_param("nbpa", 1.0), "Brake:", "NBpA"),
fiz(choice_param("material", {"P10", "P10-Bg", "P10-Bgu", "P10yBg", "P10yBgu", "FR513", "FR510", "Cosid", "Disk1", "Disk1+Mg", "Disk2"}, "P10"), "Brake:", "BM"),
};
type.single = true;
type.create = make_block<br_rigging>;
return type;
}
private:
static int material_id(std::string const &Material)
{
// as in TMoverParameters::LoadFIZ_Brake, without the magnetic track brake
if (Material == "P10-Bg")
return bp_P10Bg;
if (Material == "P10-Bgu")
return bp_P10Bgu;
if (Material == "FR513")
return bp_FR513;
if (Material == "FR510")
return bp_FR510;
if (Material == "Cosid")
return bp_Cosid;
if (Material == "P10yBg")
return bp_P10yBg;
if (Material == "P10yBgu")
return bp_P10yBgu;
if (Material == "Disk1" || Material == "Disk1+Mg")
return bp_D1;
if (Material == "Disk2")
return bp_D2;
return bp_P10;
}
std::shared_ptr<TFricMat> m_material;
};
// spring (parking) brake
class pn_spring_brake final : public block
{
public:
struct port
{
enum : std::size_t
{
release,
feed,
force,
applied
};
};
explicit pn_spring_brake(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto &spring{body.SpringBrake};
spring.Activate = false == get_bool(Frame, port::release);
body.UpdateSpringBrake(Frame.dt);
// the force the spring adds to the shoes, as in TMoverParameters::BrakeForce
auto const force{spring.IsReady ? std::max(0.0, spring.MinForcePressure - spring.Cylinder->P()) * spring.MaxBrakeForce : 0.0};
set(Frame, port::force, force);
set_bool(Frame, port::applied, spring.IsActive);
}
static block_type describe()
{
auto type{make_type("pn.spring_brake", "brakes", "Spring brake",
"Spring (parking) brake, released with air from the feed pipe. force (kN) is meant for the spring input of the brake rigging.",
block_origin::extracted)};
type.ports = {signal_in("release", value_type::boolean), physical_in("feed", domain::air, true), signal_out("force", value_type::real, "kN"),
signal_out("applied", value_type::boolean)};
type.params = {
fiz(number_param("volume", 1.0, "l"), "SpringBrake:", "Volume"),
fiz(number_param("mbf", 0.0, "kN"), "SpringBrake:", "MBF"),
fiz(number_param("max_sp", 0.0, "bar"), "SpringBrake:", "MaxSP"),
fiz(number_param("reset_p", 0.0, "bar"), "SpringBrake:", "ResetP"),
fiz(number_param("min_fp", 0.1, "bar"), "SpringBrake:", "MinFP"),
fiz(number_param("press_off", -1.0, "bar"), "SpringBrake:", "PressOff"),
fiz(number_param("press_on", -2.0, "bar"), "SpringBrake:", "PressOn"),
fiz(number_param("valve_on_area", 0.0), "SpringBrake:", "ValveOnArea"),
fiz(number_param("valve_off_area", 0.0), "SpringBrake:", "ValveOffArea"),
fiz(number_param("valve_pnb_area", 0.0), "SpringBrake:", "ValvePNBArea"),
};
type.single = true;
type.create = make_block<pn_spring_brake>;
return type;
}
};
} // namespace
void register_pneumatic_blocks(catalog &Catalog)
{
Catalog.add(pn_compressor::describe());
Catalog.add(pn_governor::describe());
Catalog.add(pn_reservoir::describe());
Catalog.add(br_handle::describe());
Catalog.add(br_local_handle::describe());
Catalog.add(br_pipe::describe());
Catalog.add(br_distributor::describe());
Catalog.add(br_rigging::describe());
Catalog.add(pn_spring_brake::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,149 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
// vigilance device and cab signalling (CA/SHP). the block drives the TSecuritySystem of the vehicle body, configured by
// TSecuritySystem::load from the parameters of the block, so the simulator's own uses of it (cab signal magnets of the
// track, the reset commands of the cab, the AI) keep working with graph vehicles
namespace vgraph
{
namespace
{
class sec_ca final : public block
{
public:
struct port
{
enum : std::size_t
{
v,
power,
reset,
reset_shp,
active,
lamp,
buzzer,
brake,
shp_lamp
};
};
struct param
{
enum : std::size_t
{
aware,
cab_signal,
separate_ack,
sifa,
aware_delay,
sound_delay,
emergency_delay,
min_speed,
max_hold_time,
radio_stop,
cab_dependent
};
};
explicit sec_ca(block_setup const &Setup) : block(Setup), m_separate(boolean(Setup, param::separate_ack)) {}
void update(frame &Frame) override
{
auto &security{Frame.body->mover->SecuritySystem};
auto const active{get_bool(Frame, port::active)};
if (m_first || active != m_active)
{
security.set_enabled(active);
m_active = active;
m_first = false;
}
auto const reset{get_bool(Frame, port::reset)};
if (reset && false == m_reset)
{
security.acknowledge_press();
}
else if (false == reset && m_reset)
{
security.acknowledge_release();
}
m_reset = reset;
auto const resetshp{get_bool(Frame, port::reset_shp)};
if (resetshp && false == m_resetshp)
{
security.cabsignal_reset();
}
m_resetshp = resetshp;
security.update(Frame.dt, get(Frame, port::v), get_bool(Frame, port::power), Frame.body->cab);
set_bool(Frame, port::lamp, m_separate ? security.is_vigilance_blinking() : security.is_blinking());
set_bool(Frame, port::buzzer, security.is_beeping() || security.is_cabsignal_beeping());
set_bool(Frame, port::brake, security.is_braking());
set_bool(Frame, port::shp_lamp, security.is_cabsignal_blinking());
}
static block_type describe()
{
block_type type;
type.id = "sec.ca";
type.category = "security";
type.name = "Vigilance device (CA/SHP)";
type.description = "Vigilance device and cab signalling (TSecuritySystem). v in km/h; brake is meant for the vent input of the brake pipe. The cab signal is "
"set by the magnets of the track, as for the .fiz vehicles.";
type.origin = block_origin::simulator;
type.ports = {signal_in("v", value_type::real, 0.0, "km/h"),
signal_in("power", value_type::boolean),
signal_in("reset", value_type::boolean),
signal_in("reset_shp", value_type::boolean, 0.0, "", true),
signal_in("active", value_type::boolean, 1.0, "", true),
signal_out("lamp", value_type::boolean),
signal_out("buzzer", value_type::boolean),
signal_out("brake", value_type::boolean),
signal_out("shp_lamp", value_type::boolean)};
// the first four make AwareSystem; a negative min_speed leaves the default of 10% of vmax
type.params = {
boolean_param("aware", true),
boolean_param("cab_signal", false),
boolean_param("separate_ack", false),
boolean_param("sifa", false),
fiz(number_param("aware_delay", 30.0, "s"), "Security:", "AwareDelay"),
fiz(number_param("sound_delay", 5.0, "s"), "Security:", "SoundSignalDelay"),
fiz(number_param("emergency_delay", 5.0, "s"), "Security:", "EmergencyBrakeDelay"),
number_param("min_speed", -1.0, "km/h"),
fiz(number_param("max_hold_time", 1.5, "s"), "Security:", "MaxHoldTime"),
fiz(boolean_param("radio_stop", false), "Security:", "RadioStop"),
fiz(boolean_param("cab_dependent", false), "Security:", "CabDependent"),
};
type.single = true;
type.create = make_block<sec_ca>;
return type;
}
private:
bool const m_separate;
bool m_first{true};
bool m_active{false};
bool m_reset{false};
bool m_resetshp{false};
};
} // namespace
void register_security_blocks(catalog &Catalog)
{
Catalog.add(sec_ca::describe());
}
} // namespace vgraph

View File

@@ -0,0 +1,424 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_body.h"
#include "McZapkie/MOVER.h"
#include <algorithm>
#include <cmath>
// the vehicle body: its motion, doors, lights, sander and the animated parts of its model, which stay native in the simulator
namespace vgraph
{
namespace
{
TMoverParameters &vehicle(frame const &Frame)
{
return *Frame.body->mover;
}
block_type make_type(std::string Id, std::string Name, std::string Description)
{
block_type type;
type.id = std::move(Id);
type.category = "vehicle";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = block_origin::native;
return type;
}
class veh_state final : public block
{
public:
struct port
{
enum : std::size_t
{
v,
v_signed,
accel,
wheel_rpm,
slipping,
cab,
time
};
};
explicit veh_state(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto const &body{*Frame.body};
set(Frame, port::v, std::abs(body.velocity) * 3.6);
set(Frame, port::v_signed, body.velocity);
set(Frame, port::accel, body.acceleration);
set(Frame, port::wheel_rpm, std::abs(body.wheel_speed) * 60.0);
set_bool(Frame, port::slipping, body.slipping);
set(Frame, port::cab, body.cab);
set(Frame, port::time, Frame.time);
}
static block_type describe()
{
auto type{make_type("veh.state", "Vehicle state",
"Motion of the vehicle: speed (km/h), signed speed (m/s, positive towards the front, cab 1), acceleration, wheel revolutions, the occupied "
"cab (1, -1, 0) and the time of the graph.")};
type.ports = {signal_out("v", value_type::real, "km/h"), signal_out("v_signed", value_type::real, "m/s"), signal_out("accel", value_type::real, "m/s2"),
signal_out("wheel_rpm", value_type::real, "1/min"), signal_out("slipping", value_type::boolean), signal_out("cab", value_type::integer),
signal_out("time", value_type::real, "s")};
type.create = make_block<veh_state>;
return type;
}
};
// passenger doors, operated by the driver through the graph; the doors of the vehicle body move them and play their sounds
class veh_doors final : public block
{
public:
struct port
{
enum : std::size_t
{
open_left,
open_right,
lock,
closed_left,
closed_right,
all_closed
};
};
explicit veh_doors(block_setup const &Setup) : block(Setup), m_lock(is_connected(Setup, port::lock)) {}
void update(frame &Frame) override
{
auto &doors{vehicle(Frame).Doors};
operate(doors.instances[side::left], get_bool(Frame, port::open_left));
operate(doors.instances[side::right], get_bool(Frame, port::open_right));
if (m_lock)
{
doors.has_lock = true;
doors.lock_enabled = get_bool(Frame, port::lock);
}
// the state from the last update of the doors; they move after the graph
auto const closedleft{doors.instances[side::left].is_closed};
auto const closedright{doors.instances[side::right].is_closed};
set_bool(Frame, port::closed_left, closedleft);
set_bool(Frame, port::closed_right, closedright);
set_bool(Frame, port::all_closed, closedleft && closedright);
}
static block_type describe()
{
auto type{make_type("veh.doors", "Doors",
"Passenger doors of the vehicle. They open while the open input is on and close when it goes off; lock blocks them above the lock speed.")};
type.ports = {signal_in("open_left", value_type::boolean, 0.0, "", true),
signal_in("open_right", value_type::boolean, 0.0, "", true),
signal_in("lock", value_type::boolean, 0.0, "", true),
signal_out("closed_left", value_type::boolean),
signal_out("closed_right", value_type::boolean),
signal_out("all_closed", value_type::boolean)};
// open_time and close_time are turned into the speeds of the .fiz with the range
type.params = {
number_param("open_time", 2.0, "s"),
number_param("close_time", 3.0, "s"),
fiz(choice_param("type", {"shift", "rotate", "fold", "plug"}, "rotate"), "Doors:", "DoorOpenMethod"),
fiz(number_param("range", 1.0, "m/deg"), "Doors:", "DoorMaxShiftL"),
fiz(number_param("range_out", 0.0, "m"), "Doors:", "DoorMaxShiftPlug"),
fiz(number_param("open_delay", 0.0, "s"), "Doors:", "DoorOpenDelay"),
fiz(number_param("close_delay", 0.0, "s"), "Doors:", "DoorCloseDelay"),
fiz(number_param("lock_speed", 10.0, "km/h"), "Doors:", "DoorLockSpeed"),
};
type.single = true;
type.create = make_block<veh_doors>;
return type;
}
private:
template <class Door_> static void operate(Door_ &Door, bool const Open)
{
Door.remote_open = Open;
Door.remote_close = false == Open;
}
bool const m_lock;
};
class veh_lights final : public block
{
public:
struct port
{
enum : std::size_t
{
front,
rear,
dim
};
};
explicit veh_lights(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
body.iLights[end::front] = static_cast<int>(std::lround(get(Frame, port::front)));
body.iLights[end::rear] = static_cast<int>(std::lround(get(Frame, port::rear)));
Frame.body->headlights_dimmed = get_bool(Frame, port::dim);
}
static block_type describe()
{
auto type{make_type("veh.lights", "Head and tail lights",
"Lights at the front and the rear end of the vehicle, as bit masks of the lights (1 left, 4 upper, 16 right white; 2 left, 32 right red...).")};
type.ports = {signal_in("front", value_type::integer, 0.0, "", true), signal_in("rear", value_type::integer, 0.0, "", true), signal_in("dim", value_type::boolean, 0.0, "", true)};
type.single = true;
type.create = make_block<veh_lights>;
return type;
}
};
class veh_sander final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
sanding
};
};
explicit veh_sander(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
auto &body{vehicle(Frame)};
auto const on{get_bool(Frame, port::on)};
if (on != m_on)
{
body.Sandbox(on, range_t::local);
m_on = on;
}
set_bool(Frame, port::sanding, body.SandDose);
}
static block_type describe()
{
auto type{make_type("veh.sander", "Sander", "Sands the rails while on, until the sand (body sand_capacity) runs out.")};
type.ports = {signal_in("on", value_type::boolean), signal_out("sanding", value_type::boolean)};
type.single = true;
type.create = make_block<veh_sander>;
return type;
}
private:
bool m_on{false};
};
// placement of the animated submodels of the vehicle's external model: the model of the vehicle body or its low poly
// interior. the vehicle (DynObj) reads these parameters by their keys and applies the block's state to the submodels
// right before the vehicle is drawn, so keep the keys and the choice ids
param_def model_param()
{
return choice_param("model", {"vehicle", "lowpoly"}, "vehicle");
}
// submodel of the external model rotated or moved by the value, like the TGauge of a cab control: the input is clamped
// to min..max, followed with the smoothing of TGauge (its friction), and gives offset + scale * value degrees
// (rotation about the submodel's y axis) or millimetres (shift along its z axis)
class veh_anim final : public block
{
public:
struct port
{
enum : std::size_t
{
value,
anim
};
};
struct param
{
enum : std::size_t
{
model,
submodel,
animation,
min,
max,
scale,
offset,
smoothing
};
};
explicit veh_anim(block_setup const &Setup)
: block(Setup), m_min(std::min(number(Setup, param::min), number(Setup, param::max))), m_max(std::max(number(Setup, param::min), number(Setup, param::max))),
m_scale(number(Setup, param::scale)), m_offset(number(Setup, param::offset)), m_smoothing(std::max(0.0, number(Setup, param::smoothing)))
{
}
void update(frame &Frame) override
{
auto const target{std::clamp(get(Frame, port::value), m_min, m_max)};
if (m_initialized && m_smoothing > 0.0 && Frame.dt < 0.5 * m_smoothing)
{
// the same as TGauge::Update
m_value += Frame.dt * (target - m_value) / m_smoothing;
if (std::abs(target - m_value) <= 0.0001)
{
m_value = target;
}
}
else
{
// the first step starts at the input, so the part doesn't move when the vehicle appears
m_value = target;
m_initialized = true;
}
set(Frame, port::anim, animation());
}
std::size_t state_count() const override
{
return 2;
}
char const *state_name(std::size_t Index) const override
{
return Index == 0 ? "value" : "anim";
}
double state_value(std::size_t Index) const override
{
return Index == 0 ? m_value : animation();
}
static block_type describe()
{
auto type{make_type("veh.anim", "Vehicle model animation",
"Rotates or moves a submodel of the vehicle's external model (or its low poly interior) by the value, like a cab control: offset + scale * value, "
"in degrees or millimetres, smoothed. Each vehicle shows its own state, although the model is shared.")};
type.ports = {signal_in("value", value_type::real), signal_out("anim", value_type::real)};
type.params = {
model_param(),
text_param("submodel"),
choice_param("animation", {"rotate", "move"}, "rotate"),
number_param("min", 0.0),
number_param("max", 1.0),
number_param("scale", 90.0, "deg/mm"),
number_param("offset", 0.0, "deg/mm"),
number_param("smoothing", 0.5, "s"),
};
type.create = make_block<veh_anim>;
return type;
}
private:
double animation() const
{
return m_offset + m_scale * m_value;
}
double const m_min;
double const m_max;
double const m_scale;
double const m_offset;
double const m_smoothing;
double m_value{0.0};
bool m_initialized{false};
};
// submodel of the external model shown while the input is on, like the TButton of a lamp: submodel visible when on,
// the optional submodel_off visible when off
class veh_visible final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
shown
};
};
struct param
{
enum : std::size_t
{
model,
submodel,
submodel_off,
blink,
blink_period
};
};
explicit veh_visible(block_setup const &Setup) : block(Setup), m_blink(boolean(Setup, param::blink)), m_blinkperiod(std::max(0.01, number(Setup, param::blink_period))) {}
void update(frame &Frame) override
{
auto on{get_bool(Frame, port::on)};
if (on && m_blink)
{
// in step with the lamps of the cab, which blink with the time of the graph too
on = std::fmod(Frame.time, m_blinkperiod) < 0.5 * m_blinkperiod;
}
m_shown = on;
set_bool(Frame, port::shown, m_shown);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "on";
}
double state_value(std::size_t) const override
{
return m_shown ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("veh.visible", "Vehicle model element visibility",
"Shows a submodel of the vehicle's external model (or its low poly interior) while the input is on, and the optional off submodel while "
"it's off, e.g. a lamp, a sign or a gadget. Each vehicle shows its own state, although the model is shared.")};
type.ports = {signal_in("on", value_type::boolean), signal_out("shown", value_type::boolean)};
type.params = {
model_param(), text_param("submodel"), text_param("submodel_off"), boolean_param("blink", false), number_param("blink_period", 1.0, "s"),
};
type.create = make_block<veh_visible>;
return type;
}
private:
bool const m_blink;
double const m_blinkperiod;
bool m_shown{false};
};
} // namespace
void register_vehicle_blocks(catalog &Catalog)
{
Catalog.add(veh_state::describe());
Catalog.add(veh_doors::describe());
Catalog.add(veh_lights::describe());
Catalog.add(veh_sander::describe());
Catalog.add(veh_anim::describe());
Catalog.add(veh_visible::describe());
}
} // namespace vgraph