diff --git a/docs/site/BUILD.bazel b/docs/site/BUILD.bazel index 61ccbfc4..cd6496ac 100644 --- a/docs/site/BUILD.bazel +++ b/docs/site/BUILD.bazel @@ -104,7 +104,7 @@ docusaurus_bin.docusaurus_binary( docusaurus_bin.docusaurus_binary( name = "start", # TODO: compute the output dir - args = ["start", "docs/site"], + args = ["start", "docs/site", "--host 0.0.0.0"], data = glob( ["**/*"], exclude = ["node_modules/**/*"], diff --git a/docs/site/src/pages/index.tsx b/docs/site/src/pages/index.tsx index f6abb644..4b415bf9 100644 --- a/docs/site/src/pages/index.tsx +++ b/docs/site/src/pages/index.tsx @@ -36,7 +36,7 @@ function HomepageHeader() { export default function Home() { const { siteConfig } = useDocusaurusContext(); const programStr = simpleProgram; - const getStream = () => getNoisySinSignal(100, 0.0015, 100, 80, 2); + const getStream = () => getNoisySinSignal(20, 0.0015, 100, 80, 2); return ( diff --git a/libs/api/BUILD.bazel b/libs/api/BUILD.bazel index a436b882..422f967e 100644 --- a/libs/api/BUILD.bazel +++ b/libs/api/BUILD.bazel @@ -26,7 +26,7 @@ cc_library( ]), hdrs = glob(["include/**/*.h"]) + [":jsonschema_to_src"], copts = [ - "-O3", + # "-O3", "-Iexternal/json-schema-validator/src", "-Ilibs/core/include/rtbot", # this is needed when building from another workspace @@ -48,7 +48,7 @@ cc_library( ) BASE_LINKOPTS = [ - "-O3", + # "-O3", "-lembind", # Enable embind "-s MODULARIZE", "--embind-emit-tsd", diff --git a/libs/api/src/FactoryOp.cpp b/libs/api/src/FactoryOp.cpp index 1695174e..71b2fe63 100644 --- a/libs/api/src/FactoryOp.cpp +++ b/libs/api/src/FactoryOp.cpp @@ -9,6 +9,7 @@ #include "rtbot/Join.h" #include "rtbot/Operator.h" #include "rtbot/Output.h" +#include "rtbot/Pipeline.h" #include "rtbot/finance/RelativeStrengthIndex.h" #include "rtbot/std/Add.h" #include "rtbot/std/And.h" @@ -20,6 +21,7 @@ #include "rtbot/std/Difference.h" #include "rtbot/std/Division.h" #include "rtbot/std/EqualTo.h" +#include "rtbot/std/FastFourierTransform.h" #include "rtbot/std/FiniteImpulseResponse.h" #include "rtbot/std/GreaterThan.h" #include "rtbot/std/HermiteResampler.h" @@ -34,10 +36,11 @@ #include "rtbot/std/Plus.h" #include "rtbot/std/Power.h" #include "rtbot/std/Scale.h" +#include "rtbot/std/Sort.h" +#include "rtbot/std/SortIndex.h" #include "rtbot/std/StandardDeviation.h" #include "rtbot/std/TimeShift.h" #include "rtbot/std/Variable.h" -#include "rtbot/Pipeline.h" using json = nlohmann::json; @@ -107,6 +110,22 @@ void from_json(const json& j, FiniteImpulseResponse& p) { p = FiniteImpulseResponse(j["id"].get(), j["coeff"].get>()); } +template +void to_json(json& j, const FastFourrierTransform& p) { + j = json{{"type", p.typeName()}, + {"id", p.id}, + {"N", p.getN()}, + {"emitPower", p.getEmitPower()}, + {"emitRePart", p.getEmitRePart()}, + {"emitImPart", p.getEmitImPart()}}; +} + +template +void from_json(const json& j, FastFourrierTransform& p) { + p = FastFourrierTransform(j["id"].get(), j["N"].get(), j["emitPower"].get(), + j["emitRePart"].get(), j["emitImPart"].get()); +} + template void to_json(json& j, const Join& p) { j = json{{"type", p.typeName()}, {"id", p.id}, {"numPorts", p.getNumDataInputs()}}; @@ -317,6 +336,32 @@ void from_json(const json& j, Scale& p) { p = Scale(j["id"].get(), j["value"].get()); } +template +void to_json(json& j, const Sort& p) { + j = json{{"type", p.typeName()}, {"id", p.id}, + {"numInputs", p.getNumInputs()}, {"numOutputs", p.getNumOutputs()}, + {"ascending", p.getAscending()}, {"maxInputBufferSize", p.getMaxInputBufferSize()}}; +} + +template +void from_json(const json& j, Sort& p) { + p = Sort(j["id"].get(), j["numInputs"].get(), j["numOutputs"].get(), + j["ascending"].get(), j["maxInputBufferSize"].get()); +} + +template +void to_json(json& j, const SortIndex& p) { + j = json{{"type", p.typeName()}, {"id", p.id}, + {"numInputs", p.getNumInputs()}, {"numOutputs", p.getNumOutputs()}, + {"ascending", p.getAscending()}, {"maxInputBufferSize", p.getMaxInputBufferSize()}}; +} + +template +void from_json(const json& j, SortIndex& p) { + p = SortIndex(j["id"].get(), j["numInputs"].get(), j["numOutputs"].get(), + j["ascending"].get(), j["maxInputBufferSize"].get()); +} + template void to_json(json& j, const Power& p) { j = json{{"type", p.typeName()}, {"id", p.id}, {"value", p.getValue()}}; @@ -416,6 +461,8 @@ FactoryOp::FactoryOp() { op_registry_add, json>(); op_registry_add, json>(); op_registry_add, json>(); + op_registry_add, json>(); + op_registry_add, json>(); op_registry_add, json>(); op_registry_add, json>(); op_registry_add, json>(); diff --git a/libs/core/BUILD.bazel b/libs/core/BUILD.bazel index 17081c93..a0a5a417 100644 --- a/libs/core/BUILD.bazel +++ b/libs/core/BUILD.bazel @@ -25,7 +25,7 @@ cc_library( "src/**/*.cpp", ]), copts = [ - "-O3", + # "-O3", ], hdrs = glob(["include/**/*.h"]), includes = ["include"], diff --git a/libs/core/include/rtbot/Join.md b/libs/core/include/rtbot/Join.md index ecc3d3c4..3596d653 100644 --- a/libs/core/include/rtbot/Join.md +++ b/libs/core/include/rtbot/Join.md @@ -27,14 +27,15 @@ Inputs: `i1`...`iN` where N defined by `numPorts` Outputs: `o1`...`oN` where N defined by `numPorts` The `Join` operator is used to synchronize two or more incoming message streams into -a single, consistent output. +a single, consistent output. `Join` waits until it can emit a consistent output: whenever +there exist messages in all buffers with common time. In such scenario we say a synchronization +has occur and synchronized messages are emitted. -The `Join` operator holds a message buffer on `i1`, `i2`, ... , `iN` respectively, -it uses the message time field to synchronize the streams and pick 1 message per input port. -If at least one of the message buffer is empty or a message with the expected time can not -be found on one of the buffers then the synchronization will not occur. When synchronization -occurs messages with older timestamps than the synchronization time are discarded since it is -understood that they can not be synchronized in the future. The `Join` operator emits the -synchronized messages through `o1`, `o2`, ... , `oN` respectively after the synchronization takes place. +Internally the `Join` operator holds a message buffer on `i1`, `i2`, ... , `iN` respectively, +it uses the message time field to synchronize the streams and pick 1 message per input port. +If at least one of the message buffer is empty, or if for each one of the buffer there isn't +a message with the same time as the incoming message, synchronization will not occur. When synchronization +occurs messages with older timestamps than the synchronization time are discarded since it is +understood that they can not be synchronized in the future. The `Join` operator emits the +synchronized messages through `o1`, `o2`, ... , `oN` respectively after the synchronization takes place. If no synchronization occurs then an empty message {} is emitted through `o1`, `o2`, ... , `oN` respectively. - diff --git a/libs/std/include/rtbot/std/FastFourierTransform.h b/libs/std/include/rtbot/std/FastFourierTransform.h new file mode 100644 index 00000000..18d8f4fd --- /dev/null +++ b/libs/std/include/rtbot/std/FastFourierTransform.h @@ -0,0 +1,141 @@ +#include +#include + +#include "rtbot/Operator.h" + +namespace rtbot { + +template +class FastFourrierTransform : public Operator { + public: + FastFourrierTransform(string const& id, size_t N = 7, size_t skip = 127, bool emitPower = true, + bool emitRePart = false, bool emitImPart = false) + : Operator(id) { + this->N = N; + this->M = pow(2, N); + this->emitPower = emitPower; + this->emitRePart = emitRePart; + this->emitImPart = emitImPart; + // recall that the N passed in the constructor is used to compute the size of the input buffer + // the actual size of the FFT is 2^N + this->addDataInput("i1", this->M); + // allocate the vector that will contain the FFT + this->a = vector>(this->M); + // declare the output ports based on the parameters passed in the constructor + for (int i = 0; i < this->M; i++) { + this->addOutput("w" + to_string(i + 1)); + if (emitPower) this->addOutput("p" + to_string(i + 1)); + if (emitRePart) this->addOutput("re" + to_string(i + 1)); + if (emitImPart) this->addOutput("im" + to_string(i + 1)); + } + } + + string typeName() const override { return "FastFourrierTransform"; } + + map>> processData() override { + this->skipCounter++; + + if (this->skipCounter < this->skip) return map>>(); + + this->skipCounter = 0; + + string inputPort; + auto in = this->getDataInputs(); + if (in.size() == 1) + inputPort = in.at(0); + else + throw runtime_error(typeName() + " : more than 1 input port found"); + map>> outputMsgs; + + auto input = this->dataInputs.find(inputPort)->second; + for (int i = 0; i < this->M; i++) { + this->a[i].real((input.at(i).value)); + this->a[i].imag(0); + } + + // compute the FFT + fft(this->a); + + auto time = this->getDataInputLastMessage(inputPort).time; + for (size_t i = 0; i < this->M; i++) { + if (this->emitRePart) { + Message re(time, this->a[i].real()); + vector> toEmit = {re}; + outputMsgs.emplace("re" + to_string(i + 1), toEmit); + } + if (this->emitImPart) { + Message im(time, this->a[i].imag()); + vector> toEmit = {im}; + outputMsgs.emplace("im" + to_string(i + 1), toEmit); + } + if (this->emitPower) { + Message p(time, pow(this->a[i].real(), 2) + pow(this->a[i].imag(), 2)); + vector> toEmit = {p}; + outputMsgs.emplace("p" + to_string(i + 1), toEmit); + } + + Message w(time, (i + 1.0) / this->M); + vector> toEmit = {w}; + outputMsgs.emplace("w" + to_string(i + 1), toEmit); + } + + return outputMsgs; + } + + size_t getSize() { return this->M; } + size_t getN() { return this->N; } + bool getEmitPower() { return this->emitPower; } + bool getEmitRePart() { return this->emitRePart; } + bool getEmitImPart() { return this->emitImPart; } + + private: + size_t skipCounter; + size_t skip; + size_t M; + size_t N; + bool emitPower; + bool emitRePart; + bool emitImPart; + vector> a; + + // see https://rosettacode.org/wiki/Fast_Fourier_transform#C.2B.2B + void fft(std::vector>& x) { + // DFT + unsigned int N = x.size(), k = N, n; + double thetaT = 3.14159265358979323846264338328L / N; + std::complex phiT = std::complex(cos(thetaT), -sin(thetaT)), Tt; + while (k > 1) { + n = k; + k >>= 1; + phiT = phiT * phiT; + Tt = 1.0L; + for (unsigned int l = 0; l < k; l++) { + for (unsigned int a = l; a < N; a += n) { + unsigned int b = a + k; + std::complex t = x[a] - x[b]; + x[a] += x[b]; + x[b] = t * Tt; + } + Tt *= phiT; + } + } + // Decimate + unsigned int m = (unsigned int)log2(N); + for (unsigned int a = 0; a < N; a++) { + unsigned int b = a; + // Reverse bits + b = (((b & 0xaaaaaaaa) >> 1) | ((b & 0x55555555) << 1)); + b = (((b & 0xcccccccc) >> 2) | ((b & 0x33333333) << 2)); + b = (((b & 0xf0f0f0f0) >> 4) | ((b & 0x0f0f0f0f) << 4)); + b = (((b & 0xff00ff00) >> 8) | ((b & 0x00ff00ff) << 8)); + b = ((b >> 16) | (b << 16)) >> (32 - m); + if (b > a) { + std::complex t = x[a]; + x[a] = x[b]; + x[b] = t; + } + } + } +}; + +} // namespace rtbot \ No newline at end of file diff --git a/libs/std/include/rtbot/std/FastFourierTransform.md b/libs/std/include/rtbot/std/FastFourierTransform.md new file mode 100644 index 00000000..a9688d6d --- /dev/null +++ b/libs/std/include/rtbot/std/FastFourierTransform.md @@ -0,0 +1,56 @@ +--- +behavior: + buffered: true + throughput: variable +view: + shape: circle + latex: + template: | + fft +jsonschema: + type: object + properties: + id: + type: string + description: The id of the operator + N: + type: integer + default: 7 + minimum: 2 + description: Use to compute the fft matrix size, which will be $2^N$ + examples: [3, 4, 5] + skip: + type: integer + default: 127 + minimum: 0 + description: Controls how many messages to skip before the next computation of the fft. This is useful for applications were the fft is not needed to be computed for each received message. Notice that a value different than 0 changes the throughput of the signal. + examples: [127, 255, 100] + emitPower: + type: boolean + description: Indicates whether the power correspondent to the different frequencies should be computed and emitted through the output ports `p1`, ..., `pM`, where $M=2^N$ + default: true + examples: [true, false] + emitRePart: + type: boolean + description: Indicates whether the real part of the amplitude correspondent to the different frequencies should be computed and emitted through the output ports `re1`, ..., `reM`, where $M=2^N$ + default: true + examples: [true, false] + emitImPart: + type: boolean + description: Indicates whether the imaginary part of the amplitude correspondent to the different frequencies should be computed and emitted through the output ports `im1`, ..., `imM`, where $M=2^N$ + default: true + examples: [true, false] + required: ["id"] +--- + +# FastFourierTransform + +Inputs: `i1` +Outputs: + +- frequencies `w1`...`wM` +- power `p1`...`pM` if `emitPower` is true +- real part `re1`...`reM` if `emitRePart` is true +- imaginary part `im1`...`imM` if `emitImPart` is true + +where `M` is equal to $2^n$. diff --git a/libs/std/include/rtbot/std/Sort.h b/libs/std/include/rtbot/std/Sort.h new file mode 100644 index 00000000..4fdba2a1 --- /dev/null +++ b/libs/std/include/rtbot/std/Sort.h @@ -0,0 +1,72 @@ +#include + +#include "rtbot/Join.h" + +namespace rtbot { + +template +class Sort : public Join { + private: + size_t numOutputs; + size_t numInputs; + size_t maxInputBufferSize; + bool ascending; + + public: + Sort() = default; + Sort(string const& id, size_t numInputs, size_t numOutputs, bool ascending = true, size_t maxInputBufferSize = 100) + : Join(id) { + if (numOutputs > numInputs) throw std::runtime_error("Sort: numOutputs must be less than or equal to numInputs"); + this->numOutputs = numOutputs; + this->numInputs = numInputs; + this->ascending = ascending; + this->maxInputBufferSize = maxInputBufferSize; + + // Register the inputs + for (size_t i = 0; i < numInputs; i++) { + this->addDataInput("i" + std::to_string(i + 1), maxInputBufferSize); + } + + // Register the outputs + for (size_t i = 0; i < numOutputs; i++) { + this->addOutput("o" + std::to_string(i + 1)); + } + } + + string typeName() const override { return "Sort"; } + + map>> processData() override { + // Get the input data + auto inputs = this->getDataInputs(); + + vector idx(this->numInputs); + iota(idx.begin(), idx.end(), 0); + + // see https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes + stable_sort(idx.begin(), idx.end(), [this](size_t i1, size_t i2) { + auto v1 = this->dataInputs.find("i" + std::to_string(i1 + 1))->second.front().value; + auto v2 = this->dataInputs.find("i" + std::to_string(i2 + 1))->second.front().value; + return this->ascending ? v1 < v2 : v1 > v2; + }); + + map>> outputMsgs; + // take only the first numOutputs elements + for (size_t i = 0; i < this->numOutputs; i++) { + auto inputPort = "i" + std::to_string(idx[i] + 1); + auto outputPort = "o" + std::to_string(i + 1); + Message out = this->dataInputs.find(inputPort)->second.front(); + vector> v; + v.push_back(out); + outputMsgs.emplace(outputPort, v); + } + + return outputMsgs; + } + + size_t getNumInputs() const { return this->numInputs; } + size_t getNumOutputs() const { return this->numOutputs; } + bool getAscending() const { return this->ascending; } + size_t getMaxInputBufferSize() const { return this->maxInputBufferSize; } +}; + +} // namespace rtbot \ No newline at end of file diff --git a/libs/std/include/rtbot/std/Sort.md b/libs/std/include/rtbot/std/Sort.md new file mode 100644 index 00000000..d03e7919 --- /dev/null +++ b/libs/std/include/rtbot/std/Sort.md @@ -0,0 +1,53 @@ +--- +behavior: + buffered: true + throughput: variable +view: + shape: circle + latex: + template: | + sort +jsonschema: + type: object + properties: + id: + type: string + description: The id of the operator + numInputs: + type: integer + description: "The number of input ports the operator will have. If equal to `N`, + the operator will have `i1`, `i2`, ..., `iN` as input ports." + minimum: 2 + examples: [3, 10, 33] + numOutputs: + type: integer + description: "The number of output ports the operator will have. If equal to `N`, + the operator will have `o1`, `o2`, ..., `oM` as output ports. The + number of output ports should be smaller than the number of input + ports, otherwise a runtime exception will be thrown." + minimum: 1 + examples: [1] + ascending: + type: boolean + description: If `true`, which is the default value, then for all `K` the value that goes out by the port `oK-1` will be smaller than the one going out through the port `oK`. In other words the output values will be ascending as the port index `K` increases. + default: true + examples: [true] + maxInputBufferSize: + type: integer + description: "The maximum length of the internal input buffers that will hold the incoming data + until synchronization occurs." + default: 100 + minimum: 1 + examples: [100, 200, 1000] + required: ["id", "numInputs", "numOutputs"] +--- + +# Sort + +Inputs: `i1`...`iN` where N defined by `numInputs` +Outputs: `o1`...`oM` where M defined by `numOutputs` and $M < N$ + +Takes $n$ input streams through the input ports `i1`, `i2`, ..., `iN`, and emits the same values, +whenever a synchronization happens, through the ports `o1`, `o2`, ..., `oM` ($M < N$), _after ordering_ them according to the `value` in the messages for _every_ synchronization time. In other words for a given $t$ through the port `o1` will go the smallest messages of all input messages, through `o2` the second smallest and so on. Using the `ascending` parameter it is possible to invert the order. + +The synchronization mechanism is inherited from the [`Join`](/docs/operators/core/Join) operator. diff --git a/libs/std/include/rtbot/std/SortIndex.h b/libs/std/include/rtbot/std/SortIndex.h new file mode 100644 index 00000000..38613f07 --- /dev/null +++ b/libs/std/include/rtbot/std/SortIndex.h @@ -0,0 +1,75 @@ +#include + +#include "rtbot/Join.h" + +namespace rtbot { + +template +class SortIndex : public Join { + private: + size_t numOutputs; + size_t numInputs; + size_t maxInputBufferSize; + bool ascending; + + public: + SortIndex() = default; + SortIndex(string const& id, size_t numInputs, size_t numOutputs, bool ascending = true, + size_t maxInputBufferSize = 100) + : Join(id) { + if (numOutputs > numInputs) + throw std::runtime_error("SortIndex: numOutputs must be less than or equal to numInputs"); + this->numOutputs = numOutputs; + this->numInputs = numInputs; + this->ascending = ascending; + this->maxInputBufferSize = maxInputBufferSize; + + // Register the inputs + for (size_t i = 0; i < numInputs; i++) { + this->addDataInput("i" + std::to_string(i + 1), maxInputBufferSize); + } + + // Register the outputs + for (size_t i = 0; i < numOutputs; i++) { + this->addOutput("o" + std::to_string(i + 1)); + } + } + + string typeName() const override { return "SortIndex"; } + + map>> processData() override { + // Get the input data + auto inputs = this->getDataInputs(); + + vector idx(this->numInputs); + iota(idx.begin(), idx.end(), 0); + + // see https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes + stable_sort(idx.begin(), idx.end(), [this](size_t i1, size_t i2) { + auto v1 = this->dataInputs.find("i" + std::to_string(i1 + 1))->second.front().value; + auto v2 = this->dataInputs.find("i" + std::to_string(i2 + 1))->second.front().value; + return this->ascending ? v1 < v2 : v1 > v2; + }); + + map>> outputMsgs; + // get the time from the first input + T time = this->dataInputs.find("i1")->second.front().time; + // take only the first numOutputs elements + for (size_t i = 0; i < this->numOutputs; i++) { + auto outputPort = "o" + std::to_string(i + 1); + auto out = Message(time, idx[i] + 1); + vector> v; + v.push_back(out); + outputMsgs.emplace(outputPort, v); + } + + return outputMsgs; + } + + size_t getNumInputs() const { return this->numInputs; } + size_t getNumOutputs() const { return this->numOutputs; } + bool getAscending() const { return this->ascending; } + size_t getMaxInputBufferSize() const { return this->maxInputBufferSize; } +}; + +} // namespace rtbot \ No newline at end of file diff --git a/libs/std/include/rtbot/std/SortIndex.md b/libs/std/include/rtbot/std/SortIndex.md new file mode 100644 index 00000000..43233909 --- /dev/null +++ b/libs/std/include/rtbot/std/SortIndex.md @@ -0,0 +1,54 @@ +--- +behavior: + buffered: true + throughput: variable +view: + shape: circle + latex: + template: | + sort +jsonschema: + type: object + properties: + id: + type: string + description: The id of the operator + numInputs: + type: integer + description: "The number of input ports the operator will have. If equal to `N`, + the operator will have `i1`, `i2`, ..., `iN` as input ports." + minimum: 2 + examples: [3, 10, 33] + numOutputs: + type: integer + description: "The number of output ports the operator will have. If equal to `N`, + the operator will have `o1`, `o2`, ..., `oM` as output ports. The + number of output ports should be smaller than the number of input + ports, otherwise a runtime exception will be thrown." + minimum: 1 + examples: [1] + ascending: + type: boolean + description: Whether the indices are sort ascending according to the value in the input messages or not. + default: true + examples: [true] + maxInputBufferSize: + type: integer + description: "The maximum length of the internal input buffers that will hold the incoming data + until synchronization occurs." + default: 100 + minimum: 1 + examples: [100, 200, 1000] + required: ["id", "numInputs", "numOutputs"] +--- + +# SortIndex + +Inputs: `i1`...`iN` where N defined by `numInputs` +Outputs: `o1`...`oM` where M defined by `numOutputs` and $M < N$ + +Takes $n$ input streams through the input ports `i1`, `i2`, ..., `iN`, and emits the _indices_ (1, 2, ..., M) of the input messages, whenever a synchronization happens, through the ports `o1`, `o2`, ..., `oM` ($M < N$), _after ordering_ them according to the `value` in the messages for _every_ synchronization time. In other words for a given $t$ through the port `o1` will go the _index_ of smallest messages of all input messages, through `o2` the _index_ of the second smallest and so on. Using the `ascending` parameter it is possible to invert the order. For example, if the smallest message `value` is in the input port `i3`, then the output port `o1` will emit the value 3. + +Here index refers to the numeric suffix associated to the port. Port `i1` has index 1, port `i2` index 2 and so on. + +The synchronization mechanism is inherited from the [`Join`](/docs/operators/core/Join) operator. diff --git a/libs/std/test/test_fast_fourier_transform.cpp b/libs/std/test/test_fast_fourier_transform.cpp new file mode 100644 index 00000000..ac2d1b1c --- /dev/null +++ b/libs/std/test/test_fast_fourier_transform.cpp @@ -0,0 +1,31 @@ +#include + +#include "rtbot/std/FastFourierTransform.h" + +using namespace rtbot; +using namespace std; + +TEST_CASE("FastFourrierTransform") { + SECTION("FFT computation and output messages") { + auto fft = FastFourrierTransform("fft", 3, 1, true, true, true); + vector signal = {1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0}; + // Create a sine wave input + for (uint64_t i = 0; i < signal.size(); i++) { + // double value = sin(2 * M_PI * i / 100.0); + fft.receiveData(Message(i, signal[i])); + } + + // Process the data and get the output messages + auto emitted = fft.executeData(); + // printEmittedMessages(emitted); + + vector realExpected = {4, 1, 0, 1, 0, 1, 0, 1}; + vector imagExpected = {0.0, -2.4142136, 0.0, -0.4142136, 0.0, 0.4142136, 0.0, 2.4142136}; + vector powerExpected = {16, 6.8284271, 0, 1.171573, 0, 1.171573, 0, 6.8284271}; + for (size_t i = 0; i < fft.getSize(); i++) { + REQUIRE(emitted.find("fft")->second.find("re" + to_string(i + 1))->second.at(0).value == Approx(realExpected[i])); + REQUIRE(emitted.find("fft")->second.find("im" + to_string(i + 1))->second.at(0).value == Approx(imagExpected[i])); + REQUIRE(emitted.find("fft")->second.find("p" + to_string(i + 1))->second.at(0).value == Approx(powerExpected[i])); + } + } +} diff --git a/libs/std/test/test_sort.cpp b/libs/std/test/test_sort.cpp new file mode 100644 index 00000000..84a5db99 --- /dev/null +++ b/libs/std/test/test_sort.cpp @@ -0,0 +1,73 @@ +#include + +#include "rtbot/std/Sort.h" + +using namespace rtbot; +using namespace std; + +TEST_CASE("Sort") { + SECTION("Sort ascending") { + // Create a Sort operator with 3 input and 1 output + Sort sort("sort", 3, 1); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 2}, {2, 3}, {3, 11}, {4, 4}, {5, 2}}; + + // Feed the input data to the Sort operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + } + } + + SECTION("Sort descending") { + // Create a Sort operator with 3 input and 1 output + Sort sort("sort", 3, 1, false); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 5}, {2, 5}, {3, 13}, {4, 8}, {5, 32}}; + + // Feed the input data to the Sort operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + } + } + + SECTION("Sort with more than 2 outputs") { + // Create a Sort operator with 3 input and 2 output + Sort sort("sort", 3, 2); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 2}, {2, 3}, {3, 11}, {4, 4}, {5, 2}}; + vector> expectedOutput2 = {{1, 3}, {2, 4}, {3, 12}, {4, 6}, {5, 12}}; + + // Feed the input data to the Sort operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + REQUIRE(outputMsgs.find("sort")->second.find("o2")->second.at(0) == expectedOutput2[i]); + } + } +} \ No newline at end of file diff --git a/libs/std/test/test_sort_index.cpp b/libs/std/test/test_sort_index.cpp new file mode 100644 index 00000000..9a7110fb --- /dev/null +++ b/libs/std/test/test_sort_index.cpp @@ -0,0 +1,73 @@ +#include + +#include "rtbot/std/SortIndex.h" + +using namespace rtbot; +using namespace std; + +TEST_CASE("SortIndex") { + SECTION("SortIndex ascending") { + // Create a SortIndex operator with 3 input and 1 output + SortIndex sort("sort", 3, 1); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 2}, {2, 1}, {3, 1}, {4, 3}, {5, 2}}; + + // Feed the input data to the SortIndex operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + } + } + + SECTION("SortIndex descending") { + // Create a SortIndex operator with 3 input and 1 output + SortIndex sort("sort", 3, 1, false); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 1}, {2, 3}, {3, 3}, {4, 2}, {5, 3}}; + + // Feed the input data to the SortIndex operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + } + } + + SECTION("SortIndex with more than 2 outputs") { + // Create a SortIndex operator with 3 input and 2 output + SortIndex sort("sort", 3, 2); + + // Create some unsorted input data + vector> inputData1 = {{1, 5}, {2, 3}, {3, 11}, {4, 6}, {5, 12}}; + vector> inputData2 = {{1, 2}, {2, 4}, {3, 12}, {4, 8}, {5, 2}}; + vector> inputData3 = {{1, 3}, {2, 5}, {3, 13}, {4, 4}, {5, 32}}; + vector> expectedOutput1 = {{1, 2}, {2, 1}, {3, 1}, {4, 3}, {5, 2}}; + vector> expectedOutput2 = {{1, 3}, {2, 2}, {3, 2}, {4, 1}, {5, 1}}; + + // Feed the input data to the SortIndex operator + for (size_t i = 0; i < inputData1.size(); i++) { + sort.receiveData(inputData1[i], "i1"); + sort.receiveData(inputData2[i], "i2"); + sort.receiveData(inputData3[i], "i3"); + auto outputMsgs = sort.executeData(); + // Check that the output is sorted + REQUIRE(outputMsgs.find("sort")->second.find("o1")->second.at(0) == expectedOutput1[i]); + REQUIRE(outputMsgs.find("sort")->second.find("o2")->second.at(0) == expectedOutput2[i]); + } + } +} \ No newline at end of file