diff --git a/README.md b/README.md index 35146c0..911c636 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ A Python library for interpolating physical field data (electromagnetic forces, - Heat flux (scalar fields) - Heat generation (volumetric) - Heat Transfer Coefficient + bulk fluid temperature (convection BCs) +- **Analysis tools**: + - Scalar field integration for computing total heat generation, flux, etc. + - EM force resultant computation for validating force/moment conservation - **Export to ANSYS APDL**: Direct export of interpolated results in APDL format - **Visualization**: Built-in VTK export for ParaView or PyVista visualization - **Efficient**: KDTree-based spatial queries for fast neighbor searches @@ -60,6 +63,58 @@ interpolator.export_to_ansys("output_directory") interpolator.build_vtk_output(outdir="vtk_output") ``` +## Analysis Methods + +After interpolation, InterpCore provides methods to analyze and validate results: + +### Scalar Integrals + +For scalar fields (heat flux, heat generation), you can compute the total integral over the destination mesh: + +```python +# Requires volume or area data in the destination mesh +file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4, "vol": 4} # or "area": 5 + +interpolator = Interpolator( + path_to_src_folder="source_data", + path_to_dest_mesh="destination_mesh.txt", + config=config, + file_idx=file_idx +) + +interpolator.interpolate_all() + +# Compute integrals (e.g., total heat generation in W) +integrals = interpolator.compute_scalar_integrals() +# Returns: {"data_001": array([total_value])} +``` + +**Note**: The destination mesh must include volume (for 3D elements) or area (for 2D elements) data. Use the APDL scripts in [`apdl-scripts/`](apdl-scripts/) to export element centroids with volumes and areas. + +### EM Force Resultants + +For EM force fields, you can compute force and moment resultants to verify conservation: + +```python +# After interpolation with EM_FORCE load type +resultants = interpolator.compute_EM_resultants(pole=np.array([0.0, 0.0, 0.0])) + +# Returns for each source file: +# { +# "force_001": { +# "R_F_EM": [Fx, Fy, Fz], # Total force from source data +# "R_F_Mech": [Fx, Fy, Fz], # Total force from interpolated data +# "R_M_EM": [Mx, My, Mz], # Total moment from source data +# "R_M_Mech": [Mx, My, Mz], # Total moment from interpolated data +# "f_err_comp": [ex, ey, ez], # Relative force error by component +# "m_err_comp": [ex, ey, ez], # Relative moment error by component +# "Unmapped_EM_Force": float # Norm of unmapped forces +# } +# } +``` + +This is useful for validating that the interpolation preserves global force and moment equilibrium. Small errors indicate good interpolation quality. + ## Examples Complete working examples with sample data are available in the [`doc/`](doc/) folder: diff --git a/apdl-scripts/export_centroids.apdl b/apdl-scripts/export_centroids.apdl index d0f1d40..7eba79a 100644 --- a/apdl-scripts/export_centroids.apdl +++ b/apdl-scripts/export_centroids.apdl @@ -26,6 +26,8 @@ CMSEL,S,%named_selection% ! Allocate arrays *DIM, nGs, ARRAY, nE, 3 ! element centroid coordinates *DIM, nTId,ARRAY, nE ! element ids +*DIM, nArea,ARRAY, nE ! element area +*DIM, nVol,ARRAY, nE ! element volume ! Loop through selected elements and store centroids *DO,i,1,nE ! loop on selected elements @@ -33,22 +35,31 @@ CMSEL,S,%named_selection% nGs(i,1) = CENTRX(iE) ! centroid X nGs(i,2) = CENTRY(iE) ! centroid Y nGs(i,3) = CENTRZ(iE) ! centroid Z + + ! Get area and volume using *GET + *GET,area_val,ELEM,iE,AREA + *GET,vol_val,ELEM,iE,VOLU + nArea(i) = area_val ! element area + nVol(i) = vol_val ! element volume + iE = ELNEXT(iE) *ENDDO ! Write results to CSV file *CFOPEN,%output_file%,csv ! Write header -*VWRITE,'Element_ID','X','Y','Z' -(A,',',A,',',A,',',A) +*VWRITE,'Element_ID','X','Y','Z','Area','Volume' +(A,',',A,',',A,',',A,',',A,',',A) ! Write data -*VWRITE,nTId(1),nGs(1,1),nGs(1,2),nGs(1,3) -(F10.0,',',E16.9,',',E16.9,',',E16.9) +*VWRITE,nTId(1),nGs(1,1),nGs(1,2),nGs(1,3),nArea(1),nVol(1) +(F10.0,',',E16.9,',',E16.9,',',E16.9,',',E16.9,',',E16.9) *CFCLOS ! Clean up arrays *DIM, nGs, DELETE *DIM, nTId, DELETE +*DIM, nArea, DELETE +*DIM, nVol, DELETE *MSG,INFO Read CDB file '%cdb_file%.cdb' and exported %nE% element centroids from '%named_selection%' to %output_file%.csv diff --git a/doc/em_force/em_force.ipynb b/doc/em_force/em_force.ipynb index 95dc79f..075b68d 100644 --- a/doc/em_force/em_force.ipynb +++ b/doc/em_force/em_force.ipynb @@ -12,10 +12,19 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 1, "id": "04f7aa2d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\ProgramData\\anaconda3\\envs\\interpcore\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], "source": [ "from interpcore.interpolator import Interpolator\n", "from interpcore.config import InterpolationConfig, QUERY_TYPE, INTERPOLATED_LOAD_TYPE\n", @@ -32,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 2, "id": "42549f4f", "metadata": {}, "outputs": [ @@ -40,7 +49,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 1/1 [00:00<00:00, 113.32it/s]" + "100%|██████████| 1/1 [00:00<00:00, 81.83it/s]" ] }, { @@ -96,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 3, "id": "6dc76b31", "metadata": {}, "outputs": [ @@ -146,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 4, "id": "d23ec832", "metadata": {}, "outputs": [], @@ -167,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 5, "id": "4adb006f", "metadata": {}, "outputs": [ @@ -222,6 +231,63 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "id": "3b0d279d", + "metadata": {}, + "source": [ + "## Compute Force and Moment Resultants\n", + "\n", + "Verify that the interpolation preserves global force and moment equilibrium by computing resultants." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "eb46e90a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Results for: em_force_data\n", + "============================================================\n", + "\n", + "Force Resultant (Source): [797.1 373.7 136.35]\n", + "Force Resultant (Interpolated): [797.1 373.7 136.35]\n", + "Force Error (relative): [-2.85251130e-16 0.00000000e+00 4.16893428e-16]\n", + "\n", + "Moment Resultant (Source): [ 758.52 -181.42 -4007.15]\n", + "Moment Resultant (Interpolated): [ 756.21485572 -179.12590679 -3990.31095618]\n", + "Moment Error (relative): [0.003039 0.01264521 0.00420225]\n", + "\n", + "Unmapped EM Force (norm): 0.000000\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "# Compute resultants with pole at origin\n", + "resultants = interpolator.compute_EM_resultants(pole=np.array([0.0, 0.0, 0.0]))\n", + "\n", + "# Display results for the first data file\n", + "for name, result in resultants.items():\n", + " print(f\"\\n{'='*60}\")\n", + " print(f\"Results for: {name}\")\n", + " print(f\"{'='*60}\")\n", + " print(f\"\\nForce Resultant (Source): {result['R_F_EM']}\")\n", + " print(f\"Force Resultant (Interpolated): {result['R_F_Mech']}\")\n", + " print(f\"Force Error (relative): {result['f_err_comp']}\")\n", + " print(f\"\\nMoment Resultant (Source): {result['R_M_EM']}\")\n", + " print(f\"Moment Resultant (Interpolated): {result['R_M_Mech']}\")\n", + " print(f\"Moment Error (relative): {result['m_err_comp']}\")\n", + " print(f\"\\nUnmapped EM Force (norm): {result['Unmapped_EM_Force']:.6f}\")" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/doc/heat_flux/destination_mesh.txt b/doc/heat_flux/destination_mesh.txt index 15eed3f..4b88768 100644 --- a/doc/heat_flux/destination_mesh.txt +++ b/doc/heat_flux/destination_mesh.txt @@ -1,301 +1,301 @@ -Node_ID X Y Z -101 0.25 0.33 0.00 -102 0.25 1.00 0.00 -103 0.25 1.67 0.00 -104 0.25 2.33 0.00 -105 0.25 3.00 0.00 -106 0.25 3.67 0.00 -107 0.25 4.34 0.00 -108 0.25 5.00 0.00 -109 0.25 5.67 0.00 -110 0.25 6.34 0.00 -111 0.25 7.00 0.00 -112 0.25 7.67 0.00 -113 0.25 8.34 0.00 -114 0.25 9.00 0.00 -115 0.25 9.67 0.00 -116 0.75 0.33 0.00 -117 0.75 1.00 0.00 -118 0.75 1.67 0.00 -119 0.75 2.33 0.00 -120 0.75 3.00 0.00 -121 0.75 3.67 0.00 -122 0.75 4.34 0.00 -123 0.75 5.00 0.00 -124 0.75 5.67 0.00 -125 0.75 6.34 0.00 -126 0.75 7.00 0.00 -127 0.75 7.67 0.00 -128 0.75 8.34 0.00 -129 0.75 9.00 0.00 -130 0.75 9.67 0.00 -131 1.25 0.33 0.00 -132 1.25 1.00 0.00 -133 1.25 1.67 0.00 -134 1.25 2.33 0.00 -135 1.25 3.00 0.00 -136 1.25 3.67 0.00 -137 1.25 4.34 0.00 -138 1.25 5.00 0.00 -139 1.25 5.67 0.00 -140 1.25 6.34 0.00 -141 1.25 7.00 0.00 -142 1.25 7.67 0.00 -143 1.25 8.34 0.00 -144 1.25 9.00 0.00 -145 1.25 9.67 0.00 -146 1.75 0.33 0.00 -147 1.75 1.00 0.00 -148 1.75 1.67 0.00 -149 1.75 2.33 0.00 -150 1.75 3.00 0.00 -151 1.75 3.67 0.00 -152 1.75 4.34 0.00 -153 1.75 5.00 0.00 -154 1.75 5.67 0.00 -155 1.75 6.34 0.00 -156 1.75 7.00 0.00 -157 1.75 7.67 0.00 -158 1.75 8.34 0.00 -159 1.75 9.00 0.00 -160 1.75 9.67 0.00 -161 2.25 0.33 0.00 -162 2.25 1.00 0.00 -163 2.25 1.67 0.00 -164 2.25 2.33 0.00 -165 2.25 3.00 0.00 -166 2.25 3.67 0.00 -167 2.25 4.34 0.00 -168 2.25 5.00 0.00 -169 2.25 5.67 0.00 -170 2.25 6.34 0.00 -171 2.25 7.00 0.00 -172 2.25 7.67 0.00 -173 2.25 8.34 0.00 -174 2.25 9.00 0.00 -175 2.25 9.67 0.00 -176 2.75 0.33 0.00 -177 2.75 1.00 0.00 -178 2.75 1.67 0.00 -179 2.75 2.33 0.00 -180 2.75 3.00 0.00 -181 2.75 3.67 0.00 -182 2.75 4.34 0.00 -183 2.75 5.00 0.00 -184 2.75 5.67 0.00 -185 2.75 6.34 0.00 -186 2.75 7.00 0.00 -187 2.75 7.67 0.00 -188 2.75 8.34 0.00 -189 2.75 9.00 0.00 -190 2.75 9.67 0.00 -191 3.25 0.33 0.00 -192 3.25 1.00 0.00 -193 3.25 1.67 0.00 -194 3.25 2.33 0.00 -195 3.25 3.00 0.00 -196 3.25 3.67 0.00 -197 3.25 4.34 0.00 -198 3.25 5.00 0.00 -199 3.25 5.67 0.00 -200 3.25 6.34 0.00 -201 3.25 7.00 0.00 -202 3.25 7.67 0.00 -203 3.25 8.34 0.00 -204 3.25 9.00 0.00 -205 3.25 9.67 0.00 -206 3.75 0.33 0.00 -207 3.75 1.00 0.00 -208 3.75 1.67 0.00 -209 3.75 2.33 0.00 -210 3.75 3.00 0.00 -211 3.75 3.67 0.00 -212 3.75 4.34 0.00 -213 3.75 5.00 0.00 -214 3.75 5.67 0.00 -215 3.75 6.34 0.00 -216 3.75 7.00 0.00 -217 3.75 7.67 0.00 -218 3.75 8.34 0.00 -219 3.75 9.00 0.00 -220 3.75 9.67 0.00 -221 4.25 0.33 0.00 -222 4.25 1.00 0.00 -223 4.25 1.67 0.00 -224 4.25 2.33 0.00 -225 4.25 3.00 0.00 -226 4.25 3.67 0.00 -227 4.25 4.34 0.00 -228 4.25 5.00 0.00 -229 4.25 5.67 0.00 -230 4.25 6.34 0.00 -231 4.25 7.00 0.00 -232 4.25 7.67 0.00 -233 4.25 8.34 0.00 -234 4.25 9.00 0.00 -235 4.25 9.67 0.00 -236 4.75 0.33 0.00 -237 4.75 1.00 0.00 -238 4.75 1.67 0.00 -239 4.75 2.33 0.00 -240 4.75 3.00 0.00 -241 4.75 3.67 0.00 -242 4.75 4.34 0.00 -243 4.75 5.00 0.00 -244 4.75 5.67 0.00 -245 4.75 6.34 0.00 -246 4.75 7.00 0.00 -247 4.75 7.67 0.00 -248 4.75 8.34 0.00 -249 4.75 9.00 0.00 -250 4.75 9.67 0.00 -251 5.25 0.33 0.00 -252 5.25 1.00 0.00 -253 5.25 1.67 0.00 -254 5.25 2.33 0.00 -255 5.25 3.00 0.00 -256 5.25 3.67 0.00 -257 5.25 4.34 0.00 -258 5.25 5.00 0.00 -259 5.25 5.67 0.00 -260 5.25 6.34 0.00 -261 5.25 7.00 0.00 -262 5.25 7.67 0.00 -263 5.25 8.34 0.00 -264 5.25 9.00 0.00 -265 5.25 9.67 0.00 -266 5.75 0.33 0.00 -267 5.75 1.00 0.00 -268 5.75 1.67 0.00 -269 5.75 2.33 0.00 -270 5.75 3.00 0.00 -271 5.75 3.67 0.00 -272 5.75 4.34 0.00 -273 5.75 5.00 0.00 -274 5.75 5.67 0.00 -275 5.75 6.34 0.00 -276 5.75 7.00 0.00 -277 5.75 7.67 0.00 -278 5.75 8.34 0.00 -279 5.75 9.00 0.00 -280 5.75 9.67 0.00 -281 6.25 0.33 0.00 -282 6.25 1.00 0.00 -283 6.25 1.67 0.00 -284 6.25 2.33 0.00 -285 6.25 3.00 0.00 -286 6.25 3.67 0.00 -287 6.25 4.34 0.00 -288 6.25 5.00 0.00 -289 6.25 5.67 0.00 -290 6.25 6.34 0.00 -291 6.25 7.00 0.00 -292 6.25 7.67 0.00 -293 6.25 8.34 0.00 -294 6.25 9.00 0.00 -295 6.25 9.67 0.00 -296 6.75 0.33 0.00 -297 6.75 1.00 0.00 -298 6.75 1.67 0.00 -299 6.75 2.33 0.00 -300 6.75 3.00 0.00 -301 6.75 3.67 0.00 -302 6.75 4.34 0.00 -303 6.75 5.00 0.00 -304 6.75 5.67 0.00 -305 6.75 6.34 0.00 -306 6.75 7.00 0.00 -307 6.75 7.67 0.00 -308 6.75 8.34 0.00 -309 6.75 9.00 0.00 -310 6.75 9.67 0.00 -311 7.25 0.33 0.00 -312 7.25 1.00 0.00 -313 7.25 1.67 0.00 -314 7.25 2.33 0.00 -315 7.25 3.00 0.00 -316 7.25 3.67 0.00 -317 7.25 4.34 0.00 -318 7.25 5.00 0.00 -319 7.25 5.67 0.00 -320 7.25 6.34 0.00 -321 7.25 7.00 0.00 -322 7.25 7.67 0.00 -323 7.25 8.34 0.00 -324 7.25 9.00 0.00 -325 7.25 9.67 0.00 -326 7.75 0.33 0.00 -327 7.75 1.00 0.00 -328 7.75 1.67 0.00 -329 7.75 2.33 0.00 -330 7.75 3.00 0.00 -331 7.75 3.67 0.00 -332 7.75 4.34 0.00 -333 7.75 5.00 0.00 -334 7.75 5.67 0.00 -335 7.75 6.34 0.00 -336 7.75 7.00 0.00 -337 7.75 7.67 0.00 -338 7.75 8.34 0.00 -339 7.75 9.00 0.00 -340 7.75 9.67 0.00 -341 8.25 0.33 0.00 -342 8.25 1.00 0.00 -343 8.25 1.67 0.00 -344 8.25 2.33 0.00 -345 8.25 3.00 0.00 -346 8.25 3.67 0.00 -347 8.25 4.34 0.00 -348 8.25 5.00 0.00 -349 8.25 5.67 0.00 -350 8.25 6.34 0.00 -351 8.25 7.00 0.00 -352 8.25 7.67 0.00 -353 8.25 8.34 0.00 -354 8.25 9.00 0.00 -355 8.25 9.67 0.00 -356 8.75 0.33 0.00 -357 8.75 1.00 0.00 -358 8.75 1.67 0.00 -359 8.75 2.33 0.00 -360 8.75 3.00 0.00 -361 8.75 3.67 0.00 -362 8.75 4.34 0.00 -363 8.75 5.00 0.00 -364 8.75 5.67 0.00 -365 8.75 6.34 0.00 -366 8.75 7.00 0.00 -367 8.75 7.67 0.00 -368 8.75 8.34 0.00 -369 8.75 9.00 0.00 -370 8.75 9.67 0.00 -371 9.25 0.33 0.00 -372 9.25 1.00 0.00 -373 9.25 1.67 0.00 -374 9.25 2.33 0.00 -375 9.25 3.00 0.00 -376 9.25 3.67 0.00 -377 9.25 4.34 0.00 -378 9.25 5.00 0.00 -379 9.25 5.67 0.00 -380 9.25 6.34 0.00 -381 9.25 7.00 0.00 -382 9.25 7.67 0.00 -383 9.25 8.34 0.00 -384 9.25 9.00 0.00 -385 9.25 9.67 0.00 -386 9.75 0.33 0.00 -387 9.75 1.00 0.00 -388 9.75 1.67 0.00 -389 9.75 2.33 0.00 -390 9.75 3.00 0.00 -391 9.75 3.67 0.00 -392 9.75 4.34 0.00 -393 9.75 5.00 0.00 -394 9.75 5.67 0.00 -395 9.75 6.34 0.00 -396 9.75 7.00 0.00 -397 9.75 7.67 0.00 -398 9.75 8.34 0.00 -399 9.75 9.00 0.00 -400 9.75 9.67 0.00 +Node_ID X Y Z Area +101 0.25 0.33 0.00 0.0005 +102 0.25 1.00 0.00 0.0005 +103 0.25 1.67 0.00 0.0005 +104 0.25 2.33 0.00 0.0005 +105 0.25 3.00 0.00 0.0005 +106 0.25 3.67 0.00 0.0005 +107 0.25 4.34 0.00 0.0005 +108 0.25 5.00 0.00 0.0005 +109 0.25 5.67 0.00 0.0005 +110 0.25 6.34 0.00 0.0005 +111 0.25 7.00 0.00 0.0005 +112 0.25 7.67 0.00 0.0005 +113 0.25 8.34 0.00 0.0005 +114 0.25 9.00 0.00 0.0005 +115 0.25 9.67 0.00 0.0005 +116 0.75 0.33 0.00 0.0005 +117 0.75 1.00 0.00 0.0005 +118 0.75 1.67 0.00 0.0005 +119 0.75 2.33 0.00 0.0005 +120 0.75 3.00 0.00 0.0005 +121 0.75 3.67 0.00 0.0005 +122 0.75 4.34 0.00 0.0005 +123 0.75 5.00 0.00 0.0005 +124 0.75 5.67 0.00 0.0005 +125 0.75 6.34 0.00 0.0005 +126 0.75 7.00 0.00 0.0005 +127 0.75 7.67 0.00 0.0005 +128 0.75 8.34 0.00 0.0005 +129 0.75 9.00 0.00 0.0005 +130 0.75 9.67 0.00 0.0005 +131 1.25 0.33 0.00 0.0005 +132 1.25 1.00 0.00 0.0005 +133 1.25 1.67 0.00 0.0005 +134 1.25 2.33 0.00 0.0005 +135 1.25 3.00 0.00 0.0005 +136 1.25 3.67 0.00 0.0005 +137 1.25 4.34 0.00 0.0005 +138 1.25 5.00 0.00 0.0005 +139 1.25 5.67 0.00 0.0005 +140 1.25 6.34 0.00 0.0005 +141 1.25 7.00 0.00 0.0005 +142 1.25 7.67 0.00 0.0005 +143 1.25 8.34 0.00 0.0005 +144 1.25 9.00 0.00 0.0005 +145 1.25 9.67 0.00 0.0005 +146 1.75 0.33 0.00 0.0005 +147 1.75 1.00 0.00 0.0005 +148 1.75 1.67 0.00 0.0005 +149 1.75 2.33 0.00 0.0005 +150 1.75 3.00 0.00 0.0005 +151 1.75 3.67 0.00 0.0005 +152 1.75 4.34 0.00 0.0005 +153 1.75 5.00 0.00 0.0005 +154 1.75 5.67 0.00 0.0005 +155 1.75 6.34 0.00 0.0005 +156 1.75 7.00 0.00 0.0005 +157 1.75 7.67 0.00 0.0005 +158 1.75 8.34 0.00 0.0005 +159 1.75 9.00 0.00 0.0005 +160 1.75 9.67 0.00 0.0005 +161 2.25 0.33 0.00 0.0005 +162 2.25 1.00 0.00 0.0005 +163 2.25 1.67 0.00 0.0005 +164 2.25 2.33 0.00 0.0005 +165 2.25 3.00 0.00 0.0005 +166 2.25 3.67 0.00 0.0005 +167 2.25 4.34 0.00 0.0005 +168 2.25 5.00 0.00 0.0005 +169 2.25 5.67 0.00 0.0005 +170 2.25 6.34 0.00 0.0005 +171 2.25 7.00 0.00 0.0005 +172 2.25 7.67 0.00 0.0005 +173 2.25 8.34 0.00 0.0005 +174 2.25 9.00 0.00 0.0005 +175 2.25 9.67 0.00 0.0005 +176 2.75 0.33 0.00 0.0005 +177 2.75 1.00 0.00 0.0005 +178 2.75 1.67 0.00 0.0005 +179 2.75 2.33 0.00 0.0005 +180 2.75 3.00 0.00 0.0005 +181 2.75 3.67 0.00 0.0005 +182 2.75 4.34 0.00 0.0005 +183 2.75 5.00 0.00 0.0005 +184 2.75 5.67 0.00 0.0005 +185 2.75 6.34 0.00 0.0005 +186 2.75 7.00 0.00 0.0005 +187 2.75 7.67 0.00 0.0005 +188 2.75 8.34 0.00 0.0005 +189 2.75 9.00 0.00 0.0005 +190 2.75 9.67 0.00 0.0005 +191 3.25 0.33 0.00 0.0005 +192 3.25 1.00 0.00 0.0005 +193 3.25 1.67 0.00 0.0005 +194 3.25 2.33 0.00 0.0005 +195 3.25 3.00 0.00 0.0005 +196 3.25 3.67 0.00 0.0005 +197 3.25 4.34 0.00 0.0005 +198 3.25 5.00 0.00 0.0005 +199 3.25 5.67 0.00 0.0005 +200 3.25 6.34 0.00 0.0005 +201 3.25 7.00 0.00 0.0005 +202 3.25 7.67 0.00 0.0005 +203 3.25 8.34 0.00 0.0005 +204 3.25 9.00 0.00 0.0005 +205 3.25 9.67 0.00 0.0005 +206 3.75 0.33 0.00 0.0005 +207 3.75 1.00 0.00 0.0005 +208 3.75 1.67 0.00 0.0005 +209 3.75 2.33 0.00 0.0005 +210 3.75 3.00 0.00 0.0005 +211 3.75 3.67 0.00 0.0005 +212 3.75 4.34 0.00 0.0005 +213 3.75 5.00 0.00 0.0005 +214 3.75 5.67 0.00 0.0005 +215 3.75 6.34 0.00 0.0005 +216 3.75 7.00 0.00 0.0005 +217 3.75 7.67 0.00 0.0005 +218 3.75 8.34 0.00 0.0005 +219 3.75 9.00 0.00 0.0005 +220 3.75 9.67 0.00 0.0005 +221 4.25 0.33 0.00 0.0005 +222 4.25 1.00 0.00 0.0005 +223 4.25 1.67 0.00 0.0005 +224 4.25 2.33 0.00 0.0005 +225 4.25 3.00 0.00 0.0005 +226 4.25 3.67 0.00 0.0005 +227 4.25 4.34 0.00 0.0005 +228 4.25 5.00 0.00 0.0005 +229 4.25 5.67 0.00 0.0005 +230 4.25 6.34 0.00 0.0005 +231 4.25 7.00 0.00 0.0005 +232 4.25 7.67 0.00 0.0005 +233 4.25 8.34 0.00 0.0005 +234 4.25 9.00 0.00 0.0005 +235 4.25 9.67 0.00 0.0005 +236 4.75 0.33 0.00 0.0005 +237 4.75 1.00 0.00 0.0005 +238 4.75 1.67 0.00 0.0005 +239 4.75 2.33 0.00 0.0005 +240 4.75 3.00 0.00 0.0005 +241 4.75 3.67 0.00 0.0005 +242 4.75 4.34 0.00 0.0005 +243 4.75 5.00 0.00 0.0005 +244 4.75 5.67 0.00 0.0005 +245 4.75 6.34 0.00 0.0005 +246 4.75 7.00 0.00 0.0005 +247 4.75 7.67 0.00 0.0005 +248 4.75 8.34 0.00 0.0005 +249 4.75 9.00 0.00 0.0005 +250 4.75 9.67 0.00 0.0005 +251 5.25 0.33 0.00 0.0005 +252 5.25 1.00 0.00 0.0005 +253 5.25 1.67 0.00 0.0005 +254 5.25 2.33 0.00 0.0005 +255 5.25 3.00 0.00 0.0005 +256 5.25 3.67 0.00 0.0005 +257 5.25 4.34 0.00 0.0005 +258 5.25 5.00 0.00 0.0005 +259 5.25 5.67 0.00 0.0005 +260 5.25 6.34 0.00 0.0005 +261 5.25 7.00 0.00 0.0005 +262 5.25 7.67 0.00 0.0005 +263 5.25 8.34 0.00 0.0005 +264 5.25 9.00 0.00 0.0005 +265 5.25 9.67 0.00 0.0005 +266 5.75 0.33 0.00 0.0005 +267 5.75 1.00 0.00 0.0005 +268 5.75 1.67 0.00 0.0005 +269 5.75 2.33 0.00 0.0005 +270 5.75 3.00 0.00 0.0005 +271 5.75 3.67 0.00 0.0005 +272 5.75 4.34 0.00 0.0005 +273 5.75 5.00 0.00 0.0005 +274 5.75 5.67 0.00 0.0005 +275 5.75 6.34 0.00 0.0005 +276 5.75 7.00 0.00 0.0005 +277 5.75 7.67 0.00 0.0005 +278 5.75 8.34 0.00 0.0005 +279 5.75 9.00 0.00 0.0005 +280 5.75 9.67 0.00 0.0005 +281 6.25 0.33 0.00 0.0005 +282 6.25 1.00 0.00 0.0005 +283 6.25 1.67 0.00 0.0005 +284 6.25 2.33 0.00 0.0005 +285 6.25 3.00 0.00 0.0005 +286 6.25 3.67 0.00 0.0005 +287 6.25 4.34 0.00 0.0005 +288 6.25 5.00 0.00 0.0005 +289 6.25 5.67 0.00 0.0005 +290 6.25 6.34 0.00 0.0005 +291 6.25 7.00 0.00 0.0005 +292 6.25 7.67 0.00 0.0005 +293 6.25 8.34 0.00 0.0005 +294 6.25 9.00 0.00 0.0005 +295 6.25 9.67 0.00 0.0005 +296 6.75 0.33 0.00 0.0005 +297 6.75 1.00 0.00 0.0005 +298 6.75 1.67 0.00 0.0005 +299 6.75 2.33 0.00 0.0005 +300 6.75 3.00 0.00 0.0005 +301 6.75 3.67 0.00 0.0005 +302 6.75 4.34 0.00 0.0005 +303 6.75 5.00 0.00 0.0005 +304 6.75 5.67 0.00 0.0005 +305 6.75 6.34 0.00 0.0005 +306 6.75 7.00 0.00 0.0005 +307 6.75 7.67 0.00 0.0005 +308 6.75 8.34 0.00 0.0005 +309 6.75 9.00 0.00 0.0005 +310 6.75 9.67 0.00 0.0005 +311 7.25 0.33 0.00 0.0005 +312 7.25 1.00 0.00 0.0005 +313 7.25 1.67 0.00 0.0005 +314 7.25 2.33 0.00 0.0005 +315 7.25 3.00 0.00 0.0005 +316 7.25 3.67 0.00 0.0005 +317 7.25 4.34 0.00 0.0005 +318 7.25 5.00 0.00 0.0005 +319 7.25 5.67 0.00 0.0005 +320 7.25 6.34 0.00 0.0005 +321 7.25 7.00 0.00 0.0005 +322 7.25 7.67 0.00 0.0005 +323 7.25 8.34 0.00 0.0005 +324 7.25 9.00 0.00 0.0005 +325 7.25 9.67 0.00 0.0005 +326 7.75 0.33 0.00 0.0005 +327 7.75 1.00 0.00 0.0005 +328 7.75 1.67 0.00 0.0005 +329 7.75 2.33 0.00 0.0005 +330 7.75 3.00 0.00 0.0005 +331 7.75 3.67 0.00 0.0005 +332 7.75 4.34 0.00 0.0005 +333 7.75 5.00 0.00 0.0005 +334 7.75 5.67 0.00 0.0005 +335 7.75 6.34 0.00 0.0005 +336 7.75 7.00 0.00 0.0005 +337 7.75 7.67 0.00 0.0005 +338 7.75 8.34 0.00 0.0005 +339 7.75 9.00 0.00 0.0005 +340 7.75 9.67 0.00 0.0005 +341 8.25 0.33 0.00 0.0005 +342 8.25 1.00 0.00 0.0005 +343 8.25 1.67 0.00 0.0005 +344 8.25 2.33 0.00 0.0005 +345 8.25 3.00 0.00 0.0005 +346 8.25 3.67 0.00 0.0005 +347 8.25 4.34 0.00 0.0005 +348 8.25 5.00 0.00 0.0005 +349 8.25 5.67 0.00 0.0005 +350 8.25 6.34 0.00 0.0005 +351 8.25 7.00 0.00 0.0005 +352 8.25 7.67 0.00 0.0005 +353 8.25 8.34 0.00 0.0005 +354 8.25 9.00 0.00 0.0005 +355 8.25 9.67 0.00 0.0005 +356 8.75 0.33 0.00 0.0005 +357 8.75 1.00 0.00 0.0005 +358 8.75 1.67 0.00 0.0005 +359 8.75 2.33 0.00 0.0005 +360 8.75 3.00 0.00 0.0005 +361 8.75 3.67 0.00 0.0005 +362 8.75 4.34 0.00 0.0005 +363 8.75 5.00 0.00 0.0005 +364 8.75 5.67 0.00 0.0005 +365 8.75 6.34 0.00 0.0005 +366 8.75 7.00 0.00 0.0005 +367 8.75 7.67 0.00 0.0005 +368 8.75 8.34 0.00 0.0005 +369 8.75 9.00 0.00 0.0005 +370 8.75 9.67 0.00 0.0005 +371 9.25 0.33 0.00 0.0005 +372 9.25 1.00 0.00 0.0005 +373 9.25 1.67 0.00 0.0005 +374 9.25 2.33 0.00 0.0005 +375 9.25 3.00 0.00 0.0005 +376 9.25 3.67 0.00 0.0005 +377 9.25 4.34 0.00 0.0005 +378 9.25 5.00 0.00 0.0005 +379 9.25 5.67 0.00 0.0005 +380 9.25 6.34 0.00 0.0005 +381 9.25 7.00 0.00 0.0005 +382 9.25 7.67 0.00 0.0005 +383 9.25 8.34 0.00 0.0005 +384 9.25 9.00 0.00 0.0005 +385 9.25 9.67 0.00 0.0005 +386 9.75 0.33 0.00 0.0005 +387 9.75 1.00 0.00 0.0005 +388 9.75 1.67 0.00 0.0005 +389 9.75 2.33 0.00 0.0005 +390 9.75 3.00 0.00 0.0005 +391 9.75 3.67 0.00 0.0005 +392 9.75 4.34 0.00 0.0005 +393 9.75 5.00 0.00 0.0005 +394 9.75 5.67 0.00 0.0005 +395 9.75 6.34 0.00 0.0005 +396 9.75 7.00 0.00 0.0005 +397 9.75 7.67 0.00 0.0005 +398 9.75 8.34 0.00 0.0005 +399 9.75 9.00 0.00 0.0005 +400 9.75 9.67 0.00 0.0005 diff --git a/doc/heat_flux/heat_flux.ipynb b/doc/heat_flux/heat_flux.ipynb index a60051f..1e1f445 100644 --- a/doc/heat_flux/heat_flux.ipynb +++ b/doc/heat_flux/heat_flux.ipynb @@ -12,10 +12,19 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 1, "id": "ec9f9d58", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\ProgramData\\anaconda3\\envs\\interpcore\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], "source": [ "from interpcore.interpolator import Interpolator\n", "from interpcore.config import InterpolationConfig, QUERY_TYPE, INTERPOLATED_LOAD_TYPE\n", @@ -32,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "ac715497", "metadata": {}, "outputs": [ @@ -40,14 +49,7 @@ "name": "stderr", "output_type": "stream", "text": [ - " 0%| | 0/1 [00:00 dict: - # if pole is None: - # pole = np.array([0.0, 0.0, 0.0]) - - # F_EM = self.src_values[name] - # if self.interpolated_results is None: - # raise ValueError( - # "No interpolated results found. Run interpolate_all() first." - # ) - # F_Mech = self.interpolated_results[name]["interpolated"] - - # R_F_EM = np.sum(F_EM, axis=0) - # R_F_Mech = np.sum(F_Mech, axis=0) - - # M_EM = np.cross(self.em_x - pole, F_EM) - # M_Mech = np.cross(self.mech_x - pole, F_Mech) - - # R_M_EM = np.sum(M_EM, axis=0) - # R_M_Mech = np.sum(M_Mech, axis=0) - - # f_err_comp = np.divide(R_F_EM - R_F_Mech, R_F_EM) - # m_err_comp = np.divide(R_M_EM - R_M_Mech, R_M_EM) - - # # give some warning if differences are very high - # if np.any(np.abs(f_err_comp) > 0.2): - # logging.warning(f"High difference in force resultant for {name}") - # if np.any(np.abs(m_err_comp) > 0.2): - # logging.warning(f"High difference in moment resultant for {name}") - - # row = { - # "Name": name, - # "Fx [N]": R_F_EM[0], - # "Fy [N]": R_F_EM[1], - # "Fz [N]": R_F_EM[2], - # "Mx [Nm]": R_M_EM[0], - # "My [Nm]": R_M_EM[1], - # "Mz [Nm]": R_M_EM[2], - # "dFx [%]": f_err_comp[0] * 100, - # "dFy [%]": f_err_comp[1] * 100, - # "dFz [%]": f_err_comp[2] * 100, - # "dMx [%]": m_err_comp[0] * 100, - # "dMy [%]": m_err_comp[1] * 100, - # "dMz [%]": m_err_comp[2] * 100, - # "Unmapped_EM_Force [N]": np.linalg.norm( - # self.interpolated_results[name]["unmapped"] - # ), - # } - - # return row + def compute_scalar_integrals(self) -> dict[str, float]: + """Compute the integrals of the interpolated results over the destination mesh""" + if self.interpolated_results is None: + raise ValueError( + "No interpolated results found. Run interpolate_all() first." + ) + + integrals = {} + for name, result in self.interpolated_results.items(): + if self.tree.config.num_components > 1: + raise IncompatibleResultsError( + "Integral computation only supported for scalar results (num_components=1)." + ) + if self.volumes is not None: + integral = np.sum( + result["interpolated"] * self.volumes[:, np.newaxis], axis=0 + ) + elif self.areas is not None: + integral = np.sum( + result["interpolated"] * self.areas[:, np.newaxis], axis=0 + ) + else: + raise IncompatibleResultsError( + "No volume or area information found. Cannot compute integrals." + ) + integrals[name] = integral + + return integrals + + def compute_EM_resultants( + self, pole: np.ndarray | None = None + ) -> dict[str, dict[str, float]]: + """Compute the force and moment resultants of the EM forces and the interpolated + forces, and their differences + + Parameters + ---------- + pole : np.ndarray | None, optional + reference pole for moment calculation, by default None (0,0,0) + + Returns + ------- + dict[str, dict[str, float]] + dictionary containing the resultants and their differences for each + interpolated load + """ + if self.interpolated_results is None: + raise ValueError( + "No interpolated results found. Run interpolate_all() first." + ) + if pole is None: + pole = np.array([0.0, 0.0, 0.0]) + + resultants = {} + for name in self.src_values.keys(): + F_EM = self.src_values[name] + F_Mech = self.interpolated_results[name]["interpolated"] + + R_F_EM = np.sum(F_EM, axis=0) + R_F_Mech = np.sum(F_Mech, axis=0) + + M_EM = np.cross(self.tree.src_coordinates - pole, F_EM) + M_Mech = np.cross(self.tree.dest_coordinates - pole, F_Mech) + + R_M_EM = np.sum(M_EM, axis=0) + R_M_Mech = np.sum(M_Mech, axis=0) + + f_err_comp = np.divide(R_F_EM - R_F_Mech, R_F_EM) + m_err_comp = np.divide(R_M_EM - R_M_Mech, R_M_EM) + + resultants[name] = { + "R_F_EM": R_F_EM, + "R_F_Mech": R_F_Mech, + "R_M_EM": R_M_EM, + "R_M_Mech": R_M_Mech, + "f_err_comp": f_err_comp, + "m_err_comp": m_err_comp, + "Unmapped_EM_Force": np.linalg.norm( + self.interpolated_results[name]["unmapped"] + ), + } + + return resultants def build_vtk_output(self, outdir: Path | None = None): """Build the VTK output files for visualization diff --git a/src/interpcore/parsers.py b/src/interpcore/parsers.py index 59cbfa9..0850f7c 100644 --- a/src/interpcore/parsers.py +++ b/src/interpcore/parsers.py @@ -6,8 +6,12 @@ def parse_mech_mesh( - filepath: Path, col_mesh_ids: int = 0, col_mesh_x: int = 1 -) -> tuple[np.ndarray, np.ndarray]: + filepath: Path, + col_mesh_ids: int = 0, + col_mesh_x: int = 1, + col_vol: int | None = None, + col_area: int | None = None, +) -> dict[str, np.ndarray]: """Parse a mechanical mesh and return the array of coordinates and the vector of node numbers @@ -19,6 +23,10 @@ def parse_mech_mesh( index of the column containing node/element numbers, by default 0 col_mesh_x : int, optional index of the column containing x coordinates, by default 1 + col_vol : int, optional + index of the column containing volume information, by default None + col_area : int, optional + index of the column containing area information, by default None Returns ------- @@ -35,7 +43,12 @@ def parse_mech_mesh( coordinates = Detailed_mesh[:, col_mesh_x : col_mesh_x + 3] id_numbers = Detailed_mesh[:, col_mesh_ids] - return coordinates, id_numbers + result = {"coordinates": coordinates, "id_numbers": id_numbers} + if col_vol is not None: + result["volumes"] = Detailed_mesh[:, col_vol] + if col_area is not None: + result["areas"] = Detailed_mesh[:, col_area] + return result class CloudParser: diff --git a/tests/test_interpolator.py b/tests/test_interpolator.py index 0cc04bb..fa5c24a 100644 --- a/tests/test_interpolator.py +++ b/tests/test_interpolator.py @@ -1,6 +1,7 @@ import pytest import tempfile import shutil +import numpy as np from pathlib import Path from interpcore.interpolator import Interpolator, _select_template from interpcore.config import ( @@ -564,6 +565,152 @@ def test_multiple_source_files(self, temp_dir, sample_config_heat_flux): assert interpolator.interpolated_results is not None assert len(interpolator.interpolated_results) == 3 + def test_compute_scalar_integrals(self, temp_dir, sample_config_heat_gen): + """Test computing scalar integrals with volume data""" + # Create destination mesh with volume column + dest_mesh = temp_dir / "destination_mesh.txt" + dest_content = """Node_ID X Y Z Volume +401 0.0 0.0 0.0 0.001 +402 1.0 0.0 0.0 0.002 +403 2.0 0.0 0.0 0.0015 +""" + dest_mesh.write_text(dest_content) + + # Create source data folder with heat generation + src_folder = temp_dir / "heat_gen_data" + src_folder.mkdir() + + src_file = src_folder / "heatgen_001.txt" + src_content = """Node_ID X Y Z HeatGen +1 0.5 0.0 0.0 1000.0 +2 1.5 0.0 0.0 2000.0 +3 2.5 0.0 0.0 1500.0 +""" + src_file.write_text(src_content) + + # Create interpolator with volume data + file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4, "vol": 4} + interpolator = Interpolator( + path_to_src_folder=str(src_folder), + path_to_dest_mesh=str(dest_mesh), + config=sample_config_heat_gen, + file_idx=file_idx, + ) + + # Run interpolation + interpolator.interpolate_all() + + # Compute integrals + integrals = interpolator.compute_scalar_integrals() + + # Verify results + assert integrals is not None + assert "heatgen_001" in integrals + assert isinstance(integrals["heatgen_001"], np.ndarray) + assert integrals["heatgen_001"].size == 1 # Scalar result + assert integrals["heatgen_001"][0] > 0 # Should be positive heat generation + + def test_compute_scalar_integrals_without_interpolation( + self, create_sample_heat_gen_files, sample_config_heat_gen + ): + """Test that compute_scalar_integrals raises ValueError before interpolation""" + file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4} + interpolator = Interpolator( + path_to_src_folder=create_sample_heat_gen_files["src_folder"], + path_to_dest_mesh=create_sample_heat_gen_files["dest_mesh"], + config=sample_config_heat_gen, + file_idx=file_idx, + ) + + with pytest.raises( + ValueError, match="No interpolated results found. Run interpolate_all" + ): + interpolator.compute_scalar_integrals() + + def test_compute_em_resultants( + self, create_sample_em_force_files, sample_config_em_force + ): + """Test computing EM force and moment resultants""" + file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4} + interpolator = Interpolator( + path_to_src_folder=create_sample_em_force_files["src_folder"], + path_to_dest_mesh=create_sample_em_force_files["dest_mesh"], + config=sample_config_em_force, + file_idx=file_idx, + ) + + # Run interpolation + interpolator.interpolate_all() + + # Compute EM resultants with default pole (0, 0, 0) + resultants = interpolator.compute_EM_resultants() + + # Verify results + assert resultants is not None + assert "force_001" in resultants + + result = resultants["force_001"] + assert "R_F_EM" in result + assert "R_F_Mech" in result + assert "R_M_EM" in result + assert "R_M_Mech" in result + assert "f_err_comp" in result + assert "m_err_comp" in result + assert "Unmapped_EM_Force" in result + + # Check that force resultants are 3D vectors + assert isinstance(result["R_F_EM"], np.ndarray) + assert isinstance(result["R_F_Mech"], np.ndarray) + assert isinstance(result["R_M_EM"], np.ndarray) + assert isinstance(result["R_M_Mech"], np.ndarray) + assert len(result["R_F_EM"]) == 3 + assert len(result["R_F_Mech"]) == 3 + assert len(result["R_M_EM"]) == 3 + assert len(result["R_M_Mech"]) == 3 + + def test_compute_em_resultants_with_custom_pole( + self, create_sample_em_force_files, sample_config_em_force + ): + """Test computing EM resultants with custom reference pole""" + file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4} + interpolator = Interpolator( + path_to_src_folder=create_sample_em_force_files["src_folder"], + path_to_dest_mesh=create_sample_em_force_files["dest_mesh"], + config=sample_config_em_force, + file_idx=file_idx, + ) + + interpolator.interpolate_all() + + # Compute with custom pole + custom_pole = np.array([1.0, 1.0, 1.0]) + resultants = interpolator.compute_EM_resultants(pole=custom_pole) + + # Verify results exist and are different from default pole + assert resultants is not None + assert "force_001" in resultants + + result = resultants["force_001"] + assert "R_M_EM" in result + assert "R_M_Mech" in result + + def test_compute_em_resultants_without_interpolation( + self, create_sample_em_force_files, sample_config_em_force + ): + """Test that compute_EM_resultants raises ValueError before interpolation""" + file_idx = {"ids": 0, "dest_x": 1, "src_x": 1, "val": 4} + interpolator = Interpolator( + path_to_src_folder=create_sample_em_force_files["src_folder"], + path_to_dest_mesh=create_sample_em_force_files["dest_mesh"], + config=sample_config_em_force, + file_idx=file_idx, + ) + + with pytest.raises( + ValueError, match="No interpolated results found. Run interpolate_all" + ): + interpolator.compute_EM_resultants() + class TestSelectTemplate: """Tests for _select_template helper function"""