Ts: fix buggy derivative cases, and improve test coverage.
authorPixneb <Pixneb@users.noreply.github.com>
Sat, 3 Feb 2024 03:59:14 +0000 (19:59 -0800)
committerpixar-oss <pixar-oss@users.noreply.github.com>
Sat, 3 Feb 2024 04:11:16 +0000 (20:11 -0800)
Derivatives are supposed to reflect what happens in value evaluation.  There
were several cases where derivatives disagreed with the curve shape given by
value evaluation.

Most of these cases involved linear and/or held knots.

Some example cases - not an exhaustive list:

- Derviatives at all linear knots were incorrectly evaluated from one of the
  spline's extrapolation slopes.

- Ineffective tangent values on linear and held knots were incorrectly being
  considered at neighboring knots.

- Ineffective extrapolation-facing tangent values on Bezier knots were
  incorrectly being returned verbatim, even when held extrapolation means that
  these slopes are zero.  This was the only case involving Bezier knots.

This change completely rewrites Ts_Eval, and partially rewrites some of its
helpers.  Some test baselines that codified the incorrect behavior have been
updated.

Add testTsDerivatives.  I believe this covers all possible derivative cases.

Derivatives are well-defined for value types that are, in the language of
TsTraits, extrapolatable.  This is a bit of a misnomer.  It is true that
"extrapolatable" means "we will perform extrapolation".  But it also means "we
can determine a slope".

(Internal change: 2313969)

pxr/base/ts/CMakeLists.txt
pxr/base/ts/data.h
pxr/base/ts/evalUtils.cpp
pxr/base/ts/keyFrame.cpp
pxr/base/ts/keyFrame.h
pxr/base/ts/testenv/testTsDerivatives.py [new file with mode: 0644]
pxr/base/ts/testenv/testTsSpline.py
pxr/base/ts/testenv/testTs_HardToReach.cpp
pxr/base/ts/types.h

index 62a2e6a7a46942577aa9b94834465f14fe3e44cf..8bb7291754a193af9f62ecf04aad5aa333f54851 100644 (file)
@@ -133,6 +133,7 @@ pxr_test_scripts(
     testenv/testTsSplineAPI.py
     testenv/testTsKeyFrame.py
     testenv/testTsSimplify.py
+    testenv/testTsDerivatives.py
     testenv/tsTest_TsFramework.py
 )
 
@@ -165,6 +166,12 @@ pxr_register_test(
     COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsSimplify"
 )
 
