Update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
.cache/
|
.cache/
|
||||||
build/
|
build/
|
||||||
|
/cmake-*/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
/dist/
|
/dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
||||||
|
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
|
||||||
set(EMSCRIPTEN_ROOT $ENV{EMSDK})
|
|
||||||
if(NOT EMSCRIPTEN_ROOT)
|
|
||||||
set(EMSCRIPTEN_ROOT /usr/lib/emscripten)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(EMSCRIPTEN_ROOT
|
|
||||||
"${EMSCRIPTEN_ROOT}"
|
|
||||||
CACHE PATH "Emscripten SDK path")
|
|
||||||
|
|
||||||
set(CMAKE_TOOLCHAIN_FILE
|
|
||||||
"${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake"
|
|
||||||
CACHE FILEPATH "Emscripten toolchain file")
|
|
||||||
|
|
||||||
project(ecgsyn.js)
|
project(ecgsyn.js)
|
||||||
|
|
||||||
|
option(BUILD_WITH_EMSCRIPTEN "Build with Emscripten" ON)
|
||||||
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
|
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
|
||||||
|
|
||||||
|
if(BUILD_WITH_EMSCRIPTEN)
|
||||||
|
|
||||||
|
set(EMSCRIPTEN_ROOT $ENV{EMSDK})
|
||||||
|
if(NOT EMSCRIPTEN_ROOT)
|
||||||
|
set(EMSCRIPTEN_ROOT /usr/lib/emscripten)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(EMSCRIPTEN_ROOT
|
||||||
|
"${EMSCRIPTEN_ROOT}"
|
||||||
|
CACHE PATH "Emscripten SDK path")
|
||||||
|
|
||||||
|
set(CMAKE_TOOLCHAIN_FILE
|
||||||
|
"${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake"
|
||||||
|
CACHE FILEPATH "Emscripten toolchain file")
|
||||||
|
|
||||||
|
set(BUILD_SHARED_LIBS OFF FORCE)
|
||||||
|
endif(BUILD_WITH_EMSCRIPTEN)
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 99)
|
set(CMAKE_C_STANDARD 99)
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
||||||
@@ -25,24 +32,40 @@ set(PFFFT_USE_TYPE_FLOAT
|
|||||||
OFF
|
OFF
|
||||||
CACHE BOOL "activate pffft float" FORCE)
|
CACHE BOOL "activate pffft float" FORCE)
|
||||||
|
|
||||||
include_directories(BEFORE SYSTEM compat)
|
include(CheckIPOSupported)
|
||||||
add_compile_options(-flto -msimd128 -mavx)
|
check_ipo_supported(RESULT ipo_supported)
|
||||||
add_link_options(-flto)
|
if(ipo_supported)
|
||||||
|
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(BUILD_WITH_EMSCRIPTEN)
|
||||||
|
include_directories(BEFORE SYSTEM compat)
|
||||||
|
|
||||||
|
add_compile_options(-msimd128 -msse -mavx -fwasm-exceptions)
|
||||||
|
set(ecgsyn_link_options # -flto
|
||||||
|
-fwasm-exceptions
|
||||||
|
-sEXPORTED_FUNCTIONS=_rr_gen_new,_rr_gen_new_series,_destroy_obj,_ecgsyn,_realloc,_free
|
||||||
|
--no-entry
|
||||||
|
-sSTRICT
|
||||||
|
-sNO_ASSERTIONS
|
||||||
|
-sNO_FILESYSTEM
|
||||||
|
-sMALLOC=emmalloc)
|
||||||
|
|
||||||
|
else()
|
||||||
|
set(TARGET_C_ARCH haswell)
|
||||||
|
set(TARGET_CXX_ARCH haswell)
|
||||||
|
|
||||||
|
set(ecgsyn_defs ECGSYN_HOST_BUILD)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(mini-odeint EXCLUDE_FROM_ALL)
|
add_subdirectory(mini-odeint EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(pffft EXCLUDE_FROM_ALL)
|
add_subdirectory(pffft EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
add_executable(ecgsyn ecgsyn.cpp)
|
add_executable(ecgsyn ecgsyn.cpp)
|
||||||
target_compile_options(ecgsyn PRIVATE)
|
target_compile_definitions(ecgsyn PRIVATE PFFFT_ENABLE_DOUBLE ${ecgsyn_defs})
|
||||||
target_link_options(
|
target_link_options(ecgsyn PRIVATE ${ecgsyn_link_options})
|
||||||
ecgsyn
|
target_link_libraries(ecgsyn PRIVATE PFFFT mini-odeint)
|
||||||
PRIVATE
|
|
||||||
-flto
|
if(BUILD_WITH_EMSCRIPTEN)
|
||||||
-sEXPORTED_FUNCTIONS=_ecgsyn
|
set_target_properties(ecgsyn PROPERTIES SUFFIX ".wasm")
|
||||||
--no-entry
|
endif(BUILD_WITH_EMSCRIPTEN)
|
||||||
-sSTRICT
|
|
||||||
-sNO_ASSERTIONS
|
|
||||||
-sNO_FILESYSTEM
|
|
||||||
-sMALLOC=emmalloc)
|
|
||||||
target_link_libraries(ecgsyn PRIVATE PFFFT)
|
|
||||||
set_target_properties(ecgsyn PROPERTIES SUFFIX ".wasm")
|
|
||||||
|
|||||||
@@ -8,15 +8,36 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic" />
|
||||||
<!-- Place favicon.ico in the root directory -->
|
<!-- Place favicon.ico in the root directory -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import load from "../dist/index.js";
|
import { load, ECGSyn } from "../dist/index.js";
|
||||||
|
await load();
|
||||||
|
const ecgsyn = new ECGSyn();
|
||||||
|
ecgsyn.rrProcess();
|
||||||
|
ecgsyn.compute();
|
||||||
|
ecgsyn.update();
|
||||||
|
|
||||||
let ecgsyn = await load();
|
const p = ecgsyn.parameters;
|
||||||
window.ecgsn = ecgsyn;
|
|
||||||
ecgsyn.ecgsyn();
|
const width = document.getElementById("r_width");
|
||||||
|
width.value = p.attractors[2].b;
|
||||||
|
|
||||||
|
const theta = document.getElementById("p_theta");
|
||||||
|
theta.value = (p.attractors[0].theta * 180) / Math.PI;
|
||||||
|
|
||||||
|
function listener(event) {
|
||||||
|
p.attractors[0].theta = (theta.value * Math.PI) / 180;
|
||||||
|
p.attractors[2].b = width.value;
|
||||||
|
ecgsyn.compute();
|
||||||
|
ecgsyn.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const el of [theta, width]) {
|
||||||
|
el.addEventListener("input", listener);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<body>
|
<body>
|
||||||
<!--[if lt IE 8]>
|
<!--[if lt IE 8]>
|
||||||
@@ -25,5 +46,15 @@
|
|||||||
<a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
|
<a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
|
||||||
</p>
|
</p>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
<canvas width="1900" height="300" id="output"></canvas>
|
||||||
|
<div>
|
||||||
|
<select>
|
||||||
|
<option value="range"></option>
|
||||||
|
</select>
|
||||||
|
<div>
|
||||||
|
<input type="range" min="0" max="1" value="0" step=".01" class="slider" id="r_width" />
|
||||||
|
<input type="range" min="-180" max="180" value="0" class="slider" id="p_theta" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
371
ecgsyn.cpp
371
ecgsyn.cpp
@@ -3,8 +3,8 @@
|
|||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
|
#include "mini-odeint.hpp"
|
||||||
#include "pffft.hpp"
|
#include "pffft.hpp"
|
||||||
|
|
||||||
#include "xoshiro.hpp"
|
#include "xoshiro.hpp"
|
||||||
|
|
||||||
#if defined(__clang__)
|
#if defined(__clang__)
|
||||||
@@ -29,7 +29,7 @@ FORCE_INLINE constexpr auto sqr(auto &&x) noexcept(noexcept(x * x))
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename T> inline T stdev(std::span<const T> data) {
|
template <typename T> inline T stdev(std::span<const T> data) {
|
||||||
const auto n = T{data.size()};
|
const auto n = static_cast<T>(data.size());
|
||||||
const auto mean = std::accumulate(data.begin(), data.end(), T{}) / n;
|
const auto mean = std::accumulate(data.begin(), data.end(), T{}) / n;
|
||||||
const auto variance =
|
const auto variance =
|
||||||
std::accumulate(data.begin(), data.end(), T{},
|
std::accumulate(data.begin(), data.end(), T{},
|
||||||
@@ -38,43 +38,53 @@ template <typename T> inline T stdev(std::span<const T> data) {
|
|||||||
return std::sqrt(variance);
|
return std::sqrt(variance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool is_wasm_bindable_v =
|
||||||
|
std::conjunction_v<std::is_trivially_copyable<T>,
|
||||||
|
std::is_standard_layout<T>>;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace ecgsyns {
|
namespace ecgsyns {
|
||||||
struct TimeParameters {
|
struct TimeParameters {
|
||||||
int num_beats = 12;
|
int num_beats{12};
|
||||||
int sr_internal = 512;
|
int sr_internal{512};
|
||||||
int decimate_factor = 2;
|
int decimate_factor{2};
|
||||||
double hr_mean = 60.0;
|
double hr_mean{60.0};
|
||||||
double hr_std = 1.0;
|
double hr_std{1.0};
|
||||||
std::uint64_t seed = 0;
|
std::uint64_t seed{8};
|
||||||
};
|
};
|
||||||
|
static_assert(is_wasm_bindable_v<TimeParameters>);
|
||||||
|
|
||||||
struct RRParameters {
|
struct RRParameters {
|
||||||
double flo = 0.1;
|
double flo{0.1};
|
||||||
double flostd = 0.01;
|
double flostd{0.01};
|
||||||
double fhi = 0.25;
|
double fhi{0.25};
|
||||||
double fhistd = 0.01;
|
double fhistd{0.01};
|
||||||
double lf_hf_ratio = 0.5;
|
double lf_hf_ratio{0.5};
|
||||||
};
|
};
|
||||||
|
static_assert(is_wasm_bindable_v<RRParameters>);
|
||||||
|
|
||||||
template <typename T> class RRSeries {
|
template <typename T = double> class RRSeries {
|
||||||
|
public:
|
||||||
|
struct Segment {
|
||||||
|
T end;
|
||||||
|
T value;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
TimeParameters time_params_;
|
TimeParameters time_params_;
|
||||||
RRParameters rr_params_;
|
RRParameters rr_params_;
|
||||||
XoshiroCpp::Xoshiro256Plus rng_;
|
XoshiroCpp::Xoshiro256Plus rng_;
|
||||||
std::size_t size_;
|
std::size_t size_;
|
||||||
|
|
||||||
struct Segment {
|
|
||||||
T end;
|
|
||||||
T value;
|
|
||||||
};
|
|
||||||
std::vector<Segment> segments_;
|
std::vector<Segment> segments_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RRSeries(TimeParameters time_params, RRParameters rr_params,
|
RRSeries(TimeParameters time_params, RRParameters rr_params,
|
||||||
XoshiroCpp::Xoshiro256Plus rng, std::span<const T> signal)
|
XoshiroCpp::Xoshiro256Plus rng, std::span<const T> signal)
|
||||||
: time_params_(std::move(time_params)), rr_params_(std::move(rr_params)),
|
: time_params_{time_params}, rr_params_{rr_params}, rng_{rng},
|
||||||
rng_(std::move(rng)), size_(signal.size()) {
|
size_{signal.size()} {
|
||||||
|
|
||||||
const auto sr = static_cast<T>(time_params_.sr_internal);
|
const auto sr = static_cast<T>(time_params_.sr_internal);
|
||||||
|
|
||||||
@@ -82,16 +92,18 @@ public:
|
|||||||
T tecg{};
|
T tecg{};
|
||||||
std::size_t i{};
|
std::size_t i{};
|
||||||
|
|
||||||
while (i < size_) {
|
while (i < signal.size()) {
|
||||||
tecg += signal[i];
|
tecg += signal[i];
|
||||||
segments_.emplace_back(tecg, signal[i]);
|
segments_.emplace_back(tecg, signal[i]);
|
||||||
i = static_cast<std::size_t>(std::nearbyint(tecg * sr * T{0.5}) *
|
i = 1 + static_cast<std::size_t>(std::nearbyint(tecg * sr * T{0.5}) *
|
||||||
T{2.0}) +
|
T{2.0});
|
||||||
1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
T operator()(T t) const noexcept {
|
||||||
auto lower = std::lower_bound(
|
auto lower = std::lower_bound(
|
||||||
segments_.begin(), segments_.end(), t,
|
segments_.begin(), segments_.end(), t,
|
||||||
@@ -102,30 +114,38 @@ public:
|
|||||||
return lower->value;
|
return lower->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimeParameters &timeParams() const noexcept { return time_params_; }
|
[[nodiscard]] const TimeParameters &time_params() const noexcept {
|
||||||
const RRParameters &rrParams() const noexcept { return rr_params_; }
|
return time_params_;
|
||||||
const XoshiroCpp::Xoshiro256Plus &rng() const noexcept { return rng_; }
|
}
|
||||||
std::size_t size() const noexcept { return size_; }
|
[[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 {
|
class FFTException : public std::exception {
|
||||||
public:
|
public:
|
||||||
FFTException() {}
|
[[nodiscard]] const char *what() const noexcept override {
|
||||||
virtual const char *what() const throw() { return "FFTException"; }
|
return "FFTException";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T> class RRGenerator {
|
template <typename T = double> class RRGenerator {
|
||||||
TimeParameters time_params_;
|
TimeParameters time_params_;
|
||||||
|
XoshiroCpp::Xoshiro256Plus rng_;
|
||||||
T rr_mean_;
|
T rr_mean_;
|
||||||
T rr_std_;
|
T rr_std_;
|
||||||
std::size_t nrr_;
|
std::size_t nrr_;
|
||||||
XoshiroCpp::Xoshiro256Plus rng_;
|
|
||||||
pffft::Fft<T> fft_;
|
pffft::Fft<T> fft_;
|
||||||
pffft::AlignedVector<T> signal_;
|
pffft::AlignedVector<T> signal_;
|
||||||
pffft::AlignedVector<std::complex<T>> spectrum_;
|
pffft::AlignedVector<std::complex<T>> spectrum_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RRGenerator(const TimeParameters &time_params)
|
explicit RRGenerator(const TimeParameters &time_params)
|
||||||
: time_params_(time_params), rng_(time_params.seed),
|
: time_params_(time_params), rng_(time_params.seed),
|
||||||
rr_mean_(60.0 / time_params.hr_mean),
|
rr_mean_(60.0 / time_params.hr_mean),
|
||||||
rr_std_(60.0 * time_params.hr_std / sqr(time_params.hr_mean)),
|
rr_std_(60.0 * time_params.hr_std / sqr(time_params.hr_mean)),
|
||||||
@@ -140,7 +160,20 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<T> generateSignal(const RRParameters ¶ms) {
|
constexpr std::size_t nrr() const noexcept { return nrr_; }
|
||||||
|
|
||||||
|
constexpr const TimeParameters &time_params() const noexcept {
|
||||||
|
return time_params_;
|
||||||
|
}
|
||||||
|
|
||||||
|
RRSeries<T> generate(const RRParameters ¶ms) {
|
||||||
|
auto buf = std::make_unique_for_overwrite<T[]>(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<T> output) {
|
||||||
const T w1 = T{2.0 * M_PI} * params.flo;
|
const T w1 = T{2.0 * M_PI} * params.flo;
|
||||||
const T w2 = T{2.0 * M_PI} * params.fhi;
|
const T w2 = T{2.0 * M_PI} * params.fhi;
|
||||||
const T c1 = T{2.0 * M_PI} * params.flostd;
|
const T c1 = T{2.0 * M_PI} * params.flostd;
|
||||||
@@ -151,10 +184,10 @@ public:
|
|||||||
|
|
||||||
const T sr = time_params_.sr_internal;
|
const T sr = time_params_.sr_internal;
|
||||||
|
|
||||||
const T dw = (sr / T{nrr_}) * T{2.0 * M_PI};
|
const T dw = (sr / T(nrr_)) * T{2.0 * M_PI};
|
||||||
|
|
||||||
for (std::size_t i{}; auto &p : spectrum_) {
|
for (std::size_t i{}; auto &p : spectrum_) {
|
||||||
const T w = dw * T{i};
|
const T w = dw * T(i);
|
||||||
|
|
||||||
const T dw1 = w - w1;
|
const T dw1 = w - w1;
|
||||||
const T dw2 = w - w2;
|
const T dw2 = w - w2;
|
||||||
@@ -176,34 +209,272 @@ public:
|
|||||||
fft_.inverse(spectrum_, signal_);
|
fft_.inverse(spectrum_, signal_);
|
||||||
|
|
||||||
std::ranges::transform(signal_, signal_.begin(),
|
std::ranges::transform(signal_, signal_.begin(),
|
||||||
[this](T x) { return x * T{1.0} / T{nrr_}; });
|
[this](T x) { return x * T(1.0 / nrr_); });
|
||||||
const T xstd = stdev(signal_);
|
const T xstd = stdev<T>(signal_);
|
||||||
const T ratio = rr_std_ / xstd;
|
const T ratio = rr_std_ / xstd;
|
||||||
std::vector<T> result;
|
std::vector<T> result;
|
||||||
result.reserve(nrr_);
|
result.reserve(nrr_);
|
||||||
|
|
||||||
std::ranges::transform(signal_, std::back_inserter(result),
|
std::ranges::transform(signal_, output.begin(),
|
||||||
[ratio, this](T x) { return x * ratio + rr_mean_; });
|
[ratio, this](T x) { return x * ratio + rr_mean_; });
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T = double> 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 <typename T = double, typename U = std::vector<Attractor<T>>>
|
||||||
|
struct Parameters {
|
||||||
|
std::pair<const T, const T> range{-0.4, 1.2};
|
||||||
|
T noise_amplitude{};
|
||||||
|
|
||||||
|
U attractors;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T> struct Parameters<T, std::vector<Attractor<T>>> {
|
||||||
|
std::pair<T, T> range{-0.4, 1.2};
|
||||||
|
T noise_amplitude{};
|
||||||
|
|
||||||
|
std::vector<Attractor<T>> attractors{
|
||||||
|
Attractor<T>::make(-70.0, 1.2, 0.25, 0.25),
|
||||||
|
Attractor<T>::make(-15.0, -5.0, 0.1, 0.5),
|
||||||
|
Attractor<T>::make(0.0, 30, 0.1),
|
||||||
|
Attractor<T>::make(15.0, -7.5, 0.1, 0.5),
|
||||||
|
Attractor<T>::make(100.0, 0.75, 0.4, 0.25),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename U = std::vector<Attractor<T>>>
|
||||||
|
std::size_t generate(const Parameters<T, U> ¶ms, RRSeries<T> &rr_series,
|
||||||
|
std::span<T> 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<ExPoint> 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<T>(sr_internal)};
|
||||||
|
|
||||||
|
std::vector<T> ts;
|
||||||
|
ts.reserve(nt);
|
||||||
|
for (std::size_t i{}; i < nt; ++i) {
|
||||||
|
ts.emplace_back(static_cast<T>(i) * dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace mini_odeint;
|
||||||
|
Vec3<T> x0{1.0, 0.0, 0.04};
|
||||||
|
std::vector<Vec3<T>> ys(nt);
|
||||||
|
explicitRungeKutta<Vec3<T>>(
|
||||||
|
std::span{ys}, std::span<const T>{ts}, x0, 1e-6, [&](Vec3<T> 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<T> 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 ecgsyns
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename T> 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 <typename T> 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<double> obj;
|
||||||
|
|
||||||
|
NRRGenerator(ecgsyns::RRGenerator<double> 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<double>::Segment *segments;
|
||||||
|
WRRSeries(std::size_t size, std::size_t output_size, std::size_t nsegments,
|
||||||
|
const ecgsyns::RRSeries<double>::Segment *segments)
|
||||||
|
: WObject{type::kRRSeries}, size{size}, output_size{output_size},
|
||||||
|
nsegments{nsegments}, segments{segments} {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NRRSeries : WRRSeries {
|
||||||
|
ecgsyns::RRSeries<double> obj;
|
||||||
|
|
||||||
|
NRRSeries(ecgsyns::RRSeries<double> o)
|
||||||
|
: WRRSeries{o.size(), o.output_size(), o.segments_size(),
|
||||||
|
o.segments_data()},
|
||||||
|
obj{std::move(o)} {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
int ecgsyn() {
|
using namespace ecgsyns;
|
||||||
|
|
||||||
|
WRRGenerator *rr_gen_new(const TimeParameters *time_params) {
|
||||||
|
return new NRRGenerator{ecgsyns::RRGenerator<double>{*time_params}};
|
||||||
|
}
|
||||||
|
|
||||||
|
WRRSeries *rr_gen_new_series(WRRGenerator *generator,
|
||||||
|
const RRParameters *params) {
|
||||||
|
return new NRRSeries{
|
||||||
|
static_cast<NRRGenerator *>(generator)->obj.generate(*params)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void rr_gen_generate(WRRGenerator *generator, const RRParameters *params,
|
||||||
|
double *output) {
|
||||||
|
auto &rr = static_cast<NRRGenerator *>(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<const NRRGenerator *>(o);
|
||||||
|
break;
|
||||||
|
case WObject::type::kRRSeries:
|
||||||
|
delete static_cast<const NRRSeries *>(o);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using WasmParameters = Parameters<double, WasmSpan<const Attractor<double>>>;
|
||||||
|
static_assert(is_wasm_bindable_v<WasmParameters>);
|
||||||
|
|
||||||
|
void ecgsyn(const WasmParameters *params, WRRSeries *rr_series,
|
||||||
|
double *output) {
|
||||||
|
|
||||||
|
auto &rr = static_cast<NRRSeries *>(rr_series)->obj;
|
||||||
|
ecgsyns::generate(*params, rr, {output, rr.output_size()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ECGSYN_HOST_BUILD)
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
int main() {
|
||||||
using namespace ecgsyns;
|
using namespace ecgsyns;
|
||||||
|
|
||||||
RRGenerator<float> rr_gen{TimeParameters{}};
|
TimeParameters time_params{};
|
||||||
const auto rr = rr_gen.generateSignal(RRParameters{});
|
time_params.num_beats = 4;
|
||||||
|
auto rr = rr_init(&time_params);
|
||||||
|
|
||||||
RRSeries<float> rr_series{TimeParameters{}, RRParameters{},
|
const RRParameters rr_params{};
|
||||||
XoshiroCpp::Xoshiro256Plus{}, rr};
|
auto rrs = rr_generate(rr, &rr_params);
|
||||||
|
|
||||||
std::printf("%f", rr_series(0.0));
|
Parameters params;
|
||||||
// XoshiroCpp::DoubleFromBits();
|
std::vector<double> output(rrs->value.output_size());
|
||||||
std::printf("hello world\n");
|
generate(params, rrs->value, std::span(output));
|
||||||
return 69;
|
std::ofstream f{"ecg.csv"};
|
||||||
}
|
for (const auto &x : output) {
|
||||||
|
f << x << '\n';
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -62,10 +62,15 @@ template <typename T> struct DormandPrince {
|
|||||||
90730570.0 / 29380423.0, -8293050.0 / 29380423.0}}};
|
90730570.0 / 29380423.0, -8293050.0 / 29380423.0}}};
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T> struct scalar_type {
|
template <typename T, typename = void> struct scalar_type {
|
||||||
using type = T;
|
using type = T;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct scalar_type<T, std::void_t<typename T::value_type>> {
|
||||||
|
using type = typename T::value_type;
|
||||||
|
};
|
||||||
|
|
||||||
template <typename T, std::size_t N> struct scalar_type<T[N]> {
|
template <typename T, std::size_t N> struct scalar_type<T[N]> {
|
||||||
using type = T;
|
using type = T;
|
||||||
};
|
};
|
||||||
@@ -269,10 +274,6 @@ inline std::floating_point auto inf_norm(std::floating_point auto v) {
|
|||||||
return std::abs(v);
|
return std::abs(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T> inline auto inf_norm(const OdeVector<T> &v) {
|
|
||||||
return inf_norm(static_cast<const T &>(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename E> inline E inf_norm(const Vec2<E> &v) {
|
template <typename E> inline E inf_norm(const Vec2<E> &v) {
|
||||||
return std::max(std::abs(v.x), std::abs(v.y));
|
return std::max(std::abs(v.x), std::abs(v.y));
|
||||||
}
|
}
|
||||||
@@ -281,6 +282,10 @@ template <typename E> inline E inf_norm(const Vec3<E> &v) {
|
|||||||
return std::max({std::abs(v.x), std::abs(v.y), std::abs(v.z)});
|
return std::max({std::abs(v.x), std::abs(v.y), std::abs(v.z)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T> inline auto inf_norm(const OdeVector<T> &v) {
|
||||||
|
return inf_norm(static_cast<const T &>(v));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace func
|
} // namespace func
|
||||||
|
|
||||||
template <typename Vector, typename Scalar = scalar_type_t<Vector>,
|
template <typename Vector, typename Scalar = scalar_type_t<Vector>,
|
||||||
|
|||||||
@@ -11,16 +11,18 @@
|
|||||||
"./ecgsyn.wasm": "./dist/ecgsyn.wasm"
|
"./ecgsyn.wasm": "./dist/ecgsyn.wasm"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cmake --build build && tsc && cp -f build/ecgsyn.wasm ./dist",
|
"build": "cmake --build cmake-build-release-wasm && tsc && cp -f cmake-build-release-wasm/ecgsyn.wasm ./dist",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write \"*.{js,ts,json,css,yml,yaml}\" \"**/*.{js,ts,json,css,yml.yaml}\"",
|
"format": "prettier --write \"*.{js,ts,json,css,yml,yaml}\" \"**/*.{js,ts,json,css,yml.yaml}\"",
|
||||||
"serve": "http-server -c-1",
|
"serve": "http-server -c-1",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"watch": "tsc -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.13.0"
|
"typescript-eslint": "^8.13.0"
|
||||||
|
|||||||
141
pnpm-lock.yaml
generated
141
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
http-server:
|
http-server:
|
||||||
specifier: ^14.1.1
|
specifier: ^14.1.1
|
||||||
version: 14.1.1
|
version: 14.1.1
|
||||||
|
nodemon:
|
||||||
|
specifier: ^3.1.7
|
||||||
|
version: 3.1.7
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
@@ -172,6 +175,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
@@ -185,6 +192,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
binary-extensions@2.3.0:
|
||||||
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
brace-expansion@1.1.11:
|
brace-expansion@1.1.11:
|
||||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||||
|
|
||||||
@@ -207,6 +218,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -350,6 +365,11 @@ packages:
|
|||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
@@ -375,6 +395,10 @@ packages:
|
|||||||
graphemer@1.4.0:
|
graphemer@1.4.0:
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||||
|
|
||||||
|
has-flag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
has-flag@4.0.0:
|
has-flag@4.0.0:
|
||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -415,6 +439,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -427,6 +454,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -505,6 +536,15 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
nodemon@3.1.7:
|
||||||
|
resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
normalize-path@3.0.0:
|
||||||
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
object-inspect@1.13.3:
|
object-inspect@1.13.3:
|
||||||
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
|
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -554,6 +594,9 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
pstree.remy@1.1.8:
|
||||||
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -565,6 +608,10 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
requires-port@1.0.0:
|
requires-port@1.0.0:
|
||||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
|
||||||
@@ -609,10 +656,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
strip-json-comments@3.1.1:
|
strip-json-comments@3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -624,6 +679,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
touch@3.1.1:
|
||||||
|
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ts-api-utils@1.4.0:
|
ts-api-utils@1.4.0:
|
||||||
resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
|
resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -648,6 +707,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
undefsafe@2.0.5:
|
||||||
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
union@0.5.0:
|
union@0.5.0:
|
||||||
resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==}
|
resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -687,7 +749,7 @@ snapshots:
|
|||||||
'@eslint/config-array@0.18.0':
|
'@eslint/config-array@0.18.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.4
|
'@eslint/object-schema': 2.1.4
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -697,7 +759,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@3.1.0':
|
'@eslint/eslintrc@3.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
espree: 10.3.0
|
espree: 10.3.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -769,7 +831,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.13.0
|
'@typescript-eslint/types': 8.13.0
|
||||||
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
|
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.13.0
|
'@typescript-eslint/visitor-keys': 8.13.0
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
eslint: 9.14.0
|
eslint: 9.14.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
@@ -785,7 +847,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
|
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
|
||||||
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
|
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
ts-api-utils: 1.4.0(typescript@5.6.3)
|
ts-api-utils: 1.4.0(typescript@5.6.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
@@ -799,7 +861,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.13.0
|
'@typescript-eslint/types': 8.13.0
|
||||||
'@typescript-eslint/visitor-keys': 8.13.0
|
'@typescript-eslint/visitor-keys': 8.13.0
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
@@ -843,6 +905,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
dependencies:
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
async@2.6.4:
|
async@2.6.4:
|
||||||
@@ -855,6 +922,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.1.2
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
brace-expansion@1.1.11:
|
brace-expansion@1.1.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
@@ -883,6 +952,18 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
anymatch: 3.1.3
|
||||||
|
braces: 3.0.3
|
||||||
|
glob-parent: 5.1.2
|
||||||
|
is-binary-path: 2.1.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readdirp: 3.6.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -903,9 +984,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.3.7:
|
debug@4.3.7(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 5.5.0
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
@@ -949,7 +1032,7 @@ snapshots:
|
|||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.5
|
cross-spawn: 7.0.5
|
||||||
debug: 4.3.7
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.2.0
|
eslint-scope: 8.2.0
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
@@ -1032,6 +1115,9 @@ snapshots:
|
|||||||
|
|
||||||
follow-redirects@1.15.9: {}
|
follow-redirects@1.15.9: {}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
@@ -1058,6 +1144,8 @@ snapshots:
|
|||||||
|
|
||||||
graphemer@1.4.0: {}
|
graphemer@1.4.0: {}
|
||||||
|
|
||||||
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
@@ -1109,6 +1197,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
@@ -1118,6 +1208,10 @@ snapshots:
|
|||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
binary-extensions: 2.3.0
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-glob@4.0.3:
|
is-glob@4.0.3:
|
||||||
@@ -1182,6 +1276,21 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
nodemon@3.1.7:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 3.6.0
|
||||||
|
debug: 4.3.7(supports-color@5.5.0)
|
||||||
|
ignore-by-default: 1.0.1
|
||||||
|
minimatch: 3.1.2
|
||||||
|
pstree.remy: 1.1.8
|
||||||
|
semver: 7.6.3
|
||||||
|
simple-update-notifier: 2.0.0
|
||||||
|
supports-color: 5.5.0
|
||||||
|
touch: 3.1.1
|
||||||
|
undefsafe: 2.0.5
|
||||||
|
|
||||||
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
object-inspect@1.13.3: {}
|
object-inspect@1.13.3: {}
|
||||||
|
|
||||||
opener@1.5.2: {}
|
opener@1.5.2: {}
|
||||||
@@ -1225,6 +1334,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.3.3: {}
|
prettier@3.3.3: {}
|
||||||
|
|
||||||
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
@@ -1233,6 +1344,10 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
requires-port@1.0.0: {}
|
requires-port@1.0.0: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
@@ -1273,8 +1388,16 @@ snapshots:
|
|||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
object-inspect: 1.13.3
|
object-inspect: 1.13.3
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.6.3
|
||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 3.0.0
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
@@ -1285,6 +1408,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
touch@3.1.1: {}
|
||||||
|
|
||||||
ts-api-utils@1.4.0(typescript@5.6.3):
|
ts-api-utils@1.4.0(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
@@ -1306,6 +1431,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.6.3: {}
|
typescript@5.6.3: {}
|
||||||
|
|
||||||
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
union@0.5.0:
|
union@0.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
qs: 6.13.0
|
qs: 6.13.0
|
||||||
|
|||||||
333
src/index.ts
333
src/index.ts
@@ -2,7 +2,13 @@ export interface Exports extends WebAssembly.Exports {
|
|||||||
memory: WebAssembly.Memory;
|
memory: WebAssembly.Memory;
|
||||||
_initialize(): void;
|
_initialize(): void;
|
||||||
|
|
||||||
ecgsyn(): number;
|
rr_gen_new(time_params: number): number;
|
||||||
|
rr_gen_new_series(gen: number, params: number): number;
|
||||||
|
rr_gen_generate(gen: number, params: number, output: number): void;
|
||||||
|
destroy_obj(obj: number): void;
|
||||||
|
ecgsyn(params: number, rr_series: number, output: number): void;
|
||||||
|
realloc(ptr: number, size: number): number;
|
||||||
|
free(ptr: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let exports: Exports;
|
let exports: Exports;
|
||||||
@@ -103,7 +109,207 @@ class WASI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function load(): Promise<Exports> {
|
export interface Attractor {
|
||||||
|
theta: number;
|
||||||
|
a: number;
|
||||||
|
b: number;
|
||||||
|
thetaRf: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attractor(deg: number, a: number, b: number, thetaRf = 0): Attractor {
|
||||||
|
return { theta: (deg * Math.PI) / 180.0, a, b, thetaRf };
|
||||||
|
}
|
||||||
|
|
||||||
|
const freeRegistry = new FinalizationRegistry((ptr: number) => exports.free(ptr));
|
||||||
|
|
||||||
|
type Realloc = (p: number, s: number) => number;
|
||||||
|
|
||||||
|
export class TimeParameters {
|
||||||
|
// layout: (num_beats: i32, sr_internal: i32, decimate_factor: i32, hr_mean: f64, hr_std: f64, seed: i64)
|
||||||
|
static readonly #SIZE = 40;
|
||||||
|
#view: DataView;
|
||||||
|
|
||||||
|
constructor(exports: Exports) {
|
||||||
|
const ptr = exports.realloc(0, TimeParameters.#SIZE);
|
||||||
|
freeRegistry.register(this, ptr);
|
||||||
|
this.#view = new DataView(exports.memory.buffer, ptr, TimeParameters.#SIZE);
|
||||||
|
this.numBeats = 12;
|
||||||
|
this.srInternal = 512;
|
||||||
|
this.decimateFactor = 2;
|
||||||
|
this.hrMean = 60.0;
|
||||||
|
this.hrStd = 1.0;
|
||||||
|
this.seed = BigInt(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
set numBeats(value: number) {
|
||||||
|
this.#view.setInt32(0, value, true);
|
||||||
|
}
|
||||||
|
get numBeats(): number {
|
||||||
|
return this.#view.getInt32(0, true);
|
||||||
|
}
|
||||||
|
set srInternal(value: number) {
|
||||||
|
this.#view.setInt32(4, value, true);
|
||||||
|
}
|
||||||
|
get srInternal(): number {
|
||||||
|
return this.#view.getInt32(4, true);
|
||||||
|
}
|
||||||
|
set decimateFactor(value: number) {
|
||||||
|
this.#view.setInt32(8, value, true);
|
||||||
|
}
|
||||||
|
get decimateFactor(): number {
|
||||||
|
return this.#view.getInt32(8, true);
|
||||||
|
}
|
||||||
|
set hrMean(value: number) {
|
||||||
|
this.#view.setFloat64(16, value, true);
|
||||||
|
}
|
||||||
|
get hrMean(): number {
|
||||||
|
return this.#view.getFloat64(16, true);
|
||||||
|
}
|
||||||
|
set hrStd(value: number) {
|
||||||
|
this.#view.setFloat64(24, value, true);
|
||||||
|
}
|
||||||
|
get hrStd(): number {
|
||||||
|
return this.#view.getFloat64(24, true);
|
||||||
|
}
|
||||||
|
set seed(value: bigint) {
|
||||||
|
this.#view.setBigUint64(32, value, true);
|
||||||
|
}
|
||||||
|
get seed(): bigint {
|
||||||
|
return this.#view.getBigInt64(32, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get _ptr(): number {
|
||||||
|
return this.#view.byteOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RRParameters {
|
||||||
|
// layout: (flo: f64, flostd: f64, fhi: f64, fhistd: f64, lf_hf_ratio: f64)
|
||||||
|
static readonly #SIZE = 40;
|
||||||
|
#view: DataView;
|
||||||
|
|
||||||
|
constructor(exports: Exports) {
|
||||||
|
const ptr = exports.realloc(0, RRParameters.#SIZE);
|
||||||
|
freeRegistry.register(this, ptr);
|
||||||
|
this.#view = new DataView(exports.memory.buffer, ptr, RRParameters.#SIZE);
|
||||||
|
this.flo = 0.04;
|
||||||
|
this.flostd = 0.01;
|
||||||
|
this.fhi = 0.15;
|
||||||
|
this.fhistd = 0.025;
|
||||||
|
this.lfHfRatio = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set flo(value: number) {
|
||||||
|
this.#view.setFloat64(0, value, true);
|
||||||
|
}
|
||||||
|
get flo(): number {
|
||||||
|
return this.#view.getFloat64(0, true);
|
||||||
|
}
|
||||||
|
set flostd(value: number) {
|
||||||
|
this.#view.setFloat64(8, value, true);
|
||||||
|
}
|
||||||
|
get flostd(): number {
|
||||||
|
return this.#view.getFloat64(8, true);
|
||||||
|
}
|
||||||
|
set fhi(value: number) {
|
||||||
|
this.#view.setFloat64(16, value, true);
|
||||||
|
}
|
||||||
|
get fhi(): number {
|
||||||
|
return this.#view.getFloat64(16, true);
|
||||||
|
}
|
||||||
|
set fhistd(value: number) {
|
||||||
|
this.#view.setFloat64(24, value, true);
|
||||||
|
}
|
||||||
|
get fhistd(): number {
|
||||||
|
return this.#view.getFloat64(24, true);
|
||||||
|
}
|
||||||
|
set lfHfRatio(value: number) {
|
||||||
|
this.#view.setFloat64(32, value, true);
|
||||||
|
}
|
||||||
|
get lfHfRatio(): number {
|
||||||
|
return this.#view.getFloat64(32, true);
|
||||||
|
}
|
||||||
|
get _ptr(): number {
|
||||||
|
return this.#view.byteOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Parameters {
|
||||||
|
// layout (min: f64, max: f64, noiseAmplitude: f64, ptr: i32, n: i32)
|
||||||
|
static readonly #SIZE = 32;
|
||||||
|
static readonly #ATTRACTOR_SIZE = 32;
|
||||||
|
|
||||||
|
attractors: Attractor[] = [
|
||||||
|
attractor(-70.0, 1.2, 0.25, 0.25),
|
||||||
|
attractor(-15.0, -5.0, 0.1, 0.5),
|
||||||
|
attractor(0.0, 30.0, 0.1),
|
||||||
|
attractor(15.0, -7.5, 0.1, 0.5),
|
||||||
|
attractor(100.0, 0.75, 0.4, 0.25),
|
||||||
|
];
|
||||||
|
#view: DataView;
|
||||||
|
readonly #realloc: Realloc;
|
||||||
|
|
||||||
|
static #size(n: number): number {
|
||||||
|
return Parameters.#SIZE + n * Parameters.#ATTRACTOR_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(exports: Exports) {
|
||||||
|
this.#realloc = exports.realloc.bind(exports);
|
||||||
|
const sz = Parameters.#size(this.attractors.length);
|
||||||
|
const ptr = this.#realloc(0, sz);
|
||||||
|
freeRegistry.register(this, ptr, this);
|
||||||
|
|
||||||
|
this.#view = new DataView(exports.memory.buffer, ptr, sz);
|
||||||
|
this.#view.setUint32(24, this.#view.byteOffset + Parameters.#SIZE, true);
|
||||||
|
|
||||||
|
this.range = { min: -0.4, max: 1.2 };
|
||||||
|
this.noiseAmplitude = 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
set range(value: { min: number; max: number }) {
|
||||||
|
this.#view.setFloat64(0, value.min, true);
|
||||||
|
this.#view.setFloat64(8, value.max, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get range(): { min: number; max: number } {
|
||||||
|
return { min: this.#view.getFloat64(0, true), max: this.#view.getFloat64(8, true) };
|
||||||
|
}
|
||||||
|
|
||||||
|
set noiseAmplitude(value: number) {
|
||||||
|
this.#view.setFloat64(16, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get noiseAmplitude(): number {
|
||||||
|
return this.#view.getFloat64(16, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setAttractors(): void {
|
||||||
|
if (this.#view.byteLength < Parameters.#size(this.attractors.length)) {
|
||||||
|
const sz = Parameters.#size(this.attractors.length);
|
||||||
|
const ptr = this.#realloc(this.#view.byteOffset, sz);
|
||||||
|
freeRegistry.unregister(this);
|
||||||
|
freeRegistry.register(this, ptr, this);
|
||||||
|
this.#view = new DataView(this.#view.buffer, ptr, sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#view.setUint32(28, this.attractors.length, true);
|
||||||
|
let offset = Parameters.#SIZE;
|
||||||
|
for (const a of this.attractors) {
|
||||||
|
this.#view.setFloat64(offset, a.theta, true);
|
||||||
|
this.#view.setFloat64(offset + 8, a.a, true);
|
||||||
|
this.#view.setFloat64(offset + 16, a.b, true);
|
||||||
|
this.#view.setFloat64(offset + 24, a.thetaRf, true);
|
||||||
|
offset += Parameters.#ATTRACTOR_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get _ptr(): number {
|
||||||
|
this.#setAttractors();
|
||||||
|
return this.#view.byteOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(): Promise<Exports> {
|
||||||
if (!exports) {
|
if (!exports) {
|
||||||
const url = new URL("./ecgsyn.wasm", import.meta.url);
|
const url = new URL("./ecgsyn.wasm", import.meta.url);
|
||||||
const wasi = new WASI();
|
const wasi = new WASI();
|
||||||
@@ -114,3 +320,126 @@ export default async function load(): Promise<Exports> {
|
|||||||
}
|
}
|
||||||
return exports;
|
return exports;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const destroyRegistry = new FinalizationRegistry((ptr: number) => exports.destroy_obj(ptr));
|
||||||
|
|
||||||
|
export class ECGSyn {
|
||||||
|
#rrGenPtr: number;
|
||||||
|
#rrSeriesPtr: number;
|
||||||
|
#signal: Float64Array;
|
||||||
|
#view: DataView;
|
||||||
|
|
||||||
|
readonly timeParams: TimeParameters;
|
||||||
|
readonly rrParams: RRParameters;
|
||||||
|
readonly parameters: Parameters;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.timeParams = new TimeParameters(exports);
|
||||||
|
this.rrParams = new RRParameters(exports);
|
||||||
|
this.parameters = new Parameters(exports);
|
||||||
|
|
||||||
|
this.#rrGenPtr = 0;
|
||||||
|
this.#rrSeriesPtr = 0;
|
||||||
|
|
||||||
|
this.#signal = new Float64Array(0);
|
||||||
|
this.#view = new DataView(exports.memory.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
rrProcess(): void {
|
||||||
|
const timeParams = this.timeParams;
|
||||||
|
const rrParams = this.rrParams;
|
||||||
|
timeParams.decimateFactor = 1;
|
||||||
|
timeParams.hrMean = 60;
|
||||||
|
timeParams.numBeats = 10;
|
||||||
|
timeParams.seed = BigInt(20);
|
||||||
|
|
||||||
|
this.#rrGenPtr = exports.rr_gen_new(timeParams._ptr);
|
||||||
|
destroyRegistry.register(this, this.#rrGenPtr);
|
||||||
|
this.#rrSeriesPtr = exports.rr_gen_new_series(this.#rrGenPtr, rrParams._ptr);
|
||||||
|
destroyRegistry.register(this, this.#rrSeriesPtr);
|
||||||
|
|
||||||
|
const length = this.#view.getUint32(this.#rrSeriesPtr + 4, true);
|
||||||
|
const mem = exports.realloc(0, length * Float64Array.BYTES_PER_ELEMENT);
|
||||||
|
this.#signal = new Float64Array(exports.memory.buffer, mem, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
compute(): void {
|
||||||
|
const params = this.parameters;
|
||||||
|
params.noiseAmplitude = 0.0;
|
||||||
|
|
||||||
|
exports.ecgsyn(params._ptr, this.#rrSeriesPtr, this.#signal.byteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): void {
|
||||||
|
const data = this.#signal;
|
||||||
|
|
||||||
|
const canvas = document.getElementById("output") as HTMLCanvasElement;
|
||||||
|
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const minValue = -0.4;
|
||||||
|
const maxValue = 1.2;
|
||||||
|
const valueRange = maxValue - minValue;
|
||||||
|
|
||||||
|
const padding = 40;
|
||||||
|
const plotWidth = canvas.width - padding * 2;
|
||||||
|
const plotHeight = canvas.height - padding * 2;
|
||||||
|
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
ctx.moveTo(padding, padding);
|
||||||
|
ctx.lineTo(padding, canvas.height - padding);
|
||||||
|
|
||||||
|
ctx.lineTo(canvas.width - padding, canvas.height - padding);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const value = minValue + (valueRange * i) / 8;
|
||||||
|
const y = canvas.height - padding - (i * plotHeight) / 8;
|
||||||
|
ctx.fillText(value.toFixed(1), padding - 30, y + 4);
|
||||||
|
|
||||||
|
ctx.strokeStyle = "lightgray";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(padding, y);
|
||||||
|
ctx.lineTo(canvas.width - padding, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// X axis labels
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const x = padding + (i * plotWidth) / 8;
|
||||||
|
const value = ((i * data.length) / (8 * 512)).toFixed(2);
|
||||||
|
ctx.fillText(value, x - 10, canvas.height - padding + 20);
|
||||||
|
|
||||||
|
// Grid lines
|
||||||
|
ctx.strokeStyle = "lightgray";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, padding);
|
||||||
|
ctx.lineTo(x, canvas.height - padding);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#2563eb";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
const initialX = padding;
|
||||||
|
const initialY = canvas.height - padding - ((data[0] - minValue) * plotHeight) / valueRange;
|
||||||
|
ctx.moveTo(initialX, initialY);
|
||||||
|
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
const x = padding + (i * plotWidth) / data.length;
|
||||||
|
const y = canvas.height - padding - ((data[i] - minValue) * plotHeight) / valueRange;
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user