diff --git a/demos/helix-example/helix-example/Documentation.pdf b/demos/helix-example/helix-example/Documentation.pdf new file mode 100644 index 000000000..fed5a7ec0 Binary files /dev/null and b/demos/helix-example/helix-example/Documentation.pdf differ diff --git a/demos/helix-example/helix-example/Graph_from_file.ipynb b/demos/helix-example/helix-example/Graph_from_file.ipynb new file mode 100644 index 000000000..bc9b01b60 --- /dev/null +++ b/demos/helix-example/helix-example/Graph_from_file.ipynb @@ -0,0 +1 @@ +{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3 (ipykernel)","language":"python"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":5,"nbformat":4,"cells":[{"id":"3a9756e8-5eb8-4e31-a2de-1c08b0b10229","cell_type":"code","source":"import matplotlib.pyplot as plt\n\nfig = plt.figure()\nax = plt.axes(projection=\"3d\")\n\nx, y, z = [], [], []\nwith open(\"output.txt\") as file:\n lines = file.readlines()\nfor line in lines:\n if line.strip() == \"end\":\n ax.plot3D(x, y, z)\n x.clear()\n y.clear()\n z.clear()\n else:\n a, b, c = [float(i) for i in line.split()]\n x.append(a)\n y.append(b)\n z.append(c)\n\nax.set_box_aspect([1,1,1])\nplt.show()","metadata":{"trusted":true},"outputs":[{"output_type":"display_data","data":{"text/plain":"
","image/png":"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"},"metadata":{}}],"execution_count":3},{"id":"50b3a6d1-98c0-4ddc-b216-d388a21502e3","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]} \ No newline at end of file diff --git a/demos/helix-example/helix-example/Helix.ipynb b/demos/helix-example/helix-example/Helix.ipynb new file mode 100644 index 000000000..3945d1480 --- /dev/null +++ b/demos/helix-example/helix-example/Helix.ipynb @@ -0,0 +1 @@ +{"metadata":{"kernelspec":{"display_name":"C++17-Clad-v1.7","language":"C++17","name":"xcpp17-clad"},"language_info":{"codemirror_mode":"text/x-c++src","file_extension":".cpp","mimetype":"text/x-c++src","name":"c++","version":"17"}},"nbformat_minor":5,"nbformat":4,"cells":[{"id":"270d637f-81da-472b-a904-783c072d9fa2","cell_type":"markdown","source":"# Helix fitter tutorial - CLAD & Jupyter Notebook ","metadata":{}},{"id":"0447116e-4a4f-412a-9de3-2a8601e11f43","cell_type":"markdown","source":"## Introduction\n\nParticle tracking is an important part of the processing and analysis of data received from particle detectors, such as the Compact Muon Solenoid (CMS). Tracking is the step that determines the momentum of charged particles escaping from the collision point. It identifies individual particles by reconstructing their trajectories from points where charged particle “hits” were measured by the detector and interpreting them.$^{[2]}$ Due to the Lorentz force, charged particles move in a helical motion when affected by the magnetic field (neglecting other effects due to material interactions, etc). This means we can figure out a specific particle trajectory through the detector by fitting a helix function to data points in such a way that the distance from the data points and the helix would be minimized. In mathematical terms, we need to find optimal helix parameters by minimizing a loss function composed of the sum of least squared distances, thus giving the best estimation of these parameters. For this purpose we can use Clad to efficiently minimize the loss function. ","metadata":{}},{"id":"7749b5ea-505f-4d8e-b97c-00efc5ab1c6e","cell_type":"markdown","source":"## Levenberg-Marquardt algorithm\n\nTo solve this nonlinear least squares problem we will be using the Levenberg-Marquardt algorithm.$^{(1)}$ The Levenberg-Marquardt algorithm combines two optimization methods: gradient descent and Gauss-Newton. Its behaviour changes based on how close the current coefficients are to the optimal value. When the coefficients are far from optimal, it uses gradient descent, which takes steps in the direction of steepest descent. When the coefficients are close to optimal, it uses Gauss-Newton, which assumes the problem is locally quadratic and finds the minimum of this quadratic. This combination allows the algorithm to provide a more reliable and efficient optimization than the other two methods mentioned.$^{[3]}$ ","metadata":{}},{"id":"8510d725-4b06-4b9d-be3b-8d378e6e5704","cell_type":"markdown","source":"## Setup","metadata":{}},{"id":"756e353f-f5bb-43b2-b310-578e1d1a3a9f","cell_type":"code","source":"#include \n#include \n#include \n#include \n#include \n#include \n#include \"clad/Differentiator/Differentiator.h\"\n//---------------------\n#include \nstd::ofstream outfile(\"output.txt\");\nauto MY_PI = 3.14159265359;","metadata":{"trusted":true},"outputs":[],"execution_count":1},{"id":"643072ca-02f4-489f-aebb-c25741041b8b","cell_type":"code","source":"namespace clad::custom_derivatives::std {\ntemplate \nCUDA_HOST_DEVICE ValueAndPushforward atan2_pushforward(T y, T x, T d_y,\n T d_x) {\n return {::std::atan2(y, x),\n -(y / ((x * x) + (y * y))) * d_x + x / ((x * x) + (y * y)) * d_y};\n}\n\ntemplate \nCUDA_HOST_DEVICE void atan2_pullback(T y, T x, U d_z, T* d_y, T* d_x) {\n *d_y += x / ((x * x) + (y * y)) * d_z;\n\n *d_x += -(y / ((x * x) + (y * y))) * d_z;\n}\n\ntemplate \nCUDA_HOST_DEVICE ValueAndPushforward acos_pushforward(T x, T d_x) {\n return {::std::acos(x), ((-1) / (::std::sqrt(1 - x * x))) * d_x};\n}\n}\nnamespace clad::custom_derivatives\n{\n using std::atan2_pushforward;\n using std::atan2_pullback;\n using std::acos_pushforward;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":2},{"id":"5f50b5c9-1e0a-4a67-9dd5-4074d5bd5ccd","cell_type":"code","source":"double DistanceSquare(double x1, double y1, double z1, double x2, double y2, double z2)\n{\n return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":3},{"id":"ba5a4f3f-310d-496c-8d27-86e40564847d","cell_type":"code","source":"double Distance(double x1, double y1, double z1, double x2, double y2, double z2)\n{\n return std::sqrt(DistanceSquare(x1, y1, z1, x2, y2, z2));\n}","metadata":{"trusted":true},"outputs":[],"execution_count":4},{"id":"192c2378-4827-4837-826d-6269e1bbe262","cell_type":"code","source":"double DistanceSquareA(double v[3], double x2, double y2, double z2)\n{\n return DistanceSquare(v[0], v[1], v[2], x2, y2, z2);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":5},{"id":"c9c455a3-0487-419e-a823-b837b4003cdc","cell_type":"code","source":"double DistanceA(double v[3], double x2, double y2, double z2)\n{\n return Distance(v[0], v[1], v[2], x2, y2, z2);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":6},{"id":"a10a4720-2b20-4225-a5fb-c6c1da5396c7","cell_type":"code","source":"// All the matrices are written as 1D arrays!\nvoid MatrixMultiply(double *a, double *b, int arows, int acols, int bcols, double *output)\n{\n for (int i = 0; i < arows; i++)\n {\n for (int j = 0; j < bcols; j++)\n {\n double sum = 0;\n for (int k = 0; k < acols; k++)\n sum = sum + a[i * acols + k] * b[k * bcols + j];\n output[i * bcols + j] = sum;\n }\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":7},{"id":"fd7cef3a-ff78-4439-8a38-a94318f355c5","cell_type":"code","source":"void Transpose(double *input, int rows, int cols, double *output)\n{\n for (int i = 0; i < rows; ++i)\n {\n for (int j = 0; j < cols; ++j)\n {\n int i_input = i * cols + j;\n\n int i_output = j * rows + i;\n\n output[i_output] = input[i_input];\n }\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":8},{"id":"7b7f0f1c-6b15-4535-b86f-48371c1659c9","cell_type":"code","source":"void DiagOfSquareM(double *input, int height, double *diag)\n{\n // Works for square matrices only\n for (int i = 0; i < height * height; i++)\n {\n diag[i] = 0;\n }\n for (int i = 0; i < height; i++)\n {\n diag[i * height + i] = input[i * height + i];\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":9},{"id":"f782c822-fde6-4396-b283-d07d4f43224a","cell_type":"code","source":"void ScalarMultiply(double *matrix, int rows, int cols, double number, double *output)\n{\n for (int i = 0; i < rows; i++)\n {\n for (int j = 0; j < cols; j++)\n {\n output[i * cols + j] = number * matrix[i * cols + j];\n }\n }\n}\n","metadata":{"trusted":true},"outputs":[],"execution_count":10},{"id":"6c5444d9-7e34-4691-8057-1e65379fee04","cell_type":"code","source":"void CopyMatrix(double *matrix, int size, double *output)\n{\n for (int i = 0; i < size; i++)\n {\n output[i] = matrix[i];\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":11},{"id":"1b67b3fa-6a9a-40e5-9ec0-2dced9bd1b4c","cell_type":"code","source":"void AddMatrices(double *a, double *b, int rows, int cols, double *output)\n{\n for (int i = 0; i < rows; i++)\n {\n for (int j = 0; j < cols; j++)\n {\n output[i * cols + j] = a[i * cols + j] + b[i * cols + j];\n }\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":12},{"id":"640e3f3f-a242-41f7-83e4-4fc99f2ee9c4","cell_type":"markdown","source":"## The implementation\n\nFirst we define our helix. We chose to do so in the Cartesian coordinates. This is done in **HelixPoint**, but we first define functions that allow our helix to be rotated around the x axis and then the y axis. The angle **alph** represents how much the helix is rotated counterclockwise around the x axis, when viewed from the positive direction of the axis. Likewise for **bet** and the y axis:","metadata":{}},{"id":"dcffd80a-03ab-4ed2-ae1d-cfe371b5ed0f","cell_type":"code","source":"void RotateAlph(double x, double y, double z, double alph, double output[3])\n{\n output[0] = x;\n output[1] = y * cos(alph) - z * sin(alph);\n output[2] = y * sin(alph) + z * cos(alph);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":13},{"id":"e4146b67-14d3-45af-bf9e-0710e55aeb3a","cell_type":"code","source":"void RotateBet(double x, double y, double z, double bet, double output[3])\n{\n output[0] = x * cos(bet) + z * sin(bet);\n output[1] = y;\n output[2] = -x * sin(bet) + z * cos(bet);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":14},{"id":"58726266-6fde-4707-a2d7-dd4212d7d8a8","cell_type":"code","source":"void Rotate(double x, double y, double z, double alph, double bet, double output[3])\n{\n double point[3];\n RotateAlph(x, y, z, alph, point);\n RotateBet(point[0], point[1], point[2], bet, output);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":15},{"id":"9c844ca6-d4ae-4d2d-be11-6b8750b81c01","cell_type":"code","source":"void UnRotate(double x, double y, double z, double alph, double bet, double output[3])\n{\n double point[3];\n RotateBet(x, y, z, -bet, point);\n RotateAlph(point[0], point[1], point[2], -alph, output);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":16},{"id":"2bc8bcc6-f292-44ce-84a1-917e1b0cc0a3","cell_type":"code","source":"void HelixPoint(double a, double b, double c, double d, double alph, double bet, double t, double output[3])\n{\n /*Describe a point on a helix in the Cartesian coordinate system.*/\n double x = a * (c + std::cos(t));\n double y = a * (d + std::sin(t));\n double z = a * b * t;\n output[0] = x;\n output[1] = y;\n output[2] = z;\n Rotate(x, y, z, alph, bet, output);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":17},{"id":"00a11ec6-4890-4f74-9b93-18921bae7acc","cell_type":"markdown","source":"\nThe function **HelixPoint** describes a circular helix of radius **a** and a slope ⁠**1/b**⁠ (in the net direction of travel, which is the z direction in the code above). The helix can not only be rotated as described above, but also be shifted from the starting coordinates by distances **c** and **d** in the x and y directions, respectively.\n
\n
\n
\nNext, for demonstration purposes, we generate a set of points using **GenerateFlawedPoints**. We pick some set of helix parameters and scan **t** in some range to generate an array containing **nr_of_points** data points. The data points are generated by calculating a point on the helix with given parameters at the time **t** using **HelixPoint** and also adding some randomness (representing measurement error), so that the helix points do not correspond to a perfect helix. We will use these points to then determine estimated helix parameters. \n
","metadata":{}},{"id":"0b5d9f88-d796-4dc5-89d0-c6618628130e","cell_type":"code","source":"void GenerateFlawedPoints(int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *points)\n{\n /*Generate points on a helix with given params but add noise. */\n auto seed = time(nullptr);\n std::mt19937_64 rng(seed);\n std::uniform_real_distribution uniform(0, 1);\n double output[3];\n double t = 0;\n for (int i = 0; i < nr_of_points; i++)\n {\n t += 0.1;\n HelixPoint(a, b, c, d, alph, bet, t, output);\n points[i * 3] = output[0] + uniform(rng);\n points[i * 3 + 1] = output[1] + uniform(rng);\n points[i * 3 + 2] = output[2] + uniform(rng);\n }\n}\n","metadata":{"trusted":true},"outputs":[],"execution_count":18},{"id":"57160b5b-2e1c-4067-a47a-cb6e4ba36391","cell_type":"markdown","source":"\nLet’s now implement the Levenberg-Marquardt algorithm. The equation that dictates how to update the parameters in the Levenberg-Marquardt algorithm is this:\n(JTWJ + λI) hlm = JT W (y - ŷ)\n\t\nIn this equation ŷ represents a vector containing the differences between the data points and the closest point of each to the helix (given a set of helix parameters: **a**, **b**, **c**, **d**, **alph**, **bet**). y is a vector of expected values: in our case, ideally, we expect the difference between the data point and the helix to be 0. \nThe jacobian is a matrix of partial derivatives of ŷ with respect to the parameters we are searching for. So in our case it is a matrix of size **6** x **nr_of_points**, where **nr_of_points** is the number of data points and **6** is the number of parameters we have: **a**, **b**, **c**, **d**, **alph**, **bet**. λ is the damping coefficient, which determines the behaviour of the algorithm. It is not static: if the sum of the distances squared is smaller than in the previous iteration, we make it smaller, otherwise, we increase its size.\n\n\nTo find the closest distance of a point to a helix, we do some scaling so that our helix is now defined by (cos𝑡,sin𝑡,ℎ𝑡). For a given point 𝑃(i,j,k), let 𝑄 be the closest point on the helix. The line segment connecting 𝑃 and 𝑄 must be perpendicular to the helix's tangent line at 𝑄, which is (−sin𝑡,cos𝑡,ℎ). Knowing that the dot product of two perpendicular vectors is 0 leads to:\n−(cos𝑡−i)sin𝑡+(sin𝑡−j)cos𝑡+(ℎ𝑡−k)ℎ=0\nThis simplifies to 𝐴sin(𝑡+𝐵)+𝐶𝑡+𝐷=0 for some constants 𝐴,𝐵,𝐶,𝐷. $^{[4]}$ \nTo find the solution, we perform a binary search (**SolveSinPlusLin**). \n\n**HelixClosestTime** is the function taking all of this into account and returning the parameter **t**, during which the point is closest to the helix.\n","metadata":{}},{"id":"94b92283-28b1-436b-81ee-8e7e7dcda57e","cell_type":"code","source":"double EvaluateSinPlusLin(double A, double B, double C, double D, double x)\n{\n /*When this equation is equal to zero, the distance between the point and the helix is the shortest.*/\n return A * std::sin(x + B) + C * x + D;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":19},{"id":"6bf86804-c192-425a-9621-099cca3f08fb","cell_type":"code","source":"// A sin (x + B) + C x + D = 0\ndouble SolveSinPlusLin(double A, double B, double C, double D, double mi, double ma)\n{\n /*Binary search to determine x, with which EvaluateSinPlusLin equation equals zero.*/\n for (int i = 0; i < 100; i++)\n {\n double mid = (mi + ma) / 2;\n double vmi = EvaluateSinPlusLin(A, B, C, D, mi);\n double vmid = EvaluateSinPlusLin(A, B, C, D, mid);\n double vma = EvaluateSinPlusLin(A, B, C, D, ma);\n\n if (vmi < 0 and 0 < vmid)\n {\n ma = mid;\n }\n else if (vmid < 0 and 0 < vma)\n {\n mi = mid;\n }\n else if (vmid < 0 and 0 < vmi)\n {\n ma = mid;\n }\n else if (vma < 0 and 0 < vmid)\n {\n mi = mid;\n }\n else\n {\n break;\n mi = mid;\n }\n }\n\n double x = (mi + ma) / 2;\n return x;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":20},{"id":"cf0065af-cd01-4986-8a4b-7c8549ffeb52","cell_type":"code","source":"double NextValPiK(double offs, double x)\n{\n\n if (x < 0)\n {\n double v = -NextValPiK(-offs, -x) + 2 * MY_PI;\n return v > x ? v : v + 2 * MY_PI;\n }\n\n double kie = std::floor(x / 2 / MY_PI);\n\n for (int i = -2; i <= 2; i++)\n {\n double v = (kie + i) * 2 * MY_PI + offs;\n\n if (v > x)\n {\n return v;\n }\n }\n\n return 1000000000;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":21},{"id":"5c153969-15d4-4889-977c-74c0086c8825","cell_type":"code","source":"// A cos(x + B) + C = 0\ndouble NextSinPlusInflection(double A, double B, double C, double x)\n{\n /* Identifies the next inflection point of the sine curve.*/\n // cos(x + B) = -C / A\n if (-C / A >= -1 && -C / A <= 1)\n {\n double inv = std::acos(-C / A);\n return std::min(NextValPiK(inv - B, x), NextValPiK(-inv - B, x));\n }\n else\n {\n return 1000000000;\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":22},{"id":"e23679a2-d99a-4084-af06-0a96a542c3e4","cell_type":"code","source":"double HelixClosestTime(double a, double b, double c, double d, double alph, double bet, double x, double y, double z)\n{\n /*Calculate t, during which a helix with given params is the closest to a given point.*/\n double point[3];\n UnRotate(x, y, z, alph, bet, point);\n point[0] /= a;\n point[1] /= a;\n point[2] /= a;\n point[0] -= c;\n point[1] -= d;\n double A = std::sqrt(point[0] * point[0] + point[1] * point[1]);\n double B = std::atan2(-point[1], point[0]);\n double C = b * b;\n double D = -point[2] * b;\n\n double mi = point[2] / b - MY_PI;\n double ma = point[2] / b + MY_PI;\n double t1 = SolveSinPlusLin(A, B, C, D, mi, ma);\n\n double ans = t1;\n HelixPoint(a, b, c, d, alph, bet, ans, point);\n double dist = DistanceSquareA(point, x, y, z);\n\n for (double t = mi; t < ma; t = t)\n {\n double ttt = NextSinPlusInflection(A, B, C, t);\n\n if (ttt == t)\n {\n break;\n }\n\n double cur = SolveSinPlusLin(A, B, C, D, t, ttt);\n t = ttt;\n HelixPoint(a, b, c, d, alph, bet, cur, point);\n double dist2 = DistanceSquareA(point, x, y, z);\n\n if (dist2 < dist)\n {\n dist = dist2;\n ans = cur;\n }\n }\n\n return ans;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":23},{"id":"b853c722-48c4-46fa-8209-59c5a9ebd952","cell_type":"markdown","source":"\nFor the Levenberg-Marquardt algorithm itself (main implementation is in **LevenbergMarquardt**), we start out by guessing the initial parameters, except **b**. Currently, the code doesn’t support determination of **b** and one right now needs to set **b** to a number close to, but not equal to zero (like 0.1) to find the other parameters.$^{(2)}$ However, that is fine for our purposes, since knowing the magnetic field in an experiment constrains **b** for a given momentum (eg, the ratio of a and b is known). It is also expected for alph and bet to be between -π and π. \n\nNext, we calculate the jacobian using **clad::gradient**.$^{(3)}$ We determine its transpose, find the closest distances of the points to the helix, do some matrix multiplication and we are left with an equation to solve for **h**. We solve it using the Gaussian elimination method (**swap_row**, **ForwardElim** & **BackSub**). We update the parameters with **h**, recalculate the sum of all squared distances and change **lambda** accordingly.$^{(4)}$ The process is repeated for some number of iterations or until there is almost no change between the recalculated sum of all squared distances between several iterations.","metadata":{}},{"id":"83151944-9950-4e96-b4f8-e88a9b961f78","cell_type":"code","source":"void swap_row(double *matrix, int size, int i, int j)\n{\n // 6x6\n for (int k = 0; k < size; k++)\n {\n double temp = matrix[i * size + k];\n matrix[i * size + k] = matrix[j * size + k];\n matrix[j * size + k] = temp;\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":24},{"id":"a6a02f26-d7c0-4144-9999-98cb4fec6c9a","cell_type":"code","source":"void ForwardElim(double *input, int size, double *res, double *output)\n{\n for (int i = 0; i < size; ++i)\n {\n for (int j = 0; j < size; ++j)\n {\n output[i * size + j] = input[i * size + j];\n }\n }\n for (int i = 0; i < size; i++)\n {\n int i_max = i;\n double v_max = output[i_max * size + i];\n\n for (int j = i + 1; j < size; j++)\n if (std::abs(output[j * size + i]) > std::abs(v_max) && output[j * size + i] != 0)\n v_max = output[j * size + i], i_max = j;\n if (i_max != i)\n {\n swap_row(output, size, i, i_max);\n double temp = res[i];\n res[i] = res[i_max];\n res[i_max] = temp;\n }\n\n if (output[i * size + i] == 0.0)\n {\n std::cerr << \"Mathematical Error!\";\n std::cerr << \"Input that caused the error is:\";\n for (int i = 0; i < size; i++)\n {\n for (int j = 0; j < size; j++)\n {\n std::cerr << output[i * size + j] << \" \";\n }\n std::cerr << std::endl;\n }\n }\n for (int j = i + 1; j < size; j++)\n {\n double ratio = output[j * size + i] / output[i * size + i];\n\n for (int k = 0; k < size; k++)\n {\n output[j * size + k] = output[j * size + k] - ratio * output[i * size + k];\n if (std::abs(output[j * size + k]) <= 1e-15)\n {\n output[j * size + k] = 0;\n }\n }\n res[j] = res[j] - ratio * res[i];\n if (std::abs(res[j]) <= 1e-15)\n {\n res[j] = 0;\n }\n }\n }\n\n // std::cerr << \"Forward elimination results:\" << std::endl;\n // std::cerr << \"Left side:\" << std::endl;\n // for (int j = 0; j < size; j++)\n // {\n // for (int k = 0; k < size; k++)\n // {\n // std::cerr << output[j * size + k] << \" \";\n // }\n // std::cerr << std::endl;\n // }\n // std::cerr << \"Right side:\" << std::endl;\n // for (int k = 0; k < size; k++)\n // {\n // std::cerr << res[k] << \" \";\n // }\n // std::cerr << std::endl;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":25},{"id":"55fec92b-49d4-4973-a9dd-de657628ccc5","cell_type":"code","source":"void BackSub(double *input, int size, double *right_side, double *results)\n{\n /*Back substitution and the result of Gaussian elimination*/\n for (int i = (size - 1); i > -1; i--)\n {\n results[i] = right_side[i];\n for (int j = (size - 1); j > i; j--)\n {\n results[i] -= input[i * size + j] * results[j];\n }\n results[i] /= input[i * size + i];\n if (std::abs(results[i]) <= 1e-15)\n {\n results[i] = 0;\n }\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":26},{"id":"774a5a5e-17a9-4048-b116-01ed8ea86fc4","cell_type":"code","source":"void CheckSolution(double *input, int size, double *right_side, double *results)\n{\n for (int i = 0; i < size; i++)\n {\n double sum = 0;\n for (int j = 0; j < size; j++)\n {\n sum += input[i * size + j] * results[j];\n }\n if (std::abs(sum - right_side[i]) >= 1e-5)\n {\n std::cerr << \"Wrong solution \" << sum << \" \" << right_side[i] << std::endl;\n std::abort();\n }\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":27},{"id":"d17256ab-669a-430c-a2d6-9252f0f3bd2d","cell_type":"code","source":"double DistanceToPoint(double a, double b, double c, double d, double alph, double bet, double x, double y, double z)\n{\n /*Calculate the distance to a single point. */\n double t = HelixClosestTime(a, b, c, d, alph, bet, x, y, z);\n double output[3];\n HelixPoint(a, b, c, d, alph, bet, t, output);\n double dist = DistanceA(output, x, y, z);\n dist += 0.001 * ((a * a) + (b * b) + (c * c) + (d * d) + (alph * alph) + (bet * bet));\n\n return dist;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":28},{"id":"3ecd9d50-f097-4d45-a92b-6a53da7e3464","cell_type":"code","source":"void DistancesToAllPoints(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *dist)\n{\n /*Calculate the distances to all points. */\n int n = 0;\n for (int i = 0; i < nr_of_points; i++)\n {\n double x = points[i * 3];\n double y = points[i * 3 + 1];\n double z = points[i * 3 + 2];\n dist[n] = DistanceToPoint(a, b, c, d, alph, bet, x, y, z);\n n++;\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":29},{"id":"abf1fdd6-d4a5-4c18-800d-854a704c0fc3","cell_type":"code","source":"double SquareErr(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet)\n{\n /*Calculate the residual sum of squares. */\n double dist;\n double square_err = 0;\n for (int i = 0; i < nr_of_points; i++)\n {\n double x = points[i * 3];\n double y = points[i * 3 + 1];\n double z = points[i * 3 + 2];\n dist = DistanceToPoint(a, b, c, d, alph, bet, x, y, z);\n square_err += (dist * dist);\n }\n return square_err;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":30},{"id":"5d559ae3-4b0e-431d-848a-d293af8f46d1","cell_type":"code","source":"void Points(int nr_of_points, double a, double b, double c, double d, double alph, double bet)\n{\n /*Generate and print out points on a helix with given params. */\n double t = 0;\n for (int i = 0; i < nr_of_points; i++)\n {\n t += 0.1;\n double output[3];\n HelixPoint(a, b, c, d, alph, bet, t, output);\n double x = output[0], y = output[1], z = output[2];\n outfile << x << \" \" << y << \" \" << z << \"\\n\";\n }\n outfile << \"end\\n\";\n}","metadata":{"trusted":true},"outputs":[],"execution_count":31},{"id":"3f6b8cb6-f309-4832-b25f-5c80328d5c71","cell_type":"code","source":"void Jacobian(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *Jacobian)\n{\n /*Construct the nr_of_points x 6 Jacobian.*/\n auto dist_grad = clad::gradient(DistanceToPoint, \"a, b, c, d, alph, bet\");\n for (int i = 0; i < nr_of_points; i++)\n {\n double x = points[i * 3];\n double y = points[i * 3 + 1];\n double z = points[i * 3 + 2];\n double output[3];\n double da = 0, db = 0, dc = 0, dd = 0, dalph = 0, dbet = 0;\n dist_grad.execute(a, b, c, d, alph, bet, x, y, z, &da, &db, &dc, &dd, &dalph, &dbet);\n Jacobian[i * 6] = da;\n Jacobian[i * 6 + 1] = db;\n Jacobian[i * 6 + 2] = dc;\n Jacobian[i * 6 + 3] = dd;\n Jacobian[i * 6 + 4] = dalph;\n Jacobian[i * 6 + 5] = dbet;\n }\n}","metadata":{"trusted":true},"outputs":[],"execution_count":32},{"id":"d525fe6b-7e43-40b1-aa96-86067e9975ea","cell_type":"code","source":"double Lambda(double *points, int nr_of_points, double &a, double &b, double &c, double &d, double &alph, double &bet, double lambda, double &square_err, double *results)\n{\n /*Calculate the damping coefficient lambda for the next iteration of the LevenbergMarquardt function.*/\n double new_lambda;\n double new_square_err = SquareErr(points, nr_of_points, a + results[0], b + results[1], c + results[2], d + results[3], alph + results[4], bet + results[5]);\n // std::cerr << \"SQUARE ERR \" << new_square_err << std::endl;\n if ((new_square_err >= square_err) && (lambda < 1000))\n new_lambda = lambda * 10;\n else\n {\n // std::cerr << \"IMPROVEMENTS!\";\n a += results[0];\n b += results[1];\n c += results[2];\n d += results[3];\n alph += results[4];\n bet += results[5];\n new_lambda = lambda / 10;\n square_err = new_square_err;\n }\n return new_lambda;\n}","metadata":{"trusted":true},"outputs":[],"execution_count":33},{"id":"df6c8ed9-2cc0-4cba-ba09-7b6e0f43b8cc","cell_type":"code","source":"void LevenbergMarquardt(double *points, int nr_of_points, double &a, double &b, double &c, double &d, double &alph, double &bet)\n/*Use the Levenberg-Marquardt algorithm to fit a helix on a given set of points. Currently produces all of the parameters of the helix, except b.*/\n{\n double true_b = b;\n a = 6.2122, b = 0.1, c = 1.9835, d = 1.707055, alph = -3.60384, bet = 1.13255; // currently breaks if the parameters are exact as the ones used for (noise free) generated points\n int diff_params = 6;\n double lambda = 1;\n double lambda_change = 1;\n double square_err;\n double jacobian[nr_of_points * diff_params];\n double tjacobian[diff_params * nr_of_points];\n double tjj[diff_params * diff_params];\n double results[diff_params];\n double counter = 0;\n {\n double dist[nr_of_points];\n DistancesToAllPoints(points, nr_of_points, a, b, c, d, alph, bet, dist);\n square_err = 0;\n for (int i = 0; i < nr_of_points; i++)\n {\n square_err += (dist[i] * dist[i]);\n }\n }\n\n for (int i = 0; i < 200; i++)\n {\n\n Jacobian(points, nr_of_points, a, b, c, d, alph, bet, jacobian);\n\n Transpose(jacobian, nr_of_points, diff_params, tjacobian);\n\n MatrixMultiply(tjacobian, jacobian, diff_params, nr_of_points, diff_params, tjj);\n\n double diag[diff_params * diff_params];\n DiagOfSquareM(tjj, diff_params, diag);\n\n double identity[diff_params * diff_params];\n ScalarMultiply(diag, diff_params, diff_params, lambda, identity);\n double left_side[diff_params * diff_params];\n AddMatrices(tjj, identity, diff_params, diff_params, left_side);\n double dist[nr_of_points];\n DistancesToAllPoints(points, nr_of_points, a, b, c, d, alph, bet, dist);\n double right_side[diff_params * 1];\n MatrixMultiply(tjacobian, dist, diff_params, nr_of_points, 1, right_side);\n ScalarMultiply(right_side, 1, diff_params, -1, right_side);\n\n // left side is 6x6, right side is 6x1, so h is 6x1.\n double forward_elim[diff_params * diff_params];\n double unchanged_rs[diff_params];\n CopyMatrix(right_side, diff_params, unchanged_rs);\n ForwardElim(left_side, diff_params, right_side, forward_elim);\n BackSub(forward_elim, diff_params, right_side, results);\n CheckSolution(left_side, diff_params, unchanged_rs, results);\n double old_square_err = square_err;\n lambda = Lambda(points, nr_of_points, a, b, c, d, alph, bet, lambda, square_err, results);\n if (int(square_err) == int(old_square_err) && counter > 10 && square_err < old_square_err)\n break;\n else if (int(square_err) == int(old_square_err))\n counter++;\n else\n counter = 0;\n old_square_err = square_err;\n\n // std::cerr << \"New params: \" << a << \" \" << b << \" \" << c << \" \" << d << \" \" << alph << \" \" << bet << \" \";\n // std::cerr << \"lambda: \" << lambda << \" squares distance: \" << square_err << std::endl;\n }\n b = true_b;\n Points(nr_of_points, a, b, c, d, alph, bet);\n}","metadata":{"trusted":true},"outputs":[],"execution_count":34},{"id":"0a5b72fc-c12a-413a-bb6e-16594f1d906a","cell_type":"code","source":" int nr_of_points = 200;\n // double points[nr_of_points * 3];\n double points[200 * 3];\n double a = 5.2122, b = 2, c = 10.835, d = 17.07055, alph = -3.60384, bet = 1.13255;\n GenerateFlawedPoints(nr_of_points, a, b, c, d, alph, bet, points);\n LevenbergMarquardt(points, nr_of_points, a, b, c, d, alph, bet);\n for (int i = 0; i < nr_of_points; i++)\n {\n outfile << points[i * 3 + 0] << \" \" << points[i * 3 + 1] << \" \" << points[i * 3 + 2] << \"\\n\";\n }\n outfile << \"end\\n\";\n outfile.close();","metadata":{"trusted":true},"outputs":[],"execution_count":35},{"id":"5bde7cea-91e8-4ecc-89ef-b67ab77da0e1","cell_type":"markdown","source":"In the end, one can produce a graph by running **Graph_from_file.ipynb**. The way it is currently implemented, the graph shows the original generated points and the best helix approximation **LevenbergMarquardt** produced. Note that parameter **b**, used for the fitted helix here, is given by the user, not the algorithm.\n\nOverall, one can say that the parameters produced by the minimisation are quite close to expected results and the function seems to work well when using data with no added randomness. However, added noise sometimes skews the results and the fit ends up being visibly inaccurate. ","metadata":{}},{"id":"5a713ed4-c642-4a62-abf1-28bc651653f8","cell_type":"markdown","source":"## Appendix\n\n**(1)** Perhaps a better way to showcase Clad (but not necessarily a better way to approximate a helix) would be to use the gradient descent method, since it is more simplistic, however, the implementation found in **fitter.h** gets stuck in a local minimum that is very far off from the actual expected results.\n\n**(2)** That is because **b** is the parameter controlling the pitch of the helix and our distance function unfortunately has a local minimum at **b ≈ 0**. This occurs because for very small values of **b**, the helix practically turns into a cylinder. A cylinder will always pass through points that would otherwise be the minima on a helix with the correct **b** value. The way our function is written, it doesn’t have the solution to overcome this local minimum. Making **b** a constant and leaving it out from the future calculations also seems to lead to highly inaccurate answers.\n\n**(3)** Another improvement would be to use **clad::jacobian**, however, we ran into a problem where Clad’s execute method doesn’t support arguments that have length specified as a variable (a template argument may not reference a variable-length array type). Const variables do not solve the problem.\n\n**(4)** Unfortunately, due to the added randomness in GenerateFlawedPoints, sometimes the end result is not as expected.\n\n\n## References\n\n[1] https://github.com/vgvassilev/clad\n\n[2] https://cmsexperiment.web.cern.ch/detector/identifying-tracks\n\n[3] https://people.duke.edu/~hpgavin/lm.pdf\n\n[4] https://math.stackexchange.com/questions/13341/shortest-distance-between-a-point-and-a-helix","metadata":{}}]} \ No newline at end of file diff --git a/demos/helix-example/helix-example/README.md b/demos/helix-example/helix-example/README.md new file mode 100644 index 000000000..191aefca7 --- /dev/null +++ b/demos/helix-example/helix-example/README.md @@ -0,0 +1,30 @@ +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/compiler-research/helix-example/HEAD) +# Helix fitter example + +## Goal + +Particle tracking is an important part of the processing and analysis of data received from particle detectors, such as the Compact Muon Solenoid (CMS). Tracking is the step that determines the momentum of charged particles escaping from the collision point. It identifies individual particles by reconstructing their trajectories from points where charged particle “hits” were measured by the detector and interpreting them.[1] Due to the Lorentz force, charged particles move in a helical motion when affected by the magnetic field (neglecting other effects due to material interactions, etc). This means we can figure out a specific particle trajectory through the detector by fitting a helix function to data points in such a way that the distance from the data points and the helix would be minimized. In mathematical terms, we need to find optimal helix parameters by minimizing a loss function composed of the sum of least squared distances, thus giving the best estimation of these parameters. For this purpose we can use Clad to efficiently minimize the loss function. + +In this repository one can find the code containing one such helix fitter implementation. + +## Content + +Besides the main fitter code, there are a few more files: + +- Documentation.pdf - a more in depth explanation of the code and methods used. +- Helix.ipynb - a Jupyter notebook containing the code and the documentation comments. Can be used to try out the code online but is considerably slower. +- Graph_from_file.ipynb - accompanies Helix.ipynb. Reads from the output.txt (produced by Helix.ipynb) and plots the results. + + +## Usage + +One can compile the files with: + +``` +clang++ main.cc -o main -I /full/path/to/clad/include/ -fplugin=/full/path/to/lib/clad.so -I /full/path/to/kokkos-4.3.01/include +``` +Running the code and plotting the output can be done by: + +``` +./main | python3 graph.py +``` diff --git a/demos/helix-example/helix-example/distance.h b/demos/helix-example/helix-example/distance.h new file mode 100644 index 000000000..d90931d42 --- /dev/null +++ b/demos/helix-example/helix-example/distance.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +inline double DistanceSquare(double x1, double y1, double z1, double x2, double y2, double z2) +{ + return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2); +} + +inline double Distance(double x1, double y1, double z1, double x2, double y2, double z2) +{ + return std::sqrt(DistanceSquare(x1, y1, z1, x2, y2, z2)); +} + +inline double DistanceSquareA(double v[3], double x2, double y2, double z2) +{ + return DistanceSquare(v[0], v[1], v[2], x2, y2, z2); +} + +inline double DistanceA(double v[3], double x2, double y2, double z2) +{ + return Distance(v[0], v[1], v[2], x2, y2, z2); +} \ No newline at end of file diff --git a/demos/helix-example/helix-example/environment.yml b/demos/helix-example/helix-example/environment.yml new file mode 100644 index 000000000..d13965e83 --- /dev/null +++ b/demos/helix-example/helix-example/environment.yml @@ -0,0 +1,6 @@ +channels: + - conda-forge +dependencies: + - xeus-cling + - clad + - matplotlib diff --git a/demos/helix-example/helix-example/equations.h b/demos/helix-example/helix-example/equations.h new file mode 100644 index 000000000..c0eac9864 --- /dev/null +++ b/demos/helix-example/helix-example/equations.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +auto MY_PI = 3.14159265359; + +double EvaluateSinPlusLin(double A, double B, double C, double D, double x) +{ + /*When this equation is equal to zero, the distance between the point and the helix is the shortest.*/ + return A * std::sin(x + B) + C * x + D; +} + +double SolveSinPlusLin(double A, double B, double C, double D, double mi, double ma) +{ + /*Binary search to determine x, with which EvaluateSinPlusLin equation equals zero.*/ + for (int i = 0; i < 100; i++) + { + double mid = (mi + ma) / 2; + double vmi = EvaluateSinPlusLin(A, B, C, D, mi); + double vmid = EvaluateSinPlusLin(A, B, C, D, mid); + double vma = EvaluateSinPlusLin(A, B, C, D, ma); + + if (vmi < 0 and 0 < vmid) + { + ma = mid; + } + else if (vmid < 0 and 0 < vma) + { + mi = mid; + } + else if (vmid < 0 and 0 < vmi) + { + ma = mid; + } + else if (vma < 0 and 0 < vmid) + { + mi = mid; + } + else + { + break; + mi = mid; + } + } + + double x = (mi + ma) / 2; + return x; +} + +double NextValPiK(double offs, double x) +{ + /*Find the next 2 * PI * k + offset (where k is an integer) that is greater than x.*/ + + if (x < 0) + { + double v = -NextValPiK(-offs, -x) + 2 * MY_PI; + return v > x ? v : v + 2 * MY_PI; + } + + double kie = std::floor(x / 2 / MY_PI); + + for (int i = -2; i <= 2; i++) + { + double v = (kie + i) * 2 * MY_PI + offs; + + if (v > x) + { + return v; + } + } + + return 1000000000; +} + +// A cos(x + B) + C = 0 +double NextSinPlusInflection(double A, double B, double C, double x) +{ + /* Identifies the next inflection point of the sine curve.*/ + // cos(x + B) = -C / A + if (-C / A >= -1 && -C / A <= 1) + { + double inv = std::acos(-C / A); + return std::min(NextValPiK(inv - B, x), NextValPiK(-inv - B, x)); + } + else + { + return 1000000000; + } +} diff --git a/demos/helix-example/helix-example/fitter.h b/demos/helix-example/helix-example/fitter.h new file mode 100644 index 000000000..11496a7b3 --- /dev/null +++ b/demos/helix-example/helix-example/fitter.h @@ -0,0 +1,266 @@ +#include +#include +#include +#include +#include + +#include "helix.h" +#include "equations.h" +#include "matrices.h" +#include "clad/Differentiator/Differentiator.h" + +double DistanceToPoint(double a, double b, double c, double d, double alph, double bet, double x, double y, double z) +{ + /*Calculate the distance to a single point. */ + double t = HelixClosestTime(a, b, c, d, alph, bet, x, y, z); + double output[3]; + HelixPoint(a, b, c, d, alph, bet, t, output); + double dist = DistanceA(output, x, y, z); + dist += 0.001 * ((a * a) + (b * b) + (c * c) + (d * d) + (alph * alph) + (bet * bet)); + + return dist; +} + +double SquareErr(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet) +{ + /*Calculate the residual sum of squares. */ + double dist; + double square_err = 0; + for (int i = 0; i < nr_of_points; i++) + { + double x = points[i * 3]; + double y = points[i * 3 + 1]; + double z = points[i * 3 + 2]; + dist = DistanceToPoint(a, b, c, d, alph, bet, x, y, z); + square_err += (dist * dist); + } + return square_err; +} + +void Points(int nr_of_points, double a, double b, double c, double d, double alph, double bet) +{ + /*Generate and print out points on a helix with given params. */ + double t = 0; + for (int i = 0; i < nr_of_points; i++) + { + t += 0.1; + double output[3]; + HelixPoint(a, b, c, d, alph, bet, t, output); + double x = output[0], y = output[1], z = output[2]; + std::cout << x << " " << y << " " << z << "\n"; + } + std::cout << "end\n"; +} + +void GenerateFlawedPoints(int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *points) +{ + /*Generate points on a helix with given params but add noise. */ + auto seed = time(nullptr); + std::mt19937_64 rng(seed); + std::uniform_real_distribution uniform(-2 * MY_PI, 2 * MY_PI); + double output[3]; + double t = 0; + for (int i = 0; i < nr_of_points; i++) + { + t += 0.1; + HelixPoint(a, b, c, d, alph, bet, t, output); + points[i * 3] = output[0] + uniform(rng) / 10; + points[i * 3 + 1] = output[1] + uniform(rng) / 10; + points[i * 3 + 2] = output[2] + uniform(rng) / 10; + } +} + +void DistancesToAllPoints(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *dist) +{ + /*Calculate the distances to all points. */ + int n = 0; + for (int i = 0; i < nr_of_points; i++) + { + double x = points[i * 3]; + double y = points[i * 3 + 1]; + double z = points[i * 3 + 2]; + dist[n] = DistanceToPoint(a, b, c, d, alph, bet, x, y, z); + n++; + } +} +void Jacobian(double *points, int nr_of_points, double a, double b, double c, double d, double alph, double bet, double *Jacobian) +{ + /*Construct the nr_of_points x 6 Jacobian.*/ + auto dist_grad = clad::gradient(DistanceToPoint, "a, b, c, d, alph, bet"); + for (int i = 0; i < nr_of_points; i++) + { + double x = points[i * 3]; + double y = points[i * 3 + 1]; + double z = points[i * 3 + 2]; + double output[3]; + double da = 0, db = 0, dc = 0, dd = 0, dalph = 0, dbet = 0; + dist_grad.execute(a, b, c, d, alph, bet, x, y, z, &da, &db, &dc, &dd, &dalph, &dbet); + Jacobian[i * 6] = da; + Jacobian[i * 6 + 1] = db; + Jacobian[i * 6 + 2] = dc; + Jacobian[i * 6 + 3] = dd; + Jacobian[i * 6 + 4] = dalph; + Jacobian[i * 6 + 5] = dbet; + } +} + +double Lambda(double *points, int nr_of_points, double &a, double &b, double &c, double &d, double &alph, double &bet, double lambda, double &square_err, double *results) +{ + /*Calculate the damping coefficient lambda for the next iteration of the LevenbergMarquardt function.*/ + double new_lambda; + double new_square_err = SquareErr(points, nr_of_points, a + results[0], b + results[1], c + results[2], d + results[3], alph + results[4], bet + results[5]); + // std::cerr << "SQUARE ERR " << new_square_err << std::endl; + if ((new_square_err >= square_err) && (lambda < 1000)) + new_lambda = lambda * 10; + else + { + // std::cerr << "IMPROVEMENTS!"; + a += results[0]; + b += results[1]; + c += results[2]; + d += results[3]; + alph += results[4]; + bet += results[5]; + new_lambda = lambda / 10; + square_err = new_square_err; + } + return new_lambda; +} + +void LevenbergMarquardt(double *points, int nr_of_points, double true_b, double &a, double &b, double &c, double &d, double &alph, double &bet) +/*Use the Levenberg-Marquardt algorithm to fit a helix on a given set of points. Currently produces all of the parameters of the helix, except b.*/ +{ + a = 6.2122, b = 0.1, c = 1.9835, d = 1.707055, alph = -3.60384, bet = 1.13255; // currently breaks if the parameters are exact as the ones used for (noise free) generated points + + int diff_params = 6; + double lambda = 1; + double lambda_change = 1; + double square_err; + double jacobian[nr_of_points * diff_params]; + double tjacobian[diff_params * nr_of_points]; + double tjj[diff_params * diff_params]; + double results[diff_params]; + double counter = 0; + { + double dist[nr_of_points]; + DistancesToAllPoints(points, nr_of_points, a, b, c, d, alph, bet, dist); + square_err = 0; + for (int i = 0; i < nr_of_points; i++) + { + square_err += (dist[i] * dist[i]); + } + } + + for (int i = 0; i < 200; i++) + { + + Jacobian(points, nr_of_points, a, b, c, d, alph, bet, jacobian); + + Transpose(jacobian, nr_of_points, diff_params, tjacobian); + + MatrixMultiply(tjacobian, jacobian, diff_params, nr_of_points, diff_params, tjj); + + double diag[diff_params * diff_params]; + DiagOfSquareM(tjj, diff_params, diag); + + double identity[diff_params * diff_params]; + ScalarMultiply(diag, diff_params, diff_params, lambda, identity); + double left_side[diff_params * diff_params]; + AddMatrices(tjj, identity, diff_params, diff_params, left_side); + double dist[nr_of_points]; + DistancesToAllPoints(points, nr_of_points, a, b, c, d, alph, bet, dist); + double right_side[diff_params * 1]; + MatrixMultiply(tjacobian, dist, diff_params, nr_of_points, 1, right_side); + ScalarMultiply(right_side, 1, diff_params, -1, right_side); + + // left side is 6x6, right side is 6x1, so h is 6x1. + double forward_elim[diff_params * diff_params]; + double unchanged_rs[diff_params]; + CopyMatrix(right_side, diff_params, unchanged_rs); + ForwardElim(left_side, diff_params, right_side, forward_elim); + BackSub(forward_elim, diff_params, right_side, results); + CheckSolution(left_side, diff_params, unchanged_rs, results); + double old_square_err = square_err; + lambda = Lambda(points, nr_of_points, a, b, c, d, alph, bet, lambda, square_err, results); + if (int(square_err) == int(old_square_err) && counter > 10 && square_err < old_square_err) + break; + else if (int(square_err) == int(old_square_err)) + counter++; + else + counter = 0; + old_square_err = square_err; + + // std::cerr << "New params: " << a << " " << b << " " << c << " " << d << " " << alph << " " << bet << " "; + // std::cerr << "lambda: " << lambda << " squares distance: " << square_err << std::endl; + } + b = true_b; + Points(nr_of_points, a, b, c, d, alph, bet); +} + +void GradientDescent(double *points, int nr_of_points) +{ + /*Implementation of the gradient descent algorithm. Gets stuck in a local minimum.*/ + double a = 5.2122, b = 0.1, c = 0.9835, d = 1.707055, alph = -3.60384, bet = 1.13255; + double lambda = 0.00001; + double jacobian[nr_of_points * 6]; + double tjacobian[6 * nr_of_points]; + double dist[nr_of_points]; + double square_err = SquareErr(points, nr_of_points, a, b, c, d, alph, bet); + double params[6] = {0}; + double prev_square_er = SquareErr(points, nr_of_points, a, b, c, d, alph, bet); + std::cerr << square_err << std::endl; + for (int i = 0; i < 2000; i++) + { + DistancesToAllPoints(points, nr_of_points, a, b, c, d, alph, bet, dist); + Jacobian(points, nr_of_points, a, b, c, d, alph, bet, jacobian); + Transpose(jacobian, nr_of_points, 6, tjacobian); + + double y_dist[nr_of_points]; + ScalarMultiply(dist, nr_of_points, 1, -1, y_dist); + double h[6]; + MatrixMultiply(tjacobian, y_dist, 6, nr_of_points, 1, h); + ScalarMultiply(h, 6, 1, lambda, h); + double new_square_err = SquareErr(points, nr_of_points, a + h[0], b + h[1], c + h[2], d + h[3], alph + h[4], bet + h[5]); + + if (new_square_err < prev_square_er) + { + lambda = lambda * 10; + } + else + { + lambda = lambda / 10; + continue; + } + a += h[0]; + b += h[1]; + c += h[2]; + d += h[3]; + alph += h[4]; + bet += h[5]; + if (new_square_err < square_err) + { + square_err = new_square_err; + params[0] = a; + params[1] = b; + params[2] = c; + params[3] = d; + params[4] = alph; + params[5] = bet; + } + prev_square_er = new_square_err; + // std::cerr << "New params: " << a << " " << b << " " << c << " " << d << " " << alph << " " << bet << " "; + // std::cerr << "lambda: " << lambda << " squares distance: " << new_square_err << std::endl; + } + + double t = -nr_of_points / 2; + for (int i = 0; i < 10 * nr_of_points; i++) + { + t += 0.1; + double output[3]; + HelixPoint(params[0], params[1], params[2], params[3], params[4], params[5], t, output); + double x = output[0], y = output[1], z = output[2]; + + std::cout << x << " " << y << " " << z << "\n"; + } + std::cout << "end\n"; +} \ No newline at end of file diff --git a/demos/helix-example/helix-example/graph.py b/demos/helix-example/helix-example/graph.py new file mode 100644 index 000000000..639fc02ae --- /dev/null +++ b/demos/helix-example/helix-example/graph.py @@ -0,0 +1,25 @@ + +import matplotlib.pyplot as plt +import sys + +fig = plt.figure() +ax = plt.axes(projection="3d") + +x, y, z = [], [], [] + +for line in sys.stdin: + if line.strip() == "end": + ax.plot3D(x, y, z) + x.clear() + y.clear() + z.clear() + else: + a, b, c = [float(i) for i in line.split()] + x.append(a) + y.append(b) + z.append(c) + +ax.set_box_aspect([1,1,1]) +plt.show() + + diff --git a/demos/helix-example/helix-example/helix.h b/demos/helix-example/helix-example/helix.h new file mode 100644 index 000000000..74e8b6421 --- /dev/null +++ b/demos/helix-example/helix-example/helix.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include "rotations.h" +#include "equations.h" +#include "distance.h" + +inline void HelixPoint(double a, double b, double c, double d, double alph, double bet, double t, double output[3]) +{ + /*Describe a point on a helix in the Cartesian coordinate system.*/ + double x = a * (c + std::cos(t)); + double y = a * (d + std::sin(t)); + double z = a * b * t; + output[0] = x; + output[1] = y; + output[2] = z; + Rotate(x, y, z, alph, bet, output); +} + +inline double HelixClosestTime(double a, double b, double c, double d, double alph, double bet, double x, double y, double z) +{ + /*Calculate t, during which a helix with given params is the closest to a given point.*/ + auto MY_PI = 3.14159265359; + double point[3]; + UnRotate(x, y, z, alph, bet, point); + point[0] /= a; + point[1] /= a; + point[2] /= a; + point[0] -= c; + point[1] -= d; + double A = std::sqrt(point[0] * point[0] + point[1] * point[1]); + double B = std::atan2(-point[1], point[0]); + double C = b * b; + double D = -point[2] * b; + + double mi = point[2] / b - MY_PI; + double ma = point[2] / b + MY_PI; + double t1 = SolveSinPlusLin(A, B, C, D, mi, ma); + + double ans = t1; + HelixPoint(a, b, c, d, alph, bet, ans, point); + double dist = DistanceSquareA(point, x, y, z); + + for (double t = mi; t < ma; t = t) + { + double ttt = NextSinPlusInflection(A, B, C, t); + + if (ttt == t) + { + break; + } + + double cur = SolveSinPlusLin(A, B, C, D, t, ttt); + t = ttt; + HelixPoint(a, b, c, d, alph, bet, cur, point); + double dist2 = DistanceSquareA(point, x, y, z); + + if (dist2 < dist) + { + dist = dist2; + ans = cur; + } + } + + return ans; +} diff --git a/demos/helix-example/helix-example/main.cc b/demos/helix-example/helix-example/main.cc new file mode 100644 index 000000000..61e536af4 --- /dev/null +++ b/demos/helix-example/helix-example/main.cc @@ -0,0 +1,21 @@ +#include +#include +#include "clad/Differentiator/Differentiator.h" + +#include "fitter.h" + +int main() +{ + int nr_of_points = 200; + double points[nr_of_points * 3]; + double a = 5.2122, b = 2, c = 10.835, d = 17.07055, alph = -3.60384, bet = 1.13255; + GenerateFlawedPoints(nr_of_points, a, b, c, d, alph, bet, points); + LevenbergMarquardt(points, nr_of_points, b, a, b, c, d, alph, bet); + // GradientDescent(points, nr_of_points); + for (int i = 0; i < nr_of_points; i++) + { + std::cout << points[i * 3 + 0] << " " << points[i * 3 + 1] << " " << points[i * 3 + 2] << "\n"; + } + std::cout << "end\n"; + std::cerr << "Results: " << a << " " << b << " " << c << " " << d << " " << alph << " " << bet << std::endl; +} diff --git a/demos/helix-example/helix-example/matrices.h b/demos/helix-example/helix-example/matrices.h new file mode 100644 index 000000000..f08a00f18 --- /dev/null +++ b/demos/helix-example/helix-example/matrices.h @@ -0,0 +1,217 @@ +#pragma once +#include +#include +#include + +// All the matrices are written as 1D arrays! +inline void MatrixMultiply(double *a, double *b, int arows, int acols, int bcols, double *output) +{ + for (int i = 0; i < arows; i++) + { + for (int j = 0; j < bcols; j++) + { + double sum = 0; + for (int k = 0; k < acols; k++) + sum = sum + a[i * acols + k] * b[k * bcols + j]; + output[i * bcols + j] = sum; + } + } +} + +inline void Transpose(double *input, int rows, int cols, double *output) +{ + for (int i = 0; i < rows; ++i) + { + for (int j = 0; j < cols; ++j) + { + int i_input = i * cols + j; + + int i_output = j * rows + i; + + output[i_output] = input[i_input]; + } + } +} + +inline void DiagOfSquareM(double *input, int height, double *diag) +{ + // Works for square matrices only + for (int i = 0; i < height * height; i++) + { + diag[i] = 0; + } + for (int i = 0; i < height; i++) + { + diag[i * height + i] = input[i * height + i]; + } +} + +inline void ScalarMultiply(double *matrix, int rows, int cols, double number, double *output) +{ + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + output[i * cols + j] = number * matrix[i * cols + j]; + } + } +} + +inline double VectorLen(double *vector, int size) +{ + double length = 0; + for (int i = 0; i < size; i++) + { + length += vector[i] * vector[i]; + } + return std::sqrt(length); +} +inline void CopyMatrix(double *matrix, int size, double *output) +{ + for (int i = 0; i < size; i++) + { + output[i] = matrix[i]; + } +} +inline void AddMatrices(double *a, double *b, int rows, int cols, double *output) +{ + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + output[i * cols + j] = a[i * cols + j] + b[i * cols + j]; + } + } +} + +inline void swap_row(double *matrix, int size, int i, int j) +{ + for (int k = 0; k < size; k++) + { + double temp = matrix[i * size + k]; + matrix[i * size + k] = matrix[j * size + k]; + matrix[j * size + k] = temp; + } +} +inline void ForwardElim(double *input, int size, double *res, double *output) +{ + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + output[i * size + j] = input[i * size + j]; + } + } + for (int i = 0; i < size; i++) + { + int i_max = i; + double v_max = output[i_max * size + i]; + + for (int j = i + 1; j < size; j++) + if (std::abs(output[j * size + i]) > std::abs(v_max) && output[j * size + i] != 0) + v_max = output[j * size + i], i_max = j; + if (i_max != i) + { + swap_row(output, size, i, i_max); + double temp = res[i]; + res[i] = res[i_max]; + res[i_max] = temp; + } + + if (output[i * size + i] == 0.0) + { + std::cerr << "Mathematical Error!"; + std::cerr << "Input that caused the error is:"; + for (int i = 0; i < size; i++) + { + for (int j = 0; j < size; j++) + { + std::cerr << output[i * size + j] << " "; + } + std::cerr << std::endl; + } + } + for (int j = i + 1; j < size; j++) + { + double ratio = output[j * size + i] / output[i * size + i]; + + for (int k = 0; k < size; k++) + { + output[j * size + k] = output[j * size + k] - ratio * output[i * size + k]; + if (std::abs(output[j * size + k]) <= 1e-15) + { + output[j * size + k] = 0; + } + } + res[j] = res[j] - ratio * res[i]; + if (std::abs(res[j]) <= 1e-15) + { + res[j] = 0; + } + } + } + + // std::cerr << "Forward elimination results:" << std::endl; + // std::cerr << "Left side:" << std::endl; + // for (int j = 0; j < size; j++) + // { + // for (int k = 0; k < size; k++) + // { + // std::cerr << output[j * size + k] << " "; + // } + // std::cerr << std::endl; + // } + // std::cerr << "Right side:" << std::endl; + // for (int k = 0; k < size; k++) + // { + // std::cerr << res[k] << " "; + // } + // std::cerr << std::endl; +} + +void BackSub(double *input, int size, double *right_side, double *results) +{ + /*Back substitution and the result of Gaussian elimination*/ + for (int i = (size - 1); i > -1; i--) + { + results[i] = right_side[i]; + for (int j = (size - 1); j > i; j--) + { + results[i] -= input[i * size + j] * results[j]; + } + results[i] /= input[i * size + i]; + if (std::abs(results[i]) <= 1e-15) + { + results[i] = 0; + } + } +} +void CheckSolution(double *input, int size, double *right_side, double *results) +{ + for (int i = 0; i < size; i++) + { + double sum = 0; + for (int j = 0; j < size; j++) + { + sum += input[i * size + j] * results[j]; + } + if (std::abs(sum - right_side[i]) >= 1e-5) + { + std::cerr << "Wrong solution " << sum << " " << right_side[i] << std::endl; + std::abort(); + } + } +} + +inline void PrintMatrix(std::string name, double *matrix, int rows, int cols) +{ + std::cerr << name << std::endl; + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + std::cerr << matrix[i * cols + j] << " "; + } + std::cerr << std::endl; + } +} \ No newline at end of file diff --git a/demos/helix-example/helix-example/rotations.h b/demos/helix-example/helix-example/rotations.h new file mode 100644 index 000000000..b7c204e47 --- /dev/null +++ b/demos/helix-example/helix-example/rotations.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +inline void RotateAlph(double x, double y, double z, double alph, double output[3]) { + output[0] = x; + output[1] = y * cos(alph) - z * sin(alph); + output[2] = y * sin(alph) + z * cos(alph); +} + +inline void RotateBet(double x, double y, double z, double bet, double output[3]) { + output[0] = x * cos(bet) + z * sin(bet); + output[1] = y; + output[2] = -x * sin(bet) + z * cos(bet); +} + +inline void Rotate(double x, double y, double z, double alph, double bet, double output[3]) { + double point[3]; + RotateAlph(x, y, z, alph, point); + RotateBet(point[0], point[1], point[2], bet, output); +} + +inline void UnRotate(double x, double y, double z, double alph, double bet, double output[3]) { + double point[3]; + RotateBet(x, y, z, -bet, point); + RotateAlph(point[0], point[1], point[2], -alph, output); +} diff --git a/demos/helix-example/helix-example/tests.cc b/demos/helix-example/helix-example/tests.cc new file mode 100644 index 000000000..285d57039 --- /dev/null +++ b/demos/helix-example/helix-example/tests.cc @@ -0,0 +1,210 @@ +#include +#include +#include "matrices.h" +#include "fitter.h" + +/* Tests for "matrices.h" */ +bool AreSame(double a, double b) +{ + return fabs(a - b) < 0.00009; +} +bool ArraysEqual(double *a, double *b, int N) +{ + for (int i = 0; i < N; i++) + { + if (AreSame(a[i], b[i])) + { + } + else + { + std::cerr << a[i] << " " << b[i] << std::endl; + return false; + } + } + return true; +} + +void TestMultiply() +{ + double a[2 * 3] = {1, 2, 3, 4, 5, 6}; + double b[3 * 4] = {11, 12, 13, 14, 14, 15, 16, 7, 17, 18, 19, 20}; + double output[2 * 4]; + MatrixMultiply(a, b, 2, 3, 4, output); + double expected_output[2 * 4] = {90, 96, 102, 88, 216, 231, 246, 211}; + for (int i = 0; i < 8; i++) + { + std::cout << output[i] << " "; + } + std::cout << std::endl; + + assert(ArraysEqual(output, expected_output, 8)); +} + +void TestTranspose() +{ + double input[2 * 3] = {1, 2, 3, 4, 5, 6}; + double output[3 * 2]; + double expected_output[3 * 2] = {1, 4, 2, 5, 3, 6}; + Transpose(input, 2, 3, output); + for (int i = 0; i < 6; i++) + { + std::cout << output[i] << " "; + } + std::cout << std::endl; + + assert(ArraysEqual(output, expected_output, 6)); +} + +void TestDiagOfSquareM() +{ + double input[3 * 3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + double diag[3 * 3]; + double expected_diag[3 * 3] = {1, 0, 0, 0, 5, 0, 0, 0, 9}; + DiagOfSquareM(input, 3, diag); + for (int i = 0; i < 9; i++) + { + std::cout << diag[i] << " "; + } + std::cout << std::endl; + assert(ArraysEqual(diag, expected_diag, 9)); +} + +void TestScalarMultiply() +{ + double input[2 * 3] = {1, 2, 3, 4, 5, 6}; + double output[2 * 3]; + double expected_output[2 * 3] = {2, 4, 6, 8, 10, 12}; + + ScalarMultiply(input, 2, 3, 2, output); + for (int i = 0; i < 6; i++) + { + std::cout << output[i] << " "; + } + std::cout << std::endl; + + assert(ArraysEqual(output, expected_output, 6)); +} + +void TestAddMatrices() +{ + double a[2 * 3] = {1, 2, 3, 4, 5, 6}; + double b[2 * 3] = {2, 4, 6, 8, 10, 12}; + double output[2 * 3]; + double expected_output[2 * 3] = {3, 6, 9, 12, 15, 18}; + AddMatrices(a, b, 2, 3, output); + for (int i = 0; i < 6; i++) + { + std::cout << output[i] << " "; + } + std::cout << std::endl; + + assert(ArraysEqual(output, expected_output, 6)); +} + +void TestGaussianElim() +{ + double left_side[36] = { + + 2, 3, 1, 5, 7, 1, // + + 4, + 7, 2, 10, 14, 2, // + + 1, + 2, 2, 3, 5, 2, // + + 3, + 5, 4, 1, 6, 4, // + + 5, + 1, 3, 2, 1, 3, // + + 2, + 4, 6, 1, 3, 5}; + + double right_side[6] = {25, 53, 18, 31, 23, 40}; + double output[36]; + double results[6]; + ForwardElim(left_side, 6, right_side, output); + std::cout << "right side " << std::endl; + for (int i = 0; i < 6; i++) + { + std::cout << right_side[i] << " "; + } + std::cout << std::endl; + + BackSub(output, 6, right_side, results); + for (int i = 0; i < 6; i++) + { + std::cout << results[i] << " "; + } + std::cout << std::endl; +} + +void TestDistanceToPoint() +{ + double a = 5.2122, b = -4.79395, c = -26.40835, d = -4.207055, alph = -3.60384, bet = 1.13255; + double t = 0.1; + double output[3]; + HelixPoint(a, b, c, d, alph, bet, t, output); + std::cout << "Generated point " << output[0] << " " << output[1] << " " << output[2] << " " << std ::endl; + double x = output[0], y = output[1], z = output[2]; + double dist = DistanceToPoint(a, b, c, d, alph, bet, x, y, z); + std::cout << "Distance to point on helix is: " << dist << std ::endl; +} + +void TestDistancesToPoints() +{ + double a = 5.2122, b = -4.79395, c = -26.40835, d = -4.207055, alph = -3.60384, bet = 1.13255; + double t = 0; + double points[10][3]; + + std::cout << "Generated points: " << std::endl; + for (int i = 0; i < 10; i++) + { + t += 0.1; + double output[3]; + HelixPoint(a, b, c, d, alph, bet, t, output); + double x = output[0], y = output[1], z = output[2]; + points[i][0] = x; + points[i][1] = y; + points[i][2] = z; + std::cout << output[0] << " " << output[1] << " " << output[2] << " " << std ::endl; + } + double points1D[10 * 3]; + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < 3; j++) + { + points1D[i * 3 + j] = points[i][j]; + } + } + double dist[10 * 3]; + DistancesToAllPoints(points1D, 10, a, b, c, d, alph, bet, dist); + std::cout << "Distances to all points: " << std::endl; + for (int i = 0; i < 10; i++) + { + std::cout << dist[i] << std::endl; + } + std::cout << "done\n"; +} + +int main() +{ + std::cout << "TestMultiply results: " << std::endl; + TestMultiply(); + std::cout << "TestTranspose results: " << std::endl; + TestTranspose(); + std::cout << "TestDiagOfSquareM results: " << std::endl; + TestDiagOfSquareM(); + std::cout << "TestScalarMultiply results: " << std::endl; + TestScalarMultiply(); + std::cout << "TestAddMatrices results: " << std::endl; + TestAddMatrices(); + std::cout << "TestGaussianElim results: " << std::endl; + TestGaussianElim(); + std::cout << "TestDistanceToPoint results: " << std::endl; + TestDistanceToPoint(); + std::cout << "TestDistancesToAllPoint results: " << std::endl; + TestDistancesToPoints(); +} \ No newline at end of file