From 1caff8a43ec9acf138ed34b570331e9341fb12be Mon Sep 17 00:00:00 2001 From: markummitchell Date: Sun, 16 Sep 2018 17:16:41 -0700 Subject: [PATCH] Restore higher order curve fits in Curve Fitting Window for #287 --- src/Fitting/FittingStatistics.h | 3 ++ src/Matrix/Matrix.cpp | 41 +++++++++--------- src/Matrix/Matrix.h | 8 ++-- src/Test/TestFitting.cpp | 77 +++++++++++++++++++++++++++++++-- src/Test/TestFitting.h | 16 ++++++- 5 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/Fitting/FittingStatistics.h b/src/Fitting/FittingStatistics.h index 70e5eaea..37c6a8a4 100644 --- a/src/Fitting/FittingStatistics.h +++ b/src/Fitting/FittingStatistics.h @@ -18,6 +18,9 @@ class Matrix; /// This class does the math to compute statistics for FittingWindow class FittingStatistics { + // For unit testing + friend class TestFitting; + public: /// Single constructor FittingStatistics (); diff --git a/src/Matrix/Matrix.cpp b/src/Matrix/Matrix.cpp index 32daad65..52192a78 100644 --- a/src/Matrix/Matrix.cpp +++ b/src/Matrix/Matrix.cpp @@ -97,7 +97,8 @@ int Matrix::fold2dIndexes (int row, int col) const double Matrix::get (int row, int col) const { - return m_vector [fold2dIndexes (row, col)]; + int foldedIndex = fold2dIndexes (row, col); + return m_vector [foldedIndex]; } void Matrix::initialize (int rows, @@ -133,12 +134,10 @@ Matrix Matrix::inverse (int significantDigits, } } - double epsilonThreshold = maxValue / qPow (10.0, significantDigits); - // Available algorithms are inverseCramersRule and inverseGaussianElimination matrixConsistent = MATRIX_CONSISTENT; - return inverseGaussianElimination (matrixConsistent, - epsilonThreshold); + return inverseGaussianElimination (significantDigits, + matrixConsistent); } Matrix Matrix::inverseCramersRule (MatrixConsistent & matrixConsistent, @@ -194,8 +193,8 @@ Matrix Matrix::inverseCramersRule (MatrixConsistent & matrixConsistent, return inv; } -Matrix Matrix::inverseGaussianElimination (MatrixConsistent &matrixConsistent, - double epsilonThreshold) const +Matrix Matrix::inverseGaussianElimination (int significantDigits, + MatrixConsistent &matrixConsistent) const { // From https://en.wikipedia.org/wiki/Gaussian_elimination @@ -254,7 +253,7 @@ Matrix Matrix::inverseGaussianElimination (MatrixConsistent &matrixConsistent, } // Normalize the 'from' row with first nonzero term set to 1 - working.normalizeRow (rowFrom, colFirstWithNonZero, matrixConsistent, epsilonThreshold); + working.normalizeRow (rowFrom, colFirstWithNonZero, significantDigits, matrixConsistent); if (matrixConsistent == MATRIX_INCONSISTENT) { return inv; } @@ -266,10 +265,6 @@ Matrix Matrix::inverseGaussianElimination (MatrixConsistent &matrixConsistent, // We need to merge rowFrom and rowTo into rowTo double denominator = working.get (rowFrom, colFirstWithNonZero); - if (valueFailsEpsilonTest (denominator, epsilonThreshold)) { - matrixConsistent = MATRIX_INCONSISTENT; - return inv; - } double factor = -1.0 * working.get (rowTo, colFirstWithNonZero) / denominator; working.addRowToAnotherWithScaling (rowFrom, rowTo, factor); } @@ -282,7 +277,7 @@ Matrix Matrix::inverseGaussianElimination (MatrixConsistent &matrixConsistent, // Normalize the 'from' row with diagonal term set to 1. The first term should be like 0.9999 or 1.0001 but we want exactly one MatrixConsistent matrixConsistent; - working.normalizeRow (rowFrom, colFirstWithNonZero, matrixConsistent, epsilonThreshold); + working.normalizeRow (rowFrom, colFirstWithNonZero, significantDigits, matrixConsistent); if (matrixConsistent == MATRIX_INCONSISTENT) { return inv; } @@ -294,10 +289,6 @@ Matrix Matrix::inverseGaussianElimination (MatrixConsistent &matrixConsistent, // We need to merge rowFro and rowTo into rowTo double denominator = working.get (rowFrom, colFirstWithNonZero); - if (valueFailsEpsilonTest (denominator, epsilonThreshold)) { - matrixConsistent = MATRIX_INCONSISTENT; - return inv; - } double factor = -1.0 * working.get (rowTo, colFirstWithNonZero) / denominator; working.addRowToAnotherWithScaling (rowFrom, rowTo, factor); } @@ -360,11 +351,21 @@ Matrix Matrix::minorReduced (int rowOmit, int colOmit) const void Matrix::normalizeRow (int rowToNormalize, int colToNormalize, - MatrixConsistent &matrixConsistent, - double epsilonThreshold) + int significantDigits, + MatrixConsistent &matrixConsistent) { double denominator = get (rowToNormalize, colToNormalize); + // Epsilon is computed from smallest value in row + double smallestAbsValue = 0; + for (int col = 0; col < cols (); col++) { + double absValue = qAbs (get (rowToNormalize, 0)); + if (col == 0 || absValue < smallestAbsValue) { + smallestAbsValue = absValue; + } + } + double epsilonThreshold = smallestAbsValue / qPow (10.0, significantDigits); + if (valueFailsEpsilonTest (denominator, epsilonThreshold)) { @@ -481,5 +482,5 @@ Matrix Matrix::transpose () const bool Matrix::valueFailsEpsilonTest (double value, double epsilonThreshold) const { - return (qAbs (value) < qAbs (epsilonThreshold)); + return qAbs (value) < qAbs (epsilonThreshold); } diff --git a/src/Matrix/Matrix.h b/src/Matrix/Matrix.h index 52cb7cb8..2394f120 100644 --- a/src/Matrix/Matrix.h +++ b/src/Matrix/Matrix.h @@ -80,13 +80,13 @@ class Matrix int cols); Matrix inverseCramersRule (MatrixConsistent &matrixConsistent, double epsilonThreshold) const; - Matrix inverseGaussianElimination (MatrixConsistent &matrixConsistent, - double epsilonThreshold) const; + Matrix inverseGaussianElimination (int significantDigits, + MatrixConsistent &matrixConsistent) const; unsigned int leadingZeros (int row) const; // Number of leading zeros in the specified zero void normalizeRow (int rowToNormalize, int colToNormalize, - MatrixConsistent &matrixConsistent, - double epsilonThreshold); + int significantDigits, + MatrixConsistent &matrixConsistent); void switchRows (int row1, int row2); diff --git a/src/Test/TestFitting.cpp b/src/Test/TestFitting.cpp index 367d732c..8950dd82 100644 --- a/src/Test/TestFitting.cpp +++ b/src/Test/TestFitting.cpp @@ -10,7 +10,8 @@ QTEST_MAIN (TestFitting) using namespace std; -const int SIGNIFICANT_DIGITS = 7; +const int NOMINAL_ORDER = 6; +const int NOMINAL_SIGNIFICANT_DIGITS = 7; TestFitting::TestFitting(QObject *parent) : QObject(parent) @@ -56,7 +57,7 @@ bool TestFitting::generalFunctionTest (int order, mse, rms, rSquared, - SIGNIFICANT_DIGITS); + NOMINAL_SIGNIFICANT_DIGITS); bool success = true; @@ -129,7 +130,7 @@ bool TestFitting::generalNonFunctionTest () const mse, rms, rSquared, - SIGNIFICANT_DIGITS); + NOMINAL_SIGNIFICANT_DIGITS); bool success = true; @@ -180,6 +181,36 @@ void TestFitting::initTestCase () w.show (); } +int TestFitting::orderReducedVersusOrderAndSignificantDigits (int order, + int significantDigits) const +{ + FittingPointsConvenient points; + FittingCurveCoefficients coefficients (MAX_POLYNOMIAL_ORDER + 1); + + // Hyperbola points + FittingStatistics fittingStatistics; + for (double x = 1; x <= 10; x += 1) { + double y = 100.0 / x; + points.append (QPointF (x, y)); + } + + fittingStatistics.calculateCurveFit (order, + points, + coefficients, + significantDigits); + + // Find first nonzero coefficient. Two cases for 0th order are y<>0 (not all coefficients are zero) + // and y=0 (all coefficients are zero). In all other cases the order is the highest nonzero coefficient + int orderReduced; + for (orderReduced = MAX_POLYNOMIAL_ORDER; orderReduced > 0; orderReduced--) { + if (coefficients [orderReduced] != 0) { + return orderReduced; + } + } + + return orderReduced; +} + void TestFitting::testFunctionExactFit01 () { QVERIFY (generalFunctionTest (0, 1)); @@ -244,3 +275,43 @@ void TestFitting::testNonFunction () { QVERIFY (generalNonFunctionTest ()); } + +void TestFitting::testOrderReduced3 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (3, NOMINAL_SIGNIFICANT_DIGITS) == 3); +} + +void TestFitting::testOrderReduced4 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (4, NOMINAL_SIGNIFICANT_DIGITS) == 4); +} + +void TestFitting::testOrderReduced5 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (5, NOMINAL_SIGNIFICANT_DIGITS) == 5); +} + +void TestFitting::testOrderReduced6 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (6, NOMINAL_SIGNIFICANT_DIGITS) == 6); +} + +void TestFitting::testSignificantDigits3 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (NOMINAL_ORDER, 3) == NOMINAL_ORDER); +} + +void TestFitting::testSignificantDigits4 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (NOMINAL_ORDER, 4) == NOMINAL_ORDER); +} + +void TestFitting::testSignificantDigits5 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (NOMINAL_ORDER, 5) == NOMINAL_ORDER); +} + +void TestFitting::testSignificantDigits6 () +{ + QVERIFY (orderReducedVersusOrderAndSignificantDigits (NOMINAL_ORDER, 6) == NOMINAL_ORDER); +} diff --git a/src/Test/TestFitting.h b/src/Test/TestFitting.h index eef637e2..325c59d6 100644 --- a/src/Test/TestFitting.h +++ b/src/Test/TestFitting.h @@ -32,14 +32,28 @@ private slots: void testFunctionUnderfit13 (); void testFunctionUnderfit24 (); void testFunctionUnderfit35 (); - + // Test case where non-function data was entered even though points should be functional void testNonFunction (); + // Test the automatic order reduction as a function of the initial order + void testOrderReduced3 (); + void testOrderReduced4 (); + void testOrderReduced5 (); + void testOrderReduced6 (); + + // Test the automatic order reduction as a function of the significant digits + void testSignificantDigits3 (); + void testSignificantDigits4 (); + void testSignificantDigits5 (); + void testSignificantDigits6 (); + private: bool generalFunctionTest (int order, int numPoints) const; bool generalNonFunctionTest () const; + int orderReducedVersusOrderAndSignificantDigits (int order, + int significantDigits) const; }; #endif // TEST_FITTING_H