+pxr_register_test(
+    testTsDerivatives
+    PYTHON
+    COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsDerivatives"
+)
+
 pxr_register_test(
     testTs_HardToReach
     COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTs_HardToReach"
index e3640c3d53dca80ffd57f2a0543cee4569ebc417..96a27c3382b60da0a34f880876380a70d31227bc 100644 (file)
@@ -96,6 +96,7 @@ public:
     virtual void SetLeftValue( VtValue ) = 0;
     virtual VtValue GetZero() const = 0;
     virtual bool ValueCanBeInterpolated() const = 0;
+    virtual bool ValueCanBeExtrapolated() const = 0;
 
     // Extrapolation.
     // Note these methods don't actually use any data from this object
@@ -197,6 +198,7 @@ public:
     void SetLeftValue( VtValue ) override;
     VtValue GetZero() const override;
     bool ValueCanBeInterpolated() const override;
+    bool ValueCanBeExtrapolated() const override;
 
     // Tangents
     bool HasTangents() const override;
@@ -215,7 +217,7 @@ public:
 
 public:
 
-    // Extrapolation methods.
+    // Slope computation methods.
 
     VtValue GetSlope(const Ts_Data &right) const override
     {
@@ -750,6 +752,13 @@ Ts_TypedData<T>::ValueCanBeInterpolated() const
     return TsTraits<T>::interpolatable;
 }
 
+template <typename T>
+bool
+Ts_TypedData<T>::ValueCanBeExtrapolated() const
+{
+    return TsTraits<T>::extrapolatable;
+}
+
 template <typename T>
 bool
 Ts_TypedData<T>::HasTangents() const
index 6c2540509180ee39f10a3452c97928ea9b81ec57..e4b6ab86ab8ff03674a5b70f65eedf0800a7f5b7 100644 (file)
@@ -29,6 +29,7 @@
 
 #include "pxr/base/gf/math.h"
 #include "pxr/base/gf/interval.h"
+#include "pxr/base/tf/diagnostic.h"
 
 #include <limits>
 
@@ -41,12 +42,6 @@ Ts_GetEffectiveExtrapolationType(
         bool kfIsOnlyKeyFrame,
         TsSide side)
 {
-    // Check for held extrapolation
-    if ((side == TsLeft  && extrapolation.first  == TsExtrapolationHeld) ||
-        (side == TsRight && extrapolation.second == TsExtrapolationHeld)) {
-        return TsExtrapolationHeld;
-    }
-
     // Extrapolation is held if key frame is Held
     if (kf.GetKnotType() == TsKnotHeld) {
         return TsExtrapolationHeld;
@@ -83,8 +78,7 @@ Ts_GetEffectiveExtrapolationType(
 ////////////////////////////////////////////////////////////////////////
 
 static VtValue
-_GetSlope(
-        TsTime time,
+_GetExtrapolationSlope(
         TsSpline::const_iterator i,
         const TsSpline & val,
         TsSide side)
@@ -103,6 +97,10 @@ _GetSlope(
                     kf.GetRightTangentSlope();
             }
             else {
+                // Linear extrapolation without explicit outward-facing tangent.
+                // Compute the extrapolation slope as the line passing through
+                // the last two knots at the end we're extrapolating from.
+
                 // Set i and j to the left and right key frames of the segment
                 // with the slope we want to extrapolate.
                 TsSpline::const_iterator j = i;
@@ -129,7 +127,7 @@ _Extrapolate(
         const TsSpline &val,
         TsSide side)
 {
-    VtValue slope = _GetSlope(time, i, val, side);
+    VtValue slope = _GetExtrapolationSlope(i, val, side);
     const TsKeyFrame& kf = *i;
     VtValue value = (side == TsLeft) ? kf.GetLeftValue() : kf.GetValue();
     TsTime dt   = time - kf.GetTime();
@@ -138,13 +136,24 @@ _Extrapolate(
 }
 
 static VtValue
-_ExtrapolateDerivative(
-        TsTime time,
-        TsSpline::const_iterator i,
-        const TsSpline &val,
-        TsSide side)
+_GetSlopeToAdjacentKnot(
+    TsSpline::const_iterator i,
+    TsSide side)
 {
-    return _GetSlope(time, i, val, side);
+    // Set i and j to the left and right key frames of the segment
+    // with the slope we want to measure.  Caller has already verified that
+    // there is an additional keyframe in that direction.
+    TsSpline::const_iterator j = i;
+    if (side == TsRight) {
+        // Want from i to next.  Increment j.
+        ++j;
+    }
+    else {
+        // Want from previous to i.  Decrement i.
+        --i;
+    }
+
+    return Ts_GetKeyFrameData(*i)->GetSlope(*Ts_GetKeyFrameData(*j));
 }
 
 VtValue
@@ -168,76 +177,122 @@ Ts_Eval(
     //if (fabs(rounded-time) < ARCH_MIN_FLOAT_EPS_SQR)
     //    time = rounded;
 
-    // Get the keyframe after time
-    TsSpline::const_iterator iAfterTime = val.upper_bound(time);
-    TsSpline::const_iterator i = iAfterTime;
-
-    // Check boundary cases
-    if (i == val.begin()) {
-        // Before first keyframe.  Extrapolate to the left.
-        return (evalType == Ts_EvalValue) ?
-            _Extrapolate(time, i, val, TsLeft) :
-            _ExtrapolateDerivative(time, i, val, TsLeft);
-    }
-    // Note if at or after last keyframe.
-    bool last = (i == val.end());
-
-    // Get the keyframe at or before time
-    --i;
-
-    if (i->GetTime() == time && side == TsLeft) {
-        // Evaluate at a keyframe on the left.  If the previous
-        // keyframe is held then use the right side of the previous
-        // keyframe.
-        if (i != val.begin()) {
-            TsSpline::const_iterator j = i;
-            --j;
-            if (j->GetKnotType() == TsKnotHeld) {
-                return (evalType == Ts_EvalValue) ?
-                    j->GetValue() :
-                    j->GetValueDerivative();
+    // Figure out where we are in the series.  Find the bracketing knots, the
+    // knot we're at, if any, and what type of position (before start, after
+    // end, at first knot, at last knot, at another knot, between knots).
+    const auto lbIt = val.lower_bound(time);
+    const auto prevIt = (lbIt != val.begin() ? lbIt - 1 : val.end());
+    const bool atKnot = (lbIt != val.end() && lbIt->GetTime() == time);
+    const auto knotIt = (atKnot ? lbIt : val.end());
+    const auto nextIt = (atKnot ? lbIt + 1 : lbIt);
+    const bool beforeStart = (nextIt == val.begin());
+    const bool afterEnd = (prevIt == val.end() - 1);
+    const bool atFirst = (knotIt == val.begin());
+    const bool atLast = (knotIt == val.end() - 1);
+
+    if (atKnot) {
+        // At a knot.
+        if (evalType == Ts_EvalValue) {
+            // Handle values.
+            if (side == TsLeft
+                    && !atFirst && prevIt->GetKnotType() == TsKnotHeld) {
+                // Left value after held knot = previous knot value.
+                return prevIt->GetValue();
+            }
+            else {
+                // Not a special case.  Return what's stored in the knot.
+                return (side == TsLeft) ?
+                    knotIt->GetLeftValue() : knotIt->GetValue();
             }
         }
-        // handle derivatives of linear knots at keyframes differently
-        if (i->GetKnotType() == TsKnotLinear && 
-            evalType == Ts_EvalDerivative) {
-            // if we are next to last, eval from the right, 
-            // otherwise use the specified direction
-            return _GetSlope(time, i, val, last && (side == TsLeft) ?
-                             TsRight :
-                             side);
+        else {
+            // Handle derivatives.
+            if (!knotIt->IsExtrapolatable()) {
+                // Not extrapolatable -> derivative always zero.
+                return knotIt->GetZero();
+            }
+            else if (side == TsLeft) {
+                if (atFirst) {
+                    // Left derivative at first knot = extrapolation slope.
+                    return _GetExtrapolationSlope(knotIt, val, TsLeft);
+                }
+                else if (prevIt->GetKnotType() == TsKnotHeld) {
+                    // Left derivative after held knot = zero.
+                    return knotIt->GetZero();
+                }
+                else if (knotIt->GetKnotType() == TsKnotHeld
+                        && prevIt->GetKnotType() == TsKnotBezier) {
+                    // Left derivative of held knot after Bezier = zero.
+                    return knotIt->GetZero();
+                }
+                else if (knotIt->GetKnotType() == TsKnotHeld
+                        && prevIt->GetKnotType() == TsKnotLinear) {
+                    // Left derivative of held after linear = slope to adjacent.
+                    return _GetSlopeToAdjacentKnot(knotIt, TsLeft);
+                }
+                else if (knotIt->GetKnotType() == TsKnotLinear) {
+                    // Derivative at linear knot = slope to adjacent knot.
+                    return _GetSlopeToAdjacentKnot(knotIt, TsLeft);
+                }
+                else {
+                    // Not a special case.  Return what's stored in the knot.
+                    return knotIt->GetLeftValueDerivative();
+                }
+            }
+            else {
+                if (atLast) {
+                    // Right derivative at last knot = extrapolation slope.
+                    return _GetExtrapolationSlope(knotIt, val, TsRight);
+                }
+                else if (knotIt->GetKnotType() == TsKnotHeld) {
+                    // Right derivative at held knot = zero.
+                    return knotIt->GetZero();
+                }
+                else if (knotIt->GetKnotType() == TsKnotLinear) {
+                    // Derivative at linear knot = slope to adjacent knot.
+                    return _GetSlopeToAdjacentKnot(knotIt, TsRight);
+                }
+                else {
+                    // Not a special case.  Return what's stored in the knot.
+                    return knotIt->GetValueDerivative();
+                }
+            }
         }
-        return (evalType == Ts_EvalValue) ?
-            i->GetLeftValue() :
-            i->GetLeftValueDerivative();
     }
-    else if (last) {
-        // After last key frame.  Extrapolate to the right.
+    else if (beforeStart) {
+        // Before first knot.  Extrapolate to the left.
         return (evalType == Ts_EvalValue) ?
-            _Extrapolate(time, i, val, TsRight) :
-            _ExtrapolateDerivative(time, i, val, TsRight);
+            _Extrapolate(time, nextIt, val, TsLeft) :
+            _GetExtrapolationSlope(nextIt, val, TsLeft);
     }
-    else if (i->GetTime() == time) {
-        // Evaluate at a keyframe on the right
-        // handle derivatives of linear knots at keyframes differently
-        if (i->GetKnotType() == TsKnotLinear 
-            && evalType == Ts_EvalDerivative)
-        {
-            return _GetSlope(time, i, val, 
-                             (i == val.begin() && side == TsRight) ?
-                             TsLeft : side);
-        }
+    else if (afterEnd) {
+        // After last knot.  Extrapolate to the right.
         return (evalType == Ts_EvalValue) ?
-            i->GetValue() :
-            i->GetValueDerivative();
+            _Extrapolate(time, prevIt, val, TsRight) :
+            _GetExtrapolationSlope(prevIt, val, TsRight);
     }
     else {
-        // Evaluate at a keyframe on the right or between keyframes
-        return (evalType == Ts_EvalValue) ?
-            Ts_UntypedEvalCache::EvalUncached(*i, *iAfterTime, time) :
-            Ts_UntypedEvalCache::EvalDerivativeUncached(
-                *i, *iAfterTime, time);
+        // Between knots.  Interpolate.
+        if (evalType == Ts_EvalDerivative
+                && prevIt->IsExtrapolatable() && !prevIt->SupportsTangents()
+                && prevIt->GetKnotType() == TsKnotLinear) {
+            // Slope-capable but not tangent-capable linear derivative.
+            // The interpolation math doesn't want to handle this case.
+            return _GetSlopeToAdjacentKnot(prevIt, TsRight);
+        }
+        else {
+            // Not a special case.  Do the interpolation math.
+            return (evalType == Ts_EvalValue) ?
+                Ts_UntypedEvalCache::EvalUncached(
+                    *prevIt, *nextIt, time) :
+                Ts_UntypedEvalCache::EvalDerivativeUncached(
+                    *prevIt, *nextIt, time);
+        }
     }
+
+    // Unreachable; make compiler happy
+    TF_CODING_ERROR("We have reached the unreachable");
+    return VtValue();
 }
 
 // For the routine below, define loose comparisons to account for precision
index 0ffef4395a4ba116736eb81e7f7f916e2d197a1e..f0196d11a16e7113a34dd99e5093d57e16660196 100644 (file)
@@ -285,6 +285,12 @@ TsKeyFrame::IsInterpolatable() const
     return _holder.Get()->ValueCanBeInterpolated();
 }
 
+bool
+TsKeyFrame::IsExtrapolatable() const
+{
+    return _holder.Get()->ValueCanBeExtrapolated();
+}
+
 bool
 TsKeyFrame::SupportsTangents() const
 {
index 8eeb70f8c3f5c8b0cd4174ff4a3adb767f727d43..6f367c348075e47bca01fcc82e3671f8ed71d0e8 100644 (file)
@@ -274,6 +274,12 @@ public: // methods
     TS_API
     bool IsInterpolatable() const;
 
+    /// Gets whether the value type of this keyframe is extrapolatable.  This
+    /// means that a slope can be computed from the line between two knots of
+    /// this knot's value type.
+    TS_API
+    bool IsExtrapolatable() const;
+
     /// Gets whether the value type of this keyframe supports tangents.  This
     /// will return true not only for Bezier, but also for Linear and Held,
     /// because when authors switch from Bezier to Linear/Held and back to
diff --git a/pxr/base/ts/testenv/testTsDerivatives.py b/pxr/base/ts/testenv/testTsDerivatives.py
new file mode 100644 (file)
index 0000000..d4dfb6b
--- /dev/null
@@ -0,0 +1,466 @@
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+#    names, trademarks, service marks, or product names of the Licensor
+#    and its affiliates, except as required to comply with Section 4(c) of
+#    the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr import Ts, Gf
+
+import unittest
+
+
+class TsTest_Derivatives(unittest.TestCase):
+
+    ############################################################################
+    # HELPERS
+
+    def _DoTest(
+            self, case, extrap,
+            types, times, vals,
+            expB, expL, expR,
+            single = True, dual = True):
+        """
+        Build a spline and validate expected derivatives.
+
+        The knots are specified by the parallel lists 'types', 'times', and
+        'vals'.  Types may be "H" (held), "L" (linear), or "B" (Bezier).  Times
+        and vals are floats.
+
+        Extrapolation mode is specified by 'extrap', which is "H" (held) or "L"
+        (linear).
+
+        Expected derivative values are specified by 'expB', 'expL', and 'expR'.
+        These respectively are between knots, on the left sides of knots, and on
+        the right sides of knots.  'expB' is one element longer than the knot
+        lists; it gives expected values before the first knot, between each pair
+        of knots, and after the last.  'expL' and 'expR' correspond exactly to
+        the knot lists.
+
+        Values in 'expB', 'expL', and 'expR' may be floats, or they may be
+        strings that denote special values; see 'expMap' in the implementation.
+
+        Single-valued and dual-valued knots may be tested.  Specify which in
+        'single' and 'dual', which are both true by default.
+
+        Float-valued splines are always tested.  If there are no Bezier knots in
+        the 'types' list, vector-valued splines will also be tested.  Vector
+        values are just Gf.Vec2d with both components set to the float values
+        specified in 'vals'.
+        """
+        # Constants we use in our splines.
+        bezTans = {"lens": [.5, .5], "slopes": [.3, .3]}
+        garbageTans = {"lens": [2.7, 0.8], "slopes": [0.4, -4.0]}
+
+        # Meanings of convenience abbreviations.
+        extrapMap = {"H": Ts.ExtrapolationHeld, "L": Ts.ExtrapolationLinear}
+        typeMap = {"H": Ts.KnotHeld, "L": Ts.KnotLinear, "B": Ts.KnotBezier}
+
+        # Special expected results that are specified by name.
+        # "b" is the Bezier tangent slope we use.
+        # "xy" is the midpoint tangent in a segment bracketed by types x and y.
+        # The midpoint tangents are empirical, not mathematically derived.
+        expMap = {"b": .3, "bb": 1.70, "bl": 1.35, "bh": 1.68, "lb": 1.19}
+
+        # Set up spline.
+        spline = Ts.Spline()
+        extrapMode = extrapMap[extrap]
+        spline.extrapolation = (extrapMode, extrapMode)
+
+        # Add knots as specified.
+        # Give non-Bezier knots garbage tangents, to ensure they have no effect.
+        # Remember whether there are any Beziers; if there are, no vectors.
+        doVectors = True
+        for i in range(len(types)):
+            knot = Ts.KeyFrame(times[i], float(vals[i]), typeMap[types[i]])
+            tans = bezTans if types[i] == "B" else garbageTans
+            knot.leftLen = tans["lens"][0]
+            knot.leftSlope = tans["slopes"][0]
+            knot.rightLen = tans["lens"][1]
+            knot.rightSlope = tans["slopes"][1]
+            spline.SetKeyFrame(knot)
+            if types[i] == "B":
+                doVectors = False
+
+        # Build the list of between-knot times.
+        # This also includes one pre-extrapolation and one post-extrapolation.
+        betTimes = []
+        betTimes.append(times[0] - 1)
+        for i in range(len(times) - 1):
+            betTimes.append((times[i] + times[i + 1]) / 2)
+        betTimes.append(times[-1] + 1)
+
+        # Translate any symbolic expected results into numeric values.
+        for expList in expB, expL, expR:
+            for i in range(len(expList)):
+                expVal = expList[i]
+                if type(expVal) == str:
+                    if expVal.startswith("-"):
+                        negate = True
+                        expVal = expVal[1:]
+                    else:
+                        negate = False
+                    expVal = expMap[expVal]
+                    if negate:
+                        expVal *= -1
+                    expList[i] = expVal
+
+        # Test with scalar values.
+        self._DoTestWithValueTypeVariation(
+            case,
+            spline, times, betTimes, expB, expL, expR,
+            single, dual)
+
+        # Test with vector values if the knot types permit.
+        if doVectors:
+            # Build vector-valued spline.
+            vecSpline = Ts.Spline()
+            vecSpline.extrapolation = (extrapMode, extrapMode)
+            for i in range(len(types)):
+                floatVal = float(vals[i])
+                value = Gf.Vec2d(floatVal, floatVal)
+                knot = Ts.KeyFrame(times[i], value, typeMap[types[i]])
+                vecSpline.SetKeyFrame(knot)
+
+            # Build vector-valued expected values.
+            vecExpB = [Gf.Vec2d(v, v) for v in expB]
+            vecExpL = [Gf.Vec2d(v, v) for v in expL]
+            vecExpR = [Gf.Vec2d(v, v) for v in expR]
+
+            # Test with vector values.
+            self._DoTestWithValueTypeVariation(
+                f"{case} (vectors)",
+                vecSpline, times, betTimes, vecExpB, vecExpL, vecExpR,
+                single, dual)
+
+    def _DoTestWithValueTypeVariation(
+            self, case, spline, times, betTimes, expB, expL, expR,
+            single, dual):
+
+        if single:
+            # Test with the original spline.
+            self._DoTestWithDualityVariation(
+                case,
+                spline, times, betTimes, expB, expL, expR)
+
+        if dual:
+            # Modify the spline to have dual-valued knots.
+            # Give each segment an offset of 1 unit from previous.
+            # This shouldn't affect derivatives at all.
+            for i in range(len(times)):
+                knot = spline[times[i]]
+                knot.isDualValued = True
+                values = list(knot.value)
+                if type(values[0]) is float:
+                    values[0] += i
+                    values[1] += i + 1
+                else:
+                    values[0][0] += i
+                    values[0][1] += i
+                    values[1][0] += i + 1
+                    values[1][1] += i + 1
+                knot.value = values
+                spline.SetKeyFrame(knot)
+
+            # Test with the dual-valued spline.
+            self._DoTestWithDualityVariation(
+                f"{case} (dual-valued)",
+                spline, times, betTimes, expB, expL, expR)
+
+    def _DoTestWithDualityVariation(
+            self, case, spline, times, betTimes, expB, expL, expR):
+
+        print()
+        print(f"{case}:")
+
+        # Compare results between knots, and at left and right sides of knots.
+        errors = 0
+        errors += self._DoTestWithPositionVariation(
+            "Betweens", spline, betTimes, expB, Ts.Right)
+        errors += self._DoTestWithPositionVariation(
+            "Lefts", spline, times, expL, Ts.Left)
+        errors += self._DoTestWithPositionVariation(
+            "Rights", spline, times, expR, Ts.Right)
+
+        self.assertEqual(errors, 0)
+
+    def _DoTestWithPositionVariation(
+            self, title, spline, times, expList, side):
+
+        DERIV_TOLERANCE = 1e-2
+        SAMPLE_DISTANCE = 1e-3
+        SAMPLE_TOLERANCE = 1e-5
+
+        errors = 0
+
+        print()
+        print(f"  {title}:")
+
+        # Verify each derivative result matches the expected result.  Also
+        # verify the derivative is mathematically correct, predicting the value
+        # a short distance away.
+        for i in range(len(times)):
+
+            # Look up time and expected derivative value.
+            time = times[i]
+            expected = expList[i]
+
+            # Evaluate derivative.
+            deriv = spline.EvalDerivative(time, side)
+
+            # Evaluate nearby predicted and actual values.
+            if side == Ts.Left:
+                valueAtTime = spline.Eval(time, Ts.Left)
+                valueNearby = spline.Eval(time - SAMPLE_DISTANCE)
+                predicted = valueAtTime - deriv * SAMPLE_DISTANCE
+            else:
+                valueAtTime = spline.Eval(time, Ts.Right)
+                valueNearby = spline.Eval(time + SAMPLE_DISTANCE)
+                predicted = valueAtTime + deriv * SAMPLE_DISTANCE
+
+            # Compute errors from expected values.
+            if type(deriv) is float:
+                diff = deriv - expected
+                sampleDiff = predicted - valueNearby
+            else:
+                diff = max(
+                    deriv[0] - expected[0],
+                    deriv[1] - expected[1])
+                sampleDiff = max(
+                    predicted[0] - valueNearby[0],
+                    predicted[1] - valueNearby[1])
+
+            # Check error tolerances.
+            if abs(diff) < DERIV_TOLERANCE \
+                   and abs(sampleDiff) < SAMPLE_TOLERANCE:
+                status = "PASS"
+            else:
+                status = "**FAIL"
+                errors += 1
+
+            # Print results.
+            print(f"    {time}: {status}: "
+                  f"expected {expected}, actual {deriv}, diff {diff}, "
+                  f"predicted {predicted}, actual {valueNearby}, "
+                  f"diff {sampleDiff}")
+
+        return errors
+
+    ############################################################################
+    # TEST ROUTINES
+
+    def test_Main(self):
+        """
+        Exercise every combination of successive knot types.
+        """
+        # Fit-in-80-column madness
+        nbl = "-bl"
+        bb = "bb"
+        nbh = "-bh"
+        lb = "lb"
+
+        # H = held, L = linear, B = bezier
+        # expB = expected between; expL = expected left; expR = expected right
+        extrap = "H"
+        types = [ "H", "H", "L", "L", "B", "B", "H", "B", "L", "B", "L", "H" ]
+        times = [  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11  ]
+        vals =  [  0,   1,   0,   1,   0,   1,   0,   1,   0,   1,   0,   1  ]
+        expB =  [0,  0,   0,   1,  nbl,  bb, nbh,  0,  nbl,  lb, nbl,  1,   0]
+        expL =  [  0,   0,   0,   1,  .3,  .3,   0,   0,  -1,  .3,  -1,   1  ]
+        expR =  [  0,   0,   1,  -1,  .3,  .3,   0,  .3,   1,  .3,   1,   0  ]
+
+        self._DoTest("Main", extrap, types, times, vals, expB, expL, expR)
+
+    def test_Vectors(self):
+        """
+        Exercise every combination of successive knot types for vector values.
+        """
+        extrap = "H"
+        types = [ "H", "H", "L", "L", "H" ]
+        times = [  0,   1,   2,   3,   4  ]
+        vals =  [  0,   1,   0,   1,   0  ]
+        expB =  [0,  0,   0,   1,   -1,  0]
+        expL =  [  0,   0,   0,   1,  -1  ]
+        expR =  [  0,   0,   1,  -1,   0  ]
+
+        self._DoTest("Vectors", extrap, types, times, vals, expB, expL, expR)
+
+    def test_Empty(self):
+        """
+        Verify that an empty spline has a nonexistent derivative.
+        """
+        spline = Ts.Spline()
+        self.assertEqual(spline.EvalDerivative(0), None)
+
+    def test_StringValued(self):
+        """
+        Verify that a string-valued spline has no meaningful derivatives.
+        """
+        spline = Ts.Spline()
+        spline.SetKeyFrame(Ts.KeyFrame(0, "welcome"))
+        spline.SetKeyFrame(Ts.KeyFrame(1, "dandelions"))
+        self.assertEqual(spline.EvalDerivative(-1), "")
+        self.assertEqual(spline.EvalDerivative(0, Ts.Left), "")
+        self.assertEqual(spline.EvalDerivative(0, Ts.Right), "")
+        self.assertEqual(spline.EvalDerivative(.5), "")
+        self.assertEqual(spline.EvalDerivative(1, Ts.Left), "")
+        self.assertEqual(spline.EvalDerivative(1, Ts.Right), "")
+        self.assertEqual(spline.EvalDerivative(2), "")
+
+    def test_QuatValued(self):
+        """
+        Verify that a quaternion-valued spline has no meaningful derivatives.
+        """
+        spline = Ts.Spline()
+        spline.SetKeyFrame(Ts.KeyFrame(0, Gf.Quatd(1, 2, 3, 4)))
+        spline.SetKeyFrame(Ts.KeyFrame(1, Gf.Quatd(5, 6, 7, 8)))
+        self.assertEqual(spline.EvalDerivative(-1), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(0, Ts.Left), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(0, Ts.Right), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(.5), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(1, Ts.Left), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(1, Ts.Right), Gf.Quatd())
+        self.assertEqual(spline.EvalDerivative(2), Gf.Quatd())
+
+    def test_Single(self):
+        """
+        Test derivatives of single-knot splines.  These splines are flat, except
+        in the case of a Bezier knot and linear extrapolation.
+        """
+        times = [1]
+        vals = [5]
+        expB = [0, 0]
+        expL = [0]
+        expR = [0]
+
+        extrap = "H"
+        types = ["H"]
+        self._DoTest("Single, held knot, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["H"]
+        self._DoTest("Single, held knot, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "H"
+        types = ["L"]
+        self._DoTest("Single, linear knot, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["L"]
+        self._DoTest("Single, linear knot, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "H"
+        types = ["B"]
+        self._DoTest("Single, Bezier knot, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["B"]
+        expB = ["b", "b"]
+        expL = ["b"]
+        expR = ["b"]
+        self._DoTest("Single, Bezier knot, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+    def test_Extrapolation(self):
+        """
+        Test derivatives in exrapolation regions.  This includes regions outside
+        all knots, and also on the outward-facing sides of edge knots.  Test all
+        combinations of knot type and extrapolation mode.
+        """
+        times = [0, 1]
+        vals = [0, 1]
+
+        extrap = "H"
+        types = ["H", "H"]
+        expB = [0, 0, 0]
+        expL = [0, 0]
+        expR = [0, 0]
+        self._DoTest("Extrapolation, held knots, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["H", "H"]
+        expB = [0, 0, 0]
+        expL = [0, 0]
+        expR = [0, 0]
+        self._DoTest("Extrapolation, held knots, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "H"
+        types = ["L", "L"]
+        expB = [0, 1, 0]
+        expL = [0, 1]
+        expR = [1, 0]
+        self._DoTest("Extrapolation, linear knots, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["L", "L"]
+        expB = [1, 1, 1]
+        expL = [1, 1]
+        expR = [1, 1]
+        self._DoTest("Extrapolation, linear knots, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR,
+                     single = True, dual = False)
+
+        # Linear edge knots, with linear extrapolation, behave differently when
+        # there are dual values.  The determination of an extrapolation slope
+        # from the line between the last two knots is abandoned because the dual
+        # values make it ambiguous.  Held extrapolation is used instead.
+        extrap = "L"
+        types = ["L", "L"]
+        expB = [0, 1, 0]
+        expL = [0, 1]
+        expR = [1, 0]
+        self._DoTest("Extrapolation, linear knots, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR,
+                     single = False, dual = True)
+
+        extrap = "H"
+        types = ["B", "B"]
+        expB = [0, "bb", 0]
+        expL = [0, "b"]
+        expR = ["b", 0]
+        self._DoTest("Extrapolation, Bezier knots, held extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+        extrap = "L"
+        types = ["B", "B"]
+        expB = ["b", "bb", "b"]
+        expL = ["b", "b"]
+        expR = ["b", "b"]
+        self._DoTest("Extrapolation, Bezier knots, linear extrap",
+                     extrap, types, times, vals, expB, expL, expR)
+
+
+if __name__ == "__main__":
+
+    # 'buffer' means that all stdout will be captured and swallowed, unless
+    # there is an error, in which case the stdout of the erroring case will be
+    # printed on stderr along with the test results.  Suppressing the output of
+    # passing cases makes it easier to find the output of failing ones.
+    unittest.main(testRunner = unittest.TextTestRunner(buffer = True))
index 2a08f64c020473df87ddbc711989929ad26557f1..484de09142596c2cd6d8465368a782dfd4cf0a2e 100644 (file)
@@ -145,7 +145,7 @@ def TestBezierDerivative():
     print("\tTest EvalDerivative at keyframes")
     spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
     assert(spline.EvalDerivative(1, Ts.Right) == 1.0)
-    assert(spline.EvalDerivative(1, Ts.Left) == 1.0)
+    assert(spline.EvalDerivative(1, Ts.Left) == 0.0)
     assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
     assert(spline.EvalDerivative(10, Ts.Left) == -1.0)
     spline.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
@@ -201,17 +201,17 @@ def TestLinearDerivative():
 
     print("\tTest EvalDerivative at keyframes")
     spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
-    assert(spline.EvalDerivative(1, Ts.Right) == 0.0)
+    assert(spline.EvalDerivative(1, Ts.Right) == 0.25)
     assert(spline.EvalDerivative(1, Ts.Left) == 0.0)
-    assert(spline.EvalDerivative(5, Ts.Right) == 0.0)
-    assert(spline.EvalDerivative(5, Ts.Left) == 0.0)
+    assert(spline.EvalDerivative(5, Ts.Right) == -0.2)
+    assert(spline.EvalDerivative(5, Ts.Left) == 0.25)
     assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
-    assert(spline.EvalDerivative(10, Ts.Left) == 0.0)
+    assert(spline.EvalDerivative(10, Ts.Left) == -0.2)
     spline.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
     assert(spline.EvalDerivative(1, Ts.Right) == 0.25)
     assert(spline.EvalDerivative(1, Ts.Left) == 0.25)
-    assert(spline.EvalDerivative(5, Ts.Right) == 0.25)
-    assert(spline.EvalDerivative(5, Ts.Left) == -0.2)
+    assert(spline.EvalDerivative(5, Ts.Right) == -0.2)
+    assert(spline.EvalDerivative(5, Ts.Left) == 0.25)
     assert(spline.EvalDerivative(10, Ts.Right) == -0.2)
     assert(spline.EvalDerivative(10, Ts.Left) == -0.2)
 
index 3236345aabd5c35a39ffbac0df3aae357f110b17..3e21fa709b19c020b4fa7ad66c53965cf8a62647 100644 (file)
@@ -1398,7 +1398,7 @@ main(int argc, char **argv)
     TF_AXIOM( val.Eval(5).Get<float>() == float(10) );
     TF_AXIOM( val.Eval(5.5).Get<float>() == float(11) );
     TF_AXIOM( val.EvalDerivative(0, TsLeft).Get<float>() == float(0) );
-    TF_AXIOM( val.EvalDerivative(0, TsRight).Get<float>() == float(0) );
+    TF_AXIOM( val.EvalDerivative(0, TsRight).Get<float>() == float(2) );
     TF_AXIOM( val.EvalDerivative(5, TsLeft).Get<float>() == float(2) );
     TF_AXIOM( val.EvalDerivative(5, TsRight).Get<float>() == float(2) );
     TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get<float>() == float(2) );
@@ -1485,11 +1485,11 @@ main(int argc, char **argv)
     TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsLeft)), 
                     VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
     TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsRight)), 
-                    VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+                    VtValue(GfVec2d(0.1, 0.1)), vec2dEps));
     TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsLeft)), 
-                    VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+                    VtValue(GfVec2d(0.1, 0.1)), vec2dEps));
     TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsRight)), 
-                    VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+                    VtValue(GfVec2d(0.1, 0.1)), vec2dEps));
 
     val.Clear();
     val.SetKeyFrame( TsKeyFrame( 0, 0.0, TsKnotHeld ) );
@@ -1511,10 +1511,10 @@ main(int argc, char **argv)
     TF_AXIOM(_IsClose<double>(val.Eval(0,  TsLeft).Get<double>(),  0.0));
     TF_AXIOM(_IsClose<double>(val.Eval(10, TsRight).Get<double>(), 10.0));
     TF_AXIOM(_IsClose<double>(val.Eval(10, TsLeft).Get<double>(),  10.0));
-    TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsRight).Get<double>(), 0.0));
+    TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsRight).Get<double>(), 1.0));
     TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsLeft).Get<double>(),  0.0));
     TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsRight).Get<double>(), 0.0));
