Skip to content

Commit add315f

Browse files
HenrZureneSchm
andauthored
1246 Export TimeSeries into csv file (#1255)
- New function to easily write the data from a TimeSeries into a csv file - This function uses mainly the already existing print_table function Co-authored-by: reneSchm <49305466+reneSchm@users.noreply.github.com>
1 parent 3252cf5 commit add315f

File tree

6 files changed

+258
-38
lines changed

6 files changed

+258
-38
lines changed

cpp/examples/abm_minimal.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ int main()
113113
auto test_parameters = model.parameters.get<mio::abm::TestData>()[test_type];
114114
auto testing_criteria_work = mio::abm::TestingCriteria();
115115
auto testing_scheme_work = mio::abm::TestingScheme(testing_criteria_work, validity_period, start_date, end_date,
116-
test_parameters, probability);
116+
test_parameters, probability);
117117
model.get_testing_strategy().add_testing_scheme(mio::abm::LocationType::Work, testing_scheme_work);
118118

119119
// Assign infection state to each person.
@@ -169,7 +169,7 @@ int main()
169169
// I_Crit = InfectedCritical, R = Recovered, D = Dead
170170
std::ofstream outfile("abm_minimal.txt");
171171
std::get<0>(historyTimeSeries.get_log())
172-
.print_table({"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4, outfile);
172+
.print_table(outfile, {"S", "E", "I_NS", "I_Sy", "I_Sev", "I_Crit", "R", "D"}, 7, 4);
173173
std::cout << "Results written to abm_minimal.txt" << std::endl;
174174

175175
return 0;

cpp/examples/ode_seair.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ int main()
8989
const std::string file_name = "seair-compare.csv";
9090
std::ofstream file(file_name);
9191
std::cout << "Writing output to " << file_name << std::endl;
92-
seair1.print_table({}, 21, 10, file);
92+
seair1.print_table(file, {}, 21, 10);
9393
file.close();
9494

9595
auto last1 = seair1.get_last_value();

cpp/memilio/utils/time_series.h

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <iterator>
2929
#include <vector>
3030
#include <ostream>
31+
#include <fstream>
3132

3233
namespace mio
3334
{
@@ -177,7 +178,7 @@ class TimeSeries
177178
}
178179

179180
/** move ctor and assignment */
180-
TimeSeries(TimeSeries&& other) = default;
181+
TimeSeries(TimeSeries&& other) = default;
181182
TimeSeries& operator=(TimeSeries&& other) = default;
182183

183184
/// Check if the time is strictly monotonic increasing.
@@ -485,7 +486,8 @@ class TimeSeries
485486
/**
486487
* @brief Print out the TimeSeries as a table.
487488
*
488-
* All entries in the table are spaced separatedly with at least one space. The first row of the table starts with
489+
* All row entries in the table are separated by the given separator, followed by additional spaces filling the width
490+
* of the next entry. Each row is terminated by a newline character '\n'. The first row of the table starts with
489491
* "Time", followed by other column labels. Each row after that contains the time (see get_time) followed by the
490492
* value (see get_value) for every row (i.e. time point) in the TimeSeries.
491493
* The width parameter sets the minimum width of each table entry. For the numbers from the TimeSeries, this width
@@ -495,26 +497,34 @@ class TimeSeries
495497
* (starting at 1, as "Time" is used for column 0). Labels in the column_labels vector that go beyond the TimeSeries column
496498
* numbers are ignored.
497499
*
498-
* @param column_labels Vector of custom labels for each column.
499-
* @param width The number of characters reserved for each number.
500-
* @param precision The number of decimals.
501-
* @param out Which ostream to use. Prints to terminal by default.
500+
* This method can be called in two ways:
501+
* 1. With an output stream as the first parameter: print_table(out, column_labels, width, precision, separator, header_prefix)
502+
* 2. Without specifying an output stream (defaults to std::cout): print_table(column_labels, width, precision, separator, header_prefix)
503+
*
504+
* @param[in,out] out Which ostream to use (optional, see above).
505+
* @param[in] column_labels Vector of custom labels for each column.
506+
* @param[in] width The number of characters reserved for each number.
507+
* @param[in] precision The number of decimals.
508+
* @param[in] separator Separator character between columns.
509+
* @param[in] header_prefix Prefix before the header row.
510+
*
511+
* @{
502512
*/
503-
void print_table(const std::vector<std::string>& column_labels = {}, size_t width = 16, size_t precision = 5,
504-
std::ostream& out = std::cout) const
513+
void print_table(std::ostream& out, const std::vector<std::string>& column_labels = {}, size_t width = 16,
514+
size_t precision = 5, char separator = ' ', const std::string header_prefix = "\n") const
505515
{
506516
// Note: input manipulators (like std::setw, std::left) are consumed by the first argument written to the stream
507517
// print column labels
508518
const auto w = width, p = precision;
509-
set_ostream_format(out, w, p) << std::left << "\nTime";
519+
out << header_prefix;
520+
set_ostream_format(out, w, p) << std::left << "Time";
510521
for (size_t k = 0; k < static_cast<size_t>(get_num_elements()); k++) {
522+
out << separator;
511523
if (k < column_labels.size()) {
512-
out << " ";
513524
set_ostream_format(out, w, p) << std::left << column_labels[k];
514525
}
515526
else {
516-
out << " ";
517-
set_ostream_format(out, w, p) << std::left << "#" + std::to_string(k + 1);
527+
set_ostream_format(out, w, p) << std::left << "C" + std::to_string(k + 1);
518528
}
519529
}
520530
// print values as table
@@ -524,13 +534,46 @@ class TimeSeries
524534
set_ostream_format(out, w, p) << std::right << get_time(i);
525535
auto res_i = get_value(i);
526536
for (size_t j = 0; j < static_cast<size_t>(res_i.size()); j++) {
527-
out << " ";
537+
out << separator;
528538
set_ostream_format(out, w, p) << std::right << res_i[j];
529539
}
530540
}
531541
out << "\n";
532542
}
533543

544+
void print_table(const std::vector<std::string>& column_labels = {}, size_t width = 16, size_t precision = 5,
545+
char separator = ' ', const std::string header_prefix = "\n") const
546+
{
547+
print_table(std::cout, column_labels, width, precision, separator, header_prefix);
548+
}
549+
/** @} */
550+
551+
/**
552+
* @brief Exports a TimeSeries object into a CSV file.
553+
*
554+
* The first column of the CSV file contains the time points. The remaining columns
555+
* contain the values at each time point. Column headers can be specified with column_labels.
556+
* This function utilizes the print_table method with a width of 1, the
557+
* specified separator, and an empty string as header prefix.
558+
*
559+
* @param[in] filepath Path to the CSV file.
560+
* @param[in] column_labels [Default: {}] Vector of labels for each column after the time column.
561+
* @param[in] separator [Default: ','] Separator character.
562+
* @param[in] precision [Default: 6] Number of decimals for floating point values.
563+
* @return IOResult<void> indicating success or failure.
564+
*/
565+
IOResult<void> export_csv(const std::string& filename, const std::vector<std::string>& column_labels = {},
566+
char separator = ',', int precision = 6) const
567+
{
568+
std::ofstream file(filename);
569+
if (!file.is_open()) {
570+
return mio::failure(mio::StatusCode::FileNotFound, "Failed to export TimeSeries as CSV to " + filename);
571+
}
572+
573+
print_table(file, column_labels, 1, precision, separator, "");
574+
return mio::success();
575+
}
576+
534577
/**
535578
* print this object (googletest)
536579
*/
@@ -617,9 +660,8 @@ struct TimeSeriesIterTraits {
617660
}
618661
using Matrix = typename TimeSeries<FP>::Matrix;
619662
using MatrixPtr = std::conditional_t<IsConst, const Matrix, Matrix>*;
620-
using VectorValue = typename decltype(std::declval<MatrixPtr>()
621-
->col(std::declval<Eigen::Index>())
622-
.tail(std::declval<Eigen::Index>()))::PlainObject;
663+
using VectorValue = typename decltype(
664+
std::declval<MatrixPtr>()->col(std::declval<Eigen::Index>()).tail(std::declval<Eigen::Index>()))::PlainObject;
623665
using VectorReference =
624666
decltype(std::declval<MatrixPtr>()->col(std::declval<Eigen::Index>()).tail(std::declval<Eigen::Index>()));
625667
using TimeValue = FP;

cpp/tests/test_time_series.cpp

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "matchers.h"
2323
#include <gtest/gtest.h>
2424
#include <gmock/gmock.h>
25+
#include "temp_file_register.h"
2526

2627
template <class T>
2728
using TestTimeSeries = ::testing::Test;
@@ -314,7 +315,7 @@ TYPED_TEST(TestTimeSeries, iteratorsRandomAccess)
314315

315316
TYPED_TEST(TestTimeSeries, create)
316317
{
317-
auto ts = mio::TimeSeries<double>::zero(5, 10);
318+
auto ts = mio::TimeSeries<TypeParam>::zero(5, 10);
318319
for (int i = 0; i < 5; i++) {
319320
ASSERT_EQ(ts.get_time(i), 0.0);
320321
for (int j = 0; j < 10; j++) {
@@ -326,40 +327,166 @@ TYPED_TEST(TestTimeSeries, create)
326327
TYPED_TEST(TestTimeSeries, print_table)
327328
{
328329
std::stringstream output;
329-
mio::TimeSeries<double> ts = mio::TimeSeries<double>::zero(2, 2);
330+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(2, 2);
330331
for (int i = 0; i < 2; i++) {
331332
for (int j = 0; j < 2; j++) {
332-
ts[i][j] = i + j + 0.123456789;
333+
ts[i][j] = static_cast<TypeParam>(i + j + 0.123456);
333334
}
334335
}
335-
ts.get_time((Eigen::Index)0) = 0.0;
336-
ts.get_time((Eigen::Index)1) = 1.0;
336+
ts.get_time((Eigen::Index)0) = static_cast<TypeParam>(0.0);
337+
ts.get_time((Eigen::Index)1) = static_cast<TypeParam>(1.0);
337338

338339
std::string expected_output_1 = "\nTime col_1 col_2\n0.00 0.12 1.12\n1.00 1.12 2.12\n";
339-
ts.print_table({"col_1", "col_2"}, 4, 2, output);
340+
ts.print_table(output, {"col_1", "col_2"}, 4, 2);
340341
std::string actual_output_1 = output.str();
341342
EXPECT_EQ(expected_output_1, actual_output_1);
342343

343344
output.str("");
344345

345-
std::string expected_output_2 = "\nTime #1 #2 \n 0.0 0.1 1.1\n 1.0 1.1 2.1\n";
346-
ts.print_table({}, 6, 1, output);
346+
std::string expected_output_2 = "\nTime C1 C2 \n 0.0 0.1 1.1\n 1.0 1.1 2.1\n";
347+
ts.print_table(output, {}, 6, 1);
347348
std::string actual_output_2 = output.str();
348349
EXPECT_EQ(expected_output_2, actual_output_2);
349350

350351
output.str("");
351352

352-
std::string expected_output_3 = "\nTime col_1 #2 \n 0.0000 0.1235 "
353+
std::string expected_output_3 = "\nTime col_1 C2 \n 0.0000 0.1235 "
353354
"1.1235\n 1.0000 1.1235 2.1235\n";
354-
ts.print_table({"col_1"}, 12, 4, output);
355+
ts.print_table(output, {"col_1"}, 12, 4);
355356
std::string actual_output_3 = output.str();
356357
EXPECT_EQ(expected_output_3, actual_output_3);
357358
}
358359

359-
TEST(TestTimeSeries, printTo)
360+
TYPED_TEST(TestTimeSeries, print_table_cout_overload)
361+
{
362+
// Just test that the print_table overload without ostream argument doesn't throw any exceptions.
363+
// The function behaviour is tested in "TestTimeSeries.print_table".
364+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(1, 1);
365+
ASSERT_NO_FATAL_FAILURE(ts.print_table());
366+
}
367+
368+
TYPED_TEST(TestTimeSeries, export_csv)
369+
{
370+
// Fill time series with test data
371+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(2, 2);
372+
for (int i = 0; i < 2; i++) {
373+
for (int j = 0; j < 2; j++) {
374+
ts[i][j] = static_cast<TypeParam>(i + j + 0.123456);
375+
}
376+
}
377+
ts.get_time((Eigen::Index)0) = static_cast<TypeParam>(0.0);
378+
ts.get_time((Eigen::Index)1) = static_cast<TypeParam>(1.0);
379+
380+
// Create a temp file for testing
381+
TempFileRegister file_register;
382+
auto csv_file_path = file_register.get_unique_path("test_csv-%%%%-%%%%.csv");
383+
384+
// Test export_csv function
385+
auto result = ts.export_csv(csv_file_path, {"column1", "column2"});
386+
ASSERT_TRUE(result);
387+
388+
// Read file and check data
389+
std::ifstream file(csv_file_path);
390+
ASSERT_TRUE(file.is_open());
391+
392+
std::string line;
393+
std::getline(file, line);
394+
EXPECT_EQ(line, "Time,column1,column2");
395+
396+
std::getline(file, line);
397+
EXPECT_EQ(line, "0.000000,0.123456,1.123456");
398+
399+
std::getline(file, line);
400+
EXPECT_EQ(line, "1.000000,1.123456,2.123456");
401+
402+
file.close();
403+
}
404+
405+
TYPED_TEST(TestTimeSeries, export_csv_no_labels)
406+
{
407+
// Fill time series with test data
408+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(2, 2);
409+
for (int i = 0; i < 2; i++) {
410+
for (int j = 0; j < 2; j++) {
411+
ts[i][j] = static_cast<TypeParam>(i + j + 0.123456);
412+
}
413+
}
414+
ts.get_time((Eigen::Index)0) = static_cast<TypeParam>(0.0);
415+
ts.get_time((Eigen::Index)1) = static_cast<TypeParam>(1.0);
416+
417+
// Create a temp file for testing
418+
TempFileRegister file_register;
419+
auto csv_file_path = file_register.get_unique_path("test_csv-%%%%-%%%%.csv");
420+
421+
// Test export_csv function without column names
422+
auto result = ts.export_csv(csv_file_path);
423+
ASSERT_TRUE(result);
424+
425+
// Read file and check data
426+
std::ifstream file(csv_file_path);
427+
ASSERT_TRUE(file.is_open());
428+
429+
std::string line;
430+
std::getline(file, line);
431+
EXPECT_EQ(line, "Time,C1,C2");
432+
433+
std::getline(file, line);
434+
EXPECT_EQ(line, "0.000000,0.123456,1.123456");
435+
436+
std::getline(file, line);
437+
EXPECT_EQ(line, "1.000000,1.123456,2.123456");
438+
439+
file.close();
440+
}
441+
442+
TYPED_TEST(TestTimeSeries, export_csv_different_separator)
443+
{
444+
// Fill time series with test data
445+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(2, 2);
446+
for (int i = 0; i < 2; i++) {
447+
for (int j = 0; j < 2; j++) {
448+
ts[i][j] = static_cast<TypeParam>(i + j + 0.123456);
449+
}
450+
}
451+
ts.get_time((Eigen::Index)0) = static_cast<TypeParam>(0.0);
452+
ts.get_time((Eigen::Index)1) = static_cast<TypeParam>(1.0);
453+
454+
// Create a temp file for testing
455+
TempFileRegister file_register;
456+
auto csv_file_path = file_register.get_unique_path("test_csv-%%%%-%%%%.csv");
457+
458+
// Export using semicolon as separator and precision of 3
459+
auto result = ts.export_csv(csv_file_path, {"col1", "col2"}, ';', 3);
460+
ASSERT_TRUE(result);
461+
462+
// Read file and check data
463+
std::ifstream file(csv_file_path);
464+
ASSERT_TRUE(file.is_open());
465+
466+
std::string line;
467+
std::getline(file, line);
468+
EXPECT_EQ(line, "Time;col1;col2");
469+
470+
std::getline(file, line);
471+
EXPECT_EQ(line, "0.000;0.123;1.123");
472+
473+
std::getline(file, line);
474+
EXPECT_EQ(line, "1.000;1.123;2.123");
475+
476+
file.close();
477+
}
478+
479+
TYPED_TEST(TestTimeSeries, export_csv_failed)
480+
{
481+
mio::TimeSeries<TypeParam> ts = mio::TimeSeries<TypeParam>::zero(2, 2);
482+
auto result = ts.export_csv("/test_false_dir/file.csv");
483+
ASSERT_FALSE(result);
484+
}
485+
486+
TYPED_TEST(TestTimeSeries, printTo)
360487
{
361488
//PrintTo is test code, so we don't check the exact output, just that it exists and doesn't fail
362-
auto ts = mio::TimeSeries<double>::zero(3, 2);
489+
auto ts = mio::TimeSeries<TypeParam>::zero(3, 2);
363490
std::stringstream ss;
364491
PrintTo(ts, &ss);
365492
ASSERT_FALSE(ss.str().empty());

pycode/memilio-simulation/memilio/simulation/bindings/utils/time_series.cpp

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,28 @@ void bind_time_series(py::module_& m, std::string const& name)
6363
}
6464
},
6565
py::is_operator(), py::arg("index"), py::arg("v"))
66-
.def("print_table",
67-
[](const mio::TimeSeries<double>& self, const std::vector<std::string>& column_labels, size_t width,
68-
size_t precision) {
69-
std::ostringstream oss;
70-
self.print_table(column_labels, width, precision, oss);
71-
return oss.str();
72-
})
66+
.def(
67+
"print_table",
68+
[](const mio::TimeSeries<double>& self, const std::vector<std::string>& column_labels, size_t width,
69+
size_t precision, char separator, const std::string& header_prefix) {
70+
std::ostringstream oss;
71+
self.print_table(oss, column_labels, width, precision, separator, header_prefix);
72+
return oss.str();
73+
},
74+
py::arg("column_labels") = std::vector<std::string>{}, py::arg("width") = 16, py::arg("precision") = 5,
75+
py::arg("separator") = ' ', py::arg("header_prefix") = "\n")
76+
77+
.def(
78+
"export_csv",
79+
[](const mio::TimeSeries<double>& self, const std::string& filename,
80+
const std::vector<std::string>& column_labels, char separator, int precision) {
81+
auto result = self.export_csv(filename, column_labels, separator, precision);
82+
if (!result) {
83+
throw py::value_error(result.error().message());
84+
}
85+
},
86+
py::arg("filename"), py::arg("column_labels") = std::vector<std::string>{}, py::arg("separator") = ',',
87+
py::arg("precision") = 6)
7388
.def("add_time_point",
7489
[](mio::TimeSeries<double>& self) {
7590
return self.add_time_point();

0 commit comments

Comments
 (0)