diff --git a/apps/sa-ews0/CMakeLists.txt b/apps/sa-ews0/CMakeLists.txt new file mode 100644 index 0000000..8d01082 --- /dev/null +++ b/apps/sa-ews0/CMakeLists.txt @@ -0,0 +1,4 @@ +ROSA_add_app(sa-ews0 sa-ews0.cpp) +ROSA_add_library_dependencies(sa-ews0 ROSAConfig) +ROSA_add_library_dependencies(sa-ews0 ROSAApp) +ROSA_add_library_dependencies(sa-ews0 ROSAAgent) diff --git a/apps/sa-ews0/sa-ews0.cpp b/apps/sa-ews0/sa-ews0.cpp new file mode 100644 index 0000000..3cfcb30 --- /dev/null +++ b/apps/sa-ews0/sa-ews0.cpp @@ -0,0 +1,387 @@ +//===-- apps/sa-ews0/sa-ews0.cpp --------------------------------*- C++ -*-===// +// +// The RoSA Framework -- Application SA-EWS0 +// +// Distributed under the terms and conditions of the Boost Software License 1.0. +// See accompanying file LICENSE. +// +// If you did not receive a copy of the license file, see +// http://www.boost.org/LICENSE_1_0.txt. +// +//===----------------------------------------------------------------------===// +/// +/// \file apps/sa-ews0/sa-ews0.cpp +/// +/// \author David Juhasz (david.juhasz@tuwien.ac.at) +/// +/// \date 2017-2020 +/// +/// \brief The application SA-EWS0 implements the conventional Early Warning +/// Score (EWS) system. +//===----------------------------------------------------------------------===// + +#include "rosa/agent/Abstraction.hpp" + +#include "rosa/config/version.h" + +#include "rosa/app/Application.hpp" + +#include "rosa/support/csv/CSVReader.hpp" +#include "rosa/support/csv/CSVWriter.hpp" +#include "rosa/support/iterator/split_tuple_iterator.hpp" + +#include "cxxopts/cxxopts.hpp" + +#include +#include + +using namespace rosa; +using namespace rosa::agent; +using namespace rosa::app; +using namespace rosa::terminal; +using namespace rosa::csv; +using namespace rosa::iterator; + +const std::string AppName = "SA-EWS0"; + +/// Representation type of warning levels. +/// \note Make sure it suits all defined enumeration values. +using WarningScoreType = uint8_t; + +/// Warning levels for abstraction. +enum WarningScore : WarningScoreType { + No = 0, + Low = 1, + High = 2, + Emergency = 3 +}; + +/// Helper function creating an application agent for pre-processing sensory +/// values. +/// +/// Received values are abstracted into a \c WarningScore value, which is the +/// result of the processing function. +/// +/// \note The result, \c WarningScore, is returned as \c WarningScoreType +/// because enumeration types are not integrated into built-in types. Hence, a +/// master to these agents receives its input as \c WarningScoreType values, and +/// may cast them to \c WarningScore explicitly. +/// +/// \tparam T type of values to receive from the sensor +/// +/// \param App the application to create the agent in +/// \param Name name of the new agent +/// \param A abstraction to use +/// +/// \return handle for the new agent +template +AgentHandle createLowLevelAgent(std::unique_ptr &App, + const std::string &Name, + const Abstraction &A) { + using result = Optional; + using handler = std::function)>; + return App->createAgent( + Name, handler([&, Name](std::pair I) -> result { + LOG_INFO_STREAM << "\n******\n" + << Name << " " << (I.second ? "" : "") + << " value: " << I.first << "\n******\n"; + return {A(I.first)}; + })); +} + +/// Helper function to print an error message in red color to the terminal and +/// exit from the application. +/// +/// \note The function never returns as it calles `exit()`. +/// +/// \param Error error message +/// \param ExitCode exit code to return from the application +void logErrorAndExit(const std::string &Error, const int ExitCode) { + LOG_ERROR_STREAM << Color::Red << Error << Color::Default << std::endl; + exit(ExitCode); +} + +int main(int argc, char *argv[]) { + LOG_INFO_STREAM << '\n' + << library_string() << " -- " << Color::Red << AppName + << " app" << Color::Default << '\n'; + + /// Paths for the CSV files for simulation. + /// + ///@{ + std::string DataCSVPath; + std::string ScoreCSVPath; + ///@} + + /// Whether CSV files have header row at the beginning. + bool CSVHeader = false; + + /// Delimiter character in CSV files. + char CSVDelimiter = ','; + + /// How many cycles of simulation to perform. + size_t NumberOfSimulationCycles = 16; + + // Handle command-line arguments. + try { + cxxopts::Options Options(argv[0], library_string() + " -- " + AppName); + Options.add_options()("i,input", + "Path for the CSV file providing input data", + cxxopts::value(DataCSVPath), "file") + ("o,output", + "Path for the CSV file to write output scores", + cxxopts::value(ScoreCSVPath), "file") + ("header", "CSV input file has header row", + cxxopts::value(CSVHeader)->default_value("false")) + ("delimiter", "CSV delimiter character", + cxxopts::value(CSVDelimiter)->default_value(","), "char") + ("c,cycles", "Number of simulation cycles to perform", + cxxopts::value(NumberOfSimulationCycles)->default_value("16"), "int") + ("h,help", "Print usage"); + + auto Args = Options.parse(argc, argv); + + if (Args.count("help")) { + LOG_INFO_STREAM << '\n' << Options.help() << std::endl; + exit(0); + } + + if (Args.count("input") == 0) { + throw std::invalid_argument("Argument --input must be defined."); + } + if (Args.count("output") == 0) { + throw std::invalid_argument("Argument --output must be defined."); + } + } catch (const cxxopts::OptionException &e) { + logErrorAndExit(e.what(), 1); + } catch (const std::invalid_argument &e) { + logErrorAndExit(e.what(), 1); + } + + std::unique_ptr App = Application::create(AppName); + + // + // Relevant types and definitions. + // + + using HRType = int32_t; + using BRType = int32_t; + using SpO2Type = int32_t; + using BPSysType = int32_t; + using BodyTempType = float; + + // + // Create deluxe sensors. + // + LOG_INFO("Creating sensors."); + + // All sensors are created without defining a normal generator function, but + // with the default value of the second argument. That, however, requires the + // data type to be explicitly defined. This is good for simulation only. + AgentHandle HRSensor = App->createSensor("HR Sensor"); + AgentHandle BRSensor = App->createSensor("BR Sensor"); + AgentHandle SpO2Sensor = App->createSensor("SpO2 Sensor"); + AgentHandle BPSysSensor = App->createSensor("BPSys Sensor"); + AgentHandle BodyTempSensor = + App->createSensor("BodyTemp Sensor"); + + // + // Create functionalities. + // + LOG_INFO("Creating Functionalities for Agents."); + + // + // Define abstractions. + // + + RangeAbstraction HRAbstraction( + {{{0, 40}, Emergency}, + {{40, 51}, High}, + {{51, 60}, Low}, + {{60, 100}, No}, + {{100, 110}, Low}, + {{110, 129}, High}, + {{129, 200}, Emergency}}, + Emergency); + + RangeAbstraction BRAbstraction({{{0, 9}, High}, + {{9, 14}, No}, + {{14, 20}, Low}, + {{20, 29}, High}, + {{29, 50}, Emergency}}, + Emergency); + + RangeAbstraction SpO2Abstraction( + {{{1, 85}, Emergency}, + {{85, 90}, High}, + {{90, 95}, Low}, + {{95, 100}, No}}, + Emergency); + + RangeAbstraction BPSysAbstraction( + {{{0, 70}, Emergency}, + {{70, 81}, High}, + {{81, 101}, Low}, + {{101, 149}, No}, + {{149, 169}, Low}, + {{169, 179}, High}, + {{179, 200}, Emergency}}, + Emergency); + + RangeAbstraction BodyTempAbstraction( + {{{0.f, 28.f}, Emergency}, + {{28.f, 32.f}, High}, + {{32.f, 35.f}, Low}, + {{35.f, 38.f}, No}, + {{38.f, 39.5f}, High}, + {{39.5f, 100.f}, Emergency}}, + Emergency); + + // + // Create low-level deluxe agents with \c createLowLevelAgent. + // + LOG_INFO("Creating low-level agents."); + + AgentHandle HRAgent = createLowLevelAgent(App, "HR Agent", HRAbstraction); + AgentHandle BRAgent = createLowLevelAgent(App, "BR Agent", BRAbstraction); + AgentHandle SpO2Agent = + createLowLevelAgent(App, "SpO2 Agent", SpO2Abstraction); + AgentHandle BPSysAgent = + createLowLevelAgent(App, "BPSys Agent", BPSysAbstraction); + AgentHandle BodyTempAgent = + createLowLevelAgent(App, "BodyTemp Agent", BodyTempAbstraction); + + // + // Connect sensors to low-level agents. + // + LOG_INFO("Connect sensors to their corresponding low-level agents."); + + App->connectSensor(HRAgent, 0, HRSensor, "HR Sensor Channel"); + App->connectSensor(BRAgent, 0, BRSensor, "BR Sensor Channel"); + App->connectSensor(SpO2Agent, 0, SpO2Sensor, "SpO2 Sensor Channel"); + App->connectSensor(BPSysAgent, 0, BPSysSensor, "BPSys Sensor Channel"); + App->connectSensor(BodyTempAgent, 0, BodyTempSensor, + "BodyTemp Sensor Channel"); + + // + // Create a high-level deluxe agent. + // + LOG_INFO("Create high-level agent."); + + // The new agent logs its input values and results in the the sum of them. + AgentHandle BodyAgent = App->createAgent( + "Body Agent", + std::function( + std::pair, std::pair, + std::pair, std::pair, + std::pair)>( + [](std::pair HR, + std::pair BR, + std::pair SpO2, + std::pair BPSys, + std::pair BodyTemp) + -> Optional { + LOG_INFO_STREAM << "\n*******\nBody Agent trigged with values:\n" + << (HR.second ? "" : "") + << " HR warning score: " << HR.first << "\n" + << (BR.second ? "" : "") + << " BR warning score: " << BR.first << "\n" + << (SpO2.second ? "" : "") + << " SpO2 warning score: " << SpO2.first << "\n" + << (BPSys.second ? "" : "") + << " BPSys warning score: " << BPSys.first << "\n" + << (BodyTemp.second ? "" : "") + << " BodyTemp warning score: " << BodyTemp.first + << "\n******\n"; + const std::array Values{ + HR.first, BR.first, SpO2.first, BPSys.first, BodyTemp.first}; + const WarningScoreType ews = + std::reduce(Values.begin(), Values.end(), (WarningScoreType)0); + return {ews}; + })); + + // + // Connect low-level agents to the high-level agent. + // + LOG_INFO("Connect low-level agents to the high-level agent."); + + App->connectAgents(BodyAgent, 0, HRAgent, "HR Agent Channel"); + App->connectAgents(BodyAgent, 1, BRAgent, "BR Agent Channel"); + App->connectAgents(BodyAgent, 2, SpO2Agent, "SpO2 Agent Channel"); + App->connectAgents(BodyAgent, 3, BPSysAgent, "BPSys Agent Channel"); + App->connectAgents(BodyAgent, 4, BodyTempAgent, "BodyTemp Agent Channel"); + + // + // For simulation output, create a logger agent writing the output of the + // high-level agent into a CSV file. + // + LOG_INFO("Create a logger agent."); + + // Create CSV writer. + std::ofstream ScoreCSV(ScoreCSVPath); + csv::CSVWriter ScoreWriter(ScoreCSV, CSVDelimiter); + + // The agent writes each new input value into a CSV file and produces nothing. + AgentHandle LoggerAgent = App->createAgent( + "Logger Agent", + std::function(std::pair)>( + [&ScoreWriter]( + std::pair Score) -> Optional { + if (Score.second) { + // The state of \p ScoreWriter is not checked, expecting good. + ScoreWriter << Score.first; + } + return {}; + })); + + // + // Connect the high-level agent to the logger agent. + // + LOG_INFO("Connect the high-level agent to the logger agent."); + + App->connectAgents(LoggerAgent, 0, BodyAgent, "Body Agent Channel"); + + // + // Do simulation. + // + LOG_INFO("Setting up and performing simulation."); + + // + // Initialize deluxe context for simulation. + // + + App->initializeSimulation(); + + // + // Open CSV files and register them for their corresponding sensors. + // + + // Type aliases for iterators. + // Type aliases for iterators. + using CSVDataIterator = + CSVIterator; + const auto CSVHeaderInfo = + CSVHeader ? HeaderInformation::HasHeader : HeaderInformation::HasNoHeader; + std::ifstream DataCSV(DataCSVPath); + auto [HRRange, BRRange, SpO2Range, BPSysRange, BodyTempRange] = + splitTupleIterator( + CSVDataIterator(DataCSV, 0, CSVHeaderInfo, CSVDelimiter), + CSVDataIterator()); + + App->registerSensorValues(HRSensor, std::move(begin(HRRange)), end(HRRange)); + App->registerSensorValues(BRSensor, std::move(begin(BRRange)), end(BRRange)); + App->registerSensorValues(SpO2Sensor, std::move(begin(SpO2Range)), + end(SpO2Range)); + App->registerSensorValues(BPSysSensor, std::move(begin(BPSysRange)), + end(BPSysRange)); + App->registerSensorValues(BodyTempSensor, std::move(begin(BodyTempRange)), + end(BodyTempRange)); + + // + // Simulate. + // + + App->simulate(NumberOfSimulationCycles); + + return 0; +} diff --git a/docs/CommandGuide/sa_ews0.rst b/docs/CommandGuide/sa_ews0.rst new file mode 100644 index 0000000..ab1b8dc --- /dev/null +++ b/docs/CommandGuide/sa_ews0.rst @@ -0,0 +1,10 @@ +SA-EWS0 - Conventional Early Warning Score (EWS) System +======================================================= + +The application implements a conventional EWS system. + +The application takes its input from a multi-column CSV file and generates its output to a multi-column CSV file. +Input and output files are to be defined as command line arguments. +A full list of available arguments is printed to the `INFO` stream when `--help` is used. + +Sample data can be used from the SA-EWS1 application.