#include #include #include #include #include "mini-odeint.hpp" #include "pffft.hpp" #include "xoshiro.hpp" #if defined(__clang__) #define FORCE_INLINE [[gnu::always_inline]] [[gnu::gnu_inline]] extern inline #elif defined(__GNUC__) #define FORCE_INLINE [[gnu::always_inline]] inline #elif defined(_MSC_VER) #pragma warning(error : 4714) #define FORCE_INLINE __forceinline #else #define FORCE_INLINE forceinline #endif namespace { FORCE_INLINE constexpr auto sqr(auto &&x) noexcept(noexcept(x * x)) -> decltype(x * x) { return x * x; } template inline T stdev(std::span data) { const auto n = static_cast(data.size()); const auto mean = std::accumulate(data.begin(), data.end(), T{}) / n; const auto variance = std::accumulate(data.begin(), data.end(), T{}, [mean](T acc, T x) { return acc + sqr(x - mean); }) / (n - 1); return std::sqrt(variance); } template constexpr bool is_wasm_bindable_v = std::conjunction_v, std::is_standard_layout>; } // namespace namespace ecgsyns { struct TimeParameters { int num_beats{12}; int sr_internal{512}; int decimate_factor{2}; double hr_mean{60.0}; double hr_std{1.0}; std::uint64_t seed{8}; }; static_assert(is_wasm_bindable_v); struct RRParameters { double flo{0.1}; double flostd{0.01}; double fhi{0.25}; double fhistd{0.01}; double lf_hf_ratio{0.5}; }; static_assert(is_wasm_bindable_v); template class RRSeries { public: struct Segment { T end; T value; }; private: TimeParameters time_params_; RRParameters rr_params_; XoshiroCpp::Xoshiro256Plus rng_; std::size_t size_; std::vector segments_; public: RRSeries(TimeParameters time_params, RRParameters rr_params, XoshiroCpp::Xoshiro256Plus rng, std::span signal) : time_params_{time_params}, rr_params_{rr_params}, rng_{rng}, size_{signal.size()} { const auto sr = static_cast(time_params_.sr_internal); { T tecg{}; std::size_t i{}; while (i < signal.size()) { tecg += signal[i]; segments_.emplace_back(tecg, signal[i]); i = 1 + static_cast(std::nearbyint(tecg * sr * T{0.5}) * T{2.0}); } } } constexpr std::size_t segments_size() const { return segments_.size(); } constexpr const Segment *segments_data() const { return segments_.data(); } T operator()(T t) const noexcept { auto lower = std::lower_bound( segments_.begin(), segments_.end(), t, [](const auto &seg, const auto t) { return seg.end < t; }); if (lower == segments_.end()) { lower = std::prev(lower); } return lower->value; } [[nodiscard]] const TimeParameters &time_params() const noexcept { return time_params_; } [[nodiscard]] const RRParameters &rr_params() const noexcept { return rr_params_; } XoshiroCpp::Xoshiro256Plus &rng() noexcept { return rng_; } [[nodiscard]] std::size_t size() const noexcept { return size_; } [[nodiscard]] std::size_t output_size() const noexcept { return size_ / time_params_.decimate_factor; } }; class FFTException : public std::exception { public: [[nodiscard]] const char *what() const noexcept override { return "FFTException"; } }; template class RRGenerator { TimeParameters time_params_; XoshiroCpp::Xoshiro256Plus rng_; T rr_mean_; T rr_std_; std::size_t nrr_; pffft::Fft fft_; pffft::AlignedVector signal_; pffft::AlignedVector> spectrum_; public: explicit RRGenerator(const TimeParameters &time_params) : time_params_(time_params), rng_(time_params.seed), rr_mean_(60.0 / time_params.hr_mean), rr_std_(60.0 * time_params.hr_std / sqr(time_params.hr_mean)), nrr_(pffft::Fft::nearestTransformSize(time_params.num_beats * time_params.sr_internal * std::ceil(rr_mean_))), fft_(pffft::Fft(nrr_)), signal_(fft_.valueVector()), spectrum_( pffft::AlignedVector>(fft_.getSpectrumSize() + 1)) { if (!fft_.isValid()) { throw FFTException(); } } constexpr std::size_t nrr() const noexcept { return nrr_; } constexpr const TimeParameters &time_params() const noexcept { return time_params_; } RRSeries generate(const RRParameters ¶ms) { auto buf = std::make_unique_for_overwrite(nrr_); std::span signal{buf.get(), nrr_}; generate_signal(params, signal); return {time_params_, params, rng_, signal}; } void generate_signal(const RRParameters ¶ms, std::span output) { const T w1 = T{2.0 * M_PI} * params.flo; const T w2 = T{2.0 * M_PI} * params.fhi; const T c1 = T{2.0 * M_PI} * params.flostd; const T c2 = T{2.0 * M_PI} * params.fhistd; const T sig2 = T{1}; const T sig1 = params.lf_hf_ratio; const T sr = time_params_.sr_internal; const T dw = (sr / T(nrr_)) * T{2.0 * M_PI}; for (std::size_t i{}; auto &p : spectrum_) { const T w = dw * T(i); const T dw1 = w - w1; const T dw2 = w - w2; const T hw = sig1 * std::exp(-sqr(dw1) / (T{2} * sqr(c1))) / std::sqrt(T{2.0 * M_PI} * sqr(c1)) + sig2 * std::exp(-sqr(dw2) / (T{2} * sqr(c2))) / std::sqrt(T{2.0 * M_PI} * sqr(c2)); const T sw = (sr / T{2}) * std::sqrt(hw); const T ph = 2.0 * M_PI * XoshiroCpp::DoubleFromBits(rng_()); p = std::polar(sw, ph); ++i; } spectrum_.front().imag(spectrum_.back().real()); fft_.inverse(spectrum_, signal_); std::ranges::transform(signal_, signal_.begin(), [this](T x) { return x * T(1.0 / nrr_); }); const T xstd = stdev(signal_); const T ratio = rr_std_ / xstd; std::vector result; result.reserve(nrr_); std::ranges::transform(signal_, output.begin(), [ratio, this](T x) { return x * ratio + rr_mean_; }); } }; template struct Attractor { T theta; T a; T b; T theta_rf; constexpr static Attractor make(T degrees, T a, T b, T rf = 0.0) { return {degrees * M_PI / 180.0, a, b, rf}; } }; template >> struct Parameters { std::pair range{-0.4, 1.2}; T noise_amplitude{}; U attractors; }; template struct Parameters>> { std::pair range{-0.4, 1.2}; T noise_amplitude{}; std::vector> attractors{ Attractor::make(-70.0, 1.2, 0.25, 0.25), Attractor::make(-15.0, -5.0, 0.1, 0.5), Attractor::make(0.0, 30, 0.1), Attractor::make(15.0, -7.5, 0.1, 0.5), Attractor::make(100.0, 0.75, 0.4, 0.25), }; }; template >> std::size_t generate(const Parameters ¶ms, RRSeries &rr_series, std::span zresult) { struct ExPoint { T ti; T ai; T bi; }; auto &rng = rr_series.rng(); const auto sr_internal{rr_series.time_params().sr_internal}; const T hr_sec{rr_series.time_params().hr_mean / 60.0}; const T hr_fact{std::sqrt(hr_sec)}; // adjust extrema parameters for mean heart rate std::vector ex; ex.reserve(params.attractors.size()); for (const auto &a : params.attractors) { ex.emplace_back(a.theta * std::pow(hr_sec, a.theta_rf), a.a, a.b * hr_fact); } const T fhi{rr_series.rr_params().fhi}; const auto nt{rr_series.size()}; const T dt{1.0 / static_cast(sr_internal)}; std::vector ts; ts.reserve(nt); for (std::size_t i{}; i < nt; ++i) { ts.emplace_back(static_cast(i) * dt); } using namespace mini_odeint; Vec3 x0{1.0, 0.0, 0.04}; std::vector> ys(nt); explicitRungeKutta>( std::span{ys}, std::span{ts}, x0, 1e-6, [&](Vec3 v, T t) { const T ta{std::atan2(v.y, v.x)}; const T r0{1.0}; const T a0{T{1.0} - std::sqrt(sqr(v.x) + sqr(v.y)) / r0}; const T w0{T{2.0 * M_PI} / rr_series(t)}; const T zbase{T{0.005} * std::sin(T{2.0 * M_PI} * fhi * t)}; Vec3 f{a0 * v.x - w0 * v.y, a0 * v.y + w0 * v.x, 0.0}; for (const auto &e : ex) { const T dt{std::remainder(ta - e.ti, T{2.0 * M_PI})}; f.z += -e.ai * dt * std::exp(T{-0.5} * sqr(dt) / sqr(e.bi)); } f.z += T{-1.0} * (v.z + zbase); return f; }); // extract z and downsample to output rate for (std::size_t i{}, j{}; i < nt && j < zresult.size(); i += rr_series.time_params().decimate_factor, ++j) { zresult[j] = ys[i].z; } const auto [zmin, zmax] = std::ranges::minmax(zresult); const T zrange = zmax - zmin; // Scale signal between -0.4 and 1.2 mV // add uniformly distributed measurement noise std::ranges::transform(zresult, zresult.begin(), [&](T z) { return (params.range.second - params.range.first) * (z - zmin) / zrange + params.range.first + T{2.0 * XoshiroCpp::DoubleFromBits(rng()) - 1.0} * params.noise_amplitude; }); return ys.size() / rr_series.time_params().decimate_factor; } } // namespace ecgsyns namespace { template class WasmSpan { public: T *ptr; std::size_t n; public: using iterator = T *; constexpr std::size_t size() const noexcept { return n; } constexpr iterator begin() const noexcept { return ptr; } constexpr iterator end() const noexcept { return ptr + n; } }; class WrapperBase { public: virtual ~WrapperBase() = default; WrapperBase(const WrapperBase &) = delete; WrapperBase &operator=(const WrapperBase &) = delete; protected: WrapperBase() = default; }; template struct Wrapper : public WrapperBase { T value; explicit Wrapper(T value) : value{std::move(value)} {} }; struct WObject { enum class type : std::uint32_t { kRRGenerator = 1, kRRSeries, } t; WObject(type t) : t{t} {} }; struct WRRGenerator : WObject { std::size_t nrr; std::size_t output_size; WRRGenerator(std::size_t nrr, std::size_t output_size) : WObject{type::kRRGenerator}, nrr{nrr}, output_size{output_size} {} }; struct NRRGenerator : WRRGenerator { ecgsyns::RRGenerator obj; NRRGenerator(ecgsyns::RRGenerator o) : WRRGenerator{o.nrr(), o.nrr() / o.time_params().decimate_factor}, obj{std::move(o)} {} }; struct WRRSeries : WObject { std::size_t size; std::size_t output_size; std::size_t nsegments; const ecgsyns::RRSeries::Segment *segments; WRRSeries(std::size_t size, std::size_t output_size, std::size_t nsegments, const ecgsyns::RRSeries::Segment *segments) : WObject{type::kRRSeries}, size{size}, output_size{output_size}, nsegments{nsegments}, segments{segments} {} }; struct NRRSeries : WRRSeries { ecgsyns::RRSeries obj; NRRSeries(ecgsyns::RRSeries o) : WRRSeries{o.size(), o.output_size(), o.segments_size(), o.segments_data()}, obj{std::move(o)} {} }; } // namespace #include extern "C" { using namespace ecgsyns; WRRGenerator *rr_gen_new(const TimeParameters *time_params) { return new NRRGenerator{ecgsyns::RRGenerator{*time_params}}; } WRRSeries *rr_gen_new_series(WRRGenerator *generator, const RRParameters *params) { return new NRRSeries{ static_cast(generator)->obj.generate(*params)}; } void rr_gen_generate(WRRGenerator *generator, const RRParameters *params, double *output) { auto &rr = static_cast(generator)->obj; rr.generate_signal(*params, {output, rr.nrr()}); } void destroy_obj(const WObject *o) { switch (o->t) { case WObject::type::kRRGenerator: delete static_cast(o); break; case WObject::type::kRRSeries: delete static_cast(o); break; } } using WasmParameters = Parameters>>; static_assert(is_wasm_bindable_v); void ecgsyn(const WasmParameters *params, WRRSeries *rr_series, double *output) { auto &rr = static_cast(rr_series)->obj; ecgsyns::generate(*params, rr, {output, rr.output_size()}); } } #if defined(ECGSYN_HOST_BUILD) #include int main() { using namespace ecgsyns; TimeParameters time_params{}; time_params.num_beats = 4; auto rr = rr_init(&time_params); const RRParameters rr_params{}; auto rrs = rr_generate(rr, &rr_params); Parameters params; std::vector output(rrs->value.output_size()); generate(params, rrs->value, std::span(output)); std::ofstream f{"ecg.csv"}; for (const auto &x : output) { f << x << '\n'; } return 0; } #endif