From 9b35ba5266975cf951890492e086fbf53da84dfe Mon Sep 17 00:00:00 2001
From: Sam Yates <halfflat@gmail.com>
Date: Tue, 9 Nov 2021 21:55:34 +0800
Subject: [PATCH] Deal with zero radius points in a morphology (#1719)
* Add test that is sensitive to small radius loss of precision in ixa computation.
* Add test that checks for consistent behaviour when there is an (isolated) zero radius point in the morphology.
* Special case p==1 q==1 case in ratelem so that it can interpolate ixa in the presence of non-finite interpolants.
* Change naming in pw_element/pw_elements: 'element' refers to the extent+value pairs comprising a 'pw_elements' object; 'value' refers to the value associated with an element; 'extent' refers to the closed interval which is the support of the element.
* Allow values (but not extents) in a pw_elements object to be mutable, using proxies for iterator access.
* Write piecewise zip (pw_zip) in terms of a lazy pw_zip_view. Rename zip functions.
* Document pw_elements more thoroughly.
* Simplify and document embed_pwlin.cpp routines, expressing everything in terms of simple interpolate/integrate operations.
* Represent integrated inverse cross-sectional area in the embedding by multiple piecewise-rational functions over a branch, each of which contribute separately to an ixa-based integration, so that precision loss associated with small (or zero) radii can be avoided.
* Comment embed_pwlin.cpp more thoroughly.
Fixes #1526
---
arbor/fvm_layout.cpp | 6 +-
arbor/fvm_layout.hpp | 8 +-
arbor/morph/embed_pwlin.cpp | 342 +++++++-----
arbor/morph/place_pwlin.cpp | 7 +-
arbor/util/iterutil.hpp | 7 +-
arbor/util/piecewise.hpp | 819 +++++++++++++++++++++--------
arbor/util/ratelem.hpp | 50 +-
test/unit/test_morph_embedding.cpp | 70 +++
test/unit/test_piecewise.cpp | 220 +++++---
test/unit/test_ratelem.cpp | 32 +-
10 files changed, 1089 insertions(+), 472 deletions(-)
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 438f3313..d926192c 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -118,7 +118,7 @@ pw_elements<U> pw_over_cable(const mcable_map<T>& mm, mcable cable, U dflt_value
}
if (cable.prox_pos!=0 || cable.dist_pos!=1) {
- pw = zip(pw, pw_elements<>({cable.prox_pos, cable.dist_pos}));
+ pw = pw_zip_with(pw, pw_elements<>({cable.prox_pos, cable.dist_pos}));
}
return pw;
}
@@ -1315,8 +1315,8 @@ fvm_mechanism_data fvm_build_mechanism_data(
const mcable_map<init_ext_concentration>& econc_on_cable = initial_econc_map[ion];
const mcable_map<init_reversal_potential>& rvpot_on_cable = initial_rvpot_map[ion];
- auto pw_times = [](const pw_elements<double>& a, const pw_elements<double>& b) {
- return zip(a, b, [](double left, double right, pw_element<double> a, pw_element<double> b) { return a.element*b.element; });
+ auto pw_times = [](const pw_elements<double>& pwa, const pw_elements<double>& pwb) {
+ return pw_zip_with(pwa, pwb, [](std::pair<double, double>, double a, double b) { return a*b; });
};
for (auto i: count_along(config.cv)) {
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index dc9ffa82..8d1356fb 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -116,15 +116,15 @@ struct cv_geometry {
size_type location_cv(size_type cell_idx, mlocation loc, cv_prefer::type prefer) const {
auto& pw_cv_offset = branch_cv_map.at(cell_idx).at(loc.branch);
auto zero_extent = [&pw_cv_offset](auto j) {
- return pw_cv_offset.interval(j).first==pw_cv_offset.interval(j).second;
+ return pw_cv_offset.extent(j).first==pw_cv_offset.extent(j).second;
};
auto i = pw_cv_offset.index_of(loc.pos);
auto i_max = pw_cv_offset.size()-1;
- auto cv_prox = pw_cv_offset.interval(i).first;
+ auto cv_prox = pw_cv_offset.extent(i).first;
// index_of() should have returned right-most matching interval.
- arb_assert(i==i_max || loc.pos<pw_cv_offset.interval(i+1).first);
+ arb_assert(i==i_max || loc.pos<pw_cv_offset.extent(i+1).first);
using namespace cv_prefer;
switch (prefer) {
@@ -145,7 +145,7 @@ struct cv_geometry {
}
index_type cv_base = cell_cv_divs.at(cell_idx);
- return cv_base+pw_cv_offset[i].element;
+ return cv_base+pw_cv_offset.value(i);
}
};
diff --git a/arbor/morph/embed_pwlin.cpp b/arbor/morph/embed_pwlin.cpp
index 03393acc..dfe84c73 100644
--- a/arbor/morph/embed_pwlin.cpp
+++ b/arbor/morph/embed_pwlin.cpp
@@ -14,118 +14,72 @@
namespace arb {
+// Each integrable or interpolated quantity is represented by
+// a sequence of spans covering each branch, where a span is itself
+// a piecewise polynomial or rational function.
+//
+// The piecewise functions are represented by util::pw_elements<util::rat_element<p, q>>
+// objects. util::rat_element describes an order (p, q) rational function in terms of
+// the values of that function at p+q+1 equally spaced points along the element, including
+// the two endpoints.
+
using util::rat_element;
+using util::pw_elements;
-template <unsigned p, unsigned q>
-using pw_ratpoly = util::pw_elements<rat_element<p, q>>;
+// Represents piecewise rational function over a subset of a branch.
+// When the function represents an integral, the integral between two points
+// in the domain of a pw_ratpoly can be computed by taking the difference
+// in the interpolated value at the two points.
template <unsigned p, unsigned q>
-using branch_pw_ratpoly = std::vector<pw_ratpoly<p, q>>;
+using pw_ratpoly = util::pw_elements<rat_element<p, q>>;
-// Special case handling required for degenerate branches of length zero:
-template <typename Elem>
-static bool is_degenerate(const util::pw_elements<Elem>& pw) {
- return pw.bounds().second==0;
-}
+// One branch can be covered by more than one pw_ratpoly (of the same degree).
+// When the function represents an integral, the integral between two points
+// that do not lie in the same domain of a pw_ratpoly must be summed by
+// considering the contribution of each pw_ratpoly element separately. Multiple
+// pw_ratpoly elements over the same branch are required to avoid cases of loss
+// of precision and singularities.
template <unsigned p, unsigned q>
-double interpolate(const branch_pw_ratpoly<p, q>& f, unsigned bid, double pos) {
- const auto& pw = f.at(bid);
- if (is_degenerate(pw)) pos = 0;
+using branch_pw_spans = util::pw_elements<pw_ratpoly<p, q>>;
- auto [bounds, element] = pw(pos);
+struct embed_pwlin_data {
+ // Vectors below are all indexed by branch index.
- if (bounds.first==bounds.second) return element[0];
- else {
- double x = (pos-bounds.first)/(bounds.second-bounds.first);
- return element(x);
- }
-}
+ // Length and directed projection are piecewise linear, continuous, and
+ // monotonically increasing. They are represented by a single piecewise
+ // linear function per branch.
-// Length, area, and ixa are polynomial or rational polynomial functions of branch position,
-// continuos and monotonically increasing with respect to distance from root.
-//
-// Integration wrt a piecewise constant function is performed by taking the difference between
-// interpolated values at the end points of each constant interval.
+ std::vector<pw_ratpoly<1, 0>> length; // [µm]
+ std::vector<pw_ratpoly<1, 0>> directed_projection; // [µm]
-template <unsigned p, unsigned q>
-double integrate(const branch_pw_ratpoly<p, q>& f, unsigned bid, const pw_constant_fn& g) {
- double accum = 0;
- for (msize_t i = 0; i<g.size(); ++i) {
- std::pair<double, double> interval = g.interval(i);
- accum += g.element(i)*(interpolate(f, bid, interval.second)-interpolate(f, bid, interval.first));
- }
- return accum;
-}
+ // Radius is piecewise linear, but not necessarily continuous. Where the
+ // morphology describes different radii at the same point, the interpolated
+ // radius is regarded as being right-continuous, and corresponds to the
+ // value described by the last segment added that covers the point.
-// Performance note: when integrating over a cable within a branch, the code effectively
-// performs a linear search for the starting interval. This can be replaced with a binary
-// search for a small increase in code complexity.
+ std::vector<pw_ratpoly<1, 0>> radius; // [µm]
-template <unsigned p, unsigned q>
-double integrate(const branch_pw_ratpoly<p, q>& f, mcable c, const pw_constant_fn& g) {
- msize_t bid = c.branch;
- double accum = 0;
+ // Morphological surface area between points on a branch is given as
+ // the difference between the interpolated value of a piecewise quadratic
+ // function over the branch. The function is monotonically increasing,
+ // but not necessarily continuous. Where the value jumps, the function
+ // is interpreted as being right-continuous.
- for (msize_t i = 0; i<g.size(); ++i) {
- std::pair<double, double> interval = g.interval(i);
+ std::vector<pw_ratpoly<2, 0>> area; // [µm²]
- if (interval.second<c.prox_pos) {
- continue;
- }
- else if (interval.first>=c.dist_pos) {
- break;
- }
- else {
- interval.first = std::max(interval.first, c.prox_pos);
- interval.second = std::min(interval.second, c.dist_pos);
-
- if (interval.first<interval.second) {
- accum += g.element(i)*(interpolate(f, bid, interval.second)-interpolate(f, bid, interval.first));
- }
- }
- }
- return accum;
-}
+ // Integrated inverse cross-sectional area (ixa) between points on a branch
+ // is used to compute the conductance between points on the morphology.
+ // It is represented by an order (1, 1) rational function (i.e. a linear
+ // function divided by a linear function).
+ //
+ // Because a small radius can lead to very large (or infinite, if the
+ // radius is zero) values, one branch may be covered by multiple
+ // piecewise functions, and computing ixa may require summing the
+ // contributions from more than one.
-template <typename operation>
-mcable_list data_cmp(const branch_pw_ratpoly<1, 0>& f, unsigned bid, double val, operation op) {
- mcable_list L;
- const auto& pw = f.at(bid);
- for (const auto& piece: pw) {
- auto extents = piece.interval;
- auto left_val = piece.element(0);
- auto right_val = piece.element(1);
-
- if (!op(left_val, val) && !op(right_val, val)) {
- continue;
- }
- if (op(left_val, val) && op(right_val, val)) {
- L.push_back({bid, extents.first, extents.second});
- continue;
- }
-
- auto cable_loc = (val - left_val)/(right_val - left_val);
- auto edge = math::lerp(extents.first, extents.second, cable_loc);
-
- if (op(left_val, val)) {
- L.push_back({bid, extents.first, edge});
- continue;
- }
- if (!op(left_val, val)) {
- L.push_back({bid, edge, extents.second});
- continue;
- }
- }
- return L;
-}
-
-struct embed_pwlin_data {
- branch_pw_ratpoly<1, 0> length; // [µm]
- branch_pw_ratpoly<1, 0> directed_projection; // [µm]
- branch_pw_ratpoly<1, 0> radius; // [µm]
- branch_pw_ratpoly<2, 0> area; // [µm²]
- branch_pw_ratpoly<1, 1> ixa; // [1/µm]
+ std::vector<branch_pw_spans<1, 1>> ixa; // [1/µm]
explicit embed_pwlin_data(msize_t n_branch):
length(n_branch),
@@ -136,24 +90,88 @@ struct embed_pwlin_data {
{}
};
+// Interpolation
+
+// Value at pos on a branch interpolated from a piecewise rational function
+// which covers pos. pos is in [0, 1], and the bounds of the piecewise rational
+// function is an interval [a, b] with 0 ≤ a ≤ pos ≤ b ≤ 1.
+
+template <unsigned p, unsigned q>
+double interpolate(double pos, const pw_ratpoly<p, q>& f) {
+ auto [extent, poly] = f(pos);
+ auto [left, right] = extent;
+
+ return left==right? poly[0]: poly((pos-left)/(right-left));
+}
+
+// Integration
+
+// Integral of piecwise constant function g with respect to a measure determined
+// by a single piecewise monotonic right continuous rational function f, over
+// an interval [r, s]: ∫[r,s] g(x) df(x) (where [r, s] is the domain of g).
+
+template <unsigned p, unsigned q>
+double integrate(const pw_constant_fn& g, const pw_ratpoly<p, q>& f) {
+ double sum = 0;
+ for (auto&& [extent, gval]: g) {
+ sum += gval*(interpolate(extent.second, f)-interpolate(extent.first, f));
+ }
+ return sum;
+}
+
+// Integral of piecewise constant function g with respect to a measure determined
+// by a contiguous sequence of piecewise monotonic right continuous rational
+// functions fi with support [aⱼ, bⱼ], over an interval [r, s] with a₀ ≤ r ≤ s ≤ bₙ:
+// Σⱼ ∫[r,s]∩[aⱼ,bⱼ] g(x)dfⱼ(x) (where [r, s] is the domain of g).
+
+template <unsigned p, unsigned q>
+double integrate(const pw_constant_fn& g, const pw_elements<pw_ratpoly<p, q>>& fs) {
+ double sum = 0;
+ for (auto&& [extent, pw_pair]: pw_zip_range(g, fs)) {
+ auto [left, right] = extent;
+ if (left==right) continue;
+
+ double gval = pw_pair.first;
+ pw_ratpoly<p, q> f = pw_pair.second;
+ sum += gval*(interpolate(right, f)-interpolate(left, f));
+ }
+ return sum;
+}
+
+// Implementation of public embed_pwlin methods:
+
double embed_pwlin::radius(mlocation loc) const {
- return interpolate(data_->radius, loc.branch, loc.pos);
+ return interpolate(loc.pos, data_->radius.at(loc.branch));
}
double embed_pwlin::directed_projection(arb::mlocation loc) const {
- return interpolate(data_->directed_projection, loc.branch, loc.pos);
+ return interpolate(loc.pos, data_->directed_projection.at(loc.branch));
}
// Point to point integration:
double embed_pwlin::integrate_length(mlocation proximal, mlocation distal) const {
- return interpolate(data_->length, distal.branch, distal.pos) -
- interpolate(data_->length, proximal.branch, proximal.pos);
+ return interpolate(distal.pos, data_->length.at(distal.branch)) -
+ interpolate(proximal.pos, data_->length.at(proximal.branch));
}
double embed_pwlin::integrate_area(mlocation proximal, mlocation distal) const {
- return interpolate(data_->area, distal.branch, distal.pos) -
- interpolate(data_->area, proximal.branch, proximal.pos);
+ return interpolate(distal.pos, data_->area.at(distal.branch)) -
+ interpolate(proximal.pos, data_->area.at(proximal.branch));
+}
+
+// Integrate piecewise function over a branch:
+
+double embed_pwlin::integrate_length(msize_t bid, const pw_constant_fn& g) const {
+ return integrate(g, data_->length.at(bid));
+}
+
+double embed_pwlin::integrate_area(msize_t bid, const pw_constant_fn& g) const {
+ return integrate(g, data_->area.at(bid));
+}
+
+double embed_pwlin::integrate_ixa(msize_t bid, const pw_constant_fn& g) const {
+ return integrate(g, data_->ixa.at(bid));
}
// Integrate over cable:
@@ -170,36 +188,65 @@ double embed_pwlin::integrate_ixa(mcable c) const {
return integrate_ixa(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
}
-// Integrate piecewise function over a branch:
-
-double embed_pwlin::integrate_length(msize_t bid, const pw_constant_fn& g) const {
- return integrate(data_->length, bid, g);
-}
-
-double embed_pwlin::integrate_area(msize_t bid, const pw_constant_fn& g) const {
- return integrate(data_->area, bid, g);
-}
+// Integrate piecewise function over a cable:
-double embed_pwlin::integrate_ixa(msize_t bid, const pw_constant_fn& g) const {
- return integrate(data_->ixa, bid, g);
+static pw_constant_fn restrict(const pw_constant_fn& g, double left, double right) {
+ return pw_zip_with(g, pw_elements<void>{{left, right}});
}
-// Integrate piecewise function over a cable:
-
double embed_pwlin::integrate_length(mcable c, const pw_constant_fn& g) const {
- return integrate(data_->length, c, g);
+ return integrate_length(c.branch, restrict(g, c.prox_pos, c.dist_pos));
}
double embed_pwlin::integrate_area(mcable c, const pw_constant_fn& g) const {
- return integrate(data_->area, c, g);
+ return integrate_area(c.branch, restrict(g, c.prox_pos, c.dist_pos));
}
double embed_pwlin::integrate_ixa(mcable c, const pw_constant_fn& g) const {
- return integrate(data_->ixa, c, g);
+ return integrate_ixa(c.branch, restrict(g, c.prox_pos, c.dist_pos));
}
// Subregions defined by geometric inequalities:
+// Given a piecewise linear function f over a branch, a comparison operation op and a threshold v,
+// determine the subset of the branch where the op(f(x), v) is true.
+//
+// Functions are supplied for each branch via a branch-index vector of pw_ratpoly<1, 0> functions;
+// supplied branch id is the index into this vector, and is used to construct the cables
+// corresponding to the matching subset.
+
+template <typename operation>
+mcable_list data_cmp(const std::vector<pw_ratpoly<1, 0>>& f_on_branch, msize_t bid, double val, operation op) {
+ mcable_list L;
+ for (auto&& piece: f_on_branch.at(bid)) {
+ auto [left, right] = piece.extent;
+ auto left_val = piece.value(0);
+ auto right_val = piece.value(1);
+
+ if (!op(left_val, val) && !op(right_val, val)) {
+ continue;
+ }
+ if (op(left_val, val) && op(right_val, val)) {
+ L.push_back({bid, left, right});
+ continue;
+ }
+
+ auto cable_loc = (val - left_val)/(right_val - left_val);
+ auto edge = math::lerp(left, right, cable_loc);
+
+ if (op(left_val, val)) {
+ L.push_back({bid, left, edge});
+ continue;
+ }
+ if (!op(left_val, val)) {
+ L.push_back({bid, edge, right});
+ continue;
+ }
+ }
+ return L;
+}
+
+
mcable_list embed_pwlin::radius_cmp(msize_t bid, double val, comp_op op) const {
switch (op) {
case comp_op::lt: return data_cmp(data_->radius, bid, val, [](auto l, auto r){return l < r;});
@@ -275,11 +322,13 @@ embed_pwlin::embed_pwlin(const arb::morphology& m) {
segment_cables_[seg.id] = mcable{bid, pos0, pos1};
}
- double length_0 = parent==mnpos? 0: data_->length[parent].back().element[1];
+ double length_0 = parent==mnpos? 0: data_->length[parent].back().value[1];
data_->length[bid].push_back(0., 1, rat_element<1, 0>(length_0, length_0+branch_length));
- double area_0 = parent==mnpos? 0: data_->area[parent].back().element[2];
- double ixa_0 = parent==mnpos? 0: data_->ixa[parent].back().element[2];
+ double area_0 = parent==mnpos? 0: data_->area[parent].back().value[2];
+
+ double ixa_last = 0;
+ pw_ratpoly<1, 1> ixa_pw;
for (auto i: util::count_along(segments)) {
auto prox = segments[i].prox;
@@ -304,11 +353,52 @@ embed_pwlin::embed_pwlin(const arb::morphology& m) {
data_->area[bid].push_back(p0, p1, rat_element<2, 0>(area_0, area_half, area_1));
area_0 = area_1;
- // (Test for positive dx explicitly in case r0 is zero.)
- double ixa_half = ixa_0 + (dx>0? dx/(pi*r0*(r0+r1)): 0);
- double ixa_1 = ixa_0 + (dx>0? dx/(pi*r0*r1): 0);
- data_->ixa[bid].push_back(p0, p1, rat_element<1, 1>(ixa_0, ixa_half, ixa_1));
- ixa_0 = ixa_1;
+ if (dx>0) {
+ double ixa_0 = 0;
+ double ixa_half = dx/(pi*r0*(r0+r1));
+ double ixa_1 = dx/(pi*r0*r1);
+
+ if (r0==0 && r1==0) {
+ // ixa is just not defined on this segment; represent with all
+ // infinite node values.
+ ixa_0 = INFINITY;
+ ixa_half = INFINITY;
+ ixa_1 = INFINITY;
+ }
+ else if (r0==0) {
+ ixa_0 = -INFINITY;
+ ixa_half = -dx/(pi*r1*r1);
+ ixa_1 = 0;
+ }
+
+ // Start a new piecewise function if last ixa value is
+ // large compared to ixa_1 - ixa_0 (leading to loss
+ // of precision), or if ixa_0 is already non-zero (something
+ // above has declared we should start anew).
+
+ constexpr double max_ixa_ratio = 1e5;
+ if (!ixa_pw.empty() && ixa_0 == 0 && ixa_last<max_ixa_ratio*ixa_1) {
+ // Extend last pw representation on the branch.
+ ixa_0 += ixa_last;
+ ixa_half += ixa_last;
+ ixa_1 += ixa_last;
+ }
+ else {
+ // Start a new pw representation on the branch:
+ if (!ixa_pw.empty()) {
+ auto [left, right] = ixa_pw.bounds();
+ data_->ixa[bid].push_back(left, right, ixa_pw);
+ }
+ ixa_pw.clear();
+ }
+ ixa_pw.push_back(p0, p1, rat_element<1, 1>(ixa_0, ixa_half, ixa_1));
+ ixa_last = ixa_1;
+ }
+ }
+ // push last ixa pw function on the branch, if nonempty.
+ if (!ixa_pw.empty()) {
+ auto [left, right] = ixa_pw.bounds();
+ data_->ixa[bid].push_back(left, right, ixa_pw);
}
arb_assert((data_->radius[bid].size()>0));
diff --git a/arbor/morph/place_pwlin.cpp b/arbor/morph/place_pwlin.cpp
index 8bec27ca..9814aeb2 100644
--- a/arbor/morph/place_pwlin.cpp
+++ b/arbor/morph/place_pwlin.cpp
@@ -52,8 +52,8 @@ mpoint place_pwlin::at(mlocation loc) const {
const auto& pw_index = data_->segment_index.at(loc.branch);
double pos = is_degenerate(pw_index)? 0: loc.pos;
- auto [bounds, index] = pw_index(pos);
- return interpolate_segment(bounds, data_->segments.at(index), pos);
+ auto index = pw_index(pos);
+ return interpolate_segment(index.extent, data_->segments.at(index), pos);
}
std::vector<mpoint> place_pwlin::all_at(mlocation loc) const {
@@ -61,7 +61,8 @@ std::vector<mpoint> place_pwlin::all_at(mlocation loc) const {
const auto& pw_index = data_->segment_index.at(loc.branch);
double pos = is_degenerate(pw_index)? 0: loc.pos;
- for (auto [bounds, index]: util::make_range(pw_index.equal_range(pos))) {
+ for (auto index: util::make_range(pw_index.equal_range(pos))) {
+ auto bounds = index.extent;
auto seg = data_->segments.at(index);
// Add both ends of zero length segment, if they differ.
diff --git a/arbor/util/iterutil.hpp b/arbor/util/iterutil.hpp
index 1556a50f..14b6cde0 100644
--- a/arbor/util/iterutil.hpp
+++ b/arbor/util/iterutil.hpp
@@ -91,8 +91,11 @@ decltype(auto) back(Seq& seq) {
template <typename V>
struct pointer_proxy {
V v;
- pointer_proxy(const V& v): v(v) {}
- pointer_proxy(V&& v): v(std::move(v)) {}
+
+ template <typename... Args>
+ pointer_proxy(Args&&... args): v(std::forward<Args>(args)...) {}
+
+ V* operator->() { return &v; }
const V* operator->() const { return &v; }
};
diff --git a/arbor/util/piecewise.hpp b/arbor/util/piecewise.hpp
index f250c1b7..8f029b4f 100644
--- a/arbor/util/piecewise.hpp
+++ b/arbor/util/piecewise.hpp
@@ -1,15 +1,105 @@
#pragma once
-// Create/manipulate 1-d piece-wise defined objects.
+// Create/manipulate 1-d piecewise defined objects.
//
-// Using vectors everywhere here for ease; consider making
-// something more container/sequence-generic later.
+// A `pw_element<A>` describes a _value_ of type `A` and an _extent_ of
+// type std::pair<double, double>. An extent with value `{l, r}` has
+// l ≤ r and represents a closed interval [l, r] of the real line.
+//
+// For void `A`, `pw_element<void>` holds only the extent of an element.
+//
+// Once constructed, the value of a `pw_element<A>` (with `A` not void) is
+// mutable, and can be assigned directly with `operator=`, but the extent is
+// constant. A `pw_element<A>` can be implicitly converted to a value of type
+// `A`.
+//
+// A `pw_element<A>` can be bound via a structured binding to an extent
+// and value, or just an extent if A is void.
+//
+// A `pw_elements<A>` object comprises a contiguous sequence Eᵢ of
+// `pw_element<A>` elements. If Eᵢ has extent [lᵢ, rᵢ] and Eᵢ₊₁ has extent
+// [lᵢ₊₁, rᵢ₊₁] then rᵢ must equal lᵢ₊₁.
+//
+// The `value_type` of `pw_elements<A>` is the type of the elements, i.e.
+// `pw_element<A>`, as `pw_elements<A>` presents as a container.
+// To avoid ambiguity the type `A` is termed the _codomain_ of a `pw_elements<A>`
+// object.
+//
+// When the codomain is `void`, which is also the default for the type
+// parameter `A`, `pw_element<>` contains only the extent, and `pw_elements<>`
+// does not hold any values for the elements.
+//
+//
+// Construction:
+//
+// A `pw_elements<A>` object can be constructed from a sequence of _vertices_
+// and a sequence of _values_ (if `A` is not `void`). A vertex sequence of
+// doubles x₀, x₁, …, xₙ and value sequence v₀, v₁, …, vₙ₋₁ defines n elements
+// with extents [x₀, x₁], [x₁, x₂], etc.
+//
+// A default constructed `pw_elements<A>` has no elements.
+//
+// A `pw_element<A>` can be appended to a `pw_elements<A>` object with:
+//
+// pw_elements<A>::push_back(pw_element<A>)
+//
+// The lower bound of the element must equal the upper bound of the existing
+// sequence, if it is not empty.
+//
+// The extent and value can be given explicitly with:
+//
+// template <typename U&&>
+// pw_elements<A>::push_back(double left, double right, U&& value)
+//
+// template <typename U&&>
+// pw_elements<A>::push_back(double right, U&& value)
+//
+// where the lower bound of the extent of the new element can be omitted
+// if the sequence is non-empty. When `A` is void, these methods are
+// instead:
+//
+// pw_elements<>::push_back(double left, double right)
+// pw_elements<>::push_back(double right)
+//
+//
+// Conversion:
+//
+// A `pw_elements<A>` object can be explicitly constructed from a `pw_elements<B>`
+// object if the values of type B can be converted to values of type A.
+//
+// A `pw_elements<void>` object can be explicitly constucted from any
+// `pw_elements<B>` object, where it takes just the element extents.
+//
+//
+// Element access:
+//
+// Elements are retrievable by index by `operator[]`. The value of the ith
+// element is given by `value(i)`, and its extent as a pair of doubles by
+// `extent(i)`.
+//
+// The method `vertices()` returns the vector of nodes defining the element
+// extents, while for non-void codomains, `values()` returns the vector of
+// element values.
+//
+// Elements can be looked up by their position with `operator()(double x)` and
+// `equal_range(double x)`. The element (or element proxy, see below) retrieved
+// for `x` is the right-most element whose extent includes `x`; this makes
+// `pw_elements<A>` act like a right-continuous function of position.
+// `equal_range(x)` returns a pair of iterators that correspond to the sequence
+// of elements whose extents all include `x`.
+//
+// Elements that are returned by a non-const lvalue `pw_elements<A>` via
+// `operator[]`, `operator()`, or via an iterator are represented by a proxy
+// object that allows in-place mutation of the element's value.
+#include <cmath>
#include <initializer_list>
#include <iterator>
#include <type_traits>
#include <vector>
+#include "util/iterutil.hpp"
+#include "util/transform.hpp"
#include "util/meta.hpp"
#include "util/partition.hpp"
@@ -19,83 +109,161 @@ namespace util {
using pw_size_type = unsigned;
constexpr pw_size_type pw_npos = -1;
-// Generic random access const iterator for a collection
-// providing operator[].
+template <typename X = void>
+struct pw_element {
+ pw_element():
+ extent(NAN, NAN),
+ value()
+ {}
-template <typename T>
-struct indexed_const_iterator {
- using size_type = decltype(std::size(std::declval<T>()));
- using difference_type = std::make_signed_t<size_type>;
+ pw_element(std::pair<double, double> extent, X value):
+ extent(std::move(extent)),
+ value(std::move(value))
+ {}
- using value_type = decltype(std::declval<T>()[0]);
- struct pointer {
- value_type v;
- const value_type* operator->() const { return &v; }
- };
+ pw_element(const pw_element&) = default;
+ pw_element(pw_element&&) = default;
- using reference = value_type;
- using iterator_category = std::random_access_iterator_tag;
+ operator X() const { return value; }
+ pw_element& operator=(X x) { value = std::move(x); return *this; };
- const T* ptr_ = nullptr;
- size_type i_ = 0;
+ double lower_bound() const { return extent.first; }
+ double upper_bound() const { return extent.second; }
- bool operator==(indexed_const_iterator x) const { return ptr_ == x.ptr_ && i_ == x.i_; }
- bool operator!=(indexed_const_iterator x) const { return !(*this==x); }
- bool operator<=(indexed_const_iterator x) const { return i_<=x.i_; }
- bool operator<(indexed_const_iterator x) const { return i_<x.i_; }
- bool operator>=(indexed_const_iterator x) const { return i_>=x.i_; }
- bool operator>(indexed_const_iterator x) const { return i_>x.i_; }
+ const std::pair<double, double> extent;
+ X value;
+};
- difference_type operator-(indexed_const_iterator x) const { return i_-x.i_; }
- indexed_const_iterator& operator++() { return ++i_, *this; }
- indexed_const_iterator& operator--() { return --i_, *this; }
- indexed_const_iterator operator++(int) { auto x = *this; return ++i_, x; }
- indexed_const_iterator operator--(int) { auto x = *this; return --i_, x; }
+template <>
+struct pw_element<void> {
+ pw_element():
+ extent(NAN, NAN)
+ {}
- indexed_const_iterator operator+(difference_type n) { return indexed_const_iterator{ptr_, i_+n}; }
- indexed_const_iterator operator-(difference_type n) { return indexed_const_iterator{ptr_, i_-n}; }
- indexed_const_iterator& operator+=(difference_type n) { return i_+=n, *this; }
- indexed_const_iterator& operator-=(difference_type n) { return i_-=n, *this; }
+ pw_element(std::pair<double, double> extent):
+ extent(std::move(extent))
+ {}
- friend indexed_const_iterator operator+(difference_type n, indexed_const_iterator x) {
- indexed_const_iterator r(std::move(x));
- return r+=n;
- }
+ pw_element(const pw_element&) = default;
+ pw_element(pw_element&&) = default;
- reference operator*() const { return (*ptr_)[i_]; }
- pointer operator->() const { return pointer{(*ptr_)[i_]}; }
-};
+ double lower_bound() const { return extent.first; }
+ double upper_bound() const { return extent.second; }
+ const std::pair<double, double> extent;
+};
template <typename X = void>
+struct pw_elements;
+
+// Proxy object for mutable iterators into a pw_elements<X> object.
+template <typename X>
+struct pw_element_proxy {
+ pw_element_proxy(pw_elements<X>& pw, pw_size_type i):
+ extent(pw.extent(i)), value(pw.value(i)) {}
+
+ operator pw_element<X>() const { return pw_element<X>{extent, value}; }
+ operator X() const { return value; }
+ pw_element_proxy& operator=(X x) {value = std::move(x); return *this; };
+
+ double lower_bound() const { return extent.first; }
+ double upper_bound() const { return extent.second; }
+
+ const std::pair<double, double> extent;
+ X& value;
+};
+
+// Compute indices into vertex set corresponding to elements that cover a point x:
+
+namespace {
+std::pair<std::ptrdiff_t, std::ptrdiff_t> equal_range_indices(const std::vector<double>& vertices, double x) {
+ if (vertices.empty()) return {0, 0};
+
+ auto eq = std::equal_range(vertices.begin(), vertices.end(), x);
+
+ // Let n be the number of elements, indexed from 0 to n-1, with
+ // vertices indexed from 0 to n. Observe:
+ // * eq.first points to least vertex v_i ≥ x.
+ // or else to vertices.end() if v < x for all vertices v.
+ // * eq.second points to vertices.end() if the last vertex v_n ≤ x,
+ // or else to the least vertex v_k > x.
+ //
+ // Elements then correspond to the index range [b, e), where:
+ // * b=0 if i=0, else b=i-1, as v_i will be the upper vertex for
+ // the first element whose (closed) support contains x.
+ // * e=k if k<n, since v_k will be the upper vertex for the
+ // the last element (index k-1) whose support contains x.
+ // Otherwise, if k==n or eq.second is vertices.end(), the
+ // last element (index n-1) contains x, and so e=n.
+
+ if (eq.first==vertices.end()) return {0, 0};
+ if (eq.first>vertices.begin()) --eq.first;
+ if (eq.second==vertices.end()) --eq.second;
+
+ return std::make_pair(eq.first-vertices.begin(), eq.second-vertices.begin());
+}
+} // anonymous namespace
+
+template <typename X>
struct pw_elements {
using size_type = pw_size_type;
+ using difference_type = std::make_signed_t<pw_size_type>;
static constexpr size_type npos = pw_npos;
- struct value_type {
- std::pair<double, double> interval;
- X element;
+ using value_type = pw_element<X>;
+ using codomain = X;
- bool operator==(const value_type& other) const { return interval==other.interval && element==other.element; }
- bool operator!=(const value_type& other) const { return interval!=other.interval || element!=other.element; }
+ struct iterator: iterator_adaptor<iterator, counter<pw_size_type>> {
+ using typename iterator_adaptor<iterator, counter<pw_size_type>>::difference_type;
+ iterator(pw_elements<X>& pw, pw_size_type index): pw_(&pw), c_(index) {}
+ iterator(): pw_(nullptr) {}
+
+ using value_type = pw_element<X>;
+ using pointer = pointer_proxy<pw_element_proxy<X>>;
+ using reference = pw_element_proxy<X>;
+
+ reference operator[](difference_type j) { return reference{*pw_, j+*c_}; }
+ reference operator*() { return reference{*pw_, *c_}; }
+ value_type operator*() const { return (*pw_)[*c_]; }
+ pointer operator->() { return pointer{*pw_, *c_}; }
+
+ // (required for iterator_adaptor)
+ counter<pw_size_type>& inner() { return c_; }
+ const counter<pw_size_type>& inner() const { return c_; }
+
+ protected:
+ pw_elements<X>* pw_;
+ counter<pw_size_type> c_;
};
- using const_iterator = indexed_const_iterator<pw_elements<X>>;
- using iterator = const_iterator;
+ struct const_iterator: iterator_adaptor<const_iterator, counter<pw_size_type>> {
+ using typename iterator_adaptor<const_iterator, counter<pw_size_type>>::difference_type;
+ const_iterator(const pw_elements<X>& pw, pw_size_type index): pw_(&pw), c_(index) {}
+ const_iterator(): pw_(nullptr) {}
- // Consistency requirements:
- // 1. empty() || element_.size()+1 = vertex_.size()
- // 2. vertex_[i]<=vertex_[j] for i<=j.
+ using value_type = pw_element<X>;
+ using pointer = const pointer_proxy<pw_element<X>>;
+ using reference = pw_element<X>;
- std::vector<double> vertex_;
- std::vector<X> element_;
+ reference operator[](difference_type j) const { return (*pw_)[j+*c_]; }
+ reference operator*() const { return (*pw_)[*c_]; }
+ pointer operator->() const { return pointer{(*pw_)[*c_]}; }
+
+ // (required for iterator_adaptor)
+ counter<pw_size_type>& inner() { return c_; }
+ const counter<pw_size_type>& inner() const { return c_; }
+
+ protected:
+ const pw_elements<X>* pw_;
+ counter<pw_size_type> c_;
+ };
// Ctors and assignment:
pw_elements() = default;
- template <typename VSeq, typename ESeq>
- pw_elements(const VSeq& vs, const ESeq& es) {
+ template <typename VertexSeq, typename ValueSeq>
+ pw_elements(const VertexSeq& vs, const ValueSeq& es) {
assign(vs, es);
}
@@ -108,7 +276,7 @@ struct pw_elements {
template <typename Y>
explicit pw_elements(const pw_elements<Y>& from):
- vertex_(from.vertex_), element_(from.element_.begin(), from.element_.end())
+ vertex_(from.vertex_), value_(from.value_.begin(), from.value_.end())
{}
pw_elements& operator=(pw_elements&&) = default;
@@ -116,90 +284,107 @@ struct pw_elements {
// Access:
- auto intervals() const { return util::partition_view(vertex_); }
- auto interval(size_type i) const { return intervals()[i]; }
+ auto extents() const { return util::partition_view(vertex_); }
+
+ auto bounds() const { return extents().bounds(); }
+ auto lower_bound() const { return bounds().first; }
+ auto upper_bound() const { return bounds().second; }
- auto bounds() const { return intervals().bounds(); }
+ auto extent(size_type i) const { return extents()[i]; }
+ auto lower_bound(size_type i) const { return extents()[i].first; }
+ auto upper_bound(size_type i) const { return extents()[i].second; }
- size_type size() const { return element_.size(); }
+ size_type size() const { return value_.size(); }
bool empty() const { return size()==0; }
bool operator==(const pw_elements& x) const {
- return vertex_==x.vertex_ && element_==x.element_;
+ return vertex_==x.vertex_ && value_==x.value_;
}
bool operator!=(const pw_elements& x) const { return !(*this==x); }
- const auto& elements() const { return element_; }
+ const auto& values() const { return value_; }
const auto& vertices() const { return vertex_; }
- X& element(size_type i) & { return element_[i]; }
- const X& element(size_type i) const & { return element_[i]; }
- value_type operator[](size_type i) const { return value_type{interval(i), element(i)}; }
+ X& value(size_type i) & { return value_[i]; }
+ const X& value(size_type i) const & { return value_[i]; }
+ X value(size_type i) const && { return value_[i]; }
- const_iterator cbegin() const { return const_iterator{this, 0}; }
+ auto operator[](size_type i) & { return pw_element_proxy<X>{*this, i}; }
+ auto operator[](size_type i) const & { return value_type{extent(i), value(i)}; }
+ auto operator[](size_type i) const && { return value_type{extent(i), value(i)}; }
+
+ auto front() & { return pw_element_proxy<X>{*this, 0}; }
+ auto front() const & { return value_type{extent(0), value(0)}; }
+ auto front() const && { return value_type{extent(0), value(0)}; }
+
+ auto back() & { return pw_element_proxy<X>{*this, size()-1}; }
+ auto back() const & { return value_type{extent(size()-1), value(size()-1)}; }
+ auto back() const && { return value_type{extent(size()-1), value(size()-1)}; }
+
+ iterator begin() { return iterator{*this, 0}; }
+ iterator end() { return iterator{*this, size()}; }
+
+ const_iterator cbegin() const { return const_iterator{*this, 0}; }
const_iterator begin() const { return cbegin(); }
- const_iterator cend() const { return const_iterator{this, size()}; }
+ const_iterator cend() const { return const_iterator{*this, size()}; }
const_iterator end() const { return cend(); }
- value_type front() const { return (*this)[0]; }
- value_type back() const { return (*this)[size()-1]; }
// Return index of right-most element whose corresponding closed interval contains x.
size_type index_of(double x) const {
if (empty()) return npos;
- auto partn = intervals();
+ auto partn = extents();
if (x == partn.bounds().second) return size()-1;
else return partn.index(x);
}
- // Return iterator pair spanning elements whose corresponding closed intervals contain x.
- std::pair<iterator, iterator> equal_range(double x) const {
- auto eq = std::equal_range(vertex_.begin(), vertex_.end(), x);
+ // Return iterator pair spanning elements whose corresponding closed extents contain x.
+ std::pair<iterator, iterator> equal_range(double x) {
+ auto qi = equal_range_indices(vertex_, x);
+ return {begin()+qi.first, begin()+qi.second};
+ }
- // Let n be the number of elements, indexed from 0 to n-1, with
- // vertices indexed from 0 to n. Observe:
- // * eq.first points to least vertex v_i ≥ x.
- // * eq.second points to vertex_.end() if the last vertex v_n ≤ x,
- // or else to the least vertex v_k > x.
- //
- // Elements then correspond to the index range [b, e), where:
- // * b=0 if i=0, else b=i-1, as v_i will be the upper vertex for
- // the first element whose (closed) support contains x.
- // * e=k if k<n, since v_k will be the upper vertex for the
- // the last element (index k-1) whose support contains x.
- // Otherwise, if k==n or eq.second is vertex_.end(), the
- // last element (index n-1) contains x, and so e=n.
+ std::pair<const_iterator, const_iterator> equal_range(double x) const {
+ auto qi = equal_range_indices(vertex_, x);
+ return {cbegin()+qi.first, cbegin()+qi.second};
+ }
- if (eq.first==vertex_.end()) return {end(), end()};
- if (eq.first>vertex_.begin()) --eq.first;
- if (eq.second==vertex_.end()) --eq.second;
+ auto operator()(double x) const && {
+ size_type i = index_of(x);
+ return i!=npos? (*this)[i]: throw std::range_error("position outside support");
+ }
- return {begin()+(eq.first-vertex_.begin()), begin()+(eq.second-vertex_.begin())};
+ auto operator()(double x) & {
+ size_type i = index_of(x);
+ return i!=npos? (*this)[i]: throw std::range_error("position outside support");
}
- value_type operator()(double x) const {
+ auto operator()(double x) const & {
size_type i = index_of(x);
- if (i==npos) {
- throw std::range_error("position outside support");
- }
- return (*this)[i];
+ return i!=npos? (*this)[i]: throw std::range_error("position outside support");
}
// mutating operations:
void reserve(size_type n) {
vertex_.reserve(n+1);
- element_.reserve(n);
+ value_.reserve(n);
}
void clear() {
vertex_.clear();
- element_.clear();
+ value_.clear();
+ }
+
+ void push_back(pw_element<X> elem) {
+ double left = elem.lower_bound();
+ double right = elem.upper_bound();
+ push_back(left, right, std::move(elem.value));
}
template <typename U>
- void push_back(double left, double right, U&& elem) {
+ void push_back(double left, double right, U&& v) {
if (!empty() && left!=vertex_.back()) {
throw std::runtime_error("noncontiguous element");
}
@@ -208,20 +393,19 @@ struct pw_elements {
throw std::runtime_error("inverted element");
}
- // Extend element_ first in case a conversion/copy/move throws.
-
- element_.push_back(std::forward<U>(elem));
+ // Extend value_ first in case a conversion/copy/move throws.
+ value_.push_back(std::forward<U>(v));
if (vertex_.empty()) vertex_.push_back(left);
vertex_.push_back(right);
}
template <typename U>
- void push_back(double right, U&& elem) {
+ void push_back(double right, U&& v) {
if (empty()) {
throw std::runtime_error("require initial left vertex for element");
}
- push_back(vertex_.back(), right, elem);
+ push_back(vertex_.back(), right, std::forward<U>(v));
}
void assign(std::initializer_list<double> vs, std::initializer_list<X> es) {
@@ -230,15 +414,15 @@ struct pw_elements {
}
template <typename Seq1, typename Seq2>
- void assign(const Seq1& vertices, const Seq2& elements) {
+ void assign(const Seq1& vertices, const Seq2& values) {
using std::begin;
using std::end;
auto vi = begin(vertices);
auto ve = end(vertices);
- auto ei = begin(elements);
- auto ee = end(elements);
+ auto ei = begin(values);
+ auto ee = end(values);
if (ei==ee) { // empty case
if (vi!=ve) {
@@ -248,13 +432,13 @@ struct pw_elements {
return;
}
- double left = *vi++;
if (vi==ve) {
throw std::runtime_error("vertex list too short");
}
clear();
+ double left = *vi++;
double right = *vi++;
push_back(left, right, *ei++);
@@ -270,91 +454,144 @@ struct pw_elements {
throw std::runtime_error("vertex list too long");
}
}
+
+private:
+ // Consistency requirements:
+ // 1. empty() || value_.size()+1 = vertex_.size()
+ // 2. vertex_[i]<=vertex_[j] for i<=j.
+
+ std::vector<double> vertex_;
+ std::vector<X> value_;
};
-// With X = void, present the element intervals only,
-// keeping othewise the same interface.
+// With X = void, present the element intervals only, keeping otherwise the
+// same interface.
-template <> struct pw_elements<void> {
+template <>
+struct pw_elements<void> {
using size_type = pw_size_type;
static constexpr size_type npos = pw_npos;
+ using difference_type = std::make_signed_t<pw_size_type>;
- std::vector<double> vertex_;
+ using value_type = pw_element<void>;
+ using codomain = void;
+
+ struct const_iterator: iterator_adaptor<const_iterator, counter<pw_size_type>> {
+ using typename iterator_adaptor<const_iterator, counter<pw_size_type>>::difference_type;
+ const_iterator(const pw_elements<void>& pw, pw_size_type index): pw_(&pw), c_(index) {}
+ const_iterator(): pw_(nullptr) {}
+
+ using value_type = pw_element<void>;
+ using pointer = const pointer_proxy<pw_element<void>>;
+ using reference = pw_element<void>;
+
+ reference operator[](difference_type j) const { return (*pw_)[j+*c_]; }
+ reference operator*() const { return (*pw_)[*c_]; }
+ pointer operator->() const { return pointer{(*pw_)[*c_]}; }
- struct value_type {
- std::pair<double, double> interval;
+ // (required for iterator_adaptor)
+ counter<pw_size_type>& inner() { return c_; }
+ const counter<pw_size_type>& inner() const { return c_; }
- bool operator==(const value_type& other) const { return interval==other.interval; }
- bool operator!=(const value_type& other) const { return interval!=other.interval; }
+ protected:
+ const pw_elements<void>* pw_;
+ counter<pw_size_type> c_;
};
- using const_iterator = indexed_const_iterator<pw_elements<void>>;
using iterator = const_iterator;
- // ctors and assignment:
+ // Ctors and assignment:
+
+ pw_elements() = default;
- template <typename VSeq>
- explicit pw_elements(const VSeq& vs) { assign(vs); }
+ template <typename VertexSeq>
+ pw_elements(const VertexSeq& vs) {
+ assign(vs);
+ }
- pw_elements(std::initializer_list<double> vs) { assign(vs); }
+ pw_elements(std::initializer_list<double> vs) {
+ assign(vs);
+ }
- pw_elements() = default;
pw_elements(pw_elements&&) = default;
pw_elements(const pw_elements&) = default;
template <typename Y>
explicit pw_elements(const pw_elements<Y>& from):
- vertex_(from.vertex_) {}
+ vertex_(from.vertices()) {}
pw_elements& operator=(pw_elements&&) = default;
pw_elements& operator=(const pw_elements&) = default;
- // access:
+ // Access:
+
+ auto extents() const { return util::partition_view(vertex_); }
- auto intervals() const { return util::partition_view(vertex_); }
- auto interval(size_type i) const { return intervals()[i]; }
- value_type operator[](size_type i) const { return value_type{interval(i)}; }
+ auto bounds() const { return extents().bounds(); }
+ auto lower_bound() const { return bounds().first; }
+ auto upper_bound() const { return bounds().second; }
- auto bounds() const { return intervals().bounds(); }
+ auto extent(size_type i) const { return extents()[i]; }
+ auto lower_bound(size_type i) const { return extents()[i].first; }
+ auto upper_bound(size_type i) const { return extents()[i].second; }
- size_type size() const { return vertex_.empty()? 0: vertex_.size()-1; }
+ size_type size() const { return empty()? 0: vertex_.size()-1; }
bool empty() const { return vertex_.empty(); }
- bool operator==(const pw_elements& x) const { return vertex_==x.vertex_; }
+ bool operator==(const pw_elements& x) const {
+ return vertex_==x.vertex_;
+ }
+
bool operator!=(const pw_elements& x) const { return !(*this==x); }
const auto& vertices() const { return vertex_; }
+ void value(size_type i) const {}
+ pw_element<void> operator[](size_type i) const { return value_type{extent(i)}; }
+
+ pw_element<void> front() const { return value_type{extent(0)}; }
+ pw_element<void> back() const { return value_type{extent(size()-1)}; }
+
+ const_iterator cbegin() const { return const_iterator{*this, 0}; }
+ const_iterator begin() const { return cbegin(); }
+ const_iterator cend() const { return const_iterator{*this, size()}; }
+ const_iterator end() const { return cend(); }
+
+ // Return index of right-most element whose corresponding closed interval contains x.
size_type index_of(double x) const {
if (empty()) return npos;
- auto partn = intervals();
+ auto partn = extents();
if (x == partn.bounds().second) return size()-1;
else return partn.index(x);
}
- // Return iterator pair spanning elements whose corresponding closed intervals contain x.
- std::pair<iterator, iterator> equal_range(double x) const {
- auto eq = std::equal_range(vertex_.begin(), vertex_.end(), x);
-
- if (eq.first==vertex_.end()) return {end(), end()};
- if (eq.first>vertex_.begin()) --eq.first;
- if (eq.second==vertex_.end()) --eq.second;
-
- return {begin()+(eq.first-vertex_.begin()), begin()+(eq.second-vertex_.begin())};
+ // Return iterator pair spanning elements whose corresponding closed extents contain x.
+ std::pair<const_iterator,const_iterator> equal_range(double x) const {
+ auto qi = equal_range_indices(vertex_, x);
+ return {cbegin()+qi.first, cbegin()+qi.second};
}
- const_iterator cbegin() const { return const_iterator{this, 0}; }
- const_iterator begin() const { return cbegin(); }
- const_iterator cend() const { return const_iterator{this, size()}; }
- const_iterator end() const { return cend(); }
- value_type front() const { return (*this)[0]; }
- value_type back() const { return (*this)[size()-1]; }
+ auto operator()(double x) const {
+ size_type i = index_of(x);
+ return i!=npos? (*this)[i]: throw std::range_error("position outside support");
+ }
// mutating operations:
- void reserve(size_type n) { vertex_.reserve(n+1); }
- void clear() { vertex_.clear(); }
+ void reserve(size_type n) {
+ vertex_.reserve(n+1);
+ }
+
+ void clear() {
+ vertex_.clear();
+ }
+
+ void push_back(const pw_element<void>& elem) {
+ double left = elem.lower_bound();
+ double right = elem.upper_bound();
+ push_back(left, right);
+ }
void push_back(double left, double right) {
if (!empty() && left!=vertex_.back()) {
@@ -373,11 +610,12 @@ template <> struct pw_elements<void> {
if (empty()) {
throw std::runtime_error("require initial left vertex for element");
}
- vertex_.push_back(right);
+
+ push_back(vertex_.back(), right);
}
void assign(std::initializer_list<double> vs) {
- assign(util::make_range(vs.begin(), vs.end()));
+ assign(make_range(vs.begin(), vs.end()));
}
template <typename Seq1>
@@ -408,66 +646,79 @@ template <> struct pw_elements<void> {
push_back(right);
}
}
-};
-template <typename X>
-using pw_element = typename pw_elements<X>::value_type;
+private:
+ // Consistency requirements:
+ // 1. empty() || value_.size()+1 = vertex_.size()
+ // 2. vertex_[i]<=vertex_[j] for i<=j.
-namespace impl {
- template <typename A, typename B>
- struct piecewise_pairify {
- std::pair<A, B> operator()(
- double, double,
- const pw_element<A>& a_elem,
- const pw_element<B>& b_elem) const
- {
- return {a_elem.element, b_elem.element};
- }
- };
+ std::vector<double> vertex_;
+};
- template <typename X>
- struct piecewise_pairify<X, void> {
- X operator()(
- double, double,
- const pw_element<X>& a_elem,
- const pw_element<void>& b_elem) const
- {
- return a_elem.element;
+// The piecewise map applies a transform to the values in a piecewise
+// object, preserving the extents. If the original piecewise object
+// is `pw_elements<void>`, then the function is called with zero arguments.
+//
+// If the mapping function returns void, return a `pw_elements<void>` object
+// with the same extents as the original piecewise object.
+
+template <typename X, typename Fn>
+auto pw_map(const pw_elements<X>& pw, Fn&& fn) {
+ if constexpr (std::is_void_v<X>) {
+ using Out = std::invoke_result_t<Fn&&>;
+ if constexpr (std::is_void_v<Out>) {
+ // Evalate fn for side effects.
+ for (const auto& elem: pw) fn();
+ return pw;
}
- };
-
- template <typename X>
- struct piecewise_pairify<void, X> {
- X operator()(
- double, double,
- const pw_element<void>& a_elem,
- const pw_element<X>& b_elem) const
- {
- return b_elem.element;
+ else {
+ auto mapped = util::transform_view(pw, [&fn](auto&&) { return fn(); });
+ return pw_elements<Out>(pw.vertices(), std::vector<Out>(mapped.begin(), mapped.end()));
}
- };
-
- template <>
- struct piecewise_pairify<void, void> {
- void operator()(
- double, double,
- const pw_element<void>&,
- const pw_element<void>&) const {}
- };
+ }
+ else {
+ using Out = std::invoke_result_t<Fn&&, X>;
+ if constexpr (std::is_void_v<Out>) {
+ // Evalate fn for side effects.
+ for (const auto& v: pw.values()) fn(v);
+ return pw_elements<void>(pw);
+ }
+ else {
+ auto mapped = util::transform_view(pw, [&fn](auto&& elem) { return fn(elem.value); });
+ return pw_elements<Out>(pw.vertices(), std::vector<Out>(mapped.begin(), mapped.end()));
+ }
+ }
}
-// Zip combines successive elements from two pw_elements sequences where the
-// elements overlap. Let A_i and B_i denote the ordered elements from each of
-// the sequences A and B, and Z_i denote the elements from the resulting
-// sequence Z.
+// The piecewise zip combines successive elements from two `pw_elements`
+// sequences where the elements overlap.
+//
+// * `pw_zip_view` performs a lazy zip, represented by a range of
+// `pw_zip_iterator` objects. The iterators dereference to objects of
+// type `pw_element<std::pair<pw_element<A>, pw_element<B>>`.
+//
+// * `pw_zip` performs a strict zip, returning a piecewise object of type
+// `pw_elements<std::pair<pw_element<A>, pw_element<B>>`.
+//
+// * `pw_zip_with` performs a map composed with a zip, where the map is
+// given by a function which takes three arguments: the extent of
+// the zipped element as `std::pair<double, double>`; the element
+// from the first sequence of type `<pw_element<A>`; and the element
+// from the second sequence of type `pw_element<B>`. It is equivalent
+// in action to performing a `pw_zip` followed by a `pw_map`.
+//
+// By default, `pw_zip_with` uses `pw_pairify` to combine elements,
+// which returns a tuple of the non-void values from each pair of elements
+// in the zip.
//
-// * The support (`bounds()`) of the zipped result is the intersections of the
-// support of the two sequences.
+// The elements of the zip are determined as follows:
//
-// * Each element Z_k in the zip corresponds to the intersection of some
+// Let A_i and B_i denote the ordered elements from each of the sequences A and
+// B, and Z_i denote the elements forming the resulting sequence Z.
+//
+// * Each element Z_k in the zip corresponds to the intersection of an
// element A_i and B_j. The extent of Z_k is the intersection of the
-// extents of A_i and B_j, and its value is determined by the supplied
-// `combine` function.
+// extents of A_i and B_j, and its value is the pair {A_i, B_j}.
//
// * For every element in A_i in A, if A_i intersects with an element of
// B, then there will be an Z_k corresponding to the intersection of A_i
@@ -477,53 +728,157 @@ namespace impl {
// not repeat. If Z_k is derived from A_i and B_j, then Z_(k+1) is derived
// from A_(i+1) and B_(j+1) if possible, or else from A_i and B_(j+1) or
// A_(i+1) and B_j.
-//
-// The Combine functional takes four arguments: double left, double right,
-// pw_elements<A>::value_type, pw_elements<B>::value_type b. The default
-// combine functional returns std::pair<A, B>, unless one of A and B is void.
-//
-// TODO: Consider making a lazy `zip_view` version of zip.
-template <typename A, typename B, typename Combine = impl::piecewise_pairify<A, B>>
-auto zip(const pw_elements<A>& a, const pw_elements<B>& b, Combine combine = {})
-{
- using Out = decltype(combine(0., 0., a.front(), b.front()));
- constexpr bool is_void = std::is_void_v<Out>;
+template <typename A, typename B>
+struct pw_zip_iterator {
+ using value_type = pw_element<std::pair<pw_element<A>, pw_element<B>>>;
+ using pointer = pointer_proxy<value_type>;
+ using reference = value_type;
+ using difference_type = std::ptrdiff_t;
+
+ // Proxy iterator so not _really_ a forward iterator.
+ using iterator_category = std::forward_iterator_tag;
+
+ bool is_end = true;
+ typename pw_elements<A>::const_iterator ai, a_end;
+ typename pw_elements<B>::const_iterator bi, b_end;
+ double left;
+
+ pw_zip_iterator() = default;
+ pw_zip_iterator(const pw_elements<A>& a, const pw_elements<B>& b) {
+ double lmax = std::max(a.lower_bound(), b.lower_bound());
+ double rmin = std::min(a.upper_bound(), b.upper_bound());
+
+ is_end = rmin<lmax;
+ if (!is_end) {
+ ai = a.equal_range(lmax).first;
+ a_end = a.equal_range(rmin).second;
+ bi = b.equal_range(lmax).first;
+ b_end = b.equal_range(rmin).second;
+ left = lmax;
+ }
+ }
- pw_elements<Out> z;
- if (a.empty() || b.empty()) return z;
+ pw_zip_iterator(const pw_zip_iterator& other):
+ is_end(other.is_end),
+ ai(other.ai),
+ a_end(other.a_end),
+ bi(other.bi),
+ b_end(other.b_end),
+ left(other.left)
+ {}
- double lmax = std::max(a.bounds().first, b.bounds().first);
- double rmin = std::min(a.bounds().second, b.bounds().second);
- if (rmin<lmax) return z;
+ bool operator==(pw_zip_iterator other) const {
+ if (is_end && other.is_end) return true;
+ return is_end==other.is_end && ai==other.ai && a_end==other.a_end && bi==other.bi && b_end==other.b_end && left==other.left;
+ }
- auto ai = a.equal_range(lmax).first;
- auto bi = b.equal_range(lmax).first;
+ bool operator!=(pw_zip_iterator other) const {
+ return !(*this==other);
+ }
- auto a_end = a.equal_range(rmin).second;
- auto b_end = b.equal_range(rmin).second;
+ pw_zip_iterator& operator++() {
+ if (is_end) return *this;
- double left = lmax;
- double a_right = ai->interval.second;
- double b_right = bi->interval.second;
- for (;;) {
+ double a_right = ai->upper_bound();
+ double b_right = bi->upper_bound();
double right = std::min(a_right, b_right);
- if constexpr (is_void) {
- z.push_back(left, right);
+
+ bool advance_a = a_right==right && std::next(ai)!=a_end;
+ bool advance_b = b_right==right && std::next(bi)!=b_end;
+ if (!advance_a && !advance_b) {
+ is_end = true;
}
else {
- z.push_back(left, right, combine(left, right, *ai, *bi));
+ if (advance_a) ++ai;
+ if (advance_b) ++bi;
+ left = right;
}
- bool advance_a = a_right==right && std::next(ai)!=a_end;
- bool advance_b = b_right==right && std::next(bi)!=b_end;
- if (!advance_a && !advance_b) break;
+ return *this;
+ }
+
+ pw_zip_iterator operator++(int) {
+ pw_zip_iterator here = *this;
+ ++*this;
+ return here;
+ }
+
+ value_type operator*() const {
+ double a_right = ai->upper_bound();
+ double b_right = bi->upper_bound();
+ double right = std::min(a_right, b_right);
+
+ return value_type{{left, right}, {*ai, *bi}};
+ }
+
+ pointer operator->() const {
+ return pointer{*this};
+ }
+};
- if (advance_a) a_right = (++ai)->interval.second;
- if (advance_b) b_right = (++bi)->interval.second;
- left = right;
+template <typename A, typename B>
+range<pw_zip_iterator<A, B>> pw_zip_range(const pw_elements<A>& a, const pw_elements<B>& b) {
+ return {pw_zip_iterator<A, B>{a, b}, pw_zip_iterator<A, B>{}};
+}
+
+template <typename A, typename B>
+auto pw_zip(const pw_elements<A>& a, const pw_elements<B>& b) {
+ pw_elements<std::pair<pw_element<A>, pw_element<B>>> out;
+ for (auto elem: pw_zip_range(a, b)) {
+ out.push_back(elem);
+ }
+ return out;
+}
+
+// `pw_pairify` is a functional intended for use with `pw_zip_with` that takes
+// an extent, which is ignored, and two elements of types `pw_element<A>` and
+// `pw_element<B>` and returns void if both A and B are void, or the pair of
+// their values if neither A nor B is void, or else the single non-void value
+// of type A or B.
+
+namespace impl {
+ template <typename A, typename B>
+ std::pair<A, B> pw_pairify_(const pw_element<A>& a_elem, const pw_element<B>& b_elem) {
+ return {a_elem.value, b_elem.value};
+ };
+
+ template <typename A>
+ A pw_pairify_(const pw_element<A>& a_elem, const pw_element<void>&) {
+ return a_elem.value;
+ };
+
+ template <typename B>
+ B pw_pairify_(const pw_element<void>&, const pw_element<B>& b_elem) {
+ return b_elem.value;
+ };
+
+ inline void pw_pairify_(const pw_element<void>&, const pw_element<void>&) {}
+}
+
+struct pw_pairify {
+ template <typename A, typename B>
+ auto operator()(std::pair<double, double>, const pw_element<A>& a_elem, const pw_element<B>& b_elem) const {
+ return impl::pw_pairify_(a_elem, b_elem);
+ }
+};
+
+template <typename A, typename B, typename Fn = pw_pairify>
+auto pw_zip_with(const pw_elements<A>& a, const pw_elements<B>& b, Fn&& fn = Fn{}) {
+ using Out = decltype(fn(std::pair<double, double>{}, a.front(), b.front()));
+ pw_elements<Out> out;
+
+ for (auto elem: pw_zip_range(a, b)) {
+ if constexpr (std::is_void_v<Out>) {
+ fn(elem.extent, elem.value.first, elem.value.second);
+ out.push_back(elem.extent.first, elem.extent.second);
+ }
+ else {
+ out.push_back(elem.extent.first, elem.extent.second,
+ fn(elem.extent, elem.value.first, elem.value.second));
+ }
}
- return z;
+ return out;
}
} // namespace util
diff --git a/arbor/util/ratelem.hpp b/arbor/util/ratelem.hpp
index cbb2bfac..396de714 100644
--- a/arbor/util/ratelem.hpp
+++ b/arbor/util/ratelem.hpp
@@ -53,7 +53,6 @@ void rat_interpolate(std::array<double, m>& h, const std::array<double, m+1>& g,
h[i] = ook*((x - i)*g[i+1] + (i+k - x)*g[i]);
}
else {
- // Using h[i] = k/(g[i+1]/(x - i) + g[i]/(i+k - x)) is more robust to
// singularities, but the expense should not be necessary if we stick
// to strictly monotonic elements for rational polynomials.
h[i] = k*g[i]*g[i+1]/(g[i]*(x - i) + g[i+1]*(i+k - x));
@@ -62,7 +61,7 @@ void rat_interpolate(std::array<double, m>& h, const std::array<double, m+1>& g,
}
template <unsigned a, unsigned c, unsigned k, bool upper>
-double rat_eval(const std::array<double, 1+a+c>& g, const std::array<double, 2+a+c>& p, double x) {
+double recursive_rat_eval(const std::array<double, 1+a+c>& g, const std::array<double, 2+a+c>& p, double x) {
if constexpr (a==0 && c==0) {
return g[0];
}
@@ -73,29 +72,49 @@ double rat_eval(const std::array<double, 1+a+c>& g, const std::array<double, 2+a
h[i] = p[i+1] + k/((x - i)/(g[i+1]-p[i+1]) + (i+k - x)/(g[i]-p[i+1]));
}
- return rat_eval<0, c-1, k+1, upper>(h, g, x);
+ return recursive_rat_eval<0, c-1, k+1, upper>(h, g, x);
}
else {
std::array<double, a+c> h;
rat_interpolate<k, upper>(h, g, x);
- return rat_eval<a-1, c, k+1, upper>(h, g, x);
+ return recursive_rat_eval<a-1, c, k+1, upper>(h, g, x);
}
}
+template <unsigned p, unsigned q>
+double rat_eval(const std::array<double, 1+p+q>& data, double x) {
+ // upper => interpolate polynomials first;
+ // !upper => interpolate reciprocal polynomials first;
+ // a is the number of (reciprocal) polynomial interpolation steps;
+ // c is the number of 'rhombus' interpolation steps.
+
+ constexpr bool upper = p>=q;
+ constexpr unsigned a = upper? p-q+(q>0): q-p+(p>0);
+ constexpr unsigned c = p+q-a;
-template <unsigned a, unsigned c, unsigned k, bool upper>
-double rat_eval(const std::array<double, 1+a+c>& g, double x) {
if constexpr (a==0 && c==0) {
- return g[0];
+ return data[0];
+ }
+ else if constexpr (a==1 && c==1) {
+ // Special case when p==1 and q==1 so that we can have
+ // well-defined behaviour in the case where the interpolants
+ // are strictly monotonic, but might take non-finite values.
+ // (Used for ixa interpolation in embed_pwlin.cpp).
+
+ double y0 = data[0];
+ double y1 = data[1];
+ double y2 = data[2];
+
+ return y1+(2*x-1)/(x/(y2-y1)+(1-x)/(y1-y0));
}
else {
+ x *= p+q;
std::array<double, a+c> h;
- rat_interpolate<k, upper>(h, g, x);
- return rat_eval<a-1, c, k+1, upper>(h, g, x);
+ rat_interpolate<1, upper>(h, data, x);
+ return recursive_rat_eval<a-1, c, 2, upper>(h, data, x);
}
}
-
} // namespace impl
template <unsigned p, unsigned q>
@@ -132,16 +151,7 @@ struct rat_element {
// Rational interpolation at x.
double operator()(double x) const {
- // upper j => interpolate polynomials first;
- // !upper => interpolate reciprocal polynomials first;
- // a is the number of (reciprocal) polynomial interpolation steps;
- // c is the number of 'rhombus' interpolation steps.
-
- constexpr bool upper = p>=q;
- constexpr unsigned a = upper? p-q+(q>0): q-p+(p>0);
- constexpr unsigned c = p+q-a;
-
- return impl::rat_eval<a, c, 1, upper>(data_, x*(p+q));
+ return impl::rat_eval<p, q>(data_, x);
}
// Node values.
diff --git a/test/unit/test_morph_embedding.cpp b/test/unit/test_morph_embedding.cpp
index 00345b08..ee8d0f73 100644
--- a/test/unit/test_morph_embedding.cpp
+++ b/test/unit/test_morph_embedding.cpp
@@ -286,3 +286,73 @@ TEST(embedding, area_0_length_segment) {
double expected_a2 = expected_a1 + pi*(20*20-10*10);
EXPECT_TRUE(near_relative(a2, expected_a2, reltol));
}
+
+TEST(embedding, small_radius) {
+ using testing::near_relative;
+ constexpr double pi = math::pi<double>;
+ constexpr double reltol = 1e-10;
+
+ segment_tree t;
+ t.append(mnpos, { 0, 0, 0, 10}, {10, 0, 0, 0.00001}, 0);
+ t.append(0, {10, 0, 0, 40}, {20, 0, 0, 40}, 0);
+
+ embedding em{morphology(t)};
+
+ // Integrated inverse cross-sectional area in segment 1
+ // corresponding to cable(0, 0.5, 1):
+ // radius r₀ = 40, r₁ = 40, length = 10.
+
+ double expected_ixa = 10/(40*40*pi);
+ double computed_ixa = em.integrate_ixa(mcable{0, 0.5, 1.0});
+ ASSERT_FALSE(std::isnan(computed_ixa));
+ EXPECT_TRUE(near_relative(expected_ixa, computed_ixa, reltol));
+}
+
+TEST(embedding, zero_radius) {
+ using testing::near_relative;
+ constexpr double pi = math::pi<double>;
+ constexpr double reltol = 1e-10;
+
+ segment_tree t;
+ t.append(mnpos, { 0, 0, 0, 10}, {10, 0, 0, 0}, 0);
+ t.append(0, {10, 0, 0, 40}, {20, 0, 0, 40}, 0);
+
+ embedding em{morphology(t)};
+
+ // Integrated inverse cross-sectional area in segment 1
+ // corresponding to cable(0, 0.5, 1):
+ // radius r₀ = 40, r₁ = 40, length = 10.
+
+ double expected_ixa = 10/(40*40*pi);
+ double computed_ixa = em.integrate_ixa(mcable{0, 0.5, 1.0});
+ ASSERT_FALSE(std::isnan(computed_ixa));
+ EXPECT_TRUE(near_relative(expected_ixa, computed_ixa, reltol));
+
+ // Integrating over the zero radius point though should give us
+ // INFINITY.
+
+ double infinite_ixa = em.integrate_ixa(mcable{0, 0.25, 0.75});
+ ASSERT_FALSE(std::isnan(infinite_ixa));
+ EXPECT_TRUE(std::isinf(infinite_ixa));
+
+ // Integrating to the zero radius point should also give us
+ // INFINITY, because we integrate over closed intervals.
+
+ double also_infinite_ixa = em.integrate_ixa(mcable{0, 0.25, 0.5});
+ ASSERT_FALSE(std::isnan(also_infinite_ixa));
+ EXPECT_TRUE(std::isinf(also_infinite_ixa));
+
+ // Should be able to integrate ixa over a tree that starts
+ // with a zero radius.
+
+ segment_tree t2;
+ t2.append(mnpos, { 0, 0, 0, 0}, {10, 0, 0, 10}, 0);
+ embedding em2{morphology(t2)};
+
+ expected_ixa = 5/(10*5*pi);
+ computed_ixa = em2.integrate_ixa(mcable{0, 0.5, 1.0});
+ ASSERT_FALSE(std::isnan(computed_ixa));
+ EXPECT_TRUE(near_relative(expected_ixa, computed_ixa, reltol));
+
+ EXPECT_TRUE(std::isinf(em2.integrate_ixa(mcable{0, 0, 0.5})));
+}
diff --git a/test/unit/test_piecewise.cpp b/test/unit/test_piecewise.cpp
index f05b540a..0f65cf89 100644
--- a/test/unit/test_piecewise.cpp
+++ b/test/unit/test_piecewise.cpp
@@ -82,33 +82,33 @@ TEST(piecewise, assign) {
ASSERT_EQ(4u, p.size());
- EXPECT_EQ(10, p[0].element);
- EXPECT_EQ( 8, p[1].element);
- EXPECT_EQ( 9, p[2].element);
- EXPECT_EQ( 4, p[3].element);
+ EXPECT_EQ(10, p[0].value);
+ EXPECT_EQ( 8, p[1].value);
+ EXPECT_EQ( 9, p[2].value);
+ EXPECT_EQ( 4, p[3].value);
using dp = std::pair<double, double>;
- EXPECT_EQ(dp(1.0, 1.5), p.interval(0));
- EXPECT_EQ(dp(1.5, 2.0), p.interval(1));
- EXPECT_EQ(dp(2.0, 2.5), p.interval(2));
- EXPECT_EQ(dp(2.5, 3.0), p.interval(3));
+ EXPECT_EQ(dp(1.0, 1.5), p.extent(0));
+ EXPECT_EQ(dp(1.5, 2.0), p.extent(1));
+ EXPECT_EQ(dp(2.0, 2.5), p.extent(2));
+ EXPECT_EQ(dp(2.5, 3.0), p.extent(3));
pw_elements<int> q1(p);
pw_elements<int> q2;
q2 = p;
pw_elements<int> q3(p);
- q3.assign(p.vertices(), p.elements());
+ q3.assign(p.vertices(), p.values());
EXPECT_EQ((std::vector<double>{1.0, 1.5, 2.0, 2.5, 3.0}), p.vertices());
- EXPECT_EQ((std::vector<int>{10, 8, 9, 4}), p.elements());
+ EXPECT_EQ((std::vector<int>{10, 8, 9, 4}), p.values());
EXPECT_EQ(q1.vertices(), p.vertices());
EXPECT_EQ(q2.vertices(), p.vertices());
EXPECT_EQ(q3.vertices(), p.vertices());
- EXPECT_EQ(q1.elements(), p.elements());
- EXPECT_EQ(q2.elements(), p.elements());
- EXPECT_EQ(q3.elements(), p.elements());
+ EXPECT_EQ(q1.values(), p.values());
+ EXPECT_EQ(q2.values(), p.values());
+ EXPECT_EQ(q3.values(), p.values());
q3.assign({}, {});
EXPECT_TRUE(q3.empty());
@@ -131,10 +131,10 @@ TEST(piecewise, assign_void) {
ASSERT_EQ(4u, p.size());
using dp = std::pair<double, double>;
- EXPECT_EQ(dp(1.0, 1.5), p.interval(0));
- EXPECT_EQ(dp(1.5, 2.0), p.interval(1));
- EXPECT_EQ(dp(2.0, 2.5), p.interval(2));
- EXPECT_EQ(dp(2.5, 3.0), p.interval(3));
+ EXPECT_EQ(dp(1.0, 1.5), p.extent(0));
+ EXPECT_EQ(dp(1.5, 2.0), p.extent(1));
+ EXPECT_EQ(dp(2.0, 2.5), p.extent(2));
+ EXPECT_EQ(dp(2.5, 3.0), p.extent(3));
pw_elements<> q1(p);
pw_elements<> q2;
@@ -168,14 +168,14 @@ TEST(piecewise, access) {
p.assign(v, x);
for (unsigned i = 0; i<4; ++i) {
- EXPECT_EQ(v[i], p[i].interval.first);
- EXPECT_EQ(v[i+1], p[i].interval.second);
+ EXPECT_EQ(v[i], p[i].extent.first);
+ EXPECT_EQ(v[i+1], p[i].extent.second);
- EXPECT_EQ(v[i], p.interval(i).first);
- EXPECT_EQ(v[i+1], p.interval(i).second);
+ EXPECT_EQ(v[i], p.extent(i).first);
+ EXPECT_EQ(v[i+1], p.extent(i).second);
- EXPECT_EQ(x[i], p[i].element);
- EXPECT_EQ(x[i], p.element(i));
+ EXPECT_EQ(x[i], p[i].value);
+ EXPECT_EQ(x[i], p.value(i));
}
EXPECT_EQ(p[0], p.front());
@@ -191,12 +191,16 @@ TEST(piecewise, bounds) {
pw_elements<int> p{{1., 1.5, 2., 2.5, 3.}, {10, 8, 9, 4}};
EXPECT_EQ(1., p.bounds().first);
+ EXPECT_EQ(1., p.lower_bound());
EXPECT_EQ(3., p.bounds().second);
+ EXPECT_EQ(3., p.upper_bound());
pw_elements<> v{{1., 1.5, 2., 2.5, 3.}};
EXPECT_EQ(1., v.bounds().first);
+ EXPECT_EQ(1., p.lower_bound());
EXPECT_EQ(3., v.bounds().second);
+ EXPECT_EQ(3., p.upper_bound());
}
TEST(piecewise, index_of) {
@@ -240,21 +244,21 @@ TEST(piecewise, equal_range) {
auto er1 = p.equal_range(1.0);
ASSERT_EQ(1, er1.second-er1.first);
- EXPECT_EQ(10, er1.first->element);
+ EXPECT_EQ(10, er1.first->value);
auto er2 = p.equal_range(2.0);
ASSERT_EQ(2, er2.second-er2.first);
auto iter = er2.first;
- EXPECT_EQ(10, iter++->element);
- EXPECT_EQ(9, iter->element);
+ EXPECT_EQ(10, iter++->value);
+ EXPECT_EQ(9, iter->value);
auto er3_5 = p.equal_range(3.5);
ASSERT_EQ(1, er3_5.second-er3_5.first);
- EXPECT_EQ(8, er3_5.first->element);
+ EXPECT_EQ(8, er3_5.first->value);
auto er4 = p.equal_range(4.0);
ASSERT_EQ(1, er4.second-er4.first);
- EXPECT_EQ(8, er4.first->element);
+ EXPECT_EQ(8, er4.first->value);
auto er5 = p.equal_range(5.0);
ASSERT_EQ(er5.first, er5.second);
@@ -269,27 +273,26 @@ TEST(piecewise, equal_range) {
auto er1 = p.equal_range(1.0);
ASSERT_EQ(2, er1.second-er1.first);
auto iter = er1.first;
- EXPECT_EQ(10, iter++->element);
- EXPECT_EQ(11, iter++->element);
+ EXPECT_EQ(10, iter++->value);
+ EXPECT_EQ(11, iter++->value);
auto er2 = p.equal_range(2.0);
ASSERT_EQ(4, er2.second-er2.first);
iter = er2.first;
- EXPECT_EQ(11, iter++->element);
- EXPECT_EQ(12, iter++->element);
- EXPECT_EQ(13, iter++->element);
- EXPECT_EQ(14, iter++->element);
+ EXPECT_EQ(11, iter++->value);
+ EXPECT_EQ(12, iter++->value);
+ EXPECT_EQ(13, iter++->value);
+ EXPECT_EQ(14, iter++->value);
auto er3 = p.equal_range(3.0);
ASSERT_EQ(2, er3.second-er3.first);
iter = er3.first;
- EXPECT_EQ(14, iter++->element);
- EXPECT_EQ(15, iter++->element);
+ EXPECT_EQ(14, iter++->value);
+ EXPECT_EQ(15, iter++->value);
auto er5 = p.equal_range(5.0);
ASSERT_EQ(er5.first, er5.second);
}
-
}
TEST(piecewise, push) {
@@ -302,14 +305,14 @@ TEST(piecewise, push) {
q.clear();
q.push_back(1.1, 3.1, 4);
q.push_back(3.1, 4.3, 5);
- EXPECT_EQ(dp(1.1, 3.1), q.interval(0));
- EXPECT_EQ(dp(3.1, 4.3), q.interval(1));
- EXPECT_EQ(4, q[0].element);
- EXPECT_EQ(5, q[1].element);
+ EXPECT_EQ(dp(1.1, 3.1), q.extent(0));
+ EXPECT_EQ(dp(3.1, 4.3), q.extent(1));
+ EXPECT_EQ(4, q[0].value);
+ EXPECT_EQ(5, q[1].value);
q.push_back(7.2, 6);
- EXPECT_EQ(dp(4.3, 7.2), q.interval(2));
- EXPECT_EQ(6, q[2].element);
+ EXPECT_EQ(dp(4.3, 7.2), q.extent(2));
+ EXPECT_EQ(6, q[2].value);
// Supplied left side doesn't match current right.
EXPECT_THROW(q.push_back(7.4, 9.1, 7), std::runtime_error);
@@ -329,12 +332,53 @@ TEST(piecewise, push_void) {
EXPECT_EQ(3u, p.size());
EXPECT_EQ((std::vector<double>{0.1, 0.2, 0.4, 0.5}), p.vertices());
- EXPECT_EQ(dp(0.2,0.4), p.interval(1));
+ EXPECT_EQ(dp(0.2,0.4), p.extent(1));
// Supplied left side doesn't match current right.
EXPECT_THROW(p.push_back(0.7, 0.9), std::runtime_error);
}
+TEST(piecewise, mutate) {
+ pw_elements<int> p({1., 2., 3., 3., 4., 5.}, {10, 11, 12, 13, 14});
+ ASSERT_EQ(10, p.value(0));
+ ASSERT_EQ(11, p.value(1));
+
+ p.value(0) = 20;
+ EXPECT_EQ((std::vector<int>{20, 11, 12, 13, 14}), p.values());
+
+ p.front() = 30;
+ EXPECT_EQ((std::vector<int>{30, 11, 12, 13, 14}), p.values());
+
+ p[0] = 40;
+ EXPECT_EQ((std::vector<int>{40, 11, 12, 13, 14}), p.values());
+
+ for (auto&& elem: util::make_range(p.equal_range(3.))) {
+ elem = 7;
+ }
+ EXPECT_EQ((std::vector<int>{40, 7, 7, 7, 14}), p.values());
+
+ p(3.) = 50; // right most element intersecting 3.
+ EXPECT_EQ((std::vector<int>{40, 7, 7, 50, 14}), p.values());
+
+ p(3.).value = 60;
+ EXPECT_EQ((std::vector<int>{40, 7, 7, 60, 14}), p.values());
+
+ ASSERT_TRUE(std::is_const_v<decltype(p[0].extent)>);
+}
+
+TEST(piecewise, map) {
+ double xx[5] = {1, 2.25, 3.25, 3.5, 4.};
+ pw_elements<int> p(xx, (int [4]){3, 4, 5, 6});
+
+ auto void_fn = [](auto) {};
+ pw_elements<> void_expected(xx);
+ EXPECT_EQ(void_expected, pw_map(p, void_fn));
+
+ auto str_fn = [](int j) { return std::string(j, '.'); };
+ pw_elements<std::string> str_expected(xx, (const char* [4]){"...", "....", ".....", "......"});
+ EXPECT_EQ(str_expected, pw_map(p, str_fn));
+}
+
TEST(piecewise, zip) {
pw_elements<int> p03;
p03.assign((double [3]){0., 1.5, 3.}, (int [2]){10, 11});
@@ -343,42 +387,57 @@ TEST(piecewise, zip) {
p14.assign((double [5]){1, 2.25, 3.25, 3.5, 4.}, (int [4]){3, 4, 5, 6});
using ii = std::pair<int, int>;
- pw_elements<ii> p03_14 = zip(p03, p14);
+ using pipi = std::pair<pw_element<int>, pw_element<int>>;
+
+ // Zipped elements are pairs of pw_element.
+ pw_elements<pipi> p03_14_pw = pw_zip(p03, p14);
+ EXPECT_EQ(1., p03_14_pw.bounds().first);
+ EXPECT_EQ(3., p03_14_pw.bounds().second);
+
+ EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), p03_14_pw.vertices());
+ EXPECT_EQ((std::vector<pipi>{{p03[0], p14[0]}, {p03[1], p14[0]}, {p03[1], p14[1]}}),
+ p03_14_pw.values());
+
+ // To get pairs of just the element values, use pw_zip_with (with the default
+ // pw_pairify map).
+
+ pw_elements<ii> p03_14 = pw_zip_with(p03, p14);
EXPECT_EQ(1., p03_14.bounds().first);
EXPECT_EQ(3., p03_14.bounds().second);
EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), p03_14.vertices());
- EXPECT_EQ((std::vector<ii>{ii(10, 3), ii(11, 3), ii(11, 4)}), p03_14.elements());
+ EXPECT_EQ((std::vector<ii>{{10, 3}, {11, 3}, {11, 4}}), p03_14.values());
- pw_elements<ii> p14_03 = zip(p14, p03);
+ pw_elements<ii> p14_03 = pw_zip_with(p14, p03);
EXPECT_EQ(p03_14.vertices(), p14_03.vertices());
- std::vector<ii> flipped = util::assign_from(util::transform_view(p14_03.elements(),
+ std::vector<ii> flipped = util::assign_from(util::transform_view(p14_03.values(),
[](ii p) { return ii{p.second, p.first}; }));
- EXPECT_EQ(p03_14.elements(), flipped);
+ EXPECT_EQ(p03_14.values(), flipped);
pw_elements<> v03;
v03.assign((double [3]){0., 1.5, 3.});
- EXPECT_EQ((std::vector<int>{3, 3, 4}), zip(v03, p14).elements());
- EXPECT_EQ((std::vector<int>{3, 3, 4}), zip(p14, v03).elements());
+ EXPECT_EQ((std::vector<int>{3, 3, 4}), pw_zip_with(v03, p14).values());
+ EXPECT_EQ((std::vector<int>{3, 3, 4}), pw_zip_with(p14, v03).values());
- EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(v03, p14).vertices());
- EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), zip(p14, v03).vertices());
+ EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), pw_zip_with(v03, p14).vertices());
+ EXPECT_EQ((std::vector<double>{1., 1.5, 2.25, 3.}), pw_zip_with(p14, v03).vertices());
- auto project = [](double l, double r, pw_element<void>, const pw_element<int>& b) -> double {
- double b_width = b.interval.second-b.interval.first;
- return b.element*(r-l)/b_width;
+ auto project = [](std::pair<double, double> extent, pw_element<void>, const pw_element<int>& b) -> double {
+ auto [l, r] = extent;
+ double b_width = b.extent.second-b.extent.first;
+ return b.value*(r-l)/b_width;
};
pw_elements<void> vxx; // elements cover bounds of p14
vxx.assign((double [6]){0.2, 1.7, 1.95, 2.325, 2.45, 4.9});
- pw_elements<double> pxx = zip(vxx, p14, project);
- double p14_sum = util::sum(util::transform_view(p14, [](auto v) { return v.element; }));
- double pxx_sum = util::sum(util::transform_view(pxx, [](auto v) { return v.element; }));
- EXPECT_DOUBLE_EQ(p14_sum, pxx_sum);
+ pw_elements<double> pxx = pw_zip_with(vxx, p14, project);
+ double p14_sum = util::sum(util::transform_view(p14, [](auto&& v) { return v.value; }));
+ double pxx_sum = util::sum(util::transform_view(pxx, [](auto&& v) { return v.value; }));
+ EXPECT_DOUBLE_EQ(p14_sum, pxx_sum);
}
TEST(piecewise, zip_zero_length_elements) {
@@ -398,71 +457,70 @@ TEST(piecewise, zip_zero_length_elements) {
using ii = std::pair<int, int>;
{
- pw_elements<ii> zz = zip(p03a, p03b);
+ pw_elements<ii> zz = pw_zip_with(p03a, p03b);
EXPECT_EQ(0., zz.bounds().first);
EXPECT_EQ(3., zz.bounds().second);
std::vector<double> expected_vertices = {0, 0, 0, 1, 1.5, 3, 3, 3};
- std::vector<ii> expected_elements = {ii(10, 20), ii(11, 21), ii(11,22), ii(11,23), ii(12, 23), ii(13,24), ii(13,25)};
+ std::vector<ii> expected_values = {ii(10, 20), ii(11, 21), ii(11,22), ii(11,23), ii(12, 23), ii(13,24), ii(13,25)};
EXPECT_EQ(expected_vertices, zz.vertices());
- EXPECT_EQ(expected_elements, zz.elements());
+ EXPECT_EQ(expected_values, zz.values());
- pw_elements<ii> yy = zip(p03b, p03a);
- flip(expected_elements);
+ pw_elements<ii> yy = pw_zip_with(p03b, p03a);
+ flip(expected_values);
EXPECT_EQ(expected_vertices, yy.vertices());
- EXPECT_EQ(expected_elements, yy.elements());
+ EXPECT_EQ(expected_values, yy.values());
}
{
- pw_elements<ii> zz = zip(p03a, p33);
+ pw_elements<ii> zz = pw_zip_with(p03a, p33);
EXPECT_EQ(3., zz.bounds().first);
EXPECT_EQ(3., zz.bounds().second);
std::vector<double> expected_vertices = {3, 3, 3};
- std::vector<ii> expected_elements = {ii(12, 30), ii(13, 31)};
+ std::vector<ii> expected_values = {ii(12, 30), ii(13, 31)};
EXPECT_EQ(expected_vertices, zz.vertices());
- EXPECT_EQ(expected_elements, zz.elements());
+ EXPECT_EQ(expected_values, zz.values());
- pw_elements<ii> yy = zip(p33, p03a);
- flip(expected_elements);
+ pw_elements<ii> yy = pw_zip_with(p33, p03a);
+ flip(expected_values);
EXPECT_EQ(expected_vertices, yy.vertices());
- EXPECT_EQ(expected_elements, yy.elements());
-
+ EXPECT_EQ(expected_values, yy.values());
}
{
- pw_elements<ii> zz = zip(p03a, p14);
+ pw_elements<ii> zz = pw_zip_with(p03a, p14);
EXPECT_EQ(1., zz.bounds().first);
EXPECT_EQ(3., zz.bounds().second);
std::vector<double> expected_vertices = {1, 1.5, 2, 3, 3};
- std::vector<ii> expected_elements = {ii(11, 40), ii(12, 40), ii(12, 41), ii(13, 41)};
+ std::vector<ii> expected_values = {ii(11, 40), ii(12, 40), ii(12, 41), ii(13, 41)};
EXPECT_EQ(expected_vertices, zz.vertices());
- EXPECT_EQ(expected_elements, zz.elements());
+ EXPECT_EQ(expected_values, zz.values());
- pw_elements<ii> yy = zip(p14, p03a);
- flip(expected_elements);
+ pw_elements<ii> yy = pw_zip_with(p14, p03a);
+ flip(expected_values);
EXPECT_EQ(expected_vertices, yy.vertices());
- EXPECT_EQ(expected_elements, yy.elements());
+ EXPECT_EQ(expected_values, yy.values());
}
{
// Check void version too!
pw_elements<> v03a(p03a), v03b(p03b);
- pw_elements<> zz = zip(v03a, v03b);
+ pw_elements<> zz = pw_zip_with(v03a, v03b);
EXPECT_EQ(0., zz.bounds().first);
EXPECT_EQ(3., zz.bounds().second);
std::vector<double> expected_vertices = {0, 0, 0, 1, 1.5, 3, 3, 3};
EXPECT_EQ(expected_vertices, zz.vertices());
- pw_elements<> yy = zip(v03b, v03a);
+ pw_elements<> yy = pw_zip_with(v03b, v03a);
EXPECT_EQ(expected_vertices, yy.vertices());
}
}
diff --git a/test/unit/test_ratelem.cpp b/test/unit/test_ratelem.cpp
index 2f21777f..d16e1fb3 100644
--- a/test/unit/test_ratelem.cpp
+++ b/test/unit/test_ratelem.cpp
@@ -1,6 +1,6 @@
#include "../gtest.h"
-#include <list>
+#include <cmath>
#include "util/ratelem.hpp"
#include "util/rangeutil.hpp"
@@ -115,3 +115,33 @@ TYPED_TEST(ratelem_pq, interpolate_monotonic) {
}
}
}
+
+TEST(ratelem, p1q1singular) {
+ // Check special case behaviour for p==1, q==1 when interpolants
+ // are strictly monotonic but possibly infinite.
+
+ auto f1 = [](double x) { return (1-x)/x; };
+ rat_element<1, 1> r1(f1);
+
+ for (unsigned i = 0; i<=4; ++i) {
+ double x = (double)i/4.0;
+ EXPECT_DOUBLE_EQ(f1(x), r1(x));
+ }
+
+ auto f2 = [](double x) { return x/(1-x); };
+ rat_element<1, 1> r2(f2);
+
+ for (unsigned i = 0; i<=4; ++i) {
+ double x = (double)i/4.0;
+ EXPECT_DOUBLE_EQ(f2(x), r2(x));
+ }
+
+ // With p==1, q==1, all infinite node values should
+ // give us NaN when we try to evaluate.
+
+ rat_element<1, 1> nope(INFINITY, INFINITY, INFINITY);
+ for (unsigned i = 0; i<=4; ++i) {
+ double x = (double)i/4.0;
+ EXPECT_TRUE(std::isnan(nope(x)));
+ }
+}
--
GitLab