From 59e15b00ea51fee4baeea750dc11ab1952dfab1d Mon Sep 17 00:00:00 2001 From: Nina Drozd Date: Thu, 25 Apr 2019 15:45:20 +0100 Subject: IVGCVSW-2834 Add dynamic quantization via datasets * Add QuantizationDataSet class for quantization data parsed from CSV file * Add QuantizationInput for retrieving quantization data for each layer ID * Add unit tests for command line processor and QuantizationDataSet Change-Id: Iaf0a747b5f25a59a766ac04f7158e8cb7909d179 Signed-off-by: Nina Drozd --- src/armnn/NetworkQuantizer.cpp | 5 + src/armnn/NetworkQuantizer.hpp | 1 + src/armnnQuantizer/ArmNNQuantizerMain.cpp | 43 +++--- src/armnnQuantizer/CommandLineProcessor.cpp | 5 +- src/armnnQuantizer/CommandLineProcessor.hpp | 8 +- src/armnnQuantizer/QuantizationDataSet.cpp | 165 +++++++++++++++++++++ src/armnnQuantizer/QuantizationDataSet.hpp | 55 +++++++ src/armnnQuantizer/QuantizationInput.cpp | 103 +++++++++++++ src/armnnQuantizer/QuantizationInput.hpp | 54 +++++++ .../test/QuantizationDataSetTests.cpp | 144 ++++++++++++++++++ 10 files changed, 561 insertions(+), 22 deletions(-) create mode 100644 src/armnnQuantizer/QuantizationDataSet.cpp create mode 100644 src/armnnQuantizer/QuantizationDataSet.hpp create mode 100644 src/armnnQuantizer/QuantizationInput.cpp create mode 100644 src/armnnQuantizer/QuantizationInput.hpp create mode 100644 src/armnnQuantizer/test/QuantizationDataSetTests.cpp (limited to 'src') diff --git a/src/armnn/NetworkQuantizer.cpp b/src/armnn/NetworkQuantizer.cpp index f577aea00e..4692a6803f 100644 --- a/src/armnn/NetworkQuantizer.cpp +++ b/src/armnn/NetworkQuantizer.cpp @@ -49,6 +49,11 @@ void NetworkQuantizer::OverrideInputRange(LayerBindingId layerId, float min, flo VisitLayers(inputLayers, overrideInputRangeVisitor); } +void NetworkQuantizer::Refine(const InputTensors& inputTensors) +{ + //Implementation in a following commit +} + INetworkPtr NetworkQuantizer::ExportNetwork() { const Graph& graph = boost::polymorphic_downcast(m_InputNetwork)->GetGraph().TopologicalSort(); diff --git a/src/armnn/NetworkQuantizer.hpp b/src/armnn/NetworkQuantizer.hpp index 5e93f70290..4f6359f36d 100644 --- a/src/armnn/NetworkQuantizer.hpp +++ b/src/armnn/NetworkQuantizer.hpp @@ -21,6 +21,7 @@ public: : m_InputNetwork(inputNetwork), m_Options(options) {} void OverrideInputRange(LayerBindingId layerId, float min, float max) override; + void Refine(const InputTensors& inputTensors) override; INetworkPtr ExportNetwork() override; private: diff --git a/src/armnnQuantizer/ArmNNQuantizerMain.cpp b/src/armnnQuantizer/ArmNNQuantizerMain.cpp index 9ac8966753..103597a72d 100644 --- a/src/armnnQuantizer/ArmNNQuantizerMain.cpp +++ b/src/armnnQuantizer/ArmNNQuantizerMain.cpp @@ -7,6 +7,8 @@ #include #include #include +#include "QuantizationDataSet.hpp" +#include "QuantizationInput.hpp" #include #include @@ -41,31 +43,32 @@ int main(int argc, char* argv[]) armnn::INetworkPtr network = parser->CreateNetworkFromBinary(binaryContent); armnn::INetworkQuantizerPtr quantizer = armnn::INetworkQuantizer::Create(network.get(), quantizerOptions); - std::string csvFileName = cmdline.GetCsvFileName(); - if (csvFileName != "") + if (cmdline.HasQuantizationData()) { - // Call the Quantizer::Refine() function which will update the min/max ranges for the quantize constants - std::ifstream csvFileStream(csvFileName); - std::string line; - std::string csvDirectory = cmdline.GetCsvFileDirectory(); - while(getline(csvFileStream, line)) + armnnQuantizer::QuantizationDataSet dataSet = cmdline.GetQuantizationDataSet(); + if (!dataSet.IsEmpty()) { - std::istringstream s(line); - std::vector row; - std::string entry; - while(getline(s, entry, ',')) + // Get the Input Tensor Infos + armnnQuantizer::InputLayerVisitor inputLayerVisitor; + network->Accept(inputLayerVisitor); + + for(armnnQuantizer::QuantizationInput quantizationInput : dataSet) { - entry.erase(std::remove(entry.begin(), entry.end(), ' '), entry.end()); - entry.erase(std::remove(entry.begin(), entry.end(), '"'), entry.end()); - row.push_back(entry); + armnn::InputTensors inputTensors; + std::vector> inputData(quantizationInput.GetNumberOfInputs()); + std::vector layerBindingIds = quantizationInput.GetLayerBindingIds(); + unsigned int count = 0; + for (armnn::LayerBindingId layerBindingId : quantizationInput.GetLayerBindingIds()) + { + armnn::TensorInfo tensorInfo = inputLayerVisitor.GetTensorInfo(layerBindingId); + inputData[count] = quantizationInput.GetDataForEntry(layerBindingId); + armnn::ConstTensor inputTensor(tensorInfo, inputData[count].data()); + inputTensors.push_back(std::make_pair(layerBindingId, inputTensor)); + count++; + } + quantizer->Refine(inputTensors); } - std::string rawFileName = cmdline.GetCsvFileDirectory() + "/" + row[2]; - // passId: row[0] - // bindingId: row[1] - // rawFileName: file contains the RAW input tensor data - // LATER: Quantizer::Refine() function will be called with those arguments when it is implemented } - csvFileStream.close(); } armnn::INetworkPtr quantizedNetwork = quantizer->ExportNetwork(); diff --git a/src/armnnQuantizer/CommandLineProcessor.cpp b/src/armnnQuantizer/CommandLineProcessor.cpp index 16afe289f6..4f0d989d3f 100644 --- a/src/armnnQuantizer/CommandLineProcessor.cpp +++ b/src/armnnQuantizer/CommandLineProcessor.cpp @@ -149,6 +149,9 @@ bool CommandLineProcessor::ProcessCommandLine(int argc, char* argv[]) boost::filesystem::path csvFilePath(m_CsvFileName); m_CsvFileDirectory = csvFilePath.parent_path().c_str(); } + + // If CSV file is defined, create a QuantizationDataSet for specified CSV file. + m_QuantizationDataSet = QuantizationDataSet(m_CsvFileName); } if (!armnnQuantizer::ValidateOutputDirectory(m_OutputDirectory)) @@ -158,7 +161,7 @@ bool CommandLineProcessor::ProcessCommandLine(int argc, char* argv[]) std::string output(m_OutputDirectory); output.append(m_OutputFileName); - + if (boost::filesystem::exists(output)) { std::cerr << "Output file [" << output << "] already exists" << std::endl; diff --git a/src/armnnQuantizer/CommandLineProcessor.hpp b/src/armnnQuantizer/CommandLineProcessor.hpp index 7e366a7664..ae39abb603 100644 --- a/src/armnnQuantizer/CommandLineProcessor.hpp +++ b/src/armnnQuantizer/CommandLineProcessor.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include "QuantizationDataSet.hpp" namespace armnnQuantizer { @@ -31,13 +33,17 @@ public: std::string GetOutputDirectoryName() {return m_OutputDirectory;} std::string GetOutputFileName() {return m_OutputFileName;} std::string GetQuantizationScheme() {return m_QuantizationScheme;} -private: + QuantizationDataSet GetQuantizationDataSet() {return m_QuantizationDataSet;} + bool HasQuantizationData() {return !m_QuantizationDataSet.IsEmpty();} + +protected: std::string m_InputFileName; std::string m_CsvFileName; std::string m_CsvFileDirectory; std::string m_OutputDirectory; std::string m_OutputFileName; std::string m_QuantizationScheme; + QuantizationDataSet m_QuantizationDataSet; }; } // namespace armnnQuantizer diff --git a/src/armnnQuantizer/QuantizationDataSet.cpp b/src/armnnQuantizer/QuantizationDataSet.cpp new file mode 100644 index 0000000000..d225883854 --- /dev/null +++ b/src/armnnQuantizer/QuantizationDataSet.cpp @@ -0,0 +1,165 @@ +// +// Copyright © 2017 Arm Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +// + +#include "QuantizationDataSet.hpp" +#include "CsvReader.hpp" + +#define BOOST_FILESYSTEM_NO_DEPRECATED + +#include +#include + +namespace armnnQuantizer +{ + +QuantizationDataSet::QuantizationDataSet() +{ +} + +QuantizationDataSet::QuantizationDataSet(const std::string csvFilePath): + m_QuantizationInputs(), + m_CsvFilePath(csvFilePath) +{ + ParseCsvFile(); +} + +void AddInputData(unsigned int passId, + armnn::LayerBindingId bindingId, + const std::string& inputFilePath, + std::map& passIdToQuantizationInput) +{ + auto iterator = passIdToQuantizationInput.find(passId); + if (iterator == passIdToQuantizationInput.end()) + { + QuantizationInput input(passId, bindingId, inputFilePath); + passIdToQuantizationInput.emplace(passId, input); + } + else + { + auto existingQuantizationInput = iterator->second; + existingQuantizationInput.AddEntry(bindingId, inputFilePath); + } +} + +QuantizationDataSet::~QuantizationDataSet() +{ +} + +void InputLayerVisitor::VisitInputLayer(const armnn::IConnectableLayer* layer, + armnn::LayerBindingId id, + const char* name) +{ + m_TensorInfos.emplace(id, layer->GetInputSlot(0).GetConnection()->GetTensorInfo()); +} + +armnn::TensorInfo InputLayerVisitor::GetTensorInfo(armnn::LayerBindingId layerBindingId) +{ + auto iterator = m_TensorInfos.find(layerBindingId); + if (iterator != m_TensorInfos.end()) + { + return m_TensorInfos.at(layerBindingId); + } + else + { + throw armnn::Exception("Could not retrieve tensor info for binding ID " + std::to_string(layerBindingId)); + } +} + + +unsigned int GetPassIdFromCsvRow(std::vector csvRows, unsigned int rowIndex) +{ + unsigned int passId; + try + { + passId = static_cast(std::stoi(csvRows[rowIndex].values[0])); + } + catch (std::invalid_argument) + { + throw armnn::ParseException("Pass ID [" + csvRows[rowIndex].values[0] + "]" + + " is not correct format on CSV row " + std::to_string(rowIndex)); + } + return passId; +} + +armnn::LayerBindingId GetBindingIdFromCsvRow(std::vector csvRows, unsigned int rowIndex) +{ + armnn::LayerBindingId bindingId; + try + { + bindingId = std::stoi(csvRows[rowIndex].values[1]); + } + catch (std::invalid_argument) + { + throw armnn::ParseException("Binding ID [" + csvRows[rowIndex].values[0] + "]" + + " is not correct format on CSV row " + std::to_string(rowIndex)); + } + return bindingId; +} + +std::string GetFileNameFromCsvRow(std::vector csvRows, unsigned int rowIndex) +{ + std::string fileName = csvRows[rowIndex].values[2]; + + if (!boost::filesystem::exists(fileName)) + { + throw armnn::ParseException("File [ " + fileName + "] provided on CSV row " + std::to_string(rowIndex) + + " does not exist."); + } + + if (fileName.empty()) + { + throw armnn::ParseException("Filename cannot be empty on CSV row " + std::to_string(rowIndex)); + } + return fileName; +} + + +void QuantizationDataSet::ParseCsvFile() +{ + std::map passIdToQuantizationInput; + armnnUtils::CsvReader reader; + + if (m_CsvFilePath == "") + { + throw armnn::Exception("CSV file not specified."); + } + + // Parse CSV file and extract data + std::vector csvRows = reader.ParseFile(m_CsvFilePath); + if (csvRows.empty()) + { + throw armnn::Exception("CSV file [" + m_CsvFilePath + "] is empty."); + } + + for (unsigned int i = 0; i < csvRows.size(); ++i) + { + if (csvRows[i].values.size() != 3) + { + throw armnn::Exception("CSV file [" + m_CsvFilePath + "] does not have correct number of entries " + + "on line " + std::to_string(i) + ". Expected 3 entries " + + "but was " + std::to_string(csvRows[i].values.size())); + } + + unsigned int passId = GetPassIdFromCsvRow(csvRows, i); + armnn::LayerBindingId bindingId = GetBindingIdFromCsvRow(csvRows, i); + std::string rawFileName = GetFileNameFromCsvRow(csvRows, i); + + AddInputData(passId, bindingId, rawFileName, passIdToQuantizationInput); + } + + if (passIdToQuantizationInput.empty()) + { + throw armnn::Exception("Could not parse CSV file."); + } + + // Once all entries in CSV file are parsed successfully and QuantizationInput map is populated, populate + // QuantizationInputs iterator for easier access and clear the map + for (auto itr = passIdToQuantizationInput.begin(); itr != passIdToQuantizationInput.end(); ++itr) + { + m_QuantizationInputs.emplace_back(itr->second); + } +} + +} diff --git a/src/armnnQuantizer/QuantizationDataSet.hpp b/src/armnnQuantizer/QuantizationDataSet.hpp new file mode 100644 index 0000000000..3a97630ccf --- /dev/null +++ b/src/armnnQuantizer/QuantizationDataSet.hpp @@ -0,0 +1,55 @@ +// +// Copyright © 2017 Arm Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +// + +#pragma once + +#include +#include "QuantizationInput.hpp" +#include "armnn/LayerVisitorBase.hpp" +#include "armnn/Tensor.hpp" + +namespace armnnQuantizer +{ + +/// QuantizationDataSet is a structure which is created after parsing a quantization CSV file. +/// It contains records of filenames which contain refinement data per pass ID for binding ID. +class QuantizationDataSet +{ + using QuantizationInputs = std::vector; +public: + + using iterator = QuantizationInputs::iterator; + using const_iterator = QuantizationInputs::const_iterator; + + QuantizationDataSet(); + QuantizationDataSet(std::string csvFilePath); + ~QuantizationDataSet(); + bool IsEmpty() const {return m_QuantizationInputs.empty();} + + iterator begin() { return m_QuantizationInputs.begin(); } + iterator end() { return m_QuantizationInputs.end(); } + const_iterator begin() const { return m_QuantizationInputs.begin(); } + const_iterator end() const { return m_QuantizationInputs.end(); } + const_iterator cbegin() const { return m_QuantizationInputs.cbegin(); } + const_iterator cend() const { return m_QuantizationInputs.cend(); } + +private: + void ParseCsvFile(); + + QuantizationInputs m_QuantizationInputs; + std::string m_CsvFilePath; +}; + +/// Visitor class implementation to gather the TensorInfo for LayerBindingID for creation of ConstTensor for Refine. +class InputLayerVisitor : public armnn::LayerVisitorBase +{ +public: + void VisitInputLayer(const armnn::IConnectableLayer *layer, armnn::LayerBindingId id, const char* name); + armnn::TensorInfo GetTensorInfo(armnn::LayerBindingId); +private: + std::map m_TensorInfos; +}; + +} // namespace armnnQuantizer \ No newline at end of file diff --git a/src/armnnQuantizer/QuantizationInput.cpp b/src/armnnQuantizer/QuantizationInput.cpp new file mode 100644 index 0000000000..bb7aff1c2c --- /dev/null +++ b/src/armnnQuantizer/QuantizationInput.cpp @@ -0,0 +1,103 @@ +// +// Copyright © 2017 Arm Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +// + +#include "QuantizationInput.hpp" + +#include +#include +#include +#include "armnn/Exceptions.hpp" + +namespace armnnQuantizer +{ + +QuantizationInput::QuantizationInput(const unsigned int passId, + const armnn::LayerBindingId bindingId, + const std::string fileName): + m_PassId(passId) +{ + m_LayerBindingIdToFileName.emplace(bindingId, fileName); +} + +QuantizationInput::QuantizationInput(const QuantizationInput& other) +{ + m_PassId = other.GetPassId(); + m_LayerBindingIdToFileName.clear(); + for (armnn::LayerBindingId bindingId : other.GetLayerBindingIds()) + { + std::string filename = other.GetFileName(bindingId); + AddEntry(bindingId, filename); + } +} + +void QuantizationInput::AddEntry(const armnn::LayerBindingId bindingId, const std::string fileName) +{ + m_LayerBindingIdToFileName.emplace(bindingId, fileName); +} + +std::vector QuantizationInput::GetDataForEntry(const armnn::LayerBindingId bindingId) const +{ + if (m_LayerBindingIdToFileName.at(bindingId).empty()) + { + throw armnn::Exception("Layer binding ID not found"); + } + + std::string fileName = m_LayerBindingIdToFileName.at(bindingId); + std::ifstream in(fileName.c_str(), std::ifstream::binary); + if (!in.is_open()) + { + throw armnn::Exception("Failed to open input tensor file " + fileName); + } + + std::string line; + std::vector values; + char* pEnd; + + while (std::getline(in, line, ' ')) + { + values.emplace_back(std::strtof(line.c_str(), &pEnd)); + } + return values; +} + +std::vector QuantizationInput::GetLayerBindingIds() const +{ + std::vector layerBindingIDs; + + for (auto iterator = m_LayerBindingIdToFileName.begin(); iterator != m_LayerBindingIdToFileName.end(); ++iterator) + { + layerBindingIDs.emplace_back(iterator->first); + } + return layerBindingIDs; +} + +unsigned long QuantizationInput::GetNumberOfInputs() const +{ + return m_LayerBindingIdToFileName.size(); +} + +unsigned int QuantizationInput::GetPassId() const +{ + return m_PassId; +} + +std::string QuantizationInput::GetFileName(const armnn::LayerBindingId bindingId) const +{ + auto iterator = m_LayerBindingIdToFileName.find(bindingId); + if (iterator != m_LayerBindingIdToFileName.end()) + { + return m_LayerBindingIdToFileName.at(bindingId); + } + else + { + throw armnn::Exception("Could not retrieve filename for binding ID " + std::to_string(bindingId)); + } +} + +QuantizationInput::~QuantizationInput() noexcept +{ +} + +} \ No newline at end of file diff --git a/src/armnnQuantizer/QuantizationInput.hpp b/src/armnnQuantizer/QuantizationInput.hpp new file mode 100644 index 0000000000..ebabdd704f --- /dev/null +++ b/src/armnnQuantizer/QuantizationInput.hpp @@ -0,0 +1,54 @@ +// +// Copyright © 2017 Arm Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +// + +#pragma once + +#include +#include +#include + +namespace armnnQuantizer +{ + +/// QuantizationInput for specific pass ID, can list a corresponding raw data file for each LayerBindingId. +class QuantizationInput +{ +public: + + /// Constructor for QuantizationInput + QuantizationInput(const unsigned int passId, + const armnn::LayerBindingId bindingId, + const std::string fileName); + + QuantizationInput(const QuantizationInput& other); + + // Add binding ID to image tensor filepath entry + void AddEntry(const armnn::LayerBindingId bindingId, const std::string fileName); + + // Retrieve tensor data for entry with provided binding ID + std::vector GetDataForEntry(const armnn::LayerBindingId bindingId) const; + + /// Retrieve Layer Binding IDs for this QuantizationInput. + std::vector GetLayerBindingIds() const; + + /// Get number of inputs for this QuantizationInput. + unsigned long GetNumberOfInputs() const; + + /// Retrieve Pass ID for this QuantizationInput. + unsigned int GetPassId() const; + + /// Retrieve filename path for specified Layer Binding ID. + std::string GetFileName(const armnn::LayerBindingId bindingId) const; + + /// Destructor + ~QuantizationInput() noexcept; + +private: + unsigned int m_PassId; + std::map m_LayerBindingIdToFileName; + +}; + +} \ No newline at end of file diff --git a/src/armnnQuantizer/test/QuantizationDataSetTests.cpp b/src/armnnQuantizer/test/QuantizationDataSetTests.cpp new file mode 100644 index 0000000000..2b46aae26e --- /dev/null +++ b/src/armnnQuantizer/test/QuantizationDataSetTests.cpp @@ -0,0 +1,144 @@ +// +// Copyright © 2017 Arm Ltd. All rights reserved. +// SPDX-License-Identifier: MIT +// + +#include + +#include "../QuantizationDataSet.hpp" +#include +#include +#include +#include + +#define BOOST_FILESYSTEM_NO_DEPRECATED + +#include +#include +#include +#include + + +using namespace armnnQuantizer; + +struct CsvTestHelper { + + CsvTestHelper() + { + BOOST_TEST_MESSAGE("setup fixture"); + } + + ~CsvTestHelper() + { + BOOST_TEST_MESSAGE("teardown fixture"); + TearDown(); + } + + std::string CreateTempCsvFile(std::map> csvData) + { + boost::filesystem::path fileDir = boost::filesystem::temp_directory_path(); + boost::filesystem::path p{fileDir / boost::filesystem::unique_path("%%%%-%%%%-%%%%.csv")}; + + boost::filesystem::path tensorInput1{fileDir / boost::filesystem::unique_path("input_0_0.raw")}; + boost::filesystem::path tensorInput2{fileDir / boost::filesystem::unique_path("input_1_0.raw")}; + boost::filesystem::path tensorInput3{fileDir / boost::filesystem::unique_path("input_2_0.raw")}; + + try + { + boost::filesystem::ofstream ofs{p}; + + boost::filesystem::ofstream ofs1{tensorInput1}; + boost::filesystem::ofstream ofs2{tensorInput2}; + boost::filesystem::ofstream ofs3{tensorInput3}; + + + for(auto entry : csvData.at(0)) + { + ofs1 << entry << " "; + } + for(auto entry : csvData.at(1)) + { + ofs2 << entry << " "; + } + for(auto entry : csvData.at(2)) + { + ofs3 << entry << " "; + } + + ofs << "0, 0, " << tensorInput1.c_str() << std::endl; + ofs << "2, 0, " << tensorInput3.c_str() << std::endl; + ofs << "1, 0, " << tensorInput2.c_str() << std::endl; + + ofs.close(); + ofs1.close(); + ofs2.close(); + ofs3.close(); + } + catch (std::exception &e) + { + std::cerr << "Unable to write to file at location [" << p.c_str() << "] : " << e.what() << std::endl; + BOOST_TEST(false); + } + + m_CsvFile = p; + return p.string(); + } + + void TearDown() + { + RemoveCsvFile(); + } + + void RemoveCsvFile() + { + if (m_CsvFile) + { + try + { + boost::filesystem::remove(*m_CsvFile); + } + catch (std::exception &e) + { + std::cerr << "Unable to delete file [" << *m_CsvFile << "] : " << e.what() << std::endl; + BOOST_TEST(false); + } + } + } + + boost::optional m_CsvFile; +}; + + +BOOST_AUTO_TEST_SUITE(QuantizationDataSetTests) + +BOOST_FIXTURE_TEST_CASE(CheckDataSet, CsvTestHelper) +{ + + std::map> csvData; + csvData.insert(std::pair>(0, { 0.111111f, 0.222222f, 0.333333f })); + csvData.insert(std::pair>(1, { 0.444444f, 0.555555f, 0.666666f })); + csvData.insert(std::pair>(2, { 0.777777f, 0.888888f, 0.999999f })); + + std::string myCsvFile = CsvTestHelper::CreateTempCsvFile(csvData); + QuantizationDataSet dataSet(myCsvFile); + BOOST_TEST(!dataSet.IsEmpty()); + + int csvRow = 0; + for(armnnQuantizer::QuantizationInput input : dataSet) + { + BOOST_TEST(input.GetPassId() == csvRow); + + BOOST_TEST(input.GetLayerBindingIds().size() == 1); + BOOST_TEST(input.GetLayerBindingIds()[0] == 0); + BOOST_TEST(input.GetDataForEntry(0).size() == 3); + + // Check that QuantizationInput data for binding ID 0 corresponds to float values + // used for populating the CSV file using by QuantizationDataSet + BOOST_TEST(input.GetDataForEntry(0).at(0) == csvData.at(csvRow).at(0)); + BOOST_TEST(input.GetDataForEntry(0).at(1) == csvData.at(csvRow).at(1)); + BOOST_TEST(input.GetDataForEntry(0).at(2) == csvData.at(csvRow).at(2)); + ++csvRow; + } +} + +BOOST_AUTO_TEST_SUITE_END(); \ No newline at end of file -- cgit v1.2.1