diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e101844f..339ed03cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] - YYYY-MM-DD +- Adding tutorial model magnet - Added new `style.pixel.field` parameters to quickly represent vector fields ([#793](https://github.com/magpylib/magpylib/pull/793)) - Added the `current_sheet_Hfield` core computation function, and the classes diff --git a/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md b/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md index 9b0071b4a..f9afbec99 100644 --- a/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md @@ -1,99 +1,284 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.17.1 +kernelspec: + name: python3 + display_name: Python 3 (ipykernel) + language: python +--- + (examples-tutorial-modeling-magnets)= -# Modeling a real magnet +# Modeling a Real Magnet + +This tutorial demonstrates how to configure Magpylib simulations to align with experimental measurements using datasheets provided by magnet manufacturers. It offers insights into the fundamental properties of permanent magnets. We gratefully acknowledge the support of [BOMATEC](https://www.bomatec.com/de), who provided high-quality datasheets and supplied the magnets used in the experimental demonstration below. + +## Remanence + +Magnet datasheets typically include a table of technical material specifications, including the remanence $B_r$. This value corresponds to the `polarization` magnitude in Magpylib when no material response is modeled (typically 95% correct). The remanence is usually given as a range to account for material tolerances. + +![data sheet snippet](../../../_static/images/examples_tutorial_magnet_table.png) + +## Material Response + +Magpylib magnets represent bodies with homogeneous magnetic polarization. A real magnet, however, has material response and "demagnetizes" itself resulting in a polarization distribution throughout the magnet that is inhomogeneous and always lower than the remanence. A Magpylib magnet, being a homogeneous body, can only model the mean of the reduced polarization. The following sections explain what happens, and how to estimate the correct value. + +%How much lower depends strongly on the shape of the magnet and its demagnetization curve, characterized in simple cases by material coercivity $H_{c,J}$ or permeability $\mu_r$. -Whenever you wish to compare Magpylib simulations with experimental data obtained using a real permanent magnet, you might wonder how to properly set up a Magpylib magnet object to reflect the physical permanent magnet in question. The goal of this tutorial is to explain how to extract this information from respective datasheets, to provide better understanding of permanent magnets, and show how to align Magpylib simulations with experimental measurements. +More detailed explanations of these phenomena are found on the [Encyclopedia Magnetica](https://www.e-magnetica.pl/doku.php/coercivity), or in the textbook [Introduction to the Theory of Ferromagnetism](https://books.google.at/books/about/Introduction_to_the_Theory_of_Ferromagne.html?id=9RvNuIDh0qMC&redir_esc=y). If you are interested in modelling inhomogeneous magnets you can make use of the [magpylib-material-response](https://github.com/magpylib/magpylib-material-response) package. -This tutorial was supported by [BOMATEC](https://www.bomatec.com/de) by providing excellent data sheets and by supplying magnets for the experimental demonstration below. -The fact that you can model even complex magnet structures with high accuracy is demonstrated in our example on [magnetic scales](examples-app-scales). +%and is expressed in the data sheet through the permeance coefficient lines (grey lines). The numbers at the end indicate the typical magnet length to diameter ratio (L/D). you should find B-H curves and J-H curves. These curves coincide at $H=0$, giving the intrinisic material remanence $B_r$. This is the `polarization` magnitude of a magpylib magnet when there is no material response. -## Short summary +%The remanence is typically given as a range accounting for production tolerances -In a magnet data sheet, you should find B-H curves and J-H curves. These curves coincide at H=0, which gives the intrinsic material remanence $B_r$. As a result of material response and self-interaction, the magnet "demagnetizes" itself so that the mean magnetic polarization of a real magnet is always below the remanence. How much below depends strongly on the shape of the magnet and is expressed in the data sheet through the permeance coefficient lines (grey lines). The numbers at the end indicate the typical magnet length to diameter ratio (L/D). +%However, a real magnet has material response and "demagnetizes" itself so that the mean magnetic polarization is always below the intrinsic remanence. -To obtain the correct magnetic polarization of a magnet from the data sheet, one must find the crossing between B-H curve and respective permeance coefficient line. This gives the "working point" which corresponds to the mean demagnetizing H-field inside the magnet. The correct polarization to use in the Magpylib simulation is the J-value at the working point which can be read off from the J-H curve. +%As a result of material response and self-interaction, the magnet "demagnetizes" itself so that the mean magnetic polarization of a real magnet is always below the remanence. How much below depends strongly on the shape of the magnet and is expressed in the data sheet through the permeance coefficient lines (grey lines). The numbers at the end indicate the typical magnet length to diameter ratio (L/D). -![data sheet snippet](../../../_static/images/examples_tutorial_magnet_datasheet2.png) +%To obtain the correct magnetic polarization of a magnet from the data sheet, one must find the crossing between B-H curve and respective permeance coefficient line. This gives the "working point" which corresponds to the mean demagnetizing H-field inside the magnet. The correct polarization to use in the Magpylib simulation is the J-value at the working point which can be read off from the J-H curve. -The following sections provide further explanation on the matter. +%![data sheet snippet](../../../_static/images/examples_tutorial_magnet_datasheet2.png) -## Hysteresis loop +%The following sections provide further explanation on the matter. -If you've worked with magnetism, chances are very high that you have seen a magnetic hysteresis loop. Hysteresis loops describe the connection between the **mean values** of an externally applied H-field and the resulting B-field, polarization J or magnetization M **within a defined volume**. This connection depends strongly on the size and shape of this volume and what is inside and what is outside. +### Intrinsic Hysteresis Loop -The B-H curve is called the "normal loop", while J-H and M-H curves are called "intrinsic loops". Hereon we only make use of the J-H loops, but the discussion is similar for M-H. Normal and intrinsic loops are connected via $B = \mu_0 H + J$. In free space the B-H connection is just a straight line defined via $B = \mu_0 H$. When the whole space is filled with magnetic material you will see something like this within an arbitrary volume: +The intrinsic hysteresis loop relates the controlled external H-field to the resulting polarization $J$ (or magnetization $M$) in a material sample volume. The intrinsic loop does not depend on sample volume size, shape, and surroundings, but only on the material properties and the history $H(t)$ - with different histories the same H-field can result in different polarizations. The intrinsic hysteresis loop of a material used for permanent magnets typically looks like the following: + +%Hysteresis loops relate the applied H-field to the resulting B-field, polarization J, or magnetization M of a material. +%*within a defined volume*. This relationship strongly depends on the volume’s size, shape, and surrounding materials. +%The B–H curve is known as the *normal loop*, while J–H (or M–H) curves are called *intrinsic loops*. The normal loops show the +%In this tutorial, we focus on J–H loops; the discussion for M–H is analogous. Normal and intrinsic loops are related by $ B = \mu_0 H + J $. In free space, $ B = \mu_0 H $ is a straight line, but if the entire space is filled with magnetic material, the loop within any volume will show nonlinear behavior. + +%The B-H curve is called the "normal loop", while J-H (and M-H) curves are called "intrinsic loops". Hereon we only make use of the J-H loops, but the discussion is similar for M-H. Normal and intrinsic loops are connected via $B = \mu_0 H + J$. In free space the B-H connection is just a straight line defined via $B = \mu_0 H$. When the whole space is filled with magnetic material you will see something like this within an arbitrary volume: ::::{grid} 2 :::{grid-item} -:columns: 3 +:columns: 1 ::: :::{grid-item} -:columns: 6 +:columns: 10 ![hysteresis loops](../../../_static/images/examples_tutorial_magnet_hysteresis.png) ::: :::: -**1st quadrant**: Initially we have $J=0$ and $H=0$. The magnetic material is not magnetized, and no external H-field is applied. When increasing the H-field, the material polarization will follow the "virgin curve" and will increase until it reaches its maximum possible value, the saturation polarization $J_S$. Higher values of $H$ will not affect $J$, while $B$ will keep increasing linearly. Now we are on the "major loop" - we will never return to the virgin curve. After reaching a large H-value we slowly turn the H-field off. As it drops to zero the material will retain its strong polarization at saturation level while the resulting $B$ decreases. At $H = 0$ the B-field then approaches the "remanence field" $B_r$, and its only contribution is $J_S$. - -**2nd quadrant**: Now the H-field becomes negative. Its amplitude increases but it is oriented opposite to the initial direction. Therefore, it is also opposite to the magnetic polarization. In the 2nd quadrant we are now trying to actively demagnetize the material. This part of the hysteresis loop is often referred to as the "demagnetization curve". With increasing negative H, the B-field continues to become smaller until it reaches zero at the "coercive field" $H_c$. At this point the net B-field inside the volume is zero, however, the material is still magnetized! In the example loop above, the polarization at $H_c$ is still at the $J_S$ level. By increasing the H-field further, a point will be reached where the material will start to demagnetize. This can be seen by the non-linear drop of $J$. The point where $J$ reaches zero is called the "intrinsic coercive field" $H_{ci}$. At this point the net polarization in the observed volume is zero. The material is demagnetized. The intrinsic coercive field is a measure of how well a magnetized material can resist a demagnetizing field. Having large values of $H_{ci}$ is a property of "hard magnets", as they can keep their magnetization $J$ even for strong external fields in the opposite direction. +**1st quadrant (create a magnet):** +We start with $ J = 0 $ and $ H = 0 $; the material is unmagnetized and there is no magnetic field. We increase $H$ and the polarization $ J $ follows the *virgin curve* — a nonlinear rise toward the saturation polarization $ J_s $. Beyond this point, further increases in $ H $ have no effect on $ J $; the material is saturated. We are now on the *major loop* and cannot easily return to the virgin curve, because as we gradually reduce $H$ back to zero, the material retains a high level of polarization. At $ H = 0 $, the remaining polarization is called the *remanent polarization* $ J_r $, which equals the remanent flux density $ B_r $. We have now created a permanent magnet. -**3rd and 4th quadrants**: Moving to the third quadrant the behavior is now mirrored. As $H$ increases past $H_{ci}$, polarization quickly aligns with the external field and the material becomes saturated $J=-J_S$. By turning the field around again, we move through the fourth quadrant to complete the hysteresis loop. +**2nd quadrant (demagnetize a magnet):** +Next, we increase the H-field in magnitude but make it point opposite to the initial direction, thus acting against the existing polarization. Initially, $ J $ remains nearly unchanged, but as the opposing field strengthens, the response becomes nonlinear and $ J $ decreases rapidly. When $ J $ reaches zero, the H-field equals the *intrinsic coercive field* $ H_{c,J} $, a value that characterizes the material’s resistance to demagnetization. In this quadrant the hysteresis loop is often called the *demagnetization curve*. Materials with a large $ H_{c,J} $ (also denoted $ H_{ci} $) are referred to as *hard magnetic*, capable of maintaining their magnetization even under strong opposing fields. -Hysteresis in magnetism as presented here is a macroscopic model that is the result of a complex interplay between dipole and exchange interaction, material texture and resulting domain formation at a microscopic level. Details can be found, for example, in Aharoni's classical textbook "Introduction to the Theory of Ferromagnetism". +**3rd and 4th quadrants:** +In the third quadrant, the behavior mirrors that of the first: as $ H $ increases beyond $ H_{c,J} $ in the negative direction, the polarization rapidly aligns with the field, reaching saturation at $ J = -J_s $. Reversing the field once more brings us through the fourth quadrant, completing the hysteresis loop. -## The demagnetizing field +Magnetic hysteresis, as described here, is a macroscopic phenomenon arising from a complex interplay of dipole and exchange interactions, material anisotropy, and domain formation at the microscopic level. -If in an application the applied external H-field is zero, it seems intuitive to use the remanence $B_r$ for the magnetic polarization in the Magpylib simulation. It is a value that you will find in the data sheet. +### The demagnetizing field -![data sheet snippet](../../../_static/images/examples_tutorial_magnet_table.png) - -However, if considering $J=B_r$, you will quickly see that the experimental results are up to ~30 % below of what you would expect. The reason for this is the self-induced demagnetizing field of the magnet which is generated by the magnetic polarization itself. Just like $J$ generates an H-field on the outside of the magnet, it also generates an H-field inside the magnet, along the opposite direction of the polarization. The H-field outside the magnet is known as the stray field, and the H-field inside the magnet is called the demagnetizing field (as it opposes the magnetic polarization). This is demonstrated by the following figure: +If the applied H-field is zero in an experiment, it may seem intuitive to use the remanent flux density $ B_r $ as the magnetic polarization amplitude in a Magpylib simulation. However, this approach neglects the strong opposing field that the magnet generates within itself. Just as the polarization $ J $ produces a magnetic field $H$ outside the magnet (known as the *stray field*), it also induces an H-field on the inside which is mostly opposed to $J$ called the *demagnetizing field*. This is illustrated by the following simulation: ![demagnetization field simulation](../../../_static/images/examples_tutorial_magnet_fieldcomparison.png) -Making use of the [streamplot example](examples-vis-mpl-streamplot), on the left side we show the cross-section of a Cuboid magnet and its homogeneous polarization. And on the right, we see the H-field generated by it. Inside the magnet the generated H-field opposes the polarization. As a result, the polarization of a magnet will not be $B_r$, but instead will be some value of $J$ in the 2nd quadrant of the J-H loop that corresponds to the mean H-field inside the magnet. This H-value is often referred to as the "working point". +Building on the [streamplot example](examples-vis-mpl-streamplot), the left panel shows the cross-section of a cuboid magnet with uniform polarization, while the right panel illustrates the H-field generated by this magnet. Inside the magnet, the H-field is opposed to $J$. The average H-field inside the magnet is commonly referred to as the *working point* of the magnet. + +Consequently, the magnet’s mean polarization is not equal to $ B_r $, but is instead a lower value $ J $ on the *demagnetization curve* (2nd quadrant, as the H-field is opposite to the polarization) corresponding to the working point. -## Finding the correct polarization +(examples-tutorial-modeling-magnets-findJ)= +### Finding the Correct Polarization -As explained above, the hysteresis loop depends strongly on the chosen observation volume geometry, the material inside, and what is outside of the volume. Magnet manufacturers provide such loops (usually only the 2nd quadrant) for their magnets, meaning that the observation volume is the whole magnet with air outside. +To determine the correct mean polarization of a magnet, we must compute its *working point* — the average internal H-field — and then read the corresponding $ J $ from the respective J–H curve provided in the data sheet. However, calculating this internal field is not trivial, as it depends not only on the magnet geometry but also on it's material response. -To obtain the correct mean polarization of a magnet we simply must compute the mean demagnetizing field (= working point) and read the resulting $J$ off the provided J-H loop. Computing the mean demagnetizing field, however, is not a simple task. In addition to the material response (permeability), it depends strongly on the magnet geometry. Fortunately, the working points can be read off from well written data sheets. +Fortunately, well-prepared datasheets often include working point information directly. ![data sheet snippet](../../../_static/images/examples_tutorial_magnet_datasheet.png) -This datasheet snippet shows the second quadrant of the B-H and J-H loops, even for two different temperatures. The working point is given by the intersection between the "permeance coefficient" lines (gray) and the B-H curve. The number at the end of these lines indicate the length to diameter ratio (L/D) of the magnet, which is the critical geometric factor. Different lines are for different L/D values, which allows one to select the correct working point for different magnets made from this specific material. Once the working point is found, the correct magnetic polarization, here denoted by $J_W$ (the magnetic polarization at the working point), can be read off. The following figure exemplifies the changes in $J_W$ for different L/D values, considering a cylinder magnet: +The snippet above shows the second quadrant of the J–H loop for two temperatures. The lower curves are the corresponding B–H loops (simply $B=J+\mu_0 H$). The working point is found at the intersection of the *permeance coefficient* lines (gray) with the B–H curves. The labels at the ends of these lines indicate the magnet’s length-to-diameter (L/D) ratio, a key geometric factor influencing internal demagnetization. Each line represents a different L/D value, enabling you to identify the correct working point for a specific magnet geometry. + +Once identified, the corresponding magnetic polarization $ J_W $ can be read directly from the J–H curve. The figure below illustrates how $ J_W $ varies with L/D for a cylindrical magnet: ![finding the working point](../../../_static/images/examples_tutorial_magnet_LDratio.png) -Permanent magnets with different geometries, such as a parallelepiped shape, will have different behavior in terms of L/D values. Make sure you are reading the data from the correct part number. -## Warning +%Keep in mind that magnets with different shapes, such as parallelepipeds, will exhibit different L/D behavior. Always ensure you refer to the correct section of the datasheet that matches your magnet’s geometry and part number. + +## Sources of Error + +Keep in mind that there are many factors that can cause discrepancies between your simulation and experimental results. Below are some of the most common issues: -Keep in mind that there are still many reasons why your simulation might not fit well to your experiment. Here are some of the most common problems: +1. **Sensor position errors:** Even small misalignments — less than 100 µm — can significantly affect the measurement. The sensitive element inside a sensor package may be offset by 10–100 µm or slightly rotated, introducing notable deviations. + +2. **Sensor errors:** The sensor may not be properly calibrated or may operate outside its optimal measurement range. Even well-calibrated sensors can exhibit systematic errors of several percent of their full scale. In addition, sensitive elements, e.g. Hall cells, are finite sized and return the mean field in their sensitive volume. In comparison, Magpylib considers ideal point-wise sensors with zero offset, infinite linear range and zero noise. + +3. **External stray fields:** Ambient magnetic fields — such as the geomagnetic field — or interference from nearby electronic equipment or magnetic objects (e.g. metal stands or tools) can distort the measurements. + +4. **Magnet quality deviations:** Off-the-shelf magnets often deviate from their datasheet specifications: + - The magnetic polarization amplitude may vary by several percent due to prior demagnetization or lower material grade. + - The polarization direction can differ by a few degrees from the nominal axis. + - Inhomogeneous polarization is common, particularly in injection-molded magnets. -1. Small position errors of less than 100 um can have a large impact on the measurement result. The reference should be the sensitive element inside a sensor package. These elements can be displaced by 10-100 um and even rotated by a few degrees. The sensor might also not be well calibrated. -2. There are external stray fields, like the earth magnetic field, that influence the measurements. Those might also come from other nearby electronic equipment or magnetic parts. -3. Off-the-shelf magnets often do not hold what is promised in the datasheet - - Magnetic polarization amplitude can be off by 5-10 %. - - The direction of polarization often varies by up to a few degrees. - - Worst-case: the polarization can be inhomogeneous which is often a problem in injection-mold magnets. ## Example -::::{grid} 2 -:::{grid-item} -:columns: 3 -::: -:::{grid-item} -:columns: 6 -![](../../../_static/images/examples_icon_WIP.png) -::: -:::: +In this example, we demonstrate how to align theory and experiment by measuring and simulating the linear motion of a magnet above a sensor. + +We use a cylindrical neodymium magnet from Bomatec with diameter $ D = 8\,\mathrm{mm} $, height $ H = 2\,\mathrm{mm} $, axial magnetization, and made of BMN35 material with a nominal remanence of $ B_{r,\mathrm{nom}} = 1220\,\mathrm{mT} $ and a minimum guaranteed value of $ B_{r,\mathrm{min}} = 1170\,\mathrm{mT} $. As the sensor, we use an Infineon TLE493D, a true 3D Hall sensor. + +Both components are mounted on our custom test bench that allows precise relative positioning between the magnet and sensor in six degrees of freedom. + +![](../../../_static/images/examples_tutorial_magnet_exp01.png) + +While the relative positioning is highly accurate — limited mainly by the motor resolution, typically a few micrometers and millidegrees — absolute positioning remains a challenge. Even with precise tools (e.g. touch probe, interferometer), achieving accurate 6D alignment between the sensor and the magnet is difficult. This is further complicated by unknown offsets or misalignments of the sensing element within the sensor package. + +Due to the lack of a robust common reference, we position the sensor below the magnet by a combination of visual alignment and touch probing, estimating a surface-to-surface distance of 6 mm. + +The magnet is then moved linearly along the x-axis from –15 mm to +15 mm in 61 steps. At each step, we perform 1000 measurements and average the results to reduce sensor noise. + +We now compare this experimental dataset with a Magpylib simulation, using the same geometry and nominal magnet properties. + +```{code-cell} ipython3 +:tags: [hide-input] + +import numpy as np +import matplotlib.pyplot as plt +import magpylib as magpy + +# Testbench positioning +xs = np.linspace(-15, 15, 61) + +# Load experimental data +data_path = '../../../_static/data/Bomatec_LD025_scan0.npy' +Bexp = np.load(data_path) # shape should be (61, 3) + +# Simulation setup +sensor = magpy.Sensor(position=(0, 0, 0)) +magnet = magpy.magnet.Cylinder( + polarization=(0, 0, 1200), # in mT + dimension=(8, 2), # in mm + position=[(x,0,7) for x in xs], # z=7 mm center height +) +Bsim = sensor.getB(magnet) + +# Plotting function +def plot_field(Bsim, Bexp, xs): + """Plot results.""" + fig, axes = plt.subplots(1, 3, figsize=(12,3.5), sharey=True) + + labels = ['Bx Field (mT)', 'By Field (mT)', 'Bz Field (mT)'] + for i,ax in enumerate(axes): + ax.plot(xs, Bsim[:, i], color='b', label='Simulation') + ax.plot(xs, Bexp[:, i], ls='', marker='.', color='orange', label='Experiment') + + ax.set_title(labels[i]) + ax.set_xlabel('Magnet Position (mm)') + ax.grid(color='.85') + ax.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +plot_field(Bsim, Bexp, xs) +``` + +The figure shows general qualitative and quantitative agreement between simulation and measurement. However, the remaining discrepancies are still significant — unsurprising, as we have not yet accounted for any tolerances. + +To address this, we introduce seven tolerance parameters: +- The sensor may be displaced in all six degrees of freedom: three translations and three rotations. +- Additionally, the exact value of the magnet's polarization is not known precisely. + +We incorporate these tolerances into our simulation model and estimate their values by fitting to the experimental data. To do this, we use a differential evolution algorithm that minimizes a cost function based on the squared differences between simulated and measured magnetic field values. + +The following code demonstrates how this optimization is performed: + +```{code-cell} ipython3 +:tags: [hide-input] + +# Continuation of previous code cell + +from scipy.optimize import differential_evolution + +def simulate_field(px, py, pz, alpha, beta, gamma, J): + """Simulate the magnetic field in our experiment with tolerances. + + Parameters: + - px, py, pz: Sensor misalignment in mm + - alpha, beta, gamma: Sensitive rotation in degrees + - J: Magnetic polarization ampliotude in mT + """ + sensor = magpy.Sensor(position=(px, py, pz)) + sensor.rotate_from_euler((alpha, beta, gamma), "xyz") + magnet = magpy.magnet.Cylinder( + polarization=(0, 0, J), + dimension=(8, 2), + position=[(x, 0, 7) for x in xs], + ) + return sensor.getB(magnet) -coming soon: -1. Magpylib simulation code -2. Experimental data -3. Comparison and discussion +def cost_function(params, Bexp): + """Returns the quadratic difference between simulated and measured fields.""" + px, py, pz, alpha, beta, gamma, J = params + Bsim = simulate_field(px, py, pz, J, alpha, beta, gamma) + mse = sum(np.linalg.norm(Bexp - Bsim, axis=1) ** 2) / len(Bexp) + return mse -**Exterior reference** -G. Martinek, S. Ruoho and U. Wyss. (2021). *Magnetic Properties of Permanents Magnets & Measuring Techniques* [White paper]. Arnold Magnetic Technologies. https://www.arnoldmagnetics.com/blog/measuring-permanent-magnets-white-paper/ +def run_optimization(): + """Finds the best parameters for the simulation by minimizing the cost function.""" + bounds = [ + (-1, 1), # px: Sensor x-position (mm) + (-1, 1), # py: Sensor y-position (mm) + (-1, 1), # pz: Sensor z-position (mm) + (-10,10), # alpha: Sensor rotation around x-axis (deg) + (-10,10), # beta: Sensor rotation around y-axis (deg) + (-10,10), # gamma: Sensor rotation around z-axis (deg) + (1000, 1300), # J: Magnet Polarization (mT) + ] + result = differential_evolution( + cost_function, + bounds=bounds, + args=(Bexp,), + popsize=25, + maxiter=1000, + ) + return result.x + +# We skip running the optimizer live here. +# Fitted result from prior optimization: +result = [ + 0.7209, # px (mm) + -0.1348, # py (mm) + 0.3692, # pz (mm) + -1.018, # alpha (deg) + 1.099, # beta (deg) + 1.131, # gamma (deg) + 1126.0 # J (mT) +] + +# Simulate with optimized parameters +Bsim = simulate_field(*result) + +# Plot comparison +plot_field(Bsim, Bexp, xs) +``` + +Now we find excellent agreement between simulation and experiment. + +Let’s take a closer look at the fitted parameters: + +- The estimated sensor displacement is less than 1 mm in all directions — remarkably close, considering the positioning was done “by eye.” The rotational misalignments are also modest, below 1.5°. Yet, despite these relatively small deviations, they had a significant impact on the first simulation result. This highlights how sensitive magnetic field measurements can be to even subtle misalignments. + +- The fitted magnetic polarization is approximately **1126 mT**, which is notably below the specified minimum remanence value of $ B_{r,\mathrm{min}} = 1170\,\mathrm{mT} $. However, if we return to the [datasheet](examples-tutorial-modeling-magnets-findJ) and take demagnetization effects into account, this result becomes plausible. In fact, 1126 mT corresponds well to the polarization in the expected working point when the material remanence is near its lower limit. + +![](../../../_static/images/examples_tutorial_magnet_exp02.png) + +In this datasheet snippet, the green curves are parallel-shifted versions of the 20 °C hysteresis loops, representing materials with a remanence of about 1170 mT. The blue scale on the right offers finer resolution in the region of interest. The working point given by the 0.25-permeance line (D8H2) lies at about 720 kA/m, where $J$ is reduced to about 1125 mT confirming that the fitted polarization falls within a physically meaningful range. + +## A Final Word of Caution + +Despite their effectiveness for capturing key qualitative and quantitative effects, it is important to emphasize that analytical models with homogeneous polarization may not faithfully represent the true underlying polarization distribution. Fitting remains a challenge in magnetic inverse problems such as this one, where different physical configurations can yield nearly indistinguishable field profiles. + +%**Exterior reference** +%G. Martinek, S. Ruoho and U. Wyss. (2021). *Magnetic Properties of Permanents Magnets & Measuring Techniques* [White paper]. Arnold Magnetic Technologies. https://www.arnoldmagnetics.com/blog/measuring-permanent-magnets-white-paper/ diff --git a/docs/_static/data/Bomatec_LD025_scan0.npy b/docs/_static/data/Bomatec_LD025_scan0.npy new file mode 100644 index 000000000..c5176afbb Binary files /dev/null and b/docs/_static/data/Bomatec_LD025_scan0.npy differ diff --git a/docs/_static/images/examples_tutorial_magnet_datasheet2.png b/docs/_static/images/examples_tutorial_magnet_datasheet2.png deleted file mode 100644 index 8dfa38689..000000000 Binary files a/docs/_static/images/examples_tutorial_magnet_datasheet2.png and /dev/null differ diff --git a/docs/_static/images/examples_tutorial_magnet_exp01.png b/docs/_static/images/examples_tutorial_magnet_exp01.png new file mode 100644 index 000000000..245a4f833 Binary files /dev/null and b/docs/_static/images/examples_tutorial_magnet_exp01.png differ diff --git a/docs/_static/images/examples_tutorial_magnet_exp02.png b/docs/_static/images/examples_tutorial_magnet_exp02.png new file mode 100644 index 000000000..d6821b08b Binary files /dev/null and b/docs/_static/images/examples_tutorial_magnet_exp02.png differ diff --git a/docs/_static/images/examples_tutorial_magnet_hysteresis.png b/docs/_static/images/examples_tutorial_magnet_hysteresis.png index 2378539e2..5a95cb4af 100644 Binary files a/docs/_static/images/examples_tutorial_magnet_hysteresis.png and b/docs/_static/images/examples_tutorial_magnet_hysteresis.png differ diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 627f817fb..b049cb446 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -78,7 +78,7 @@ def test_input_objects_pixel_good(pixel): """good input: magpy.Sensor(pixel=pixel)""" sens = magpy.Sensor(pixel=pixel) - np.testing.assert_allclose(sens.pixel, pixel) + np.testing.assert_allclose(sens.pixel, pixel) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -164,7 +164,7 @@ def test_input_objects_current_good(current): if current is None: assert src.current is None else: - np.testing.assert_allclose(src.current, current) + np.testing.assert_allclose(src.current, current) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -202,7 +202,7 @@ def test_input_objects_diameter_good(diameter): if diameter is None: assert src.diameter is None else: - np.testing.assert_allclose(src.diameter, diameter) + np.testing.assert_allclose(src.diameter, diameter) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -242,7 +242,7 @@ def test_input_objects_vertices_good(vertices): if vertices is None: assert src.vertices is None else: - np.testing.assert_allclose(src.vertices, vertices) + np.testing.assert_allclose(src.vertices, vertices) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -289,8 +289,8 @@ def test_input_objects_magnetization_moment_good(pol_or_mom): assert src.polarization is None assert src2.moment is None else: - np.testing.assert_allclose(src.polarization, pol_or_mom) - np.testing.assert_allclose(src2.moment, pol_or_mom) + np.testing.assert_allclose(src.polarization, pol_or_mom) # type: ignore[attr-defined] + np.testing.assert_allclose(src2.moment, pol_or_mom) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -337,7 +337,7 @@ def test_input_objects_dimension_cuboid_good(dimension): if dimension is None: assert src.dimension is None else: - np.testing.assert_allclose(src.dimension, dimension) + np.testing.assert_allclose(src.dimension, dimension) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -379,7 +379,7 @@ def test_input_objects_dimension_cylinder_good(dimension): if dimension is None: assert src.dimension is None else: - np.testing.assert_allclose(src.dimension, dimension) + np.testing.assert_allclose(src.dimension, dimension) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -425,7 +425,7 @@ def test_input_objects_dimension_cylinderSegment_good(dimension): if dimension is None: assert src.dimension is None else: - np.testing.assert_allclose(src.dimension, dimension) + np.testing.assert_allclose(src.dimension, dimension) # type: ignore[attr-defined] @pytest.mark.parametrize( @@ -718,9 +718,9 @@ def test_input_rotate_axis_bad(axis): @pytest.mark.parametrize( "observers", [ - magpy.Sensor(position=(1, 1, 1)), - magpy.Collection(magpy.Sensor(position=(1, 1, 1))), - magpy.Collection(magpy.Sensor(), magpy.Sensor()), + magpy.Sensor(position=(0.1, -1, 0.3)), + magpy.Collection(magpy.Sensor(position=(0.1, -1, 0.3))), + magpy.Collection(magpy.Sensor(position=(0.1, -1, 0.3)), magpy.Sensor()), (1, 2, 3), [(1, 2, 3)] * 2, [[(1, 2, 3)] * 2] * 3, @@ -1009,112 +1009,4 @@ def test_magnet_polarization_magnetization_input(): c = magpy.magnet.Cuboid() c.magnetization = mag np.testing.assert_allclose(mag, c.magnetization) - np.testing.assert_allclose(mag * (4 * np.pi * 1e-7), c.polarization) - - -def test_current_sheet_init(): - """good inputs""" - cds = [(1, 0, 0)] * 5 - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 3)) - - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - -def test_current_strip_init(): - """good inputs""" - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - magpy.current.TriangleStrip( - current=1, - vertices=verts, - ) - - -def test_current_sheet_init_bad(): - """bad inputs""" - - # verts.len < 3 - cds = [(1, 0, 0)] * 5 - verts = ((0, 0, 0), (1, 0, 0)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 3)) - - with pytest.raises(ValueError): # noqa: PT011 - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - # faces.len != cds.len - cds = [(1, 0, 0)] * 3 - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 3)) - - with pytest.raises(ValueError): # noqa: PT011 - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - # bad face indices - cds = [(1, 0, 0)] * 5 - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 33)) - - with pytest.raises(IndexError): - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - # bad input formats - cds = 1 - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 33)) - - with pytest.raises(MagpylibBadUserInput): - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - # bad input formats - cds = [(1, 2)] * 5 - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - facs = ((0, 1, 2), (0, 2, 3), (0, 1, 3), (1, 2, 3), (1, 2, 33)) - - with pytest.raises(MagpylibBadUserInput): - magpy.current.TriangleSheet( - current_densities=cds, - vertices=verts, - faces=facs, - ) - - -def test_current_strip_init_bad(): - """bad inputs""" - verts = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - curr = [1] * 4 - with pytest.raises(MagpylibBadUserInput): - magpy.current.TriangleStrip( - current=curr, - vertices=verts, - ) - - verts = ( - (0, 0, 0), - (1, 0, 0), - ) - curr = 1 - with pytest.raises(MagpylibBadUserInput): - magpy.current.TriangleStrip( - current=curr, - vertices=verts, - ) + np.testing.assert_allclose(mag * (4 * np.pi * 1e-7), c.polarization) # type: ignore[attr-defined]