-    TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft).Get<double>(),  0.0));
+    TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft).Get<double>(),  1.0));
     printf("\tpassed\n");
 
     val.Clear();
@@ -1524,10 +1524,10 @@ main(int argc, char **argv)
     TF_AXIOM(_IsClose<double>(val.Eval(0,  TsLeft),  VtValue(0.0)));
     TF_AXIOM(_IsClose<double>(val.Eval(10, TsRight), VtValue(10.0)));
     TF_AXIOM(_IsClose<double>(val.Eval(10, TsLeft),  VtValue(10.0)));
-    TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsRight), VtValue(0.0)));
+    TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsRight), VtValue(1.0)));
     TF_AXIOM(_IsClose<double>(val.EvalDerivative(0,  TsLeft),  VtValue(0.0)));
     TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsRight), VtValue(0.0)));
-    TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft),  VtValue(0.0)));
+    TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft),  VtValue(1.0)));
     printf("\tpassed\n");
 
     // Test evaluation of cached segments.
index 268c613eb168a2b43e86f31ab725ec334dc852fb..04f760dc12304a186d38731b773b65d96a619fa6 100644 (file)
@@ -138,7 +138,9 @@ struct TsTraits {
     static const bool interpolatable = true;
 
     // True if the value can be extrapolated outside of the keyframe
-    // range. If this is false we always use TsExtrapolateHeld behaviour
+    // range. If this is false we always use TsExtrapolateHeld behaviour.
+    // This is true if a slope can be computed from the line between two knots
+    // of this type.
     static const bool extrapolatable = false;
 
     // True if the value type supports tangents.