19 [[nodiscard]]
inline double ramp(
double edge0,
double edge1,
double x)
noexcept
22 return x < edge0 ? 0.0 : 1.0;
23 const double t = std::clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
24 return t * t * (3.0 - 2.0 * t);
67 [[nodiscard]] Eigen::VectorXd
delta(
const Eigen::VectorXd&
a,
const Eigen::VectorXd&
b)
const
69 Eigen::VectorXd d =
a -
b;
70 if (
periods.size() == d.size()) {
71 for (Eigen::Index i = 0; i < d.size(); ++i) {
74 d(i) = std::remainder(d(i), p);
87 [[nodiscard]]
float distance_sq(
const Eigen::VectorXd&
a,
const Eigen::VectorXd&
b)
const
89 const Eigen::VectorXd d =
delta(
a,
b);
91 for (Eigen::Index i = 0; i < d.size(); ++i) {
93 const double s = w * d(i);
96 return static_cast<float>(acc);
105 return { .
weights = Eigen::VectorXd::Ones(dimensions),
106 .periods = Eigen::VectorXd::Zero(dimensions) };
172 const Eigen::VectorXd& point,
176 for (Eigen::Index i = 0; i < point.size(); ++i) {
177 const double centre = 0.5 * (extent.
lower(i) + extent.
upper(i));
178 const double half = 0.5 * (extent.
upper(i) - extent.
lower(i));
180 double offset = point(i) - centre;
181 if (metric.
periods.size() == point.size() && metric.
periods(i) > 0.0)
184 const double outside = std::abs(
offset) - half;
185 const double soft = (extent.
softness.size() == point.size())
189 const double axis = (outside <= 0.0)
191 : 1.0 -
detail::ramp(0.0, std::max(soft, 1e-12), outside);
193 worst = std::min(worst, axis);
210 const Eigen::VectorXd& point,
213 const Eigen::VectorXd d = metric.
delta(point, extent.
centre);
215 for (Eigen::Index i = 0; i < d.size(); ++i) {
216 const double r = (extent.
radii(i) > 0.0) ? extent.
radii(i) : 1e-12;
217 const double s = d(i) / r;
241 std::span<const Eigen::VectorXd> samples,
242 const Eigen::VectorXd& periods = {})
247 const Eigen::Index n = samples.front().size();
248 Eigen::VectorXd
mean = Eigen::VectorXd::Zero(n);
249 for (
const auto& s : samples)
251 mean /=
static_cast<double>(samples.size());
253 Eigen::VectorXd var = Eigen::VectorXd::Zero(n);
254 for (
const auto& s : samples) {
255 const Eigen::VectorXd d = s -
mean;
256 var += d.cwiseProduct(d);
258 var /=
static_cast<double>(samples.size());
260 Eigen::VectorXd weights(n);
261 for (Eigen::Index i = 0; i < n; ++i) {
262 const double sd = std::sqrt(var(i));
263 weights(i) = (sd > 1e-12) ? (1.0 / sd) : 1.0;
266 return { .weights = weights,
267 .periods = (periods.size() == n) ? periods : Eigen::VectorXd::Zero(n) };
285 std::span<const Eigen::VectorXd> samples,
286 double coverage = 0.9,
287 double softness_fraction = 0.15)
292 const Eigen::Index n = samples.front().size();
294 .
lower = Eigen::VectorXd::Zero(n),
295 .upper = Eigen::VectorXd::Zero(n),
296 .softness = Eigen::VectorXd::Zero(n)
299 const double tail = 0.5 * (1.0 - std::clamp(coverage, 0.0, 1.0));
300 std::vector<double> axis;
301 axis.reserve(samples.size());
303 for (Eigen::Index i = 0; i < n; ++i) {
305 for (
const auto& s : samples)
306 axis.push_back(s(i));
307 std::ranges::sort(axis);
309 const auto last =
static_cast<double>(axis.size() - 1);
310 const auto lo_idx =
static_cast<size_t>(std::floor(tail * last));
311 const auto hi_idx =
static_cast<size_t>(std::ceil((1.0 - tail) * last));
313 extent.lower(i) = axis[lo_idx];
314 extent.upper(i) = axis[hi_idx];
315 extent.softness(i) = std::max(
316 (extent.upper(i) - extent.lower(i)) * softness_fraction, 1e-9);
338 std::span<const Eigen::VectorXd> samples,
339 double radius_sigma = 2.0,
340 double softness = 0.5)
345 const Eigen::Index n = samples.front().size();
346 Eigen::VectorXd
mean = Eigen::VectorXd::Zero(n);
347 for (
const auto& s : samples)
349 mean /=
static_cast<double>(samples.size());
351 Eigen::VectorXd var = Eigen::VectorXd::Zero(n);
352 for (
const auto& s : samples) {
353 const Eigen::VectorXd d = s -
mean;
354 var += d.cwiseProduct(d);
356 var /=
static_cast<double>(samples.size());
358 Eigen::VectorXd radii(n);
359 for (Eigen::Index i = 0; i < n; ++i)
360 radii(i) = std::max(radius_sigma * std::sqrt(var(i)), 1e-9);
362 return { .centre =
mean, .radii = radii, .softness = softness };
ThresholdLatch(double engage, double release)
Construct a latch.
bool update(double value)
Feed one value and report the resulting state.
bool engaged() const
Current state without feeding a value.
void reset()
Force the latch off, for use on a known discontinuity.
Two-level hysteresis turning a continuous value into a held boolean.
double ramp(double edge0, double edge1, double x) noexcept
Hermite ramp from 0 at edge0 to 1 at edge1.
EllipsoidExtent fit_ellipsoid(std::span< const Eigen::VectorXd > samples, double radius_sigma=2.0, double softness=0.5)
Fit an ellipsoid extent from the mean and spread of a sample set.
double membership(const BoxExtent &extent, const Eigen::VectorXd &point, const FeatureMetric &metric)
Graded membership of a point in a box extent.
FeatureMetric fit_metric(std::span< const Eigen::VectorXd > samples, const Eigen::VectorXd &periods={})
Derive per-axis metric weights from the spread of a sample set.
BoxExtent fit_box(std::span< const Eigen::VectorXd > samples, double coverage=0.9, double softness_fraction=0.15)
Fit a box extent covering a central fraction of a sample set.
std::vector< double > normalized(const std::vector< double > &data, double target_peak)
Normalize single-channel data (non-destructive)
double mean(const std::vector< double > &data)
Calculate mean of single-channel data.
Eigen::VectorXd softness
Outward margin per axis over which membership falls to zero. Empty means hard.
Eigen::VectorXd upper
Inclusive upper bound per axis.
Eigen::VectorXd lower
Inclusive lower bound per axis.
An axis-aligned interval per axis, with a soft outer margin.
Eigen::VectorXd radii
Per-axis extent at which normalized distance reaches one.
Eigen::VectorXd centre
Prototype point.
double softness
Additional normalized distance over which membership falls to zero.
A centre, a radius per axis, and a soft outer shell.
Eigen::VectorXd periods
Per-axis wrap period. Zero or empty means the axis is linear.
static FeatureMetric uniform(Eigen::Index dimensions)
A metric with unit weights and no wrapping.
Eigen::VectorXd weights
Per-axis scale applied before differencing. Empty means unit weights.
float distance_sq(const Eigen::VectorXd &a, const Eigen::VectorXd &b) const
Weighted squared distance under this metric.
Eigen::VectorXd delta(const Eigen::VectorXd &a, const Eigen::VectorXd &b) const
Componentwise difference, wrapped on any axis with a period.
Per-axis weighting and wrapping for a space whose axes carry different units.