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