1 /* -------------------------------------------------------------------------- * 2 * OpenSim: DelimFileAdapter.h * 3 * -------------------------------------------------------------------------- * 4 * The OpenSim API is a toolkit for musculoskeletal modeling and simulation. * 5 * OpenSim is developed at Stanford University and supported by the US * 6 * National Institutes of Health (U54 GM072970, R24 HD065690) and by DARPA * 7 * through the Warrior Web program. * 8 * * 9 * Copyright (c) 2005-2017 Stanford University and the Authors * 10 * * 11 * Licensed under the Apache License, Version 2.0 (the "License"); you may * 12 * not use this file except in compliance with the License. You may obtain a * 13 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0. * 14 * * 15 * Unless required by applicable law or agreed to in writing, software * 16 * distributed under the License is distributed on an "AS IS" BASIS, * 17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 18 * See the License for the specific language governing permissions and * 19 * limitations under the License. * 20 * -------------------------------------------------------------------------- */ 21 22 #ifndef OPENSIM_DELIM_FILE_ADAPTER_H_ 23 #define OPENSIM_DELIM_FILE_ADAPTER_H_ 24 25 #include "SimTKcommon.h" 26 27 #include "About.h" 28 #include "FileAdapter.h" 29 #include "TimeSeriesTable.h" 30 #include "OpenSim/Common/IO.h" 31 32 #include <string> 33 #include <fstream> 34 #include <regex> 35 36 namespace OpenSim { 37 38 class IncorrectNumTokens : public Exception { 39 public: IncorrectNumTokens(const std::string & file,size_t line,const std::string & func,const std::string & msg)40 IncorrectNumTokens(const std::string& file, 41 size_t line, 42 const std::string& func, 43 const std::string& msg) : 44 Exception(file, line, func) { 45 46 addMessage(msg); 47 } 48 }; 49 50 class DataTypeMismatch : public Exception { 51 public: DataTypeMismatch(const std::string & file,size_t line,const std::string & func,const std::string & expected,const std::string & received)52 DataTypeMismatch(const std::string& file, 53 size_t line, 54 const std::string& func, 55 const std::string& expected, 56 const std::string& received) : 57 Exception(file, line, func) { 58 std::string msg = "expected = " + expected; 59 msg += " received = " + received; 60 61 addMessage(msg); 62 } 63 }; 64 65 namespace { 66 template<typename T> 67 struct is_SimTK_Vec : std::false_type {}; 68 69 template<int M, typename ELT, int Stride> 70 struct is_SimTK_Vec<SimTK::Vec<M, ELT, Stride>> { 71 static constexpr bool value = (M >= 2 && M <= 12); 72 }; 73 } // namespace 74 75 /** DelimFileAdapter is a FileAdapter that reads and writes text files with 76 given delimiters. CSVFileAdapter and MOTFileAdapter derive from this class and 77 set the delimiters appropriately for the files they parse. The read/write 78 functions return/accept a specific type of DataTable referred to as Table in 79 this class. 80 Header in the file is assumed to end with string "endheader" occupying a full 81 line. */ 82 template<typename T> 83 class DelimFileAdapter : public FileAdapter { 84 static_assert(std::is_same<T, double >::value || 85 is_SimTK_Vec<T >::value || 86 std::is_same<T, SimTK::UnitVec3 >::value || 87 std::is_same<T, SimTK::Quaternion>::value || 88 std::is_same<T, SimTK::SpatialVec>::value, 89 "Template argument T must be one of the following types : " 90 "double, SimTK::Vec2 to SimTK::Vec9, SimTK::Vec<10> to " 91 "SimTK::Vec<12>, SimTK::UnitVec, SimTK::Quaternion, " 92 "SimTK::SpatialVec"); 93 public: 94 DelimFileAdapter() = delete; 95 DelimFileAdapter(const DelimFileAdapter&) = default; 96 DelimFileAdapter(DelimFileAdapter&&) = default; 97 DelimFileAdapter& operator=(const DelimFileAdapter&) = default; 98 DelimFileAdapter& operator=(DelimFileAdapter&&) = default; 99 ~DelimFileAdapter() = default; 100 101 /** Create the adapter by setting the delimiters. */ 102 DelimFileAdapter(const std::string& delimitersRead, 103 const std::string& delimterWrite); 104 105 /** Create the adapter by setting the delimiters. */ 106 DelimFileAdapter(const std::string& delimitersRead, 107 const std::string& delimterWrite, 108 const std::string& compDelimRead, 109 const std::string& compDelimWrite); 110 111 DelimFileAdapter* clone() const override; 112 113 /** Key used for table associative array returned/accepted by write/read. */ 114 static const std::string tableString(); 115 116 /** Name of the data type T (template parameter). */ 117 static inline std::string dataTypeName(); 118 119 protected: 120 /** Implementation of the read functionality. */ 121 OutputTables extendRead(const std::string& filename) const override; 122 123 /** Implementation of the write functionality. */ 124 void extendWrite(const InputTables& tables, 125 const std::string& filename) const override; 126 127 /** Read elements of type T (template parameter) from a sequence of 128 tokens. */ 129 inline SimTK::RowVector_<T> 130 readElems(const std::vector<std::string>& tokens) const; 131 132 /** Write an element of type T (template parameter) to stream with the 133 specified precision. */ 134 inline void writeElem(std::ostream& stream, 135 const T& elem, 136 const unsigned& prec) const; 137 138 private: 139 /** Following overloads implement dataTypeName(). */ 140 static inline std::string dataTypeName_impl(double); 141 static inline std::string dataTypeName_impl(SimTK::UnitVec3); 142 static inline std::string dataTypeName_impl(SimTK::Quaternion); 143 static inline std::string dataTypeName_impl(SimTK::SpatialVec); 144 template<int M> 145 static inline std::string dataTypeName_impl(SimTK::Vec<M>); 146 147 /** Following overloads implement readElems(). */ 148 inline SimTK::RowVector_<double> 149 readElems_impl(const std::vector<std::string>& tokens, 150 double) const; 151 inline SimTK::RowVector_<SimTK::UnitVec3> 152 readElems_impl(const std::vector<std::string>& tokens, 153 SimTK::UnitVec3) const; 154 inline SimTK::RowVector_<SimTK::Quaternion> 155 readElems_impl(const std::vector<std::string>& tokens, 156 SimTK::Quaternion) const; 157 inline SimTK::RowVector_<SimTK::SpatialVec> 158 readElems_impl(const std::vector<std::string>& tokens, 159 SimTK::SpatialVec) const; 160 template<int M> 161 inline SimTK::RowVector_<SimTK::Vec<M>> 162 readElems_impl(const std::vector<std::string>& tokens, 163 SimTK::Vec<M>) const; 164 165 /** Following overloads implement writeElem(). */ 166 inline void writeElem_impl(std::ostream& stream, 167 const double& elem, 168 const unsigned& prec) const; 169 inline void writeElem_impl(std::ostream& stream, 170 const SimTK::SpatialVec& elem, 171 const unsigned& prec) const; 172 template<int M> 173 inline void writeElem_impl(std::ostream& stream, 174 const SimTK::Vec<M>& elem, 175 const unsigned& prec) const; 176 177 /** Trim string -- remove specified leading and trailing characters from 178 string. Trims out whitespace by default. */ 179 static std::string trim(const std::string& str, const char& ch = ' '); 180 181 /** Delimiters used for reading. */ 182 const std::string _delimitersRead; 183 /** Delimiter used for writing. Separates elements from each other. */ 184 const std::string _delimiterWrite; 185 /** Delimiter used for reading. Separates components of an element. */ 186 const std::string _compDelimRead; 187 /** Delimiter used for writing. Separates components of an element. */ 188 const std::string _compDelimWrite; 189 /** String representing the end of header in the file. */ 190 static const std::string _endHeaderString; 191 /** Column label of the time column. */ 192 static const std::string _timeColumnLabel; 193 /** Key used to read/write data-type name. */ 194 static const std::string _dataTypeString; 195 /** Key used to read/write file version number. */ 196 static const std::string _versionString; 197 /** Key used to read/write OpenSim version number. */ 198 static const std::string _opensimVersionString; 199 /** File version number. */ 200 static const std::string _versionNumber; 201 }; 202 203 204 template<typename T> 205 const std::string 206 DelimFileAdapter<T>::tableString() { 207 return "table"; 208 } 209 210 template<typename T> 211 const std::string 212 DelimFileAdapter<T>::_endHeaderString = "endheader"; 213 214 template<typename T> 215 std::string 216 DelimFileAdapter<T>::dataTypeName() { 217 return dataTypeName_impl(T{}); 218 } 219 220 template<typename T> 221 std::string 222 DelimFileAdapter<T>::dataTypeName_impl(double) { 223 return "double"; 224 } 225 226 template<typename T> 227 std::string 228 DelimFileAdapter<T>::dataTypeName_impl(SimTK::UnitVec3) { 229 return "UnitVec3"; 230 } 231 232 template<typename T> 233 std::string 234 DelimFileAdapter<T>::dataTypeName_impl(SimTK::Quaternion) { 235 return "Quaternion"; 236 } 237 238 template<typename T> 239 std::string 240 DelimFileAdapter<T>::dataTypeName_impl(SimTK::SpatialVec) { 241 return "SpatialVec"; 242 } 243 244 template<typename T> 245 template<int M> 246 std::string 247 DelimFileAdapter<T>::dataTypeName_impl(SimTK::Vec<M>) { 248 return std::string{"Vec"} + std::to_string(M); 249 } 250 251 template<typename T> 252 const std::string 253 DelimFileAdapter<T>::_timeColumnLabel = "time"; 254 255 template<typename T> 256 const std::string 257 DelimFileAdapter<T>::_dataTypeString = "DataType"; 258 259 template<typename T> 260 const std::string 261 DelimFileAdapter<T>::_versionString = "version"; 262 263 template<typename T> 264 const std::string 265 DelimFileAdapter<T>::_versionNumber = "3"; 266 267 template<typename T> 268 const std::string 269 DelimFileAdapter<T>::_opensimVersionString = "OpenSimVersion"; 270 271 template<typename T> 272 std::string 273 DelimFileAdapter<T>::trim(const std::string& str, 274 const char& ch) { 275 auto begin = str.find_first_not_of(ch); 276 if(begin == std::string::npos) 277 return std::string{}; 278 279 auto count = str.find_last_not_of(ch) - begin + 1; 280 return str.substr(begin, count); 281 } 282 283 template<typename T> 284 DelimFileAdapter<T>::DelimFileAdapter(const std::string& delimitersRead, 285 const std::string& delimiterWrite) : 286 _delimitersRead{delimitersRead}, 287 _delimiterWrite{delimiterWrite} 288 {} 289 290 template<typename T> 291 DelimFileAdapter<T>::DelimFileAdapter(const std::string& delimitersRead, 292 const std::string& delimiterWrite, 293 const std::string& compDelimRead, 294 const std::string& compDelimWrite) : 295 _delimitersRead{delimitersRead}, 296 _delimiterWrite{delimiterWrite}, 297 _compDelimRead{compDelimRead}, 298 _compDelimWrite{compDelimWrite} 299 {} 300 301 template<typename T> 302 DelimFileAdapter<T>* 303 DelimFileAdapter<T>::clone() const { 304 return new DelimFileAdapter{*this}; 305 } 306 307 template<typename T> 308 typename DelimFileAdapter<T>::OutputTables 309 DelimFileAdapter<T>::extendRead(const std::string& fileName) const { 310 OPENSIM_THROW_IF(fileName.empty(), 311 EmptyFileName); 312 313 std::ifstream in_stream{fileName}; 314 OPENSIM_THROW_IF(!in_stream.good(), 315 FileDoesNotExist, 316 fileName); 317 318 OPENSIM_THROW_IF(in_stream.peek() == std::ifstream::traits_type::eof(), 319 FileIsEmpty, 320 fileName); 321 322 size_t line_num{}; 323 // All the lines until "endheader" is header. 324 std::regex endheader{R"([ \t]*)" + _endHeaderString + R"([ \t]*)"}; 325 std::regex keyvalue{R"((.*)=(.*))"}; 326 std::string header{}; 327 std::string line{}; 328 ValueArrayDictionary keyValuePairs; 329 while(std::getline(in_stream, line)) { 330 ++line_num; 331 332 // We might be parsing a file with CRLF (\r\n) line endings on a 333 // platform that uses only LF (\n) line endings, in which case the \r 334 // is part of `line` and we must remove it manually. 335 if (!line.empty() && line.back() == '\r') 336 line.pop_back(); 337 338 if(std::regex_match(line, endheader)) 339 break; 340 341 // Detect Key value pairs of the form "key = value" and add them to 342 // metadata. 343 std::smatch matchRes{}; 344 if(std::regex_match(line, matchRes, keyvalue)) { 345 auto key = matchRes[1].str(); 346 auto value = matchRes[2].str(); 347 if(!key.empty() && !value.empty()) { 348 const auto trimmed_key = trim(key); 349 if(trimmed_key == _dataTypeString) { 350 // Discard key-value pair specifying datatype. Datatype is 351 // known at this point. 352 OPENSIM_THROW_IF(value != dataTypeName(), 353 DataTypeMismatch, 354 dataTypeName(), 355 value); 356 } else if(trimmed_key == _versionString) { 357 // Discard STO version number. Version number is added 358 // during writing. 359 } else if(trimmed_key == _opensimVersionString) { 360 // Discard OpenSim version number. Version number is added 361 // during writing. 362 } else { 363 keyValuePairs.setValueForKey(key, value); 364 } 365 continue; 366 } 367 } 368 369 if(header.empty()) 370 header = line; 371 else 372 header += "\n" + line; 373 } 374 keyValuePairs.setValueForKey("header", header); 375 376 // Callable to get the next line in form of vector of tokens. 377 auto nextLine = [&] { 378 return getNextLine(in_stream, _delimitersRead); 379 }; 380 381 // Read the line containing column labels and fill up the column labels 382 // container. 383 std::vector<std::string> column_labels{}; 384 while (column_labels.size() == 0) { // keep going down rows to find labels 385 column_labels = nextLine(); 386 // for labels we never expect empty elements, so remove them 387 IO::eraseEmptyElements(column_labels); 388 ++line_num; 389 } 390 391 OPENSIM_THROW_IF(column_labels.size() == 0, Exception, 392 "No column labels detected in file '" + fileName + "'."); 393 394 // Column 0 is the time column. Check and get rid of it. The data in this 395 // column is maintained separately from rest of the data. 396 OPENSIM_THROW_IF(column_labels[0] != _timeColumnLabel, 397 UnexpectedColumnLabel, 398 fileName, 399 _timeColumnLabel, 400 column_labels[0]); 401 column_labels.erase(column_labels.begin()); 402 403 // Read the rows one at a time and fill up the time column container and 404 // the data container. Start with a reasonable initial capacity for 405 // tradeoff between a small file and larger files. 100 worked well for 406 // a 50 MB file with ~80000 lines. 407 std::vector<double> timeVec; 408 int initCapacity = 100; 409 int ncol = static_cast<int>(column_labels.size()); 410 timeVec.reserve(initCapacity); 411 SimTK::Matrix_<T> matrix(initCapacity, ncol); 412 413 // Initialize current row and capacity 414 int curCapacity = initCapacity; 415 int curRow = 0; 416 417 // Start looping through each line 418 auto row = nextLine(); 419 while (!row.empty()) { 420 ++line_num; 421 422 // Double capacity if we reach the end of the containers. 423 // This is necessary until Simbody issue #401 is addressed. 424 if (curRow+1 > curCapacity) { 425 curCapacity *= 2; 426 timeVec.reserve(curCapacity); 427 matrix.resizeKeep(curCapacity, ncol); 428 } 429 430 // Time is column 0. 431 timeVec.push_back(std::stod(row.front())); 432 row.erase(row.begin()); 433 434 auto row_vector = readElems(row); 435 436 OPENSIM_THROW_IF(row_vector.size() != column_labels.size(), 437 RowLengthMismatch, 438 fileName, 439 line_num, 440 column_labels.size(), 441 static_cast<size_t>(row_vector.size())); 442 443 matrix.updRow(curRow) = std::move(row_vector); 444 445 row = nextLine(); 446 ++curRow; 447 } 448 449 // Resize the matrix down to the correct number of rows. 450 // This is necessary until Simbody issue #401 is addressed. 451 matrix.resizeKeep(curRow, ncol); 452 453 // Create the table and update other metadata from above 454 auto table = 455 std::make_shared<TimeSeriesTable_<T>>(timeVec, matrix, column_labels); 456 table->updTableMetaData() = keyValuePairs; 457 458 OutputTables output_tables{}; 459 output_tables.emplace(tableString(), table); 460 461 return output_tables; 462 } 463 464 template<typename T> 465 SimTK::RowVector_<T> 466 DelimFileAdapter<T>::readElems(const std::vector<std::string>& tokens) const { 467 return readElems_impl(tokens, T{}); 468 } 469 470 template<typename T> 471 SimTK::RowVector_<double> 472 DelimFileAdapter<T>::readElems_impl(const std::vector<std::string>& tokens, 473 double) const { 474 SimTK::RowVector_<double> elems{static_cast<int>(tokens.size())}; 475 for(auto i = 0u; i < tokens.size(); ++i) 476 elems[static_cast<int>(i)] = std::stod(tokens[i]); 477 478 return elems; 479 } 480 481 template<typename T> 482 SimTK::RowVector_<SimTK::UnitVec3> 483 DelimFileAdapter<T>::readElems_impl(const std::vector<std::string>& tokens, 484 SimTK::UnitVec3) const { 485 SimTK::RowVector_<SimTK::UnitVec3> elems{static_cast<int>(tokens.size())}; 486 for(auto i = 0u; i < tokens.size(); ++i) { 487 auto comps = tokenize(tokens[i], _compDelimRead); 488 OPENSIM_THROW_IF(comps.size() != 3, 489 IncorrectNumTokens, 490 "Expected 3x (multiple of 3) number of tokens."); 491 elems[i] = SimTK::UnitVec3{std::stod(comps[0]), 492 std::stod(comps[1]), 493 std::stod(comps[2])}; 494 } 495 496 return elems; 497 } 498 499 template<typename T> 500 SimTK::RowVector_<SimTK::Quaternion> 501 DelimFileAdapter<T>::readElems_impl(const std::vector<std::string>& tokens, 502 SimTK::Quaternion) const { 503 SimTK::RowVector_<SimTK::Quaternion> elems{static_cast<int>(tokens.size())}; 504 for(auto i = 0u; i < tokens.size(); ++i) { 505 auto comps = tokenize(tokens[i], _compDelimRead); 506 OPENSIM_THROW_IF(comps.size() != 4, 507 IncorrectNumTokens, 508 "Expected 4x (multiple of 4) number of tokens."); 509 elems[i] = SimTK::Quaternion{std::stod(comps[0]), 510 std::stod(comps[1]), 511 std::stod(comps[2]), 512 std::stod(comps[3])}; 513 } 514 515 return elems; 516 } 517 518 template<typename T> 519 SimTK::RowVector_<SimTK::SpatialVec> 520 DelimFileAdapter<T>::readElems_impl(const std::vector<std::string>& tokens, 521 SimTK::SpatialVec) const { 522 SimTK::RowVector_<SimTK::SpatialVec> elems{static_cast<int>(tokens.size())}; 523 for(auto i = 0u; i < tokens.size(); ++i) { 524 auto comps = tokenize(tokens[i], _compDelimRead); 525 OPENSIM_THROW_IF(comps.size() != 6, 526 IncorrectNumTokens, 527 "Expected 6x (multiple of 6) number of tokens."); 528 elems[i] = SimTK::SpatialVec{{std::stod(comps[0]), 529 std::stod(comps[1]), 530 std::stod(comps[2])}, 531 {std::stod(comps[3]), 532 std::stod(comps[4]), 533 std::stod(comps[5])}}; 534 } 535 536 return elems; 537 } 538 539 template<typename T> 540 template<int M> 541 SimTK::RowVector_<SimTK::Vec<M>> 542 DelimFileAdapter<T>::readElems_impl(const std::vector<std::string>& tokens, 543 SimTK::Vec<M>) const { 544 SimTK::RowVector_<SimTK::Vec<M>> elems{static_cast<int>(tokens.size())}; 545 for(auto i = 0u; i < tokens.size(); ++i) { 546 auto comps = tokenize(tokens[i], _compDelimRead); 547 OPENSIM_THROW_IF(comps.size() != M, 548 IncorrectNumTokens, 549 "Expected " + std::to_string(M) + 550 "x (multiple of " + std::to_string(M) + 551 ") number of tokens."); 552 for(int j = 0; j < M; ++j) { 553 elems[i][j] = std::stod(comps[j]); 554 } 555 } 556 557 return elems; 558 } 559 560 template<typename T> 561 void 562 DelimFileAdapter<T>::extendWrite(const InputTables& absTables, 563 const std::string& fileName) const { 564 OPENSIM_THROW_IF(absTables.empty(), 565 NoTableFound); 566 567 const TimeSeriesTable_<T>* table{}; 568 try { 569 auto abs_table = absTables.at(tableString()); 570 table = dynamic_cast<const TimeSeriesTable_<T>*>(abs_table); 571 } catch(std::out_of_range&) { 572 OPENSIM_THROW(KeyMissing, 573 tableString()); 574 } catch(std::bad_cast&) { 575 OPENSIM_THROW(IncorrectTableType); 576 } 577 578 OPENSIM_THROW_IF(fileName.empty(), 579 EmptyFileName); 580 581 std::ofstream out_stream{fileName}; 582 583 // First line of the stream is the header. 584 if (table->getTableMetaData().hasKey("header")) { 585 out_stream << table-> 586 getTableMetaData(). 587 getValueForKey("header"). 588 template getValue<std::string>() << "\n"; 589 } 590 // Write rest of the key-value pairs and end the header. 591 for(const auto& key : table->getTableMetaDataKeys()) { 592 try { 593 if(key != "header") 594 out_stream << key << "=" 595 << table-> 596 template getTableMetaData<std::string>(key) 597 << "\n"; 598 } catch(const InvalidTemplateArgument&) {} 599 } 600 // Write name of the data-type -- vec3, vec6, etc. 601 out_stream << _dataTypeString << "=" << dataTypeName() << "\n"; 602 // Write version number. 603 out_stream << _versionString << "=" << _versionNumber << "\n"; 604 out_stream << _opensimVersionString << "=" << GetVersion() << "\n"; 605 out_stream << _endHeaderString << "\n"; 606 607 // Line containing column labels. 608 out_stream << _timeColumnLabel; 609 for(unsigned col = 0; col < table->getNumColumns(); ++col) 610 out_stream << _delimiterWrite 611 << table-> 612 getDependentsMetaData(). 613 getValueArrayForKey("labels")[col]. 614 template getValue<std::string>(); 615 out_stream << "\n"; 616 617 // Data rows. 618 for(unsigned row = 0; row < table->getNumRows(); ++row) { 619 constexpr auto prec = std::numeric_limits<double>::digits10 + 1; 620 out_stream << std::setprecision(prec) 621 << table->getIndependentColumn()[row]; 622 const auto& row_r = table->getRowAtIndex(row); 623 for(unsigned col = 0; col < table->getNumColumns(); ++col) { 624 const auto& elt = row_r[col]; 625 out_stream << _delimiterWrite; 626 writeElem(out_stream, elt, prec); 627 } 628 out_stream << "\n"; 629 } 630 } 631 632 template<typename T> 633 void 634 DelimFileAdapter<T>::writeElem(std::ostream& stream, 635 const T& elem, 636 const unsigned& prec) const { 637 writeElem_impl(stream, elem, prec); 638 } 639 640 template<typename T> 641 void 642 DelimFileAdapter<T>::writeElem_impl(std::ostream& stream, 643 const double& elem, 644 const unsigned& prec) const { 645 stream << std::setprecision(prec) << elem; 646 } 647 648 template<typename T> 649 void 650 DelimFileAdapter<T>::writeElem_impl(std::ostream& stream, 651 const SimTK::SpatialVec& elem, 652 const unsigned& prec) const { 653 stream << std::setprecision(prec) << elem[0][0] 654 << _compDelimWrite << std::setprecision(prec) << elem[0][1] 655 << _compDelimWrite << std::setprecision(prec) << elem[0][2] 656 << _compDelimWrite << std::setprecision(prec) << elem[1][0] 657 << _compDelimWrite << std::setprecision(prec) << elem[1][1] 658 << _compDelimWrite << std::setprecision(prec) << elem[1][2]; 659 } 660 661 template<typename T> 662 template<int M> 663 void 664 DelimFileAdapter<T>::writeElem_impl(std::ostream& stream, 665 const SimTK::Vec<M>& elem, 666 const unsigned& prec) const { 667 stream << std::setprecision(prec) << elem[0]; 668 for(auto i = 1u; i < M; ++i) 669 stream << _compDelimWrite << std::setprecision(prec) << elem[i]; 670 } 671 672 } // namespace OpenSim 673 674 #endif // OPENSIM_DELIM_FILE_ADAPTER_H_ 675