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