2025-12-10 00:40:59 -06:00
{
"cells": [
{
"cell_type": "markdown",
"id": "91e4c90f",
"metadata": {},
"source": [
"Step 1: Clean out non-model simulation points: gap heights below 5mm"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 26,
2025-12-10 00:40:59 -06:00
"id": "e4ff9106",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
2025-12-12 08:56:30 -06:00
"import os\n",
2025-12-10 00:40:59 -06:00
"plt.rcParams['text.usetex'] = True"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 27,
2025-12-10 00:40:59 -06:00
"id": "32946653",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 4471 entries, 0 to 4470\n",
"Data columns (total 6 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 currL [A] 4471 non-null int64 \n",
" 1 currR [A] 4471 non-null int64 \n",
" 2 rollDeg [deg] 4471 non-null float64\n",
" 3 GapHeight [mm] 4471 non-null float64\n",
" 4 YokeForce.Force_z [newton] 4471 non-null float64\n",
" 5 YokeTorque.Torque [mNewtonMeter] 4471 non-null float64\n",
"dtypes: float64(4), int64(2)\n",
"memory usage: 209.7 KB\n",
"\n",
"After adding mirrored data:\n",
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 8281 entries, 0 to 8280\n",
"Data columns (total 6 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 currL [A] 8281 non-null int64 \n",
" 1 currR [A] 8281 non-null int64 \n",
" 2 rollDeg [deg] 8281 non-null float64\n",
" 3 GapHeight [mm] 8281 non-null float64\n",
" 4 YokeForce.Force_z [newton] 8281 non-null float64\n",
" 5 YokeTorque.Torque [mNewtonMeter] 8281 non-null float64\n",
"dtypes: float64(4), int64(2)\n",
"memory usage: 388.3 KB\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"magDf = pd.read_csv(\"Ansys Results 12-9.csv\")\n",
"magDf.info()\n",
"\n",
"condition = magDf['GapHeight [mm]'] < 5\n",
"\n",
"magDf = magDf[~condition]\n",
"\n",
"nzRoll = magDf[magDf['rollDeg [deg]'] != 0]\n",
"\n",
"mirrored_data = nzRoll.copy()\n",
"mirrored_data['rollDeg [deg]'] = -nzRoll['rollDeg [deg]']\n",
"mirrored_data['currL [A]'], mirrored_data['currR [A]'] = nzRoll['currR [A]'].values, nzRoll['currL [A]'].values\n",
"mirrored_data['YokeTorque.Torque [mNewtonMeter]'] = -nzRoll['YokeTorque.Torque [mNewtonMeter]']\n",
"\n",
"magDf = pd.concat([magDf, mirrored_data], ignore_index=True)\n",
"\n",
"magDf = magDf.sort_values(['rollDeg [deg]', 'currL [A]', 'currR [A]', 'GapHeight [mm]']).reset_index(drop=True)\n",
"\n",
"print()\n",
"print(\"After adding mirrored data:\")\n",
2025-12-10 00:40:59 -06:00
"magDf.info()"
]
},
{
"cell_type": "markdown",
"id": "ecd1f124",
"metadata": {},
"source": [
2025-12-12 13:42:41 -06:00
"### After removing non-model rows, we have 4459 rows * ~2 for the mirrored roll angles, which matches the number of simulations conducted in Ansys."
2025-12-10 00:40:59 -06:00
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 28,
2025-12-10 00:40:59 -06:00
"id": "1a76017e",
"metadata": {},
"outputs": [
{
"data": {
2025-12-12 08:56:30 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"first_subset = magDf.iloc[0:13]\n",
"x = first_subset[\"GapHeight [mm]\"]\n",
"x = 1/x\n",
"y = first_subset[\"YokeForce.Force_z [newton]\"]\n",
"plt.plot(x, y)\n",
"plt.title(r\"$-15A, -15A, 0^\\circ roll$\")\n",
"plt.xlabel(\"Inverse Gap Height (1/mm)\")\n",
"plt.ylabel(\"Force on magnetic yoke (N)\")\n",
2025-12-12 13:42:41 -06:00
"plt.show()\n",
"\n",
"# Experimented here with ways to represent gap-height-to-force relationship."
2025-12-10 00:40:59 -06:00
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 29,
2025-12-10 00:40:59 -06:00
"id": "1ba9a1b6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 8281 entries, 0 to 8280\n",
"Data columns (total 7 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 currL [A] 8281 non-null int64 \n",
" 1 currR [A] 8281 non-null int64 \n",
" 2 rollDeg [deg] 8281 non-null float64\n",
" 3 GapHeight [mm] 8281 non-null float64\n",
" 4 YokeForce.Force_z [newton] 8281 non-null float64\n",
" 5 YokeTorque.Torque [mNewtonMeter] 8281 non-null float64\n",
" 6 invGap 8281 non-null float64\n",
"dtypes: float64(5), int64(2)\n",
"memory usage: 453.0 KB\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>currL [A]</th>\n",
" <th>currR [A]</th>\n",
" <th>rollDeg [deg]</th>\n",
" <th>GapHeight [mm]</th>\n",
" <th>YokeForce.Force_z [newton]</th>\n",
" <th>YokeTorque.Torque [mNewtonMeter]</th>\n",
" <th>invGap</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>-15</td>\n",
" <td>-15</td>\n",
" <td>-4.0</td>\n",
" <td>6.0</td>\n",
" <td>260.518631</td>\n",
" <td>-6976.851677</td>\n",
" <td>0.166667</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>-15</td>\n",
" <td>-15</td>\n",
" <td>-4.0</td>\n",
" <td>8.0</td>\n",
" <td>163.529872</td>\n",
" <td>-3335.704070</td>\n",
" <td>0.125000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>-15</td>\n",
" <td>-15</td>\n",
" <td>-4.0</td>\n",
" <td>9.0</td>\n",
" <td>136.554807</td>\n",
" <td>-2506.094902</td>\n",
" <td>0.111111</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>-15</td>\n",
" <td>-15</td>\n",
" <td>-4.0</td>\n",
" <td>10.0</td>\n",
" <td>117.403213</td>\n",
" <td>-1959.725693</td>\n",
" <td>0.100000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>-15</td>\n",
" <td>-15</td>\n",
" <td>-4.0</td>\n",
" <td>10.5</td>\n",
" <td>109.107025</td>\n",
" <td>-1759.467304</td>\n",
" <td>0.095238</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" currL [A] currR [A] rollDeg [deg] GapHeight [mm] \\\n",
"0 -15 -15 -4.0 6.0 \n",
"1 -15 -15 -4.0 8.0 \n",
"2 -15 -15 -4.0 9.0 \n",
"3 -15 -15 -4.0 10.0 \n",
"4 -15 -15 -4.0 10.5 \n",
"\n",
" YokeForce.Force_z [newton] YokeTorque.Torque [mNewtonMeter] invGap \n",
"0 260.518631 -6976.851677 0.166667 \n",
"1 163.529872 -3335.704070 0.125000 \n",
"2 136.554807 -2506.094902 0.111111 \n",
"3 117.403213 -1959.725693 0.100000 \n",
"4 109.107025 -1759.467304 0.095238 "
]
},
2026-02-11 17:33:18 -06:00
"execution_count": 29,
2025-12-10 00:40:59 -06:00
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# sooooo it looks like we can invert the entire gap height column and see how that works out...\n",
"magDf[\"invGap\"] = magDf[\"GapHeight [mm]\"].transform(lambda x: 1/x)\n",
"magDf.info()\n",
"magDf.head()"
]
},
{
"cell_type": "markdown",
"id": "9cd13fd5",
"metadata": {},
"source": [
"## Let's try fitting a polynomial to this..."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 30,
2025-12-10 00:40:59 -06:00
"id": "bd16a120",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
2026-02-11 17:33:18 -06:00
"Force Coeffs: [-8.65837181e-02 2.83478672e-01 2.86948934e-01 3.01765452e-09\n",
" 3.53295632e+02 -1.79173937e-03 -1.72965608e-03 -2.24271786e-02\n",
" -1.14467613e+01 -2.16073380e-03 2.24271783e-02 -1.16266321e+01\n",
" 3.17540504e-01 -1.63205830e-08 6.56934791e+03 -4.22076677e-05\n",
" -1.06778310e-05 -2.15844497e-04 1.18908945e-01 -7.64609651e-06\n",
" -6.00663086e-11 1.01686164e-01 -1.01983869e-02 1.54885340e-01\n",
" -1.03778353e+02 -4.02599166e-05 2.15844499e-04 1.29958896e-01\n",
" -1.01471000e-02 -1.54885337e-01 -1.00615041e+02 2.44554291e-10\n",
" 2.06864028e+00 -5.79189873e-07 -4.19530515e+04 -3.84322448e-07\n",
" 3.91430177e-07 -7.41838903e-07 7.93047135e-04 -4.85816599e-07\n",
" -2.53746609e-06 1.17129797e-04 -2.29814329e-04 4.03023264e-04\n",
" 2.97296754e-01 -1.05011480e-07 2.53746711e-06 -3.78159442e-06\n",
" -4.82390444e-05 6.31672492e-12 -5.68285023e-01 3.49652510e-03\n",
" 4.38290920e-01 -8.84140415e+00 7.71042932e+02 1.32237346e-07\n",
" 7.41837766e-07 7.27510018e-04 -2.31026415e-04 -4.03023107e-04\n",
" 1.91281532e-01 -3.49652510e-03 4.35555849e-01 8.84140411e+00\n",
" 7.49444071e+02 -3.20760957e-02 7.25108196e-10 -1.33481028e+02\n",
" 5.39284901e-06 7.71176013e+04 3.63953134e-08 1.21055308e-08\n",
" 1.38327010e-07 -2.26843442e-05 2.02886312e-08 -1.26534253e-07\n",
" -5.30119439e-06 8.63664127e-08 4.12729541e-05 -8.00925426e-04\n",
" 2.31829560e-08 1.45661261e-13 1.06948469e-05 -7.75307058e-07\n",
" 3.44198882e-05 7.75195525e-04 9.67991207e-06 3.94281513e-03\n",
" 1.52654026e-01 -5.85408108e-01 2.66529749e-08 1.26534701e-07\n",
" -1.16266847e-07 -8.31896955e-07 -3.44199448e-05 1.37334978e-03\n",
" 3.41060513e-13 7.56972800e-04 -5.81983350e-10 1.67361830e+00\n",
" -1.53624150e-04 -5.22167175e-02 -4.26268983e+00 -1.86825485e+01\n",
" -1.80981191e+03 3.91169848e-08 -1.38327266e-07 -2.90007854e-05\n",
" 5.72626959e-08 -4.12729217e-05 -4.53297520e-04 -9.67991258e-06\n",
" 3.95070565e-03 -1.52654027e-01 -2.35783105e-01 -1.49754214e-04\n",
" 5.22167175e-02 -4.24980327e+00 1.86825486e+01 -1.75997408e+03\n",
" 8.13571432e-13 4.47231699e-01 -4.49006710e-09 1.08183457e+03\n",
" -1.37863751e-05 4.14731645e+04]\n",
"Torque Coeffs: [ 8.38589158e-01 8.73851984e+00 -8.81670175e+00 -6.05820759e+01\n",
" -9.82724462e+01 -8.14346461e-02 -8.55573216e-03 -1.26586339e+00\n",
" -2.28782707e+02 7.92025829e-02 -1.26586339e+00 2.32329852e+02\n",
" 1.59724442e-01 2.40322153e+02 1.91664541e+03 -1.60378740e-03\n",
" 1.33688455e-03 -1.09883429e-02 4.99001686e+00 -1.13086452e-03\n",
" -5.01228566e-04 2.95245993e-01 -5.53027250e-01 1.40436061e+01\n",
" -7.51330187e+03 1.58514086e-03 -1.09883430e-02 -5.01157087e+00\n",
" 5.39147323e-01 1.40436061e+01 7.47720714e+03 8.04333346e+00\n",
" -9.27393815e-01 5.61703430e+04 -1.36592025e+04 -8.85043095e-05\n",
" 5.78297146e-05 -3.32036715e-04 1.42290241e-02 9.30856299e-06\n",
" -1.81943314e-04 4.52452056e-03 -1.03937269e-02 -1.18018278e-01\n",
" 1.97297584e+01 -5.50804572e-05 -1.81943331e-04 -3.44842175e-03\n",
" 5.10215386e-05 -1.04187648e-01 -3.02785064e+00 2.03017291e-01\n",
" 2.26755123e+01 -3.31637755e+02 4.95131780e+04 9.86640682e-05\n",
" -3.32036717e-04 -1.38740169e-02 1.04393181e-02 -1.18018278e-01\n",
" -1.90316043e+01 2.03017291e-01 -2.25845778e+01 -3.31637755e+02\n",
" -4.94386582e+04 -4.28909175e-03 -2.85648016e+02 -2.71181234e+00\n",
" -4.00600713e+05 2.87297365e+04 1.94950204e-06 -5.18825317e-06\n",
" -7.73369460e-06 5.36214594e-04 1.10845519e-06 -1.84292549e-06\n",
" -4.65903428e-04 6.42878210e-06 3.08081235e-03 -1.09668591e-03\n",
" -1.62664696e-06 6.42940023e-06 -1.00144481e-04 -2.50111810e-06\n",
" 1.71620767e-03 -2.14977215e-02 7.20307351e-04 1.76411744e-01\n",
" 8.07086541e+00 -3.68279934e+01 4.55521419e-06 -1.84292367e-06\n",
" 4.37143508e-04 -1.60120084e-06 1.71620770e-03 1.99691899e-02\n",
" 1.48517200e-04 -1.19885601e-03 1.78969342e+00 9.92388686e+00\n",
" -7.19411511e-03 -2.98153220e+00 -2.04344115e+02 -1.80247523e+03\n",
" -1.11849627e+05 -1.66017404e-06 -7.73369428e-06 -7.23356811e-04\n",
" -4.16883619e-06 3.08081229e-03 -6.83985220e-03 7.20307349e-04\n",
" -1.75664631e-01 8.07086541e+00 3.49676909e+01 7.76960323e-03\n",
" -2.98153220e+00 2.03737293e+02 -1.80247523e+03 1.12167507e+05\n",
" 4.69772030e-02 -2.00828167e-02 2.13341632e+03 5.37309113e+01\n",
" 1.24121750e+06 1.52681419e+04]\n"
2025-12-10 00:40:59 -06:00
]
}
],
"source": [
"from sklearn.preprocessing import PolynomialFeatures\n",
"from sklearn.linear_model import LinearRegression\n",
"\n",
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [4]\n",
2025-12-10 00:40:59 -06:00
"# 1. Load your Ansys CSV - using invGap for modeling\n",
"X = magDf[['currL [A]', 'currR [A]', 'rollDeg [deg]', 'invGap']]\n",
"y = magDf[['YokeForce.Force_z [newton]', 'YokeTorque.Torque [mNewtonMeter]']]\n",
"\n",
"# 2. Create Features (e.g. z^2, z^3, z*IL, etc.)\n",
"poly = PolynomialFeatures(degree=5) \n",
"X_poly = poly.fit_transform(X)\n",
"\n",
"# 3. Fit\n",
"model = LinearRegression()\n",
"model.fit(X_poly, y)\n",
"\n",
"# 4. Extract Equation\n",
"print(\"Force Coeffs:\", model.coef_[0])\n",
"print(\"Torque Coeffs:\", model.coef_[1])"
]
},
{
"cell_type": "markdown",
"id": "dd6861d0",
"metadata": {},
"source": [
"## Compare Model Predictions with Raw Data"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 31,
2025-12-10 00:40:59 -06:00
"id": "dc79d9fe",
"metadata": {},
"outputs": [
{
"data": {
2025-12-12 08:56:30 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1400x500 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Force R² Score: 0.999873\n",
"Torque R² Score: 0.999661\n"
]
}
],
"source": [
"y_pred = model.predict(X_poly)\n",
"\n",
"force_pred = y_pred[:, 0]\n",
"torque_pred = y_pred[:, 1]\n",
"\n",
"force_actual = y.iloc[:, 0].values\n",
"torque_actual = y.iloc[:, 1].values\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
"\n",
2025-12-12 13:42:41 -06:00
"# Force comparison\n",
2025-12-10 00:40:59 -06:00
"axes[0].scatter(force_actual, force_pred, alpha=0.5, s=10)\n",
"axes[0].plot([force_actual.min(), force_actual.max()], \n",
" [force_actual.min(), force_actual.max()], \n",
" 'r--', linewidth=2, label='Perfect Fit')\n",
"axes[0].set_xlabel('Actual Force (N)', fontsize=12)\n",
"axes[0].set_ylabel('Predicted Force (N)', fontsize=12)\n",
"axes[0].set_title('Force: Predicted vs Actual', fontsize=14)\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
2025-12-12 13:42:41 -06:00
"# Torque comparison\n",
2025-12-10 00:40:59 -06:00
"axes[1].scatter(torque_actual, torque_pred, alpha=0.5, s=10)\n",
"axes[1].plot([torque_actual.min(), torque_actual.max()], \n",
" [torque_actual.min(), torque_actual.max()], \n",
" 'r--', linewidth=2, label='Perfect Fit')\n",
"axes[1].set_xlabel('Actual Torque (mN·m)', fontsize=12)\n",
"axes[1].set_ylabel('Predicted Torque (mN·m)', fontsize=12)\n",
"axes[1].set_title('Torque: Predicted vs Actual', fontsize=14)\n",
"axes[1].legend()\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"from sklearn.metrics import r2_score\n",
"print(f\"Force R² Score: {r2_score(force_actual, force_pred):.6f}\")\n",
"print(f\"Torque R² Score: {r2_score(torque_actual, torque_pred):.6f}\")"
]
},
{
"cell_type": "markdown",
"id": "5a5d0634",
"metadata": {},
"source": [
"### Visualize a specific subset (like the first 13 rows)"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 32,
2025-12-10 00:40:59 -06:00
"id": "837a814f",
"metadata": {},
"outputs": [
{
"data": {
2025-12-12 08:56:30 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1000x600 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# For the first subset\n",
2025-12-10 00:40:59 -06:00
"subset_indices = range(0, 13)\n",
"x_subset = magDf.iloc[subset_indices][\"GapHeight [mm]\"]\n",
"y_actual_subset = magDf.iloc[subset_indices][\"YokeForce.Force_z [newton]\"]\n",
"y_pred_subset = force_pred[subset_indices]\n",
"\n",
"# Plot actual vs predicted for this specific condition\n",
"plt.figure(figsize=(10, 6))\n",
"plt.plot(x_subset, y_actual_subset, 'o-', label='Actual (Ansys)', linewidth=2, markersize=8)\n",
"plt.plot(x_subset, y_pred_subset, 's--', label='Predicted (Model)', linewidth=2, markersize=6)\n",
"plt.title(r\"$-15A, -15A, 0^\\circ$ roll: Model vs Ansys\", fontsize=14)\n",
"plt.xlabel(\"Gap Height (mm)\", fontsize=12)\n",
"plt.ylabel(\"Force on magnetic yoke (N)\", fontsize=12)\n",
"plt.legend(fontsize=11)\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "b46ce772",
"metadata": {},
"source": [
"## Better Interpolation Methods\n",
"\n",
"Let's improve interpolation accuracy without manually guessing polynomial order."
]
},
{
"cell_type": "markdown",
"id": "a019f402",
"metadata": {},
"source": [
"### Method 1: Cross-Validation to Find Best Polynomial Degree\n",
"\n",
"Automatically test different polynomial degrees and find the one with best validation score."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 33,
2025-12-10 00:40:59 -06:00
"id": "fd12d56b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Degree 1: Train R² = 0.752592, Test R² = 0.757407\n",
"Degree 2: Train R² = 0.965022, Test R² = 0.965732\n",
"Degree 3: Train R² = 0.993618, Test R² = 0.993764\n",
"Degree 4: Train R² = 0.998598, Test R² = 0.998637\n",
"Degree 5: Train R² = 0.999764, Test R² = 0.999752\n",
"Degree 6: Train R² = 0.999932, Test R² = 0.999927\n",
"Degree 7: Train R² = 0.999950, Test R² = 0.999947\n",
"Degree 8: Train R² = 0.999889, Test R² = 0.999802\n"
]
},
{
"data": {
2025-12-12 08:56:30 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1000x600 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Best polynomial degree: 7\n",
"Best test R² score: 0.999947\n"
]
}
],
"source": [
"from sklearn.model_selection import cross_val_score\n",
"from sklearn.pipeline import Pipeline\n",
"\n",
"from sklearn.model_selection import train_test_split\n",
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n",
"\n",
"# Test polynomial degrees from 1 to 8\n",
"degrees = range(1, 9)\n",
"train_scores_force = []\n",
"test_scores_force = []\n",
"train_scores_torque = []\n",
"test_scores_torque = []\n",
"\n",
"for degree in degrees:\n",
" pipe = Pipeline([\n",
" ('poly', PolynomialFeatures(degree=degree)),\n",
" ('model', LinearRegression())\n",
" ])\n",
" \n",
" pipe.fit(X_train, y_train)\n",
" \n",
" train_score = pipe.score(X_train, y_train)\n",
" test_score = pipe.score(X_test, y_test)\n",
" \n",
" train_scores_force.append(train_score)\n",
" test_scores_force.append(test_score)\n",
" \n",
" print(f\"Degree {degree}: Train R² = {train_score:.6f}, Test R² = {test_score:.6f}\")\n",
"\n",
"plt.figure(figsize=(10, 6))\n",
"plt.plot(degrees, train_scores_force, 'o-', label='Training Score', linewidth=2, markersize=8)\n",
"plt.plot(degrees, test_scores_force, 's-', label='Test Score', linewidth=2, markersize=8)\n",
"plt.xlabel('Polynomial Degree', fontsize=12)\n",
"plt.ylabel('R² Score', fontsize=12)\n",
"plt.title('Model Performance vs Polynomial Degree', fontsize=14)\n",
"plt.legend(fontsize=11)\n",
"plt.grid(True, alpha=0.3)\n",
"plt.xticks(degrees)\n",
"plt.show()\n",
"\n",
"best_degree = degrees[np.argmax(test_scores_force)]\n",
"print(f\"\\nBest polynomial degree: {best_degree}\")\n",
"print(f\"Best test R² score: {max(test_scores_force):.6f}\")"
]
},
{
"cell_type": "markdown",
"id": "86aaa7f5",
"metadata": {},
"source": [
"### Visualize Best Polynomial Function vs Ansys Data\n",
"\n",
"Plot the continuous polynomial function to check for overfitting/oscillations between data points."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 34,
2025-12-10 00:40:59 -06:00
"id": "8270efa4",
"metadata": {},
"outputs": [],
"source": [
2025-12-12 08:56:30 -06:00
"best_degree = 6 # Degree 7 actually overfits to the data points and struggles to generalize"
2025-12-10 00:40:59 -06:00
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 35,
2025-12-10 00:40:59 -06:00
"id": "d94aa4c1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Using polynomial degree: 6\n"
]
},
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1200x700 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Checking for overfitting indicators:\n",
"Number of actual data points in this condition: 13\n",
"Gap spacing: 1.50 mm average\n",
"Polynomial curve resolution: 500 points over 25.0 mm range\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [5]\n",
2025-12-10 00:40:59 -06:00
"# Train best degree polynomial on full dataset\n",
"poly_best = PolynomialFeatures(degree=best_degree)\n",
"X_poly_best = poly_best.fit_transform(X)\n",
"model_best = LinearRegression()\n",
"model_best.fit(X_poly_best, y)\n",
"\n",
"print(f\"Using polynomial degree: {best_degree}\")\n",
"\n",
"# Select a specific operating condition to visualize\n",
"# Using the same condition as earlier: -15A, -15A, 0° roll\n",
"test_condition = magDf.iloc[0:13].copy()\n",
"currL_val = test_condition['currL [A]'].iloc[0]\n",
"currR_val = test_condition['currR [A]'].iloc[0]\n",
"roll_val = test_condition['rollDeg [deg]'].iloc[0]\n",
"\n",
"# Create very fine grid for gap heights to see polynomial behavior between points\n",
"gap_very_fine = np.linspace(5, 30, 500) # 500 points for smooth curve\n",
"X_fine = pd.DataFrame({\n",
" 'currL [A]': [currL_val] * 500,\n",
" 'currR [A]': [currR_val] * 500,\n",
" 'rollDeg [deg]': [roll_val] * 500,\n",
" 'invGap': 1/gap_very_fine # Use inverse gap height for modeling\n",
"})\n",
"X_fine_poly = poly_best.transform(X_fine)\n",
"y_fine_pred = model_best.predict(X_fine_poly)\n",
"\n",
"# Extract actual Ansys data for this condition\n",
"gap_actual = test_condition['GapHeight [mm]'].values\n",
"force_actual = test_condition['YokeForce.Force_z [newton]'].values\n",
"\n",
"# Plot\n",
"fig, ax = plt.subplots(1, 1, figsize=(12, 7))\n",
"\n",
"ax.plot(gap_very_fine, y_fine_pred[:, 0], '-', linewidth=2.5, \n",
" label=f'Degree {best_degree} Polynomial Function', color='#2ca02c', alpha=0.8)\n",
"\n",
"# Plot actual Ansys data points\n",
"ax.scatter(gap_actual, force_actual, s=120, marker='o', \n",
" color='#d62728', edgecolors='black', linewidths=1.5,\n",
" label='Ansys Simulation Data', zorder=5)\n",
"\n",
"ax.set_ylabel('Force (N)', fontsize=13)\n",
"ax.set_title(f'Degree {best_degree} Polynomial vs Ansys Data (currL={currL_val}A, currR={currR_val}A, roll={roll_val}°)', \n",
" fontsize=14, fontweight='bold')\n",
"ax.legend(fontsize=12, loc='best')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Add vertical lines at data points to highlight gaps\n",
"for gap_point in gap_actual:\n",
" ax.axvline(gap_point, color='gray', linestyle=':', alpha=0.3, linewidth=0.8)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Check for excessive oscillations by looking at derivative\n",
"print(\"Checking for overfitting indicators:\")\n",
"print(f\"Number of actual data points in this condition: {len(gap_actual)}\")\n",
"\n",
"print(f\"Gap spacing: {np.diff(gap_actual).mean():.2f} mm average\")\n",
"print(f\"Polynomial curve resolution: 500 points over {gap_very_fine.max() - gap_very_fine.min():.1f} mm range\")"
]
},
{
"cell_type": "markdown",
"id": "8e4487e0",
"metadata": {},
"source": [
"### Compare Multiple Conditions Side-by-Side\n",
"\n",
"Let's check several different operating conditions to ensure consistent behavior."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 36,
2025-12-10 00:40:59 -06:00
"id": "74d6e98f",
"metadata": {},
"outputs": [
{
"data": {
2025-12-12 08:56:30 -06:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAS1CAYAAADjrFn4AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs/X1wW9l5J/h/L0iKeqFIEBT10t1qt8Bud2zHjg1SbsfxxC8i7XGcTe+4QfVuTXbjikfkuuza3XI2ZDSp2q3e3+wqZCZVuzX2xKR6Zu2ajackMjPOzqQzMdFx3uzYLRFuO7Zju5tQv+qFEkmQFCVRJHF/f8C4AkgCwn0u8ODg8Pup6mqBwME557kPL87hwbnXcV3XBRERERERERERERERkeFCtW4AERERERERERERERFRObioQUREREREREREREREdYGLGkREREREREREREREVBe4qEFERERERERERERERHWBixpERERERERERERERFQXuKhBRERERERERERERER1gYsaRERERERERERERERUF7ioQUREREREREREREREdYGLGkRERGS9dDoNx3G2/a+7uxvj4+O1bmJdKBbH9vZ29PX1IZlMit97fHwcjuMglUpVsMX6xsfH0d3dLYpFpWOQOz7Dw8P3fW17ezscx0F/f7+oLr9tNylOm6XTaQwODnox6erqwuDgINLpdFXq0xYk9tWSa1MuZ7u6utDf349EIqHelu7ubnR3d29pW7nxMjG+RERERLbhogYRERHtGL29vZiZmfH+m5iYQE9PDwYHB9HX11fr5qnI/cG2q6vL++Oh3z8c5sdxenoaZ8+eBZD9Y+Do6Gg1ml03ZmZmkEwmMT8/X+umeCYnJ0s+n0gk1P9gb2KcACCZTOLYsWNIJBIYGBjAxMQE4vE4zp8/b80fqk2KfTqdRl9fHwYHB9HT04OJiQmMjY0hHo8jkUhgYmKi1k30HS+T4ktERERkq8ZaN4CIiIhISzQaRTQaLXgcj8fR39+Pvr4+dHd3Y3p6uoYtrK5UKoW+vj6Ew2GMjIwgGo0ilUohEon4ep/NcYzFYojH4xgcHMTw8DB6e3sRi8Uq3fy6MDIygpGRkVo3wxOLxZBMJpFMJosek4mJCe91lTY5OYloNLqlbtPiBGQXNLq7u9Hb24uJiQmEw2EAQDwex+nTp3HixAl0d3djZmam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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1600x1200 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Visual inspection guide:\n",
"✓ Look for smooth curves between data points (good)\n",
"✗ Look for wild oscillations or wiggles between points (overfitting)\n",
"✓ Polynomial should interpolate naturally without excessive curvature\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [6]\n",
2025-12-10 00:40:59 -06:00
"# Select 4 different conditions to visualize\n",
"# Each condition has 13 gap height measurements (one complete parameter set)\n",
"condition_indices = [\n",
" (0, 13, '-15A, -15A, 0°'), # First condition\n",
" (13, 26, '-15A, -15A, 0.5°'), # Same currents, different roll\n",
" (91, 104, '-15A, -10A, 0°'), # Different right current\n",
" (182, 195, '-15A, -10A, 1°') # Different right current and roll\n",
"]\n",
"\n",
"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
"axes = axes.flatten()\n",
"\n",
"for idx, (start, end, label) in enumerate(condition_indices):\n",
" ax = axes[idx]\n",
" \n",
" # Get condition data\n",
" condition_data = magDf.iloc[start:end]\n",
" currL = condition_data['currL [A]'].iloc[0]\n",
" currR = condition_data['currR [A]'].iloc[0]\n",
" roll = condition_data['rollDeg [deg]'].iloc[0]\n",
" \n",
" # Create fine grid\n",
" gap_fine = np.linspace(5, 30, 500)\n",
" X_condition_fine = pd.DataFrame({\n",
" 'currL [A]': [currL] * 500,\n",
" 'currR [A]': [currR] * 500,\n",
" 'rollDeg [deg]': [roll] * 500,\n",
" 'invGap': 1/gap_fine # Use inverse gap height for modeling\n",
" })\n",
" \n",
" # Predict with best degree polynomial\n",
" X_condition_poly = poly_best.transform(X_condition_fine)\n",
" y_condition_pred = model_best.predict(X_condition_poly)\n",
" \n",
" # Plot polynomial curve\n",
" ax.plot(gap_fine, y_condition_pred[:, 0], '-', linewidth=2.5, \n",
" color='#2ca02c', alpha=0.8, label=f'Degree {best_degree} Polynomial')\n",
" \n",
" # Plot actual data\n",
" ax.scatter(condition_data['GapHeight [mm]'], \n",
" condition_data['YokeForce.Force_z [newton]'],\n",
" s=100, marker='o', color='#d62728', \n",
" edgecolors='black', linewidths=1.5,\n",
" label='Ansys Data', zorder=5)\n",
" \n",
" # Formatting\n",
" ax.set_xlabel('Gap Height (mm)', fontsize=11)\n",
" ax.set_ylabel('Force (N)', fontsize=11)\n",
" ax.set_title(f'currL={currL:.0f}A, currR={currR:.0f}A, roll={roll:.0f}°', \n",
" fontsize=12, fontweight='bold')\n",
" ax.legend(fontsize=10, loc='best')\n",
" ax.grid(True, alpha=0.3)\n",
" \n",
" # Add vertical lines at data points\n",
" for gap_point in condition_data['GapHeight [mm]'].values:\n",
" ax.axvline(gap_point, color='gray', linestyle=':', alpha=0.2, linewidth=0.8)\n",
"\n",
"plt.suptitle(f'Degree {best_degree} Polynomial: Multiple Operating Conditions', \n",
" fontsize=15, fontweight='bold', y=1.00)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(\"Visual inspection guide:\")\n",
"print(\"✓ Look for smooth curves between data points (good)\")\n",
"print(\"✗ Look for wild oscillations or wiggles between points (overfitting)\")\n",
"print(\"✓ Polynomial should interpolate naturally without excessive curvature\")"
]
},
{
"cell_type": "markdown",
"id": "81a46ab5",
"metadata": {},
"source": [
"### Find Worst-Performing Segment\n",
"\n",
"Let's identify which parameter set has the worst R² score to showcase model performance even in challenging cases."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 37,
2025-12-10 00:40:59 -06:00
"id": "35f4438e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Total segments analyzed: 637\n",
"\n",
"Worst performing segment:\n",
2026-02-11 17:33:18 -06:00
" Rows: 1261 to 1274\n",
" Parameters: currL=15A, currR=15A, roll=-3.0°\n",
2025-12-10 00:40:59 -06:00
" R² Score: 0.999302\n",
"\n",
"Best performing segment R²: 0.999992\n",
"Mean R² across all segments: 0.999934\n",
"Median R² across all segments: 0.999953\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [7]\n",
2025-12-10 00:40:59 -06:00
"# Calculate R² score for each 13-row segment (each parameter set)\n",
"num_segments = len(magDf) // 13\n",
"segment_scores = []\n",
"segment_info = []\n",
"\n",
"for i in range(num_segments):\n",
" start_idx = i * 13\n",
" end_idx = start_idx + 13\n",
" \n",
" # Get actual and predicted values for this segment\n",
" segment_actual = y.iloc[start_idx:end_idx, 0].values # Force only\n",
" segment_pred = model_best.predict(poly_best.transform(X.iloc[start_idx:end_idx]))[:, 0]\n",
" \n",
" # Calculate R² for this segment\n",
" segment_r2 = r2_score(segment_actual, segment_pred)\n",
" segment_scores.append(segment_r2)\n",
" \n",
" # Store segment info\n",
" segment_data = magDf.iloc[start_idx:end_idx]\n",
" currL = segment_data['currL [A]'].iloc[0]\n",
" currR = segment_data['currR [A]'].iloc[0]\n",
" roll = segment_data['rollDeg [deg]'].iloc[0]\n",
" segment_info.append({\n",
" 'start': start_idx,\n",
" 'end': end_idx,\n",
" 'currL': currL,\n",
" 'currR': currR,\n",
" 'roll': roll,\n",
" 'r2': segment_r2\n",
" })\n",
"\n",
"# Find worst performing segment\n",
"worst_idx = np.argmin(segment_scores)\n",
"worst_segment = segment_info[worst_idx]\n",
"\n",
"print(f\"Total segments analyzed: {num_segments}\")\n",
"print(f\"\\nWorst performing segment:\")\n",
"print(f\" Rows: {worst_segment['start']} to {worst_segment['end']}\")\n",
"print(f\" Parameters: currL={worst_segment['currL']:.0f}A, currR={worst_segment['currR']:.0f}A, roll={worst_segment['roll']:.1f}°\")\n",
"print(f\" R² Score: {worst_segment['r2']:.6f}\")\n",
"print(f\"\\nBest performing segment R²: {max(segment_scores):.6f}\")\n",
"print(f\"Mean R² across all segments: {np.mean(segment_scores):.6f}\")\n",
"print(f\"Median R² across all segments: {np.median(segment_scores):.6f}\")"
]
},
{
"cell_type": "markdown",
"id": "080f954b",
"metadata": {},
"source": [
"### Visualize Worst-Performing Segment\n",
"\n",
"Even our worst case shows excellent model performance!"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 38,
2025-12-10 00:40:59 -06:00
"id": "6ceaf2b2",
"metadata": {},
"outputs": [
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1200x700 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Even in the worst case, the model achieves R² = 0.999302\n",
"This demonstrates excellent interpolation performance across all operating conditions!\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [8]\n",
2025-12-10 00:40:59 -06:00
"# Get worst segment data\n",
"worst_data = magDf.iloc[worst_segment['start']:worst_segment['end']]\n",
"worst_currL = worst_segment['currL']\n",
"worst_currR = worst_segment['currR']\n",
"worst_roll = worst_segment['roll']\n",
"worst_r2 = worst_segment['r2']\n",
"\n",
"# Create fine grid for this worst segment\n",
"gap_fine_worst = np.linspace(5, 30, 500)\n",
"X_worst_fine = pd.DataFrame({\n",
" 'currL [A]': [worst_currL] * 500,\n",
" 'currR [A]': [worst_currR] * 500,\n",
" 'rollDeg [deg]': [worst_roll] * 500,\n",
" 'invGap': 1/gap_fine_worst\n",
"})\n",
"\n",
"# Get predictions\n",
"X_worst_poly = poly_best.transform(X_worst_fine)\n",
"y_worst_pred = model_best.predict(X_worst_poly)\n",
"\n",
"# Extract actual data\n",
"gap_worst_actual = worst_data['GapHeight [mm]'].values\n",
"force_worst_actual = worst_data['YokeForce.Force_z [newton]'].values\n",
"\n",
"# Create the plot\n",
"fig, ax = plt.subplots(1, 1, figsize=(12, 7))\n",
"\n",
"# Plot polynomial curve\n",
"ax.plot(gap_fine_worst, y_worst_pred[:, 0], '-', linewidth=2.5, \n",
" label=f'Degree {best_degree} Polynomial', color='#2ca02c', alpha=0.8)\n",
"\n",
"# Plot actual data points\n",
"ax.scatter(gap_worst_actual, force_worst_actual, s=120, marker='o', \n",
" color='#d62728', edgecolors='black', linewidths=1.5,\n",
" label='Ansys Data', zorder=5)\n",
"\n",
"# Formatting\n",
"ax.set_xlabel('Gap Height (mm)', fontsize=13)\n",
"ax.set_ylabel('Force (N)', fontsize=13)\n",
"ax.set_title(f'Worst Case Performance (R² = {worst_r2:.6f}) - currL={worst_currL:.0f}A, currR={worst_currR:.0f}A, roll={worst_roll:.1f}°', \n",
" fontsize=14, fontweight='bold')\n",
"ax.legend(fontsize=12, loc='best')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Add vertical lines at data points\n",
"for gap_point in gap_worst_actual:\n",
" ax.axvline(gap_point, color='gray', linestyle=':', alpha=0.3, linewidth=0.8)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Even in the worst case, the model achieves R² = {worst_r2:.6f}\")\n",
"print(f\"This demonstrates excellent interpolation performance across all operating conditions!\")"
]
},
{
"cell_type": "markdown",
"id": "a8f24086",
"metadata": {},
"source": [
"## Torque Prediction Analysis\n",
"\n",
"Now let's examine torque predictions. Torque is influenced by multiple variables:\n",
"- **Roll angle** (primary driver of torque asymmetry)\n",
"- **Current imbalance** (currL vs currR difference)\n",
"- **Gap height** (via invGap feature)\n",
"\n",
"We'll visualize torque accuracy by sweeping across roll angles first, then examining current variations."
]
},
{
"cell_type": "markdown",
"id": "d896f5a7",
"metadata": {},
"source": [
"### Overall Torque Prediction Accuracy\n",
"\n",
"First, let's check the overall R² score for torque predictions across all data."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 39,
2025-12-10 00:40:59 -06:00
"id": "c120048d",
"metadata": {},
"outputs": [
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1000x800 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Overall Torque R² Score: 0.999895\n",
"Overall Force R² Score: 0.999967\n",
"\n",
"Torque prediction is excellent!\n"
]
}
],
"source": [
"y_pred_full = model_best.predict(X_poly_best)\n",
"torque_actual_full = y.iloc[:, 1].values\n",
"torque_pred_full = y_pred_full[:, 1]\n",
"\n",
"torque_r2_overall = r2_score(torque_actual_full, torque_pred_full)\n",
"\n",
"fig, ax = plt.subplots(1, 1, figsize=(10, 8))\n",
"\n",
"ax.scatter(torque_actual_full, torque_pred_full, alpha=0.4, s=15, color='#1f77b4')\n",
"ax.plot([torque_actual_full.min(), torque_actual_full.max()], \n",
" [torque_actual_full.min(), torque_actual_full.max()], \n",
" 'r--', linewidth=2, label='Perfect Fit')\n",
"\n",
"ax.set_xlabel('Actual Torque (mN·m)', fontsize=13)\n",
"ax.set_ylabel('Predicted Torque (mN·m)', fontsize=13)\n",
"ax.set_title(f'Torque: Predicted vs Actual (R² = {torque_r2_overall:.6f})', \n",
" fontsize=14, fontweight='bold')\n",
"ax.legend(fontsize=12)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Overall Torque R² Score: {torque_r2_overall:.6f}\")\n",
"print(f\"Overall Force R² Score: {r2_score(y.iloc[:, 0].values, y_pred_full[:, 0]):.6f}\")\n",
"print(f\"\\nTorque prediction is {'excellent' if torque_r2_overall > 0.99 else 'good' if torque_r2_overall > 0.95 else 'moderate'}!\")"
]
},
{
"cell_type": "markdown",
"id": "1dd6a624",
"metadata": {},
"source": [
"### Torque vs Roll Angle (Fixed Currents, Multiple Gap Heights)\n",
"\n",
"Examine how torque varies with roll angle for different gap heights. Roll angle is the primary driver of torque asymmetry."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 40,
2025-12-10 00:40:59 -06:00
"id": "70b9b4e1",
"metadata": {},
"outputs": [
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1600x1200 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Showing torque variation with roll angle at 4 different gap heights.\n",
"Gap height affects the magnitude of torque but the relationship with roll angle remains consistent.\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [9]\n",
2025-12-10 00:40:59 -06:00
"# Select 4 different gap heights to visualize\n",
"# Using currL = -15A, currR = -15A configuration\n",
"gap_heights_to_plot = [8, 10, 15, 21] # mm\n",
"\n",
"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
"axes = axes.flatten()\n",
"\n",
"for idx, gap_height in enumerate(gap_heights_to_plot):\n",
" ax = axes[idx]\n",
" \n",
" # Fixed current configuration\n",
" currL = -15\n",
" currR = -15\n",
" \n",
" # Get actual data for this gap height\n",
" gap_data = magDf[(magDf['GapHeight [mm]'] == gap_height) & \n",
" (magDf['currL [A]'] == currL) & \n",
" (magDf['currR [A]'] == currR)]\n",
" \n",
" # Create fine grid for smooth curve across roll angles\n",
" roll_fine = np.linspace(-5, 5.0, 500)\n",
" X_condition_fine = pd.DataFrame({\n",
" 'currL [A]': [currL] * 500,\n",
" 'currR [A]': [currR] * 500,\n",
" 'rollDeg [deg]': roll_fine,\n",
" 'invGap': [1/gap_height] * 500\n",
" })\n",
" \n",
" # Predict with best degree polynomial\n",
" X_condition_poly = poly_best.transform(X_condition_fine)\n",
" y_condition_pred = model_best.predict(X_condition_poly)\n",
" \n",
" # Plot polynomial curve for TORQUE (index 1)\n",
" ax.plot(roll_fine, y_condition_pred[:, 1], '-', linewidth=2.5, \n",
" color='#ff7f0e', alpha=0.8, label=f'Degree {best_degree} Polynomial')\n",
" \n",
" # Plot actual torque data\n",
" ax.scatter(gap_data['rollDeg [deg]'], \n",
" gap_data['YokeTorque.Torque [mNewtonMeter]'],\n",
" s=100, marker='o', color='#9467bd', \n",
" edgecolors='black', linewidths=1.5,\n",
" label='Ansys Data', zorder=5)\n",
" \n",
" # Formatting\n",
" ax.set_xlabel('Roll Angle (deg)', fontsize=11)\n",
" ax.set_ylabel('Torque (mN·m)', fontsize=11)\n",
" ax.set_title(f'Gap Height = {gap_height} mm (currL={currL:.0f}A, currR={currR:.0f}A)', \n",
" fontsize=12, fontweight='bold')\n",
" ax.legend(fontsize=10, loc='best')\n",
" ax.grid(True, alpha=0.3)\n",
" \n",
" # Add vertical lines at actual roll angle data points\n",
" for roll_point in gap_data['rollDeg [deg]'].unique():\n",
" ax.axvline(roll_point, color='gray', linestyle=':', alpha=0.2, linewidth=0.8)\n",
"\n",
"plt.suptitle(f'Torque vs Roll Angle at Different Gap Heights (Degree {best_degree} Polynomial)', \n",
" fontsize=15, fontweight='bold', y=1.00)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Showing torque variation with roll angle at {len(gap_heights_to_plot)} different gap heights.\")\n",
"print(\"Gap height affects the magnitude of torque but the relationship with roll angle remains consistent.\")"
]
},
{
"cell_type": "markdown",
"id": "02d02793",
"metadata": {},
"source": [
"### Torque vs Current Imbalance (Fixed Roll Angle, Multiple Gap Heights)\n",
"\n",
"Now examine how torque varies with current imbalance at different gap heights for a fixed roll angle."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 41,
2025-12-10 00:40:59 -06:00
"id": "3b8d7f6a",
"metadata": {},
"outputs": [
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAS1CAYAAADjrFn4AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xt0W9d9J/rvAV8iKVIgJephS7YJ+iHbciKDpGTHeQtM4yhppgkotZ2mcu2QnFSePnxbcjQz697le9eqSjbTdlp7OqSSRu7kJZKJ7SRWnBBuEidxLJGE6Vi25Qchv2VLFgmRFJ/gOfeP4wMTxIMgCfz2AfD9rMVlC+cA2PxiYxMb++y9NcMwDBAREREREREREREREdmcQ3UBiIiIiIiIiIiIiIiIksFBDSIiIiIiIiIiIiIiyggc1CAiIiIiIiIiIiIioozAQQ0iIiIiIiIiIiIiIsoIHNQgIiIiIiIiIiIiIqKMwEENIiIiIiIiIiIiIiLKCBzUICIiIiIiIiIiIiKijMBBDSIiIiIiIiIiIiIiyggc1CAiIiIiIiIiIiIioozAQQ0iIiIiIiJaNp/PB7/fr7oYRDmJ7z8iIsplHNQgIqKs5/P5UFFRsayfYDCoutgZr6urC7W1tdA0DZqmoaamBo2NjfD5fKqLJi4QCEDTNDQ2Nqb0cbu6uqBpGgKBgC0fLxsFg8FwnV78U1tbi66urrj3td4Tsb6E6u3tRU1NDTRNQ0VFRfj2lpYWVFRUQNM0tLS0pOV3Um1hW1FRUYHa2lq0tLQoq4fJvF+t90pvb++yHjtRHUjmvpqm2ebvk8/nQ0tLC1wul+qiREhXe5sNrDpUW1ub0seN1y5WVFSgoaFh2e+ThWL9XeLfKpPL5cKePXtyPgciIspN+aoLQERElG51dXVob2+Pur2lpQVOpzPmMafTKVCy7BQMBsODF83NzTh06BBGRkYwPDyMrq4uVFZWwuPxqC4m0ap4PB50dnaG/+33+9HX14eWlhb09PSgr68v6j7Dw8Pw+/0YGRmJuL23txeNjY1ob2+H2+0Of0HV0NCAkZERHDlyJGvbpIaGBvh8Pni93vCgzeDgILq7u9Hd3Y3R0VHFJUyteHUg0wQCATQ2NqKnpydr66ZKwWAQPp8PbW1tGBwcjMrYajNi6ezsRHNzc8xjPT09AMz2KhAIpHxAanG7GAgE0NPTg8bGRni93vDzZ5uWlhZ0d3cjGAzC6/Um3WYn+zr6/X60tbVhZGQE+/fvR2trKwBzUOPIkSOora3FmTNn+F4kIqKcwkENIiLKek6nM2YH37rCNF7nn1amtrYWgUAAfX19UYMX7e3tGX1FYW9vL1wuF9xut+qiUAqs5vV0uVwRXwi6XC54vV40NjaioaEBtbW1GBwcjLhPe3t7zEHUw4cPo7m5OfxFFWB+Gejz+TA4OGjr+raaDGtqasJfenq93ohj7e3tCWe9ZKp4dSDV0t1WtbS0wOPxcIA6xQKBAGpqagCYn12WmpUTa1Ap3kCFNVDS3t6OtrY29Pb2RrQ5qRCrXfR4POF2saOjI+XPqVptbS2CwSAOHToEl8uFw4cPo7q6elkDsku9jm1tbeFzGhoa4PV6w8e9Xi8OHz6Mtra2iAElIiKibMflp4iIiChl2tra4g5oWOy2VMlyNDU14dixY6qLQSmSjtfTulLZ7/cnveSK3++P+kLLWp7I7lferjTDjo4OBAIBdHZ2Rg1oAObvnW1ffkpKZ1tlDbhl63JoKrlcLgwPD8MwjKQuuLAGlhb+xPsbaw0Stra2wul0in4BbpUr2/5+dnV1hWfptba2wuv14rHHHkMwGERbW1vSj7PU67jUzK5Dhw6hq6vLNkvTERERSeCgBhEREaVEMBhER0dH+MpMolzV3NwMp9O5rC+1cs3hw4fjzqIje7NmmrCdT490DfwfO3YsPIC4b98+BAIB8ZmT2fale09PT9QAhNWurWYfkcWOHDmCPXv2oKamBg0NDVF1xHpds3F2GxERUTwc1CAiIoojGAyipaUFNTU1qKioQGNjY9QXAL29veFNfXt7e8Mb3FoCgQAaGhrCm2X6fD7U1tZGbNIZb8PLxedZZWpsbERFRQVqamqS/tK0ra0t7gaz1lI5C8u7cOPjjo6OpJ6ju7s7/FzJSvZ3T5TzUq/BUplZ91/8ei98jMbGxnB+HR0d4XxW+wXC4ue2Nqq3ntvv94c3UK6pqYn7JUkgEIhbdiuDtra28EbUNTU1Sb+uyd43mRwXss6J95grrevJlDddr+dC1peGC99zi+u79dwL/99aesZi/R4Lf4dk67T1/3Z7T/h8vvByLcuRbF202tlAIBA+32rD022pbOO1eQvf69aG89bjNDQ0RJw7MjKy4tdlNW28xdoDJZFE72+Jdj+XBIPBJQcnAoEA/H4/9u/fDwDh94LUbI3e3t7w36mFkvmcZWc+ny/mEm9W+7OcQZxEr6Pb7cbg4CCGh4fjzmBzu90x93IiIiLKVhzUICIiiiEYDKK6ujq8/vSRI0cQDAZRW1sbXhYGML9csr48amxsRGVlZfgLKGttbJ/Ph0OHDqGxsRFtbW0R91+OQCCA6upqBAIBHDlyBG1tbejq6krqy17riwxr4GHh7+nz+cLHrS+U+vr6wlcgJttJHh4eBpCeq0wT5bzUa7BUZtb9a2trwxvHezwedHV1hb+Ia29vD++P4PV6MTg4iMHBQezbty8lv5f13EeOHAk/d0NDAxobG3Ho0KHwF0+NjY0xvyRpbGyMKvvCL3C7u7vDG41am1m3tbUlVXeSvW8yOQJmnaupqUF3d3d4U21rTXDrvbGaup5MedP1ei5kDU4MDAzEPae5uTmqHIvfb52dnRgcHAzPaFhOnbbre8J6nZe730OyddH6Atdqz9ra2lBXV5dwU95YgsEg/H5/zJ/F+6VYz7uSemtlXVdXh8HBQXg8HrS0tKC+vh6PPfZY1B4cq3ldVtPGW2UNBAKor6+Pe3yp93eyVtru55KFA2gVFRXw+Xwxz7MGxK3ZNdZ/UzmbAHj/vWf9WO+5xsZGeDyeiC/kk/2cZVfW3+L169dHHausrASApAdokn0dE6mrq0v494aIiCjrGERERDkKgOF2u2Me83q9hsvlirrd7XZH3Kezs9MAYAAw+vr6Is71eDwGAGN4eDh82/DwcNTzWo+x8LxYz+XxeAyn0xlxTl9fn5Hsn3OXy2V4PJ6I2xY+9+DgYMxyJKu5uXnZ90/2d0+U81KvwVKZWfdvbm6OOA9AVF5Op9NobW1N+vezWK+71+uNWfbFz+10Og0AxuDgYFS5Ozs7o+6/+PdO5rXwer1RdSfe67Gc+y6VY3Nzs+F0Oo3R0dG4j7/aup5MeQ1jZa/n6OhozN9zsZ6enriv1+J8Yz2edf+FdcAwllen7fqeWElbEU+iutjT0xNxe6w2ORbr/ZrMz8LnWE62C8vQ2toadT+XyxWV/2pfl9W28QsfY3G2lmTe3+lu9+O1t8vV3NwcLlOyPwvf76vV2tpqAIiZpdU+uFwuY3Bw0BgdHQ2/Fxa3GYZh1qfFn3es90Os85fLahdj/bhcrpj1ZbmfsxbWl2T/VlnS8Vpa9SzWedb7fnHdXGy5r2Mi7e3tcesLERFRNuJMDSIiokWCwSB6e3tjXl3b3t4evgJxodbW1qj1xX0+H5qbmyNmLrhcrhXNZLBmVDQ3NyMYDIZ/6urq4HQ6k7qqz+v1hpd9sXR2dsLtdsPlcoU3JG5vb1/RutfW/dO5dESsnOMdW25mi6/gdrvdS27OmSqLn9t6PRZeyW7Vm1ivzeI6ZdXdRFfhWldar+S1TnTfpXLs6uoK7zkRSyrq+nLKmy7W75zKmUvLzcau7wnrtY/1evh8voifpSR6bRfPBLFmPCR7dbrX64VhGDF/Fi/bs5p66/f7UVdXF3Gby+WK25au9HVZbRsPLF2vl3p/r8Ry2v1UsmZJLedHao8Yl8sFr9eLvr4+uN1uOJ1O9PT0AIheAtLaO8OakWlJxxJUzc3NEe8T62/Z4uXKVvI5azXs+lou53VcivWek/rcQkREpBoHNYiIiBaxpu8
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1600x1200 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Showing torque variation with current imbalance at 4 different gap heights.\n",
"All data uses currL = -15A with varying currR to create different imbalances.\n",
"Current imbalance (currR - currL) affects torque magnitude and direction.\n",
"Positive imbalance (currR > currL) and negative imbalance (currR < currL) produce opposite torques.\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was written by AI - see [10]\n",
2025-12-10 00:40:59 -06:00
"# Select 4 different gap heights to visualize\n",
"# Using roll = 0.5 degrees (small but non-zero torque)\n",
"gap_heights_to_plot = [8, 10, 15, 21] # mm\n",
"roll_target = 0.5\n",
"\n",
"# Use a fixed reference current for currL\n",
"currL_ref = -15\n",
"\n",
"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
"axes = axes.flatten()\n",
"\n",
"for idx, gap_height in enumerate(gap_heights_to_plot):\n",
" ax = axes[idx]\n",
" \n",
" # Get actual data for this gap height, roll angle, AND fixed currL\n",
" gap_roll_data = magDf[(magDf['GapHeight [mm]'] == gap_height) & \n",
" (magDf['rollDeg [deg]'] == roll_target) &\n",
" (magDf['currL [A]'] == currL_ref)]\n",
" \n",
" # Calculate imbalance for actual data points (all have same currL now)\n",
" actual_imbalances = []\n",
" actual_torques = []\n",
" for _, row in gap_roll_data.iterrows():\n",
" imbalance = row['currR [A]'] - row['currL [A]']\n",
" torque = row['YokeTorque.Torque [mNewtonMeter]']\n",
" actual_imbalances.append(imbalance)\n",
" actual_torques.append(torque)\n",
" \n",
" # Create fine grid for smooth curve across current imbalances\n",
" # Range from -10A to +10A imbalance (currR - currL)\n",
" imbalance_fine = np.linspace(0, 35, 500)\n",
" \n",
" # Vary currR while keeping currL fixed\n",
" currR_fine = currL_ref + imbalance_fine\n",
" \n",
" X_condition_fine = pd.DataFrame({\n",
" 'currL [A]': [currL_ref] * 500,\n",
" 'currR [A]': currR_fine,\n",
" 'rollDeg [deg]': [roll_target] * 500,\n",
" 'invGap': [1/gap_height] * 500\n",
" })\n",
" \n",
" # Predict with best degree polynomial\n",
" X_condition_poly = poly_best.transform(X_condition_fine)\n",
" y_condition_pred = model_best.predict(X_condition_poly)\n",
" \n",
" # Plot polynomial curve for TORQUE (index 1)\n",
" ax.plot(imbalance_fine, y_condition_pred[:, 1], '-', linewidth=2.5, \n",
" color='#ff7f0e', alpha=0.8, label=f'Degree {best_degree} Polynomial')\n",
" \n",
" # Plot actual torque data (now only one point per imbalance value)\n",
" ax.scatter(actual_imbalances, actual_torques,\n",
" s=100, marker='o', color='#9467bd', \n",
" edgecolors='black', linewidths=1.5,\n",
" label='Ansys Data', zorder=5)\n",
" \n",
" # Formatting\n",
" ax.set_xlabel('Current Imbalance (currR - currL) [A]', fontsize=11)\n",
" ax.set_ylabel('Torque (mN·m)', fontsize=11)\n",
" ax.set_title(f'Gap Height = {gap_height} mm (currL = {currL_ref}A, roll = {roll_target}°)', \n",
" fontsize=12, fontweight='bold')\n",
" ax.legend(fontsize=10, loc='best')\n",
" ax.grid(True, alpha=0.3)\n",
" ax.axhline(0, color='black', linestyle='-', linewidth=0.5, alpha=0.3)\n",
" ax.axvline(0, color='black', linestyle='-', linewidth=0.5, alpha=0.3)\n",
" \n",
" # Add vertical lines at actual imbalance data points\n",
" for imb in set(actual_imbalances):\n",
" ax.axvline(imb, color='gray', linestyle=':', alpha=0.2, linewidth=0.8)\n",
"\n",
"plt.suptitle(f'Torque vs Current Imbalance at Different Gap Heights (currL = {currL_ref}A, Roll = {roll_target}°)', \n",
" fontsize=15, fontweight='bold', y=1.00)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Showing torque variation with current imbalance at {len(gap_heights_to_plot)} different gap heights.\")\n",
"print(f\"All data uses currL = {currL_ref}A with varying currR to create different imbalances.\")\n",
"print(f\"Current imbalance (currR - currL) affects torque magnitude and direction.\")\n",
"print(f\"Positive imbalance (currR > currL) and negative imbalance (currR < currL) produce opposite torques.\")"
]
},
{
"cell_type": "markdown",
"id": "d71ac3cc",
"metadata": {},
"source": [
"### Torque Segment-Wise Performance\n",
"\n",
"Calculate R² scores for torque predictions across all segments, similar to what we did for force."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 42,
2025-12-10 00:40:59 -06:00
"id": "902343b2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Torque Prediction Analysis (across 637 segments):\n",
"\n",
"Best R² Score: 0.999964\n",
" Parameters: currL=15A, currR=-15A, roll=-1.5°\n",
"\n",
2026-02-11 17:33:18 -06:00
"Worst R² Score: -0.638044\n",
2025-12-10 00:40:59 -06:00
" Parameters: currL=-10A, currR=-10A, roll=0.0°\n",
"\n",
"Mean R²: 0.981564\n",
"Median R²: 0.999392\n",
"Std Dev: 0.124378\n"
]
},
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1000x600 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# Following generated by AI - see [11]\n",
2025-12-10 00:40:59 -06:00
"# Calculate R² score for torque in each 13-row segment\n",
"torque_segment_scores = []\n",
"torque_segment_info = []\n",
"\n",
"for i in range(num_segments):\n",
" start_idx = i * 13\n",
" end_idx = start_idx + 13\n",
" \n",
" # Get actual and predicted torque values for this segment\n",
" segment_actual_torque = y.iloc[start_idx:end_idx, 1].values # Torque column\n",
" segment_pred_torque = model_best.predict(poly_best.transform(X.iloc[start_idx:end_idx]))[:, 1]\n",
" \n",
" # Calculate R² for this segment\n",
" segment_r2_torque = r2_score(segment_actual_torque, segment_pred_torque)\n",
" torque_segment_scores.append(segment_r2_torque)\n",
" \n",
" # Store segment info\n",
" segment_data = magDf.iloc[start_idx:end_idx]\n",
" currL = segment_data['currL [A]'].iloc[0]\n",
" currR = segment_data['currR [A]'].iloc[0]\n",
" roll = segment_data['rollDeg [deg]'].iloc[0]\n",
" torque_segment_info.append({\n",
" 'start': start_idx,\n",
" 'end': end_idx,\n",
" 'currL': currL,\n",
" 'currR': currR,\n",
" 'roll': roll,\n",
" 'r2': segment_r2_torque\n",
" })\n",
"\n",
"# Find worst and best performing segments for torque\n",
"worst_torque_idx = np.argmin(torque_segment_scores)\n",
"best_torque_idx = np.argmax(torque_segment_scores)\n",
"worst_torque_segment = torque_segment_info[worst_torque_idx]\n",
"best_torque_segment = torque_segment_info[best_torque_idx]\n",
"\n",
"print(f\"Torque Prediction Analysis (across {num_segments} segments):\")\n",
"print(f\"\\nBest R² Score: {max(torque_segment_scores):.6f}\")\n",
"print(f\" Parameters: currL={best_torque_segment['currL']:.0f}A, currR={best_torque_segment['currR']:.0f}A, roll={best_torque_segment['roll']:.1f}°\")\n",
"print(f\"\\nWorst R² Score: {min(torque_segment_scores):.6f}\")\n",
"print(f\" Parameters: currL={worst_torque_segment['currL']:.0f}A, currR={worst_torque_segment['currR']:.0f}A, roll={worst_torque_segment['roll']:.1f}°\")\n",
"print(f\"\\nMean R²: {np.mean(torque_segment_scores):.6f}\")\n",
"print(f\"Median R²: {np.median(torque_segment_scores):.6f}\")\n",
"print(f\"Std Dev: {np.std(torque_segment_scores):.6f}\")\n",
"\n",
"# Create histogram of torque R² scores\n",
"fig, ax = plt.subplots(1, 1, figsize=(10, 6))\n",
"ax.hist(torque_segment_scores, bins=30, edgecolor='black', alpha=0.7, color='#9467bd')\n",
"ax.axvline(np.mean(torque_segment_scores), color='red', linestyle='--', linewidth=2, label=f'Mean = {np.mean(torque_segment_scores):.6f}')\n",
"ax.axvline(np.median(torque_segment_scores), color='orange', linestyle='--', linewidth=2, label=f'Median = {np.median(torque_segment_scores):.6f}')\n",
"ax.set_xlabel('R² Score', fontsize=12)\n",
"ax.set_ylabel('Number of Segments', fontsize=12)\n",
"ax.set_title('Distribution of Torque R² Scores Across All Parameter Segments', fontsize=14, fontweight='bold')\n",
"ax.legend(fontsize=11)\n",
"ax.grid(True, alpha=0.3, axis='y')\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "5bffc9a9",
"metadata": {},
"source": [
"### Worst-Case Torque Prediction Visualization\n",
"\n",
"Let's visualize the worst-performing torque segment to assess model robustness."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 43,
2025-12-10 00:40:59 -06:00
"id": "9d5580f3",
"metadata": {},
"outputs": [
{
"data": {
2026-02-11 17:33:18 -06:00
"image/png": "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
2025-12-10 00:40:59 -06:00
"text/plain": [
"<Figure size 1200x700 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
2026-02-11 17:33:18 -06:00
"Worst-case torque prediction achieves R² = -0.638044\n"
2025-12-10 00:40:59 -06:00
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# Following generated by AI - see [11]\n",
2025-12-10 00:40:59 -06:00
"# Get worst torque segment data\n",
"worst_torque_data = magDf.iloc[worst_torque_segment['start']:worst_torque_segment['end']]\n",
"worst_torque_currL = worst_torque_segment['currL']\n",
"worst_torque_currR = worst_torque_segment['currR']\n",
"worst_torque_roll = worst_torque_segment['roll']\n",
"worst_torque_r2 = worst_torque_segment['r2']\n",
"\n",
"# Create fine grid for this worst segment\n",
"gap_fine_worst_torque = np.linspace(2, 30, 500)\n",
"X_worst_torque_fine = pd.DataFrame({\n",
" 'currL [A]': [worst_torque_currL] * 500,\n",
" 'currR [A]': [worst_torque_currR] * 500,\n",
" 'rollDeg [deg]': [worst_torque_roll] * 500,\n",
" 'invGap': 1/gap_fine_worst_torque\n",
"})\n",
"\n",
"# Get predictions\n",
"X_worst_torque_poly = poly_best.transform(X_worst_torque_fine)\n",
"y_worst_torque_pred = model_best.predict(X_worst_torque_poly)\n",
"\n",
"# Extract actual data\n",
"gap_worst_torque_actual = worst_torque_data['GapHeight [mm]'].values\n",
"torque_worst_actual = worst_torque_data['YokeTorque.Torque [mNewtonMeter]'].values\n",
"\n",
"# Create the plot\n",
"fig, ax = plt.subplots(1, 1, figsize=(12, 7))\n",
"\n",
"# Plot polynomial curve for TORQUE\n",
"ax.plot(gap_fine_worst_torque, y_worst_torque_pred[:, 1], '-', linewidth=2.5, \n",
" label=f'Degree {best_degree} Polynomial', color='#ff7f0e', alpha=0.8)\n",
"\n",
"# Plot actual data points\n",
"ax.scatter(gap_worst_torque_actual, torque_worst_actual, s=120, marker='o', \n",
" color='#9467bd', edgecolors='black', linewidths=1.5,\n",
" label='Ansys Data', zorder=5)\n",
"\n",
"# Formatting\n",
"ax.set_xlabel('Gap Height (mm)', fontsize=13)\n",
"ax.set_ylabel('Torque (mN·m)', fontsize=13)\n",
"ax.set_title(f'Worst Torque Case (R² = {worst_torque_r2:.6f}) - currL={worst_torque_currL:.0f}A, currR={worst_torque_currR:.0f}A, roll={worst_torque_roll:.1f}°', \n",
" fontsize=14, fontweight='bold')\n",
"ax.legend(fontsize=12, loc='best')\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# Add vertical lines at data points\n",
"for gap_point in gap_worst_torque_actual:\n",
" ax.axvline(gap_point, color='gray', linestyle=':', alpha=0.3, linewidth=0.8)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Worst-case torque prediction achieves R² = {worst_torque_r2:.6f}\")"
]
},
{
"cell_type": "markdown",
"id": "c63aca1e",
"metadata": {},
"source": [
"### We can see here that at roll angle and current differential of 0 (when torque will be very close to 0):\n",
"\n",
"There is significant overfitting to noise, but within the operating range of 6 - 24 mm, there is never more variation than there would be with simple linear interpolation.\n",
"\n",
"Hopefully this means we are ok"
]
},
{
"cell_type": "markdown",
"id": "1e1c384f",
"metadata": {},
"source": [
"### Export Model Summary\n",
"\n",
"Display key model information before exporting."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 44,
2025-12-10 00:40:59 -06:00
"id": "778f58df",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"============================================================\n",
"MODEL EXPORT SUMMARY\n",
"============================================================\n",
"\n",
"Model Type: Polynomial Regression\n",
"Polynomial Degree: 6\n",
"Number of Features: 4\n",
"Number of Polynomial Terms: 210\n",
"\n",
"Input Features:\n",
" - currL [A]\n",
" - currR [A]\n",
" - rollDeg [deg]\n",
" - invGap (1/GapHeight)\n",
"\n",
"Outputs:\n",
" - YokeForce.Force_z [newton]\n",
" - YokeTorque.Torque [mNewtonMeter]\n",
"\n",
"Model Performance:\n",
" Force R² Score: 0.999967\n",
" Torque R² Score: 0.999895\n",
"\n",
"Model Coefficients Shape:\n",
" Force coefficients: (210,)\n",
" Torque coefficients: (210,)\n",
"============================================================\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was generated by AI - see [12]\n",
2025-12-10 00:40:59 -06:00
"print(\"=\" * 60)\n",
"print(\"MODEL EXPORT SUMMARY\")\n",
"print(\"=\" * 60)\n",
"print(f\"\\nModel Type: Polynomial Regression\")\n",
"print(f\"Polynomial Degree: {best_degree}\")\n",
"print(f\"Number of Features: {poly_best.n_features_in_}\")\n",
"print(f\"Number of Polynomial Terms: {poly_best.n_output_features_}\")\n",
"print(f\"\\nInput Features:\")\n",
"print(\" - currL [A]\")\n",
"print(\" - currR [A]\")\n",
"print(\" - rollDeg [deg]\")\n",
"print(\" - invGap (1/GapHeight)\")\n",
"print(f\"\\nOutputs:\")\n",
"print(\" - YokeForce.Force_z [newton]\")\n",
"print(\" - YokeTorque.Torque [mNewtonMeter]\")\n",
"print(f\"\\nModel Performance:\")\n",
"print(f\" Force R² Score: {r2_score(y.iloc[:, 0].values, y_pred_full[:, 0]):.6f}\")\n",
"print(f\" Torque R² Score: {torque_r2_overall:.6f}\")\n",
"print(f\"\\nModel Coefficients Shape:\")\n",
"print(f\" Force coefficients: {model_best.coef_[0].shape}\")\n",
"print(f\" Torque coefficients: {model_best.coef_[1].shape}\")\n",
"print(\"=\" * 60)"
]
},
{
"cell_type": "markdown",
2026-02-11 17:33:18 -06:00
"id": "gf4ju6ai5xg",
2025-12-10 00:40:59 -06:00
"metadata": {},
"source": [
2026-02-11 17:33:18 -06:00
"### Extract Explicit Polynomial Equations\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"Since `PolynomialFeatures` + `LinearRegression` is literally a polynomial, we can extract the full closed-form equations for force and torque as functions of the four input variables."
2025-12-10 00:40:59 -06:00
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 45,
"id": "t1olo3k5j7q",
2025-12-10 00:40:59 -06:00
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
2026-02-11 17:33:18 -06:00
"================================================================================\n",
"FORCE POLYNOMIAL — Top 20 terms by |coefficient|\n",
"================================================================================\n",
" Constant: -1.383776e+01\n",
" -5.340149e+03 * invGap^3\n",
" -2.235411e+03 * invGap^4\n",
" +1.376722e+03 * invGap^2\n",
" +9.205603e+02 * rollDeg [deg]^2 * invGap^3\n",
" +6.340440e+02 * invGap\n",
" -5.962319e+02 * invGap^5\n",
" +4.005882e+02 * rollDeg [deg]^2 * invGap^4\n",
" +3.681499e+02 * currR [A]^2 * invGap^4\n",
" +3.595010e+02 * currL [A]^2 * invGap^4\n",
" +3.592905e+02 * currR [A] * rollDeg [deg] * invGap^4\n",
" -3.592904e+02 * currL [A] * rollDeg [deg] * invGap^4\n",
" -2.356399e+02 * rollDeg [deg]^2 * invGap^2\n",
" -1.492356e+02 * currR [A]^2 * invGap^3\n",
" -1.460868e+02 * currL [A]^2 * invGap^3\n",
" -1.315156e+02 * invGap^6\n",
" -1.266494e+02 * currR [A] * rollDeg [deg] * invGap^3\n",
" +1.266493e+02 * currL [A] * rollDeg [deg] * invGap^3\n",
" +1.251491e+02 * currR [A] * invGap^3\n",
" +1.234347e+02 * currL [A] * invGap^3\n",
" +5.262747e+01 * currR [A] * invGap^4\n",
" ... (206 non-zero terms total)\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"================================================================================\n",
"TORQUE POLYNOMIAL — Top 20 terms by |coefficient|\n",
"================================================================================\n",
" Constant: -2.560964e+00\n",
" -9.101655e+04 * rollDeg [deg] * invGap^3\n",
" +4.700717e+04 * rollDeg [deg] * invGap^2\n",
" -3.973067e+04 * rollDeg [deg] * invGap^4\n",
" +1.915057e+04 * rollDeg [deg]^3 * invGap^3\n",
" -1.794691e+04 * currR [A] * rollDeg [deg] * invGap^4\n",
" -1.794691e+04 * currL [A] * rollDeg [deg] * invGap^4\n",
" -1.076582e+04 * rollDeg [deg] * invGap^5\n",
" +8.596591e+03 * currL [A] * invGap^3\n",
" -8.155426e+03 * currR [A] * invGap^3\n",
" +5.456996e+03 * currR [A] * rollDeg [deg] * invGap^3\n",
" +5.456996e+03 * currL [A] * rollDeg [deg] * invGap^3\n",
" -3.769444e+03 * rollDeg [deg]^3 * invGap^2\n",
" +3.652133e+03 * currL [A] * invGap^4\n",
" -3.464337e+03 * currR [A] * invGap^4\n",
" -2.679629e+03 * currL [A] * invGap^2\n",
" +2.538618e+03 * currR [A] * invGap^2\n",
" +1.385161e+03 * currL [A]^2 * invGap^4\n",
" -1.269193e+03 * currL [A] * rollDeg [deg]^2 * invGap^3\n",
" +1.235618e+03 * currR [A] * rollDeg [deg]^2 * invGap^3\n",
" -1.145728e+03 * currR [A]^2 * invGap^4\n",
" ... (209 non-zero terms total)\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"Full equations (all 210 terms) saved to: polynomial_equations.txt\n"
2025-12-10 00:40:59 -06:00
]
}
],
"source": [
2026-02-11 17:33:18 -06:00
"import re\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"# Extract the explicit polynomial equations from the fitted model\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"feature_names_raw = poly_best.get_feature_names_out()\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"# sklearn already uses the real column names from our DataFrame.\n",
"# Term separators are spaces followed by a lowercase letter (start of next variable),\n",
"# while spaces inside names like \"currL [A]\" are followed by \"[\".\n",
"def prettify_term(raw_name):\n",
" if raw_name == '1':\n",
" return '1'\n",
" return re.sub(r' (?=[a-z])', ' * ', raw_name)\n",
"\n",
"pretty_names = [prettify_term(n) for n in feature_names_raw]\n",
"\n",
"# The full prediction is: y = intercept + sum(coef_i * feature_i)\n",
"# where feature_0 = 1 (bias from PolynomialFeatures), so the effective\n",
"# constant term = intercept + coef_for_bias_column.\n",
"\n",
"force_coefs = model_best.coef_[0]\n",
"torque_coefs = model_best.coef_[1]\n",
"force_intercept = model_best.intercept_[0]\n",
"torque_intercept = model_best.intercept_[1]\n",
"\n",
"# Combine intercept with the '1' term coefficient\n",
"force_constant = force_intercept + force_coefs[0]\n",
"torque_constant = torque_intercept + torque_coefs[0]\n",
"\n",
"def build_equation_string(coefs, intercept, names, threshold=1e-10):\n",
" \"\"\"Build a readable polynomial equation string, skipping near-zero terms.\"\"\"\n",
" constant = intercept + coefs[0] # combine intercept with bias column\n",
" terms = [f\"{constant:.10e}\"]\n",
" for name, c in zip(names[1:], coefs[1:]): # skip the '1' term\n",
" if abs(c) > threshold:\n",
" terms.append(f\"({c:+.10e}) * {name}\")\n",
" return terms\n",
"\n",
"# --- Print top terms by magnitude for quick overview ---\n",
"print(\"=\" * 80)\n",
"print(\"FORCE POLYNOMIAL — Top 20 terms by |coefficient|\")\n",
"print(\"=\" * 80)\n",
"\n",
"# Pair names with coefficients (skip bias, fold into constant)\n",
"force_terms = [(pretty_names[i], force_coefs[i]) for i in range(1, len(pretty_names))]\n",
"force_terms_sorted = sorted(force_terms, key=lambda x: abs(x[1]), reverse=True)\n",
"\n",
"print(f\" Constant: {force_constant:.6e}\")\n",
"for name, c in force_terms_sorted[:20]:\n",
" print(f\" {c:+.6e} * {name}\")\n",
"print(f\" ... ({len([c for _, c in force_terms if abs(c) > 1e-10])} non-zero terms total)\")\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"print()\n",
"print(\"=\" * 80)\n",
"print(\"TORQUE POLYNOMIAL — Top 20 terms by |coefficient|\")\n",
"print(\"=\" * 80)\n",
"\n",
"torque_terms = [(pretty_names[i], torque_coefs[i]) for i in range(1, len(pretty_names))]\n",
"torque_terms_sorted = sorted(torque_terms, key=lambda x: abs(x[1]), reverse=True)\n",
"\n",
"print(f\" Constant: {torque_constant:.6e}\")\n",
"for name, c in torque_terms_sorted[:20]:\n",
" print(f\" {c:+.6e} * {name}\")\n",
"print(f\" ... ({len([c for _, c in torque_terms if abs(c) > 1e-10])} non-zero terms total)\")\n",
"\n",
"# --- Save complete equations to text file ---\n",
"with open('polynomial_equations.txt', 'w') as f:\n",
" f.write(\"Magnetic Levitation Force & Torque — Explicit Polynomial Equations\\n\")\n",
" f.write(f\"Polynomial Degree: {best_degree}\\n\")\n",
" f.write(f\"Variables:\\n\")\n",
" f.write(f\" currL [A] = Left coil current\\n\")\n",
" f.write(f\" currR [A] = Right coil current\\n\")\n",
" f.write(f\" rollDeg [deg] = Roll angle\\n\")\n",
" f.write(f\" invGap = 1 / GapHeight [1/mm]\\n\")\n",
" f.write(f\"\\n{'='*80}\\n\")\n",
" f.write(f\"FORCE [N] = \\n\")\n",
" force_eq = build_equation_string(force_coefs, force_intercept, pretty_names)\n",
" for i, term in enumerate(force_eq):\n",
" prefix = \" \" if i == 0 else \" + \"\n",
" f.write(f\"{prefix}{term}\\n\")\n",
" f.write(f\"\\n{'='*80}\\n\")\n",
" f.write(f\"TORQUE [mN*m] = \\n\")\n",
" torque_eq = build_equation_string(torque_coefs, torque_intercept, pretty_names)\n",
" for i, term in enumerate(torque_eq):\n",
" prefix = \" \" if i == 0 else \" + \"\n",
" f.write(f\"{prefix}{term}\\n\")\n",
"\n",
"print(f\"\\nFull equations (all {len(pretty_names)} terms) saved to: polynomial_equations.txt\")"
]
},
{
"cell_type": "markdown",
"id": "88208b13",
"metadata": {},
"source": [
"### Create Standalone Python Predictor Class\n",
"\n",
"Generate a self-contained Python module that can be imported into your simulator without scikit-learn dependencies."
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "edb239e1",
"metadata": {},
"outputs": [],
"source": [
"# # The following was generated by AI - see [12]\n",
"# # Generate standalone predictor class\n",
"# predictor_code = f'''\"\"\"\n",
"# Magnetic Levitation Force and Torque Predictor\n",
"# Generated from polynomial regression model (degree {best_degree})\n",
"\n",
"# Performance:\n",
"# - Force R²: {r2_score(y.iloc[:, 0].values, y_pred_full[:, 0]):.6f}\n",
"# - Torque R²: {torque_r2_overall:.6f}\n",
"\n",
"# Usage:\n",
"# predictor = MaglevPredictor()\n",
"# force, torque = predictor.predict(currL=-15, currR=-15, roll=1.0, gap_height=10.0)\n",
"# \"\"\"\n",
"\n",
"# import numpy as np\n",
"# from itertools import combinations_with_replacement\n",
"\n",
"# class MaglevPredictor:\n",
"# def __init__(self):\n",
"# \"\"\"Initialize the magnetic levitation force/torque predictor.\"\"\"\n",
"# self.degree = {best_degree}\n",
"# self.n_features = 4 # currL, currR, roll, invGap\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Force model coefficients\n",
"# self.force_intercept = {model_best.intercept_[0]}\n",
"# self.force_coef = np.array({model_best.coef_[0].tolist()})\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Torque model coefficients \n",
"# self.torque_intercept = {model_best.intercept_[1]}\n",
"# self.torque_coef = np.array({model_best.coef_[1].tolist()})\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# def _polynomial_features(self, X):\n",
"# \"\"\"\n",
"# Generate polynomial features up to specified degree.\n",
"# Mimics sklearn's PolynomialFeatures with include_bias=True.\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Args:\n",
"# X: numpy array of shape (n_samples, 4) with [currL, currR, roll, invGap]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Returns:\n",
"# Polynomial features array\n",
"# \"\"\"\n",
"# n_samples = X.shape[0]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Start with bias term (column of ones)\n",
"# features = [np.ones(n_samples)]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Add original features\n",
"# for i in range(self.n_features):\n",
"# features.append(X[:, i])\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Add polynomial combinations\n",
"# for deg in range(2, self.degree + 1):\n",
"# for combo in combinations_with_replacement(range(self.n_features), deg):\n",
"# term = np.ones(n_samples)\n",
"# for idx in combo:\n",
"# term *= X[:, idx]\n",
"# features.append(term)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# return np.column_stack(features)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# def predict(self, currL, currR, roll, gap_height):\n",
"# \"\"\"\n",
"# Predict force and torque for given operating conditions.\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Args:\n",
"# currL: Left coil current in Amps\n",
"# currR: Right coil current in Amps\n",
"# roll: Roll angle in degrees\n",
"# gap_height: Gap height in mm\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Returns:\n",
"# tuple: (force [N], torque [mN·m])\n",
"# \"\"\"\n",
"# # Compute inverse gap (critical transformation!)\n",
"# invGap = 1.0 / gap_height\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Create input array\n",
"# X = np.array([[currL, currR, roll, invGap]])\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Generate polynomial features\n",
"# X_poly = self._polynomial_features(X)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Compute predictions\n",
"# force = self.force_intercept + np.dot(X_poly, self.force_coef)[0]\n",
"# torque = self.torque_intercept + np.dot(X_poly, self.torque_coef)[0]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# return force, torque\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# def predict_batch(self, currL_array, currR_array, roll_array, gap_height_array):\n",
"# \"\"\"\n",
"# Predict force and torque for multiple operating conditions.\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Args:\n",
"# currL_array: Array of left coil currents [A]\n",
"# currR_array: Array of right coil currents [A]\n",
"# roll_array: Array of roll angles [deg]\n",
"# gap_height_array: Array of gap heights [mm]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# Returns:\n",
"# tuple: (force_array [N], torque_array [mN·m])\n",
"# \"\"\"\n",
"# # Convert to numpy arrays\n",
"# currL_array = np.asarray(currL_array)\n",
"# currR_array = np.asarray(currR_array)\n",
"# roll_array = np.asarray(roll_array)\n",
"# gap_height_array = np.asarray(gap_height_array)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Compute inverse gaps\n",
"# invGap_array = 1.0 / gap_height_array\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Stack into feature matrix\n",
"# X = np.column_stack([currL_array, currR_array, roll_array, invGap_array])\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Generate polynomial features\n",
"# X_poly = self._polynomial_features(X)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Compute predictions\n",
"# force_array = self.force_intercept + np.dot(X_poly, self.force_coef)\n",
"# torque_array = self.torque_intercept + np.dot(X_poly, self.torque_coef)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# return force_array, torque_array\n",
2025-12-10 00:40:59 -06:00
"\n",
"\n",
2026-02-11 17:33:18 -06:00
"# if __name__ == \"__main__\":\n",
"# # Example usage\n",
"# predictor = MaglevPredictor()\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Single prediction\n",
"# force, torque = predictor.predict(currL=-15, currR=-15, roll=1.0, gap_height=10.0)\n",
"# print(f\"Single prediction:\")\n",
"# print(f\" Force: {{force:.2f}} N\")\n",
"# print(f\" Torque: {{torque:.2f}} mN·m\")\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Batch prediction\n",
"# currL = np.array([-15, -15, -10])\n",
"# currR = np.array([-15, -10, -10])\n",
"# roll = np.array([0, 0.5, 1.0])\n",
"# gap = np.array([10, 12, 15])\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# forces, torques = predictor.predict_batch(currL, currR, roll, gap)\n",
"# print(f\"\\\\nBatch prediction:\")\n",
"# for i in range(len(forces)):\n",
"# print(f\" Condition {{i+1}}: Force={{forces[i]:.2f}} N, Torque={{torques[i]:.2f}} mN·m\")\n",
"# '''\n",
"\n",
"# # Save to file\n",
"# predictor_filename = 'maglev_predictor.py'\n",
"# with open(predictor_filename, 'w') as f:\n",
"# f.write(predictor_code)\n",
"\n",
"# print(f\"✓ Saved standalone predictor: {predictor_filename}\")\n",
"# print(f\"\\nThis module:\")\n",
"# print(f\" - Has NO scikit-learn dependency (only numpy)\")\n",
"# print(f\" - Can be imported directly into your simulator\")\n",
"# print(f\" - Includes both single and batch prediction methods\")\n",
"# print(f\" - Implements polynomial feature generation internally\")\n",
"# print(f\"\\nTo use:\")\n",
"# print(f\"```python\")\n",
"# print(f\"from maglev_predictor import MaglevPredictor\")\n",
"# print(f\"predictor = MaglevPredictor()\")\n",
"# print(f\"force, torque = predictor.predict(currL=-15, currR=-15, roll=1.0, gap_height=10.0)\")\n",
"# print(f\"```\")"
2025-12-10 00:40:59 -06:00
]
},
{
"cell_type": "markdown",
"id": "e4f40fb4",
"metadata": {},
"source": [
"### Test the Exported Predictor\n",
"\n",
"Verify the standalone predictor produces identical results to the original model."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 47,
2025-12-10 00:40:59 -06:00
"id": "4a1ddacd",
"metadata": {},
2026-02-11 17:33:18 -06:00
"outputs": [],
2025-12-10 00:40:59 -06:00
"source": [
2026-02-11 17:33:18 -06:00
"# # The following was generated by AI - see [12]\n",
"# # Import the standalone predictor (reload to get latest version)\n",
"# import importlib\n",
"# import maglev_predictor\n",
"# importlib.reload(maglev_predictor)\n",
"# from maglev_predictor import MaglevPredictor\n",
"\n",
"# # Create predictor instance\n",
"# standalone_predictor = MaglevPredictor()\n",
"\n",
"# # Test cases\n",
"# test_cases = [\n",
"# {'currL': -15, 'currR': -15, 'roll': 0.0, 'gap': 10.0},\n",
"# {'currL': -15, 'currR': -15, 'roll': 1.0, 'gap': 10.0},\n",
"# {'currL': -15, 'currR': -10, 'roll': 0.5, 'gap': 12.0},\n",
"# {'currL': -10, 'currR': -10, 'roll': 2.0, 'gap': 15.0},\n",
"# ]\n",
"\n",
"# print(\"Validation: Comparing Standalone Predictor vs Original Model\")\n",
"# print(\"=\" * 80)\n",
"\n",
"# for i, case in enumerate(test_cases):\n",
"# # Standalone predictor\n",
"# force_standalone, torque_standalone = standalone_predictor.predict(\n",
"# case['currL'], case['currR'], case['roll'], case['gap']\n",
"# )\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Original model\n",
"# X_test = pd.DataFrame({\n",
"# 'currL [A]': [case['currL']],\n",
"# 'currR [A]': [case['currR']],\n",
"# 'rollDeg [deg]': [case['roll']],\n",
"# 'invGap': [1/case['gap']]\n",
"# })\n",
"# X_test_poly = poly_best.transform(X_test)\n",
"# y_test_pred = model_best.predict(X_test_poly)\n",
"# force_original = y_test_pred[0, 0]\n",
"# torque_original = y_test_pred[0, 1]\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Compare\n",
"# force_diff = abs(force_standalone - force_original)\n",
"# torque_diff = abs(torque_standalone - torque_original)\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# print(f\"\\nTest Case {i+1}: currL={case['currL']}A, currR={case['currR']}A, \" + \n",
"# f\"roll={case['roll']}°, gap={case['gap']}mm\")\n",
"# print(f\" Force: Standalone={force_standalone:9.4f} N | Original={force_original:9.4f} N | Diff={force_diff:.2e}\")\n",
"# print(f\" Torque: Standalone={torque_standalone:9.4f} mN·m | Original={torque_original:9.4f} mN·m | Diff={torque_diff:.2e}\")\n",
2025-12-10 00:40:59 -06:00
" \n",
2026-02-11 17:33:18 -06:00
"# # Verify match (should be essentially identical, accounting for floating point)\n",
"# assert force_diff < 1e-8, f\"Force mismatch in test case {i+1}\"\n",
"# assert torque_diff < 1e-8, f\"Torque mismatch in test case {i+1}\"\n",
2025-12-10 00:40:59 -06:00
"\n",
2026-02-11 17:33:18 -06:00
"# print(\"\\n\" + \"=\" * 80)\n",
"# print(\"✓ All tests passed! Standalone predictor matches original model perfectly.\")\n",
"# print(\"\\nThe standalone predictor is ready for integration into your simulator!\")"
2025-12-10 00:40:59 -06:00
]
2025-12-10 15:50:20 -06:00
},
2025-12-12 08:56:30 -06:00
{
"cell_type": "markdown",
"id": "d473d5fa",
"metadata": {},
"source": [
"### Export Sklearn Model (Lossless & Faster)\n",
"\n",
2025-12-12 13:42:41 -06:00
"Save the actual sklearn model using joblib for lossless serialization. This is also faster than the standalone predictor"
2025-12-12 08:56:30 -06:00
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 48,
2025-12-12 08:56:30 -06:00
"id": "5fd33470",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ Saved sklearn model: maglev_model.pkl\n",
"\n",
"File size: 6.13 KB\n",
"\n",
"Contents:\n",
" - PolynomialFeatures transformer (degree 6)\n",
" - LinearRegression model\n",
" - Feature names and metadata\n",
" - Performance metrics\n",
"\n",
"To use:\n",
"```python\n",
"import joblib\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# Load model\n",
"data = joblib.load('maglev_model.pkl')\n",
"poly = data['poly_features']\n",
"model = data['model']\n",
"\n",
"# Make prediction\n",
"X = pd.DataFrame({'currL [A]': [-15], 'currR [A]': [-15], \n",
" 'rollDeg [deg]': [1.0], 'invGap': [1/10.0]})\n",
"X_poly = poly.transform(X)\n",
"force, torque = model.predict(X_poly)[0]\n",
"```\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was generated by AI - see [12]\n",
2025-12-12 08:56:30 -06:00
"import joblib\n",
"import pickle\n",
"\n",
"# Save the polynomial transformer and model\n",
"model_data = {\n",
" 'poly_features': poly_best,\n",
" 'model': model_best,\n",
" 'degree': best_degree,\n",
" 'feature_names': ['currL [A]', 'currR [A]', 'rollDeg [deg]', 'invGap'],\n",
" 'performance': {\n",
" 'force_r2': r2_score(y.iloc[:, 0].values, y_pred_full[:, 0]),\n",
" 'torque_r2': torque_r2_overall\n",
" }\n",
"}\n",
"\n",
"# Save using joblib (preferred for sklearn models)\n",
"joblib.dump(model_data, 'maglev_model.pkl')\n",
"\n",
"print(\"✓ Saved sklearn model: maglev_model.pkl\")\n",
"print(f\"\\nFile size: {os.path.getsize('maglev_model.pkl') / 1024:.2f} KB\")\n",
"print(f\"\\nContents:\")\n",
"print(f\" - PolynomialFeatures transformer (degree {best_degree})\")\n",
"print(f\" - LinearRegression model\")\n",
"print(f\" - Feature names and metadata\")\n",
"print(f\" - Performance metrics\")\n",
"print(f\"\\nTo use:\")\n",
"print(f\"```python\")\n",
"print(f\"import joblib\")\n",
"print(f\"import numpy as np\")\n",
"print(f\"import pandas as pd\")\n",
"print(f\"\")\n",
"print(f\"# Load model\")\n",
"print(f\"data = joblib.load('maglev_model.pkl')\")\n",
"print(f\"poly = data['poly_features']\")\n",
"print(f\"model = data['model']\")\n",
"print(f\"\")\n",
"print(f\"# Make prediction\")\n",
"print(f\"X = pd.DataFrame({{'currL [A]': [-15], 'currR [A]': [-15], \")\n",
"print(f\" 'rollDeg [deg]': [1.0], 'invGap': [1/10.0]}})\")\n",
"print(f\"X_poly = poly.transform(X)\")\n",
"print(f\"force, torque = model.predict(X_poly)[0]\")\n",
"print(f\"```\")"
]
},
{
"cell_type": "markdown",
"id": "99b93e74",
"metadata": {},
"source": [
"### Compare Performance: Sklearn Model vs Standalone Predictor\n",
"\n",
"Benchmark both approaches to show the speed advantage of using sklearn directly."
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 49,
2025-12-12 08:56:30 -06:00
"id": "889df820",
"metadata": {},
2026-02-11 17:33:18 -06:00
"outputs": [],
2025-12-12 08:56:30 -06:00
"source": [
2026-02-11 17:33:18 -06:00
"# # The following was generated by AI - see [12]\n",
"# import time\n",
"\n",
"# # Load the sklearn model\n",
"# loaded_data = joblib.load('maglev_model.pkl')\n",
"# loaded_poly = loaded_data['poly_features']\n",
"# loaded_model = loaded_data['model']\n",
"\n",
"# # Create test data (1000 predictions)\n",
"# n_tests = 1000\n",
"# test_currL = np.random.uniform(-15, 0, n_tests)\n",
"# test_currR = np.random.uniform(-15, 0, n_tests)\n",
"# test_roll = np.random.uniform(-5, 5, n_tests)\n",
"# test_gap = np.random.uniform(6, 24, n_tests)\n",
"\n",
"# print(\"Performance Comparison (1000 predictions)\")\n",
"# print(\"=\" * 60)\n",
"\n",
"# # Benchmark 1: Sklearn model (loaded from pickle)\n",
"# start = time.perf_counter()\n",
"# X_test = pd.DataFrame({\n",
"# 'currL [A]': test_currL,\n",
"# 'currR [A]': test_currR,\n",
"# 'rollDeg [deg]': test_roll,\n",
"# 'invGap': 1/test_gap\n",
"# })\n",
"# X_test_poly = loaded_poly.transform(X_test)\n",
"# sklearn_results = loaded_model.predict(X_test_poly)\n",
"# sklearn_time = time.perf_counter() - start\n",
"\n",
"# print(f\"Sklearn Model (pickled): {sklearn_time*1000:.2f} ms\")\n",
"\n",
"# # Benchmark 2: Standalone predictor\n",
"# start = time.perf_counter()\n",
"# standalone_results = standalone_predictor.predict_batch(test_currL, test_currR, test_roll, test_gap)\n",
"# standalone_time = time.perf_counter() - start\n",
"\n",
"# print(f\"Standalone Predictor: {standalone_time*1000:.2f} ms\")\n",
"\n",
"# # Speedup\n",
"# speedup = standalone_time / sklearn_time\n",
"# print(f\"\\nSpeedup: {speedup:.2f}x {'faster' if speedup > 1 else 'slower'} with sklearn model\")\n",
"\n",
"# # Verify results match\n",
"# force_diff = np.abs(sklearn_results[:, 0] - standalone_results[0]).max()\n",
"# torque_diff = np.abs(sklearn_results[:, 1] - standalone_results[1]).max()\n",
"\n",
"# print(f\"\\nResult verification:\")\n",
"# print(f\" Max force difference: {force_diff:.2e} N\")\n",
"# print(f\" Max torque difference: {torque_diff:.2e} mN·m\")\n",
"# print(f\" ✓ Results match!\" if (force_diff < 1e-8 and torque_diff < 1e-8) else \" ✗ Results differ!\")\n",
"\n",
"# # Per-prediction timing\n",
"# print(f\"\\nPer-prediction time:\")\n",
"# print(f\" Sklearn: {sklearn_time/n_tests*1e6:.2f} μs\")\n",
"# print(f\" Standalone: {standalone_time/n_tests*1e6:.2f} μs\")\n",
"\n",
"# print(f\"\\n{'='*60}\")\n",
"# print(f\"Recommendation:\")\n",
"# if speedup > 1.2:\n",
"# print(f\" Use sklearn model (maglev_model.pkl) - {speedup:.1f}x faster!\")\n",
"# elif speedup < 0.8:\n",
"# print(f\" Use standalone predictor - {1/speedup:.1f}x faster!\")\n",
"# else:\n",
"# print(f\" Performance is similar - choose based on dependencies:\")\n",
"# print(f\" • Sklearn model: Faster, requires sklearn+joblib\")\n",
"# print(f\" • Standalone: No sklearn dependency, portable\")"
2025-12-12 08:56:30 -06:00
]
},
2025-12-10 15:50:20 -06:00
{
"cell_type": "markdown",
"id": "21babeb3",
"metadata": {},
"source": [
"### Now, let's find the equilibrium height for our pod, given mass of 5.8 kg. \n",
"\n",
"5.8 kg * 9.81 $m/s^2$ = 56.898 N"
]
},
{
"cell_type": "code",
2026-02-11 17:33:18 -06:00
"execution_count": 50,
2025-12-10 15:50:20 -06:00
"id": "badbc379",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"EQUILIBRIUM GAP HEIGHT FINDER (Analytical Solution)\n",
"======================================================================\n",
"Pod mass: 5.8 kg\n",
"Total weight: 56.898 N\n",
"Target force per yoke: 28.449 N\n",
"Parameters: currL = 0 A, currR = 0 A, roll = 0°\n",
"\n",
"using scipy.optimize.fsolve:\n",
2026-02-11 17:33:18 -06:00
" Gap: 16.491742 mm → Force: 28.449000 N\n",
2025-12-10 15:50:20 -06:00
"\n"
]
}
],
"source": [
2025-12-12 13:42:41 -06:00
"# The following was generated by AI - see [13]\n",
2025-12-10 15:50:20 -06:00
"# Find equilibrium gap height for 5.8 kg pod using polynomial root finding\n",
"import numpy as np\n",
"from maglev_predictor import MaglevPredictor\n",
"from scipy.optimize import fsolve\n",
"\n",
"# Initialize predictor\n",
"predictor = MaglevPredictor()\n",
"\n",
"# Target force for 5.8 kg pod (total force = weight)\n",
"# Since we have TWO yokes (front and back), each produces this force\n",
"target_force_per_yoke = 5.8 * 9.81 / 2 # 28.449 N per yoke\n",
"\n",
"print(\"=\" * 70)\n",
"print(\"EQUILIBRIUM GAP HEIGHT FINDER (Analytical Solution)\")\n",
"print(\"=\" * 70)\n",
"print(f\"Pod mass: 5.8 kg\")\n",
"print(f\"Total weight: {5.8 * 9.81:.3f} N\")\n",
"print(f\"Target force per yoke: {target_force_per_yoke:.3f} N\")\n",
"print(f\"Parameters: currL = 0 A, currR = 0 A, roll = 0°\")\n",
"print()\n",
"\n",
"# Method 2: Use scipy.optimize.fsolve for verification\n",
"def force_error(gap_height):\n",
" # Handle array input from fsolve (convert to scalar)\n",
" gap_height = float(np.atleast_1d(gap_height)[0])\n",
" force, _ = predictor.predict(currL=0, currR=0, roll=0, gap_height=gap_height)\n",
" return force - target_force_per_yoke\n",
"\n",
"# Try multiple initial guesses to find all solutions\n",
"initial_guesses = [8, 10, 15, 20, 25]\n",
"scipy_solutions = []\n",
"\n",
"print(\"using scipy.optimize.fsolve:\")\n",
"for guess in initial_guesses:\n",
" solution = fsolve(force_error, guess)[0]\n",
" if 5 <= solution <= 30: # Valid range\n",
" force, torque = predictor.predict(currL=0, currR=0, roll=0, gap_height=solution)\n",
" error = abs(force - target_force_per_yoke)\n",
" if error < 0.01: # Valid solution (within 10 mN)\n",
" scipy_solutions.append((solution, force))\n",
"\n",
"# Remove duplicates (solutions within 0.1 mm)\n",
"unique_solutions = []\n",
"for sol, force in scipy_solutions:\n",
" is_duplicate = False\n",
" for unique_sol, _ in unique_solutions:\n",
" if abs(sol - unique_sol) < 0.1:\n",
" is_duplicate = True\n",
" break\n",
" if not is_duplicate:\n",
" unique_solutions.append((sol, force))\n",
"\n",
"for gap_val, force in unique_solutions:\n",
" print(f\" Gap: {gap_val:.6f} mm → Force: {force:.6f} N\")\n",
"print()"
]
2025-12-10 00:40:59 -06:00
}
],
"metadata": {
"kernelspec": {
2025-12-12 13:42:41 -06:00
"display_name": "RLenv",
2025-12-10 00:40:59 -06:00
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
2025-12-12 13:42:41 -06:00
"version": "3.10.9"
2025-12-10 00:40:59 -06:00
}
},
"nbformat": 4,
"nbformat_minor": 5
}