1 /* -------------------------------------------------------------------------- *
2  *                            OpenSim:  TimeSeriesTable.h                     *
3  * -------------------------------------------------------------------------- *
4  * The OpenSim API is a toolkit for musculoskeletal modeling and simulation.  *
5  * See http://opensim.stanford.edu and the NOTICE file for more information.  *
6  * OpenSim is developed at Stanford University and supported by the US        *
7  * National Institutes of Health (U54 GM072970, R24 HD065690) and by DARPA    *
8  * through the Warrior Web program.                                           *
9  *                                                                            *
10  * Copyright (c) 2005-2017 Stanford University and the Authors                *
11  * Authors:                                                                   *
12  *                                                                            *
13  * Licensed under the Apache License, Version 2.0 (the "License"); you may    *
14  * not use this file except in compliance with the License. You may obtain a  *
15  * copy of the License at http://www.apache.org/licenses/LICENSE-2.0.         *
16  *                                                                            *
17  * Unless required by applicable law or agreed to in writing, software        *
18  * distributed under the License is distributed on an "AS IS" BASIS,          *
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
20  * See the License for the specific language governing permissions and        *
21  * limitations under the License.                                             *
22  * -------------------------------------------------------------------------- */
23 
24 #ifndef OPENSIM_TIME_SERIES_DATA_TABLE_H_
25 #define OPENSIM_TIME_SERIES_DATA_TABLE_H_
26 
27 /** \file
28 This file defines the TimeSeriesTable_ class, which is used by OpenSim to
29 provide an in-memory container for data access and manipulation.              */
30 
31 #include "OpenSim/Common/DataTable.h"
32 #include <SimTKsimbody.h>
33 
34 namespace OpenSim {
35 
36 class InvalidTable : public Exception {
37 public:
38     using Exception::Exception;
39 };
40 
41 class TimeColumnNotIncreasing : public InvalidTable {
42 public:
TimeColumnNotIncreasing(const std::string & file,size_t line,const std::string & func)43     TimeColumnNotIncreasing(const std::string& file,
44                             size_t line,
45                             const std::string& func) :
46         InvalidTable(file, line, func) {
47         std::string msg = "Time column is not strictly increasing";
48 
49         addMessage(msg);
50     }
51 };
52 
53 class InvalidTimestamp : public InvalidRow {
54 public:
55 #ifndef SWIG
56     using InvalidRow::InvalidRow;
57 #endif
58 };
59 
60 class TimestampLessThanEqualToPrevious : public InvalidTimestamp {
61 public:
TimestampLessThanEqualToPrevious(const std::string & file,size_t line,const std::string & func,size_t rowIndex,double new_timestamp,double prev_timestamp)62     TimestampLessThanEqualToPrevious(const std::string& file,
63                                      size_t line,
64                                      const std::string& func,
65                                      size_t rowIndex,
66                                      double new_timestamp,
67                                      double prev_timestamp) :
68         InvalidTimestamp(file, line, func) {
69         std::string msg = "Timestamp at row " + std::to_string(rowIndex);
70         msg += " with value " + std::to_string(new_timestamp);
71         msg += " is less-than/equal to timestamp at row ";
72         msg += std::to_string(rowIndex - 1) + " with value ";
73         msg += std::to_string(prev_timestamp);
74 
75         addMessage(msg);
76     }
77 };
78 
79 class TimestampGreaterThanEqualToNext : public InvalidTimestamp {
80 public:
TimestampGreaterThanEqualToNext(const std::string & file,size_t line,const std::string & func,size_t rowIndex,double new_timestamp,double next_timestamp)81     TimestampGreaterThanEqualToNext(const std::string& file,
82                                     size_t line,
83                                     const std::string& func,
84                                     size_t rowIndex,
85                                     double new_timestamp,
86                                     double next_timestamp) :
87         InvalidTimestamp(file, line, func) {
88         std::string msg = "Timestamp at row " + std::to_string(rowIndex);
89         msg += " with value " + std::to_string(new_timestamp);
90         msg += " is greater-than/equal to timestamp at row ";
91         msg += std::to_string(rowIndex + 1) + " with value ";
92         msg += std::to_string(next_timestamp);
93 
94         addMessage(msg);
95     }
96 };
97 
98 class TimeOutOfRange : public Exception {
99 public:
TimeOutOfRange(const std::string & file,size_t line,const std::string & func,const double time,const double min,const double max)100     TimeOutOfRange(const std::string& file,
101                    size_t line,
102                    const std::string& func,
103                    const double time,
104                    const double min,
105                    const double max) :
106         Exception(file, line, func) {
107         std::string msg = "Time " + std::to_string(time) +
108             " is out of time range [" + std::to_string(min) +
109             ", " + std::to_string(max) + "]";
110 
111         addMessage(msg);
112     }
113 };
114 
115 class InvalidTimeRange : public Exception {
116 public:
InvalidTimeRange(const std::string & file,size_t line,const std::string & func,const double begTime,const double endTime)117     InvalidTimeRange(const std::string& file,
118                      size_t line,
119                      const std::string& func,
120                      const double begTime,
121                      const double endTime) :
122         Exception(file, line, func) {
123         std::string msg = " Invalid time range: initial time " +
124             std::to_string(begTime)  + " >= final time = " +
125             std::to_string(endTime);
126 
127         addMessage(msg);
128     }
129 };
130 
131 /** TimeSeriesTable_ is a DataTable_ where the independent column is time of
132 type double. The time column is enforced to be strictly increasing.           */
133 template<typename ETY = SimTK::Real>
134 class TimeSeriesTable_ : public DataTable_<double, ETY> {
135 public:
136     typedef SimTK::RowVector_<ETY>     RowVector;
137     typedef SimTK::RowVectorView_<ETY> RowVectorView;
138 
139     TimeSeriesTable_()                                   = default;
140     TimeSeriesTable_(const TimeSeriesTable_&)            = default;
141     TimeSeriesTable_(TimeSeriesTable_&&)                 = default;
142     TimeSeriesTable_& operator=(const TimeSeriesTable_&) = default;
143     TimeSeriesTable_& operator=(TimeSeriesTable_&&)      = default;
144     ~TimeSeriesTable_()                                  = default;
145 
146     /** Convenience constructor to efficiently populate a time series table
147     from available data. This is primarily useful for constructing with large
148     data read in from file without having to reallocate and copy memory.*/
TimeSeriesTable_(const std::vector<double> & indVec,const SimTK::Matrix_<ETY> & depData,const std::vector<std::string> & labels)149     TimeSeriesTable_(const std::vector<double>& indVec,
150         const SimTK::Matrix_<ETY>& depData,
151         const std::vector<std::string>& labels) :
152             DataTable_<double, ETY>(indVec, depData, labels) {
153         try {
154             // Perform the validation of the data of this TimeSeriesTable.
155             // validateDependentsMetaData() invoked by the DataTable_
156             // constructor via setColumnLabels(), but we invoke it again
157             // because base classes cannot properly invoke virtual functions.
158             this->validateDependentsMetaData();
159             for (size_t i = 0; i < indVec.size(); ++i) {
160                 this->validateRow(i, indVec[i], depData.row(int(i)));
161             }
162         }
163         catch (std::exception&) {
164             // wipe out the data loaded if any
165             this->_indData.clear();
166             this->_depData.clear();
167             this->removeDependentsMetaDataForKey("labels");
168             throw;
169         }
170     }
171 
172     /** Construct a table with only the independent (time) column and 0
173     dependent columns. This constructor is useful if you want to populate the
174     table by appending columns rather than by appending rows.                 */
TimeSeriesTable_(const std::vector<double> & indVec)175     TimeSeriesTable_(const std::vector<double>& indVec) :
176             DataTable_<double, ETY>(indVec) {
177         try {
178             // Perform the validation of the data of this TimeSeriesTable.
179             // validateDependentsMetaData() invoked by the DataTable_
180             // constructor via setColumnLabels(), but we invoke it again
181             // because base classes cannot properly invoke virtual functions.
182             this->validateDependentsMetaData();
183             for (size_t i = 0; i < indVec.size(); ++i) {
184                 this->validateRow(i, indVec[i], this->_depData.row(int(i)));
185             }
186         }
187         catch (std::exception&) {
188             // wipe out the data loaded if any
189             this->_indData.clear();
190             this->_depData.clear(); // should be empty
191             this->removeDependentsMetaDataForKey("labels"); // should be empty
192             throw;
193         }
194 
195     }
196 
197 #ifndef SWIG
198     using DataTable_<double, ETY>::DataTable_;
199     using DataTable_<double, ETY>::operator=;
200     /** Flatten the columns of this table to create a TimeSeriesTable_<double>.
201     See documentation of DataTable_::flatten() for details.                   */
202     using DataTable_<double, ETY>::flatten;
203     /** Pack the columns of this table (which should be TimeSeriesTable_<double>
204     ) to create a TimeSeriesTable_<SimTK::Vec3>, TimeSeriesTable_<SimTK::Vec6>,
205     TimeSeriesTable_<SimTK::UnitVec3> and so on. See documentation for
206     DataTable_::pack().                                                       */
207     using DataTable_<double, ETY>::pack;
208 #endif
209 
210     /** Construct a TimeSeriesTable_ from a DataTable_.
211 
212     \throws InvalidTable If the input table's independent column is not strictly
213                          increasing.                                          */
TimeSeriesTable_(const DataTable_<double,ETY> & datatable)214     TimeSeriesTable_(const DataTable_<double, ETY>& datatable) :
215         DataTable_<double, ETY>(datatable) {
216         using DT = DataTable_<double, ETY>;
217 
218         OPENSIM_THROW_IF(!std::is_sorted(DT::_indData.cbegin(),
219                                          DT::_indData.cend()) ||
220                          std::adjacent_find(DT::_indData.cbegin(),
221                                             DT::_indData.cend()) !=
222                          DT::_indData.cend(),
223                          TimeColumnNotIncreasing);
224     }
225 
226     /** Construct TimeSeriesTable_ from a file.
227 
228     \param filename Name of the file.
229 
230     \throws InvalidArgument If the input file contains more than one table.
231     \throws InvalidArgument If the input file contains a table that is not of
232                             this TimeSeriesTable_ type.                       */
TimeSeriesTable_(const std::string & filename)233     TimeSeriesTable_(const std::string& filename) :
234         TimeSeriesTable_{filename, ""} {}
235 
236     /** Construct TimeSeriesTable_ from a file.
237 
238     \param filename Name of the file.
239     \param tablename Name of the table in the file to construct this
240                      TimeSeriesTable_ from. For example, a c3d file contains
241                      tables named 'markers' and 'forces'.
242 
243     \throws InvalidArgument If the input file contains more than one table and
244                             tablename was not specified.
245     \throws InvalidArgument If the input file contains a table that is not of
246                             this TimeSeriesTable_ type.                       */
TimeSeriesTable_(const std::string & filename,const std::string & tablename)247     TimeSeriesTable_(const std::string& filename,
248                      const std::string& tablename) {
249         auto absTables = FileAdapter::createAdapterFromExtension(filename)->read(filename);
250 
251         OPENSIM_THROW_IF(absTables.size() > 1 && tablename.empty(),
252                          InvalidArgument,
253                          "File '" + filename +
254                          "' contains more than one table and tablename not "
255                          "specified.");
256 
257         AbstractDataTable* absTable{};
258         if(tablename.empty()) {
259             absTable = (absTables.cbegin()->second).get();
260         } else {
261             try {
262                 absTable = absTables.at(tablename).get();
263             } catch (const std::out_of_range&) {
264                 OPENSIM_THROW(InvalidArgument,
265                               "File '" + filename + "' contains no table named "
266                               "'"+ tablename + "'.");
267             }
268         }
269         auto table = dynamic_cast<TimeSeriesTable_*>(absTable);
270         OPENSIM_THROW_IF(table == nullptr,
271                          InvalidArgument,
272                          "DataTable cannot be created from file '" + filename +
273                          "'. Type mismatch.");
274 
275         *this = std::move(*table);
276     }
277 
278     /** Get index of row whose time is nearest/closest to the given value.
279 
280     \param time Value to search for.
281     \param restrictToTimeRange  When true -- Exception is thrown if the given
282                                 value is out-of-range of the time column. A value
283                                 within SimTK::SignifcantReal of a time column
284                                 bound is considered to be equal to the bound.
285                                 When false -- If the given value is less than or
286                                 equal to the first value in the time column, the
287                                 index returned is of the first row. If the given
288                                 value is greater than or equal to the last value
289                                 in the time column, the index of the last row is
290                                 returned. Defaults to 'true'.
291 
292     \throws TimeOutOfRange If the given value is out-of-range of time column.
293     \throws EmptyTable If the table is empty.                                 */
294     size_t getNearestRowIndexForTime(const double time,
295                                      const bool restrictToTimeRange = true) const {
296         using DT = DataTable_<double, ETY>;
297         const auto& timeCol = DT::getIndependentColumn();
298         OPENSIM_THROW_IF(timeCol.size() == 0,
299             EmptyTable);
300         const SimTK::Real eps = SimTK::SignificantReal;
301         OPENSIM_THROW_IF(restrictToTimeRange &&
302             ((time < timeCol.front() - eps) ||
303             (time > timeCol.back() + eps)),
304             TimeOutOfRange,
305             time, timeCol.front(), timeCol.back());
306 
307         auto iter = std::lower_bound(timeCol.begin(), timeCol.end(), time);
308         if (iter == timeCol.end())
309             return timeCol.size() - 1;
310         if (iter == timeCol.begin())
311             return 0;
312         if ((*iter - time) <= (time - *std::prev(iter)))
313             return std::distance(timeCol.begin(), iter);
314         else
315             return std::distance(timeCol.begin(), std::prev(iter));
316     }
317     /** Get index of row whose time is first to be higher than the given value.
318 
319      \param time Value to search for.
320      */
getRowIndexAfterTime(const double & time)321     size_t getRowIndexAfterTime(
322             const double& time) const {
323         size_t candidate = getNearestRowIndexForTime(time, false);
324         using DT = DataTable_<double, ETY>;
325         const auto& timeCol = DT::getIndependentColumn();
326         const SimTK::Real eps = SimTK::SignificantReal;
327         // increment if less than time passed in
328         if (timeCol[candidate] < time-eps)
329             candidate++;
330         OPENSIM_THROW_IF(candidate > timeCol.size() - 1,
331                     TimeOutOfRange, time, timeCol.front(), timeCol.back());
332         return candidate;
333     }
334     /** Get index of row whose time is the largest time less than the given value.
335 
336      \param time Value to search for.
337      */
getRowIndexBeforeTime(const double & time)338     size_t getRowIndexBeforeTime(const double& time) const {
339         size_t candidate = getNearestRowIndexForTime(time, false);
340         using DT = DataTable_<double, ETY>;
341         const auto& timeCol = DT::getIndependentColumn();
342         const SimTK::Real eps = SimTK::SignificantReal;
343         // increment if less than time passed in
344         if (timeCol[candidate] > time+eps) candidate--;
345         OPENSIM_THROW_IF(candidate < 0, TimeOutOfRange, time,
346                 timeCol.front(), timeCol.back());
347         return candidate;
348     }
349 
350     /** Get row whose time column is nearest/closest to the given value.
351 
352     \param time Value to search for.
353     \param restrictToTimeRange When true -- Exception is thrown if the given
354                                value is out-of-range of the time column.
355                                When false -- If the given value is less than or
356                                equal to the first value in the time column, the
357                                row returned is the first row. If the given value
358                                is greater than or equal to the last value in the
359                                time column, the row returned is the last row.
360                                This operation only returns existing rows and
361                                does not perform any interpolation. Defaults to
362                                'true'.
363 
364     \throws TimeOutOfRange If the given value is out-of-range of time column.
365     \throws EmptyTable If the table is empty.                                 */
366     RowVectorView
367     getNearestRow(const double& time,
368                   const bool restrictToTimeRange = true) const {
369         using DT = DataTable_<double, ETY>;
370         return DT::getRowAtIndex(
371             getNearestRowIndexForTime(time, restrictToTimeRange) );
372     }
373 
374     /** Get writable reference to row whose time column is nearest/closest to
375     the given value.
376 
377     \param time Value to search for.
378     \param restrictToTimeRange When true -- Exception is thrown if the given
379                                value is out-of-range of the time column.
380                                When false -- If the given value is less than or
381                                equal to the first value in the time column, the
382                                row returned is the first row. If the given value
383                                is greater than or equal to the last value in the
384                                time column, the row returned is the last row.
385                                This operation only returns existing rows and
386                                does not perform any interpolation. Defaults to
387                                'true'.
388 
389     \throws TimeOutOfRange If the given value is out-of-range of time column.
390     \throws EmptyTable If the table is empty.                                 */
391     RowVectorView
392     updNearestRow(const double& time,
393                   const bool restrictToTimeRange = true) {
394         using DT = DataTable_<double, ETY>;
395         return DT::updRowAtIndex(
396             getNearestRowIndexForTime(time, restrictToTimeRange));
397     }
398 
399     /** Compute the average row in the time range (inclusive) given. This
400     operation does not modify the table. It just computes and returns an average
401     row.
402 
403     \throws InvalidTimeRange If beginTime is greater than or equal to endTime.
404     \throws TimeOutOfRange If beginTime or endTime is out of range of time
405                            column.                                            */
averageRow(const double & beginTime,const double & endTime)406     RowVector averageRow(const double& beginTime, const double& endTime) const {
407         using DT = DataTable_<double, ETY>;
408         OPENSIM_THROW_IF(endTime <= beginTime,
409                          InvalidTimeRange,
410                          beginTime, endTime);
411         const auto& timeCol = DT::getIndependentColumn();
412         OPENSIM_THROW_IF(beginTime < timeCol.front() ||
413                          beginTime > timeCol.back(),
414                          TimeOutOfRange,
415                          beginTime, timeCol.front(), timeCol.back(),);
416         OPENSIM_THROW_IF(endTime < timeCol.front() ||
417                          endTime > timeCol.back(),
418                          TimeOutOfRange,
419                          endTime, timeCol.front(), timeCol.back());
420 
421         std::vector<double> comps(DT::numComponentsPerElement(), 0);
422         RowVector row{static_cast<int>(DT::getNumColumns()),
423                       DT::makeElement(comps.begin(), comps.end())};
424         unsigned numRowsInRange{};
425         for(unsigned r = 0; r < DT::getNumRows(); ++r) {
426             if(timeCol[r] >= beginTime && timeCol[r] <= endTime) {
427                 row += DT::getRowAtIndex(r);
428                 ++numRowsInRange;
429             }
430         }
431         row /= numRowsInRange;
432 
433         return row;
434     }
435     /**
436      * Trim TimeSeriesTable to rows that have times that lies between
437      * newStartTime, newFinalTime. The trimming is done in place, no copy is made.
438      * Uses getRowIndexAfterTime to locate first row and
439      * getNearestRowIndexForTime method to locate last row.
440      */
trim(const double & newStartTime,const double & newFinalTime)441     void trim(const double& newStartTime, const double& newFinalTime) {
442         OPENSIM_THROW_IF(newFinalTime < newStartTime, EmptyTable);
443         const auto& timeCol = this->getIndependentColumn();
444         size_t start_index = 0;
445         size_t last_index = this->getNumRows() - 1;
446         // Avoid throwing exception if newStartTime is less than first time
447         // or newFinalTime is greater than last value in table
448         start_index = this->getRowIndexAfterTime(newStartTime);
449         last_index = this->getRowIndexBeforeTime(newFinalTime);
450         // Make sure last_index >= start_index before attempting to trim
451         OPENSIM_THROW_IF(last_index < start_index, EmptyTable);
452         // do the actual trimming based on index instead of time.
453         trimToIndices(start_index, last_index);
454         // If resulting table is empty, throw
455         if (this->getNumRows()==0)
456             std::cout << "WARNING: trimming resulted in an Empty Table" << std::endl;
457     }
458     /**
459      * trim TimeSeriesTable, keeping rows at newStartTime to the end.
460      */
trimFrom(const double & newStartTime)461     void trimFrom(const double& newStartTime) {
462         this->trim(newStartTime, this->getIndependentColumn().back());
463     }
464     /**
465      * trim TimeSeriesTable, keeping rows up to newFinalTime
466      */
trimTo(const double & newFinalTime)467     void trimTo(const double& newFinalTime) {
468         this->trim(this->getIndependentColumn().front(), newFinalTime);
469     }
470 protected:
471     /** Validate the given row.
472 
473     \throws InvalidRow If the timestamp for the row breaks strictly increasing
474                        property of the independent column.                    */
validateRow(size_t rowIndex,const double & time,const RowVector & row)475     void validateRow(size_t rowIndex,
476                      const double& time,
477                      const RowVector& row) const override {
478         using DT = DataTable_<double, ETY>;
479 
480         if(DT::_indData.empty())
481             return;
482 
483         if(rowIndex > 0) {
484             OPENSIM_THROW_IF(DT::_indData[rowIndex - 1] >= time,
485                              TimestampLessThanEqualToPrevious, rowIndex, time,
486                              DT::_indData[rowIndex - 1]);
487         }
488 
489         if(rowIndex < DT::_indData.size() - 1) {
490             OPENSIM_THROW_IF(DT::_indData[rowIndex + 1] <= time,
491                              TimestampGreaterThanEqualToNext, rowIndex, time,
492                              DT::_indData[rowIndex + 1]);
493         }
494     }
495     /** trim table to rows ebtween start_index and last_index incluslively
496      */
trimToIndices(const size_t & start_index,const size_t & last_index)497     void trimToIndices(const size_t& start_index, const size_t& last_index) {
498         // This uses the rather invasive but efficient mechanism to copy a
499         // block of the underlying Matrix.
500         // Side effect may include that headers/metaData may be left stale.
501         // Alternatively we can create a new TimeSeriesTable and copy contents
502         // one row at a time but that's rather overkill
503         SimTK::Matrix_<ETY> matrixBlock = this->updMatrix()((int)start_index, 0,
504                 (int)(last_index - start_index + 1),
505                 (int)this->getNumColumns());
506         this->updMatrix() = matrixBlock;
507         std::vector<double> newIndependentVector = std::vector<double>(
508                 this->getIndependentColumn().begin() + start_index,
509                 this->getIndependentColumn().begin() + last_index + 1);
510         this->_indData = newIndependentVector;
511     }
512 
513 }; // TimeSeriesTable_
514 
515 /** See TimeSeriesTable_ for details on the interface.                        */
516 typedef TimeSeriesTable_<SimTK::Real> TimeSeriesTable;
517 
518 /** See TimeSeriesTable_ for details on the interface.                        */
519 typedef TimeSeriesTable_<SimTK::Vec3> TimeSeriesTableVec3;
520 
521 /** See TimeSeriesTable_ for details on the interface.                        */
522 typedef TimeSeriesTable_<SimTK::Quaternion> TimeSeriesTableQuaternion;
523 
524 /** See TimeSeriesTable_ for details on the interface.                        */
525 typedef TimeSeriesTable_<SimTK::Rotation> TimeSeriesTableRotation;
526 
527 } // namespace OpenSim
528 
529 #endif // OPENSIM_TIME_SERIES_DATA_TABLE_H_
530