diff --git a/FlucomaClients.cmake b/FlucomaClients.cmake index 2208b82b1..b0935fa5c 100644 --- a/FlucomaClients.cmake +++ b/FlucomaClients.cmake @@ -142,7 +142,9 @@ add_client(Transients clients/rt/TransientClient.hpp CLASS RTTransientClient ) #lib manipulation client group add_client(DataSet clients/nrt/DataSetClient.hpp CLASS NRTThreadedDataSetClient GROUP MANIPULATION) +add_client(DataSeries clients/nrt/DataSeriesClient.hpp CLASS NRTThreadedDataSeriesClient GROUP MANIPULATION) add_client(DataSetQuery clients/nrt/DataSetQueryClient.hpp CLASS NRTThreadedDataSetQueryClient GROUP MANIPULATION) +add_client(DataSeries clients/nrt/DataSeriesClient.hpp CLASS NRTThreadedDataSeriesClient GROUP MANIPULATION) add_client(LabelSet clients/nrt/LabelSetClient.hpp CLASS NRTThreadedLabelSetClient GROUP MANIPULATION) add_client(KDTree clients/nrt/KDTreeClient.hpp CLASS NRTThreadedKDTreeClient GROUP MANIPULATION) add_client(KMeans clients/nrt/KMeansClient.hpp CLASS NRTThreadedKMeansClient GROUP MANIPULATION) @@ -158,3 +160,6 @@ add_client(UMAP clients/nrt/UMAPClient.hpp CLASS NRTThreadedUMAPClient GROUP MAN add_client(MLPRegressor clients/nrt/MLPRegressorClient.hpp CLASS NRTThreadedMLPRegressorClient GROUP MANIPULATION) add_client(MLPClassifier clients/nrt/MLPClassifierClient.hpp CLASS NRTThreadedMLPClassifierClient GROUP MANIPULATION) add_client(Grid clients/nrt/GridClient.hpp CLASS NRTThreadedGridClient GROUP MANIPULATION) +add_client(DTW clients/nrt/DTWClient.hpp CLASS NRTThreadedDTWClient GROUP MANIPULATION) +add_client(DTWClassifier clients/nrt/DTWClassifierClient.hpp CLASS NRTThreadedDTWClassifierClient GROUP MANIPULATION) +add_client(DTWRegressor clients/nrt/DTWRegressorClient.hpp CLASS NRTThreadedDTWRegressorClient GROUP MANIPULATION) diff --git a/include/algorithms/public/DTW.hpp b/include/algorithms/public/DTW.hpp new file mode 100644 index 000000000..f929a5faf --- /dev/null +++ b/include/algorithms/public/DTW.hpp @@ -0,0 +1,238 @@ +/* +Part of the Fluid Corpus Manipulation Project (http://www.flucoma.org/) +Copyright University of Huddersfield. +Licensed under the BSD-3 License. +See license.md file in the project root for full license information. +This project has received funding from the European Research Council (ERC) +under the European Union’s Horizon 2020 research and innovation programme +(grant agreement No 725899). +*/ + +#pragma once + +#include "../util/FluidEigenMappings.hpp" +#include "../../data/FluidDataSet.hpp" +#include "../../data/FluidIndex.hpp" +#include "../../data/FluidMemory.hpp" +#include "../../data/FluidTensor.hpp" +#include "../../data/TensorTypes.hpp" +#include +#include +#include +#include + +namespace fluid { +namespace algorithm { + + +enum class DTWConstraint { kUnconstrained, kIkatura, kSakoeChiba }; + +// debt of gratitude to the wonderful article on +// https://rtavenar.github.io/blog/dtw.html a better explanation of DTW than any +// other algorithm explanation I've seen + +class DTW +{ + struct Constraint; + +public: + explicit DTW() = default; + ~DTW() = default; + + void init() {} + void clear() {} + + index size() const { return mPNorm; } + constexpr index dims() const { return 0; } + constexpr index initialized() const { return true; } + + double process(InputRealMatrixView x1, InputRealMatrixView x2, + DTWConstraint constr = DTWConstraint::kUnconstrained, + index param = 2, + Allocator& alloc = FluidDefaultAllocator()) const + { + ScopedEigenMap x1r(x1.cols(), alloc), + x2r(x2.cols(), alloc); + Constraint constraint(constr, x1.rows(), x2.rows(), param); + + mDistanceMetrics.resize(x1.rows(), x2.rows()); + mDistanceMetrics.fill(std::numeric_limits::max()); + + constraint.iterate([&, this](index r, index c) { + x1r = _impl::asEigen(x1.row(r)); + x2r = _impl::asEigen(x2.row(c)); + + mDistanceMetrics(r, c) = differencePNormToTheP(x1r, x2r); + + if (r > 0 || c > 0) + { + double minimum = std::numeric_limits::max(); + + if (r > 0) minimum = std::min(minimum, mDistanceMetrics(r - 1, c)); + if (c > 0) minimum = std::min(minimum, mDistanceMetrics(r, c - 1)); + if (r > 0 && c > 0) + minimum = std::min(minimum, mDistanceMetrics(r - 1, c - 1)); + + mDistanceMetrics(r, c) += minimum; + } + }); + + return std::pow(mDistanceMetrics(x1.rows() - 1, x2.rows() - 1), + 1.0 / mPNorm); + } + +private: + mutable RealMatrix mDistanceMetrics; + index mPNorm{2}; + + // P-Norm of the difference vector + // Lp{vec} = (|vec[0]|^p + |vec[1]|^p + ... + |vec[n-1]|^p + |vec[n]|^p)^(1/p) + // i.e., the 2-norm of a vector is the euclidian distance from the origin + // the 1-norm is the sum of the absolute value of the elements + // To the power P since we'll be summing multiple Norms together and they + // can combine into a single norm if you calculate the norm of multiple norms + // (normception) + inline double + differencePNormToTheP(const Eigen::Ref& v1, + const Eigen::Ref& v2) const + { + // assert(v1.size() == v2.size()); + return (v1.array() - v2.array()).abs().pow(mPNorm).sum(); + } + + // fun little fold operation to do a variadic minimum + template + inline static auto min(Args&&... args) + { + auto m = (args, ...); + return ((m = std::min(m, args)), ...); + } + + // filter for minimum chaining, if cond evaluates to false then the value + // isn't used (never will be the minimum if its the numeric maximum) + template + inline static T useIf(bool cond, T val) + { + return cond ? val : std::numeric_limits::max(); + } + + struct Constraint + { + Constraint(DTWConstraint c, index rows, index cols, float param) + : mType{c}, mRows{rows}, mCols{cols}, mParam{param} + { + // ifn't gradient more than digonal set it to be the diagonal + // (sakoe-chiba with radius 0) + if (c == DTWConstraint::kIkatura) + { + float big = std::max(mRows, mCols), smol = std::min(mRows, mCols); + + if (mParam <= big / smol) + { + mType = DTWConstraint::kSakoeChiba; + mParam = 0; + } + } + }; + + void iterate(std::function f) + { + index first, last; + + for (index r = 0; r < mRows; ++r) + { + first = firstCol(r); + last = lastCol(r); + + for (index c = first; c <= last; ++c) f(r, c); + } + }; + + private: + DTWConstraint mType; + index mRows, mCols; + float mParam; // mParam is either radius (SC) or gradient (Ik) + + inline static index rasterLineMinY(index x1, index y1, float dydx, index x) + { + return std::round(y1 + (x - x1) * dydx); + } + + inline static index rasterLineMinY(index x1, index y1, index x2, index y2, + index x) + { + float dy = y2 - y1, dx = x2 - x1; + return rasterLineMinY(x1, y1, dy / dx, x); + } + + inline static index rasterLineMaxY(index x1, index y1, float dydx, index x) + { + if (dydx > 1) + return rasterLineMinY(x1, y1, dydx, x + 1) - 1; + else + return rasterLineMinY(x1, y1, dydx, x); + } + + inline static index rasterLineMaxY(index x1, index y1, index x2, index y2, + index x) + { + float dy = y2 - y1, dx = x2 - x1; + return rasterLineMaxY(x1, y1, dy / dx, x); + } + + index firstCol(index row) + { + switch (mType) + { + case DTWConstraint::kUnconstrained: return 0; + + case DTWConstraint::kIkatura: { + index colNorm = rasterLineMinY(mRows - 1, mCols - 1, mParam, row); + index colInv = rasterLineMinY(0, 0, 1 / mParam, row); + + index col = std::max(colNorm, colInv); + + return col < 0 ? 0 : col > mCols - 1 ? mCols - 1 : col; + } + + case DTWConstraint::kSakoeChiba: { + index col = rasterLineMinY(mParam, -mParam, mRows - 1 + mParam, + mCols - 1 - mParam, row); + + return col < 0 ? 0 : col > mCols - 1 ? mCols - 1 : col; + } + } + + return 0; + }; + + index lastCol(index row) + { + switch (mType) + { + case DTWConstraint::kUnconstrained: return mCols - 1; + + case DTWConstraint::kIkatura: { + index colNorm = rasterLineMaxY(0, 0, mParam, row); + index colInv = rasterLineMaxY(mRows - 1, mCols - 1, 1 / mParam, row); + + index col = std::min(colNorm, colInv); + + return col < 0 ? 0 : col > mCols - 1 ? mCols - 1 : col; + } + + case DTWConstraint::kSakoeChiba: { + index col = rasterLineMaxY(-mParam, mParam, mRows - 1 - mParam, + mCols - 1 + mParam, row); + + return col < 0 ? 0 : col > mCols - 1 ? mCols - 1 : col; + } + } + + return mCols - 1; + }; + }; // struct Constraint +}; + +} // namespace algorithm +} // namespace fluid \ No newline at end of file diff --git a/include/clients/nrt/CommonResults.hpp b/include/clients/nrt/CommonResults.hpp index 6195fa6ba..7d719c07b 100644 --- a/include/clients/nrt/CommonResults.hpp +++ b/include/clients/nrt/CommonResults.hpp @@ -28,8 +28,10 @@ static const std::string LargeK{"k is too large"}; static const std::string SmallDim{"Number of dimensions is too small"}; static const std::string LargeDim{"Number of dimensions is too large"}; static const std::string EmptyDataSet{"DataSet is empty"}; +static const std::string EmptyDataSeries{"DataSeries is empty"}; static const std::string EmptyLabelSet{"LabelSet is empty"}; static const std::string NoDataSet{"DataSet does not exist"}; +static const std::string NoDataSeries{"DataSeries does not exist"}; static const std::string NoLabelSet{"LabelSet does not exist"}; static const std::string NoDataFitted{"No data fitted"}; static const std::string NotEnoughData{"Not enough data"}; diff --git a/include/clients/nrt/DTWClassifierClient.hpp b/include/clients/nrt/DTWClassifierClient.hpp new file mode 100644 index 000000000..4afd580f7 --- /dev/null +++ b/include/clients/nrt/DTWClassifierClient.hpp @@ -0,0 +1,259 @@ +/* +Part of the Fluid Corpus Manipulation Project (http://www.flucoma.org/) +Copyright University of Huddersfield. +Licensed under the BSD-3 License. +See license.md file in the project root for full license information. +This project has received funding from the European Research Council (ERC) +under the European Union’s Horizon 2020 research and innovation programme +(grant agreement No 725899). +*/ + +#pragma once + +#include "DataSeriesClient.hpp" +#include "DataSetClient.hpp" +#include "LabelSetClient.hpp" +#include "NRTClient.hpp" +#include "../../algorithms/public/DTW.hpp" +#include "../../data/FluidDataSeries.hpp" +#include "../../data/FluidDataSet.hpp" + +namespace fluid { +namespace client { +namespace dtwclassifier { + +struct DTWClassifierData +{ + algorithm::DTW dtw; + FluidDataSeries series{0}; + FluidDataSet labels{1}; + + index size() const { return series.size(); } + index dims() const { return series.dims(); } + void clear() + { + labels = FluidDataSet(1); + series = FluidDataSeries(0); + + dtw.clear(); + } + bool initialized() const { return dtw.initialized(); } +}; + +void to_json(nlohmann::json& j, const DTWClassifierData& data) +{ + j["labels"] = data.labels; + j["series"] = data.series; +} + +bool check_json(const nlohmann::json& j, const DTWClassifierData&) +{ + return fluid::check_json(j, {"labels", "series"}, + {JSONTypes::OBJECT, JSONTypes::OBJECT}); +} + +void from_json(const nlohmann::json& j, DTWClassifierData& data) +{ + data.series = j.at("series").get>(); + data.labels = j.at("labels").get>(); +} + +constexpr auto DTWClassifierParams = defineParameters( + StringParam>("name", "Name"), + LongParam("numNeighbours", "Number of Nearest Neighbours", 3, Min(1)), + EnumParam("constraint", "Constraint Type", 0, "Unconstrained", "Ikatura", + "Sakoe-Chiba"), + FloatParam("constraintParam", "Sakoe-Chiba radius or Ikatura max gradient", + 3, Min(0))); + +class DTWClassifierClient : public FluidBaseClient, + OfflineIn, + OfflineOut, + ModelObject, + public DataClient +{ + enum { kName, kNumNeighbors, kConstraint, kParam }; + +public: + using string = std::string; + using BufferPtr = std::shared_ptr; + using InputBufferPtr = std::shared_ptr; + using LabelSet = FluidDataSet; + using DataSet = FluidDataSet; + using StringVector = FluidTensor; + + using ParamDescType = decltype(DTWClassifierParams); + + using ParamSetViewType = ParameterSetView; + std::reference_wrapper mParams; + + void setParams(ParamSetViewType& p) { mParams = p; } + + template + auto& get() const + { + return mParams.get().template get(); + } + + static constexpr auto& getParameterDescriptors() + { + return DTWClassifierParams; + } + + DTWClassifierClient(ParamSetViewType& p, FluidContext&) : mParams(p) {} + + template + Result process(FluidContext&) + { + return {}; + } + + // not fitting anything, you just set the input series and output labels + MessageResult fit(InputDataSeriesClientRef dataSeriesClient, + InputLabelSetClientRef labelSetClient) + { + auto dataSeriesClientPtr = dataSeriesClient.get().lock(); + if (!dataSeriesClientPtr) return Error(NoDataSeries); + + auto labelSetPtr = labelSetClient.get().lock(); + if (!labelSetPtr) return Error(NoLabelSet); + + auto dataSeries = dataSeriesClientPtr->getDataSeries(); + if (dataSeries.size() == 0) return Error(EmptyDataSeries); + + auto labelSet = labelSetPtr->getLabelSet(); + if (labelSet.size() == 0) return Error(EmptyLabelSet); + + if (dataSeries.size() != labelSet.size()) return Error(SizesDontMatch); + + auto seriesIds = dataSeries.getIds(), labelIds = labelSet.getIds(); + + bool everySeriesHasALabel = std::is_permutation( + seriesIds.begin(), seriesIds.end(), labelIds.begin()); + + if (everySeriesHasALabel) + { + mAlgorithm.series = dataSeries; + mAlgorithm.labels = labelSet; + + return OK(); + } + else + return Error(EmptyLabel); + } + + MessageResult predictSeries(InputBufferPtr data) const + { + BufferAdaptor::ReadAccess buf = data.get(); + RealMatrix series(buf.numFrames(), buf.numChans()); + + if (buf.numChans() < mAlgorithm.dims()) + return Error(WrongPointSize); + + series <<= buf.allFrames().transpose(); + + return kNearestModeLabel(series); + } + + MessageResult predict(InputDataSeriesClientRef source, + LabelSetClientRef dest) const + { + + auto sourcePtr = source.get().lock(); + if (!sourcePtr) return Error(NoDataSeries); + + auto destPtr = dest.get().lock(); + if (!destPtr) return Error(NoLabelSet); + + auto dataSeries = sourcePtr->getDataSeries(); + if (dataSeries.size() == 0) return Error(EmptyDataSeries); + + if (dataSeries.pointSize() != mAlgorithm.dims()) + return Error(WrongPointSize); + + if (mAlgorithm.size() == 0) return Error(NoDataFitted); + + FluidTensorView ids = dataSeries.getIds(); + LabelSet result(1); + + for (index i = 0; i < dataSeries.size(); i++) + { + StringVector label = {kNearestModeLabel(dataSeries.getSeries(ids[i]))}; + result.add(ids(i), label); + } + + destPtr->setLabelSet(result); + return OK(); + } + + + static auto getMessageDescriptors() + { + return defineMessages( + makeMessage("fit", &DTWClassifierClient::fit), + makeMessage("predict", &DTWClassifierClient::predict), + makeMessage("predictSeries", &DTWClassifierClient::predictSeries), + makeMessage("clear", &DTWClassifierClient::clear), + makeMessage("size", &DTWClassifierClient::size), + makeMessage("load", &DTWClassifierClient::load), + makeMessage("dump", &DTWClassifierClient::dump), + makeMessage("write", &DTWClassifierClient::write), + makeMessage("read", &DTWClassifierClient::read)); + } + +private: + MessageResult kNearestModeLabel(InputRealMatrixView series) const + { + index k = get(); + if (k < 1) return Error(SmallK); + if (k > mAlgorithm.size()) return Error(LargeK); + + rt::vector ds = mAlgorithm.series.getData(); + + if (series.cols() < mAlgorithm.dims()) return Error(WrongPointSize); + + rt::vector indices(asUnsigned(mAlgorithm.size())); + rt::vector distances(asUnsigned(mAlgorithm.size())); + + std::iota(indices.begin(), indices.end(), 0); + + algorithm::DTWConstraint constraint = + (algorithm::DTWConstraint) get(); + + std::transform( + indices.begin(), indices.end(), distances.begin(), + [&series, &ds, &constraint, this](index i) { + double dist = + mAlgorithm.dtw.process(series, ds[i], constraint, get()); + return std::max(std::numeric_limits::epsilon(), dist); + }); + + std::sort(indices.begin(), indices.end(), [&distances](index a, index b) { + return distances[asUnsigned(a)] < distances[asUnsigned(b)]; + }); + + rt::unordered_map labelCount; + FluidTensorView labels = mAlgorithm.labels.getData(); + + std::for_each(indices.begin(), indices.begin() + get(), + [&](index& i) { + return labelCount[labels(i, 0)] += 1.0 / distances[i]; + }); + + auto result = std::max_element( + labelCount.begin(), labelCount.end(), + [](auto& left, auto& right) { return left.second < right.second; }); + + return result->first; + } +}; + +using DTWClassifierRef = SharedClientRef; + +} // namespace dtwclassifier + +using NRTThreadedDTWClassifierClient = + NRTThreadingAdaptor; + +} // namespace client +} // namespace fluid diff --git a/include/clients/nrt/DTWClient.hpp b/include/clients/nrt/DTWClient.hpp new file mode 100644 index 000000000..b34c84dcb --- /dev/null +++ b/include/clients/nrt/DTWClient.hpp @@ -0,0 +1,135 @@ +/* +Part of the Fluid Corpus Manipulation Project (http://www.flucoma.org/) +Copyright University of Huddersfield. +Licensed under the BSD-3 License. +See license.md file in the project root for full license information. +This project has received funding from the European Research Council (ERC) +under the European Union’s Horizon 2020 research and innovation programme +(grant agreement No 725899). +*/ + +#pragma once + +#include "DataClient.hpp" +#include "DataSeriesClient.hpp" +#include "DataSetClient.hpp" +#include "NRTClient.hpp" +#include "../../algorithms/public/DTW.hpp" +#include + +namespace fluid { +namespace client { +namespace dtw { + +constexpr auto DTWParams = defineParameters( + StringParam>("name", "Name"), + EnumParam("constraint", "Constraint Type", 0, "Unconstrained", "Ikatura", + "Sakoe-Chiba"), + FloatParam("constraintParam", "Sakoe-Chiba radius or Ikatura max gradient", + 3, Min(0))); + +class DTWClient : public FluidBaseClient, + OfflineIn, + OfflineOut, + ModelObject, + public DataClient +{ + enum { kName, kConstraint, kParam }; + +public: + using string = std::string; + using BufferPtr = std::shared_ptr; + using InputBufferPtr = std::shared_ptr; + using StringVector = FluidTensor; + + using ParamDescType = decltype(DTWParams); + using ParamSetViewType = ParameterSetView; + using ParamValues = typename ParamSetViewType::ValueTuple; + + std::reference_wrapper mParams; + + void setParams(ParamSetViewType& p) { mParams = p; } + + template + auto& get() const + { + return mParams.get().template get(); + } + + static constexpr auto& getParameterDescriptors() { return DTWParams; } + + DTWClient(ParamSetViewType& p, FluidContext&) : mParams(p) + { + controlChannelsIn(1); + controlChannelsOut({1, 1}); + } + + template + Result process(FluidContext&) + { + return {}; + } + + MessageResult cost(InputDataSeriesClientRef dataseriesClient, + string id1, string id2) + { + auto dataseriesClientPtr = dataseriesClient.get().lock(); + if (!dataseriesClientPtr) return Error(NoDataSeries); + + auto srcDataSeries = dataseriesClientPtr->getDataSeries(); + if (srcDataSeries.size() == 0) return Error(EmptyDataSeries); + + index i1 = srcDataSeries.getIndex(id1), i2 = srcDataSeries.getIndex(id2); + + if (i1 < 0 || i2 < 0) return Error(PointNotFound); + + InputRealMatrixView series1 = srcDataSeries.getSeries(id1), + series2 = srcDataSeries.getSeries(id2); + + algorithm::DTWConstraint constraint = + (algorithm::DTWConstraint) get(); + + return mAlgorithm.process(series1, series2, constraint, get()); + } + + MessageResult bufCost(InputBufferPtr data1, InputBufferPtr data2) + { + if (!data1 || !data2) return Error(NoBuffer); + + BufferAdaptor::ReadAccess buf1(data1.get()), buf2(data2.get()); + + if (!buf1.exists() || !buf2.exists()) return Error(InvalidBuffer); + if (buf1.numChans() != buf2.numChans()) + return Error(WrongPointSize); + if (buf1.numFrames() == 0 || buf2.numFrames() == 0) + return Error(EmptyBuffer); + + RealMatrix buf1frames(buf1.numFrames(), buf1.numChans()), + buf2frames(buf2.numFrames(), buf2.numChans()); + + buf1frames <<= buf1.allFrames().transpose(); + buf2frames <<= buf2.allFrames().transpose(); + + algorithm::DTWConstraint constraint = + (algorithm::DTWConstraint) get(); + + return mAlgorithm.process(buf1frames, buf2frames, constraint, + get()); + } + + static auto getMessageDescriptors() + { + return defineMessages(makeMessage("cost", &DTWClient::cost), + makeMessage("bufCost", &DTWClient::bufCost)); + } +}; + +using DTWRef = SharedClientRef; + +} // namespace dtw + +using NRTThreadedDTWClient = + NRTThreadingAdaptor; + +} // namespace client +} // namespace fluid diff --git a/include/clients/nrt/DTWRegressorClient.hpp b/include/clients/nrt/DTWRegressorClient.hpp new file mode 100644 index 000000000..7d257e3a4 --- /dev/null +++ b/include/clients/nrt/DTWRegressorClient.hpp @@ -0,0 +1,293 @@ +/* +Part of the Fluid Corpus Manipulation Project (http://www.flucoma.org/) +Copyright University of Huddersfield. +Licensed under the BSD-3 License. +See license.md file in the project root for full license information. +This project has received funding from the European Research Council (ERC) +under the European Union’s Horizon 2020 research and innovation programme +(grant agreement No 725899). +*/ + +#pragma once + +#include "DataSeriesClient.hpp" +#include "DataSetClient.hpp" +#include "NRTClient.hpp" +#include "../../algorithms/public/DTW.hpp" +#include "../../algorithms/util/FluidEigenMappings.hpp" +#include "../../data/FluidDataSeries.hpp" +#include "../../data/FluidDataSet.hpp" +#include "../../data/FluidMemory.hpp" +#include "../../data/FluidTensor.hpp" +#include "../../data/TensorTypes.hpp" + +namespace fluid { +namespace client { +namespace dtwclassifier { + +struct DTWRegressorData +{ + algorithm::DTW dtw; + FluidDataSeries series{0}; + FluidDataSet mappings{0}; + + index size() const { return series.size(); } + index dims() const { return series.dims(); } + + void clear() + { + mappings = FluidDataSet(0); + series = FluidDataSeries(0); + + dtw.clear(); + } + bool initialized() const { return dtw.initialized(); } +}; + +void to_json(nlohmann::json& j, const DTWRegressorData& data) +{ + j["mappings"] = data.mappings; + j["series"] = data.series; +} + +bool check_json(const nlohmann::json& j, const DTWRegressorData&) +{ + return fluid::check_json(j, {"mappings", "series"}, + {JSONTypes::OBJECT, JSONTypes::OBJECT}); +} + +void from_json(const nlohmann::json& j, DTWRegressorData& data) +{ + data.series = j.at("series").get>(); + data.mappings = j.at("mappings").get>(); +} + +constexpr auto DTWRegressorParams = defineParameters( + StringParam>("name", "Name"), + LongParam("numNeighbours", "Number of Nearest Neighbours", 3, Min(1)), + EnumParam("constraint", "Constraint Type", 0, "Unconstrained", "Ikatura", + "Sakoe-Chiba"), + FloatParam("constraintParam", "Sakoe-Chiba radius or Ikatura max gradient", + 3, Min(0))); + +class DTWRegressorClient : public FluidBaseClient, + OfflineIn, + OfflineOut, + ModelObject, + public DataClient +{ + enum { kName, kNumNeighbors, kConstraint, kParam }; + +public: + using string = std::string; + using BufferPtr = std::shared_ptr; + using InputBufferPtr = std::shared_ptr; + using LabelSet = FluidDataSet; + using DataSet = FluidDataSet; + using StringVector = FluidTensor; + + using ParamDescType = decltype(DTWRegressorParams); + + using ParamSetViewType = ParameterSetView; + std::reference_wrapper mParams; + + void setParams(ParamSetViewType& p) { mParams = p; } + + template + auto& get() const + { + return mParams.get().template get(); + } + + static constexpr auto& getParameterDescriptors() + { + return DTWRegressorParams; + } + + DTWRegressorClient(ParamSetViewType& p, FluidContext&) : mParams(p) {} + + template + Result process(FluidContext&) + { + return {}; + } + + // not fitting anything, you just set the input series and output labels + MessageResult fit(InputDataSeriesClientRef dataSeriesClient, + InputDataSetClientRef dataSetClient) + { + auto dataSeriesClientPtr = dataSeriesClient.get().lock(); + if (!dataSeriesClientPtr) return Error(NoDataSeries); + + auto dataSetPtr = dataSetClient.get().lock(); + if (!dataSetPtr) return Error(NoDataSet); + + auto dataSeries = dataSeriesClientPtr->getDataSeries(); + if (dataSeries.size() == 0) return Error(EmptyDataSeries); + + auto dataSet = dataSetPtr->getDataSet(); + if (dataSet.size() == 0) return Error(EmptyDataSet); + + if (dataSeries.size() != dataSet.size()) return Error(SizesDontMatch); + + auto seriesIds = dataSeries.getIds(), mappingIds = dataSet.getIds(); + + bool everySeriesHasALabel = std::is_permutation( + seriesIds.begin(), seriesIds.end(), mappingIds.begin()); + + if (everySeriesHasALabel) + { + mAlgorithm.series = dataSeries; + mAlgorithm.mappings = dataSet; + + return OK(); + } + else + return Error(PointNotFound); + } + + MessageResult predictSeries(InputBufferPtr in, BufferPtr out) const + { + if (!in || !out) return Error(NoBuffer); + + BufferAdaptor::ReadAccess inBuf = in.get(); + BufferAdaptor::Access outBuf = out.get(); + + if (!inBuf.exists()) return Error(InvalidBuffer); + if (!outBuf.exists()) return Error(InvalidBuffer); + + if (inBuf.numChans() < mAlgorithm.series.dims()) + return Error(WrongPointSize); + + Result resizeResult = + outBuf.resize(mAlgorithm.mappings.dims(), 1, inBuf.sampleRate()); + if (!resizeResult.ok()) return Error(BufferAlloc); + + RealMatrix series(inBuf.numFrames(), inBuf.numChans()); + series <<= inBuf.allFrames().transpose(); + + MessageResult result = kNearestWeightedSum(series); + if (result.ok()) + outBuf.samps(0, result.value().size(), 0) <<= result.value(); + else + return Error(result.message()); + + return OK(); + } + + MessageResult predict(InputDataSeriesClientRef source, + DataSetClientRef dest) const + { + + auto sourcePtr = source.get().lock(); + if (!sourcePtr) return Error(NoDataSeries); + + auto destPtr = dest.get().lock(); + if (!destPtr) return Error(NoLabelSet); + + auto dataSeries = sourcePtr->getDataSeries(); + if (dataSeries.size() == 0) return Error(EmptyDataSeries); + + if (dataSeries.pointSize() != mAlgorithm.series.dims()) + return Error(WrongPointSize); + + if (mAlgorithm.size() == 0) return Error(NoDataFitted); + + FluidTensorView ids = dataSeries.getIds(); + DataSet result(mAlgorithm.mappings.dims()); + + for (index i = 0; i < dataSeries.size(); i++) + { + MessageResult point = + kNearestWeightedSum(dataSeries.getSeries(ids[i])); + + if (point.ok()) + { + RealVector pred = point; + result.add(ids(i), pred); + } + else + return MessageResult{Result::Status::kError, point.message()}; + } + + destPtr->setDataSet(result); + return OK(); + } + + + static auto getMessageDescriptors() + { + return defineMessages( + makeMessage("fit", &DTWRegressorClient::fit), + makeMessage("predict", &DTWRegressorClient::predict), + makeMessage("predictSeries", &DTWRegressorClient::predictSeries), + makeMessage("clear", &DTWRegressorClient::clear), + makeMessage("size", &DTWRegressorClient::size), + makeMessage("load", &DTWRegressorClient::load), + makeMessage("dump", &DTWRegressorClient::dump), + makeMessage("write", &DTWRegressorClient::write), + makeMessage("read", &DTWRegressorClient::read)); + } + +private: + MessageResult + kNearestWeightedSum(InputRealMatrixView series, + Allocator& alloc = FluidDefaultAllocator()) const + { + using namespace algorithm::_impl; + + index k = get(); + if (k < 1) return Error(SmallK); + if (k > mAlgorithm.size()) return Error(LargeK); + + rt::vector ds = mAlgorithm.series.getData(); + + if (series.cols() < mAlgorithm.series.dims()) + return Error(WrongPointSize); + + rt::vector indices(asUnsigned(mAlgorithm.size())); + rt::vector distances(asUnsigned(mAlgorithm.size())); + + std::iota(indices.begin(), indices.end(), 0); + + algorithm::DTWConstraint constraint = + (algorithm::DTWConstraint) get(); + + std::transform( + indices.begin(), indices.end(), distances.begin(), + [&series, &ds, &constraint, this](index i) { + double dist = + mAlgorithm.dtw.process(series, ds[i], constraint, get()); + return std::max(std::numeric_limits::epsilon(), dist); + }); + + std::sort(indices.begin(), indices.end(), [&distances](index a, index b) { + return distances[asUnsigned(a)] < distances[asUnsigned(b)]; + }); + + ScopedEigenMap result(mAlgorithm.mappings.dims(), alloc); + InputRealMatrixView mappings = mAlgorithm.mappings.getData(); + + double totalWeight = 0.0; + std::for_each(indices.begin(), indices.begin() + get(), + [&](index& i) { + double weight = 1.0 / distances[i]; + + totalWeight += weight; + result += asEigen(mappings.row(i)) * weight; + }); + + result.noalias() = result * (1 / totalWeight); + return RealVector(asFluid(result)); + } +}; + +using DTWRegressorRef = SharedClientRef; + +} // namespace dtwclassifier + +using NRTThreadedDTWRegressorClient = + NRTThreadingAdaptor; + +} // namespace client +} // namespace fluid diff --git a/include/clients/nrt/DataSeriesClient.hpp b/include/clients/nrt/DataSeriesClient.hpp new file mode 100644 index 000000000..97128733c --- /dev/null +++ b/include/clients/nrt/DataSeriesClient.hpp @@ -0,0 +1,459 @@ +/* +Part of the Fluid Corpus Manipulation Project (http://www.flucoma.org/) +Copyright University of Huddersfield. +Licensed under the BSD-3 License. +See license.md file in the project root for full license information. +This project has received funding from the European Research Council (ERC) +under the European Union’s Horizon 2020 research and innovation programme +(grant agreement No 725899). +*/ + +#pragma once +#include "DataClient.hpp" +#include "DataSetClient.hpp" +#include "LabelSetClient.hpp" +#include "NRTClient.hpp" +#include "../common/SharedClientUtils.hpp" +#include "../../algorithms/public/DTW.hpp" +#include "../../algorithms/public/DataSetIdSequence.hpp" +#include "../../data/FluidDataSeries.hpp" +#include +#include + +namespace fluid { +namespace client { +namespace dataseries { + +enum { kName }; + +constexpr auto DataSeriesParams = defineParameters( + StringParam>("name", "Name of the DataSeries")); + +class DataSeriesClient + : public FluidBaseClient, + OfflineIn, + OfflineOut, + public DataClient> +{ +public: + using string = std::string; + using BufferPtr = std::shared_ptr; + using InputBufferPtr = std::shared_ptr; + using DataSeries = FluidDataSeries; + using DataSet = FluidDataSet; + using LabelSet = FluidDataSet; + + template + Result process(FluidContext&) + { + return {}; + } + + using ParamDescType = decltype(DataSeriesParams); + + using ParamSetViewType = ParameterSetView; + std::reference_wrapper mParams; + + void setParams(ParamSetViewType& p) { mParams = p; } + + template + auto& get() const + { + return mParams.get().template get(); + } + + static constexpr auto& getParameterDescriptors() { return DataSeriesParams; } + + DataSeriesClient(ParamSetViewType& p, FluidContext&) : mParams(p) {} + + MessageResult addFrame(string id, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numFrames() == 0) return Error(EmptyBuffer); + + if (mAlgorithm.size() == 0) + { + if (mAlgorithm.dims() != buf.numFrames()) + mAlgorithm = DataSeries(buf.numFrames()); + } + else if (buf.numFrames() != mAlgorithm.dims()) + { + return Error(WrongPointSize); + } + + RealVector frame(buf.numFrames()); + frame <<= buf.samps(0, mAlgorithm.dims(), 0); + + mAlgorithm.addFrame(id, frame); + + return OK(); + } + + MessageResult addSeries(string id, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numFrames() == 0) return Error(EmptyBuffer); + + if (mAlgorithm.size() == 0) + { + if (mAlgorithm.dims() != buf.numChans()) + mAlgorithm = DataSeries(buf.numChans()); + } + else if (buf.numChans() != mAlgorithm.dims()) + { + return Error(WrongPointSize); + } + + RealMatrix series(buf.numFrames(), buf.numChans()); + series <<= buf.allFrames().transpose(); + + return mAlgorithm.addSeries(id, series) ? OK() : Error(DuplicateIdentifier); + } + + MessageResult getFrame(string id, index time, BufferPtr data) const + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::Access buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + + Result resizeResult = buf.resize(mAlgorithm.dims(), 1, buf.sampleRate()); + if (!resizeResult.ok()) + return {resizeResult.status(), resizeResult.message()}; + + RealVector point(mAlgorithm.dims()); + point <<= buf.samps(0, mAlgorithm.dims(), 0); + + bool result = mAlgorithm.getFrame(id, time, point); + if (result) + { + buf.samps(0, mAlgorithm.dims(), 0) <<= point; + return OK(); + } + else { return Error(PointNotFound); } + } + + MessageResult getSeries(string id, BufferPtr data) const + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::Access buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + + index numFrames = mAlgorithm.getNumFrames(id); + if (numFrames < 0) return Error(PointNotFound); + + Result resizeResult = + buf.resize(numFrames, mAlgorithm.dims(), buf.sampleRate()); + if (!resizeResult.ok()) + return {resizeResult.status(), resizeResult.message()}; + + RealMatrix point(numFrames, mAlgorithm.dims()); + bool result = mAlgorithm.getSeries(id, point); + if (result) + { + buf.allFrames() <<= point.transpose(); + return OK(); + } + else { return Error(PointNotFound); } + } + + MessageResult updateFrame(string id, index time, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numFrames() < mAlgorithm.dims()) return Error(WrongPointSize); + + RealVector frame(buf.numFrames()); + frame <<= buf.samps(0, mAlgorithm.dims(), 0); + + return mAlgorithm.updateFrame(id, time, frame) ? OK() + : Error(PointNotFound); + } + + MessageResult updateSeries(string id, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numChans() < mAlgorithm.dims()) return Error(WrongPointSize); + + RealMatrix series(buf.numFrames(), buf.numChans()); + series <<= buf.allFrames().transpose(); + + return mAlgorithm.updateSeries(id, series) ? OK() : Error(PointNotFound); + } + + MessageResult setFrame(string id, index time, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + { // restrict buffer lock to this scope in case addPoint is called + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numFrames() < mAlgorithm.dims()) return Error(WrongPointSize); + + RealVector frame(buf.numFrames()); + frame <<= buf.samps(0, mAlgorithm.dims(), 0); + + bool result = mAlgorithm.updateFrame(id, time, frame); + if (result) return OK(); + } + + return addFrame(id, data); + } + + MessageResult setSeries(string id, InputBufferPtr data) + { + if (!data) return Error(NoBuffer); + + { // restrict buffer lock to this scope in case addPoint is called + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error(InvalidBuffer); + if (buf.numChans() < mAlgorithm.dims()) return Error(WrongPointSize); + + RealMatrix series(buf.numFrames(), buf.numChans()); + series <<= buf.allFrames().transpose(); + + bool result = mAlgorithm.updateSeries(id, series); + if (result) return OK(); + } + + return addSeries(id, data); + } + + MessageResult deleteFrame(string id, index time) + { + return mAlgorithm.removeFrame(id, time) ? OK() : Error(PointNotFound); + } + + MessageResult deleteSeries(string id) + { + return mAlgorithm.removeSeries(id) ? OK() : Error(PointNotFound); + } + + MessageResult + merge(SharedClientRef dataseriesClient, + bool overwrite) + { + auto dataseriesClientPtr = dataseriesClient.get().lock(); + if (!dataseriesClientPtr) return Error(NoDataSeries); + + auto srcDataSeries = dataseriesClientPtr->getDataSeries(); + if (srcDataSeries.size() == 0) return Error(EmptyDataSeries); + if (srcDataSeries.pointSize() != mAlgorithm.pointSize()) + return Error(WrongPointSize); + + auto ids = srcDataSeries.getIds(); + + for (index i = 0; i < srcDataSeries.size(); i++) + { + InputRealMatrixView series = srcDataSeries.getSeries(ids(i)); + bool added = mAlgorithm.addSeries(ids(i), series); + if (!added && overwrite) mAlgorithm.updateSeries(ids(i), series); + } + + return OK(); + } + + MessageResult getDataSet(index time, DataSetClientRef dest) const + { + auto destPtr = dest.get().lock(); + if (!destPtr) return Error(NoDataSet); + destPtr->setDataSet(getSliceDataSet(time)); + + if (destPtr->size() == 0) return Error(EmptyDataSet); + + return OK(); + } + + MessageResult getIds(LabelSetClientRef dest) + { + auto destPtr = dest.get().lock(); + if (!destPtr) return Error(NoLabelSet); + destPtr->setLabelSet(getIdsLabelSet()); + + return OK(); + } + + MessageResult> kNearest(InputBufferPtr data, + index nNeighbours) const + { + // check for nNeighbours > 0 and < size of DS + if (mAlgorithm.size() == 0) + return Error>(EmptyDataSeries); + if (nNeighbours > mAlgorithm.size()) + return Error>(LargeK); + if (nNeighbours <= 0) return Error>(SmallK); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error>(InvalidBuffer); + if (buf.numChans() < mAlgorithm.dims()) + return Error>(WrongPointSize); + + FluidTensor series(buf.allFrames().transpose()); + + rt::vector indices(asUnsigned(mAlgorithm.size())); + rt::vector distances(asUnsigned(mAlgorithm.size())); + + std::iota(indices.begin(), indices.end(), 0); + + auto ds = mAlgorithm.getData(); + + std::transform( + indices.begin(), indices.end(), distances.begin(), + [&series, &ds, this](index i) { return distance(series, ds[i], 2); }); + + std::sort(indices.begin(), indices.end(), [&distances](index a, index b) { + return distances[asUnsigned(a)] < distances[asUnsigned(b)]; + }); + + FluidTensor labels(nNeighbours); + + std::transform( + indices.begin(), indices.begin() + nNeighbours, labels.begin(), + [this](index i) { + std::string const& id = mAlgorithm.getIds()[i]; + return rt::string{id, 0, id.size(), FluidDefaultAllocator()}; + }); + + return labels; + } + + MessageResult> kNearestDist(InputBufferPtr data, + index nNeighbours) const + { + // check for nNeighbours > 0 and < size of DS + if (mAlgorithm.size() == 0) + return Error>(EmptyDataSeries); + if (nNeighbours > mAlgorithm.size()) + return Error>(LargeK); + if (nNeighbours <= 0) return Error>(SmallK); + + BufferAdaptor::ReadAccess buf(data.get()); + if (!buf.exists()) return Error>(InvalidBuffer); + if (buf.numChans() < mAlgorithm.dims()) + return Error>(WrongPointSize); + + FluidTensor series(buf.allFrames().transpose()); + + rt::vector indices(asUnsigned(mAlgorithm.size())); + rt::vector distances(asUnsigned(mAlgorithm.size())); + + std::iota(indices.begin(), indices.end(), 0); + + auto ds = mAlgorithm.getData(); + + std::transform( + indices.begin(), indices.end(), distances.begin(), + [&series, &ds, this](index i) { return distance(series, ds[i], 2); }); + + std::sort(indices.begin(), indices.end(), [&distances](index a, index b) { + return distances[asUnsigned(a)] < distances[asUnsigned(b)]; + }); + + FluidTensor labels(nNeighbours); + + std::transform(indices.begin(), indices.begin() + nNeighbours, + labels.begin(), + [&distances](index i) { return distances[i]; }); + + return labels; + } + + MessageResult clear() + { + mAlgorithm = DataSeries(0); + return OK(); + } + + MessageResult print() + { + return "DataSeries " + std::string(get()) + ": " + + mAlgorithm.print(); + } + + const DataSeries getDataSeries() const { return mAlgorithm; } + void setDataSeries(DataSeries ds) { mAlgorithm = ds; } + + static auto getMessageDescriptors() + { + return defineMessages( + makeMessage("addFrame", &DataSeriesClient::addFrame), + makeMessage("addSeries", &DataSeriesClient::addSeries), + makeMessage("getFrame", &DataSeriesClient::getFrame), + makeMessage("getSeries", &DataSeriesClient::getSeries), + makeMessage("setFrame", &DataSeriesClient::setFrame), + makeMessage("setSeries", &DataSeriesClient::setSeries), + makeMessage("updateFrame", &DataSeriesClient::updateFrame), + makeMessage("updateSeries", &DataSeriesClient::updateSeries), + makeMessage("deleteFrame", &DataSeriesClient::deleteFrame), + makeMessage("deleteSeries", &DataSeriesClient::deleteSeries), + makeMessage("merge", &DataSeriesClient::merge), + makeMessage("dump", &DataSeriesClient::dump), + makeMessage("load", &DataSeriesClient::load), + makeMessage("print", &DataSeriesClient::print), + makeMessage("size", &DataSeriesClient::size), + makeMessage("cols", &DataSeriesClient::dims), + makeMessage("clear", &DataSeriesClient::clear), + makeMessage("write", &DataSeriesClient::write), + makeMessage("read", &DataSeriesClient::read), + makeMessage("kNearest", &DataSeriesClient::kNearest), + makeMessage("kNearestDist", &DataSeriesClient::kNearestDist), + makeMessage("getIds", &DataSeriesClient::getIds), + makeMessage("getDataSet", &DataSeriesClient::getDataSet)); + } + +private: + LabelSet getIdsLabelSet() + { + algorithm::DataSetIdSequence seq("", 0, 0); + FluidTensor newIds(mAlgorithm.size()); + FluidTensor labels(mAlgorithm.size(), 1); + labels.col(0) <<= mAlgorithm.getIds(); + seq.generate(newIds); + return LabelSet(newIds, labels); + }; + + DataSet getSliceDataSet(index time) const + { + DataSet ds(mAlgorithm.dims()); + decltype(mAlgorithm)::FrameType frame(mAlgorithm.dims()); + + for (auto id : mAlgorithm.getIds()) + { + bool ret = mAlgorithm.getFrame(id, time, frame); + if (ret) ds.add(id, frame); + } + + return ds; + } + + double distance(InputRealMatrixView x1, InputRealMatrixView x2, index p) const + { + algorithm::DTW dtw; + return dtw.process(x1, x2, algorithm::DTWConstraint::kSakoeChiba, + std::min(x1.size(), x2.size()) / 4); + } +}; + +} // namespace dataseries + +using DataSeriesClientRef = SharedClientRef; +using InputDataSeriesClientRef = + SharedClientRef; + +using NRTThreadedDataSeriesClient = + NRTThreadingAdaptor; + +} // namespace client +} // namespace fluid diff --git a/include/data/FluidDataSeries.hpp b/include/data/FluidDataSeries.hpp new file mode 100644 index 000000000..a4baefc44 --- /dev/null +++ b/include/data/FluidDataSeries.hpp @@ -0,0 +1,370 @@ +#pragma once + +#include "data/FluidIndex.hpp" +#include "data/FluidTensor.hpp" +#include "data/TensorTypes.hpp" +#include +#include +#include +#include + + +// TODO:: REMOVE TEMPLATE THINGY AND TURN IT INTO CONSISTNE ALLOCATION OF MATRIX +namespace fluid { + +template +class FluidDataSeries +{ + +public: + using FrameType = FluidTensor; + + explicit FluidDataSeries() = default; + ~FluidDataSeries() = default; + + // Construct from list of dimensions for each data point, + // e.g. FluidDataSet(2, 3) is a dataset of 2x3 tensors + template ()>> + FluidDataSeries(Dims... dims) + : mData(0, FluidTensor(0, dims...)), mDim(dims...) + { + static_assert(sizeof...(dims) == N, "Number of dimensions doesn't match"); + } + + // Construct from existing tensors of ids and data points + FluidDataSeries(FluidTensorView ids, + rt::vector> points) + : mIds(ids), mData(points) + { + initFromData(); + } + + // Construct from existing tensors of ids and data points + // (from convertible type for data, typically float -> double) + template + FluidDataSeries(FluidTensorView ids, + rt::vector> points, + std::enable_if_t::value>* = nullptr) + : mIds(ids), mData(points) + { + initFromData(); + } + + // Resize data point layout (if empty) + template ()>> + bool resize(Dims... dims) + { + static_assert(sizeof...(dims) == N, "Number of dimensions doesn't match"); + if (size() == 0) + { + mData = rt::vector>(); + mDim = FluidTensorSlice(dims...); + return true; + } + else { return false; } + } + + bool addFrame(idType const& id, FluidTensorView frame) + { + assert(sameExtents(mDim, frame.descriptor())); + + auto pos = mIndex.find(id); + if (pos == mIndex.end()) + { + FluidTensorView newPoint(frame); + return addSeries(id, newPoint); + } + + mData[pos->second].resizeDim(0, 1); + mData[pos->second].row(mData[pos->second].rows() - 1) <<= frame; + + return true; + } + + bool addSeries(idType const& id, + FluidTensorView series) + { + auto result = mIndex.insert({id, mData.size()}); + if (!result.second) return false; + + mData.emplace_back(series); + + mIds.resizeDim(0, 1); + mIds(mIds.rows() - 1) = id; + + return true; + } + + bool getFrame(idType const& id, index time, + FluidTensorView frame) const + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) return false; + + if (time >= mData[pos->second].rows()) return false; + if (time < -mData[pos->second].rows()) return false; + + time += time < 0 ? mData[pos->second].rows() : 0; + + frame <<= mData[pos->second].row(time); + + return true; + } + + FluidTensorView getFrame(idType const& id, + index time) const + { + auto pos = mIndex.find(id); + if (pos != mIndex.end()) + { + assert(time < mData[pos->second].rows() || + time >= -mData[pos->second].rows()); + + time += time < 0 ? mData[pos->second].rows() : 0; + return mData[pos->second].row(time); + } + else { return FluidTensorView{nullptr, 0, 0}; } + } + + bool getSeries(idType const& id, + FluidTensorView series) const + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) return false; + + series <<= mData[pos->second]; + + return true; + } + + FluidTensorView getSeries(idType const& id) const + { + auto pos = mIndex.find(id); + return pos != mIndex.end() + ? mData[pos->second] + : FluidTensorView{nullptr, 0, 0, 0}; + } + + bool updateFrame(idType const& id, index time, + FluidTensorView frame) + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) return false; + + if (time >= mData[pos->second].rows()) return false; + if (time < -mData[pos->second].rows()) return false; + + time += time < 0 ? mData[pos->second].rows() : 0; + mData[pos->second].row(time) <<= frame; + + return true; + } + + bool updateSeries(idType const& id, + FluidTensorView series) + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) + return false; + else + { + mData[pos->second].resizeDim(0, + series.rows() - mData[pos->second].rows()); + mData[pos->second] <<= series; + } + + return true; + } + + bool removeFrame(idType const& id, index time) + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) return false; + + if (time >= mData[pos->second].rows()) return false; + if (time < -mData[pos->second].rows()) return false; + + time += time < 0 ? mData[pos->second].rows() : 0; + + if (mData[pos->second].rows() == 1) + return removeSeries(id); + else + mData[pos->second].deleteRow(time); + + return true; + } + + bool removeSeries(idType const& id) + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) return false; + + mData.erase(mData.begin() + pos->second); + mIds.deleteRow(pos->second); + mIndex.erase(id); + + for (auto& point : mIndex) + { + if (point.second > pos->second) point.second--; + } + + return true; + } + + index getIndex(idType const& id) const + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) + return -1; + else + return pos->second; + } + + index getNumFrames(idType const& id) const + { + auto pos = mIndex.find(id); + if (pos == mIndex.end()) + return -1; + else + return mData[pos->second].rows(); + } + + rt::vector> getData() + { + rt::vector> viewVec(mData.size()); + + // hacky fix to force conversion of vector of tensors to vector of views of + // mData doesn't actually copy anything, it uses the FluidTensor ctor of + // FluidTensorView which creates a view/ref, so ends up creating what we + // want + std::copy(mData.begin(), mData.end(), std::back_inserter(viewVec)); + + return viewVec; + } + + const rt::vector> getData() const + { + rt::vector> viewVec; + + // hacky fix to force conversion of vector to views of mData + // doesn't actually copy anything, it uses the FluidTensor ctor of + // FluidTensorView which creates a view/ref, so ends up creating what we + // want + std::copy(mData.cbegin(), mData.cend(), std::back_inserter(viewVec)); + + return viewVec; + } + + FluidTensorView getIds() { return mIds; } + FluidTensorView getIds() const { return mIds; } + + index pointSize() const { return mDim.size; } + index dims() const { return mDim.size; } + index size() const { return mIds.size(); } + bool initialized() const { return (size() > 0); } + + std::string printFrame(FluidTensorView frame, + index maxCols) const + { + using namespace std; + ostringstream result; + + if (frame.size() < maxCols) + { + for (index c = 0; c < frame.size(); c++) + result << setw(10) << setprecision(5) << frame(c); + } + else + { + for (index c = 0; c < maxCols / 2; c++) + result << setw(10) << setprecision(5) << frame(c); + + result << setw(10) << "..."; + + for (index c = maxCols / 2; c > 0; c--) + result << setw(10) << setprecision(5) << frame(frame.size() - c); + } + + return result.str(); + } + + std::string printSeries(FluidTensorView series, + index maxFrames, index maxCols) const + { + using namespace std; + ostringstream result; + + if (series.rows() < maxFrames) + { + for (index t = 0; t < series.rows(); t++) + result << setw(10) << t << ": " + << printFrame(series.row(t), maxCols) << endl; + } + else + { + for (index t = 0; t < maxFrames / 2; t++) + result << setw(10) << t << ": " + << printFrame(series.row(t), maxCols) << endl; + + result << setw(10) << "..." << std::endl; + + for (index t = maxFrames / 2; t > 0; t--) + { + index rownum = series.rows() - t; + result << setw(10) << (rownum) << ": " + << printFrame(series.row(rownum), maxCols) << endl; + } + } + + return result.str(); + } + + std::string print(index maxRows = 6, index maxFrames = 6, + index maxCols = 6) const + { + using namespace std; + ostringstream result; + + if (size() == 0) return "{}"; + result << endl + << "series: " << size() << endl + << "cols: " << pointSize() << endl; + + if (size() < maxRows) + { + for (index r = 0; r < size(); r++) + result << mIds(r) << ":" << endl + << printSeries(mData[r], maxFrames, maxCols) << endl; + } + else + { + for (index r = 0; r < maxRows / 2; r++) + result << mIds(r) << ":" << endl + << printSeries(mData[r], maxFrames, maxCols) << endl; + + result << setw(10) << "⋮" << endl; + + for (index r = maxRows / 2; r > 0; r--) + result << mIds(size() - r) << ":" << endl + << printSeries(mData[size() - r], maxFrames, maxCols) << endl; + } + + return result.str(); + } + +private: + void initFromData() + { + assert(mIds.rows() == mData.size()); + mDim = mData[0].cols(); + for (index i = 0; i < mIds.size(); i++) mIndex.insert({mIds[i], i}); + } + + rt::vector> mData; + rt::unordered_map mIndex; + FluidTensor mIds; + FluidTensorSlice mDim; // dimensions for one frame +}; +} // namespace fluid diff --git a/include/data/FluidJSON.hpp b/include/data/FluidJSON.hpp index 0cdd5ede4..4687f7de1 100644 --- a/include/data/FluidJSON.hpp +++ b/include/data/FluidJSON.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,52 @@ void from_json(const nlohmann::json &j, FluidDataSet &ds) { } } +// FluidDataSeries +template +void to_json(nlohmann::json &j, const FluidDataSeries &ds) { + auto ids = ds.getIds(); + auto data = ds.getData(); + j["cols"] = ds.pointSize(); + std::stringstream timestring; + for (index r = 0; r < ds.size(); r++) { + auto series = data[r]; + index stringlen = std::ceil(std::log10(series.rows() - 1)); + for (index s = 0; s < series.rows(); s++) { + timestring.str(""); + timestring.clear(); + timestring << std::setw(stringlen) << std::setfill('0') << s; + j["data"][ids[r]][timestring.str()] = data[r].row(s); + } + } +} + +template +bool check_json(const nlohmann::json &j, + const FluidDataSeries &) { + return fluid::check_json(j, + {"cols", "data"}, + {JSONTypes::NUMBER, JSONTypes::OBJECT} + ); +} + +template +void from_json(const nlohmann::json &j, FluidDataSeries &ds) { + auto data = j.at("data"); + index pointSize = j.at("cols").get(); + FluidTensor tmp(pointSize); + + ds.resize(pointSize); + + for (auto r = data.begin(); r != data.end(); ++r) + { + for (auto s = r->begin(); s != r->end(); ++s) + { + s.value().get_to(tmp); + ds.addFrame(r.key(), FluidTensorView{tmp}); + } + } +} + namespace algorithm { // KDTree void to_json(nlohmann::json &j, const KDTree &tree) { diff --git a/include/data/FluidMemory.hpp b/include/data/FluidMemory.hpp index 07768249e..748db1eb5 100644 --- a/include/data/FluidMemory.hpp +++ b/include/data/FluidMemory.hpp @@ -47,6 +47,9 @@ using deque = foonathan::memory::deque; template using queue = foonathan::memory::queue; + +template +using unordered_map = foonathan::memory::unordered_map; } // namespace rt inline Allocator& FluidDefaultAllocator()