diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index 48715d7958f54b8bf0245bfb45f497772bb8469a..42a66fc7693b13594690bbcb0a4c97f2e7b0d257 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -36,6 +36,7 @@ set(arbor_sources
     morph/morphexcept.cpp
     morph/morphology.cpp
     morph/mprovider.cpp
+    morph/place_pwlin.cpp
     morph/primitives.cpp
     morph/region.cpp
     morph/sample_tree.cpp
diff --git a/arbor/include/arbor/morph/place_pwlin.hpp b/arbor/include/arbor/morph/place_pwlin.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5be0c0a11d3163c9e96f5e685ecc07aed88f84ca
--- /dev/null
+++ b/arbor/include/arbor/morph/place_pwlin.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+// 'Place' morphology in 3-d by applying an isometry to
+// sample points and interpolating linearly.
+
+#include <cmath>
+#include <utility>
+
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/math.hpp>
+
+namespace arb {
+
+struct isometry {
+    // Represent a 3-d isometry as a rotation (via quaterion q_)
+    // and a subsequent translation (tx_, ty_, tz_).
+
+    isometry() = default;
+
+    // Composition: rotations are interpreted as applied to intrinsic
+    // coordinates (composed by right multiplication), and translations
+    // as applied to absolute coordinates (composed by addition).
+
+    friend isometry operator*(const isometry& a, const isometry& b) {
+        return isometry(b.q_*a.q_, a.tx_+b.tx_, a.ty_+b.ty_, a.tz_+b.tz_);
+    }
+
+    template <typename PointLike>
+    PointLike apply(PointLike p) const {
+        auto w = quaternion(p.x, p.y, p.z)^q_;
+        p.x = w.x+tx_;
+        p.y = w.y+ty_;
+        p.z = w.z+tz_;
+        return p;
+    }
+
+private:
+    using quaternion = arb::math::quaternion;
+    quaternion q_{1, 0, 0, 0};
+    double tx_ = 0, ty_ = 0, tz_ = 0;
+
+    isometry(quaternion q, double tx, double ty, double tz):
+        q_(std::move(q)), tx_(tx), ty_(ty), tz_(tz) {}
+
+public:
+    static isometry translate(double x, double y, double z) {
+        return isometry(quaternion{1, 0, 0, 0}, x, y, z);
+    }
+
+    template <typename PointLike>
+    static isometry translate(const PointLike& p) {
+        return translate(p.x, p.y, p.z);
+    }
+
+    // Rotate about axis in direction (ax, ay, az) by theta radians.
+    static isometry rotate(double theta, double ax, double ay, double az) {
+        double norm = std::sqrt(ax*ax+ay*ay+az*az);
+        double vscale = std::sin(theta/2)/norm;
+
+        return isometry(quaternion{std::cos(theta/2), ax*vscale, ay*vscale, az*vscale}, 0, 0, 0);
+    }
+
+    template <typename PointLike>
+    static isometry rotate(double theta, const PointLike& p) {
+        return rotate(theta, p.x, p.y, p.z);
+    }
+};
+
+struct place_pwlin_data;
+
+struct place_pwlin {
+    explicit place_pwlin(const morphology& m, const isometry& iso = isometry{});
+    mpoint at(mlocation loc) const;
+
+private:
+    std::shared_ptr<place_pwlin_data> data_;
+};
+
+} // namespace arb
+
+
diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp
index a77864fa643242b08655dc25004ade29852b8149..7fc8e87b256ebd9c0e507d0f3cc979f0c764eab8 100644
--- a/arbor/include/arbor/morph/primitives.hpp
+++ b/arbor/include/arbor/morph/primitives.hpp
@@ -23,6 +23,12 @@ struct mpoint {
     double radius;   // [μm]
 
     friend std::ostream& operator<<(std::ostream&, const mpoint&);
+    friend bool operator==(const mpoint& a, const mpoint& b) {
+        return a.x==b.x && a.y==b.y && a.z==b.z && a.radius==b.radius;
+    }
+    friend bool operator!=(const mpoint& a, const mpoint& b) {
+        return !(a==b);
+    }
 };
 
 mpoint lerp(const mpoint& a, const mpoint& b, double u);
diff --git a/arbor/morph/embed_pwlin.cpp b/arbor/morph/embed_pwlin.cpp
index f20bfb272f6917d63581eed8bba5974748fe5406..64971a03e62d5f7b6780ca4cc7e4eeade5b3aae3 100644
--- a/arbor/morph/embed_pwlin.cpp
+++ b/arbor/morph/embed_pwlin.cpp
@@ -6,6 +6,7 @@
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 
+#include "morph/pwlin_common.hpp"
 #include "util/piecewise.hpp"
 #include "util/range.hpp"
 #include "util/rangeutil.hpp"
@@ -16,27 +17,6 @@ namespace arb {
 
 using util::rat_element;
 
-template <unsigned p, unsigned q>
-using pw_ratpoly = util::pw_elements<rat_element<p, q>>;
-
-template <unsigned p, unsigned q>
-using branch_pw_ratpoly = std::vector<pw_ratpoly<p, q>>;
-
-template <unsigned p, unsigned q>
-double interpolate(const branch_pw_ratpoly<p, q>& f, unsigned bid, double pos) {
-    const auto& pw = f.at(bid);
-    unsigned index = pw.index_of(pos);
-
-    const auto& element = pw.element(index);
-    std::pair<double, double> bounds = pw.interval(index);
-
-    if (bounds.first==bounds.second) return element[0];
-    else {
-        double x = (pos-bounds.first)/(bounds.second-bounds.first);
-        return element(x);
-    }
-}
-
 // Length, area, and ixa are polynomial or rational polynomial functions of branch position,
 // continuos and monotonically increasing with respect to distance from root.
 //
diff --git a/arbor/morph/place_pwlin.cpp b/arbor/morph/place_pwlin.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f7a78ef55ea7f2daf752afd26f6142c5f4475ec
--- /dev/null
+++ b/arbor/morph/place_pwlin.cpp
@@ -0,0 +1,144 @@
+#include <cmath>
+#include <memory>
+#include <vector>
+
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/place_pwlin.hpp>
+#include <arbor/morph/primitives.hpp>
+
+#include "morph/pwlin_common.hpp"
+#include "util/piecewise.hpp"
+#include "util/ratelem.hpp"
+#include "util/span.hpp"
+
+namespace arb {
+
+using util::rat_element;
+
+struct place_pwlin_data {
+    branch_pw_ratpoly<1, 0> x, y, z, r; // [µm]
+
+    explicit place_pwlin_data(msize_t n_branch):
+        x(n_branch), y(n_branch), z(n_branch), r(n_branch)
+    {}
+};
+
+mpoint place_pwlin::at(mlocation loc) const {
+    return { interpolate(data_->x, loc.branch, loc.pos),
+             interpolate(data_->y, loc.branch, loc.pos),
+             interpolate(data_->z, loc.branch, loc.pos),
+             interpolate(data_->r, loc.branch, loc.pos) };
+}
+
+place_pwlin::place_pwlin(const arb::morphology& m, const isometry& iso) {
+    msize_t n_branch = m.num_branches();
+    data_ = std::make_shared<place_pwlin_data>(n_branch);
+
+    if (!n_branch) return;
+
+    const auto& samples = m.samples();
+
+    std::vector<double> sample_pos_on_branch;
+
+    for (msize_t bid = 0; bid<n_branch; ++bid) {
+        auto sample_indices = util::make_range(m.branch_indexes(bid));
+        if (bid==0 && m.spherical_root()) {
+            arb_assert(sample_indices.size()==1);
+            arb_assert(sample_indices.front()==0);
+
+            // Use the next distinct sample, if it exists, to determine the
+            // proximal-distal axis for a spherical root.
+
+            mpoint c = samples[0].loc;
+            mpoint d = c;
+            for (msize_t i = 1; i<samples.size(); ++i) {
+                const auto p = samples[i].loc;
+                if (p.x!=c.x || p.y!=c.y || p.z!=c.z) {
+                    d = p;
+                    break;
+                }
+            }
+
+            mpoint u0 = c, u1 = c;
+
+            if (c.x!=d.x || c.y!=d.y || c.z!=d.z) {
+                double dx = d.x-c.x;
+                double dy = d.y-c.y;
+                double dz = d.z-c.z;
+                double scale = c.radius/std::sqrt(dx*dx+dy*dy+dz*dz);
+
+                u0.x -= dx*scale;
+                u0.y -= dy*scale;
+                u0.z -= dz*scale;
+
+                u1.x += dx*scale;
+                u1.y += dy*scale;
+                u1.z += dz*scale;
+            }
+            else {
+                // No luck, so pick distal as negative z-axis.
+                u0.z += c.radius;
+                u1.z -= c.radius;
+            }
+
+            u0 = iso.apply(u0);
+            u1 = iso.apply(u1);
+
+            data_->x[bid].push_back(0., 1., rat_element<1, 0>(u0.x, u1.x));
+            data_->y[bid].push_back(0., 1., rat_element<1, 0>(u0.y, u1.y));
+            data_->z[bid].push_back(0., 1., rat_element<1, 0>(u0.z, u1.z));
+            data_->r[bid].push_back(0., 1., rat_element<1, 0>(u0.radius, u1.radius));
+        }
+        else {
+            arb_assert(sample_indices.size()>1);
+
+            sample_pos_on_branch.reserve(samples.size());
+            sample_pos_on_branch = {0};
+
+            for (auto i: util::count_along(sample_indices)) {
+                if (!i) continue;
+
+                sample_pos_on_branch.push_back(
+                    sample_pos_on_branch.back()+
+                    distance(samples[sample_indices[i-1]], samples[sample_indices[i]]));
+            }
+
+            double branch_length = sample_pos_on_branch.back();
+            double length_scale = branch_length>0? 1./branch_length: 0;
+
+            for (auto& x: sample_pos_on_branch) {
+                x *= length_scale;
+            }
+
+            if (length_scale==0) {
+                // Zero-length branch case?
+
+                mpoint p = iso.apply(samples[sample_indices[0]].loc);
+
+                data_->x[bid].push_back(0., 1., rat_element<1, 0>(p.x, p.x));
+                data_->y[bid].push_back(0., 1., rat_element<1, 0>(p.y, p.y));
+                data_->z[bid].push_back(0., 1., rat_element<1, 0>(p.z, p.z));
+                data_->r[bid].push_back(0., 1., rat_element<1, 0>(p.radius, p.radius));
+            }
+            else {
+                for (auto i: util::count_along(sample_indices)) {
+                    if (!i) continue;
+
+                    double p0 = i>1? sample_pos_on_branch[i-1]: 0;
+                    double p1 = sample_pos_on_branch[i];
+                    if (p0==p1) continue;
+
+                    mpoint u0 = iso.apply(samples[sample_indices[i-1]].loc);
+                    mpoint u1 = iso.apply(samples[sample_indices[i]].loc);
+
+                    data_->x[bid].push_back(p0, p1, rat_element<1, 0>(u0.x, u1.x));
+                    data_->y[bid].push_back(p0, p1, rat_element<1, 0>(u0.y, u1.y));
+                    data_->z[bid].push_back(p0, p1, rat_element<1, 0>(u0.z, u1.z));
+                    data_->r[bid].push_back(p0, p1, rat_element<1, 0>(u0.radius, u1.radius));
+                }
+            }
+        }
+    }
+};
+
+} // namespace arb
diff --git a/arbor/morph/pwlin_common.hpp b/arbor/morph/pwlin_common.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..54d1d983f25b6e8c3b549d79ca37cea342dec056
--- /dev/null
+++ b/arbor/morph/pwlin_common.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+// Per-branch piecewise rational polynomial definitions and interpolation.
+
+#include <utility>
+#include <vector>
+
+#include "util/piecewise.hpp"
+#include "util/ratelem.hpp"
+
+namespace arb {
+
+template <unsigned p, unsigned q>
+using pw_ratpoly = util::pw_elements<util::rat_element<p, q>>;
+
+template <unsigned p, unsigned q>
+using branch_pw_ratpoly = std::vector<pw_ratpoly<p, q>>;
+
+template <unsigned p, unsigned q>
+double interpolate(const pw_ratpoly<p, q>& f, double pos) {
+    unsigned index = f.index_of(pos);
+
+    const auto& element = f.element(index);
+    std::pair<double, double> bounds = f.interval(index);
+
+    if (bounds.first==bounds.second) return element[0];
+    else {
+        double x = (pos-bounds.first)/(bounds.second-bounds.first);
+        return element(x);
+    }
+}
+
+template <unsigned p, unsigned q>
+double interpolate(const branch_pw_ratpoly<p, q>& f, unsigned bid, double pos) {
+    return interpolate(f.at(bid), pos);
+}
+
+} // namespace arb
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 3ae498dfa8b32a8a2cea72d4decc3f4ac1b11ea4..f914964ad32669b19af14639e333dca9cf6f2997 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -119,6 +119,7 @@ set(unit_sources
     test_morphology.cpp
     test_morph_embedding.cpp
     test_morph_expr.cpp
+    test_morph_place.cpp
     test_morph_primitives.cpp
     test_multi_event_stream.cpp
     test_optional.cpp
diff --git a/test/unit/test_morph_place.cpp b/test/unit/test_morph_place.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7ba2308efc8a54301f58b375b3ceb3ca3c94f502
--- /dev/null
+++ b/test/unit/test_morph_place.cpp
@@ -0,0 +1,293 @@
+#include <cmath>
+#include <vector>
+
+#include <arbor/math.hpp>
+#include <arbor/morph/place_pwlin.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/sample_tree.hpp>
+
+#include "util/piecewise.hpp"
+
+#include "../test/gtest.h"
+#include "common.hpp"
+
+using namespace arb;
+
+namespace {
+
+struct v3 {
+    double x, y, z;
+    friend double dot(v3 p, v3 q) {
+        return p.x*q.x+p.y*q.y+p.z*q.z;
+    }
+    friend v3 cross(v3 p, v3 q) {
+        return {p.y*q.z-p.z*q.y, p.z*q.x-p.x*q.z, p.x*q.y-p.y*q.x};
+    }
+    friend v3 operator*(v3 p, double s) {
+        return {s*p.x, s*p.y, s*p.z};
+    }
+    friend v3 operator*(double s, v3 p) {
+        return p*s;
+    }
+    friend v3 operator/(v3 p, double s) {
+        return p*(1./s);
+    }
+    friend v3 operator+(v3 p, v3 q) {
+        return {p.x+q.x, p.y+q.y, p.z+q.z};
+    }
+    friend double length(v3 x) {
+        return std::sqrt(dot(x, x));
+    }
+};
+
+::testing::AssertionResult mpoint_almost_eq(mpoint a, mpoint b) {
+    using FP = testing::internal::FloatingPoint<double>;
+    if (FP(a.x).AlmostEquals(FP(b.x)) &&
+        FP(a.y).AlmostEquals(FP(b.y)) &&
+        FP(a.z).AlmostEquals(FP(b.z)) &&
+        FP(a.radius).AlmostEquals(FP(b.radius)))
+    {
+        return ::testing::AssertionSuccess();
+    }
+    else {
+        return ::testing::AssertionFailure() << "mpoint values "
+            << '(' << a.x << ',' << a.y << ',' << a.z << ';' << a.radius << ')'
+            << " and "
+            << '(' << b.x << ',' << b.y << ',' << b.z << ';' << b.radius << ')'
+            << " differ";
+    }
+}
+
+::testing::AssertionResult v3_almost_eq(v3 a, v3 b) {
+    using FP = testing::internal::FloatingPoint<double>;
+    if (FP(a.x).AlmostEquals(FP(b.x)) &&
+        FP(a.y).AlmostEquals(FP(b.y)) &&
+        FP(a.z).AlmostEquals(FP(b.z)))
+    {
+        return ::testing::AssertionSuccess();
+    }
+    else {
+        return ::testing::AssertionFailure() << "xyz values "
+            << '(' << a.x << ',' << a.y << ',' << a.z << ')'
+            << " and "
+            << '(' << b.x << ',' << b.y << ',' << b.z << ')'
+            << " differ";
+    }
+}
+
+} // anonymous namespace
+
+TEST(isometry, translate) {
+    mpoint p{1, 2, 3, 4};
+
+    {
+        isometry id;
+        mpoint q = id.apply(p);
+        EXPECT_EQ(p.x, q.x);
+        EXPECT_EQ(p.y, q.y);
+        EXPECT_EQ(p.z, q.z);
+        EXPECT_EQ(p.radius, q.radius);
+    }
+    {
+        isometry shift = isometry::translate(0.5, 1.5, 3.5);
+        mpoint q = shift.apply(p);
+
+        EXPECT_EQ(p.x+0.5, q.x);
+        EXPECT_EQ(p.y+1.5, q.y);
+        EXPECT_EQ(p.z+3.5, q.z);
+        EXPECT_EQ(p.radius, q.radius);
+
+        // Should work with anything with floating point x, y, z.
+        struct v3 {
+            double x, y, z;
+        };
+
+        isometry shift_bis = isometry::translate(v3{0.5, 1.5, 3.5});
+        mpoint q_bis = shift_bis.apply(p);
+        EXPECT_EQ(q, q_bis);
+    }
+}
+
+TEST(isometry, rotate) {
+    {
+        // Rotation about axis through p should do nothing.
+        mpoint p{1, 2, 3, 4};
+
+        isometry rot = isometry::rotate(1.23, p);
+        mpoint q = rot.apply(p);
+
+        EXPECT_TRUE(mpoint_almost_eq(p, q));
+    }
+    {
+        // Rotate about an arbitrary axis.
+        //
+        // Geometry says rotating x about (normalized) u by
+        // θ should give a vector u(u.x) + (u×x)×u cos θ + u×x sin 0
+
+        double theta = 0.234;
+        v3 axis{-1, 3, 2.2};
+
+        isometry rot = isometry::rotate(theta, axis);
+        v3 x = {1, 2, 3};
+        v3 q = rot.apply(x);
+
+        v3 u = axis/length(axis);
+        v3 p = u*dot(u, x) + cross(cross(u, x), u)*std::cos(theta) + cross(u, x)*std::sin(theta);
+
+        EXPECT_TRUE(v3_almost_eq(p, q));
+    }
+}
+
+TEST(isometry, compose) {
+    // For an isometry (r, t) (r the rotation, t the translation),
+    // we compose them by:
+    //   (r, t) ∘ (s, u) = (sr, t+u).
+    //
+    // On the other hand, for a vector x:
+    //   (r, t) ((s, u) x)
+    //      = (r, t) (sx + u)
+    //      = rsx + ru + t
+    //      = (rs, ru+t) x
+    //      = ((s, ru) ∘ (r, t)) x
+
+    double theta1 = -0.3;
+    v3 axis1{1, -3, 4};
+    v3 shift1{-2, -1, 3};
+
+    double theta2 = 0.6;
+    v3 axis2{-1, 2, 0};
+    v3 shift2{3, 0, -1};
+
+    v3 p{5., 6., 7.};
+
+    // Construct (r, t):
+    isometry r = isometry::rotate(theta1, axis1);
+    isometry r_t = r*isometry::translate(shift1);
+
+    // Construct (s, u):
+    isometry s = isometry::rotate(theta2, axis2);
+    isometry s_u = s*isometry::translate(shift2);
+
+    // Construct (s, ru):
+    isometry s_ru = s*isometry::translate(r.apply(shift2));
+
+    // Compare application of s_u and r_t against application
+    // of (s, ru) ∘ (r, t).
+
+    v3 u = r_t.apply(s_u.apply(p));
+    v3 v = (s_ru * r_t).apply(p);
+
+    EXPECT_TRUE(v3_almost_eq(u, v));
+}
+
+TEST(place_pwlin, cable) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+
+    // L-shaped simple cable.
+    // 0.25 of the length in the z-direction,
+    // 0.75 of the length in the x-direction.
+
+    pvec parents = {mnpos, 0, 1};
+    svec samples = {
+        {{  0,  0,  0,  2}, 1},
+        {{  0,  0,  1,  2}, 1},
+        {{  3,  0,  1,  2}, 1}
+    };
+
+    sample_tree sm(samples, parents);
+    morphology m(sm, false);
+
+    {
+        // With no transformation:
+        place_pwlin pl(m);
+
+        // Interpolated points.
+        mpoint p_0 = pl.at(mlocation{0, 0.});
+        mpoint p_1 = pl.at(mlocation{0, 0.125});
+        mpoint p_2 = pl.at(mlocation{0, 0.25});
+        mpoint p_3 = pl.at(mlocation{0, 0.5});
+
+        // Expected results.
+        mpoint x_0{0.0, 0.0, 0.0, 2.};
+        mpoint x_1{0.0, 0.0, 0.5, 2.};
+        mpoint x_2{0.0, 0.0, 1.0, 2.};
+        mpoint x_3{1.0, 0.0, 1.0, 2.};
+
+        EXPECT_TRUE(mpoint_almost_eq(x_0, p_0));
+        EXPECT_TRUE(mpoint_almost_eq(x_1, p_1));
+        EXPECT_TRUE(mpoint_almost_eq(x_2, p_2));
+        EXPECT_TRUE(mpoint_almost_eq(x_3, p_3));
+    }
+    {
+        // With a rotation and translation:
+
+        double theta = -0.3;
+        v3 axis{1, -3, 4};
+        v3 shift{-2, -1, 3};
+
+        isometry iso = isometry::rotate(theta, axis)*isometry::translate(shift);
+        place_pwlin pl(m, iso);
+
+        // Interpolated points.
+        mpoint p_0 = pl.at(mlocation{0, 0.});
+        mpoint p_1 = pl.at(mlocation{0, 0.125});
+        mpoint p_2 = pl.at(mlocation{0, 0.25});
+        mpoint p_3 = pl.at(mlocation{0, 0.5});
+
+        // Expected results.
+        mpoint x_0 = iso.apply(mpoint{0.0, 0.0, 0.0, 2.});
+        mpoint x_1 = iso.apply(mpoint{0.0, 0.0, 0.5, 2.});
+        mpoint x_2 = iso.apply(mpoint{0.0, 0.0, 1.0, 2.});
+        mpoint x_3 = iso.apply(mpoint{1.0, 0.0, 1.0, 2.});
+
+        EXPECT_TRUE(mpoint_almost_eq(x_0, p_0));
+        EXPECT_TRUE(mpoint_almost_eq(x_1, p_1));
+        EXPECT_TRUE(mpoint_almost_eq(x_2, p_2));
+        EXPECT_TRUE(mpoint_almost_eq(x_3, p_3));
+    }
+}
+
+TEST(place_pwlin, branched) {
+    using pvec = std::vector<msize_t>;
+    using svec = std::vector<msample>;
+
+    // Y-shaped branched morphology.
+    // Second branch (branch 1) tapers radius.
+
+    pvec parents = {mnpos, 0, 1, 2, 3, 4, 2, 6};
+    svec samples = {
+        // branch 0
+        {{  0,  0,  0,  2}, 1},
+        {{  0,  0,  1,  2}, 1},
+        {{  3,  0,  1,  2}, 1},
+        // branch 1
+        {{  3,  0,  1,  1.0}, 1},
+        {{  3,  1,  1,  1.0}, 1},
+        {{  3,  2,  1,  0.0}, 1},
+        // branch 2
+        {{  3,  0,  1,  2} , 1},
+        {{  3,  -1, 1,  2} , 1}
+    };
+
+    sample_tree sm(samples, parents);
+    morphology m(sm, false);
+
+    isometry iso = isometry::translate(2, 3, 4);
+    place_pwlin pl(m, iso);
+
+    // Examnine points on branch 1.
+
+    mpoint p_0 = pl.at(mlocation{1, 0.});
+    mpoint p_1 = pl.at(mlocation{1, 0.25});
+    mpoint p_2 = pl.at(mlocation{1, 0.75});
+
+    mpoint x_0 = iso.apply(mpoint{3, 0,   1, 1});
+    mpoint x_1 = iso.apply(mpoint{3, 0.5, 1, 1});
+    mpoint x_2 = iso.apply(mpoint{3, 1.5, 1, 0.5});
+
+    EXPECT_TRUE(mpoint_almost_eq(x_0, p_0));
+    EXPECT_TRUE(mpoint_almost_eq(x_1, p_1));
+    EXPECT_TRUE(mpoint_almost_eq(x_2, p_2));
+}