17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 22:39:19 +02:00
Files
maszyna/vehicle/graph/graph_standalone.cpp

537 lines
16 KiB
C++

/*
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_standalone.h"
#include "vehicle/graph/graph_catalog.h"
#include "vehicle/graph/graph_instance.h"
#include "vehicle/graph/graph_lua.h"
#include "vehicle/graph/blocks/cab.h"
#include "McZapkie/MOVER.h"
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
// command line tools of the vehicle graph. they run without the rest of the simulator; the log service doesn't run
// in this mode, so the results are reported directly to the console
namespace vgraph
{
namespace
{
struct script_event
{
double time{0.0};
std::string operation;
std::string target;
double value{0.0};
int line{0};
std::size_t vehicle{0}; // index of the vehicle in the consist; the target was prefixed "k:" for the vehicle k
};
bool load_script(std::string const &Path, std::vector<script_event> &Events)
{
std::ifstream file{Path};
if (false == file.is_open())
{
std::printf("can't open script file \"%s\"\n", Path.c_str());
return false;
}
std::string line;
int linenumber{0};
while (std::getline(file, line))
{
++linenumber;
auto const comment{line.find('#')};
if (comment != std::string::npos)
{
line.erase(comment);
}
std::istringstream stream{line};
script_event event;
event.line = linenumber;
if (false == static_cast<bool>(stream >> event.time))
{
// empty line
continue;
}
if (false == static_cast<bool>(stream >> event.operation >> event.target))
{
std::printf("%s:%d: expected: <time> <operation> <target> [value]\n", Path.c_str(), linenumber);
return false;
}
auto const needsvalue{event.operation == "set" || event.operation == "value" || event.operation == "force"};
if (needsvalue && false == static_cast<bool>(stream >> event.value))
{
std::printf("%s:%d: operation \"%s\" needs a value\n", Path.c_str(), linenumber, event.operation.c_str());
return false;
}
Events.push_back(event);
}
std::stable_sort(std::begin(Events), std::end(Events), [](script_event const &Left, script_event const &Right) { return Left.time < Right.time; });
return true;
}
bool apply_event(graph_instance &Instance, script_event const &Event)
{
if (Event.operation == "force" || Event.operation == "unforce")
{
auto const slot{Instance.definition().find_signal(Event.target)};
if (slot < 0)
{
std::printf("script line %d: unknown signal port \"%s\"\n", Event.line, Event.target.c_str());
return false;
}
if (Event.operation == "force")
{
Instance.force(slot, Event.value);
}
else
{
Instance.release(slot);
}
return true;
}
auto *control{dynamic_cast<cab_switch *>(Instance.find_block(Event.target))};
if (control == nullptr)
{
std::printf("script line %d: \"%s\" isn't a cab switch\n", Event.line, Event.target.c_str());
return false;
}
if (Event.operation == "press")
{
control->press();
}
else if (Event.operation == "release")
{
control->release();
}
else if (Event.operation == "toggle")
{
control->toggle();
}
else if (Event.operation == "inc")
{
control->increase();
}
else if (Event.operation == "dec")
{
control->decrease();
}
else if (Event.operation == "set")
{
control->set_position(static_cast<int>(std::lround(Event.value)));
}
else if (Event.operation == "value")
{
control->set_value(Event.value);
}
else
{
std::printf("script line %d: unknown operation \"%s\"\n", Event.line, Event.operation.c_str());
return false;
}
return true;
}
// motion of the vehicle without the track: the forces of the graph, the motion resistance and the mass of the body
// (TMoverParameters::ComputeTotalForce and ComputeMovement, much simplified)
void move_body(TMoverParameters &Body, double const Deltatime)
{
auto const &state{Body.Graph->body()};
if (Body.TotalMass <= 0.0)
{
return;
}
auto const tractive{state.tractive_force};
auto const resistance{state.brake_force + Body.FrictionForce()};
double force;
if (Body.V == 0.0)
{
// standing still until the tractive force overcomes the resistance
force = std::abs(tractive) > resistance ? tractive - (tractive > 0.0 ? resistance : -resistance) : 0.0;
}
else
{
force = tractive - (Body.V > 0.0 ? resistance : -resistance);
}
auto const previous{Body.V};
Body.AccS = force / Body.TotalMass;
Body.V += Body.AccS * Deltatime;
if (previous != 0.0 && Body.V * previous <= 0.0)
{
// stopped by the brakes and the resistance
Body.V = 0.0;
}
Body.Vel = std::abs(Body.V) * 3.6;
Body.nrot = Body.WheelDiameter > 0.0 ? Body.V / (M_PI * Body.WheelDiameter) : 0.0;
Body.DistCounter += std::abs(Body.V) * Deltatime / 1000.0;
Body.FTrain = tractive;
Body.Fb = state.brake_force;
}
std::string format_number(double const Value)
{
char buffer[32];
// negative zero is written as zero, so equal results give equal files
std::snprintf(buffer, sizeof(buffer), "%.9g", Value == 0.0 ? 0.0 : Value);
return buffer;
}
// vehicle run by -graph-run: the first one, and the ones coupled behind it (-consist)
struct run_vehicle
{
std::string path;
bool reversed{false}; // coupled by its rear coupler, facing the other way than the first vehicle
std::shared_ptr<graph_template const> graph;
std::unique_ptr<TMoverParameters> body;
std::vector<std::pair<std::string, int>> signals;
bool hasbody{false};
bool mains{false}; // the graph has several line breakers, so the Mains of the body is written as well
std::string prefix; // of the result columns: empty for the first vehicle, "k:" for the vehicle k
};
// path of a vehicle of the consist: as given, or relative to the directory of the first vehicle's file
std::string consist_path(std::string const &Path, std::string const &First)
{
if (std::ifstream{Path}.is_open())
{
return Path;
}
auto const relative{std::filesystem::path{First}.parent_path() / Path};
if (std::filesystem::path{Path}.is_relative() && std::ifstream{relative}.is_open())
{
return relative.string();
}
return Path;
}
bool load_vehicle(run_vehicle &Vehicle)
{
std::vector<diagnostic> diagnostics;
Vehicle.graph = load_graph(Vehicle.path, diagnostics);
for (auto const &entry : diagnostics)
{
std::printf("%s: %s\n", Vehicle.path.c_str(), to_string(entry).c_str());
}
if (Vehicle.graph == nullptr)
{
return false;
}
auto const &graph{*Vehicle.graph};
std::printf("%s: %zu blocks, %zu signals, %zu links\n", Vehicle.path.c_str(), graph.nodes().size(), graph.signal_defaults().size(), graph.link_count());
return true;
}
// the body of the vehicle, filled from the graph as in the simulator, with the body entries the graph doesn't give
// taken from the .fiz file of the same name, if there is one
bool create_body(run_vehicle &Vehicle, std::string const &Name, bool const Ready, int const Cab)
{
auto const &input{Vehicle.path};
auto const &graph{Vehicle.graph};
Vehicle.body = std::make_unique<TMoverParameters>(0.0, "graph", Name, 1);
auto &body{*Vehicle.body};
auto fizfile{input};
fizfile = fizfile.substr(0, std::min(fizfile.size(), fizfile.find_last_of('.'))) + ".fiz";
if (false == body.AssignGraph(graph, fizfile))
{
std::printf("%s: the body section or the block parameters were rejected by the vehicle body\n", input.c_str());
return false;
}
auto const vehicleblocks{std::any_of(std::begin(graph->nodes()), std::end(graph->nodes()), [](auto const &Node) {
auto const &category{Node.type->category};
return category != "logic" && category != "cab" && category != "indicators" && category != "script" && category != "module";
})};
if (vehicleblocks && (body.Dim.L <= 0.0 || body.Mass <= 0.0))
{
// the simulator doesn't load such a vehicle; the graph itself can still be run here
std::printf("%s: warning: the body has no length or mass (body section, or %s), the simulator won't load the vehicle\n", input.c_str(), fizfile.c_str());
}
body.ComputeMass();
body.CheckLocomotiveParameters(Ready, 1);
body.CabActive = Cab;
body.Graph->body().standalone = true;
Vehicle.hasbody = (false == graph->body().empty());
Vehicle.mains = std::count_if(std::begin(graph->nodes()), std::end(graph->nodes()), [](auto const &Node) { return Node.type->id == "el.hsb"; }) > 1;
Vehicle.signals = graph->signal_outputs();
return true;
}
void write_header(std::ostringstream &Results, run_vehicle const &Vehicle)
{
auto const &graph{*Vehicle.graph};
auto const &instance{*Vehicle.body->Graph};
auto const &prefix{Vehicle.prefix};
for (auto const &signal : Vehicle.signals)
{
Results << ';' << prefix << signal.first;
}
for (std::size_t nodeindex = 0; nodeindex < graph.nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
Results << ';' << prefix << graph.nodes()[nodeindex].id << ':' << item.state_name(stateindex);
}
}
if (Vehicle.hasbody)
{
// the vehicle body, for graphs describing whole vehicles
Results << ';' << prefix << "body:v;" << prefix << "body:ft;" << prefix << "body:fb;" << prefix << "body:pg;" << prefix << "body:zg;" << prefix << "body:bc";
}
if (Vehicle.mains)
{
Results << ';' << prefix << "body:mains";
}
}
void write_values(std::ostringstream &Results, run_vehicle const &Vehicle)
{
auto const &graph{*Vehicle.graph};
auto const &body{*Vehicle.body};
auto const &instance{*body.Graph};
for (auto const &signal : Vehicle.signals)
{
Results << ';' << format_number(instance.signal(signal.second));
}
for (std::size_t nodeindex = 0; nodeindex < graph.nodes().size(); ++nodeindex)
{
auto const &item{instance.node_block(nodeindex)};
for (std::size_t stateindex = 0; stateindex < item.state_count(); ++stateindex)
{
Results << ';' << format_number(item.state_value(stateindex));
}
}
if (Vehicle.hasbody)
{
Results << ';' << format_number(body.Vel) << ';' << format_number(body.FTrain / 1000.0) << ';' << format_number(body.Fb / 1000.0) << ';' << format_number(body.PipePress)
<< ';' << format_number(body.Compressor) << ';' << format_number(body.BrakePress);
}
if (Vehicle.mains)
{
Results << ';' << (body.Mains ? 1 : 0);
}
}
} // namespace
int graph_catalog_standalone(std::string const &Output)
{
auto const text{catalog::get().export_json()};
if (Output.empty())
{
std::printf("%s\n", text.c_str());
return 0;
}
std::ofstream file{Output, std::ios::binary};
if (false == file.is_open())
{
std::printf("can't create file \"%s\"\n", Output.c_str());
return 1;
}
file << text << '\n';
std::printf("block catalog written to %s (%zu block types)\n", Output.c_str(), catalog::get().types().size());
return 0;
}
int graph_run_standalone(int const Argc, char *Argv[])
{
// -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step>] [-t <duration>] [-every <steps>] [-state braked|ready] [-consist <file.vhg>[:r]]...
if (Argc < 3)
{
std::printf("usage: -graph-run <file.vhg> [-script <events.txt>] [-o <results.csv>] [-dt <step, s>] [-t <duration, s>] [-every <steps>] [-state braked|ready] "
"[-consist <file.vhg>[:r]]...\n");
return 1;
}
std::string const input{Argv[2]};
std::vector<run_vehicle> vehicles(1);
vehicles.front().path = input;
std::string script;
std::string output;
double deltatime{0.01};
double duration{0.0};
int every{1};
bool ready{false};
for (int idx = 3; idx + 1 < Argc; idx += 2)
{
std::string const option{Argv[idx]};
std::string const value{Argv[idx + 1]};
if (option == "-script")
{
script = value;
}
else if (option == "-o")
{
output = value;
}
else if (option == "-dt")
{
deltatime = std::atof(value.c_str());
}
else if (option == "-t")
{
duration = std::atof(value.c_str());
}
else if (option == "-every")
{
every = std::max(1, std::atoi(value.c_str()));
}
else if (option == "-state")
{
// initial state of the vehicle body, as for a vehicle placed in a scenario standing (braked) or moving (ready)
ready = (value == "ready");
}
else if (option == "-consist")
{
// next vehicle of the consist, coupled behind the previous one by its front coupler, or with ":r" reversed (by its rear coupler)
run_vehicle vehicle;
vehicle.reversed = (value.size() > 2 && value.ends_with(":r"));
vehicle.path = consist_path(vehicle.reversed ? value.substr(0, value.size() - 2) : value, input);
vehicle.prefix = std::to_string(vehicles.size() + 1) + ":";
vehicles.push_back(std::move(vehicle));
}
else
{
std::printf("unknown option \"%s\"\n", option.c_str());
return 1;
}
}
if (deltatime <= 0.0)
{
std::printf("the step has to be positive\n");
return 1;
}
// messages of the lua blocks go straight to the console
set_log_sink([](std::string const &Message) { std::printf("%s\n", Message.c_str()); });
for (auto &vehicle : vehicles)
{
if (false == load_vehicle(vehicle))
{
return 1;
}
}
std::vector<script_event> events;
if (false == script.empty() && false == load_script(script, events))
{
return 1;
}
for (auto &event : events)
{
// "k:" before the target picks the vehicle k of the consist
auto const separator{event.target.find(':')};
if (separator == std::string::npos || separator == 0 || event.target.find_first_not_of("0123456789") != separator)
{
continue;
}
auto const number{std::atoi(event.target.substr(0, separator).c_str())};
if (number < 1 || number > static_cast<int>(vehicles.size()))
{
std::printf("script line %d: there's no vehicle %d in the consist\n", event.line, number);
return 1;
}
event.vehicle = static_cast<std::size_t>(number - 1);
event.target.erase(0, separator + 1);
}
if (duration <= 0.0 && false == events.empty())
{
// by default run a second past the last event
duration = events.back().time + 1.0;
}
if (duration <= 0.0)
{
return 0;
}
for (std::size_t idx = 0; idx < vehicles.size(); ++idx)
{
// the first vehicle has the occupied cab
if (false == create_body(vehicles[idx], idx == 0 ? "graph" : "graph" + std::to_string(idx + 1), ready, idx == 0 ? 1 : 0))
{
return 1;
}
}
// the consist: every vehicle coupled to the previous one with all the couplings, so the brake pipes connect as well
auto const allcouplings{coupling::coupler | coupling::brakehose | coupling::control | coupling::highvoltage | coupling::gangway | coupling::mainhose | coupling::heating |
coupling::permanent | coupling::power24v | coupling::power110v | coupling::power3x400v};
for (std::size_t idx = 1; idx < vehicles.size(); ++idx)
{
auto &previous{vehicles[idx - 1]};
auto &vehicle{vehicles[idx]};
previous.body->Attach(previous.reversed ? end::front : end::rear, vehicle.reversed ? end::rear : end::front, vehicle.body.get(), allcouplings, true, false);
}
std::ostringstream results;
results << "time";
for (auto const &vehicle : vehicles)
{
write_header(results, vehicle);
}
results << '\n';
auto const stepcount{static_cast<long long>(std::ceil(duration / deltatime - 1e-9))};
std::size_t nextevent{0};
for (long long step = 0; step < stepcount; ++step)
{
// events take effect at the beginning of the first step which starts at or after their time
auto const steptime{step * deltatime};
while (nextevent < events.size() && events[nextevent].time <= steptime + 1e-9)
{
auto const &event{events[nextevent]};
if (false == apply_event(*vehicles[event.vehicle].body->Graph, event))
{
return 1;
}
++nextevent;
}
// the vehicles in the order of the consist; each one moves on its own, without the forces of the couplers
for (auto &vehicle : vehicles)
{
auto &body{*vehicle.body};
body.EqvtPipePress = body.PipePress;
body.compute_movement_graph_(deltatime);
move_body(body, deltatime);
}
if ((step + 1) % every != 0 && step + 1 != stepcount)
{
continue;
}
results << format_number((step + 1) * deltatime);
for (auto const &vehicle : vehicles)
{
write_values(results, vehicle);
}
results << '\n';
}
if (output.empty())
{
std::printf("%s", results.str().c_str());
return 0;
}
std::ofstream file{output, std::ios::binary};
if (false == file.is_open())
{
std::printf("can't create file \"%s\"\n", output.c_str());
return 1;
}
file << results.str();
std::printf("%lld steps written to %s\n", stepcount, output.c_str());
return 0;
}
} // namespace vgraph