diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml deleted file mode 100644 index 839ee54f8..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: Report a problem ๐ -description: Problem reports are for when something behaves incorrectly, or differently from how you'd expect. -labels: ["bug"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - type: dropdown - id: version - attributes: - label: Magpylib version - description: What version of Magpylib are you running? - options: - - 5.x (Latest) - - 5.x (Unreleased) - - 4.x - - 3.x - - 2.x - - 1.x - validations: - required: true - - type: textarea - id: what-happened - attributes: - label: What happened? - placeholder: Tell us what you see! Also tell us, what did you expect to happen? - validations: - required: true - - type: textarea - id: code-example - attributes: - label: Code example - description: | - A complete, minimal, self-contained example code that reproduces the issue. - This will be automatically formatted into code, so no need for backticks. - placeholder: | - import numpy as np - import magpylib as magpy - - cuboid = magpy.magnet.Cuboid(polarization=(0.,0.,1.), dimension=(1.,1.,1.)) - cuboid.move(np.linspace((0,0,0), (0,0,10), 11), start=0) - cuboid.show() - render: Python - - type: textarea - id: addition-context - attributes: - label: Additional context - placeholder: | - OS: [e.g. Linux 32bit, MacOs Monterey, Windows 10 64bit] - IDE: [e.g. Spyder, PyCharm, VSCode, Jupyterlab] diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml deleted file mode 100644 index f7424db92..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Request an enhancement ๐ก -description: Suggest an idea for this project -labels: ["feature-request"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this feature request report! - - type: textarea - id: feature-wish - attributes: - label: Describe the feature you'd like - description: | - A clear and concise description of what the problem is and/or what you want to happen. - placeholder: I'm always frustrated when .. - - type: textarea - id: considered-options - attributes: - label: Alternatives or solutions considered - description: A clear and concise description of any alternative solutions or features you've considered. - - type: textarea - id: addition-context - attributes: - label: Additional context - description: Add any other context or screenshots about the feature request here. - diff --git a/.github/ISSUE_TEMPLATE/question_magnetics.yaml b/.github/ISSUE_TEMPLATE/question_magnetics.yaml deleted file mode 100644 index 2eb6998a6..000000000 --- a/.github/ISSUE_TEMPLATE/question_magnetics.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: Questions or general discussion with the community ๐ฑ -description: This is for general conversations that aren't meant for actionable Issues. We try to answer your questions about magnetic Systems and Magpylib. -labels: ["question"] -body: - - type: markdown - attributes: - value: | - Thanks for participating in the Magpylib community - - type: textarea - id: what-happened - attributes: - label: What is your question? - description: We try to answer your questions about magnetic Systems and Magpylib - validations: - required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7bcadbae4..a58e337f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1 @@ # Related Issues - -# Notes - diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d7a6835b5..f9046a523 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,20 +24,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 if: ${{ matrix.language == 'javascript' || matrix.language == 'python' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4d06205f8..78adddfd7 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Set up testing tools and environment for pylint @@ -38,20 +38,18 @@ jobs: - "3.12" - "3.11" - "3.10" - - "3.9" - - "3.8" os: - ubuntu-latest - macos-latest - windows-latest steps: - name: Setup headless display - uses: pyvista/setup-headless-display-action@v2 + uses: pyvista/setup-headless-display-action@v3 - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup python for test ${{ matrix.py }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} - name: Install tox @@ -72,10 +70,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v4 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.10" - name: Install flit run: pip install flit - name: Build package diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c20e9ff8e..393552ab1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -12,20 +12,20 @@ repos: language_version: python3 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort name: isort (python) - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: "24.4.0" + rev: "25.1.0" hooks: - id: black diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a9d46a073..17ed253d1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.10" # You can also specify other tool versions: # nodejs: "16" # rust: "1.55" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae771a2c..62c67d76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,40 @@ -All notable changes to magpylib are documented here. - - # Changelog +## [Unreleased] - YYYY-MM-DD +- Improved docstrings by adding examples where missing and by using rounding in all examples to avoid doctest fails +- Fixed a bug where a core getH would return the B-field +- Input `in_out` is by default set to `"auto"` for collections to avoiding ambiguities. +- Improve documentation ([#829](https://github.com/magpylib/magpylib/issues/829)) + +## [5.1.1] - 2024-10-31 +- Included magpylib-force version 0.3.1 in documentation +- Removed unused issue templates and improved PR template + +## [5.1.0] - 2024-10-09 +- Fixed a bug where the magnetization arrow graphical representation would be anchored badly after rotation ([#805](https://github.com/magpylib/magpylib/issues/805)) +- Added `units_length` input to the `show` function to allow displaying axes with different length units. This parameter can be set individually for each subplot. ([#786](https://github.com/magpylib/magpylib/pull/786)) +- Small documentation and Readme improvement. Change naming from "explicit expression" to "analytical expression" as described in ([#794](https://github.com/magpylib/magpylib/issues/794)). +- Fixed Pvyvista plot bounds not fitting on animation. Also enables `zoom` feature which was not working until now. ([#798](https://github.com/magpylib/magpylib/pull/798)) +- Fixed canvas layout being modified even when user-provided. Also added a new `canvas_update` parameter to choose the layout behavior (by default `"auto"`) ([#799](https://github.com/magpylib/magpylib/pull/799)) +- Improved documentation ([#766](https://github.com/magpylib/magpylib/issues/766), [#802](https://github.com/magpylib/magpylib/issues/802)) +- Documentation now includes force computation, which is still in a separate package "magpylib-force", but which will be integrated into Magplyib in the coming months. ([#787](https://github.com/magpylib/magpylib/issues/787)) + +## [5.0.4] - 2024-06-18 +- Added support for Numpy 2.0 ([#795](https://github.com/magpylib/magpylib/pull/789)) +- Fixed markers legend not being suppressible ([#795](https://github.com/magpylib/magpylib/pull/789)) + +## [5.0.3] - 2024-06-03 +- Fixed subplot object properties propagation ([#780](https://github.com/magpylib/magpylib/pull/780)) +- Migrate to pydata-sphinx-theme and fix docs search function ([#762](https://github.com/magpylib/magpylib/pull/762)) +- Fixed docs version-switcher ([#782](https://github.com/magpylib/magpylib/pull/782)) + +## [5.0.2] - 2024-05-21 +- Fixed a display issue causing incorrect calculation of view box limits ([#772](https://github.com/magpylib/magpylib/pull/772)) +- Removed support for python 3.8 and 3.9 by now following the scientific-python.org support timelines ([#773](https://github.com/magpylib/magpylib/pull/773)) +- Fixed CI testing with newer backend versions ([#774](https://github.com/magpylib/magpylib/pull/774)) +- Updated site notice to mention the awarded "small development grant" by NumFocus. ([#758](https://github.com/magpylib/magpylib/pull/758)) +- Fixed inaccurate citation year for Yang publication ([#764](https://github.com/magpylib/magpylib/pull/764), with thanks to @feldnerd for the contribution!) + ## [5.0.1] - 2024-04-12 - Fixed a bug where `getBHJM` of a Collection would produce one extra dimension ([#753](https://github.com/magpylib/magpylib/issues/753)) - Fixed a bug where the legend of a deeply nested Collection would be wrong ([#756](https://github.com/magpylib/magpylib/issues/756)) @@ -12,7 +44,7 @@ All notable changes to magpylib are documented here. - The Magpylib inputs and outputs are now in **SI Units**. - The `magnetization` parameter has also been redefined to reflect the true physical magnetization quantity in units of A/m. ### Other Improvements -- The `magnetization` parameter is now codependent with the new `polarization` parameter that is the physical magnetic polarization ([#712](https://github.com/magpylib/magpylib/issues/712)) in units of Tesla. +- The `magnetization` parameter is now codependent with the new `polarization` parameter that is the physical magnetic polarization ([#712](https://github.com/magpylib/magpylib/issues/712)) in units of Tesla - Added `getM` (magnetization) and `getJ` (polarization) top level functions and class methods reminiscent of `getB` and `getH`. - The `in_out` (inside/outside) parameter is added to all field functions (`getBHJM`) to specify the location of the observers relative to the magnet body in order to increase performance ([#717](https://github.com/magpylib/magpylib/issues/717), [#608](https://github.com/magpylib/magpylib/issues/608)) - Review of documentation and adding a few requested things ([#685](https://github.com/magpylib/magpylib/issues/685), some of [#659](https://github.com/magpylib/magpylib/issues/659)) @@ -29,8 +61,8 @@ All notable changes to magpylib are documented here. - Rework CI/CD workflows ([#686](https://github.com/magpylib/magpylib/pull/686)) ## [4.4.1] - 2023-11-09 -- Fix deployment release ([#682](https://github.com/magpylib/magpylib/pull/682)) -- Fix axis mismatch on show/hide of sensor arrows ([#679](https://github.com/magpylib/magpylib/pull/679)) +- Fixed deployment release ([#682](https://github.com/magpylib/magpylib/pull/682)) +- Fixed axis mismatch on show/hide of sensor arrows ([#679](https://github.com/magpylib/magpylib/pull/679)) - Documentation improvements ([#673](https://github.com/magpylib/magpylib/pull/673)) @@ -49,7 +81,7 @@ All notable changes to magpylib are documented here. ## [4.3.0] - 2023-06-25 -- New `TriangularMesh` magnet class added to conveniently work with triangular surface meshes instead of large collections of individual `Triangle` objects. The `TriangularMesh` class performs important checks (closed, connected, oriented) and can directly import pyvista objects and for convex hull bodies. ([#569](https://github.com/magpylib/magpylib/issues/569), [#598](https://github.com/magpylib/magpylib/pull/598)). +- New `TriangularMesh` magnet class added to conveniently work with triangular surface meshes instead of large collections of individual `Triangle` objects. The `TriangularMesh` class performs important checks (closed, connected, oriented) and can directly import Pyvista objects and for convex hull bodies. ([#569](https://github.com/magpylib/magpylib/issues/569), [#598](https://github.com/magpylib/magpylib/pull/598)). - Added magnetization coloring for `matplotlib` backend ([#597](https://github.com/magpylib/magpylib/pull/597)) - New automatic backend behavior, set to a dynamic default `auto` depending on the current environment and the given `canvas`, if provided. ([#617](https://github.com/magpylib/magpylib/pull/617)) - Drop python 3.7 support, following python life cycle. ([#616](https://github.com/magpylib/magpylib/pull/616)) @@ -57,10 +89,10 @@ All notable changes to magpylib are documented here. ## [4.2.0] - 2023-01-27 - (Re)introducing the powerful `misc.Triangle` class that can be used to compute magnetic fields of arbitrarily shaped bodies by approximating their surface with triangular faces. ([#568](https://github.com/magpylib/magpylib/issues/568)) - Introducing the `magnet.Tetrahedron` class as a derivate of the Triangle class. ([#289](https://github.com/magpylib/magpylib/issues/289)) -- Change pyvista plotting defaults when using `show(backend='pyvista')` to fit better with other libraries. ([#551](https://github.com/magpylib/magpylib/issues/551)) +- Change Pyvista plotting defaults when using `show(backend='pyvista')` to fit better with other libraries. ([#551](https://github.com/magpylib/magpylib/issues/551)) - Added code of conduct attempting to align with NumFocus standards ([#558](https://github.com/magpylib/magpylib/issues/558)) - Improved Loop field computation in terms of performance and numerical stability ([#374](https://github.com/magpylib/magpylib/issues/374)) -- Add `magnetization.mode` style to allow showing magnetization direction for any backend ([#576](https://github.com/magpylib/magpylib/pull/576)) +- Added `magnetization.mode` style to allow showing magnetization direction for any backend ([#576](https://github.com/magpylib/magpylib/pull/576)) - Documentation changes: - Correct conda install command - Integration of Triangle and Tetrahedron @@ -69,11 +101,11 @@ All notable changes to magpylib are documented here. - Fixed some bugs, minor performance increase, internal refactoring ## [4.1.2] - 2023-01-15 -- Fix wrong magnetization arrow direction for some edge cases ([#570](https://github.com/magpylib/magpylib/discussions/570), [#571](https://github.com/magpylib/magpylib/issues/571), [#572](https://github.com/magpylib/magpylib/pull/572)) -- Fix cryptic `getB`/`getH` error message ([#562](https://github.com/magpylib/magpylib/issues/562), [#563](https://github.com/magpylib/magpylib/pull/563)) +- Fixed wrong magnetization arrow direction for some edge cases ([#570](https://github.com/magpylib/magpylib/discussions/570), [#571](https://github.com/magpylib/magpylib/issues/571), [#572](https://github.com/magpylib/magpylib/pull/572)) +- Fixed cryptic `getB`/`getH` error message ([#562](https://github.com/magpylib/magpylib/issues/562), [#563](https://github.com/magpylib/magpylib/pull/563)) ## [4.1.1] - 2022-08-11 -- Fix inverted y and z axes colors for sensor representations ([#556](https://github.com/magpylib/magpylib/pull/556)) +- Fixed inverted y and z axes colors for sensor representations ([#556](https://github.com/magpylib/magpylib/pull/556)) ## [4.1.0] - 2022-08-08 - Field computation `getB`/`getH` now supports 2D [pandas](https://pandas.pydata.org/).[dataframe](https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe) in addition to the `numpy.ndarray` as output type. ([#523](https://github.com/magpylib/magpylib/pull/523)) @@ -89,16 +121,16 @@ All notable changes to magpylib are documented here. ## [4.0.3] - 2022-05-13 -- Fix copy order Bug ([#530](https://github.com/magpylib/magpylib/issues/530)) +- Fixed copy order Bug ([#530](https://github.com/magpylib/magpylib/issues/530)) ## [4.0.2] - 2022-05-04 -- Fix magnetization coloring with mesh grouping (plotly) ([#526](https://github.com/magpylib/magpylib/pull/526)) +- Fixed magnetization coloring with mesh grouping (Plotly) ([#526](https://github.com/magpylib/magpylib/pull/526)) - Allow float color quadruples ([#529](https://github.com/magpylib/magpylib/pull/529)) ## [4.0.1] - 2022-04-29 -- Graphic performance update for plotly when showing a large number of objects. ([#524](https://github.com/magpylib/magpylib/pull/524)) +- Graphic performance update for Plotly when showing a large number of objects. ([#524](https://github.com/magpylib/magpylib/pull/524)) ## [4.0.0] - 2022-04-14 @@ -124,15 +156,15 @@ This is a major update that includes - Negative dimension input taken as absolute when only positive dimensions are allowed. - Scale invariant field evaluations. - Special cases caught within 1e-15 rtol and atol to account for numerical imprecision with positioning (e.g. object rotation). - - Suppress numpy divide/invalid warnings. return `np.nan` as `(0,0,0)` (e.g. on magnet edges or on line currents) and allow return of `np.inf`. + - Suppress Numpy divide/invalid warnings. return `np.nan` as `(0,0,0)` (e.g. on magnet edges or on line currents) and allow return of `np.inf`. - New closed form implementation for `Cylinder` with diametral magnetization is much faster (100-1000x) and numerically stable for small `r`. ([#404](https://github.com/magpylib/magpylib/issues/404), [#370](https://github.com/magpylib/magpylib/issues/370)) - Improved numerical stability of current loop field. Now 12-14 correct digits everywhere. ([#374](https://github.com/magpylib/magpylib/issues/374)) - Fixed `Collection` of `Lines` field computation error. ([#368](https://github.com/magpylib/magpylib/issues/368)) - Object oriented interface fixes and modifications: - Improved performance of `getB` and `getH`. - - Fixed array dimension wrongly reduced when `sumup=True` and `squeeze=False` ind `getB` and `getH` functions ([#425](https://github.com/magpylib/magpylib/issues/425), [#426](https://github.com/magpylib/magpylib/pull/426)) + - Fixed array dimension wrongly reduced when `sumup=True` and `squeeze=False` in `getB` and `getH` functions ([#425](https://github.com/magpylib/magpylib/issues/425), [#426](https://github.com/magpylib/magpylib/pull/426)) - Minimal non-squeeze output shape is (1,1,1,1,3), meaning that a single pixel is now also represented. ([#493](https://github.com/magpylib/magpylib/pull/493)) -- With the new kwarg `pixel_agg` it is now possible to apply a numpy function with reducing functionality (like `mean`, `min`, `average`) to the pixel output. In this case, it is allowed to provide `getB` and `getH` with different observer input shapes. ([#503](https://github.com/magpylib/magpylib/pull/503)) +- With the new kwarg `pixel_agg` it is now possible to apply a Numpy function with reducing functionality (like `mean`, `min`, `average`) to the pixel output. In this case, it is allowed to provide `getB` and `getH` with different observer input shapes. ([#503](https://github.com/magpylib/magpylib/pull/503)) ### Major graphic output overhaul: - Styles: @@ -161,10 +193,10 @@ This is a major update that includes - Completely new structure and layout. ([#399](https://github.com/magpylib/magpylib/issues/399), [#294](https://github.com/magpylib/magpylib/issues/294)) - Binder links and live code. ([#389](https://github.com/magpylib/magpylib/issues/389)) - Example galleries with practical user examples -- Guidelines for advanced subclassing of `Collection` to form complex dynamic compound objects that seamlessly integrate into the MAgpylib interface. +- Guidelines for advanced subclassing of `Collection` to form complex dynamic compound objects that seamlessly integrate into the Magpylib interface. ### Geometry interface modification -- Added all scipy Rotation forms as rotation object methods. ([#427](https://github.com/magpylib/magpylib/pull/427)) +- Added all Scipy Rotation forms as rotation object methods. ([#427](https://github.com/magpylib/magpylib/pull/427)) - `move` and `rotate` inputs differentiate between scalar and vector input. Scalar input is applied to the whole path vector input is merged. ([#438](https://github.com/magpylib/magpylib/discussions/438), [#444](https://github.com/magpylib/magpylib/issues/444), [#442](https://github.com/magpylib/magpylib/issues/443)) - `move` and `rotate` methods have default `start='auto'` (scalar input: `start=0`-> applied to whole path, vector input: `start=len_path`-> append) instead of `start=-1`. - `move` and `rotate` methods maintain collection geometry when applied to a collection. @@ -215,7 +247,7 @@ This is a major update that includes ## [3.0.1] - 2021-06-27 -- Add deployment automation ([#260](https://github.com/magpylib/magpylib/issues/260), [#296](https://github.com/magpylib/magpylib/issues/296), [#341](https://github.com/magpylib/magpylib/pull/341), [#342](https://github.com/magpylib/magpylib/pull/342)) +- Added deployment automation ([#260](https://github.com/magpylib/magpylib/issues/260), [#296](https://github.com/magpylib/magpylib/issues/296), [#341](https://github.com/magpylib/magpylib/pull/341), [#342](https://github.com/magpylib/magpylib/pull/342)) --- @@ -232,8 +264,8 @@ This is a major update that includes - The `orientation` attribute stores the relative rotation of an object with respect to the reference orientation (defined in each class docstring). - The default (`orientation=None`) corresponds to a unit rotation. - `orientation` is stored as a `scipy.spatial.transform.Rotation` object. - - Calling the attribute `source.orientation` returns a scipy Rotation object `R`. - - Make use of all advantages of this great scipy package: + - Calling the attribute `source.orientation` returns a Scipy Rotation object `R`. + - Make use of all advantages of this great Scipy package: - define `R.from_rotvec()` or `R.from_quat()` or ... - view with `R.as_rotvec()` or `R.as_quat()` or ... - combine subsequent rotations `R1 * R2 * R3` @@ -290,7 +322,7 @@ This is a major update that includes 2. `sensor.getB(*sources)` 3. `magpylib.getB(sources, observers)` - The output shape is always (L, M, K, N1, N2, ..., 3) with L sources, M path positions, K sensors and N (pixel) positions. - - Objects with shorter paths will be considered as static once their path ends while other paths still continue. + - Objects with shorter paths will be considered as static once their path ends while other paths continue. 4. `magpylib.getBv(**kwargs)` gives direct access to the field formulas and mostly replaces the `getBv_XXX()` functionality of v2. All inputs must be arrays of length N or of length 1 (statics will be tiled). - While `getBv` is the fastest way to compute the fields it is much more convenient to use `getB()` which mostly provides the same performance. Specifically,the new `getB()` automatically groups all inputs for combined vectorized evaluation. This leads to a massive speedup when dealing with large Collections of similar sources. - In addition to `getB`, the new `getH` returns the field in kA/m. @@ -302,8 +334,8 @@ This is a major update that includes ## [2.3.0b] - 2020-01-17 ### Changed -- Improved performance of getB for diametral magnetized Cylinders by 20%. -- GetB of Line current now uses vectorized code which leads to massive performance enhancement. +- Improved performance of `getB` for diametral magnetized Cylinders by 20%. +- `getB` of Line current now uses vectorized code which leads to massive performance enhancement. - **IMPORTANT:** position arguments of `getBv` functions have been flipped! First comes the source position POSm THEN the observer position POSo! @@ -316,7 +348,7 @@ This is a major update that includes ### Added - Docstrings for vector functions. -- displaySystem kwarg `figsize` +- `displaySystem` kwarg `figsize` - bringing documentation up to speed ### Fixed @@ -351,7 +383,7 @@ Improved internal workings ## [1.2.1b0] - 2019-07-31 ### Changed -- Optimized getB call (utility integrated) +- Optimized `getB` call (utility integrated) - Improved Documentation (added Sensor class v1) --- @@ -359,10 +391,10 @@ Improved internal workings ## [1.2.0b0] - 2019-07-16 ### Added - Sensor Class - - This allows users to create a coordinate system-enabled Sensor object, which can be placed, rotated, moved and oriented. + - This allows users to create a coordinate system-enabled Sensor object, which can be placed, rotated, moved, and oriented. - This object can take the B-Field of a system (be it single source or a Collection) with the added functionality of having its own reference in the coordinate space, allowing users to easily acquire relative B-Field measurements of a system from an arbitrarily placed sensor object. - Sensors in a list may be displayed in the `Collection.displaySystem()` by using the `sensors` keyword argument. -- Added content to the `__repr__` builtin to all source classes for quick console evaluations, simply call a defined object in your Python shell to print out its attributes. +- Added content to the `__repr__` built-in to all source classes for quick console evaluations, simply call a defined object in your Python shell to print out its attributes. ### Changed - Edge cases in field calculations now return a proper [RuntimeWarning](https://docs.python.org/3/library/exceptions.html#RuntimeWarning) instead of console prints ### Fixed @@ -379,8 +411,8 @@ Improved internal workings - This should cause no API changes, if you have problems please notify us. ### Fixed -- Fix multiprocessing enabled `Collection.getBsweep()` for lots of objects with few positions causing great performance loss. This functionality now behaves as expected for the use case. -- Fix `Collection.displaySystem()`'s drawing of Dipole objects in external axes (plots) using the `subplotAx` kwarg crashing the application. This functionality now behaves as expected for the use case. +- Fixed multiprocessing enabled `Collection.getBsweep()` for lots of objects with few positions causing great performance loss. This functionality now behaves as expected for the use case. +- Fixed `Collection.displaySystem()`'s drawing of Dipole objects in external axes (plots) using the `subplotAx` kwarg crashing the application. This functionality now behaves as expected for the use case. --- @@ -391,8 +423,8 @@ Improved internal workings > `subplotAx=None` Draw into a subplot axe that already exists. The subplot needs to be 3D projected - This allows for creating side-by-side plots using displaySystem. - Figure information must be set manually in pyplot.figure() in order to not squash the plots upon subplotting. + This allows for creating side-by-side plots using `displaySystem`. + Figure information must be set manually in pyplot.figure() in order to not squash the plots upon sub plotting. <details> @@ -407,7 +439,7 @@ Improved internal workings ### Changed -- `getBsweep()` for Collections and Sources now always returns a numpy array +- `getBsweep()` for Collections and Sources now always returns a Numpy array - Zero-length segments in Line sources now return `[0,0,0]` and a warning, making it easier to draw spirals without letting users do this unaware. ### Fixed @@ -419,7 +451,7 @@ Improved internal workings ### Added -- `MANIFEST.in` file containing the LICENSE for bundling in PyPi +- `MANIFEST.in` file containing the LICENSE for bundling in PyPI --- @@ -429,7 +461,7 @@ Improved internal workings - Issue and Pull Request Templates to Repository - Continuous Integration settings (Azure and Appveyor) -- Code Coverage Reports with codecov +- Code Coverage Reports with Codecov @@ -441,7 +473,7 @@ Improved internal workings ## [1.0.0b0] - 2019-05-21 -The first official release of the magpylib library. +The first official release of the Magpylib library. ### Added @@ -456,7 +488,13 @@ The first official release of the magpylib library. --- -[5.0.1]:https://github.com/magpylib/magpylib/compare/5.0.0...HEAD +[Unreleased]:https://github.com/magpylib/magpylib/compare/5.1.1...HEAD +[5.1.1]:https://github.com/magpylib/magpylib/compare/5.1.0...5.1.1 +[5.1.0]:https://github.com/magpylib/magpylib/compare/5.0.4...5.1.0 +[5.0.4]:https://github.com/magpylib/magpylib/compare/5.0.3...5.0.4 +[5.0.3]:https://github.com/magpylib/magpylib/compare/5.0.2...5.0.3 +[5.0.2]:https://github.com/magpylib/magpylib/compare/5.0.1...5.0.2 +[5.0.1]:https://github.com/magpylib/magpylib/compare/5.0.0...5.0.1 [5.0.0]:https://github.com/magpylib/magpylib/compare/4.5.1...5.0.0 [4.5.1]:https://github.com/magpylib/magpylib/compare/4.5.0...4.5.1 [4.5.0]:https://github.com/magpylib/magpylib/compare/4.4.0...4.5.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b5bef42d6..d4bf6982c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -32,7 +32,7 @@ Community leaders have the right and responsibility to remove, edit, or reject c ## Scope -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. +This Code of Conduct applies within all community spaces, and applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85edcb196..4f65e7f95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ -# Contribute +# Contribution Guide The success of Magpylib relies on its user-friendliness. Your feedback and participation in discussions is strongly encouraged. Ask questions about Magpylib. Tell us what you like and what you dislike. Start general discussions in our informal [Discussions](https://github.com/magpylib/magpylib/discussions) channel on GitHub. -We use GitHub [Issues and Milestones](https://github.com/magpylib/magpylib/issues) to plan and track the Magpylib project. Open new Issues to report a bug, to point out a problem, or to make a feature request, e.g. following a fruitful discussion. Within the Issue we will together define in detail what should be done. For small bug fixes, code cleanups, and other small improvements its not necessary to create issues. +We use GitHub [Issues and Milestones](https://github.com/magpylib/magpylib/issues) to plan and track the Magpylib project. Open new Issues to report a bug, to point out a problem, or to make a feature request, e.g. following a fruitful discussion. Within the issue we will define in detail what should be done. For small bug fixes, code cleanups, and other small improvements it's not necessary to create issues. Always feel free to reach out through the official email <magpylib@gmail.com>. -## How to Contribute with Coding... +## How to Contribute with Coding You are most welcome to become a project contributor by helping us with coding. This includes the implementation of new features, fixing bugs, code cleanup and restructuring as well as documentation improvements. Please abide by the following procedure to make things easy for us to review and to manage the project. @@ -20,7 +20,7 @@ You are most welcome to become a project contributor by helping us with coding. (coding-requ)= ## Coding Requirements -- All code is well documented and all top level doc strings abide by the [Numpy docstring style](https://numpydoc.readthedocs.io/en/latest/format.html). +- All code is well documented and all top level doc strings abide by the [NumPy docstring style](https://numpydoc.readthedocs.io/en/latest/format.html). - All unit tests are running. We recommend using the [Pytest](https://docs.pytest.org/en/7.4.x/) package. - New unit tests are written aiming for 100% code coverage. We use [Coverage](https://coverage.readthedocs.io/en/) to test this. - [Pylint](https://pylint.readthedocs.io/en/stable/) rates your code 10/10 and there are no formatting issues reported (e.g. line-too-long). diff --git a/README.md b/README.md index 719368c52..432da3ca9 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ </a> <a href="https://anaconda.org/conda-forge/magpylib"> <img src="https://anaconda.org/conda-forge/magpylib/badges/version.svg" alt="Conda Cloud" height="18"> </a> -<a href="https://mybinder.org/v2/gh/magpylib/magpylib/5.0.0?filepath=docs%2Fexamples"> <img src="https://mybinder.org/badge_logo.svg" alt="MyBinder link" height="18"> +<a href="https://mybinder.org/v2/gh/magpylib/magpylib/5.1.1?filepath=docs%2Fexamples"> <img src="https://mybinder.org/badge_logo.svg" alt="MyBinder link" height="18"> </a> <a href="https://github.com/psf/black"> <img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="black" height="18"> </a> </div> -Magpylib is a Python package for calculating **3D static magnetic fields** of magnets, line currents and other sources. The computation is based on explicit expressions and is therefore **extremely fast**. A **user friendly API** enables convenient positioning of sources and observers. +Magpylib is an **open-source Python package** for calculating static **magnetic fields** of magnets, currents, and other sources. It uses **analytical expressions**, solutions to macroscopic magnetostatic problems, implemented in **vectorized** form which makes the computation **extremely fast** and leverages the open-source Python ecosystem for spectacular visualizations! # Installation @@ -38,11 +38,11 @@ Install from conda forge using **conda** ``` conda install -c conda-forge magpylib ``` -Magpylib supports _Python3.8+_ and relies on common scientific computation libraries _Numpy_, _Scipy_, _Matplotlib_ and _Plotly_. Optionally, _Pyvista_ is recommended as graphical backend. +Magpylib supports _Python3.10+_ and relies on common scientific computation libraries _NumPy_, _Scipy_, _Matplotlib_ and _Plotly_. Optionally, _Pyvista_ is recommended as graphical backend. # Resources - - Check out our **[Documentation](https://magpylib.readthedocs.io/en/latest)** for detailed information. + - Check out our **[Documentation](https://magpylib.readthedocs.io/en/stable)** for detailed information about the last stable release, or the **[Dev Docs](https://magpylib.readthedocs.io/en/latest)** to see the unreleased development version features. - Please abide by our **[Code of Conduct](https://github.com/magpylib/magpylib/blob/main/CODE_OF_CONDUCT.md)**. - Contribute through **[Discussions](https://github.com/magpylib/magpylib/discussions)** and coding by following the **[Contribution Guide](https://github.com/magpylib/magpylib/blob/main/CONTRIBUTING.md)**. The Git project **[Issues](https://github.com/magpylib/magpylib/issues)** give an up-to-date list of potential enhancements and planned milestones. Propose new ones. - A **[Youtube video](https://www.youtube.com/watch?v=LeUx6cM1vcs)** introduction to Magpylib v4.0.0 within the **[GSC network](https://www.internationalcollaboration.org/).** @@ -105,7 +105,7 @@ print(B.round(2)) # --> [[-0.12 -0.04 -0.02] magpy.show(cube, sensor, backend="pyvista") ``` -More details and other important features are described in detail in the **[Documentation](https://magpylib.readthedocs.io/en/latest)**. Key features are: +More details and other important features are described in detail in the **[Documentation](https://magpylib.readthedocs.io/en/stable)**. Key features are: - **Collections**: Group multiple objects for common manipulation - **Complex shapes**: Create magnets with arbitrary shapes @@ -136,7 +136,7 @@ A valid software citation could be author = {{Michael-Ortner et al.}}, title = {magpylib}, url = {https://magpylib.readthedocs.io/en/latest/}, - version = {5.0.1}, + version = {5.1.1}, date = {2023-06-25}, } ``` diff --git a/docs/README.md b/docs/README.md index 067ac0a64..0a0ba2474 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,9 @@ ## About Magpylib Documentation -The Documentation is built with [Sphinx](http://www.sphinx-doc.org/en/main/) v5.3.0 and the configuration file is [conf.py](./conf.py). Files get converted to `.html` files by Sphinx during build time. Images, web code and videos are kept in the [_static](./_static) folder. +The Documentation is built with [Sphinx](http://www.sphinx-doc.org/en/main/) and the configuration file is [conf.py](./conf.py). Files get converted to `.html` files by Sphinx during build time. Images, web code and videos are kept in the [_static](./_static) folder. ### API docs - The docstring format is under the [Numpy Convention](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html). Sphinx is configured to read Docstring information from the codebase and convert it into pages utilizing the [autodoc extension](http://www.sphinx-doc.org/en/main/usage/extensions/autodoc.html). The generated files are created at build time and put into a folder called `_autogen` + The docstring format is under the [NumPy Convention](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html). Sphinx is configured to read Docstring information from the codebase and convert it into pages utilizing the [autodoc extension](http://www.sphinx-doc.org/en/main/usage/extensions/autodoc.html). The generated files are created at build time and put into a folder called `_autogen` ### Handwritten documents Handwritten pages and guides are kept in the [_pages](./_pages) folder. They are all written in [Markdown](https://www.markdownguide.org/) using [myst-parser](https://github.com/executablebooks/MyST-Parser) as interface. Some documents like in the examples folder are dynamically computed with [myst-nb](https://github.com/executablebooks/myst-nb) as jupyter notebooks. With the help of the [jupytext](https://github.com/mwouts/jupytext) library ands its jupyterlab extension, examples can be written and executed within the jupyterlab ecosystem and saved as markdown file. It is recommended to use the [jupyterlab-myst](https://github.com/executablebooks/jupyterlab-myst) extension to be able to work with the full set of myst markdown flavor within jupyterlab. When editing the docs with vscode, use the [MyST-Markdown](https://marketplace.visualstudio.com/items?itemName=ExecutableBookProject.myst-highlight) extension to visualize the rendered document. diff --git a/docs/_pages/API_reference.md b/docs/_pages/API_reference.md new file mode 100644 index 000000000..9c7ec255d --- /dev/null +++ b/docs/_pages/API_reference.md @@ -0,0 +1,10 @@ +(docu-APIref)= +# API reference + +The API reference includes all Magpylib docstrings. + +```{toctree} +:maxdepth: 2 + +../../_autogen/magpylib.rst +``` \ No newline at end of file diff --git a/docs/_pages/changelog_.md b/docs/_pages/changelog_.md new file mode 100644 index 000000000..76a8f304f --- /dev/null +++ b/docs/_pages/changelog_.md @@ -0,0 +1,12 @@ + + +(changelog)= +# Changelog + +The changelog provides a compressed history of the Magpylib development since its publication in 2019. + +```{include} ../../CHANGELOG.md +:relative-docs: docs/ +:relative-images: +:start-line: 2 +``` \ No newline at end of file diff --git a/docs/_pages/contributing/cont_code_of_conduct.md b/docs/_pages/contributing/cont_code_of_conduct.md new file mode 100644 index 000000000..1dbcfc576 --- /dev/null +++ b/docs/_pages/contributing/cont_code_of_conduct.md @@ -0,0 +1,10 @@ +(code-of-conduct)= +# Code of Conduct + + + +```{include} ../../../CODE_OF_CONDUCT.md +:relative-docs: docs/ +:relative-images: +:start-line: 2 +``` diff --git a/docs/_pages/contributing/cont_contributing.md b/docs/_pages/contributing/cont_contributing.md new file mode 100644 index 000000000..ad09b1627 --- /dev/null +++ b/docs/_pages/contributing/cont_contributing.md @@ -0,0 +1,10 @@ +(contributing)= +# Contribution Guide + + + +```{include} ../../../CONTRIBUTING.md +:relative-docs: docs/ +:relative-images: +:start-line: 2 +``` diff --git a/docs/_pages/contributing/cont_index.md b/docs/_pages/contributing/cont_index.md new file mode 100644 index 000000000..3feaf1907 --- /dev/null +++ b/docs/_pages/contributing/cont_index.md @@ -0,0 +1,11 @@ +# Contributing + +Magpylib is a free-of-use open-source project aiming to help researchers and engineers with magnetic field computation. Your participation is most welcome! + +```{toctree} +:maxdepth: 2 + +cont_contributing.md +cont_code_of_conduct.md +cont_license.md +``` diff --git a/docs/_pages/reso_license.md b/docs/_pages/contributing/cont_license.md similarity index 89% rename from docs/_pages/reso_license.md rename to docs/_pages/contributing/cont_license.md index 8bc1cc434..fa4c916db 100644 --- a/docs/_pages/reso_license.md +++ b/docs/_pages/contributing/cont_license.md @@ -1,7 +1,10 @@ (license)= - # License + + +## Overview + Magpylib is published under the open source [FreeBSD](https://www.freebsd.org/copyright/freebsd-license/) license. - *Permissions:* Commercial use, Modification, Distribution, Private use @@ -10,7 +13,7 @@ Magpylib is published under the open source [FreeBSD](https://www.freebsd.org/co ## License Text -```{include} ../../LICENSE +```{include} ../../../LICENSE :relative-docs: docs/ :relative-images: ``` diff --git a/docs/_pages/docu/docu_graphics.md b/docs/_pages/docu/docu_graphics.md deleted file mode 100644 index 4ff02f897..000000000 --- a/docs/_pages/docu/docu_graphics.md +++ /dev/null @@ -1,1113 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.0 -kernelspec: - display_name: Python 3 - language: python - name: python3 -orphan: true ---- - -(docu-magpylib-show)= -(docu-graphics)= - -# Graphics - -Once all Magpylib objects and their paths have been created, **`show`** provides a convenient way to graphically display the geometric arrangement using the Matplotlib (default) and Plotly packages. When `show` is called, it generates a new figure which is then automatically displayed. - -The desired graphic backend is selected with the `backend` keyword argument. To bring the output to a given, user-defined figure, the `canvas` argument is used. This is demonstrated in {ref}`examples-backends-canvas`. - -The following example shows the graphical representation of various Magpylib objects and their paths using the default Matplotlib graphic backend. - -```{code-cell} ipython3 -import numpy as np -import pyvista as pv - -import magpylib as magpy - -objects = { - "Cuboid": magpy.magnet.Cuboid( - polarization=(0, -0.1, 0), - dimension=(0.01, 0.01, 0.01), - position=(-0.06, 0, 0), - ), - "Cylinder": magpy.magnet.Cylinder( - polarization=(0, 0, 0.01), - dimension=(0.01, 0.01), - position=(-0.05, 0, 0), - ), - "CylinderSegment": magpy.magnet.CylinderSegment( - polarization=(0, 0, 0.01), - dimension=(0.003, 0.01, 0.01, 0, 140), - position=(-0.03, 0, 0), - ), - "Sphere": magpy.magnet.Sphere( - polarization=(0, 0, 0.01), - diameter=0.01, - position=(-0.01, 0, 0), - ), - "Tetrahedron": magpy.magnet.Tetrahedron( - polarization=(0, 0, 0.01), - vertices=((-0.01, 0, 0), (0.01, 0, 0), (0, -0.01, 0), (0, -0.01, -0.01)), - position=(-0.04, 0, 0.04), - ), - "TriangularMesh": magpy.magnet.TriangularMesh.from_pyvista( - polarization=(0, 0, 0.01), - polydata=pv.Dodecahedron(radius=0.01), - position=(-0.01, 0, 0.04), - ), - "Circle": magpy.current.Circle( - current=1, - diameter=0.01, - position=(0.04, 0, 0), - ), - "Polyline": magpy.current.Polyline( - current=1, - vertices=[ - (0.01, 0, 0), - (0, 0.01, 0), - (-0.01, 0, 0), - (0, -0.01, 0), - (0.01, 0, 0), - ], - position=(0.01, 0, 0), - ), - "Dipole": magpy.misc.Dipole( - moment=(0, 0, 1), - position=(0.03, 0, 0), - ), - "Triangle": magpy.misc.Triangle( - polarization=(0, 0, 0.01), - vertices=((-0.01, 0, 0), (0.01, 0, 0), (0, 0.01, 0)), - position=(0.02, 0, 0.04), - ), - "Sensor": magpy.Sensor( - pixel=[(0, 0, z) for z in (-0.005, 0, 0.005)], - position=(0, -0.03, 0), - ), -} - -objects["Circle"].move(np.linspace((0, 0, 0), (0, 0, 0.05), 20)) -objects["Cuboid"].rotate_from_angax(np.linspace(0, 90, 20), "z", anchor=0) - -magpy.show(*objects.values()) -``` - -Notice that objects and their paths are automatically assigned different colors, the magnetization is shown by coloring the poles (default) or by an arrow (via styles). Current directions and dipole objects are indicated by arrows and sensors are shown as tri-colored coordinate cross with pixel as markers. - -How objects are represented graphically (color, line thickness, etc.) is defined by their **style** properties. The default style, which can be seen above, is accessed and manipulated through `magpy.defaults.display.style`. In addition, each object can have an individual style, which takes precedence over the default setting. A local style override is also possible by passing style arguments directly to `show`. - -The hierarchy that decides about the final graphic object representation, a list of all style parameters and other options for tuning the `show`-output are described in {ref}`examples-graphic-styles` and {ref}`examples-animation`. - -+++ - -(examples-backends-canvas)= -## Plotting backends - - -The plotting backend refers to the plotting library that is used for graphic output. Canvas refers to the frame/window/canvas/axes object the graphic output is forwarded to. - - -Magpylib supports several common graphic backends. - -```{code-cell} ipython3 -from magpylib import SUPPORTED_PLOTTING_BACKENDS - -SUPPORTED_PLOTTING_BACKENDS -``` - -The installation default is set to `'auto'`. In this case the backend is dynamically inferred depending on the current running environment (command-line or notebook), the available installed backend libraries and the set canvas: - -| environment | canvas | inferred backend | -|------------------|---------------------------------------------------|-----------------------------------------| -| Command-Line | `None` | `matplotlib` | -| IPython notebook | `None` | `plotly` if installed else `matplotlib` | -| all | `matplotlib.axes.Axes` | `matplotlib` | -| all | `plotly.graph_objects.Figure` (or `FigureWidget`) | `plotly` | -| all | `pyvista.Plotter` | `pyvista` | - -To explicitly select a graphic backend one can -1. Change the library default with `magpy.defaults.display.backend = 'plotly'`. -2. Set the `backend` kwarg in the `show` function, `show(..., backend='matplotlib')`. - -There is a high level of **feature parity**, however, not all graphic features are supported by all backends. In addition, some common Matplotlib syntax (e.g. color `'r'`, linestyle `':'`) is automatically translated to other backends. - -| Feature | Matplotlib | Plotly | Pyvista | -|:---------------------------------------------------------------:|:----------:|:------:|:-------:| -| triangular mesh 3d | โ๏ธ | โ๏ธ | โ๏ธ | -| line 3d | โ๏ธ | โ๏ธ | โ๏ธ | -| line style | โ๏ธ | โ๏ธ | โ | -| line color | โ๏ธ | โ๏ธ | โ๏ธ | -| line width | โ๏ธ | โ๏ธ | โ๏ธ | -| marker 3d | โ๏ธ | โ๏ธ | โ๏ธ | -| marker color | โ๏ธ | โ๏ธ | โ๏ธ | -| marker size | โ๏ธ | โ๏ธ | โ๏ธ | -| marker symbol | โ๏ธ | โ๏ธ | โ | -| marker numbering | โ๏ธ | โ๏ธ | โ | -| zoom level | โ๏ธ | โ๏ธ | โ[^2] | -| magnetization color | โ๏ธ[^7] | โ๏ธ | โ๏ธ | -| animation | โ๏ธ | โ๏ธ | โ๏ธ[^5] | -| animation time | โ๏ธ | โ๏ธ | โ๏ธ[^5] | -| animation fps | โ๏ธ | โ๏ธ | โ๏ธ[^5] | -| animation slider | โ๏ธ[^1] | โ๏ธ | โ | -| subplots 2D | โ๏ธ | โ๏ธ | โ๏ธ[^6] | -| subplots 3D | โ๏ธ | โ๏ธ | โ๏ธ | -| user canvas | โ๏ธ | โ๏ธ | โ๏ธ | -| user extra 3d model [^3] | โ๏ธ | โ๏ธ | โ๏ธ [^4] | - - -[^1]: when returning animation object and exporting it as jshtml. - -[^2]: possible but not implemented at the moment. - -[^3]: only `"scatter3d"`, and `"mesh3d"`. Gets "translated" to every other backend. - -[^4]: custom user defined trace constructors allowed, which are specific to the backend. - -[^5]: animation is only available through export as `gif` or `mp4` - -[^6]: 2D plots are not supported for all jupyter_backends. As of pyvista>=0.38 these are deprecated and replaced by the [trame](https://docs.pyvista.org/api/plotting/trame.html) backend. - -[^7]: Matplotlib does not support color gradient. Instead magnetization is shown through object slicing and coloring. - -The following example demonstrates the currently supported backends: - -```{code-cell} ipython3 -import numpy as np -import pyvista as pv - -import magpylib as magpy - -# define sources and paths -loop = magpy.current.Circle(current=1, diameter=0.01) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 - -cylinder = magpy.magnet.Cylinder( - polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) -) -cylinder.rotate_from_angax(np.linspace(0, 300, 40), "z", anchor=0, start=0) - -# show the system using different backends -for backend in magpy.SUPPORTED_PLOTTING_BACKENDS: - print(f"Plotting backend: {backend!r}") - magpy.show(loop, cylinder, backend=backend) -``` - -### Output in custom figure - -When calling `show`, a figure is automatically generated and displayed. It is also possible to display the `show` output on a given user-defined canvas with the `canvas` argument. - -In the following example we show how to combine a 2D field plot with the 3D `show` output in **Matplotlib**: - -```{code-cell} ipython3 -import matplotlib.pyplot as plt -import numpy as np - -import magpylib as magpy - -# setup matplotlib figure and subplots -fig = plt.figure(figsize=(10, 4)) -ax1 = fig.add_subplot(121) # 2D-axis -ax2 = fig.add_subplot(122, projection="3d") # 3D-axis - -# define sources and paths -loop = magpy.current.Circle(current=1, diameter=0.01) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 - -cylinder = magpy.magnet.Cylinder( - polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) -) -cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) - -# compute field and plot in 2D-axis -B = magpy.getB([loop, cylinder], (0, 0, 0), sumup=True) -ax1.plot(B) - -# display show() output in 3D-axis -magpy.show(loop, cylinder, canvas=ax2) - -# generate figure -plt.tight_layout() -plt.show() -``` - -A similar example with **Plotly**: - -```{code-cell} ipython3 -import numpy as np -import plotly.graph_objects as go - -import magpylib as magpy - -# setup plotly figure and subplots -fig = go.Figure().set_subplots( - rows=1, cols=2, specs=[[{"type": "xy"}, {"type": "scene"}]] -) - -# define sources and paths -loop = magpy.current.Circle(current=1, diameter=0.01) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 - -cylinder = magpy.magnet.Cylinder( - polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) -) -cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) - -# compute field and plot in 2D-axis -B = magpy.getB([loop, cylinder], (0, 0, 0), sumup=True) -for i, lab in enumerate(["Bx", "By", "Bz"]): - fig.add_trace(go.Scatter(x=np.linspace(0, 1, 40), y=B[:, i], name=lab)) - -# display show() output in 3D-axis -temp_fig = go.Figure() -magpy.show(loop, cylinder, canvas=temp_fig, backend="plotly") -fig.add_traces(temp_fig.data, rows=1, cols=2) -fig.layout.scene.update(temp_fig.layout.scene) - -# generate figure -fig.show() -``` - -An example with **Pyvista**: - -```{code-cell} ipython3 -import numpy as np -import pyvista as pv - -import magpylib as magpy - -# define sources and paths -loop = magpy.current.Circle(current=1, diameter=5) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 - -cylinder = magpy.magnet.Cylinder( - polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) -) -cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) - -# create a pyvista plotting scene with some graphs -pl = pv.Plotter() -line = np.array( - [(t * np.cos(15 * t), t * np.sin(15 * t), t - 8) for t in np.linspace(3, 5, 200)] -) -pl.add_lines(line, color="black") - -# add magpylib.show() output to existing scene -magpy.show(loop, cylinder, backend="pyvista", canvas=pl) - -# display scene -pl.camera.position = (.50, 10, 10) -pl.set_background("black", top="white") -pl.show() -``` - -### Return figure - -Instead of forwarding a figure to an existing canvas, it is also possible to return the figure object for further manipulation using the `return_fig` command. In the following example this is demonstrated for the pyvista backend. - -```{code-cell} ipython3 -import numpy as np -import pyvista as pv - -import magpylib as magpy - -# define sources and paths -loop = magpy.current.Circle(current=1, diameter=0.05) -loop.position = np.linspace((0, 0, -3), (0, 0, 3), 40) / 100 - -cylinder = magpy.magnet.Cylinder( - polarization=(0, -0.1, 0), dimension=(0.01, 0.02), position=(0, -0.03, 0) -) -cylinder.rotate_from_angax(np.linspace(0, 300, 40)[1:], "z", anchor=0) - -# return pyvista scene from magpylib.show() -pl = magpy.show(loop, cylinder, backend="pyvista", return_fig=True) - -# add line to the pyvista scene -line = np.array( - [ - (t * np.cos(15 * t), t * np.sin(15 * t), t - 8) - for t in np.linspace(0.03, 0.05, 200) - ] -) -pl.add_lines(line, color="black") - -# display scene -pl.camera.position = (.05, .01, .01) -pl.set_background("yellow", top="lightgreen") -pl.enable_anti_aliasing("ssaa") -pl.show() -``` - -(examples-graphic-styles)= -## Styles - -The graphic styles define how Magpylib objects are displayed visually when calling `show`. They can be fine-tuned and individualized in many ways. - -There are multiple hierarchy levels that decide about the final graphical representation of the objects: - -1. When no input is given, the **default style** will be applied. -2. Collections will override the color property of all children with their own color. -3. Object **individual styles** will take precedence over these values. -4. Setting a **local style** in `show()` will take precedence over all other settings. - -### Setting the default style - -The default style is stored in `magpylib.defaults.display.style`. Default styles can be set as properties, - -```python -magpy.defaults.display.style.magnet.magnetization.show = True -magpy.defaults.display.style.magnet.magnetization.color.middle = 'grey' -magpy.defaults.display.style.magnet.magnetization.color.mode = 'bicolor' -``` - -by assigning a style dictionary with equivalent keys, - -```python -magpy.defaults.display.style.magnet = { - 'magnetization': {'show': True, 'color': {'middle': 'grey', 'mode': 'tricolor'}} -} -``` - -or by making use of the `update` method: - -```python -magpy.defaults.display.style.magnet.magnetization.update( - 'show': True, - 'color': {'middle'='grey', mode='tricolor',} -) -``` - -All three examples result in the same default style. - -Once modified, the library default can always be restored with the `magpylib.style.reset()` method. The following practical example demonstrates how to create and set a user defined magnetization style as default, - -```{code-cell} ipython3 -import magpylib as magpy - -magpy.defaults.reset() - -cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) -cylinder = magpy.magnet.Cylinder( - polarization=(0, -1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) -) -sphere = magpy.magnet.Sphere( - polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) -) - -print("Default magnetization style") -magpy.show(cube, cylinder, sphere, backend="plotly") - -user_defined_style = { - "show": True, - "mode": "arrow+color", - "size": 0.9, - "arrow": { - "color": "black", - "offset": 0.8, - "show": True, - "size": 2, - "sizemode": "scaled", - "style": "solid", - "width": 3, - }, - "color": { - "transition": 0, - "mode": "tricolor", - "middle": "white", - "north": "magenta", - "south": "turquoise", - }, -} -magpy.defaults.display.style.magnet.magnetization = user_defined_style - -print("Custom magnetization style") -magpy.show(cube, cylinder, sphere, backend="plotly") -``` - -### Magic underscore notation - -To facilitate working with deeply nested properties, all style constructors and object style methods support the magic underscore notation. It enables referencing nested properties by joining together multiple property names with underscores. This feature mainly helps reduce the code verbosity and is heavily inspired by the `plotly` implementation (see [plotly underscore notation](https://plotly.com/python/creating-and-updating-figures/#magic-underscore-notation)). - -With magic underscore notation, the previous examples can be written as: - -```python -import magpylib as magpy -magpy.defaults.display.style.magnet = { - 'magnetization_show': True, - 'magnetization_color_middle': 'grey', - 'magnetization_color_mode': 'tricolor', -} -``` - -or directly as named keywords in the `update` method as: - -```python -import magpylib as magpy -magpy.defaults.display.style.magnet.update( - magnetization_show=True, - magnetization_color_middle='grey', - magnetization_color_mode='tricolor', -) -``` - -### Setting individual styles - -Any Magpylib object can have its own individual style that will take precedence over the default values when `show` is called. When setting individual styles, the object family specifier such as `magnet` or `current` which is required for the defaults settings, but is implicitly defined by the object type, can be omitted. - -```{warning} -Users should be aware that specifying individual style attributes massively increases object initializing time (from <50 to 100-500 $\mu$s). There is however the possibility to define styles without affecting the object creation time, but only if the style is defined in the initialization (e.g.: `magpy.magnet.Cuboid(..., style_label="MyCuboid")`). In this case the style attribute creation is deferred to when it is called the first time, typically when calling the `show` function, or accessing the `style` attribute of the object. -While this may not be noticeable for a small number of objects, it is best to avoid setting styles until it is plotting time. -``` - -In the following example the individual style of `cube` is set at initialization, the style of `cylinder` is the default one, and the individual style of `sphere` is set using the object style properties. - -```{code-cell} ipython3 -import magpylib as magpy - -magpy.defaults.reset() # reset defaults defined in previous example - -cube = magpy.magnet.Cuboid( - polarization=(1, 0, 0), - dimension=(0.01, 0.01, 0.01), - style_magnetization_color_mode="tricycle", -) -cylinder = magpy.magnet.Cylinder( - polarization=(0, 1, 0), - dimension=(0.01, 0.01), - position=(0.02, 0, 0), -) -sphere = magpy.magnet.Sphere( - polarization=(0, 1, 1), - diameter=0.01, - position=(0.04, 0, 0), -) - -sphere.style.magnetization.color.mode = "bicolor" - -magpy.show(cube, cylinder, sphere, backend="plotly") -``` - -### Setting style via collections - -When displaying collections, the collection object `color` property will be automatically assigned to all its children and override the default style. In addition, it is possible to modify the individual style properties of all children with the `set_children_styles` method. Non-matching properties are simply ignored. - -In the following example we show how the french magnetization style is applied to all children in a collection, - -```{code-cell} ipython3 -import magpylib as magpy - -magpy.defaults.reset() # reset defaults defined in previous example - -cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) -cylinder = magpy.magnet.Cylinder( - polarization=(0, 1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) -) -sphere = magpy.magnet.Sphere( - polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) -) - -coll = cube + cylinder - -coll.set_children_styles(magnetization_color_south="blue") - -magpy.show(coll, sphere, backend="plotly") -``` - -### Local style override - -Finally it is possible to hand style input to the `show` function directly and locally override the given properties for this specific `show` output. Default or individual style attributes will not be modified. Such inputs must start with the `style` prefix and the object family specifier must be omitted. Naturally underscore magic is supported. - -```{code-cell} ipython3 -import magpylib as magpy - -cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(0.01, 0.01, 0.01)) -cylinder = magpy.magnet.Cylinder( - polarization=(0, 1, 0), dimension=(0.01, 0.01), position=(0.02, 0, 0) -) -sphere = magpy.magnet.Sphere( - polarization=(0, 1, 1), diameter=0.01, position=(0.04, 0, 0) -) - -# use local style override -magpy.show(cube, cylinder, sphere, backend="plotly", style_magnetization_show=False) -``` - -(examples-list-of-styles)= - -### List of style properties - -```{code-cell} ipython3 -magpy.defaults.display.style.as_dict(flatten=True, separator=".") -``` - -(examples-animation)= - -## Animation - -With some backends, paths can automatically be animated with `show(animation=True)`. Animations can be fine-tuned with the following properties: - -1. `animation_time` (default=3), must be a positive number that gives the animation time in seconds. -2. `animation_slider` (default=`True`), is boolean and sets if a slider should be displayed in addition. -3. `animation_fps` (default=30), sets the maximal frames per second. - -Ideally, the animation will show all path steps, but when e.g. `time` and `fps` are too low, specific equidistant frames will be selected to adjust to the limited display possibilities. For practicality, the input `animation=x` will automatically set `animation=True` and `animation_time=x`. - -The following example demonstrates the animation feature, - -```{code-cell} ipython3 -import numpy as np - -import magpylib as magpy - -# define objects with paths -coll = magpy.Collection( - magpy.magnet.Cuboid(polarization=(0, 1, 0), dimension=(0.02, 0.02, 0.02)), - magpy.magnet.Cylinder(polarization=(0, 1, 0), dimension=(0.02, 0.02)), - magpy.magnet.Sphere(polarization=(0, 1, 0), diameter=0.02), -) - -start_positions = np.array([(1.414, 0, 1), (-1, -1, 1), (-1, 1, 1)]) / 100 -for pos, src in zip(start_positions, coll): - src.position = np.linspace(pos, pos * 5, 50) - src.rotate_from_angax(np.linspace(0, 360, 50), "z", anchor=0, start=0) - -ts = np.linspace(-0.6, 0.6, 5) / 100 -sensor = magpy.Sensor(pixel=[(x, y, 0) for x in ts for y in ts]) -sensor.position = np.linspace((0, 0, -5), (0, 0, 5), 20) / 100 - -# show with animation -magpy.show( - coll, - sensor, - animation=3, - animation_fps=20, - animation_slider=True, - backend="plotly", - showlegend=False, # kwarg to plotly -) -``` - -Notice that the sensor with the shorter path stops before the magnets do. This is an example where {ref}`gallery-tutorial-paths-edge-padding-end-slicing` is applied. - -```{warning} -Even with some implemented failsafes, such as a maximum frame rate and frame count, there is no guarantee that the animation will be rendered properly. This is particularly relevant when the user tries to animate many objects and/or many path positions at the same time. -``` - -+++ - -(docu-graphics-subplots)= - -## Subplots - -:::{versionadded} 4.4 -Coupled subplots -::: - -Magpylib also offers the possibility to display objects into separate subplots. It also allows the user to easily display the magnetic field data into 2D scatter along the corresponding 3D models. Objects paths can finally be animated in a coupled 2D/3D manner. - -+++ - -### Subplots 3D - -+++ - -3D subplots can be directly defined in the `show` function by passing input objects as dictionaries with the arguments `objects`, `col` (column) and `row`, as in the example below. If now `row` or no `col` is specified, it defaults to 1. - -```{code-cell} ipython3 -import numpy as np - -import magpylib as magpy - -# define sensor and sources -sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) -cyl1 = magpy.magnet.Cylinder( - polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" -) - -# define paths -N = 40 -sensor.position = np.linspace((0, 0, -0.03), (0, 0, 0.03), N) -cyl1.position = (0.04, 0, 0) -cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 0.05)) - -# display system in 3D with dict syntax -magpy.show( - {"objects": [cyl1, cyl2], "col": 1}, - {"objects": [sensor], "col": 2}, -) -``` - -### Subplots via context manager `magpylib.show_context` - -In order to make the subplot syntax more convenient we introduced the new `show_context` native Python context manager. It allows to defer calls to the `show` function while passing additional arguments. This is necessary for Magpylib to know how many rows and columns are been demanded by the user, which single calls to the `show` would not keep track of. - -The above example becomes: - -```{code-cell} ipython3 -import numpy as np - -import magpylib as magpy - -# define sensor and sources -sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) -cyl1 = magpy.magnet.Cylinder( - polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" -) - -# define paths -N = 40 -sensor.position = np.linspace((0, 0, -0.03), (0, 0, 0.03), N) -cyl1.position = (0.04, 0, 0) -cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 0.05)) - -# display system in 3D with context manager -with magpy.show_context(backend="matplotlib") as sc: - sc.show(cyl1, cyl2, col=1) - sc.show(sensor, col=2) -``` - -````{note} -Using the context manager object as in: - -```python -import magpylib as magpy - -obj1 = magpy.magnet.Cuboid() -obj2 = magpy.magnet.Cylinder() - -with magpy.show_context() as sc: - sc.show(obj1, col=1) - sc.show(obj2, col=2) -``` - -is equivalent to the use of `magpylib.show` directly, as long as within the context manager: - -```python -import magpylib as magpy - -obj1 = magpy.magnet.Cuboid() -obj2 = magpy.magnet.Cylinder() - -with magpy.show_context(): - magpy.show(obj1, col=1) - magpy.show(obj2, col=2) -``` -```` - -+++ - -### Subplots 2D - -+++ - -In addition the usual 3D models, it is also possible to draw 2D scatter plots of magnetic field data. This is achieved by assigning the `output` argument in the `show` function. -By default `output='model3d'` displays the 3D representations of the objects. If output is a tuple of strings, it must be a combination of 'B' or 'H' and 'x', 'y' and/or 'z'. When having multiple coordinates, the field value is the combined vector length (e.g. `('Bx', 'Hxy', 'Byz')`). `'Bxy'` is equivalent to `sqrt(|Bx|ยฒ + |By|ยฒ)`. A 2D line plot is then represented accordingly if the objects contain at least one source and one sensor. -By default source outputs are summed up and sensor pixels, if any, are aggregated by mean (`pixel_agg="mean"`). - -```{code-cell} ipython3 -import numpy as np - -import magpylib as magpy - -# define sensor and sources -sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) -cyl1 = magpy.magnet.Cylinder( - polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" -) - -# define paths -N = 40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) / 100 -cyl1.position = (0.04, 0, 0) -cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 0.05)) - -# display field data with context manager -with magpy.show_context(cyl1, cyl2, sensor): - magpy.show(col=1, output=("Hx", "Hy", "Hz")) - magpy.show(col=2, output=("Bx", "By", "Bz")) - -# display field data with context manager, no sumup -with magpy.show_context(cyl1, cyl2, sensor): - magpy.show(col=1, output="Hxy", sumup=False) - magpy.show(col=2, output="Byz", sumup=False) - -# display field data with context manager, no sumup, no pixel_agg -with magpy.show_context(cyl1, cyl2, sensor, sumup=False): - magpy.show(col=1, output="H", pixel_agg=None) - magpy.show(col=2, output="B", pixel_agg=None) -``` - -### Coupled 2D/3D Animation - -Finally, Magpylib lets us show coupled 3D models with their field data while animating it. - -```{code-cell} ipython3 -import numpy as np - -import magpylib as magpy - -# define sensor and sources -sensor = magpy.Sensor(pixel=[(-0.02, 0, 0), (0.02, 0, 0)]) -cyl1 = magpy.magnet.Cylinder( - polarization=(0.1, 0, 0), dimension=(0.01, 0.02), style_label="Cylinder1" -) - -# define paths -N = 40 -sensor.position = np.linspace((0, 0, -3), (0, 0, 3), N) / 100 -cyl1.position = (0.04, 0, 0) -cyl1.rotate_from_angax(angle=np.linspace(0, 300, N), start=0, axis="z", anchor=0) -cyl2 = cyl1.copy().move((0, 0, 0.05)) - -# display field data with context manager, no sumup, no pixel_agg -with magpy.show_context(cyl1, cyl2, sensor, animation=True, style_pixel_size=0.2): - magpy.show(col=1) - magpy.show(col=2, output="Bx") -``` - -(examples-3d-models)= - -## Special 3D models - -(examples-own-3d-models)= -### Custom 3D models - -Each Magpylib object has a default 3D representation that is displayed with `show`. Users can add a custom 3D model to any Magpylib object with help of the `style.model3d.add_trace` method. The new trace is stored in `style.model3d.data`. User-defined traces move with the object just like the default models do. The default trace can be hidden with the command `obj.model3d.showdefault=False`. When using the `'generic'` backend, custom traces are automatically translated into any other backend. If a specific backend is used, it will only show when called with the corresponding backend. - -The input `trace` is a dictionary which includes all necessary information for plotting or a `magpylib.graphics.Trace3d` object. A `trace` dictionary has the following keys: - -1. `'backend'`: `'generic'`, `'matplotlib'` or `'plotly'` -2. `'constructor'`: name of the plotting constructor from the respective backend, e.g. plotly `'Mesh3d'` or matplotlib `'plot_surface'` -3. `'args'`: default `None`, positional arguments handed to constructor -4. `'kwargs'`: default `None`, keyword arguments handed to constructor -5. `'coordsargs'`: tells Magpylib which input corresponds to which coordinate direction, so that geometric representation becomes possible. By default `{'x': 'x', 'y': 'y', 'z': 'z'}` for the `'generic'` backend and Plotly backend, and `{'x': 'args[0]', 'y': 'args[1]', 'z': 'args[2]'}` for the Matplotlib backend. -6. `'show'`: default `True`, toggle if this trace should be displayed -7. `'scale'`: default 1, object geometric scaling factor -8. `'updatefunc'`: default `None`, updates the trace parameters when `show` is called. Used to generate dynamic traces. - -The following example shows how a **generic** trace is constructed with `Mesh3d` and `Scatter3d` and is displayed with three different backends: - -```{code-cell} ipython3 -import numpy as np -import pyvista as pv - -import magpylib as magpy - -# Mesh3d trace ######################### - -trace_mesh3d = { - "backend": "generic", - "constructor": "Mesh3d", - "kwargs": { - "x": (0.01, 0, -0.01, 0), - "y": (-0.005, 0.012, -0.005, 0), - "z": (-0.005, -0.005, -0.005, 0.01), - "i": (0, 0, 0, 1), - "j": (1, 1, 2, 2), - "k": (2, 3, 3, 3), - #'opacity': 0.5, - }, -} -coll = magpy.Collection(position=(0, -0.03, 0), style_label="'Mesh3d' trace") -coll.style.model3d.add_trace(trace_mesh3d) - -# Scatter3d trace ###################### - -ts = np.linspace(0, 2 * np.pi, 30) -trace_scatter3d = { - "backend": "generic", - "constructor": "Scatter3d", - "kwargs": { - "x": np.cos(ts) / 100, - "y": np.zeros(30), - "z": np.sin(ts) / 100, - "mode": "lines", - }, -} -dipole = magpy.misc.Dipole( - moment=(0, 0, 1), style_label="'Scatter3d' trace", style_size=6 -) -dipole.style.model3d.add_trace(trace_scatter3d) -# show the system using different backends -for backend in magpy.SUPPORTED_PLOTTING_BACKENDS: - print(f"Plotting backend: {backend!r}") - magpy.show(coll, dipole, backend=backend) -``` - -It is possible to have multiple user-defined traces that will be displayed at the same time. In addition, the following code shows how to quickly copy and manipulate trace dictionaries and `Trace3d` objects, - -```{code-cell} ipython3 -import copy - -import numpy as np - -import magpylib as magpy - -ts = np.linspace(0, 2 * np.pi, 30) -trace_scatter3d = { - "backend": "generic", - "constructor": "Scatter3d", - "kwargs": { - "x": np.cos(ts) / 100, - "y": np.zeros(30), - "z": np.sin(ts) / 100, - "mode": "lines", - }, -} -dipole = magpy.misc.Dipole( - moment=(0, 0, 1), - style_label="'Scatter3d' trace", - style_size=0.01, - style_sizemode="absolute", -) - - -# generate new trace from dictionary -trace2 = copy.deepcopy(trace_scatter3d) -trace2["kwargs"]["y"] = np.sin(ts) / 100 -trace2["kwargs"]["z"] = np.zeros(30) - -dipole.style.model3d.add_trace(trace2) - -# generate new trace from Trace3d object -trace3 = copy.deepcopy(dipole.style.model3d.data[0]) -trace3.kwargs["x"] = np.zeros(30) -trace3.kwargs["z"] = np.cos(ts) / 100 - -dipole.style.model3d.add_trace(trace3) - -dipole.show(dipole, backend="matplotlib") -``` - -**Matplotlib** plotting functions often use positional arguments for $(x,y,z)$ input, that are handed over from `args=(x,y,z)` in `trace`. The following examples show how to construct traces with `plot`, `plot_surface` and `plot_trisurf`: - -```{code-cell} ipython3 -import matplotlib.pyplot as plt -import matplotlib.tri as mtri -import numpy as np - -import magpylib as magpy - -# plot trace ########################### - -ts = np.linspace(-10, 10, 100) -xs = np.cos(ts) / 100 -ys = np.sin(ts) / 100 -zs = ts / 20 / 100 - -trace_plot = { - "backend": "matplotlib", - "constructor": "plot", - "args": (xs, ys, zs), - "kwargs": {"ls": "--", "lw": 2}, -} -magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.005, 0.01)) -magnet.style.model3d.add_trace(trace_plot) - -# plot_surface trace ################### - -u, v = np.mgrid[0 : 2 * np.pi : 30j, 0 : np.pi : 20j] -xs = np.cos(u) * np.sin(v) / 100 -ys = np.sin(u) * np.sin(v) / 100 -zs = np.cos(v) / 100 - -trace_surf = { - "backend": "matplotlib", - "constructor": "plot_surface", - "args": (xs, ys, zs), - "kwargs": {"cmap": plt.cm.YlGnBu_r}, -} -ball = magpy.Collection(position=(-0.03, 0, 0)) -ball.style.model3d.add_trace(trace_surf) - -# plot_trisurf trace ################### - -u, v = np.mgrid[0 : 2 * np.pi : 50j, -0.5:0.5:10j] -u, v = u.flatten(), v.flatten() - -xs = (1 + 0.5 * v * np.cos(u / 2.0)) * np.cos(u) / 100 -ys = (1 + 0.5 * v * np.cos(u / 2.0)) * np.sin(u) / 100 -zs = 0.5 * v * np.sin(u / 2.0) / 100 - -tri = mtri.Triangulation(u, v) - -trace_trisurf = { - "backend": "matplotlib", - "constructor": "plot_trisurf", - "args": (xs, ys, zs), - "kwargs": { - "triangles": tri.triangles, - "cmap": plt.cm.coolwarm, - }, -} -mobius = magpy.misc.CustomSource(style_model3d_showdefault=False, position=(0.03, 0, 0)) -mobius.style.model3d.add_trace(trace_trisurf) - -magpy.show(magnet, ball, mobius, backend="matplotlib") -``` - -### Pre-defined 3D models - -Automatic trace generators are provided for several basic 3D models in `magpylib.graphics.model3d`. If no backend is specified, it defaults back to `'generic'`. They can be used as follows, - -```{code-cell} ipython3 -import magpylib as magpy -from magpylib.graphics import model3d - -# prism trace ################################### -trace_prism = model3d.make_Prism( - base=6, - diameter=0.02, - height=0.01, - position=(-0.03, 0, 0), -) -obj0 = magpy.Sensor(style_model3d_showdefault=False, style_label="Prism") -obj0.style.model3d.add_trace(trace_prism) - -# pyramid trace ################################# -trace_pyramid = model3d.make_Pyramid( - base=30, - diameter=0.02, - height=0.01, - position=(0.03, 0, 0), -) -obj1 = magpy.Sensor(style_model3d_showdefault=False, style_label="Pyramid") -obj1.style.model3d.add_trace(trace_pyramid) - -# cuboid trace ################################## -trace_cuboid = model3d.make_Cuboid( - dimension=(0.02, 0.02, 0.02), - position=(0, 0.03, 0), -) -obj2 = magpy.Sensor(style_model3d_showdefault=False, style_label="Cuboid") -obj2.style.model3d.add_trace(trace_cuboid) - -# cylinder segment trace ######################## -trace_cylinder_segment = model3d.make_CylinderSegment( - dimension=(0.01, 0.02, 0.01, 140, 220), - position=(0.01, 0, -0.03), -) -obj3 = magpy.Sensor(style_model3d_showdefault=False, style_label="Cylinder Segment") -obj3.style.model3d.add_trace(trace_cylinder_segment) - -# ellipsoid trace ############################### -trace_ellipsoid = model3d.make_Ellipsoid( - dimension=(0.02, 0.02, 0.02), - position=(0, 0, 0.03), -) -obj4 = magpy.Sensor(style_model3d_showdefault=False, style_label="Ellipsoid") -obj4.style.model3d.add_trace(trace_ellipsoid) - -# arrow trace ################################### -trace_arrow = model3d.make_Arrow( - base=30, - diameter=0.006, - height=0.02, - position=(0, -0.03, 0), -) -obj5 = magpy.Sensor(style_model3d_showdefault=False, style_label="Arrow") -obj5.style.model3d.add_trace(trace_arrow) - -magpy.show(obj0, obj1, obj2, obj3, obj4, obj5, backend="plotly") -``` - -(examples-adding-CAD-model)= - -### Adding a CAD model - -As shown in {ref}`examples-3d-models`, it is possible to attach custom 3D model representations to any Magpylib object. In the example below we show how a standard CAD model can be transformed into a generic Magpylib graphic trace, and displayed by both `matplotlib` and `plotly` backends. - -```{note} -The code below requires installation of the `numpy-stl` package. -``` - -```{code-cell} ipython3 -import os -import tempfile - -import numpy as np -import requests -from matplotlib.colors import to_hex -from stl import mesh # requires installation of numpy-stl - -import magpylib as magpy - - -def bin_color_to_hex(x): - """transform binary rgb into hex color""" - sb = f"{x:015b}"[::-1] - r = int(sb[:5], base=2) / 31 - g = int(sb[5:10], base=2) / 31 - b = int(sb[10:15], base=2) / 31 - return to_hex((r, g, b)) - - -def trace_from_stl(stl_file): - """ - Generates a Magpylib 3D model trace dictionary from an *.stl file. - backend: 'matplotlib' or 'plotly' - """ - # load stl file - stl_mesh = mesh.Mesh.from_file(stl_file) - - # extract vertices and triangulation - p, q, r = stl_mesh.vectors.shape - vertices, ixr = np.unique( - stl_mesh.vectors.reshape(p * q, r), return_inverse=True, axis=0 - ) - i = np.take(ixr, [3 * k for k in range(p)]) - j = np.take(ixr, [3 * k + 1 for k in range(p)]) - k = np.take(ixr, [3 * k + 2 for k in range(p)]) - x, y, z = vertices.T - - # generate and return a generic trace which can be translated into any backend - colors = stl_mesh.attr.flatten() - facecolor = np.array([bin_color_to_hex(c) for c in colors]).T - x, y, z = x / 1000, y / 1000, z / 1000 # mm->m - trace = { - "backend": "generic", - "constructor": "mesh3d", - "kwargs": dict(x=x, y=y, z=z, i=i, j=j, k=k, facecolor=facecolor), - } - return trace - - -# load stl file from online resource -url = "https://raw.githubusercontent.com/magpylib/magpylib-files/main/PG-SSO-3-2.stl" -file = url.split("/")[-1] -with tempfile.TemporaryDirectory() as temp: - fn = os.path.join(temp, file) - with open(fn, "wb") as f: - response = requests.get(url) - f.write(response.content) - - # create traces for both backends - trace = trace_from_stl(fn) - -# create sensor and add CAD model -sensor = magpy.Sensor(style_label="PG-SSO-3 package") -sensor.style.model3d.add_trace(trace) - -# create magnet and sensor path -magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.015, 0.02)) -sensor.position = np.linspace((-0.015, 0, 0.008), (-0.015, 0, -0.004), 21) -sensor.rotate_from_angax(np.linspace(0, 180, 21), "z", anchor=0, start=0) - -# display with matplotlib and plotly backends -args = (sensor, magnet) -kwargs = dict(style_path_frames=5) -magpy.show(args, **kwargs, backend="matplotlib") -magpy.show(args, **kwargs, backend="plotly") -``` - -```{code-cell} ipython3 - -``` diff --git a/docs/_pages/docu/docu_index.md b/docs/_pages/docu/docu_index.md deleted file mode 100644 index f0eec1511..000000000 --- a/docs/_pages/docu/docu_index.md +++ /dev/null @@ -1,39 +0,0 @@ -(docu-index)= - -# Documentation - -::::{grid} 1 1 3 3 -:margin: 4 4 0 0 -:gutter: 4 - -:::{grid-item-card} {ref}`docu-magpylib` -:link: docu-magpylib -:link-type: ref -:img-top: ../../_static/images/docu_index_icon_magpylib.png -Fundamentals and magnetic field computation -::: - -:::{grid-item-card} {ref}`docu-graphics` -:link: docu-magpylib-show -:link-type: ref -:img-top: ../../_static/images/docu_index_icon_magpylib_show.png -Magpylib graphics, styles, and backend -::: - -:::{grid-item-card} {ref}`docu-physics` -:link: docu-physics -:link-type: ref -:img-top: ../../_static/images/docu_index_icon_physics.png -Physics and computation background -::: - -:::: - -```{toctree} -:maxdepth: 2 -:hidden: - -docu_magpylib_api.md -docu_graphics.md -docu_physics.md -``` \ No newline at end of file diff --git a/docs/_pages/docu/docu_magpylib_api.md b/docs/_pages/docu/docu_magpylib_api.md deleted file mode 100644 index d83001bbf..000000000 --- a/docs/_pages/docu/docu_magpylib_api.md +++ /dev/null @@ -1,777 +0,0 @@ - -<hr style="border:3px solid gray"> - -(docu-magpylib)= -(docu-io)= - -# Magpylib API - -<hr style="border:3px solid gray"> - -## Types - -Magpylib requires no special input format. All scalar types (`int`, `float`, ...) and vector types (`list`, `tuple`, `np.ndarray`, ... ) are accepted. Magpylib returns everything as `np.ndarray`. - -(docu-units)= -## Units - -The important vacuum permeability $\mu_0$ is provided at the package top-level <span style="color: orange">**mu_0**</span>. It's value is not $4 \pi 10^{-7}$ since [the redefinition of the SI base units](https://en.wikipedia.org/wiki/2019_redefinition_of_the_SI_base_units), but a value close to it. - -For historical reasons Magpylib used non-SI units until Version 4. Starting with version 5 all inputs and outputs are SI-based. - -::::{grid} 3 -:::{grid-item} -:columns: 1 -::: - -:::{grid-item} -:columns: 10 -| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNITS from v5| UNITS until v4| -|:---:|:---:|:---:|:---:| -| Magnetic Polarization $\vec{J}$ | `polarization`, `getJ()` | **T** | - | -| Magnetization $\vec{M}$ | `magnetization`, `getM()` | **A/m** | mT | -| Electric Current $i_0$ | `current` | **A** | A | -| Magnetic Dipole Moment $\vec{m}$ | `moment` | **Aยทmยฒ** | mTยทmmยณ | -| B-field $\vec{B}$ | `getB()` | **T** | mT | -| H-field $\vec{H}$ | `getH()` | **A/m** | kA/m | -| Length-inputs | `position`, `dimension`, `vertices`, ... | **m** | mm | -| Angle-inputs | `angle`, `dimension`, ... | **ยฐ** | ยฐ | -::: - -:::: - -```{warning} -Up to version 4, Magpylib was unfortunately contributing to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib < v5 was referring to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. From version 5 onwards this is fixed. -``` - -(docu-api-scale-invariance)= -```{hint} -All input and output units in Magpylib (version 5 and higher) are SI-based, see table above. However, for advanced use one should be aware that the analytical solutions are **scale invariant** - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant, but it is critical to keep the same length unit for all inputs in one computation. - -In addition, `getB` returns the same unit as given by the `polarization` input. With polarization input in mT, getB will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $ยต_0$. -``` - -```{note} -The connection between the magnetic polarization J, the magnetization M and the material parameters of a real permanent magnet are shown in {ref}`gallery-tutorial-modelling-magnets`. -``` - -<!-- ################################################################## --> -<!-- ################################################################## --> -<!-- ################################################################## --> - -<br/><br/> -<hr style="border:3px solid gray"> - -(docu-classes)= - -# The Magpylib Classes - -<hr style="border:3px solid gray"> - -In Magpylib's object oriented interface magnetic field **sources** (generate the field) and **observers** (read the field) are created as Python objects with various defining attributes and methods. - -## Base properties - -The following basic properties are shared by all Magpylib classes: - -* The <span style="color: orange">**position**</span> and <span style="color: orange">**orientation**</span> attributes describe the object placement in the global coordinate system. - -* The <span style="color: orange">**move()**</span> and <span style="color: orange">**rotate()**</span> methods enable relative object positioning. - -* The <span style="color: orange">**reset_path()**</span> method sets position and orientation to default values. - -* The <span style="color: orange">**barycenter**</span> property returns the object barycenter (often the same as position). - -See {ref}`docu-position` for more information on these features. - - -* The <span style="color: orange">**style**</span> attribute includes all settings for graphical object representation. - -* The <span style="color: orange">**show()**</span> method gives quick access to the graphical representation. - -See {ref}`docu-graphics` for more information on graphic output, default styles and customization possibilities. - -* The <span style="color: orange">**getB()**</span>, <span style="color: orange">**getH()**</span>, <span style="color: orange">**getJ()**</span> and <span style="color: orange">**getM()**</span> methods give quick access to field computation. - -See {ref}`docu-field-computation` for more information. - - -* The <span style="color: orange">**parent**</span> attribute references a [Collection](docu-collection) that the object is part of. - -* The <span style="color: orange">**copy()**</span> method creates a clone of any object where selected properties, given by kwargs, are modified. - -* The <span style="color: orange">**describe()**</span> method provides a brief description of the object and returns the unique object id. - - ---------------------------------------------- - - -## Local and global coordinates - -::::{grid} 2 -:::{grid-item} -:columns: 9 -Magpylib objects span a local reference frame, and all object properties are defined within this frame, for example the vertices of a `Tetrahedron` magnet. The position and orientation attributes describe how the local frame lies within the global coordinates. The two frames coincide by default, when `position=(0,0,0)` and `orientation=None` (=unit rotation). The `position` and `orientation` attributes are described in detail in {ref}`docu-position`. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - - ---------------------------------------------- - - -(docu-magnet-classes)= -## Magnet classes - -All magnets are sources. They have the <span style="color: orange">**polarization**</span> attribute which is of the format $\vec{J}=(J_x, J_y, J_z)$ and denotes a homogeneous magnetic polarization vector in the local object coordinates in units of T. Alternatively, the magnetization vector can be set via the <span style="color: orange">**magnetization**</span> attribute of the format $\vec{M}=(M_x, M_y, M_z)$. These two parameters are codependent and Magpylib ensures that they stay in sync via the relation $\vec{J}=\mu_0\cdot\vec{M}$. Information on how this is related to material properties from data sheets is found in {ref}`gallery-tutorial-modelling-magnets`. - - -### Cuboid -```python -magpylib.magnet.Cuboid(position, orientation, dimension, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Cuboid` objects represent magnets with cuboid shape. The <span style="color: orange">**dimension**</span> attribute has the format $(a,b,c)$ and denotes the sides of the cuboid units of meter. The center of the cuboid lies in the origin of the local coordinates, and the sides are parallel to the coordinate axes. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - - -### Cylinder -```python -magpylib.magnet.Cylinder(position, orientation, dimension, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Cylinder` objects represent magnets with cylindrical shape. The <span style="color: orange">**dimension**</span> attribute has the format $(d,h)$ and denotes diameter and height of the cylinder in units of meter. The center of the cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - - -### CylinderSegment -```python -magpylib.magnet.CylinderSegment(position, orientation, dimension, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`CylinderSegment` objects represent magnets with the shape of a cylindrical ring section. The <span style="color: orange">**dimension**</span> attribute has the format $(r_1,r_2,h,\varphi_1,\varphi_2)$ and denotes inner radius, outer radius and height in units of meter, and the two section angles $\varphi_1<\varphi_2$ in ยฐ. The center of the full cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** When the cylinder section angles span 360ยฐ, then the much faster `Cylinder` methods are used for the field computation. -::: -:::: - - -### Sphere -```python -magpylib.magnet.Sphere(position, orientation, diameter, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Sphere` objects represent magnets of spherical shape. The <span style="color: orange">**diameter**</span> attribute is the sphere diameter $d$ in units of meter. The center of the sphere lies in the origin of the local coordinates. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - - -### Tetrahedron -```python -magpylib.magnet.Tetrahedron(position, orientation, vertices, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Tetrahedron` objects represent magnets of tetrahedral shape. The <span style="color: orange">**vertices**</span> attribute stores the four corner points $(\vec{P}_1, \vec{P}_2, \vec{P}_3, \vec{P}_4)$ in the local object coordinates in units of m. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** The `Tetrahedron` field is computed from four `Triangle` fields. -::: -:::: - -(docu-magpylib-api-trimesh)= - -### TriangularMesh -```python -magpylib.magnet.TriangularMesh(position, orientation, vertices, faces, polarization, magnetization, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`TriangularMesh` objects represent magnets with surface given by a triangular mesh. The mesh is defined by the <span style="color: orange">**vertices**</span> attribute, an array of all unique corner points $(\vec{P}_1, \vec{P}_2, ...)$ in units of meter, and the <span style="color: orange">**faces**</span> attribute, which is an array of index-triplets that define individual faces $(\vec{F}_1, \vec{F}_2, ...)$. The property <span style="color: orange">**mesh**</span> returns an array of all faces as point-triples $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -At initialization the mesh integrity is automatically checked, and all faces are reoriented to point outwards. These actions are controlled via the kwargs -* <span style="color: orange">**check_open**</span> -* <span style="color: orange">**check_disconnected**</span> -* <span style="color: orange">**check_selfintersecting**</span> -* <span style="color: orange">**reorient_faces**</span> - -which are all by default set to `"warn"`. Options are `"skip"` (don't perform check), `"ignore"` (ignore if check fails), `"warn"` (warn if check fails), `"raise"` (raise error if check fails). - -Results of the checks are stored in the following object attributes -* <span style="color: orange">**status_open**</span> can be `True`, `False` or `None` (unchecked) -* <span style="color: orange">**status_open_data**</span> contains an array of open edges -* <span style="color: orange">**status_disconnected**</span> can be `True`, `False` or `None` (unchecked) -* <span style="color: orange">**status_disconnected_data**</span> contains an array of mesh parts -* <span style="color: orange">**status_selfintersecting**</span> can be `True`, `None` or `None` (unchecked) -* <span style="color: orange">**status_selfintersecting_data**</span> contains an array of self-intersecting faces -* <span style="color: orange">**status_reoriented**</span> can be `True` or `False` - -The checks can also be performed after initialization using the methods -* <span style="color: orange">**check_open()**</span> -* <span style="color: orange">**check_disconnected()**</span> -* <span style="color: orange">**check_selfintersecting()**</span> -* <span style="color: orange">**reorient_faces()**</span> - -The following class methods enable easy mesh creating and mesh loading. - -* <span style="color: orange">**TriangularMesh.from_mesh()**</span> generates a `TriangularMesh` objects from the input <span style="color: orange">**mesh**</span>, which is an array in the mesh format $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. -* <span style="color: orange">**TriangularMesh.from_ConvexHull()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**points**</span>, which is an array of positions $(\vec{P}_1, \vec{P}_2, \vec{P}_3, ...)$ from which the convex Hull is computed via the [Scipy ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) implementation. -* <span style="color: orange">**TriangularMesh.from_triangles()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**triangles**</span>, which is a list or a `Collection` of `Triangle` objects. -* <span style="color: orange">**TriangularMesh.from_pyvista()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**polydata**</span>, which is a [Pyvista PolyData](https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.PolyData.html) object. - -The method <span style="color: orange">**to_TriangleCollection()**</span> transforms a `TriangularMesh` object into a `Collection` of `Triangle` objects. - -**Info:** While the checks may be disabled, the field computation guarantees correct results only if the mesh is closed, connected, not self-intersecting and all faces are oriented outwards. Examples of working with the `TriangularMesh` class are found in {ref}`gallery-shapes-triangle` and in {ref}`gallery-shapes-pyvista`. -::: -:::: - - ---------------------------------------------- - - -## Current classes - -All currents are sources. Current objects have the <span style="color: orange">**current**</span> attribute which is a scalar that denotes the electrical current in units of ampere. - -### Circle -```python -magpylib.current.Circle(position, orientation, diameter, current, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Circle` objects represent circular line current loops. The <span style="color: orange">**diameter**</span> attribute is the loop diameter $d$ in units of meter. The loop lies in the xy-plane with it's center in the origin of the local coordinates. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - -### Polyline -```python -magpylib.current.Polyline(position, orientation, vertices, current, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Polyline` objects represent line current segments where the electric current flows in straight lines from vertex to vertex. The <span style="color: orange">**vertices**</span> attribute is a vector of all vertices $(\vec{P}_1, \vec{P}_2, ...)$ given in the local coordinates in units of meter. -::: -:::{grid-item} -:columns: 3 - -::: -:::: - ---------------------------------------------- - -## Miscellaneous classes - -There are classes listed hereon that function as sources, but they do not represent physical magnets or current distributions. - - -### Dipole -```python -magpylib.misc.Dipole(position, orientation, moment, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Dipole` objects represent magnetic dipole moments with the <span style="color: orange">**moment**</span> attribute that describes the magnetic dipole moment $\vec{m}=(m_x,m_y,m_z)$ in SI-units of Amยฒ, which lies in the origin of the local coordinates. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** The total dipole moment of a homogeneous magnet with body volume $V$ is given by $\vec{m}=\vec{J}\cdot V$. -::: -:::: - - -### Triangle -```python -magpylib.misc.Triangle(position, orientation, vertices, polarization, magnetization, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Triangle` objects represent triangular surfaces with homogeneous charge density given by the projection of the polarization or magnetization vector onto the surface normal. The attributes <span style="color: orange">**polarization**</span> and <span style="color: orange">**magnetization**</span> are treated similar as by the {ref}`docu-magnet-classes`. The <span style="color: orange">**vertices**</span> attribute is a set of the three triangle corners $(\vec{P}_1, \vec{P}_2, \vec{P}_3)$ in units of meter in the local coordinates. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** When multiple Triangles with similar magnetization/polarization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. In this case, the B-field is only correct on the outside of the body. On the inside the polarization must be added to the field. This is demonstrated in the tutorial {ref}`gallery-shapes-triangle`. -::: -:::: - -(docu-magpylib-api-custom)= - -### CustomSource -```python -magpylib.misc.CustomSource(field_func, position, orientation, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -The `CustomSource` class is used to create user defined sources provided with with custom field computation functions. The argument <span style="color: orange">**field_func**</span> takes a function that is then automatically called for the field computation. This custom field function is treated like a [core function](docu-field-comp-core). It must have the positional arguments `field` with values `"B"` or `"H"`, and `observers` (must accept array with shape (n,3)) and return the B-field and the H-field with a similar shape. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** A tutorial {ref}`gallery-tutorial-custom` is found in the gallery. -::: -:::: - - ---------------------------------------------- - - -## Sensor -```python -magpylib.Sensor(position, orientation, pixel, handedness, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -`Sensor` objects represent observers of the magnetic field and can be used as Magpylib `observers` input for magnetic field computation. The <span style="color: orange">**pixel**</span> attribute is an array of positions $(\vec{P}_1, \vec{P}_2, ...)$ provided in units of meter in the local sensor coordinates. A sensor returns the magnetic field at these pixel positions. By default `pixel=(0,0,0)` and the sensor simply returns the field at it's position. The <span style="color: orange">**handedness**</span> attribute can be `"left"` or `"right"` (default) to set a left- or right-handed sensor coordinate system for the field computation. -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** Sensors can have their own position and orientation and enable easy relative positioning between sources and observers. The field is always computed in the reference frame of the sensor, which might itself be moving in the global coordinate system. Magpylib sensors can be understood as perfect magnetic field sensors with infinitesimally sensitive elements. An example how to use sensors is given in {ref}`gallery-tutorial-field-computation-sensors`. -::: -:::: - - ---------------------------------------------- - - -(docu-collection)= - -## Collection -```python -magpylib.Collection(*children, position, orientation, override_parent, style) -``` - -::::{grid} 2 -:::{grid-item} -:columns: 9 -A `Collection` is a group of Magpylib objects that is used for common manipulation. All these objects are stored by reference in the <span style="color: orange">**children**</span> attribute. The collection becomes the <span style="color: orange">**parent**</span> of the object. An object can only have one parent. There are several options for accessing only specific children via the following properties - -* <span style="color: orange">**sources**</span>: return only sources -* <span style="color: orange">**observers**</span>: return only observers -* <span style="color: orange">**collections**</span>: return only collections -* <span style="color: orange">**sources_all**</span>: return all sources, including the ones from sub-collections -* <span style="color: orange">**observers_all**</span>: return all observers, including the ones from sub-collections -* <span style="color: orange">**collections_all**</span>: return all collections, including the ones from sub-collections - -Additional methods for adding and removing children: - -- <span style="color: orange">**add()**</span>: Add an object to the collection -- <span style="color: orange">**remove()**</span>: Remove an object from the collection -::: -:::{grid-item} -:columns: 3 - -::: -:::{grid-item} -:columns: 12 -**Info:** A collection object has its own `position` and `orientation` attributes and spans a local reference frame for all its children. An operation applied to a collection moves the frame, and is individually applied to all children such that their relative position in the local reference frame is maintained. This means that the collection functions as a container for manipulation, but child position and orientation are always updated in the global coordinate system. After being added to a collection, it is still possible to manipulate the individual children, which will also move them to a new relative position in the collection frame. - -Collections have **format** as an additional argument for **describe()** method. Default value is `format="type+id+label"`. Any combination of `"type"`, `"id"`, and `"label"` is allowed. - -A tutorial {ref}`gallery-tutorial-collection` is provided in the example gallery. -::: -:::: - - -<!-- ################################################################## --> -<!-- ################################################################## --> -<!-- ################################################################## --> - - -<br/><br/> -<hr style="border:3px solid gray"> - -(docu-position)= - -# Position, Orientation, and Paths - -<hr style="border:3px solid gray"> - -::::{grid} 2 -:gutter: 2 - -:::{grid-item} -:columns: 12 7 7 7 -The explicit magnetic field expressions found in the literature, implemented in the [Magpylib core](docu-field-comp-core), are given in convenient source-coordinates. It is a common problem to transform the field into an application relevant observer coordinate system. While not technically difficult, such transformations are prone to error. - -Here Magpylib helps out. All Magpylib sources and observers lie in a global Cartesian coordinate system. Object position and orientation are defined by the attributes `position` and `orientation`, ๐. Objects can easily be moved around using the `move()` and `rotate()` methods. Eventually, the field is computed in the reference frame of the observers (e.g. Sensor objects). Positions are given in units of meter, and the default unit for orientation is ยฐ. -::: -:::{grid-item} -:columns: 12 5 5 5 - -::: -:::: - -Position and orientation of all Magpylib objects are defined by the two attributes - -::::{grid} 2 -:gutter: 2 - -:::{grid-item-card} -:shadow: none -:columns: 12 5 5 5 -<span style="color: orange">**position**</span> - a point $(x,y,z)$ in the global coordinates, or a set of such points $(\vec{P}_1, \vec{P}_2, ...)$. By default objects are created with `position=(0,0,0)`. -::: -:::{grid-item-card} -:shadow: none -:columns: 12 7 7 7 -<span style="color: orange">**orientation**</span> - a [Scipy Rotation object](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html) which describes the object rotation relative to its default orientation (defined in {ref}`docu-classes`). By default, objects are created with unit rotation `orientation=None`. -::: -:::: - -The position and orientation attributes can be either **scalar**, i.e. a single position or a single rotation, or **vector**, when they are arrays of such scalars. The two attributes together define the **path** of an object - Magpylib makes sure that they are always of the same length. When the field is computed, it is automatically computed for the whole path. - -```{tip} -To enable vectorized field computation, paths should always be used when modeling multiple object positions. Avoid using Python loops at all costs for that purpose! If your path is difficult to realize, consider using the [functional interface](docu-functional-interface) instead. -``` - -Magpylib offers two powerful methods for object manipulation: - -::::{grid} 2 -:gutter: 2 - -:::{grid-item-card} -:columns: 12 5 5 5 -:shadow: none -<span style="color: orange">**move(**</span>`displacement`, `start="auto"`<span style="color: orange">**)**</span> - move object by `displacement` input. `displacement` is a position vector (scalar input) or a set of position vectors (vector input). -::: -:::{grid-item-card} -:columns: 12 7 7 7 -:shadow: none -<span style="color: orange">**rotate(**</span>`rotation`, `anchor=None`, `start="auto"`<span style="color: orange">**)**</span> - rotates the object by the `rotation` input about an anchor point defined by the `anchor` input. `rotation` is a [Scipy Rotation object](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html), and `anchor` is a position vector. Both can be scalar or vector inputs. With `anchor=None` the object is rotated about its `position`. -::: -:::: - -- **Scalar input** is applied to the whole object path, starting with path index `start`. With the default `start="auto"` the index is set to `start=0` and the functionality is **moving objects around** (incl. their whole paths). -- **Vector input** of length $n$ applies the $n$ individual operations to $n$ object path entries, starting with path index `start`. Padding applies when the input exceeds the existing path length. With the default `start="auto"` the index is set to `start=len(object path)` and the functionality is **appending the input**. - -The practical application of this formalism is best demonstrated by the following program - -```python -import magpylib as magpy -# Note that all units are in SI - -sensor = magpy.Sensor() -print(sensor.position) # default value -# --> [0. 0. 0.] - -sensor.move((1,1,1)) # scalar input is by default applied -print(sensor.position) # to the whole path -# --> [1. 1. 1.] - -sensor.move([(1,1,1), (2,2,2)]) # vector input is by default appended -print(sensor.position) # to the existing path -# --> [[1. 1. 1.] [2. 2. 2.] [3. 3. 3.]] - -sensor.move((1,1,1), start=1) # scalar input and start=1 is applied -print(sensor.position) # to whole path starting at index 1 -# --> [[1. 1. 1.] [3. 3. 3.] [4. 4. 4.]] - -sensor.move([(0,0,10), (0,0,20)], start=1) # vector input and start=1 merges -print(sensor.position) # the input with the existing path -# --> [[ 1. 1. 1.] [ 3. 3. 13.] [ 4. 4. 24.]] # starting at index 1. -``` - -Several extensions of the `rotate` method give a lot of flexibility with object rotation. They all feature the arguments `anchor` and `start` which work as described above. - -:::{dropdown} <span style="color: orange">**rotate_from_angax(**</span>`angle`, `axis`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> -* `angle`: scalar or array with shape (n). Angle(s) of rotation. -* `axis`: array of shape (3,) or string. The direction of the rotation axis. String input can be 'x', 'y' or 'z' to denote respective directions. -* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). -::: - -:::{dropdown} <span style="color: orange">**rotate_from_rotvec(**</span>`rotvec`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> -* `rotvec` : array with shape (n,3) or (3,). The rotation vector direction is the rotation axis and the vector length is the rotation angle in units of deg. -* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). -::: - -:::{dropdown} <span style="color: orange">**rotate_from_euler(**</span> `angle`, `seq`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> -* `angle`: scalar or array with shape (n). Angle(s) of rotation in units of deg (by default). -* `seq` : string. Specifies sequence of axes for rotations. Up to 3 characters belonging to the set {'X', 'Y', 'Z'} for intrinsic rotations, or {'x', 'y', 'z'} for extrinsic rotations. Extrinsic and intrinsic rotations cannot be mixed in one function call. -* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). -::: - -:::{dropdown} <span style="color: orange">**rotate_from_quat(**</span>`quat`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> -* `quat` : array with shape (n,4) or (4,). Rotation input in quaternion form. -::: - -:::{dropdown} <span style="color: orange">**rotate_from_mrp(**</span>`matrix`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> -* `matrix` : array with shape (n,3,3) or (3,3). Rotation matrix. See scipy.spatial.transform.Rotation for details. -::: - -:::{dropdown} <span style="color: orange">**rotate_from_mrp(**</span>`mrp`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> -* `mrp` : array with shape (n,3) or (3,). Modified Rodrigues parameter input. See scipy Rotation package for details. -::: - -When objects with different path lengths are combined, e.g. when computing the field, the shorter paths are treated as static beyond their end to make the computation sensible. Internally, Magpylib follows a philosophy of edge-padding and end-slicing when adjusting paths. - -::::{grid} 2 -:gutter: 2 - -:::{grid-item-card} -:columns: 12 7 7 7 -:shadow: none -**Edge-padding:** whenever path entries beyond the existing path length are needed the edge-entries of the existing path are returned. This means that the object is considered to be โstaticโ beyond its existing path. -::: -:::{grid-item-card} -:columns: 12 5 5 5 -:shadow: none -**End-slicing:** whenever a path is automatically reduced in length, Magpylib will slice such that the ending of the path is kept. -::: -:::: - -The tutorial {ref}`gallery-tutorial-paths` shows intuitive good practice examples of the important functionality described in this section. - - -<!-- ################################################################## --> -<!-- ################################################################## --> -<!-- ################################################################## --> -<br/><br/> -<hr style="border:3px solid gray"> - -(docu-field-computation)= -# Field Computation - -<hr style="border:3px solid gray"> - -Magnetic field computation in Magpylib is done via four top-level functions <span style="color: orange">**getB**</span>, <span style="color: orange">**getH**</span>, <span style="color: orange">**getJ**</span> and, <span style="color: orange">**getM**</span>, - - -```python -magpylib.getB(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") -magpylib.getH(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") -magpylib.getJ(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") -magpylib.getM(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") -``` - -that compute the respective field generated by `sources` as seen by the `observers` in their local coordinates. `sources` can be any Magpylib source object (e.g. magnets) or a flat list thereof. `observers` can be an array of position vectors with shape `(n1,n2,n3,...,3)`, any Magpylib observer object (e.g. sensors), or a flat list thereof. For quick-access, the functions `getBHJM` are also methods of all Magpylib objects, such that the `sources` or `observers` input is the object itself. - -The following code shows a minimal example for Magpylib field computation. - -```python -import magpylib as magpy -# All inputs and outputs in SI units - -# define source and observer objects -loop = magpy.current.Circle(current=1, diameter=.001) -sens = magpy.Sensor() - -# compute field -B = magpy.getB(loop, sens) - -print(B) -# --> [0. 0. 0.00125664] -``` - -By default, `getB` returns the B-field in units of T, `getH` the H-field in units of A/m, `getJ` the magnetic polarization in T and, `getM` the magnetization in A/m, assuming that all inputs are given in SI units as described in the docstrings. - -```{note} -In reality, `getB` returns the same unit as given by the `polarization` input. For example, with polarization input in mT, getB will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $ยต_0$. -``` - -The output of a field computation `magpy.getB(sources, observers)` is by default a Numpy array of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of observers, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position array input and `3` the three magnetic field components $(B_x, B_y, B_z)$. - - -* `squeeze`: If True (default) all axes of length 1 in the output (e.g. only a single source) are squeezed. - -* `pixel_agg`: Select a compatible numpy aggregator function (e.g. `"min"`, `"mean"`) that is applied to the output. For example, with `pixel_agg="mean"` the mean field of all observer points is returned. With this option it is possible to supply `getBHJM` with multiple observers that have different pixel shapes. - -* `output`: Change the output format. Options are `"ndarray"` (default, returns a Numpy ndarray) and `"dataframe"` (returns a 2D-table Pandas DataFrame). - -```{note} -Magpylib collects all inputs (object parameters), and vectorizes them for the computation which reduces the computation time dramatically for large inputs. - -Try to make all field computations with as few calls to `getBHJM` as possible. Avoid Python loops at all costs! -``` - -The tutorial {ref}`gallery-tutorial-field-computation` shows good practices with Magpylib field computation. - - -(docu-functional-interface)= -## Functional interface - -Users can bypass the object oriented functionality of Magpylib and instead compute the field for n given parameter sets. This is done by providing the following inputs to the top level functions `getB`, `getH`, `getJ` and, `getM`. - -1. `sources`: a string denoting the source type. Allowed values are the Magpylib source class names, see {ref}`docu-classes`. -2. `observers`: array-like of shape (3,) or (n,3) giving the observer positions. -3. `kwargs`: a dictionary with inputs of shape (x,) or (n,x). Must include all mandatory class-specific inputs. By default, `position=(0,0,0)` and `orientation=None`(=unit rotation). - -All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to create a set of n computation instances. The field is returned in the shape (n,3). The following code demonstrates the functional interface. - -```python -import numpy as np -import magpylib as magpy -# All inputs and outputs in SI units - -# compute the cuboid field for 3 input instances -N = 3 # number of instances -B = magpy.getB( - sources='Cuboid', - observers=np.linspace((0,0,1), (0,0,3), N), - dimension=np.linspace((1,1,1), (3,3,3),3, N), - polarization=(0,0,1), -) - -# This example demonstrates the scale invariance -print(B) -# --> [[0. 0. 0.13478239] -# [0. 0. 0.13478239] -# [0. 0. 0.13478239]] -``` - -```{note} -The functional interface is potentially faster than the object oriented one if users know how to generate the input arrays efficiently with numpy (e.g. `np.arange`, `np.linspace`, `np.tile`, `np.repeat`, ...). -``` - - -(docu-field-comp-core)= -## Core interface - -At the heart of Magpylib lies a set of core functions that are our implementations of explicit field expressions found in the literature, see {ref}`docu-physics`. Direct access to these functions is given through the `magpylib.core` subpackage which includes, - -::::{grid} 1 -:gutter: 1 - -:::{grid-item} -<span style="color: orange">**magnet_cuboid_Bfield(**</span> `observers`, `dimensions`, `polarizations`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**magnet_cylinder_axial_Bfield(**</span> `z0`, `r`, `z`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**magnet_cylinder_diametral_Hfield(**</span> `z0`, `r`, `z`, `phi`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**magnet_cylinder_segment_Hfield(**</span> `observers`, `dimensions`, `magnetizations`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**magnet_sphere_Bfield(**</span>`observers`, `diameters`, `polarizations`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**current_circle_Hfield(**</span>`r0`, `r`, `z`, `i0`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**current_polyline_Hfield(**</span>`observers`, `segments_start`, `segments_end`, `currents`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**dipole_Hfield(**</span>`observers`, `moments`<span style="color: orange">**)**</span> -::: - -:::{grid-item} -<span style="color: orange">**triangle_Bfield(**</span>`observers`, `vertices`, `polarizations`<span style="color: orange">**)**</span> -::: - -:::: - -All inputs must be Numpy ndarrays of shape (n,x). Details can be found in the respective function docstrings. The following example demonstrates the core interface. - - -```python -import numpy as np -import magpylib as magpy -# All inputs and outputs in SI units - -# prepare input -z0 = np.array([1,1]) -r = np.array([1,1]) -z = np.array([2,2]) - -# compute field with core functions -B = magpy.core.magnet_cylinder_axial_Bfield(z0=z0, r=r, z=z).T - -print(B) -# --> [[0.05561469 0. 0.06690167] -# [0.05561469 0. 0.06690167]] -``` - -## Field computation workflow - -The Magpylib field computation internal workflow and different approaches of the three interfaces is outlined in the following sketch. - - diff --git a/docs/_pages/gallery/gallery_app_halbach.md b/docs/_pages/gallery/gallery_app_halbach.md deleted file mode 100644 index 518f90364..000000000 --- a/docs/_pages/gallery/gallery_app_halbach.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -orphan: true -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.13.7 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(gallery-app-halbach)= - -# Halbach Magnets - -Magpylib is an excellent tool to create magnet assemblies. In this example we will show how to model Halbach magnets. - -Example 1: Halbach magnetization using CylinderSegments - -Example 2: Discrete Halbach magnets diff --git a/docs/_pages/gallery/gallery_index.md b/docs/_pages/gallery/gallery_index.md deleted file mode 100644 index 20af19520..000000000 --- a/docs/_pages/gallery/gallery_index.md +++ /dev/null @@ -1,210 +0,0 @@ -(gallery)= - -# Examples and Tutorials - -Notice that most examples use interactive notebooks via [sphinx-thebe](https://sphinx-thebe.readthedocs.io/en/latest/), see ๐. -## Tutorials - -::::{grid} 2 3 4 4 -:gutter: 4 - -:::{grid-item-card} {ref}`gallery-tutorial-paths` -:text-align: center -:link: gallery-tutorial-paths -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_tutorial_paths.png -::: - -:::{grid-item-card} {ref}`gallery-tutorial-field-computation` -:text-align: center -:link: gallery-tutorial-field-computation -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_tutorial_field_computation.png -::: - -:::{grid-item-card} {ref}`gallery-tutorial-collection` -:text-align: center -:link: gallery-tutorial-collection -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_tutorial_collection.png -::: - -:::{grid-item-card} {ref}`gallery-tutorial-custom` -:text-align: center -:link: gallery-tutorial-custom -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_tutorial_custom.png -::: - -:::{grid-item-card} {ref}`gallery-tutorial-modelling-magnets` -:text-align: center -:link: gallery-tutorial-modelling-magnets -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_tutorial_modelling_magnets.png -::: - -:::: - - -## Visualizations - -::::{grid} 2 3 4 4 -:gutter: 4 - -:::{grid-item-card} {ref}`gallery-vis-mpl-streamplot` -:text-align: center -:link: gallery-vis-mpl-streamplot -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_viz_mpl_streamplot.png -::: - -:::{grid-item-card} {ref}`gallery-vis-pv-streamlines` -:text-align: center -:link: gallery-vis-pv-streamlines -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_viz_pv_streamlines.png -::: - -:::{grid-item-card} {ref}`gallery-vis-animations` -:text-align: center -:link: gallery-vis-animations -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_viz_animations.png -::: - -:::{grid-item-card} {ref}`gallery-vis-subplots` -:text-align: center -:link: gallery-vis-subplots -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_WIP.png -::: - -:::: - -## Complex Magnet Shapes - -::::{grid} 2 3 4 4 -:gutter: 4 - -:::{grid-item-card} {ref}`gallery-shapes-superpos` -:text-align: center -:link: gallery-shapes-superpos -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_shapes_superpos.png -::: - -:::{grid-item-card} {ref}`gallery-shapes-convex-hull` -:text-align: center -:link: gallery-shapes-convex-hull -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_shapes_convex_hull.png -::: - -:::{grid-item-card} {ref}`gallery-shapes-triangle` -:text-align: center -:link: gallery-shapes-triangle -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_shapes_triangle.png -::: - -:::{grid-item-card} {ref}`gallery-shapes-pyvista` -:text-align: center -:link: gallery-shapes-pyvista -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_shapes_pyvista.png -::: - -:::{grid-item-card} {ref}`gallery-shapes-3d-models` -:text-align: center -:link: gallery-shapes-3d-models -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_WIP.png -::: - -:::: - - -## Miscellaneous - -::::{grid} 2 3 4 4 -:gutter: 4 - -:::{grid-item-card} {ref}`gallery-misc-compound` -:text-align: center -:link: gallery-misc-compound -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_misc_compound.png -::: - -:::{grid-item-card} {ref}`gallery-misc-field-interpolation` -:text-align: center -:link: gallery-misc-field-interpolation -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_misc_field_interpolation.png -::: - -:::{grid-item-card} {ref}`gallery-misc-inhom` -:text-align: center -:link: gallery-misc-inhom -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_WIP.png -::: - -:::: - -## Applications - -::::{grid} 2 3 4 4 -:gutter: 4 - -:::{grid-item-card} {ref}`gallery-app-end-of-shaft` -:text-align: center -:link: gallery-app-end-of-shaft -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_app_end_of_shaft.png -::: - -:::{grid-item-card} {ref}`gallery-app-halbach` -:text-align: center -:link: gallery-app-halbach -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_WIP.png -::: - -:::{grid-item-card} {ref}`gallery-app-helmholtz` -:text-align: center -:link: gallery-app-helmholtz -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_app_helmholtz.png -::: - -:::{grid-item-card} {ref}`gallery-app-scales` -:text-align: center -:link: gallery-app-scales -:link-type: ref -:link-alt: link to example -:img-bottom: ../../_static/images/gallery_icon_WIP.png -::: - -:::: - diff --git a/docs/_pages/gallery/gallery_misc_inhom.md b/docs/_pages/gallery/gallery_misc_inhom.md deleted file mode 100644 index 0927fb3a6..000000000 --- a/docs/_pages/gallery/gallery_misc_inhom.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -orphan: true -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.13.7 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(gallery-misc-inhom)= - -# Inhomogeneous Magnetization - -In this tutorial we will show how to deal with inhomogeneous magnetization with discretization. diff --git a/docs/_pages/gallery/gallery_shapes_3d_models.md b/docs/_pages/gallery/gallery_shapes_3d_models.md deleted file mode 100644 index 5d654c32a..000000000 --- a/docs/_pages/gallery/gallery_shapes_3d_models.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -orphan: true -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.13.7 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(gallery-shapes-3d-models)= - -# 3D Models - -In this example we will show how to make - -1. CAD imports -2. Pyvista imports -3. Inkscape xy to magnets diff --git a/docs/_pages/gallery/gallery_vis_animations.md b/docs/_pages/gallery/gallery_vis_animations.md deleted file mode 100644 index c2b60d424..000000000 --- a/docs/_pages/gallery/gallery_vis_animations.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -orphan: true -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.14.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(gallery-vis-animations)= - -# Animations - -Magpylib can display the motion of objects along paths in the form of animations. The following example shows how to set up such an animation. - -```{hint} -If your browser window opens, but your animation does not load, reload the page (ctrl+r in chrome). -``` - -Detailed information about how to tune animations can be found in the [graphics documentation](examples-animation). Animations work best in the [plotly backend](examples-backends-canvas). Avoid rendering too many frames. - -```{code-cell} ipython3 -import numpy as np -import magpylib as magpy - -# create sensor with path -sensor=magpy.Sensor().rotate_from_angax( - angle=35*np.sin(np.linspace(-4,4,36)), - axis='y', - anchor=(0,0,-.05), - start=0, -) - -# create magnet with path -magnet = magpy.magnet.Cuboid( - dimension=(0.02,0.01,0.01), - polarization=(0.3,0,0), - position=(0,0,-.03) -).rotate_from_angax( - angle=np.linspace(0,360,37), - axis='z', - start=0, -) - -# display as animation - prefers plotly backend -magpy.show(sensor, magnet, animation=True, backend='plotly') -``` diff --git a/docs/_pages/gallery/gallery_vis_subplots.md b/docs/_pages/gallery/gallery_vis_subplots.md deleted file mode 100644 index 2173127f8..000000000 --- a/docs/_pages/gallery/gallery_vis_subplots.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -orphan: true -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.14.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(gallery-vis-subplots)= - -# Subplots - -It is very illustrative to combine 2D and 3D subplots, especially when doing animations. In this tutorial we will show how to create such plots with minimal syntax, using Magpylib built-ins. Until this example is ready, read up everything about subplots [here](docu-graphics-subplots). diff --git a/docs/_pages/reso_changelog.md b/docs/_pages/reso_changelog.md deleted file mode 100644 index 2fe3f818b..000000000 --- a/docs/_pages/reso_changelog.md +++ /dev/null @@ -1,6 +0,0 @@ -(changelog)= - -```{include} ../../CHANGELOG.md -:relative-docs: docs/ -:relative-images: -``` \ No newline at end of file diff --git a/docs/_pages/reso_code_of_conduct.md b/docs/_pages/reso_code_of_conduct.md deleted file mode 100644 index 3772e418c..000000000 --- a/docs/_pages/reso_code_of_conduct.md +++ /dev/null @@ -1,6 +0,0 @@ -(code_of_conduct)= - -```{include} ../../CODE_OF_CONDUCT.md -:relative-docs: docs/ -:relative-images: -``` diff --git a/docs/_pages/reso_contributing.md b/docs/_pages/reso_contributing.md deleted file mode 100644 index 77e1e7a6c..000000000 --- a/docs/_pages/reso_contributing.md +++ /dev/null @@ -1,6 +0,0 @@ -(contributing)= - -```{include} ../../CONTRIBUTING.md -:relative-docs: docs/ -:relative-images: -``` \ No newline at end of file diff --git a/docs/_pages/reso_site_notice.md b/docs/_pages/reso_site_notice.md deleted file mode 100644 index 4478c1633..000000000 --- a/docs/_pages/reso_site_notice.md +++ /dev/null @@ -1,9 +0,0 @@ -(site-notice)= - -# Site Notice - -Magpylib is supported by [Silicon Austria Labs](https://silicon-austria-labs.com/). - -Magpylib is a [NumFocus](https://numfocus.org/sponsored-projects/affiliated-projects) affiliated project. - -We are all members of the [EMA](https://magnetism.eu/). \ No newline at end of file diff --git a/docs/_pages/user_guide/docs/docs_classes.md b/docs/_pages/user_guide/docs/docs_classes.md new file mode 100644 index 000000000..c7e8d00eb --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_classes.md @@ -0,0 +1,383 @@ +(docs-classes)= +# The Magpylib Classes + +In Magpylib's [object oriented interface](docs-fieldcomp-oo) magnetic field **sources** (generate the field) and **observers** (read the field) are created as Python objects with various defining attributes and methods. + +## Base properties + +The following basic properties are shared by all Magpylib classes: + +* The <span style="color: orange">**position**</span> and <span style="color: orange">**orientation**</span> attributes describe the object placement in the global coordinate system. + +* The <span style="color: orange">**move()**</span> and <span style="color: orange">**rotate()**</span> methods enable relative object positioning. + +* The <span style="color: orange">**reset_path()**</span> method sets position and orientation to default values. + +* The <span style="color: orange">**barycenter**</span> property returns the object barycenter (often the same as position). + +See {ref}`docs-position` for more information on these features. + + +* The <span style="color: orange">**style**</span> attribute includes all settings for graphical object representation. + +* The <span style="color: orange">**show()**</span> method gives quick access to the graphical representation. + +See {ref}`guide-graphics` for more information on graphic output, default styles and customization possibilities. + +* The <span style="color: orange">**getB()**</span>, <span style="color: orange">**getH()**</span>, <span style="color: orange">**getJ()**</span> and <span style="color: orange">**getM()**</span> methods give quick access to field computation. + +See {ref}`docs-fieldcomp` for more information. + + +* The <span style="color: orange">**parent**</span> attribute references a [Collection](guide-docs-classes-collections) that the object is part of. + +* The <span style="color: orange">**copy()**</span> method creates a clone of any object where selected properties, given by kwargs, are modified. + +* The <span style="color: orange">**describe()**</span> method provides a brief description of the object and returns the unique object id. + + +--------------------------------------------- + + +## Local and global coordinates + +::::{grid} 2 +:::{grid-item} +:columns: 9 +Magpylib objects span a local reference frame, and all object properties are defined within this frame, for example the vertices of a `Tetrahedron` magnet. The position and orientation attributes describe how the local frame lies within the global coordinates. The two frames coincide by default, when `position=(0,0,0)` and `orientation=None` (=unit rotation). The `position` and `orientation` attributes are described in detail in {ref}`docs-position`. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + + +--------------------------------------------- + + +(docu-magnet-classes)= +## Magnet classes + +All magnets are sources. They have the <span style="color: orange">**polarization**</span> attribute which is of the format $\vec{J}=(J_x, J_y, J_z)$ and denotes a homogeneous magnetic polarization vector in the local object coordinates in units of T. Alternatively, the magnetization vector can be set via the <span style="color: orange">**magnetization**</span> attribute of the format $\vec{M}=(M_x, M_y, M_z)$. These two parameters are codependent and Magpylib ensures that they stay in sync via the relation $\vec{J}=\mu_0\cdot\vec{M}$. Information on how this is related to material properties from data sheets is found in {ref}`examples-tutorial-modeling-magnets`. + + +### Cuboid +```python +magpylib.magnet.Cuboid(position, orientation, dimension, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Cuboid` objects represent magnets with cuboid shape. The <span style="color: orange">**dimension**</span> attribute has the format $(a,b,c)$ and denotes the sides of the cuboid units of meter. The center of the cuboid lies in the origin of the local coordinates, and the sides are parallel to the coordinate axes. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + + +### Cylinder +```python +magpylib.magnet.Cylinder(position, orientation, dimension, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Cylinder` objects represent magnets with cylindrical shape. The <span style="color: orange">**dimension**</span> attribute has the format $(d,h)$ and denotes diameter and height of the cylinder in units of meter. The center of the cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + + +### CylinderSegment +```python +magpylib.magnet.CylinderSegment(position, orientation, dimension, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`CylinderSegment` objects represent magnets with the shape of a cylindrical ring section. The <span style="color: orange">**dimension**</span> attribute has the format $(r_1,r_2,h,\varphi_1,\varphi_2)$ and denotes inner radius, outer radius and height in units of meter, and the two section angles $\varphi_1<\varphi_2$ in ยฐ. The center of the full cylinder lies in the origin of the local coordinates, and the cylinder axis coincides with the z-axis. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** When the cylinder section angles span 360ยฐ, then the much faster `Cylinder` methods are used for the field computation. +::: +:::: + + +### Sphere +```python +magpylib.magnet.Sphere(position, orientation, diameter, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Sphere` objects represent magnets of spherical shape. The <span style="color: orange">**diameter**</span> attribute is the sphere diameter $d$ in units of meter. The center of the sphere lies in the origin of the local coordinates. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + + +### Tetrahedron +```python +magpylib.magnet.Tetrahedron(position, orientation, vertices, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Tetrahedron` objects represent magnets of tetrahedral shape. The <span style="color: orange">**vertices**</span> attribute stores the four corner points $(\vec{P}_1, \vec{P}_2, \vec{P}_3, \vec{P}_4)$ in the local object coordinates in units of m. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** The `Tetrahedron` field is computed from four `Triangle` fields. +::: +:::: + +(docu-magpylib-api-trimesh)= + +### TriangularMesh +```python +magpylib.magnet.TriangularMesh(position, orientation, vertices, faces, polarization, magnetization, check_open, check_disconnected, check_selfintersecting, reorient_faces, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`TriangularMesh` objects represent magnets with surface given by a triangular mesh. The mesh is defined by the <span style="color: orange">**vertices**</span> attribute, an array of all unique corner points $(\vec{P}_1, \vec{P}_2, ...)$ in units of meter, and the <span style="color: orange">**faces**</span> attribute, which is an array of index-triplets that define individual faces $(\vec{F}_1, \vec{F}_2, ...)$. The property <span style="color: orange">**mesh**</span> returns an array of all faces as point-triples $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +At initialization the mesh integrity is automatically checked, and all faces are reoriented to point outwards. These actions are controlled via the kwargs +* <span style="color: orange">**check_open**</span> +* <span style="color: orange">**check_disconnected**</span> +* <span style="color: orange">**check_selfintersecting**</span> +* <span style="color: orange">**reorient_faces**</span> + +which are all by default set to `"warn"`. Options are `"skip"` (don't perform check), `"ignore"` (ignore if check fails), `"warn"` (warn if check fails), `"raise"` (raise error if check fails). + +Results of the checks are stored in the following object attributes +* <span style="color: orange">**status_open**</span> can be `True`, `False` or `None` (unchecked) +* <span style="color: orange">**status_open_data**</span> contains an array of open edges +* <span style="color: orange">**status_disconnected**</span> can be `True`, `False` or `None` (unchecked) +* <span style="color: orange">**status_disconnected_data**</span> contains an array of mesh parts +* <span style="color: orange">**status_selfintersecting**</span> can be `True`, `None` or `None` (unchecked) +* <span style="color: orange">**status_selfintersecting_data**</span> contains an array of self-intersecting faces +* <span style="color: orange">**status_reoriented**</span> can be `True` or `False` + +The checks can also be performed after initialization using the methods +* <span style="color: orange">**check_open()**</span> +* <span style="color: orange">**check_disconnected()**</span> +* <span style="color: orange">**check_selfintersecting()**</span> +* <span style="color: orange">**reorient_faces()**</span> + +The following class methods enable easy mesh creating and mesh loading. + +* <span style="color: orange">**TriangularMesh.from_mesh()**</span> generates a `TriangularMesh` objects from the input <span style="color: orange">**mesh**</span>, which is an array in the mesh format $[(\vec{P}_1^1, \vec{P}_2^1, \vec{P}_3^1), (\vec{P}_1^2, \vec{P}_2^2, \vec{P}_3^2), ...]$. +* <span style="color: orange">**TriangularMesh.from_ConvexHull()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**points**</span>, which is an array of positions $(\vec{P}_1, \vec{P}_2, \vec{P}_3, ...)$ from which the convex Hull is computed via the [Scipy ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) implementation. +* <span style="color: orange">**TriangularMesh.from_triangles()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**triangles**</span>, which is a list or a `Collection` of `Triangle` objects. +* <span style="color: orange">**TriangularMesh.from_pyvista()**</span> generates a `TriangularMesh` object from the input <span style="color: orange">**polydata**</span>, which is a [Pyvista PolyData](https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.PolyData.html) object. + +The method <span style="color: orange">**to_TriangleCollection()**</span> transforms a `TriangularMesh` object into a `Collection` of `Triangle` objects. + +**Info:** While the checks may be disabled, the field computation guarantees correct results only if the mesh is closed, connected, not self-intersecting and all faces are oriented outwards. Examples of working with the `TriangularMesh` class are found in {ref}`examples-shapes-triangle` and in {ref}`examples-shapes-pyvista`. +::: +:::: + + +--------------------------------------------- + + +## Current classes + +All currents are sources. Current objects have the <span style="color: orange">**current**</span> attribute which is a scalar that denotes the electrical current in units of ampere. + +### Circle +```python +magpylib.current.Circle(position, orientation, diameter, current, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Circle` objects represent circular line current loops. The <span style="color: orange">**diameter**</span> attribute is the loop diameter $d$ in units of meter. The loop lies in the xy-plane with it's center in the origin of the local coordinates. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + +### Polyline +```python +magpylib.current.Polyline(position, orientation, vertices, current, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Polyline` objects represent line current segments where the electric current flows in straight lines from vertex to vertex. The <span style="color: orange">**vertices**</span> attribute is a vector of all vertices $(\vec{P}_1, \vec{P}_2, ...)$ given in the local coordinates in units of meter. +::: +:::{grid-item} +:columns: 3 + +::: +:::: + +--------------------------------------------- + +## Miscellaneous classes + +There are classes listed hereon that function as sources, but they do not represent physical magnets or current distributions. + + +### Dipole +```python +magpylib.misc.Dipole(position, orientation, moment, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Dipole` objects represent magnetic dipole moments with the <span style="color: orange">**moment**</span> attribute that describes the magnetic dipole moment $\vec{m}=(m_x,m_y,m_z)$ in SI-units of Amยฒ, which lies in the origin of the local coordinates. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** The total dipole moment of a homogeneous magnet with body volume $V$ is given by $\vec{m}=\vec{M}\cdot V$. +::: +:::: + + +### Triangle +```python +magpylib.misc.Triangle(position, orientation, vertices, polarization, magnetization, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Triangle` objects represent triangular surfaces with homogeneous charge density given by the projection of the polarization or magnetization vector onto the surface normal. The attributes <span style="color: orange">**polarization**</span> and <span style="color: orange">**magnetization**</span> are treated similar as by the {ref}`docu-magnet-classes`. The <span style="color: orange">**vertices**</span> attribute is a set of the three triangle corners $(\vec{P}_1, \vec{P}_2, \vec{P}_3)$ in units of meter in the local coordinates. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** When multiple Triangles with similar magnetization/polarization vectors form a closed surface, and all their orientations (right-hand-rule) point outwards, their total H-field is equivalent to the field of a homogeneous magnet of the same shape. In this case, the B-field is only correct on the outside of the body. On the inside the polarization must be added to the field. This is demonstrated in the tutorial {ref}`examples-shapes-triangle`. +::: +:::: + +(guide-docs-classes-custom-source)= +### CustomSource +```python +magpylib.misc.CustomSource(field_func, position, orientation, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +The `CustomSource` class is used to create user defined sources provided with with custom field computation functions. The argument <span style="color: orange">**field_func**</span> takes a function that is then automatically called for the field computation. This custom field function is treated like a [core function](docs-field-core). It must have the positional arguments `field` with values `"B"` or `"H"`, and `observers` (must accept array with shape (n,3)) and return the B-field and the H-field with a similar shape. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** A tutorial {ref}`examples-tutorial-custom` is found in the examples. +::: +:::: + + +--------------------------------------------- + + +## Sensor +```python +magpylib.Sensor(position, orientation, pixel, handedness, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +`Sensor` objects represent observers of the magnetic field and can be used as Magpylib `observers` input for magnetic field computation. The <span style="color: orange">**pixel**</span> attribute is an array of positions $(\vec{P}_1, \vec{P}_2, ...)$ provided in units of meter in the local sensor coordinates. A sensor returns the magnetic field at these pixel positions. By default `pixel=(0,0,0)` and the sensor simply returns the field at it's position. The <span style="color: orange">**handedness**</span> attribute can be `"left"` or `"right"` (default) to set a left- or right-handed sensor coordinate system for the field computation. +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** Sensors can have their own position and orientation and enable easy relative positioning between sources and observers. The field is always computed in the reference frame of the sensor, which might itself be moving in the global coordinate system. Magpylib sensors can be understood as perfect magnetic field sensors with infinitesimally sensitive elements. An example how to use sensors is given in {ref}`examples-tutorial-field-computation-sensors`. +::: +:::: + + +--------------------------------------------- + + +(guide-docs-classes-collections)= +## Collection +```python +magpylib.Collection(*children, position, orientation, override_parent, style) +``` + +::::{grid} 2 +:::{grid-item} +:columns: 9 +A `Collection` is a group of Magpylib objects that is used for common manipulation. All these objects are stored by reference in the <span style="color: orange">**children**</span> attribute. The collection becomes the <span style="color: orange">**parent**</span> of the object. An object can only have one parent. There are several options for accessing only specific children via the following properties + +* <span style="color: orange">**sources**</span>: return only sources +* <span style="color: orange">**observers**</span>: return only observers +* <span style="color: orange">**collections**</span>: return only collections +* <span style="color: orange">**sources_all**</span>: return all sources, including the ones from sub-collections +* <span style="color: orange">**observers_all**</span>: return all observers, including the ones from sub-collections +* <span style="color: orange">**collections_all**</span>: return all collections, including the ones from sub-collections + +Additional methods for adding and removing children: + +- <span style="color: orange">**add()**</span>: Add an object to the collection +- <span style="color: orange">**remove()**</span>: Remove an object from the collection +::: +:::{grid-item} +:columns: 3 + +::: +:::{grid-item} +:columns: 12 +**Info:** A collection object has its own `position` and `orientation` attributes and spans a local reference frame for all its children. An operation applied to a collection moves the frame and is individually applied to all children such that their relative position in the local reference frame is maintained. This means that the collection functions as a container for manipulation, but child position and orientation are always updated in the global coordinate system. After being added to a collection, it is still possible to manipulate the individual children, which will also move them to a new relative position in the collection frame. + +Collections have **format** as an additional argument for **describe()** method. Default value is `format="type+id+label"`. Any combination of `"type"`, `"id"`, and `"label"` is allowed. + +A tutorial {ref}`examples-tutorial-collection` is provided in the example examples. +::: +:::: diff --git a/docs/_pages/user_guide/docs/docs_fieldcomp.md b/docs/_pages/user_guide/docs/docs_fieldcomp.md new file mode 100644 index 000000000..c1632f3cf --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_fieldcomp.md @@ -0,0 +1,175 @@ +(docs-fieldcomp)= +# Field Computation + +The following is a detailed technical documentation of Magpylib field computation. +The tutorial {ref}`examples-tutorial-field-computation` shows good practices and illustrative examples. + +(docs-fieldcomp-oo)= +## Object-oriented interface + +The object-oriented interface relies on the idea that sources of the magnetic field and observers thereof are created as Python objects which can be manipulated at will, and called for field computation. This is done via four top-level functions <span style="color: orange">**getB**</span>, <span style="color: orange">**getH**</span>, <span style="color: orange">**getJ**</span> and, <span style="color: orange">**getM**</span>, + +```python +magpylib.getB(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +magpylib.getH(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +magpylib.getJ(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +magpylib.getM(sources, observers, squeeze=True, pixel_agg=None, output="ndarray") +``` + +that compute the respective fields B (B-field), H (H-field), J (polarization) or M (magnetization) generated by `sources` as seen by the `observers` in their local coordinates. `sources` can be any Magpylib source object (e.g. magnets) or a flat list thereof. `observers` can be an array of position vectors with shape `(n1,n2,n3,...,3)`, any Magpylib observer object (e.g. sensors), or a flat list thereof. The following code shows a minimal example for Magpylib field computation. + +```python +import magpylib as magpy + +# Define source and observer objects +loop = magpy.current.Circle(current=1, diameter=.001) +sens = magpy.Sensor() + +# Compute field +B = magpy.getB(loop, sens) + +print(B) +# --> [0. 0. 0.00125664] +``` + +For quick access, the functions `getBHJM` are also methods of all Magpylib objects, such that the `sources` or `observers` input is the object itself. The above example can be continued as + +```python +# Call getB as method of loop +B = loop.getB(sens) + +# Call getB as method of loop +B = sens.getB(loop) +``` + +with the same result for `B`. + +By default, `getB` returns the B-field in units of T, `getH` the H-field in units of A/m, `getJ` the magnetic polarization in T and, `getM` the magnetization in A/m, assuming that all inputs are given in SI units as described in the docstrings. + +```{hint} +In reality, `getB` is proportional to the `polarization` input and therefore returns the same unit. For example, with polarization input in mT, `getB` will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $ยต_0$. +``` + +The output of a field computation `magpy.getB(sources, observers)` is by default a NumPy array of shape `(l, m, k, n1, n2, n3, ..., 3)` where `l` is the number of input sources, `m` the (maximal) object path length, `k` the number of observers, `n1,n2,n3,...` the sensor pixel shape or the shape of the observer position array input and `3` the three magnetic field components $(B_x, B_y, B_z)$. + +* `squeeze`: If True (default) all axes of length 1 in the output (e.g. only a single source) are squeezed. + +* `pixel_agg`: Select a compatible NumPy aggregator function (e.g. `"min"`, `"mean"`) that is applied to the output. For example, with `pixel_agg="mean"` the mean field of all observer points is returned. With this option it is possible to supply `getBHJM` with multiple observers that have different pixel shapes. + +* `output`: Change the output format. Options are `"ndarray"` (default, returns a NumPy ndarray) and `"dataframe"` (returns a 2D-table Pandas DataFrame). + +```{note} +Magpylib collects all inputs (object parameters), and vectorizes them for the computation which reduces the computation time dramatically for large inputs. + +Try to make all field computations with as few calls to `getBHJM` as possible. Avoid Python loops at all costs! +``` + +(docs-field-functional)= +## Functional interface + +Users can bypass the object oriented functionality of Magpylib and instead compute the field for n given parameter sets. This is done by providing the following inputs to the top level functions `getB`, `getH`, `getJ` and, `getM`. + +1. `sources`: a string denoting the source type. Allowed values are the Magpylib source class names, see {ref}`docs-classes`. +2. `observers`: array-like of shape (3,) or (n,3) giving the observer positions. +3. `kwargs`: a dictionary with inputs of shape (x,) or (n,x). Must include all mandatory class-specific inputs. By default, `position=(0,0,0)` and `orientation=None`(=unit rotation). + +All "scalar" inputs of shape (x,) are automatically tiled up to shape (n,x) to create a set of n computation instances. The field is returned in the shape (n,3). The following code demonstrates the functional interface. + +```python +import numpy as np +import magpylib as magpy +# All inputs and outputs in SI units + +# Compute the cuboid field for 3 input instances +N = 3 # number of instances +B = magpy.getB( + sources='Cuboid', + observers=np.linspace((0,0,1), (0,0,3), N), + dimension=np.linspace((1,1,1), (3,3,3),3, N), + polarization=(0,0,1), +) + +# This example demonstrates the scale invariance +print(B) +# --> [[0. 0. 0.13478239] +# [0. 0. 0.13478239] +# [0. 0. 0.13478239]] +``` + +```{note} +The functional interface is potentially faster than the object oriented one if users know how to generate the input arrays efficiently with numpy (e.g. `np.arange`, `np.linspace`, `np.tile`, `np.repeat`, ...). +``` + + +(docs-field-core)= +## Core interface + +At the heart of Magpylib lies a set of core functions that are our implementations of analytical field expressions found in the literature, see {ref}`guide-ressources-physics`. Direct access to these functions is given through the `magpylib.core` subpackage which includes, + +::::{grid} 1 +:gutter: 1 + +:::{grid-item} +<span style="color: orange">**magnet_cuboid_Bfield(**</span> `observers`, `dimensions`, `polarizations`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**magnet_cylinder_axial_Bfield(**</span> `z0`, `r`, `z`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**magnet_cylinder_diametral_Hfield(**</span> `z0`, `r`, `z`, `phi`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**magnet_cylinder_segment_Hfield(**</span> `observers`, `dimensions`, `magnetizations`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**magnet_sphere_Bfield(**</span>`observers`, `diameters`, `polarizations`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**current_circle_Hfield(**</span>`r0`, `r`, `z`, `i0`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**current_polyline_Hfield(**</span>`observers`, `segments_start`, `segments_end`, `currents`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**dipole_Hfield(**</span>`observers`, `moments`<span style="color: orange">**)**</span> +::: + +:::{grid-item} +<span style="color: orange">**triangle_Bfield(**</span>`observers`, `vertices`, `polarizations`<span style="color: orange">**)**</span> +::: + +:::: + +All inputs must be NumPy ndarrays of shape (n,x). Details can be found in the respective function docstrings. The following example demonstrates the core interface. + + +```python +import numpy as np +import magpylib as magpy +# All inputs and outputs in SI units + +# Prepare input +z0 = np.array([1,1]) +r = np.array([1,1]) +z = np.array([2,2]) + +# Compute field with core functions +B = magpy.core.magnet_cylinder_axial_Bfield(z0=z0, r=r, z=z).T + +print(B) +# --> [[0.05561469 0. 0.06690167] +# [0.05561469 0. 0.06690167]] +``` + +## Field computation workflow + +The Magpylib field computation internal workflow and different approaches of the three interfaces is outlined in the following sketch. + + diff --git a/docs/_pages/user_guide/docs/docs_graphics.md b/docs/_pages/user_guide/docs/docs_graphics.md new file mode 100644 index 000000000..74ab19846 --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_graphics.md @@ -0,0 +1,470 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.1 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +orphan: true +--- + +(guide-graphics)= +# Graphic output + +(guide-graphics-show)= +## 3D graphics with show + +Once all Magpylib objects and their paths have been created, `show` creates a 3D plot of the geometric arrangement using the Matplotlib (command line default) and Plotly (notebook default) packages. `show` generates a new figure which is automatically displayed. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +magnet = magpy.magnet.Cylinder( + polarization=(0, 0, 1), + dimension=(1, 1), +) +current = magpy.current.Circle( + current=1, + diameter=3, +) +dipole = magpy.misc.Dipole( + moment=(0, 0, 1), + position=np.linspace((2, 0, -2), (2, 0, 2), 20), +) +sensor = magpy.Sensor( + pixel=[(0, 0, z) for z in (-0.5, 0, 0.5)], + position=(-2, 0, 0), +) +magpy.show(magnet, current, dipole, sensor) +``` + +Notice that objects and their paths are automatically assigned different colors. The polarization of the magnet is displayed by default (Plotly and Pyvista) by coloring the poles, which overwrites the object color. In Matplotlib the polarization is by default displayed by an arrow. Current directions and dipole objects are indicated by arrows and sensors are shown as tri-colored coordinate cross with pixel as markers. + +How objects are represented graphically (color, line thickness, etc.) is defined by their [style properties](guide-graphic-styles). + +(guide-graphic-backends)= +## Graphic backends + +The graphic backend refers to the plotting library that is used for graphic output. A plotting canvas refers to the frame/window/canvas/axes object the graphic output is forwarded to. + +The graphic backend is set via the kwarg `backend` in the `show` function, which is demonstrated in the following example + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Define sources and paths +loop = magpy.current.Circle( + current=1, diameter=1, position=np.linspace((0, 0, -3), (0, 0, 3), 40) +) +cylinder = magpy.magnet.Cylinder( + polarization=(0, -1, 0), dimension=(1, 2), position=(0, -3, 0) +) +cylinder.rotate_from_angax(np.linspace(0, 300, 40), "z", anchor=0, start=0) + +for backend in magpy.SUPPORTED_PLOTTING_BACKENDS: + print(backend) + magpy.show(loop, cylinder, backend=backend) +``` + +With the installation default setting, `backend='auto'`, Magpylib infers the graphic backend from the environment running the code, or from the requested canvas. + +| environment | canvas | inferred backend | +|------------------|-------------------------------------------------|------------------| +| Command-Line | `None` | `matplotlib` | +| IPython notebook | `None` | `plotly` | +| all | `matplotlib.axes.Axes` | `matplotlib` | +| all | `plotly.graph_objects.Figure` or `FigureWidget` | `plotly` | +| all | `pyvista.Plotter` | `pyvista` | + +The library default can be changed, e.g. with the command `magpy.defaults.display.backend = 'plotly'`. + +There is a high level of **feature parity**, however, not all graphic features are supported by all backends, and not all graphic features work equally well, so that [default style settings](guide-graphic-styles-default) differ slightly. In addition, some common Matplotlib syntax (e.g. color `'r'`, linestyle `':'`) is automatically translated to other backends. + +| Feature | Matplotlib | Plotly | Pyvista | +|:------------------------:|:----------:|:------:|:-------:| +| triangular mesh 3d | โ๏ธ | โ๏ธ | โ๏ธ | +| line 3d | โ๏ธ | โ๏ธ | โ๏ธ | +| line style | โ๏ธ | โ๏ธ | โ | +| line color | โ๏ธ | โ๏ธ | โ๏ธ | +| line width | โ๏ธ | โ๏ธ | โ๏ธ | +| marker 3d | โ๏ธ | โ๏ธ | โ๏ธ | +| marker color | โ๏ธ | โ๏ธ | โ๏ธ | +| marker size | โ๏ธ | โ๏ธ | โ๏ธ | +| marker symbol | โ๏ธ | โ๏ธ | โ | +| marker numbering | โ๏ธ | โ๏ธ | โ | +| zoom level | โ๏ธ | โ๏ธ | โ[2] | +| magnetization color | โ๏ธ[7] | โ๏ธ | โ๏ธ | +| animation | โ๏ธ | โ๏ธ | โ๏ธ[5] | +| animation time | โ๏ธ | โ๏ธ | โ๏ธ[5] | +| animation fps | โ๏ธ | โ๏ธ | โ๏ธ[5] | +| animation slider | โ๏ธ[1] | โ๏ธ | โ | +| subplots 2D | โ๏ธ | โ๏ธ | โ๏ธ[6] | +| subplots 3D | โ๏ธ | โ๏ธ | โ๏ธ | +| user canvas | โ๏ธ | โ๏ธ | โ๏ธ | +| user extra 3d model [3] | โ๏ธ | โ๏ธ | โ๏ธ[4] | + +[1]: when returning animation object and exporting it as jshtml. + +[2]: possible but not implemented at the moment. + +[3]: only `"scatter3d"`, and `"mesh3d"`. Gets "translated" to every other backend. + +[4]: custom user defined trace constructors allowed, which are specific to the backend. + +[5]: animation is only available through export as `gif` or `mp4` + +[6]: 2D plots are not supported for all jupyter_backends. As of pyvista>=0.38 these are deprecated and replaced by the [trame](https://docs.pyvista.org/api/plotting/trame.html) backend. + +[7]: Matplotlib does not support color gradient. Instead magnetization is shown through object slicing and coloring. + +`show` will also pass on all kwargs to the respective plotting backends. For example, in the [animation sample code](guide-graphic-animations) the kwarg `show_legend` is forwarded to the Plotly backend. + + +(guide-graphics-canvas)= +## Plotting canvas + +When calling `show`, a figure is automatically generated and displayed. It is also possible to place the `show` output in a given figure using the `canvas` argument. Consider the following Magpylib field computation, + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Magpylib field computation +loop = magpy.current.Circle(current=1, diameter=0.1) +sens = magpy.Sensor(position=np.linspace((0, 0, -0.1), (0, 0, 0.1), 100)) +B = loop.getB(sens) +``` + +The following examples demonstrate how to place the Magpylib `show` output in figures created with the three supported graphic backends. + ++++ + +In **Matplotlib** we combine a 2D-field plot with the 3D show output and modify the 3D show output with a line. + +```{code-cell} ipython3 +import magpylib as magpy +import matplotlib.pyplot as plt +import numpy as np + +# Magpylib field computation +loop = magpy.current.Circle(current=1, diameter=0.1) +sens = magpy.Sensor(position=np.linspace((0, 0, -0.1), (0, 0, 0.1), 100)) +B = loop.getB(sens) + +# Create Matplotlib figure with subplots +fig = plt.figure(figsize=(10, 4)) +ax1 = fig.add_subplot(121) +ax2 = fig.add_subplot(122, projection="3d") + +# 2D Matplotlib plot +ax1.plot(B) + +# Place Magpylib show output in Matplotlib figure +magpy.show(loop, sens, canvas=ax2) + +# Modify show output +ax2.plot([-0.1, 0.1], [0, 0], [0, 0], color="k") + +# Render figure +plt.tight_layout() +plt.show() +``` + +```{attention} +When providing a canvas, no update to its layout is performed by Magpylib, unless explicitly specified by setting `canvas_update=True` in `show()`. By default `canvas_update="auto"` only updates the canvas if is not provided by the user. The example above outputs a 3D scene with the default Matplotlib settings and will not match the standard Magpylib settings. +``` + ++++ + +In **Plotly** we combine a 2D-field plot with the 3D show output and modify the 3D show output with a line. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np +import plotly.graph_objects as go + +# Magpylib field computation +loop = magpy.current.Circle(current=1, diameter=0.1) +sens = magpy.Sensor(position=np.linspace((0, 0, -0.1), (0, 0, 0.1), 100)) +B = loop.getB(sens) + +# Create Plotly figure and subplots +fig = go.Figure().set_subplots( + rows=1, cols=2, specs=[[{"type": "xy"}, {"type": "scene"}]] +) + +# 2D Plotly plot +fig.add_scatter(y=B[:, 2], name="Bz") + +# Draw 3d model in the existing Plotly figure +magpy.show(loop, sens, canvas=fig, col=2, canvas_update=True) + +# Add 3d scatter trace to main figure model +fig.add_scatter3d(x=(-0.1, 0.1), y=(0, 0), z=(0, 0), col=2, row=1) + +# Render figure +fig.show() +``` + +**Pyvista** is not made for 2D plotting. Here we simply add a line to the 3D show output. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +import pyvista as pv + +# Create Pyvista scene +pl = pv.Plotter() + +# Place Magpylib show output in Pyvista scene +magpy.show(loop, sens, canvas=pl) + +# Add a Line to 3D scene +line = np.array([(-0.1, 0, 0), (0.1, 0, 0)]) +pl.add_lines(line, color="black") + +# Render figure +pl.show() +``` + +(guide-graphics-return_fig)= +## Return figure + +Instead of forwarding a figure to an existing canvas, it is also possible to return the figure object for further manipulation using the `return_fig` command. In the following example this is demonstrated for the pyvista backend. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop = magpy.current.Circle(current=1, diameter=0.1) +sens = magpy.Sensor(position=np.linspace((0, 0, -0.1), (0, 0, 0.1), 100)) + +# Return pyvista scene object with show +pl = magpy.show(loop, sens, backend="pyvista", return_fig=True) + +# Modify Pyvista scene +pl.add_lines(np.array([(-0.1, 0, 0), (0.1, 0, 0)]), color="black") +pl.camera.position = (0.5, 0.2, 0.1) +pl.set_background("yellow", top="lightgreen") +pl.enable_anti_aliasing("ssaa") + +# Display scene +pl.show() +``` + +(guide-graphic-animations)= +## Animation + +The Magpylib [object paths](docs-position-paths) visualized with `show` can be animated by setting the kwarg `animation=True`. This synergize specifically well with the Plotly backend. + +The animations can be fine-tuned with the following kwargs of `show`: +1. `animation_time` (default=3), must be a positive number that gives the animation time in seconds. +2. `animation_slider` (default=`True`), is boolean and sets if a slider should be displayed. +3. `animation_fps` (default=30), sets the maximal frames per second. + +Each path step will generate one frame of the animation, unless `animation_fps` would be exceeded. In this case specific equidistant frames will be selected automatically to adjust to the limited display possibilities. For practicality, the input `animation=x` will automatically set `animation=True` and `animation_time=x`. + +The following example demonstrates the animation feature, + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop = magpy.current.Circle(current=1, diameter=0.1) +sens = magpy.Sensor(position=np.linspace((0, 0, -0.1), (0, 0, 0.1), 100)) + +# Show animation +magpy.show( + loop, + sens, + animation=1, + animation_fps=20, + animation_slider=True, + backend="plotly", + showlegend=False, +) +``` + +```{warning} +Even with some implemented fail safes, such as a maximum frame rate and frame count, there is no guarantee that the animation will be rendered properly. This is particularly relevant when the user tries to animate many objects and/or many path positions at the same time. +``` + +(guide-graphics-subplots)= +## Built-in Subplots + +:::{versionadded} 4.4 +Coupled subplots +::: + +It is often tedious to integrate the Magpylib `show` output into sub-plots as shown above, especially when dealing with animations and combinations of 2D and 3D plots. + +For this, Magpylib offers the possibility to show the sensor output along a path in addition to the 3D-output, and to place 2D and 3D outputs in subplots. + +### With show + +All of this is achieved via the `show` function by passing input objects as dictionaries with the arguments. + +1. `objects`: list of Magpylib objects +2. `col`: int which selects the subplot column. Default is `col=1`. +3. `row`: int which selects the subplot row. Default is `row=1`. +4. `output`: string which selects the type of output that should be displayed in this subplot. Options are + + 1. `"model3d"` is the default value and selects the 3D output. + 2. `"Xa"` selects a 2D line-plot of a field component (combination) as seen by the sensor(s) along their path. The sensor(s) must be part of the `objects` input. Here "X" selects the field and must be one of "BHJM", and "a" selects the respective component combination and must be a subset of "xyz". For example, `output=Hx` displays the x-component of the H-field, or `output=Bxz` displays `sqrt(|Bx|ยฒ + |Bz|ยฒ)`. By default, source outputs are summed up (`sumup=True`) and sensor pixels, are aggregated by mean (`pixel_agg="mean"`). + +The following code demonstrates these features. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop = magpy.current.Circle(current=1, diameter=0.1, style_label="L") +sens = magpy.Sensor( + position=np.linspace((-0.1, 0, 0.1), (0.1, 0, 0.1), 50), style_label="S" +) + +# Use built-in subplots +magpy.show( + {"objects": [loop, sens]}, + {"objects": [loop, sens], "output": "Bx", "col": 2}, + {"objects": [loop, sens], "output": ["Hx", "Hy", "Hz"], "row": 2}, + {"objects": [loop, sens], "output": "Hxyz", "col": 2, "row": 2}, + backend="matplotlib", +) +``` + +Each input dictionary can contain kwargs, like `pixel_agg=None` or `sumup=False` for 2D plots. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop1 = magpy.current.Circle(current=1, diameter=0.1, style_label="L1") +loop2 = loop1.copy(diameter=0.2, style_label="L2") +sens = magpy.Sensor( + pixel=[(0.01, 0, 0), (-0.01, 0, 0)], + position=np.linspace((-0.2, 0, 0.1), (0.2, 0, 0.1), 50), + style_label="S", +) +obj = [loop1, loop2, sens] + +# Use built-in subplots +magpy.show( + {"objects": obj, "output": "Hx"}, + {"objects": obj, "output": "Hx", "pixel_agg": None, "col": 2}, + {"objects": obj, "output": "Hx", "sumup": False, "row": 2}, + { + "objects": obj, + "output": "Hx", + "pixel_agg": None, + "sumup": False, + "row": 2, + "col": 2, + }, +) +``` + +(guide-graphics-show_context)= +### With show_context + +To make the subplot syntax more convenient we introduced the `show_context` native Python context manager. It allows to defer calls to the `show` function while passing additional arguments. This is necessary for Magpylib to know how many rows and columns are requested by the user, which single `show` calls do not keep track of. All kwargs, e.g. `backend` are handed directly to the context manager. + +The above example becomes: + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop = magpy.current.Circle(current=1, diameter=0.1, style_label="L") +sens = magpy.Sensor( + position=np.linspace((-0.1, 0, 0.1), (0.1, 0, 0.1), 50), style_label="S" +) + +# Use built-in subplots via show_context +with magpy.show_context(loop, sens, backend="plotly") as sc: + sc.show() + sc.show(output="Bx", col=2) + sc.show(output=["Hx", "Hy", "Hz"], row=2) + sc.show(output="Hxyz", col=2, row=2) +``` + +````{note} +Using the context manager object as in: + +```python +import magpylib as magpy + +obj1 = magpy.magnet.Cuboid() +obj2 = magpy.magnet.Cylinder() + +with magpy.show_context() as sc: + sc.show(obj1, col=1) + sc.show(obj2, col=2) +``` + +is equivalent to the use of `magpylib.show` directly, as long as within the context manager: + +```python +import magpylib as magpy + +obj1 = magpy.magnet.Cuboid() +obj2 = magpy.magnet.Cylinder() + +with magpy.show_context(): + magpy.show(obj1, col=1) + magpy.show(obj2, col=2) +``` +```` + +### Coupled 2D/3D Animation + +It is very helpful to combine 2D and 3D subplots in an animation that shows the motion of the 3D system, while displaying the field at the respective path instance at the same time. Unfortunately, it is quite tedious to create such animations. The most powerful feature and main reason behind built-in subplots is the ability to do just that with few lines of code. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Create Magpylib objects with paths +loop = magpy.current.Circle(current=1, diameter=0.1, style_label="L") +sens = magpy.Sensor( + position=np.linspace((-0.1, 0, 0.1), (0.1, 0, 0.1), 50), style_label="S" +) + +# Use built-in subplots via show_context +with magpy.show_context(loop, sens, animation=True) as sc: + sc.show() + sc.show(output="Bx", col=2) + sc.show(output=["Hx", "Hy", "Hz"], row=2) + sc.show(output="Hxyz", col=2, row=2) +``` + +### Canvas length units + +When displaying very small Magpylib objects, the axes scaling in meters might be inadequate and you may want to use other units that fit the system dimensions more nicely. The example below shows how to display an object (in this case the same) with different length units and zoom levels. + +```{tip} +Setting `units_length="auto"` will infer the most suitable units based on the maximum range of the system. +``` + +```{code-cell} ipython3 +import magpylib as magpy + +c1 = magpy.magnet.Cuboid(dimension=(0.001, 0.001, 0.001), polarization=(1, 2, 3)) + +with magpy.show_context(c1, backend="matplotlib") as s: + s.show(row=1, col=1, units_length="auto", zoom=0) + s.show(row=1, col=2, units_length="mm", zoom=1) + s.show(row=2, col=1, units_length="ยตm", zoom=2) + s.show(row=2, col=2, units_length="m", zoom=3) +``` diff --git a/docs/_pages/user_guide/docs/docs_magpylib_force.md b/docs/_pages/user_guide/docs/docs_magpylib_force.md new file mode 100644 index 000000000..71536f4af --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_magpylib_force.md @@ -0,0 +1,93 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(docs-magpylib-force)= +# Magpylib Force v0.3.1 + +The package `magpylib-force` provides an addon for magnetic force and torque computation between magpylib source objects. + +## Installation + +Install `magpylib-force` with pip: + +```console +pip install magpylib-force +``` + +## API + +The package provides only a single top-level function <span style="color: orange">**getFT()**</span> for computing force and torque. + +```python +import magpylib_force as mforce +mforce.getFT(sources, targets, anchor, eps=1e-5, squeeze=True) +``` + +Here `sources` are Magpylib source objects that generate the magnetic field. The `targets` are the objects on which the magnetic field of the sources acts to generate force and torque. With current version 0.3.1 only `Cuboid`, `Cylinder`, `CylinderSegment`, `Polyline`, and `Circle` objects can be targets. The `anchor` denotes an anchor point which is the barycenter of the target. If no barycenter is given, homogeneous mass density is assumed and the geometric center of the target is chose as it's barycenter. `eps` refers to the finite difference length when computing the magnetic field gradient and should be adjusted to be much smaller than size of the system. `squeeze` can be used to squeeze the output array dimensions as in Magpylib's `getB`, `getH`, `getJ`, and `getM`. + +The computation is based on numerically integrating the magnetic field generated by the `sources` over the `targets`, see [here](docs-force-computation) for more details. This requires that each target has a <span style="color: orange">**meshing**</span> directive, which must be provided via an attribute to the object. How `meshing` is defined: + +For all objects as an integer, which defines the target number of mesh-points. In some cases an algorithm will attempt to come close to this number by splitting up magnets into quasi-cubical cells. Exceptions are: + +- `Cuboid`: takes also a 3-vector that defines the number of equidistant splits along each axis resulting in a rectangular regular grid. Keep in mind that the accuracy is increased by cubical aspect ratios. +- `PolyLine`: defines the number of equidistant splits of each PolyLine segment, not of the whole multi-segmented object. The total number of mesh-points will be number of segments times meshing. + +The function `getFT()` returns force and torque as `np.ndarray` of shape (2,3), or (t,2,3) when t targets are given. + +The following example code computes the force acting on a cuboid magnet, generated by a current loop. + +```python +import magpylib as magpy +import magpylib_force as mforce + +# create source and target objects +loop = magpy.current.Circle(diameter=2e-3, current=10, position=(0,0,-1e-3)) +cube = magpy.magnet.Cuboid(dimension=(1e-3,1e-3,1e-3), polarization=(1,0,0)) + +# provide meshing for target object +cube.meshing = (5,5,5) + +# compute force and torque +FT = mforce.getFT(loop, cube) +print(FT) +# [[ 1.36304272e-03 6.35274710e-22 6.18334051e-20] # force in N +# [-0.00000000e+00 -1.77583097e-06 1.69572026e-23]] # torque in Nm +``` + +```{warning} +[Scaling invariance](guide-docs-io-scale-invariance) does not hold for force computations! Be careful to provide the inputs in the correct units! +``` + +(docs-force-computation)= +## Computation details + +The force $\vec{F}_m$ acting on a magnetization distribution $\vec{M}$ in a magnetic field $\vec{B}$ is given by + +$$\vec{F}_m = \int \nabla (\vec{M}\cdot\vec{B}) \ dV.$$ + +The torque $\vec{T}_m$ which acts on the magnetization distribution is + +$$\vec{T}_m = \int \vec{M} \times \vec{B} \ dV.$$ + +The force $\vec{F}_c$ which acts on a current distribution $\vec{j}$ in a magnetic field is + +$$\vec{F}_c = \int \vec{j}\times \vec{B} \ dV.$$ + +And there is no torque. However, one must not forget that a force, when applied off-center, adds to the torque as + +$$\vec{T}' = \int \vec{r} \times \vec{F} \ dV,$$ + +where $\vec{r}$ points from the body barycenter to the position where the force is applied. + +The idea behind `magplyib-force` is to compute the above integrals by discretization. For this purpose, the target body is split up into small cells using the object `meshing` attribute. The force and torque computation is performed for all cells in a vectorized form, and the sum is returned. diff --git a/docs/_pages/user_guide/docs/docs_pos_ori.md b/docs/_pages/user_guide/docs/docs_pos_ori.md new file mode 100644 index 000000000..0025c8f89 --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_pos_ori.md @@ -0,0 +1,167 @@ +(docs-position)= +# Position, Orientation, and Paths + +The following secions are detiled technical documentations of the Magpylib position and orientation interface. Practical examples and good practice usage is demonstrated in the tutorial {ref}`examples-tutorial-paths`. + +::::{grid} 2 +:gutter: 2 + +:::{grid-item} +:columns: 12 7 7 7 +The analytical magnetic field expressions found in the literature, implemented in the [Magpylib core](docs-field-core), are given in native coordinates of the sources which is convenient for the mathematical formulation. It is a common problem to transform the field into an application relevant observer coordinate system. While not technically difficult, such transformations are prone to error. +::: +:::{grid-item} +:columns: 12 5 5 5 + +::: +:::: + +Here Magpylib helps. All Magpylib sources and observers lie in a global Cartesian coordinate system. Object position and orientation are defined by the attributes `position` and `orientation`, ๐. Objects can easily be moved around using the `move()` and `rotate()` methods. Eventually, the field is computed in the reference frame of the observers (e.g. Sensor objects). Positions are given in units of meter, and the default unit for orientation is ยฐ. + +(docs-position-paths)= +## Position and orientation attributes + +Position and orientation of all Magpylib objects are defined by the two attributes + +::::{grid} 2 +:gutter: 2 + +:::{grid-item-card} +:shadow: none +:columns: 12 5 5 5 +<span style="color: orange">**position**</span> - a point $(x,y,z)$ in the global coordinates, or a set of such points $(\vec{P}_1, \vec{P}_2, ...)$. By default objects are created with `position=(0,0,0)`. +::: +:::{grid-item-card} +:shadow: none +:columns: 12 7 7 7 +<span style="color: orange">**orientation**</span> - a [Scipy Rotation object](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html) which describes the object rotation relative to its default orientation (defined in {ref}`docs-classes`). By default, objects are created with unit rotation `orientation=None`. +::: +:::: + +The position and orientation attributes can be either **scalar**, i.e. a single position or a single rotation, or **vector**, when they are arrays of such scalars. The two attributes together define the **path** of an object - Magpylib makes sure that they are always of the same length. When the field is computed, it is automatically computed for the whole path. + +```{tip} +To enable vectorized field computation, paths should always be used when modeling multiple object positions. Avoid using Python loops at all costs for that purpose! If your path is difficult to realize, consider using the [functional interface](docs-field-functional) instead. +``` + +## Move and Rotate + +Magpylib offers two powerful methods for object manipulation: + +::::{grid} 2 +:gutter: 2 + +:::{grid-item-card} +:columns: 12 5 5 5 +:shadow: none +<span style="color: orange">**move(**</span>`displacement`, `start="auto"`<span style="color: orange">**)**</span> - move object by `displacement` input. `displacement` is a position vector (scalar input) or a set of position vectors (vector input). +::: +:::{grid-item-card} +:columns: 12 7 7 7 +:shadow: none +<span style="color: orange">**rotate(**</span>`rotation`, `anchor=None`, `start="auto"`<span style="color: orange">**)**</span> - rotates the object by the `rotation` input about an anchor point defined by the `anchor` input. `rotation` is a [Scipy Rotation object](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html), and `anchor` is a position vector. Both can be scalar or vector inputs. With `anchor=None` the object is rotated about its `position`. +::: +:::: + +- **Scalar input** is applied to the whole object path, starting with path index `start`. With the default `start="auto"` the index is set to `start=0` and the functionality is **moving objects around** (incl. their whole paths). +- **Vector input** of length $n$ applies the $n$ individual operations to $n$ object path entries, starting with path index `start`. Padding applies when the input exceeds the existing path length. With the default `start="auto"` the index is set to `start=len(object path)` and the functionality is **appending the input**. + +The practical application of this formalism is best demonstrated by the following program + +```python +import magpylib as magpy +# Note that all units are in SI + +sensor = magpy.Sensor() +print(sensor.position) # Default value +# --> [0. 0. 0.] + +sensor.move((1,1,1)) # Scalar input is by default applied +print(sensor.position) # to the whole path +# --> [1. 1. 1.] + +sensor.move([(1,1,1), (2,2,2)]) # Vector input is by default appended +print(sensor.position) # to the existing path +# --> [[1. 1. 1.] [2. 2. 2.] [3. 3. 3.]] + +sensor.move((1,1,1), start=1) # Scalar input and start=1 is applied +print(sensor.position) # to whole path starting at index 1 +# --> [[1. 1. 1.] [3. 3. 3.] [4. 4. 4.]] + +sensor.move([(0,0,10), (0,0,20)], start=1) # Vector input and start=1 merges +print(sensor.position) # the input with the existing path +# --> [[ 1. 1. 1.] [ 3. 3. 13.] [ 4. 4. 24.]] # starting at index 1. +``` + +Several extensions of the `rotate` method give a lot of flexibility with object rotation. They all feature the arguments `anchor` and `start` which work as described above. + +::::{grid} 1 +:gutter: 2 + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_angax(**</span>`angle`, `axis`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> +* `angle`: scalar or array with shape (n). Angle(s) of rotation. +* `axis`: array of shape (3,) or string. The direction of the rotation axis. String input can be 'x', 'y' or 'z' to denote respective directions. +* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). +::: + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_rotvec(**</span>`rotvec`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> +* `rotvec` : array with shape (n,3) or (3,). The rotation vector direction is the rotation axis and the vector length is the rotation angle in units of deg. +* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). +::: + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_euler(**</span> `angle`, `seq`, `anchor=None`, `start="auto"`, `degrees=True` <span style="color: orange">**)**</span> +* `angle`: scalar or array with shape (n). Angle(s) of rotation in units of deg (by default). +* `seq` : string. Specifies sequence of axes for rotations. Up to 3 characters belonging to the set {'X', 'Y', 'Z'} for intrinsic rotations, or {'x', 'y', 'z'} for extrinsic rotations. Extrinsic and intrinsic rotations cannot be mixed in one function call. +* `degrees`: bool, default=True. Interpret angle input in units of deg (True) or rad (False). +::: + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_quat(**</span>`quat`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> +* `quat` : array with shape (n,4) or (4,). Rotation input in quaternion form. +::: + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_mrp(**</span>`matrix`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> +* `matrix` : array with shape (n,3,3) or (3,3). Rotation matrix. See scipy.spatial.transform.Rotation for details. +::: + +:::{grid-item-card} +:columns: 12 +:shadow: none +<span style="color: orange">**rotate_from_mrp(**</span>`mrp`, `anchor=None`, `start="auto"` <span style="color: orange">**)**</span> +* `mrp` : array with shape (n,3) or (3,). Modified Rodrigues parameter input. See scipy Rotation package for details. +::: + +:::: + +When objects with different path lengths are combined, e.g. when computing the field, the shorter paths are treated as static beyond their end to make the computation sensible. Internally, Magpylib follows a philosophy of edge-padding and end-slicing when adjusting paths. + +::::{grid} 2 +:gutter: 2 + +:::{grid-item-card} +:columns: 12 7 7 7 +:shadow: none +**Edge-padding:** whenever path entries beyond the existing path length are needed the edge-entries of the existing path are returned. This means that the object is โstaticโ beyond its existing path. +::: +:::{grid-item-card} +:columns: 12 5 5 5 +:shadow: none +**End-slicing:** whenever a path is automatically reduced in length, Magpylib will slice such that the ending of the path is kept. +::: +:::: + +The tutorial {ref}`examples-tutorial-paths` shows intuitive good practice examples of the important functionality described in this section. \ No newline at end of file diff --git a/docs/_pages/user_guide/docs/docs_styles.md b/docs/_pages/user_guide/docs/docs_styles.md new file mode 100644 index 000000000..39075b285 --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_styles.md @@ -0,0 +1,562 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.0 +kernelspec: + display_name: Python 3 + language: python + name: python3 +orphan: true +--- + +(guide-graphic-styles)= +# Graphic styles + +The graphic styles define how Magpylib objects are displayed visually when calling `show`. They can be fine-tuned and individualized to suit requirements and taste. + +Graphic styles can be defined in various ways: + +1. There is a **default style** setting which is applied when no other inputs are made. +2. An **individual style** can be defined at object level. If the object is a [Collection](guide-docs-classes-collections) it will apply its color to all children. +3. Finally, styles that are defined in the `show` function call will override all other settings. This is referred to as **local style override**. + +The following sections describe these styling options and how to customize them. + +(guide-graphic-styles-default)= +## Default style + +The default style is stored in `magpylib.defaults.display.style`. Note that the installation default styles differ slightly between different [graphic backends](guide-graphic-backends) depending on their respective capabilities. Specifically, the magnet magnetization in Matplotlib is displayed with arrows by default, while it is displayed using a color scheme in Plotly and Pyvista. The color scheme is also implemented in Matplotlib, but it is visually unsatisfactory. + +The default styles can be modified in three ways: + +1. By setting the default properties, + +```python +magpy.defaults.display.style.magnet.magnetization.show = True +magpy.defaults.display.style.magnet.magnetization.color.mode = 'bicolor' +magpy.defaults.display.style.magnet.magnetization.color.north = 'grey' +``` + +2. By assigning a style dictionary with equivalent keys, + +```python +magpy.defaults.display.style.magnet = { + 'magnetization': {'show': True, 'color': {'north': 'grey', 'mode': 'tricolor'}} +} +``` + +3. By making use of the `update` method: + +```python +magpy.defaults.display.style.magnet.magnetization.update( + 'show': True, + 'color': {'north'='grey', mode='tricolor',} +) +``` + +All three examples result in the same default style. Once modified, the library default can always be restored with the `magpylib.style.reset()` method. The following practical example demonstrates how to create and set a user defined magnet magnetization style as default. The chosen custom style combines a 3-color scheme with an arrow which points in the magnetization direction. + +```{code-cell} ipython3 +import magpylib as magpy + +# Define Magpylib magnet objects +cube = magpy.magnet.Cuboid(polarization=(1, 0, 0), dimension=(1, 1, 1)) +cylinder = magpy.magnet.Cylinder( + polarization=(0, -1, 0), dimension=(1, 1), position=(2, 0, 0) +) +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), diameter=1, position=(4, 0, 0) +) + +# Show with Magpylib default style +print("Default magnetization style") +magpy.show(cube, cylinder, sphere, backend="plotly") + +# Create and set user-defined default style for magnetization +user_magnetization_style = { + "show": True, + "mode": "arrow+color", + "size": 1, + "arrow": { + "color": "black", + "offset": 1, + "show": True, + "size": 2, + "sizemode": "scaled", + "style": "solid", + "width": 3, + }, + "color": { + "transition": 0, + "mode": "tricolor", + "middle": "white", + "north": "magenta", + "south": "turquoise", + }, +} +magpy.defaults.display.style.magnet.magnetization = user_magnetization_style + +# Show with new default style +print("Custom magnetization style") +magpy.show(cube, cylinder, sphere, backend="plotly") +``` + +```{note} +The default Magpylib style abides by the tri-color scheme for ideal-typical magnetic scales introduced in the DIN specification [91411](https://www.dinmedia.de/de/technische-regel/din-spec-91411/354972979) and its succeeding standard DIN SPEC 91479. +``` + +A list of all style options can be found [here](examples-list-of-styles). + + +## Magic underscore notation + +To facilitate working with deeply nested properties, all style constructors and object style methods support the "magic underscore notation". It enables referencing nested properties by joining together multiple property names with underscores. This feature mainly helps reduce the code verbosity and is heavily inspired by the [Plotly underscore notation](https://plotly.com/python/creating-and-updating-figures/#magic-underscore-notation)). + +With magic underscore notation, the previous examples can be written as: + +```python +import magpylib as magpy +magpy.defaults.display.style.magnet = { + 'magnetization_show': True, + 'magnetization_color_middle': 'grey', + 'magnetization_color_mode': 'tricolor', +} +``` + +or directly as named keywords in the `update` method as: + +```python +import magpylib as magpy +magpy.defaults.display.style.magnet.update( + magnetization_show=True, + magnetization_color_middle='grey', + magnetization_color_mode='tricolor', +) +``` + +## Individual style + +Any Magpylib object can have its own individual style that will take precedence over the default values when `show` is called. When setting individual styles, the object family specifier such as `magnet` or `current` can be omitted. + +```{note} +Users should be aware that the individual object style is organized in classes that take much longer to initialize than bare Magpylib objects, i.e. objects without individual style. This can lead to a computational bottleneck when setting individual styles of many Magpylib objects. For this reason Magpylib automatically defers style initialization until it is needed the first time, e.g. when calling the `show` function, so that object creatin time is not affected. However, this works only if style properties are set at initialization (e.g.: `magpy.magnet.Cuboid(..., style_label="MyCuboid")`). While this effect may not be noticeable for a small number of objects, it is best to avoid setting styles until it is plotting time. +``` + +In the following example `cube` has no individual style, so the default style is used. `cylinder` has an individual style set for `magnetization` which is a tricolor scheme that will display the object color in the middle. The individual style is set at object initialization (good practice), and it will be applied only when `show` is called at the end of the example. Finally, `sphere` is also given an individual style for `magnetization` that displays the latter using a 2-color scheme. In this case, however, the individual style is applied after object initialization (bad practice), which results in style initialization before it is needed. + +```{code-cell} ipython3 +import magpylib as magpy + +# Reset defaults from previous example +magpy.defaults.reset() + +# Default style +cube = magpy.magnet.Cuboid( + polarization=(1, 0, 0), + dimension=(1, 1, 1), +) + +# Good practice: define individual style at object init +cylinder = magpy.magnet.Cylinder( + polarization=(0, 1, 0), + dimension=(1, 1), + position=(2, 0, 0), + style_magnetization_color_mode="tricycle", +) + +# Bad practice: set individual style after object init +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), + diameter=1, + position=(4, 0, 0), +) +sphere.style.magnetization.color.mode = "bicolor" + +# Show styled objects +magpy.show(cube, cylinder, sphere, backend="plotly") +``` + +## Collection style + +When displaying [Collection objects](guide-docs-classes-collections) their `color` property will be assigned to all its children override the default color cycle. In the following example this is demonstrated. Therefore, we make use of the [Matplotlib backend](guide-graphic-backends) which displays magnet color by default and shows the magnetization as an arrow rather than a color sequence. + +```{code-cell} ipython3 +import magpylib as magpy + +# Define 3 magnets +cube = magpy.magnet.Cuboid( + polarization=(1,0,0), dimension=(1,1,1) +) +cylinder = magpy.magnet.Cylinder( + polarization=(0,1,0), dimension=(1,1), position=(2,0,0) +) +sphere = magpy.magnet.Sphere( + polarization=(0,1,1), diameter=1, position=(4,0,0) +) + +# Create collection from 2 magnets +coll = cube + cylinder + +# Show styled objects +magpy.show(coll, sphere, backend="matplotlib") +``` + +In addition, it is possible to modify individual style properties of all children, that cannot be set at Collection level, with the `set_children_styles` method. Non-matching properties, e.g. magnetization color for children that are currents, are simply ignored. + +```{code-cell} ipython3 +coll.set_children_styles(magnetization_color_south="blue") +magpy.show(coll, sphere, backend="plotly") +``` + +The child-styles are individual style properties of the collection object and are not set as individual styles on each child-object. This means that when displayed individually with `show`, the above child-objects will have Magpylib default style. + +## Local style override + +Finally, it is possible to hand style input to the `show` function directly and locally override all style properties for this specific `show` output. Default or individual style attributes will not be modified. Such inputs must start with the `style` prefix and the object family specifier must be omitted. Naturally underscore magic is supported. + +In the following example the default `style.magnetization.show=True` is overridden locally, so that object colors become visible instead of magnetization colors in the Plotly backend. + +```{code-cell} ipython3 +import magpylib as magpy + +cube = magpy.magnet.Cuboid( + polarization=(1, 0, 0), dimension=(1, 1, 1) +) +cylinder = magpy.magnet.Cylinder( + polarization=(0, 1, 0), dimension=(1, 1), position=(2, 0, 0) +) +sphere = magpy.magnet.Sphere( + polarization=(0, 1, 1), diameter=1, position=(4, 0, 0) +) + +# Show with local style override +magpy.show(cube, cylinder, sphere, backend="plotly", style_magnetization_show=False) +``` + +(examples-list-of-styles)= + +## List of style properties + +```{code-cell} ipython3 +magpy.defaults.display.style.as_dict(flatten=True, separator=".") +``` + +(examples-own-3d-models)= +## Custom 3D models + +Each Magpylib object has a default 3D representation that is displayed with `show`. It is possible to disable the default model and to provide Magpylib with a custom model. + +There are several reasons why this can be of interest. For example, the integration of a [custom source](guide-docs-classes-custom-source) object that has its own geometry, to display a sensor in the form of a realistic package provided in CAD form, representation of a [Collection](guide-docs-classes-collections) as a parts holder, integration of environmental parts to the Magpylib 3D plotting scene, or simply highlighting an object when colors do not suffice. + +The default trace of a Magpylib object `obj` can simply be turned off using the individual style command `obj.style.model3d.showdefault = False`. A custom 3D model can be added using the function `obj.style.model3d.add_trace()`. The new trace is then stored in the `obj.style.model3d.data` property. This property is a list and it is possible to store multiple custom traces there. The default style is not included in this property. It is instead inherently stored in the Magpylib classes to enable visualization of the magnetization with a color scheme. + +The input of `add_trace()` must be a `magpylib.graphics.Trace3d` object, or a dictionary that contains all necessary information for generating a 3D model. Because different plotting libraries require different directives, traces might be bound to specific [backends](guide-graphic-backends). For example, a trace dictionary might contain all information for Matplotlib to generate a 3D model using the [plot_surface](https://matplotlib.org/stable/plot_types/3D/surface3d_simple.html) function. + +To enable visualization of custom objects with different graphic backends Magpylib implements a **generic backend**. Traces defined in the generic backend are translated to all other backends automatically. If a specific backend is used, the model will only appear when called with the corresponding backend. + +A trace-dictionary has the following keys: + +1. `'backend'`: `'generic'`, `'matplotlib'` or `'plotly'` +2. `'constructor'`: name of the plotting constructor from the respective backend, e.g. plotly `'Mesh3d'` or matplotlib `'plot_surface'` +3. `'args'`: default `None`, positional arguments handed to constructor +4. `'kwargs'`: default `None`, keyword arguments handed to constructor +5. `'coordsargs'`: tells Magpylib which input corresponds to which coordinate direction, so that geometric representation becomes possible. By default `{'x': 'x', 'y': 'y', 'z': 'z'}` for the `'generic'` backend and Plotly backend, and `{'x': 'args[0]', 'y': 'args[1]', 'z': 'args[2]'}` for the Matplotlib backend. +6. `'show'`: default `True`, toggle if this trace should be displayed +7. `'scale'`: default 1, object geometric scaling factor +8. `'updatefunc'`: default `None`, updates the trace parameters when `show` is called. Used to generate dynamic traces. + +The following example shows how a trace is constructed using the generic backend with the `Mesh3d` constructor. We create a `Sensor` object and replace its default 3d model by a tetrahedron. + +```{code-cell} ipython3 +import magpylib as magpy + +# Create trace dictionary +trace_mesh3d = { + "backend": "generic", + "constructor": "Mesh3d", + "kwargs": { + "x": (1, 0, -1, 0), + "y": (-0.5, 1.2, -0.5, 0), + "z": (-0.5, -0.5, -0.5, 1), + "i": (0, 0, 0, 1), + "j": (1, 1, 2, 2), + "k": (2, 3, 3, 3), + #'opacity': 0.5, + }, +} + +# Create sensor +sensor = magpy.Sensor(style_label="sensor") + +# Disable default model +sensor.style.model3d.showdefault = False + +# Apply custom model +sensor.style.model3d.add_trace(trace_mesh3d) + +# Show the system using different backends +for backend in magpy.SUPPORTED_PLOTTING_BACKENDS: + print(f"Plotting backend: {backend!r}") + magpy.show(sensor, backend=backend) +``` + +As noted above, it is possible to have multiple user-defined traces that will be displayed at the same time. The following example continuation demonstrates this by adding two more traces using the `Scatter3d` constructor in the generic backend. In addition, it showns how to copy and manipulate `Trace3d` objects. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +import copy +import numpy as np + +# Generate trace and add to sensor +ts = np.linspace(0, 2 * np.pi, 30) +trace_scatter3d = { + "backend": "generic", + "constructor": "Scatter3d", + "kwargs": { + "x": 1.2*np.cos(ts), + "y": 1.2*np.sin(ts), + "z": np.zeros(30), + "mode": "lines", + }, +} +sensor.style.model3d.add_trace(trace_scatter3d) + +# Generate new trace from Trace3d object +trace2 = copy.deepcopy(sensor.style.model3d.data[1]) +trace2.kwargs["x"] = np.zeros(30) +trace2.kwargs["z"] = 1.2*np.cos(ts) + +sensor.style.model3d.add_trace(trace2) + +# Show +magpy.show(sensor) +``` + +### Matplotlib constructors + +The following examples show how to construct traces with `plot`, `plot_surface` and `plot_trisurf`: + +```{code-cell} ipython3 +import matplotlib.pyplot as plt +import matplotlib.tri as mtri +import numpy as np +import magpylib as magpy + +# plot trace ########################### + +ts = np.linspace(-10, 10, 100) +xs = np.cos(ts) / 100 +ys = np.sin(ts) / 100 +zs = ts / 20 / 100 + +trace_plot = { + "backend": "matplotlib", + "constructor": "plot", + "args": (xs, ys, zs), + "kwargs": {"ls": "--", "lw": 2}, +} +magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.005, 0.01)) +magnet.style.model3d.add_trace(trace_plot) + +# plot_surface trace ################### + +u, v = np.mgrid[0 : 2 * np.pi : 30j, 0 : np.pi : 20j] +xs = np.cos(u) * np.sin(v) / 100 +ys = np.sin(u) * np.sin(v) / 100 +zs = np.cos(v) / 100 + +trace_surf = { + "backend": "matplotlib", + "constructor": "plot_surface", + "args": (xs, ys, zs), + "kwargs": {"cmap": plt.cm.YlGnBu_r}, +} +ball = magpy.Collection(position=(-0.03, 0, 0)) +ball.style.model3d.add_trace(trace_surf) + +# plot_trisurf trace ################### + +u, v = np.mgrid[0 : 2 * np.pi : 50j, -0.5:0.5:10j] +u, v = u.flatten(), v.flatten() + +xs = (1 + 0.5 * v * np.cos(u / 2.0)) * np.cos(u) / 100 +ys = (1 + 0.5 * v * np.cos(u / 2.0)) * np.sin(u) / 100 +zs = 0.5 * v * np.sin(u / 2.0) / 100 + +tri = mtri.Triangulation(u, v) + +trace_trisurf = { + "backend": "matplotlib", + "constructor": "plot_trisurf", + "args": (xs, ys, zs), + "kwargs": { + "triangles": tri.triangles, + "cmap": plt.cm.coolwarm, + }, +} +mobius = magpy.misc.CustomSource(style_model3d_showdefault=False, position=(0.03, 0, 0)) +mobius.style.model3d.add_trace(trace_trisurf) + +magpy.show(magnet, ball, mobius, backend="matplotlib") +``` + +## Pre-defined 3D models + +Automatic trace generators are provided for several basic 3D models in `magpylib.graphics.model3d`. They can be used as follows, + +```{code-cell} ipython3 +from magpylib import Collection +from magpylib.graphics import model3d + +# Prism trace ################################### +trace_prism = model3d.make_Prism( + base=6, + diameter=2, + height=1, + position=(-3, 0, 0), +) +obj0 = Collection(style_label="Prism") +obj0.style.model3d.add_trace(trace_prism) + +# Pyramid trace ################################# +trace_pyramid = model3d.make_Pyramid( + base=30, + diameter=2, + height=1, + position=(3, 0, 0), +) +obj1 = Collection(style_label="Pyramid") +obj1.style.model3d.add_trace(trace_pyramid) + +# Cuboid trace ################################## +trace_cuboid = model3d.make_Cuboid( + dimension=(2, 2, 2), + position=(0, 3, 0), +) +obj2 = Collection(style_label="Cuboid") +obj2.style.model3d.add_trace(trace_cuboid) + +# Cylinder segment trace ######################## +trace_cylinder_segment = model3d.make_CylinderSegment( + dimension=(1, 2, 1, 140, 220), + position=(1, 0, -3), +) +obj3 = Collection(style_label="Cylinder Segment") +obj3.style.model3d.add_trace(trace_cylinder_segment) + +# Ellipsoid trace ############################### +trace_ellipsoid = model3d.make_Ellipsoid( + dimension=(2, 2, 2), + position=(0, 0, 3), +) +obj4 = Collection(style_label="Ellipsoid") +obj4.style.model3d.add_trace(trace_ellipsoid) + +# Arrow trace ################################### +trace_arrow = model3d.make_Arrow( + base=30, + diameter=0.6, + height=2, + position=(0, -3, 0), +) +obj5 = Collection(style_label="Arrow") +obj5.style.model3d.add_trace(trace_arrow) + +obj0.show(obj1, obj2, obj3, obj4, obj5, backend="plotly") +``` + +((guide-docs-style-cad))= +## Adding a CAD model + +The following code sample shows how a standard CAD model (*.stl file) can be transformed into a Magpylib `Trace3d` object. + +```{note} +The code below requires installation of the `numpy-stl` package. +``` + +```{code-cell} ipython3 +import os +import tempfile + +import numpy as np +import requests +from matplotlib.colors import to_hex +from stl import mesh # requires installation of numpy-stl + +import magpylib as magpy + + +def bin_color_to_hex(x): + """transform binary rgb into hex color""" + sb = f"{x:015b}"[::-1] + r = int(sb[:5], base=2) / 31 + g = int(sb[5:10], base=2) / 31 + b = int(sb[10:15], base=2) / 31 + return to_hex((r, g, b)) + + +def trace_from_stl(stl_file): + """ + Generates a Magpylib 3D model trace dictionary from an *.stl file. + backend: 'matplotlib' or 'plotly' + """ + # Load stl file + stl_mesh = mesh.Mesh.from_file(stl_file) + + # Extract vertices and triangulation + p, q, r = stl_mesh.vectors.shape + vertices, ixr = np.unique( + stl_mesh.vectors.reshape(p * q, r), return_inverse=True, axis=0 + ) + i = np.take(ixr, [3 * k for k in range(p)]) + j = np.take(ixr, [3 * k + 1 for k in range(p)]) + k = np.take(ixr, [3 * k + 2 for k in range(p)]) + x, y, z = vertices.T + + # Create a generic backend trace + colors = stl_mesh.attr.flatten() + facecolor = np.array([bin_color_to_hex(c) for c in colors]).T + x, y, z = x / 1000, y / 1000, z / 1000 # mm->m + trace = { + "backend": "generic", + "constructor": "mesh3d", + "kwargs": dict(x=x, y=y, z=z, i=i, j=j, k=k, facecolor=facecolor), + } + return trace + + +# Load stl file from online resource +url = "https://raw.githubusercontent.com/magpylib/magpylib-files/main/PG-SSO-3-2.stl" +file = url.split("/")[-1] +with tempfile.TemporaryDirectory() as temp: + fn = os.path.join(temp, file) + with open(fn, "wb") as f: + response = requests.get(url) + f.write(response.content) + + # Create traces for both backends + trace = trace_from_stl(fn) + +# Create sensor and add CAD model +sensor = magpy.Sensor(style_label="PG-SSO-3 package") +sensor.style.model3d.add_trace(trace) + +# Create magnet and sensor path +magnet = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(0.015, 0.02)) +sensor.position = np.linspace((-0.015, 0, 0.008), (-0.015, 0, -0.004), 21) +sensor.rotate_from_angax(np.linspace(0, 180, 21), "z", anchor=0, start=0) + +# Display with matplotlib and plotly backends +args = (sensor, magnet) +kwargs = dict(style_path_frames=5) +magpy.show(args, **kwargs, backend="matplotlib") +magpy.show(args, **kwargs, backend="plotly") +``` + +```{code-cell} ipython3 + +``` \ No newline at end of file diff --git a/docs/_pages/user_guide/docs/docs_units_types.md b/docs/_pages/user_guide/docs/docs_units_types.md new file mode 100644 index 000000000..9d12b32e1 --- /dev/null +++ b/docs/_pages/user_guide/docs/docs_units_types.md @@ -0,0 +1,50 @@ +# Units and Types + +(guide-docs-units)= +## Units + +The important vacuum permeability $\mu_0$ is provided at the package top-level <span style="color: orange">**mu_0**</span>. It's value is not $4 \pi 10^{-7}$ since [the redefinition of the SI base units](https://en.wikipedia.org/wiki/2019_redefinition_of_the_SI_base_units), but a value close to it. + +For historical reasons Magpylib used non-SI units until Version 4. Starting with version 5 all inputs and outputs are SI-based. + +::::{grid} 3 +:::{grid-item} +:columns: 1 +::: + +:::{grid-item} +:columns: 10 +| PHYSICAL QUANTITY | MAGPYLIB PARAMETER | UNITS from v5| UNITS until v4| +|:---:|:---:|:---:|:---:| +| Magnetic Polarization $\vec{J}$ | `polarization`, `getJ()` | **T** | - | +| Magnetization $\vec{M}$ | `magnetization`, `getM()` | **A/m** | mT | +| Electric Current $i_0$ | `current` | **A** | A | +| Magnetic Dipole Moment $\vec{m}$ | `moment` | **Aยทmยฒ** | mTยทmmยณ | +| B-field $\vec{B}$ | `getB()` | **T** | mT | +| H-field $\vec{H}$ | `getH()` | **A/m** | kA/m | +| Length-inputs | `position`, `dimension`, `vertices`, ... | **m** | mm | +| Angle-inputs | `angle`, `dimension`, ... | **ยฐ** | ยฐ | +::: + +:::: + +```{warning} +Up to version 4, Magpylib was unfortunately contributing to the naming confusion in magnetism that is explained well [here](https://www.e-magnetica.pl/doku.php/confusion_between_b_and_h). The input `magnetization` in Magpylib < v5 was referring to the magnetic polarization (and not the magnetization), the difference being only in the physical unit. From version 5 onwards this is fixed. +``` + +```{note} +The connection between the magnetic polarization J, the magnetization M and the material parameters of a real permanent magnet are shown in {ref}`examples-tutorial-modeling-magnets`. +``` + +(guide-docs-io-scale-invariance)= +## Arbitrary unit Convention + +```{hint} +All input and output units in Magpylib (version 5 and higher) are SI-based, see table above. However, for advanced use one should be aware that the analytical solutions are **scale invariant** - _"a magnet with 1 mm sides creates the same field at 1 mm distance as a magnet with 1 m sides at 1 m distance"_. The choice of length input unit is therefore not relevant, but it is critical to keep the same length unit for all inputs in one computation. + +In addition, `getB` returns the same unit as given by the `polarization` input. With polarization input in mT, `getB` will return mT as well. At the same time when the `magnetization` input is kA/m, then `getH` returns kA/m as well. The B/H-field outputs are related to a M/J-inputs via a factor of $ยต_0$. +``` + +## Types + +Magpylib requires no special input format. All scalar types (`int`, `float`, ...) and vector types (`list`, `tuple`, `np.ndarray`, ... ) are accepted. Magpylib returns everything as `np.ndarray`. \ No newline at end of file diff --git a/docs/_pages/gallery/gallery_app_helmholtz.md b/docs/_pages/user_guide/examples/examples_app_coils.md similarity index 81% rename from docs/_pages/gallery/gallery_app_helmholtz.md rename to docs/_pages/user_guide/examples/examples_app_coils.md index c38fb8e26..aebd64078 100644 --- a/docs/_pages/gallery/gallery_app_helmholtz.md +++ b/docs/_pages/user_guide/examples/examples_app_coils.md @@ -12,7 +12,7 @@ kernelspec: name: python3 --- -(gallery-app-helmholtz)= +(examples-app-helmholtz)= # Coils @@ -57,7 +57,9 @@ coil2.show() **Model 3:** A [Helmholtz coil](https://en.wikipedia.org/wiki/Helmholtz_coil) is a device for producing a region of nearly uniform magnetic field. It consists of two coils on the same axis, carrying an equal electric current in the same direction. In classical layouts, the distance between the coils is similar to the coil radius. ```{code-cell} ipython3 -# create a finite sized Helmholtz coil-pair +# Continuation from above - ensure previous code is executed + +# Create a finite sized Helmholtz coil-pair coil1 = magpy.Collection() for z in np.linspace(-1, 1, 5): for r in np.linspace(4, 5, 5): @@ -81,35 +83,34 @@ helmholtz.show() Streamplot from Matplotlib is a powerful tool to outline the field lines. However, it must be understood that streamplot shows only a projection of the field onto the observation plane. All field components that point out of the plane become invisible. In out example we choose symmetry planes, where the perpendicular component is negligible. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import matplotlib.pyplot as plt fig, ax = plt.subplots(1, 1, figsize=(6,5)) -# create grid -ts = np.linspace(-13, 13, 20) -grid = np.array([[(x,0,z) for x in ts] for z in ts]) +# Compute field and plot the coil pair field on yz-grid +grid = np.mgrid[0:0:1j, -13:13:20j, -13:13:20j].T[:,:,0] +_, Y, Z = np.moveaxis(grid, 2, 0) -# compute and plot field of Helmholtz B = magpy.getB(helmholtz, grid) +_, By, Bz = np.moveaxis(B, 2, 0) + Bamp = np.linalg.norm(B, axis=2) Bamp /= np.amax(Bamp) -sp = ax.streamplot( - grid[:,:,0], grid[:,:,2], B[:,:,0], B[:,:,2], - density=2, - color=Bamp, - linewidth=np.sqrt(Bamp)*3, - cmap='coolwarm', +sp = ax.streamplot(Y, Z, By, Bz, density=2, color=Bamp, + linewidth=np.sqrt(Bamp)*3, cmap='coolwarm', ) -# plot coil outline +# Plot coil outline from matplotlib.patches import Rectangle for loc in [(4,4), (4,-6), (-6,4), (-6,-6)]: ax.add_patch(Rectangle(loc, 2, 2, color='k', zorder=10)) -# figure styling +# Figure styling ax.set( title='Magnetic field of Helmholtz', - xlabel='x-position (m)', + xlabel='y-position (m)', ylabel='z-position (m)', aspect=1, ) @@ -124,27 +125,31 @@ plt.show() While the optimal solution is given by two current loops, real world applications must deal with finite sizes and limited construction space. Here Magpylib enables fast analysis of different possible geometries. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +import matplotlib.pyplot as plt fig, ax = plt.subplots(1, 1, figsize=(6,5)) -# compute field of the coil pair on grid -ts = np.linspace(-3, 3, 20) -grid = np.array([[(x,0,z) for x in ts] for z in ts]) +# Compute field of the coil pair on yz-grid +grid = np.mgrid[0:0:1j, -3:3:20j, -3:3:20j].T[:,:,0] +_, Y, Z = np.moveaxis(grid, 2, 0) + B = helmholtz.getB(grid) -# field at center +# Field at center B0 = helmholtz.getB((0,0,0)) B0amp = np.linalg.norm(B0) -# homogeneity error +# Homogeneity error err = np.linalg.norm((B-B0)/B0amp, axis=2) -# plot error on grid -sp = ax.contourf(grid[:,:,0], grid[:,:,2], err*100) +# Plot error on grid +sp = ax.contourf(Y, Z, err*100) -# figure styling +# Figure styling ax.set( title='Helmholtz homogeneity error', - xlabel='x-position (m)', + xlabel='y-position (m)', ylabel='z-position (m)', aspect=1, ) diff --git a/docs/_pages/gallery/gallery_app_end_of_shaft.md b/docs/_pages/user_guide/examples/examples_app_end_of_shaft.md similarity index 75% rename from docs/_pages/gallery/gallery_app_end_of_shaft.md rename to docs/_pages/user_guide/examples/examples_app_end_of_shaft.md index 310ed8424..f0fa70179 100644 --- a/docs/_pages/gallery/gallery_app_end_of_shaft.md +++ b/docs/_pages/user_guide/examples/examples_app_end_of_shaft.md @@ -12,11 +12,11 @@ kernelspec: orphan: true --- -(gallery-app-end-of-shaft)= +(examples-app-end-of-shaft)= # Magnetic Angle Sensor -End of shaft angle sensing is a classical example for a magnetic position system. The goal is to determine the angular position of a rotating shaft. A magnet, typically a diametrically magnetized cylinder, is mounted at the end of the shaft. A 2D sensor is mounted below. When the shaft rotates the two sensor outputs will be $s_1=B_0 sin(\varphi)$ and $s_2=B_0 cos(\varphi)$, so that the angle is uniquely given by $\varphi = arctan_2(s_1,s_2)$. +End of shaft angle sensing is a classic example for a magnetic position system. The goal is to determine the angular position of a rotating shaft. A magnet, typically a diametrically magnetized cylinder, is mounted at the end of the shaft. A 2D sensor is mounted below. When the shaft rotates the two sensor outputs will be $s_1=B_0 sin(\varphi)$ and $s_2=B_0 cos(\varphi)$, so that the angle is uniquely given by $\varphi = arctan_2(s_1,s_2)$. In the example below we show such a typical end-of-shaft system with a 2-pixel sensor, that is commonly used to eliminate external stray fields. In addition, we assume that the magnet is not perfectly mounted at the end of the shaft, but slightly displaced to the side, which results in a wobble motion. Such tolerances are easily implemented with Magpylib, they can be visualized and their influence on the sensor output signal can be tested quickly. @@ -26,7 +26,7 @@ import plotly.express as px import magpylib as magpy import plotly.graph_objects as go -# create magnet +# Create magnet magnet = magpy.magnet.Cylinder( polarization=(1, 0, 0), dimension=(.06, .02), @@ -35,7 +35,7 @@ magnet = magpy.magnet.Cylinder( style_color=".7", ) -# create shaft dummy with 3D model +# Create shaft dummy with 3D model shaft = magpy.misc.CustomSource( position=(0, 0, .07), style_color=".7", @@ -50,14 +50,14 @@ shaft_trace = magpy.graphics.model3d.make_Prism( ) shaft.style.model3d.add_trace(shaft_trace) -# shaft rotation / magnet wobble motion +# Shaft rotation / magnet wobble motion displacement = .01 angles = np.linspace(0, 360, 72) coll = magnet + shaft magnet.move((displacement, 0, 0)) coll.rotate_from_angax(angles, "z", anchor=0, start=0) -# create sensor +# Create sensor gap = .03 sens = magpy.Sensor( position=(0, 0, -gap), @@ -66,13 +66,13 @@ sens = magpy.Sensor( style_size=1.5, ) -# show 3D animation of wobble motion +# Show 3D animation of wobble motion fig1 = go.Figure() magpy.show(magnet, sens, shaft, animation=True, backend="plotly", canvas=fig1) fig1.update_layout(scene_camera_eye_z=-1.1) fig1.show() -# show sensor output in plotly +# Show sensor output in plotly fig2 = go.Figure() df = sens.getB(magnet, output="dataframe") df["angle (deg)"] = angles[df["path"]] diff --git a/docs/_pages/user_guide/examples/examples_app_halbach.md b/docs/_pages/user_guide/examples/examples_app_halbach.md new file mode 100644 index 000000000..90e4f1096 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_app_halbach.md @@ -0,0 +1,85 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-app-halbach)= + +# Halbach Magnets + +Magpylib is an excellent tool to create magnet assemblies. In this example we will show how to model Halbach magnets. + +```{note} +In the following examples we make use of the [arbitrary unit convention](guide-docs-io-scale-invariance). +``` + +The original Halbach-magnetization describes a hollow cylinder with a polarization direction that rotates twice while going around the cylinder once. In reality such polarizations are difficult to fabricate. What is commonly done instead are "Discreete Halbach Arrays", which are magnet assemblies that approximate a Halbach magnetization. + +The following code creates a Discreete Halbach Cylinder generated from Cuboids: + +```{code-cell} ipython3 +import numpy as np +import magpylib as magpy + +N = 10 +angles = np.linspace(0, 360, N, endpoint=False) + +halbach = magpy.Collection() + +for a in angles: + cube = magpy.magnet.Cuboid( + dimension=(1,1,1), + polarization=(1,0,0), + position=(2.3,0,0) + ) + cube.rotate_from_angax(a, 'z', anchor=0) + cube.rotate_from_angax(a, 'z') + halbach.add(cube) + +halbach.show(backend='plotly') +``` + +Next we compute and display the field on an xy-grid in the symmetry plane using the [matplotlib streamplot](examples-vis-mpl-streamplot) example. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +import matplotlib.pyplot as plt +fig, ax = plt.subplots() + +# Compute and plot field on x-y grid +grid = np.mgrid[-3.5:3.5:100j, -3.5:3.5:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) + +B = halbach.getB(grid) +Bx, By, _ = np.moveaxis(B, 2, 0) +Bamp = np.linalg.norm(B, axis=2) + +pc = ax.contourf(X, Y, Bamp, levels=50, cmap="coolwarm") +ax.streamplot(X, Y, Bx, By, color="k", density=1.5, linewidth=1) + +# Add colorbar +fig.colorbar(pc, ax=ax, label="|B|") + +# Figure styling +ax.set( + xlabel="x-position", + ylabel="z-position", + aspect=1, +) + +plt.show() +``` + +```{warning} +Magpylib models magnets with perfect polarization. However, such magnets do not exist in reality due to fabrication tolerances and material response. While fabrication tolerances can be estimated easily, our [tutorial](examples-tutorial-modeling-magnets) explains how to deal with material response. +``` diff --git a/docs/_pages/gallery/gallery_app_scales.md b/docs/_pages/user_guide/examples/examples_app_scales.md similarity index 75% rename from docs/_pages/gallery/gallery_app_scales.md rename to docs/_pages/user_guide/examples/examples_app_scales.md index e8d9d90ac..d07cd28cf 100644 --- a/docs/_pages/gallery/gallery_app_scales.md +++ b/docs/_pages/user_guide/examples/examples_app_scales.md @@ -12,12 +12,12 @@ kernelspec: name: python3 --- -(gallery-app-scales)= +(examples-app-scales)= # Magnetic Scales In this example we will show how magnetic scales can be constructed with Magpylib for fast field computation. -- model -- experiment -- refer to DS 91411 and 91479 +- reference to DS 91411 and 91479 +- ideal-typical scales +- PWS experiment and PWS reference diff --git a/docs/_pages/user_guide/examples/examples_force_floating.md b/docs/_pages/user_guide/examples/examples_force_floating.md new file mode 100644 index 000000000..25c28a323 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_force_floating.md @@ -0,0 +1,239 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-force-floating)= + +# Floating Magnets + +The examples here require installaion of the [magpylib-force package](https://pypi.org/project/magpylib-force/). See also the [magpylib-force documentation](docs-magpylib-force). + +## Formalism + +With force and torque we can compute how a magnet moves in a magnetic field by solving the equations of motion, + +$$ \vec{F} = \dot{\vec{p}} \ \text{and} \ \vec{T} = \dot{\vec{L}}$$ + +with force $\vec{F}$, momentum $\vec{p}$, torque $\vec{T}$ and angular momentum $\vec{L}$. + +We implement a first order semi-implicit Euler method that is used to compute [planetary motion](https://www.mgaillard.fr/2021/07/11/euler-integration.html). The algorithm splits the computation into small subsequent time-steps $\Delta t$, resulting in the following equations for the position $\vec{s}$, the velocity $\vec{v} = \dot{\vec{s}}$, the rotation angle $\vec{\varphi}$ and the angular velocity $\vec{\omega}$, + +$$\vec{v}(t+\Delta t) = \vec{v}(t) + \frac{\Delta t}{m} \vec{F}(t)$$ + +$$\vec{s}(t+\Delta t) = \vec{s}(t) + \Delta t \cdot \vec{v} (t + \Delta t)$$ + +$$\vec{\omega} (t + \Delta t) = \vec{ฯ}(t) + \Delta t \cdot J^{-1} \cdot \vec{T}(t)$$ + +$$\vec{\varphi} (t + \Delta t) = \vec{\varphi}(t) + \Delta t \cdot \vec{\omega} (t + \Delta t) $$ + +## Magnet and Coil + +In the following example we show an implementation of the proposed Euler scheme. A cubical magnet is accelerated by a current loop along the z-axis as show in the following sketch: + +```{figure} ../../../_static/images/examples_force_floating_coil-magnet.png +:width: 40% +:align: center +:alt: Sketch of current loop and magnet. + +A cubical magnet is accelerated by a current loop. +``` + +Due to the symmetry of the problem there is no torque so we solve only the translation part of the equations of motion. + +In the beginning, the magnet is at rest and slightly displaced in z-direction from the center of the current loop. With time the magnet is accelerated and it's z-position is displayed in the figure below. + +```{code-cell} ipython3 +import numpy as np +import matplotlib.pyplot as plt +import magpylib as magpy +from magpylib_force import getFT +from scipy.spatial.transform import Rotation as R + +def timestep(source, target, dt): + """ + Apply translation-only Euler-sceme timestep to target. + + Parameters: + ----------- + source: Magpylib source object that generates the magnetic field + + target: Magpylib target object viable for force computation. In addition, + the target object must have the following parameters describing + the motion state: v (velocity), m (mass) + + dt: Euler scheme length of timestep + """ + + # compute force + F, _ = getFT(source, target) + + # compute/set new velocity and position + target.v = target.v + dt/target.m * F + target.position = target.position + dt * target.v + +# Current loop that generates the field +loop = magpy.current.Circle(diameter=10e-3, current=10) + +# Magnets which are accelerated in the loop-field +cube1 = magpy.magnet.Cuboid(dimension=(5e-3,5e-3,5e-3), polarization=(0,0,1)) +cube1.meshing=(3,3,3) +cube2 = cube1.copy(polarization=(0,0,-1)) + +# Compute motion +for cube, lab in zip([cube1, cube2], ["attractive", "repulsive"]): + + # Set initial conditions + cube.m = 1e-3 + cube.position=(0,0,3e-3) + cube.v = np.array([0,0,0]) + + # Compute timesteps + z = [] + for _ in range(100): + z.append(cube.position[2]*1000) + timestep(loop, cube, dt=1e-3) + + plt.plot(z, marker='.', label=lab) + +# Graphic styling +plt.gca().legend() +plt.gca().grid() +plt.gca().set( + title="Magnet motion", + xlabel="timestep ()", + ylabel="z-Position (mm)", +) +plt.show() +``` + +The simulation is made with two magnets with opposing polarizations. In the "repulsive" case (orange) the magnetic moment of magnet and coil are anti-parallel and the magnet is simply pushed away from the coil in positive z-direction. In the "attractive" case (blue) the moments are parallel to each other, and the magnet is accelerated towards the coil center. Due to inertia it then comes out on the other side, and is again attracted towards the center resulting in an oscillation. + +```{warning} +This algorithm accumulates its error over time, which can be avoided by choosing smaller timesteps. +``` + +## Two-body problem + +In the following example we demonstrate a fully dynamic simulation with two magnetic bodies that rotate around each other, attracted towards each other by the magnetic force, and repelled by the centrifugal force. + +```{figure} ../../../_static/images/examples_force_floating_ringdown.png +:width: 80% +:align: center +:alt: Sketch of two-magnet ringdown. + +Two freely moving magnets rotate around each other. +``` + +Contrary to the simple case above, we apply the Euler scheme also to the rotation degrees of freedom, as the magnets will change their orientation while they circle around each other. + +```{code-cell} ipython3 +import numpy as np +import matplotlib.pyplot as plt +import magpylib as magpy +from magpylib_force import getFT +from scipy.spatial.transform import Rotation as R + +def timestep(source, target, dt): + """ + Apply full Euler-sceme timestep to target. + + Parameters: + ----------- + source: Magpylib source object that generates the magnetic field + + target: Magpylib target object viable for force computation. In addition, + the target object must have the following parameters describing + the motion state: v (velocity), m (mass), w (angular velocity), + I_inv (inverse inertial tensor) + + dt: Euler scheme length of timestep + """ + # compute force + F, T = getFT(source, target) + + # compute/set new velocity and position + target.v = target.v + dt/target.m * F + target.position = target.position + dt * target.v + + # compute/set new angular velocity and rotation angle + target.w = target.w + dt*target.orientation.apply(np.dot(target.I_inv, target.orientation.inv().apply(T))) + target.orientation = R.from_rotvec(dt*target.w)*target.orientation + + +v0 = 5.18 # init velocity +steps=505 # number of timesteps +dt = 1e-2 # timstep size + +# Create the two magnets and set initial conditions +sphere1 = magpy.magnet.Sphere(position=(5,0,0), diameter=1, polarization=(1,0,0)) +sphere1.meshing = 5 +sphere1.m = 2 +sphere1.v = np.array([0, v0, 0]) +sphere1.w = np.array([0, 0, 0]) +sphere1.I_inv = 1 * np.eye(3) + +sphere2 = sphere1.copy(position=(-5,0,0)) +sphere2.v = np.array([0,-v0, 0]) + +# Solve equations of motion +data = np.zeros((4,steps,3)) +for i in range(steps): + timestep(sphere1, sphere2, dt) + timestep(sphere2, sphere1, dt) + + # Store results of each timestep + data[0,i] = sphere1.position + data[1,i] = sphere2.position + data[2,i] = sphere1.orientation.as_euler('xyz') + data[3,i] = sphere2.orientation.as_euler('xyz') + +# Plot results +fig, (ax1,ax2) = plt.subplots(2,1,figsize=(10,5)) + +for j,ls in enumerate(["-", "--"]): + + # Plot positions + for i,a in enumerate("xyz"): + ax1.plot(data[j,:,i], label= a + str(j+1), ls=ls) + + # Plot orientations + for i,a in enumerate(["phi", "psi", "theta"]): + ax2.plot(data[j+2,:,i], label= a + str(j+1), ls=ls) + +# Figure styling +for ax in fig.axes: + ax.legend(fontsize=9, loc=6, facecolor='.8') + ax.grid() +ax1.set( + title="Floating Magnet Ringdown", + ylabel="Positions (m)", +) +ax2.set( + ylabel="Orientations (rad)", + xlabel="timestep ()", +) +plt.tight_layout() +plt.show() +``` + +In the figure one can see, that the initial velocity is chosen so that the magnets approach each other in a ringdown-like behavior. The magnets are magnetically locked towards each other - both always show the same orientation. However, given no initial angular velocity, the rotation angle is oscillating several times while circling once. + +A video is helpful in this case to understand what is going on. From the computation above, we build the following gif making use of this [export-animation](examples-vis-exporting-animations) tutorial. + +```{figure} ../../../_static/videos/example_force_floating_ringdown.gif +:width: 60% +:align: center +:alt: animation of simulated magnet ringdown. + +Animation of above simulated magnet ringdown. +``` \ No newline at end of file diff --git a/docs/_pages/user_guide/examples/examples_force_force.md b/docs/_pages/user_guide/examples/examples_force_force.md new file mode 100644 index 000000000..342ab1338 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_force_force.md @@ -0,0 +1,60 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-force-force)= + +# Magnetic Force and Torque + +The `magpylib-force` extension provides force and torque computation between Magpylib objects. A detailed description of the API and how the computation is achieved can be found in the [user guide](docs-force-computation). + +```{warning} +[Scaling invariance](guide-docs-io-scale-invariance) does not hold for force computations! Be careful to provide the inputs in the correct units! +``` + +In the following example we show how to compute force and torque between two objects and how to represent it graphically. + +```{code-cell} ipython3 +import pyvista as pv +import magpylib as magpy +from magpylib_force import getFT + +# Source +coil = magpy.current.Circle(position=(0,0,-.5), diameter=4, current=1000) +coil.rotate_from_angax(angle=-20, axis='y') + +# Target +cube = magpy.magnet.Cuboid(dimension=(.7,.7,.7), polarization=(0,0,1)) +cube.meshing = (10,10,10) + +# Compute force and torque +F,T = getFT(coil, cube, anchor=None) + +print(f"Force (blue): {[round(f) for f in F]} N") +print(f"Torque (yellow): {[round(t) for t in T]} Nm") +``` + +Force and torque are really strong in this example, because the magnet and the coil are very large objects. With 0.7 m side length, the magnet has a Volume of ~1/3rd cubic meter :). + +```{code-cell} ipython3 +# Example continued from above + +# Plot force and torque in Pyvista with arrows +pl = magpy.show(coil, cube, backend='pyvista', return_fig=True) +arrowF = pv.Arrow(start=(0,0,0), direction=F) +pl.add_mesh(arrowF, color="blue") +arrowT = pv.Arrow(start=(0,0,0), direction=T) +pl.add_mesh(arrowT, color="yellow") + +pl.show() +``` \ No newline at end of file diff --git a/docs/_pages/user_guide/examples/examples_force_holding_force.md b/docs/_pages/user_guide/examples/examples_force_holding_force.md new file mode 100644 index 000000000..d755e1d07 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_force_holding_force.md @@ -0,0 +1,40 @@ +(examples-force-holding-force)= + +# Magnetic Holding Force + +The examples here require installaion of the [magpylib-force package](https://pypi.org/project/magpylib-force/). See also the [magpylib-force documentation](docs-magpylib-force). + +With Magpylib-force it is possible to compute the holding force of a magnet attached magnetically to a soft-ferromagnetic plate. The "pull-force" is the opposing force that is required to detach the magnet from the surface. + +```{figure} ../../../_static/images/examples_force_holding_force.png +:width: 40% +:align: center +:alt: Sketch of holding force. + +Sketch of holding force F that must be overcome to detach the magnet from a soft-magnetic plate. +``` + +For this we make use of the "magnetic mirror" effect, which is quite similar to the well-known electrostatic "mirror-charge" model. The magnetic field of a magnetic dipole moment that lies in front of a highly permeable surface is similar to the field of two dipole moments: the original one and one that is mirrored across the surface such that each "magnetic charge" that makes up the dipole moment is mirrored in both position and charge. + +The following example computes the holding force of a Cuboid magnet using the magnetic mirror effect. + +```{code-block} python +import magpylib as magpy +from magpylib_force import getFT + +# Target magnet +m1 = magpy.magnet.Cuboid( + dimension=(5e-3, 2.5e-3, 1e-3), + polarization=(0, 0, 1.33), +) +m1.meshing = 100 + +# Mirror magnet +m2 = m1.copy(position=(0,0,1e-3)) + +F,T = getFT(m2, m1) +print(f"Holding Force: {round(F[2]*100)} g") +# Holding Force: 349 g +``` + +Magnet dimensions and material from this example are taken from the [web](https://www.supermagnete.at/quadermagnete-neodym/quadermagnet-5mm-2.5mm-1.5mm_Q-05-2.5-1.5-HN). The remanence of N45 material lies within 1.32 and 1.36 T which corresponds to the polarization, see also the ["Modeling a real magnet"](examples-tutorial-modeling-magnets) tutorial. The computation confirms what is stated on the web-page, that the holding force of this magnet is about 350 g. diff --git a/docs/_pages/user_guide/examples/examples_index.md b/docs/_pages/user_guide/examples/examples_index.md new file mode 100644 index 000000000..7c4135d06 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_index.md @@ -0,0 +1,288 @@ +(examples)= +# Examples + +## Tutorials + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-tutorial-paths` +:text-align: center +:link: examples-tutorial-paths +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_tutorial_paths.png +::: + +:::{grid-item-card} {ref}`examples-tutorial-field-computation` +:text-align: center +:link: examples-tutorial-field-computation +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_tutorial_field_computation.png +::: + +:::{grid-item-card} {ref}`examples-tutorial-collection` +:text-align: center +:link: examples-tutorial-collection +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_tutorial_collection.png +::: + +:::{grid-item-card} {ref}`examples-tutorial-custom` +:text-align: center +:link: examples-tutorial-custom +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_tutorial_custom.png +::: + +:::{grid-item-card} {ref}`examples-tutorial-modeling-magnets` +:text-align: center +:link: examples-tutorial-modeling-magnets +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_tutorial_modeling_magnets.png +::: + +:::: + + +## Visualizations + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-vis-magnet-colors` +:text-align: center +:link: examples-vis-magnet-colors +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_vis_magnet_colors.png +::: + +:::{grid-item-card} {ref}`examples-vis-animations` +:text-align: center +:link: examples-vis-animations +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_vis_animations.png +::: + +:::{grid-item-card} {ref}`examples-vis-subplots` +:text-align: center +:link: examples-vis-subplots +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_vis_subplots.png +::: + +:::{grid-item-card} {ref}`examples-vis-mpl-streamplot` +:text-align: center +:link: examples-vis-mpl-streamplot +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_vis_mpl_streamplot.png +::: + +:::{grid-item-card} {ref}`examples-vis-pv-streamlines` +:text-align: center +:link: examples-vis-pv-streamlines +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_vis_pv_streamlines.png +::: + + + +:::: + +(examples-complex-magnet-shapes)= +## Complex Magnet Shapes + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-shapes-superpos` +:text-align: center +:link: examples-shapes-superpos +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_shapes_superpos.png +::: + +:::{grid-item-card} {ref}`examples-shapes-convex-hull` +:text-align: center +:link: examples-shapes-convex-hull +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_shapes_convex_hull.png +::: + +:::{grid-item-card} {ref}`examples-shapes-triangle` +:text-align: center +:link: examples-shapes-triangle +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_shapes_triangle.png +::: + +:::{grid-item-card} {ref}`examples-shapes-pyvista` +:text-align: center +:link: examples-shapes-pyvista +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_shapes_pyvista.png +::: + +:::{grid-item-card} {ref}`examples-shapes-cad` +:text-align: center +:link: examples-shapes-cad +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_shapes_cad.png +::: + +:::: + + +## Miscellaneous + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-misc-compound` +:text-align: center +:link: examples-misc-compound +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_misc_compound.png +::: + +:::{grid-item-card} {ref}`examples-misc-field-interpolation` +:text-align: center +:link: examples-misc-field-interpolation +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_misc_field_interpolation.png +::: + +:::{grid-item-card} {ref}`examples-misc-inhom` +:text-align: center +:link: examples-misc-inhom +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_misc_inhom.png +::: + +:::: + + +## Applications + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-app-end-of-shaft` +:text-align: center +:link: examples-app-end-of-shaft +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_app_end_of_shaft.png +::: + +:::{grid-item-card} {ref}`examples-app-halbach` +:text-align: center +:link: examples-app-halbach +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_app_halbach.png +::: + +:::{grid-item-card} {ref}`examples-app-helmholtz` +:text-align: center +:link: examples-app-helmholtz +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_app_helmholtz.png +::: + +:::{grid-item-card} {ref}`examples-app-scales` +:text-align: center +:link: examples-app-scales +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_WIP.png +::: + +:::: + +## Magpylib-Force + +::::{grid} 2 3 4 4 +:gutter: 4 + +:::{grid-item-card} {ref}`examples-force-force` +:text-align: center +:link: examples-force-force +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_force_force.png +::: + +:::{grid-item-card} {ref}`examples-force-holding-force` +:text-align: center +:link: examples-force-holding-force +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_force_holding_force.png +::: + +:::{grid-item-card} {ref}`examples-force-floating` +:text-align: center +:link: examples-force-floating +:link-type: ref +:link-alt: link to example +:img-bottom: ../../../_static/images/examples_icon_force_floating.png +::: + +:::: + + +```{toctree} +:maxdepth: 2 +:hidden: + +examples_tutorial_paths.md +examples_tutorial_field_computation.md +examples_tutorial_collection.md +examples_tutorial_custom.md +examples_tutorial_modeling_magnets.md + +examples_vis_magnet_colors.md +examples_vis_animations.md +examples_vis_subplots.md +examples_vis_mpl_streamplot.md +examples_vis_pv_streamlines.md + +examples_shapes_superpos.md +examples_shapes_convex_hull.md +examples_shapes_triangle.md +examples_shapes_pyvista.md +examples_shapes_cad.md + +examples_misc_compound.md +examples_misc_field_interpolation.md +examples_misc_inhom.md + +examples_app_end_of_shaft.md +examples_app_halbach.md +examples_app_coils.md +examples_app_scales.md + +examples_force_force.md +examples_force_holding_force.md +examples_force_floating.md +``` + diff --git a/docs/_pages/gallery/gallery_misc_compound.md b/docs/_pages/user_guide/examples/examples_misc_compound.md similarity index 94% rename from docs/_pages/gallery/gallery_misc_compound.md rename to docs/_pages/user_guide/examples/examples_misc_compound.md index 29753c789..6f2616d6f 100644 --- a/docs/_pages/gallery/gallery_misc_compound.md +++ b/docs/_pages/user_guide/examples/examples_misc_compound.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-misc-compound)= +(examples-misc-compound)= # Compounds @@ -20,7 +20,7 @@ The `Collection` class is a powerful tool for grouping and tracking object assem ## Subclassing collections -In the following example we design a compound class `MagnetRing` which represents a ring of cuboid magnets with the parameter `cubes` that should refer to the number of magnets on the ring. The ring will automatically adjust its size when `cubes` is modified. We also add an encompassing 3D model. +In the following example we design a compound class `MagnetRing` which represents a ring of cuboid magnets with the parameter `cubes` that should refer to the number of magnets on the ring. The ring will automatically adjust its size when `cubes` is modified, including an additionally added encompassing 3D model that may, for example, represent a mechanical magnet holder. ```{code-cell} ipython3 import magpylib as magpy @@ -90,6 +90,8 @@ class MagnetRing(magpy.Collection): This new `MagnetRing` class seamlessly integrates into Magpylib and makes use of the position and orientation interface, field computation and graphic display. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + # Add a sensor sensor = magpy.Sensor(position=(0, 0, 0)) @@ -109,6 +111,8 @@ magpy.show(ring, sensor, backend='plotly') The `MagnetRing` parameter `cubes` can be modified dynamically: ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + print(f"B-field at sensor for modified ring โ {ring.getB(sensor).round(3)}") ring.cubes = 10 @@ -190,6 +194,8 @@ class MagnetRingAdv(magpy.Collection): We have removed the trace construction from the `_update` method, and instead provided `_custom_trace3d` as a callable. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + ring0 = MagnetRing() %time for _ in range(10): ring0.cubes=10 diff --git a/docs/_pages/gallery/gallery_misc_field_interpolation.md b/docs/_pages/user_guide/examples/examples_misc_field_interpolation.md similarity index 93% rename from docs/_pages/gallery/gallery_misc_field_interpolation.md rename to docs/_pages/user_guide/examples/examples_misc_field_interpolation.md index bc486bfb5..63a779352 100644 --- a/docs/_pages/gallery/gallery_misc_field_interpolation.md +++ b/docs/_pages/user_guide/examples/examples_misc_field_interpolation.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-misc-field-interpolation)= +(examples-misc-field-interpolation)= # Field Interpolation @@ -118,9 +118,11 @@ magpy.show(custom, cube, zoom=1, backend="matplotlib") ## Testing Interpolation Accuracy -Finally, we compare the "exact" field of the cuboid source with the interpolated field of the custom source. For this purpose a sensor is added and a generic rotation is applied to the sources. Naturally there is some error that can be reduced by increasing the interpolation grid finesse. +Finally, we compare the "exact" field of the cuboid source with the interpolated field of the custom source. For this purpose, a sensor is added and a generic rotation is applied to the sources. Naturally there is some error that can be reduced by increasing the interpolation grid finesse. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import matplotlib.pyplot as plt # Modify orientation of cube and custom diff --git a/docs/_pages/user_guide/examples/examples_misc_inhom.md b/docs/_pages/user_guide/examples/examples_misc_inhom.md new file mode 100644 index 000000000..4f12f9669 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_misc_inhom.md @@ -0,0 +1,141 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-misc-inhom)= + +# Inhomogeneous Magnetization + +The analytical expressions implemented in Magpylib treat only simple homogeneous polarizations. When dealing with high-grade materials that are magnetized in homogeneous fields this is a good approximation. However, there are many cases where such a homogeneous model is not justified. The tutorial {ref}`examples-tutorial-modeling-magnets` and the user-guide {ref}`guide-physics-demag` provide some insights on this topic. + +Here we show how to deal with inhomogeneous polarization based on a commonly misunderstood example of a cylindrical quadrupol magnet. While graphical representations of such magnets usually depict only four poles, see {ref}`examples-vis-magnet-colors`, such magnets exhibit complex polarization given by the magnetization device that is used to magnetize them. + +The following code shows how the field of such a magnetization device would look like and what magnetization field it produces. To relize a Cylindrical Quadrupole magnert there are four coils with ferromagnetic cores involved, arranged in circle around the magnet. In the example, coils and cores are modeled by Cuboid magnets. + +```{note} +While Magpylib uses SI units by default, in this example we make use of [scaling invariance](guide-docs-io-scale-invariance) and consider arbitrary input length units. For this example millimeters are sensible. +``` + +```{code-cell} ipython +import numpy as np +import matplotlib.pyplot as plt +import magpylib as magpy + +# Create figure with 2D and 3D canvas +fig = plt.figure(figsize=(8, 4)) +ax1 = fig.add_subplot(121, projection='3d') +ax2 = fig.add_subplot(122) + +# Model of magnetization tool +tool1 = magpy.magnet.Cuboid( + dimension=(5, 3, 3), + polarization=(1, 0, 0), + position=(9, 0, 0) +).rotate_from_angax(50, 'z', anchor=0) +tool2 = tool1.copy(polarization=(-1,0,0)).rotate_from_angax(-100, 'z', 0) +tool3 = tool1.copy().rotate_from_angax(180, 'z', 0) +tool4 = tool2.copy().rotate_from_angax(180, 'z', 0) +tool = magpy.Collection(tool1, tool2, tool3, tool4) + +# Model of Quadrupole Cylinder +cyl = magpy.magnet.CylinderSegment( + dimension=(2, 5, 1, 0, 360), + polarization=(0, 0, 0), + style_magnetization_show=False, +) + +# Plot 3D model on ax1 +magpy.show(cyl, tool, canvas=ax1, style_legend_show=False, style_magnetization_mode="color") +ax1.view_init(90, -90) + +# Compute and plot tool-field on grid +grid = np.mgrid[-6:6:50j, -6:6:50j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) + +B = tool.getB(grid) +Bx, By, Bz = np.moveaxis(B, 2, 0) + +ax2.streamplot(X, Y, Bx, By, + color=np.linalg.norm(B, axis=2), + cmap='autumn', + density=1.5, + linewidth=1, +) + +# Outline magnet boundary +ts = np.linspace(0,2*np.pi,200) +ax2.plot(2*np.sin(ts), 2*np.cos(ts), color='k', lw=2) +ax2.plot(5*np.sin(ts), 5*np.cos(ts), color='k', lw=2) + +# Plot styling +ax2.set( + title="B-field in xy-plane", + xlabel="x-position", + ylabel="y-position", + aspect=1, +) + +plt.tight_layout() +plt.show() +``` + +It can be assumed that the polarization that is written into the unmagnetized Cylinder will, in a lowest order approximation, follow the magnetic field generated by the magnetization tool. To create a Cylinder magnet with such a polarization pattern we apply the [superposition principle](examples-shapes-superpos) and approximate the inhomogneous polarization as the sum of multiple small homogeneous cells using the `CylinderSegment` class. Splitting up the Cylinder into many cells is easily done by hand, but for practicality we make use of the [magpylib-material-response](https://pypi.org/project/magpylib-material-response/) package which provides an excellent function for this purpose. + +```{code-cell} ipython +# Continuation from above - ensure previous code is executed + +from magpylib_material_response.meshing import mesh_Cylinder + +# Create figure with 2D and 3D canvas +fig = plt.figure(figsize=(8, 4)) +ax1 = fig.add_subplot(121, projection='3d') +ax2 = fig.add_subplot(122) + +# Show Cylinder cells +mesh = mesh_Cylinder(cyl,30) +magpy.show(*mesh, canvas=ax1, style_magnetization_show=False) + +# Apply polarization +for m in mesh: + Btool = tool.getB(m.barycenter) + m.polarization = Btool/np.linalg.norm(Btool) + +# Compute and plot polarization +J = mesh.getJ(grid) +J[np.linalg.norm(J, axis=2) == 0] = np.nan # remove J=0 from plot +Jx, Jy, _ = np.moveaxis(J, 2, 0) + +Jangle = np.arctan2(Jx, Jy) + +ax2.contourf(X, Y, Jangle, cmap="rainbow", levels=30) +ax2.streamplot(X, Y, Jx, Jy, color='k') + +# Outline magnet boundary +ts = np.linspace(0,2*np.pi,200) +ax2.plot(2*np.sin(ts), 2*np.cos(ts), color='k', lw=2) +ax2.plot(5*np.sin(ts), 5*np.cos(ts), color='k', lw=2) + +# Plot styling +ax2.set( + title="Polarization J in xy-plane", + xlabel="x-position", + ylabel="y-position", + aspect=1, +) +plt.tight_layout() +plt.show() +``` + +The color on the right-hand-side corresponds to the angle of orientation of the material polarization. Increasing the mesh finesse will improve the approximation but slow down any field computation at the same time. + +**What is the purpose of this example ?** In addition to demonstrating how inhomogeneous polarizations can be modeled, this example should raise awareness that many magnets can look simple on a data sheet (color pattern) but may have an inhomogeneous, complex, unknown polarization distribution. It goes without saying that the magnetic field generated by such a magnet, for example in an angle sensing application, will depend strongly on the polarization. diff --git a/docs/_pages/user_guide/examples/examples_shapes_cad.md b/docs/_pages/user_guide/examples/examples_shapes_cad.md new file mode 100644 index 000000000..41615f458 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_shapes_cad.md @@ -0,0 +1,43 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.7 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-shapes-cad)= + +# Magnets from CAD + +The easiest way to create complex magnet shapes from CAD files is through Pyvista using the [TriangularMesh class](docu-magpylib-api-trimesh). Pyvista supports *.stl files, and any open CAD file format is easily transformed to stl. + +```{warning} +CAD files might include many Triangles, especially when dealing with round sides and edges, that do not significantly contribute to the field and will slow down the Magpylib computation. +``` + +```{code-cell} ipython3 +import pyvista as pv +from magpylib.magnet import TriangularMesh + +# Import *.stl file with Pyvista +mesh = pv.read("logo.stl") + +# Transform into Magpylib magnet +magnet = TriangularMesh.from_pyvista( + polydata=mesh, + polarization=(1,-1,0), + check_disconnected=False, +) +magnet.show(backend="plotly") +``` + +```{hint} +A quick way to work with cad files, especially transforming 2D *.svg to 3D *.stl, is provided by [Tinkercad](https://www.tinkercad.com). +``` diff --git a/docs/_pages/gallery/gallery_shapes_convex_hull.md b/docs/_pages/user_guide/examples/examples_shapes_convex_hull.md similarity index 83% rename from docs/_pages/gallery/gallery_shapes_convex_hull.md rename to docs/_pages/user_guide/examples/examples_shapes_convex_hull.md index e125fc3b5..69f493834 100644 --- a/docs/_pages/gallery/gallery_shapes_convex_hull.md +++ b/docs/_pages/user_guide/examples/examples_shapes_convex_hull.md @@ -12,13 +12,13 @@ kernelspec: orphan: true --- -(gallery-shapes-convex-hull)= +(examples-shapes-convex-hull)= # Convex Hull In geometry the convex hull of a point cloud is the smallest convex shape that contains all points, see [Wikipedia](https://en.wikipedia.org/wiki/Convex_hull). -Magpylib offers construction of convex hull magnets by combining the `magpylib.magnets.TriangularMesh` and the [scipy.spatial.ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) classes via the classmethod `from_ConvexHull`. Note, that the Scipy method does not guarantee correct face orientations if `reorient_faces` is disabled. +Magpylib offers construction of convex hull magnets by combining the `magpylib.magnets.TriangularMesh` and the [scipy.spatial.ConvexHull](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html) classes via the class method `from_ConvexHull`. Note, that the Scipy method does not guarantee correct face orientations if `reorient_faces` is disabled. ## Pyramid magnet @@ -26,7 +26,6 @@ This is the fastest way to construct a pyramid magnet. ```{code-cell} ipython3 import numpy as np - import magpylib as magpy # Create pyramid magnet diff --git a/docs/_pages/gallery/gallery_shapes_pyvista.md b/docs/_pages/user_guide/examples/examples_shapes_pyvista.md similarity index 94% rename from docs/_pages/gallery/gallery_shapes_pyvista.md rename to docs/_pages/user_guide/examples/examples_shapes_pyvista.md index 5c6657095..d4bf7763d 100644 --- a/docs/_pages/gallery/gallery_shapes_pyvista.md +++ b/docs/_pages/user_guide/examples/examples_shapes_pyvista.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-shapes-pyvista)= +(examples-shapes-pyvista)= # Pyvista Bodies @@ -88,13 +88,13 @@ The result cannot be used for magnetic field computation. Even if all faces were 1. giving Pyvista a finer mesh to work with from the start 2. Pyvista mesh cleaning (merge duplicate points, remove unused points, remove degenerate faces) -The following code produces a clean magnet . +The following code produces a clean magnet. ```{code-cell} ipython3 import pyvista as pv import magpylib as magpy -# Create a complex pyvista PolyData object using a boolean operation. Start with +# Create a complex Pyvista PolyData object using a boolean operation. Start with # finer mesh and clean after operation sphere = pv.Sphere(radius=0.6) cube = pv.Cube().triangulate().subdivide(2) @@ -116,11 +116,3 @@ print(f'mesh status reoriented: {magnet.status_reoriented}') magnet.show(backend="plotly") ``` - -```{code-cell} ipython3 - -``` - -```{code-cell} ipython3 - -``` diff --git a/docs/_pages/gallery/gallery_shapes_superpos.md b/docs/_pages/user_guide/examples/examples_shapes_superpos.md similarity index 75% rename from docs/_pages/gallery/gallery_shapes_superpos.md rename to docs/_pages/user_guide/examples/examples_shapes_superpos.md index 4e4d0b11e..287d88df4 100644 --- a/docs/_pages/gallery/gallery_shapes_superpos.md +++ b/docs/_pages/user_guide/examples/examples_shapes_superpos.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-shapes-superpos)= +(examples-shapes-superpos)= # Superposition @@ -24,13 +24,13 @@ When two magnets overlap geometrically, the magnetization in the overlap region :gutter: 4 ::::{grid-item-card} Union -:img-bottom: ../../_static/images/docu_field_superpos_union.png +:img-bottom: ../../../_static/images/docu_field_superpos_union.png :shadow: None Build complex forms by aligning base shapes (no overlap) with each other with similar magnetization vector. :::: ::::{grid-item-card} Cut-Out -:img-bottom: ../../_static/images/docu_field_superpos_cutout.png +:img-bottom: ../../../_static/images/docu_field_superpos_cutout.png :shadow: None When two objects with opposing magnetization vectors of similar amplitude overlap, they will just cancel in the overlap region. This enables geometric cut-out operations. :::: @@ -76,7 +76,7 @@ with magpy.show_context(magnet, sensor, backend="plotly", style_legend_show=Fals ## Cut-out operation -When two objects with opposing magnetization vectors of similar amplitude overlap, they will just cancel in the overlap region. This enables geometric cut-out operations. In the following example we construct an exact hollow cylinder solution from two concentric cylinder shapes with opposite magnetizations, and compare the result to the `CylinderSegment` class solution. +When two objects with opposing magnetization vectors of similar amplitude overlap, they will just cancel in the overlap region. This enables geometric cut-out operations. In the following example we construct an exact hollow cylinder solution from two concentric cylinder shapes with opposite magnetizations and compare the result to the `CylinderSegment` class solution. Here the `getM` and `getJ` functions come in handy. They allow us to see the magnetization distribution that is the result of the superposition. @@ -87,25 +87,26 @@ import magpylib as magpy fig, [ax1, ax2] = plt.subplots(1, 2, figsize=(12, 5)) -# Create an observer grid in the xy-symmetry plane -X, Y = np.mgrid[-4:4:100j, -4:4:100j].transpose((0, 2, 1)) -grid_xy = np.stack([X, Y, np.zeros((100, 100))], axis=2) -grid_xz = np.stack([X, np.zeros((100, 100)), Y], axis=2) - # Create ring with cut-out inner = magpy.magnet.Cylinder(polarization=(0, 0, -1), dimension=(4, 5)) outer = magpy.magnet.Cylinder(polarization=(0, 0, 1), dimension=(6, 5)) ring0 = inner + outer -# Compute J-field of ring -M_xy = np.linalg.norm(ring0.getM(grid_xy), axis=2) -M_xz = np.linalg.norm(ring0.getM(grid_xz), axis=2) +# Compute and plot Magnetization in xy-plane +grid = np.mgrid[-4:4:100j, -4:4:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) + +M = np.linalg.norm(ring0.getM(grid), axis=2) +ax1.contourf(X, Y, M, cmap=plt.cm.hot_r) + +# Compute and plot Magnetization in xz-plane +grid = np.mgrid[-4:4:100j, 0:0:1j, -4:4:100j].T[:,0] +X, _, Z = np.moveaxis(grid, 2, 0) -# Display field with Pyplot -ax1.contourf(grid_xy[:, :, 0], grid_xy[:, :, 1], M_xy, cmap=plt.cm.hot_r) -ax2.contourf(grid_xz[:, :, 0], grid_xz[:, :, 2], M_xz, cmap=plt.cm.hot_r) +M = np.linalg.norm(ring0.getM(grid), axis=2) +ax2.contourf(X, Z, M, cmap=plt.cm.hot_r) -# plot styling +# Plot styling ax1.set( title="|M| in xy-plane", xlabel="x-position", @@ -129,9 +130,11 @@ plt.show() The two figures show that the magnetization is zero outside of the cylinder, as well as in the overlap region where the two magnetizations cancel. -Finally we want to show that the superposition gives the same result as a computation from the CylinderSegment solution. +Finally, we want to show that the superposition gives the same result as a computation from the CylinderSegment solution. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + from magpylib.magnet import Cylinder, CylinderSegment # Create ring with CylinderSegment @@ -142,9 +145,9 @@ print("CylinderSegment result:", ring1.getB((.01, .02, .03))) print(" Cut-out result:", ring0.getB((.01, .02, .03))) ``` -Note that, it is faster to compute the `Cylinder` field two times than computing the `CylinderSegment` field one time. This is why Magpylib automatically falls back to the `Cylinder` solution whenever `CylinderSegment` is called with 360 deg section angles. +Note that it is faster to compute the `Cylinder` field two times than computing the `CylinderSegment` field one time. This is why Magpylib automatically falls back to the `Cylinder` solution whenever `CylinderSegment` is called with 360 deg section angles. -Unfortunately, with respect to 3D-models, cut-out operations cannot be displayed graphically at the moment, but {ref}`examples-own-3d-models` offer custom solutions. +Unfortunately, with respect to 3D-models, cut-out operations cannot be displayed graphically at this point in time, but {ref}`examples-own-3d-models` offer custom solutions. ## Nice example @@ -170,21 +173,17 @@ pt3 = magpy.magnet.Cuboid( magnet = magpy.Collection(pt1, pt2, pt3) # Compute J on mesh and plot with streamplot -X, Y = np.mgrid[-6:6:100j, -6:6:100j].transpose((0, 2, 1)) -grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) +grid = np.mgrid[-6:6:100j, -6:6:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) J = magnet.getJ(grid) J[J<1e-12] = 0 # cut off numerically small values +Jx, Jy, _ = np.moveaxis(J, 2, 0) -ax.contourf(grid[:, :, 0], grid[:, :, 1], np.linalg.norm(J,axis=2), cmap=plt.cm.cool) -ax.streamplot( - grid[:, :, 0], - grid[:, :, 1], - J[:, :, 0], - J[:, :, 1], color='k', density=1.5, -) +ax.contourf(X, Y, np.linalg.norm(J,axis=2), cmap=plt.cm.cool) +ax.streamplot(X, Y, Jx, Jy, color='k', density=1.5) -# plot styling +# Plot styling ax.set( title="Polarization J in xy-plane", xlabel="x-position", diff --git a/docs/_pages/gallery/gallery_shapes_triangle.md b/docs/_pages/user_guide/examples/examples_shapes_triangle.md similarity index 77% rename from docs/_pages/gallery/gallery_shapes_triangle.md rename to docs/_pages/user_guide/examples/examples_shapes_triangle.md index 175a67dbf..bb2a211b8 100644 --- a/docs/_pages/gallery/gallery_shapes_triangle.md +++ b/docs/_pages/user_guide/examples/examples_shapes_triangle.md @@ -12,18 +12,18 @@ kernelspec: orphan: true --- -(gallery-shapes-triangle)= +(examples-shapes-triangle)= # Triangular Meshes -The magnetic field of a homogeneously magnetized body is equivalent to the field of a charged surface. The surface is the hull of the body and the charge density is proportional to the projection of the magnetization vector onto the surface normal. +The magnetic field of a homogeneously magnetized body is equivalent to the field of a charged surface. The surface is the hull of the body, and the charge density is proportional to the projection of the magnetization vector onto the surface normal. It is very common to approximate the surface of bodies by triangular meshes, which can then be transformed into magnets using the `Triangle` and the `TriangularMesh` classes. When using these classes one should abide by the following rules: -1. The surface must be closed, or, all missing faces must have zero charge (magnetization vector perpendicular to surface normal). +1. The surface must be closed, or all missing faces must have zero charge (magnetization vector perpendicular to surface normal). 2. All triangles are oriented outwards (right-hand-rule) 3. The surface must not be self-intersecting. -4. For the B-field the magnetic polarization must be added on the inside of the body. +4. For the B-field magnetic polarization must be added on the inside of the body. ## Cuboctahedron Magnet @@ -77,9 +77,9 @@ magpy.show( ## Triangular Prism Magnet -Consider a prism with triangular base that is magnetized orthogonal to the base. All surface normals of the sides of the prism are orthogonal to the magnetization vector. As a result the sides do not contribute to the magnetic field because their charge density disappears. Only top and bottom surfaces contribute. One must be very careful when defining those surfaces in such a way that the surface normals point outwards. +Consider a prism with triangular base that is magnetized orthogonal to the base. All surface normals of the sides of the prism are orthogonal to the magnetization vector. As a result, the sides do not contribute to the magnetic field because their charge density disappears. Only the top and bottom surfaces contribute. One must be very careful when defining those surfaces in such a way that the surface normals point outwards. -Leaving out parts of the surface that do not contribute to the field is beneficial for the computation speed. +Leaving out parts of the surface that do not contribute to the field is beneficial for computation speed. ```{code-cell} ipython3 import magpylib as magpy @@ -103,13 +103,13 @@ magpy.show(*prism, backend="plotly", style_opacity=0.5, style_magnetization_show ## TriangularMesh class -While `Triangle` simply provides the field of a charged triangle and can be used to contruct complex forms, it is prone to error and tedious to work with when meshes become large. For this purpose the `TriangularMesh` class ensures proper and convenient magnet creation by automatically checking mesh integrity and by orienting the faces at initialization. +While `Triangle` simply provides the field of a charged triangle and can be used to construct complex forms, it is prone to error and tedious to work with when meshes become large. For this purpose, the `TriangularMesh` class ensures proper and convenient magnet creation by automatically checking mesh integrity and by orienting the faces at initialization. ```{attention} Automatic face reorientation of `TriangularMesh` may fail when the mesh is open. ``` -In this example we revisit the cuboctahedron, but generate it through the `TriangularMesh` class. +In this example we revisit the cuboctahedron but generate it through the `TriangularMesh` class. ```{code-cell} ipython3 import magpylib as magpy @@ -138,21 +138,21 @@ magpy.show( ) ``` -The `TriangularMesh` class is extremely powerful as it enables almost arbitrary magnet shapes. It is described in detail in {ref}`docu-magpylib-api-trimesh`. There are many ways to generate such triangular meshes. An example thereof is shown in {ref}`gallery-shapes-pyvista`. +The `TriangularMesh` class is extremely powerful as it enables almost arbitrary magnet shapes. It is described in detail in {ref}`docu-magpylib-api-trimesh`. There are many ways to generate such triangular meshes. An example thereof is shown in {ref}`examples-shapes-pyvista`. ```{caution} * `getB` and `getH` compute the fields correctly only if the mesh is closed, not self-intersecting, and all faces are properly oriented outwards. * Input checks and face reorientation can be computationally expensive. The checks can individually be deactivated by setting `reorient_faces="skip"`, `check_open="skip"`, `check_disconnected="skip"`, and `check_selfintersecting="skip"` at initialization of `TriangularMesh` objects. The checks can also be performed by hand after initialization. -* Meshing tools such as the [Pyvista](https://docs.pyvista.org/) library can be very convenient for building complex shapes, but often do not guarantee that the mesh is properly closed or connected - see {ref}`gallery-shapes-pyvista`. +* Meshing tools such as the [Pyvista](https://docs.pyvista.org/) library can be very convenient for building complex shapes, but often do not guarantee that the mesh is properly closed or connected - see {ref}`examples-shapes-pyvista`. -* Meshing tools often create meshes with a lot of faces, especially when working with curved surfaces. Keep in mind that the field computation takes of the order of a few microseconds per observer position per face, and that RAM is a limited resource. +* Meshing tools often create meshes with a lot of faces, especially when working with curved surfaces. Keep in mind that field computation takes of the order of a few microseconds per observer position per face, and that RAM is a limited resource. ``` ## Open TriangularMesh -In some cases it may be desirable to generate a `TriangularMesh` object from an open mesh (see Prism example above). In this case one has to be extremely careful because one cannot rely on the checks. Not to generate warnings or error messages, these checks can be disabled with `"skip"` or their outcome can be ignored with `"ignore"`. The `show` function can be used to view open edges and disconnected parts. In the following example we generate such an open mesh directly from `Triangle` objects. +In some cases, it may be desirable to generate a `TriangularMesh` object from an open mesh (see Prism example above). In this case one must be extremely careful because one cannot rely on the checks. Not to generate warnings or error messages, these checks can be disabled with `"skip"` or their outcome can be ignored with `"ignore"`. The `show` function can be used to view open edges and disconnected parts. In the following example we generate such an open mesh directly from `Triangle` objects. ```{code-cell} ipython3 import magpylib as magpy diff --git a/docs/_pages/gallery/gallery_tutorial_collection.md b/docs/_pages/user_guide/examples/examples_tutorial_collection.md similarity index 90% rename from docs/_pages/gallery/gallery_tutorial_collection.md rename to docs/_pages/user_guide/examples/examples_tutorial_collection.md index 2e0cc5594..ac1a81d3b 100644 --- a/docs/_pages/gallery/gallery_tutorial_collection.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_collection.md @@ -12,15 +12,15 @@ kernelspec: orphan: true --- -(gallery-tutorial-collection)= +(examples-tutorial-collection)= # Working with Collections -The top level class `Collection` allows users to group objects by reference for common manipulation. The idea is that +The top-level class `Collection` allows users to group objects by reference for common manipulation. The following concepts apply to Magpylib Collections: 1. A collection spans its own local frame of reference with position and orientation, to which the children are added. Thus, any operation applied to the collection is individually applied to all its children. 2. The collection itself behaves like a single Magpylib object (can be source and/or observer). -3. All children inside the collection can be individually addressed and manipulated at all times. +3. All children inside the collection can be individually addressed and manipulated, which will automatically manipulate their state inside the parent collection. 4. Collections have their own `style` attributes, their paths are displayed in `show`, and all children are automatically assigned their parent color. ## Constructing Collections @@ -45,6 +45,8 @@ print(f"collections: {coll.collections}") New additions are always added at the end. Use the **`add`** method or the parameters. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + # Copy adjusts object label automatically x2 = x1.copy() s2 = s1.copy() @@ -65,6 +67,8 @@ print(f"collections: {coll.collections}") The **`describe`** method is a very convenient way to view a Collection structure, especially when the collection is nested, i.e., when containing other collections. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + # Add more objects c1.add(x2.copy()) c2.add(s2.copy()) @@ -142,6 +146,8 @@ for child in coll: and makes it possible to directly reference to a child object by index: ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + print(coll[0]) ``` @@ -154,7 +160,7 @@ s1 = magpy.Sensor(style_label="s1") s2 = s1.copy() s3 = s2.copy() -# this creates a nested collection +# This creates a nested collection coll = s1 + s2 + s3 coll.describe(format="label") @@ -164,11 +170,10 @@ print([s.style.label for s in coll.sensors_all]) ## Practical Example -The following example demonstrates how collections enable user-friendly manipulation of groups, sub-groups and individual objects. +The following example demonstrates how collections enable user-friendly manipulation of groups, sub-groups, and individual objects. ```{code-cell} ipython3 import numpy as np - import magpylib as magpy # Construct two coils from windings @@ -201,6 +206,8 @@ magpy.show(*helmholtz, animation=True, style_path_show=False) For magnetic field computation, a collection with source children behaves like a single source object, and a collection with sensor children behaves like a flat list of its sensors when provided as `sources` and `observers` input respectively. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import matplotlib.pyplot as plt B = magpy.getB(helmholtz, (0.01, 0, 0)) @@ -219,7 +226,7 @@ plt.show() ## Efficient 3D Models -The graphical backend libraries were not designed for complex 3D graphic output. As a result, it becomes often inconvenient and slow when attempting to display many 3D objects. One solution to this problem when dealing with large collections is to represent the latter by a single encompassing body, and to deactivate the individual 3D models of all children. +The graphical backend libraries were not designed for complex 3D graphic output. As a result, it often becomes inconvenient and slow when attempting to display many 3D objects. One solution to this problem when dealing with large collections is to represent the latter by a single encompassing body, and to deactivate the individual 3D models of all children. ```{code-cell} ipython3 import magpylib as magpy @@ -253,4 +260,4 @@ coll.show() ## Compound Objects -Collections can be subclassed to form dynamic groups that seamlessly integrate into Magpylib. Such classes are referred to as **compounds**. An example how this is done is shown in {ref}`gallery-misc-compound`. +Collections can be subclassed to form dynamic groups that seamlessly integrate into Magpylib. Such classes are referred to as **compounds**. An example of how this is done is shown in {ref}`examples-misc-compound`. diff --git a/docs/_pages/gallery/gallery_tutorial_custom.md b/docs/_pages/user_guide/examples/examples_tutorial_custom.md similarity index 67% rename from docs/_pages/gallery/gallery_tutorial_custom.md rename to docs/_pages/user_guide/examples/examples_tutorial_custom.md index 02afd57cc..ea9983b84 100644 --- a/docs/_pages/gallery/gallery_tutorial_custom.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_custom.md @@ -12,11 +12,11 @@ kernelspec: orphan: true --- -(gallery-tutorial-custom)= +(examples-tutorial-custom)= # CustomSource -The {ref}`docu-magpylib-api-custom` class was implemented to offer easy integration of user field implementations into Magpylib's object-oriented interface. +The {ref}`guide-docs-classes-custom-source` class was implemented to offer easy integration of user field implementations into Magpylib's object-oriented interface. ```{note} Obviously, any field implementation can be integrated. Specifically, fields where superposition holds and interactions do not disturb the sources (e.g. electric, gravitational, ...) can benefit from Magpylib's position and orientation interface. @@ -36,10 +36,8 @@ We create this field as a Python function and hand it over to a CustomSource `fi ```{code-cell} ipython3 import numpy as np - import magpylib as magpy - # Create monopole field def mono_field(field, observers): """ @@ -54,7 +52,7 @@ def mono_field(field, observers): Returns: np.ndarray, shape (n,3) Magnetic monopole field """ - Qm = 1e-6 # unit Tยทmยฒ + Qm = 1 # unit Tยทmยฒ obs = np.array(observers).T # unit m B = Qm * (obs / np.linalg.norm(obs, axis=0) ** 3).T # unit T if field == "B": @@ -65,33 +63,40 @@ def mono_field(field, observers): else: raise ValueError("Field Value must be either B or H") - # Create CustomSource with monopole field mono = magpy.misc.CustomSource(field_func=mono_field) # Compute field -print(mono.getB((0.001, 0, 0))) -print(mono.getH((0.001, 0, 0))) +print(mono.getB((1, 0, 0))) +print(mono.getH((1, 0, 0))) ``` Multiple of these sources can now be combined, making use of the Magpylib position/orientation interface. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import matplotlib.pyplot as plt # Create two monopole charges -mono1 = magpy.misc.CustomSource(field_func=mono_field, position=(0.002, 0.002, 0)) -mono2 = magpy.misc.CustomSource(field_func=mono_field, position=(-0.002, -0.002, 0)) +mono1 = magpy.misc.CustomSource(field_func=mono_field, position=(2, 2, 0)) +mono2 = magpy.misc.CustomSource(field_func=mono_field, position=(-2, -2, 0)) # Compute field on observer-grid -X, Y = np.mgrid[-0.005:0.005:40j, -0.005:0.005:40j].transpose((0, 2, 1)) -grid = np.stack([X, Y, np.zeros((40, 40))], axis=2) +grid = np.mgrid[-5:5:100j, -5:5:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) + B = magpy.getB([mono1, mono2], grid, sumup=True) +Bx, By, _ = np.moveaxis(B, 2, 0) normB = np.linalg.norm(B, axis=2) # Plot field in x-y symmetry plane cp = plt.contourf(X, Y, np.log10(normB), cmap="gray_r", levels=10) -plt.streamplot(X, Y, B[:, :, 0], B[:, :, 1], color="k", density=1) +plt.streamplot(X, Y, Bx, By, color="k", density=1) + +plt.title("Field of two Monopoles") +plt.xlabel("x-position (m)") +plt.ylabel("y-position (m)") plt.tight_layout() plt.show() @@ -99,12 +104,14 @@ plt.show() ## Adding a 3D model -While the CustomSource is graphically represented by a simple marker by default, we can easily add a 3D model as described in {ref}`examples-own-3d-models`. +While `CustomSource` is graphically represented by a simple marker by default, we can easily add a 3D model as described in {ref}`examples-own-3d-models`. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + # Load Sphere model trace_pole = magpy.graphics.model3d.make_Ellipsoid( - dimension=np.array([3, 3, 3]) * 1e-4, + dimension=np.array([.3, .3, .3]), ) for mono in [mono1, mono2]: @@ -120,9 +127,12 @@ magpy.show(mono1, mono2) ## Subclassing CustomSource -In the above example it would be nice to make the CustomSource dynamic, so that it would have a property `charge` that can be changed at will, rather than having to redefine the `field_func` and initialize a new object every time. In the following example we show how to sub-class `CustomSource` to achieve this. The problem is reminiscent of {ref}`gallery-misc-compound`. +In the above example it would be nice to make the `CustomSource` dynamic, so that it would have a property `charge` that can be changed at will, rather than having to redefine the `field_func` and initialize a new object every time. In the following example we show how to sub-class `CustomSource` to achieve this. The problem is reminiscent of {ref}`examples-misc-compound`. ```{code-cell} ipython3 +import numpy as np +import magpylib as magpy + class Monopole(magpy.misc.CustomSource): """Magnetic Monopole class @@ -138,7 +148,7 @@ class Monopole(magpy.misc.CustomSource): # Add spherical 3d model trace_pole = magpy.graphics.model3d.make_Ellipsoid( - dimension=np.array([3, 3, 3]) * 1e-4, + dimension=np.array([.3, .3, .3]), ) self.style.model3d.showdefault = False self.style.model3d.add_trace(trace_pole) @@ -176,77 +186,51 @@ class Monopole(magpy.misc.CustomSource): self._charge = input self._update() - # Use new class -mono = Monopole(charge=1e-6) -print(mono.getB((0.001, 0, 0))) +mono = Monopole(charge=1) +print(mono.getB((1, 0, 0))) -# Make use of new property -mono.charge = -1e-6 -print(mono.getB((0.001, 0, 0))) +# Change property charge of object +mono.charge = -1 +print(mono.getB((1, 0, 0))) ``` -The new class seamlessly integrates into the Magpylib interface as we show in the following example where we have a look at the Quadrupole field +The new class seamlessly integrates into the Magpylib interface as we show in the following example where we have a look at the Quadrupole field. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import matplotlib.pyplot as plt # Create a quadrupole from four monopoles -mono1 = Monopole(charge=1e-6, style_color="r", position=(0.001, 0, 0)) -mono2 = Monopole(charge=1e-6, style_color="r", position=(-0.001, 0, 0)) -mono3 = Monopole(charge=-1e-6, style_color="b", position=(0, 0, 0.001)) -mono4 = Monopole(charge=-1e-6, style_color="b", position=(0, 0, -0.001)) +mono1 = Monopole(charge=1, style_color="r", position=(1, 0, 0)) +mono2 = Monopole(charge=1, style_color="r", position=(-1, 0, 0)) +mono3 = Monopole(charge=-1, style_color="b", position=(0, 0, 1)) +mono4 = Monopole(charge=-1, style_color="b", position=(0, 0, -1)) qpole = magpy.Collection(mono1, mono2, mono3, mono4) # Matplotlib figure with 3d and 2d axis -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_subplot( - 121, - projection="3d", - azim=-80, - elev=15, -) -ax2 = fig.add_subplot( - 122, -) +fig = plt.figure(figsize=(10, 5)) +ax1 = fig.add_subplot(121, projection="3d", azim=-80, elev=15) +ax2 = fig.add_subplot(122) # Show 3D model in ax1 -magpy.show(*qpole, canvas=ax1) +magpy.show(*qpole, canvas=ax1, style_legend_show=False) # Compute B-field on xz-grid and display in ax2 -ts = np.linspace(-3, 3, 30) -grid = np.array([[(x / 1000, 0, z / 1000) for x in ts] for z in ts]) +grid = np.mgrid[-2:2:100j, 0:0:1j, -2:2:100j].T[:,0] +X, _, Z = np.moveaxis(grid, 2, 0) + B = qpole.getB(grid) +Bx, _, Bz = np.moveaxis(B, 2, 0) +scale = np.linalg.norm(B, axis=2)**.3 -scale = np.linalg.norm(B, axis=2) -cp = ax2.contourf( - grid[:, :, 0], - grid[:, :, 2], - np.log(scale), - levels=100, - cmap="rainbow", -) -ax2.streamplot( - grid[:, :, 0], - grid[:, :, 2], - B[:, :, 0], - B[:, :, 2], - density=2, - color="k", - linewidth=scale**0.3, -) +cp = ax2.contourf(X, Z, np.log(scale), levels=100, cmap="rainbow") +ax2.streamplot(X, Z, Bx, Bz, density=2, color="k", linewidth=scale) # Display pole position in ax2 -pole_pos = np.array([mono.position for mono in qpole]) -ax2.plot( - pole_pos[:, 0], - pole_pos[:, 2], - marker="o", - ms=10, - mfc="k", - mec="w", - ls="", -) +ppos = np.array([mono.position for mono in qpole]) +ax2.plot(ppos[:, 0], ppos[:, 2], marker="o", ms=10, mfc="k", mec="w", ls="") # Figure styling ax1.set( @@ -261,7 +245,7 @@ ax2.set( ylabel="z-position (m)", aspect=1, ) -fig.colorbar(cp, label="[$charge/m^2$]", ax=ax2) +fig.colorbar(cp, ax=ax2) plt.tight_layout() plt.show() diff --git a/docs/_pages/gallery/gallery_tutorial_field_computation.md b/docs/_pages/user_guide/examples/examples_tutorial_field_computation.md similarity index 82% rename from docs/_pages/gallery/gallery_tutorial_field_computation.md rename to docs/_pages/user_guide/examples/examples_tutorial_field_computation.md index fee926989..0e54212ee 100644 --- a/docs/_pages/gallery/gallery_tutorial_field_computation.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_field_computation.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-tutorial-field-computation)= +(examples-tutorial-field-computation)= # Computing the Field @@ -32,68 +32,40 @@ print(B) There are four field computation functions: `getB` will compute the B-field in T. `getH` computes the H-field in A/m. `getJ` computes the magnetic polarization in units of T. `getM` computes the magnetization in units of A/m. -All these functions will return the field in the shape of the input. In the following example, BHJM-fields of a diametrically magnetized cylinder magnet are computed on a position grid in the symmetry plane, and are then displayed using Matplotlib. +All these functions will return the field in the shape of the input. In the following example, BHJM-fields of a diametrically magnetized cylinder magnet are computed on a position grid in the symmetry plane and are then displayed using Matplotlib. ```{code-cell} ipython3 import matplotlib.pyplot as plt import numpy as np - +from numpy.linalg import norm import magpylib as magpy fig, [[ax1,ax2], [ax3,ax4]] = plt.subplots(2, 2, figsize=(10, 10)) -# Create an observer grid in the xz-symmetry plane -X, Y = np.mgrid[-50:50:100j, -50:50:100j].transpose((0, 2, 1)) -grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) +# Create an observer grid in the xy-symmetry plane +grid = np.mgrid[-50:50:100j, -50:50:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) # Compute BHJM-fields of a cylinder magnet on the grid cyl = magpy.magnet.Cylinder(polarization=(0.5, 0.5, 0), dimension=(40, 20)) + B = cyl.getB(grid) +Bx, By, _ = np.moveaxis(B, 2, 0) + H = cyl.getH(grid) +Hx, Hy, _ = np.moveaxis(H, 2, 0) + J = cyl.getJ(grid) +Jx, Jy, _ = np.moveaxis(J, 2, 0) + M = cyl.getM(grid) +Mx, My, _ = np.moveaxis(M, 2, 0) # Display field with Pyplot -ax1.streamplot( - grid[:, :, 0], - grid[:, :, 1], - B[:, :, 0], - B[:, :, 1], - density=1.5, - color=np.log(np.linalg.norm(B, axis=2)), - linewidth=1, - cmap="spring_r", -) -ax2.streamplot( - grid[:, :, 0], - grid[:, :, 1], - H[:, :, 0], - H[:, :, 1], - density=1.5, - color=np.log(np.linalg.norm(H, axis=2)), - linewidth=1, - cmap="winter_r", -) -ax3.streamplot( - grid[:, :, 0], - grid[:, :, 1], - J[:, :, 0], - J[:, :, 1], - density=1.5, - color=np.linalg.norm(J, axis=2), - linewidth=1, - cmap="summer_r", -) -ax4.streamplot( - grid[:, :, 0], - grid[:, :, 1], - M[:, :, 0], - M[:, :, 1], - density=1.5, - color=np.linalg.norm(M, axis=2), - linewidth=1, - cmap="autumn_r", -) +ax1.streamplot(X, Y, Bx, By, color=np.log(norm(B, axis=2)), cmap="spring_r") +ax2.streamplot(X, Y, Hx, Hy, color=np.log(norm(H, axis=2)), cmap="winter_r") +ax3.streamplot(X, Y, Jx, Jy, color=norm(J, axis=2), cmap="summer_r") +ax4.streamplot(X, Y, Mx, My, color=norm(M, axis=2), cmap="autumn_r") ax1.set_title("B-Field") ax2.set_title("H-Field") @@ -116,7 +88,7 @@ plt.tight_layout() plt.show() ``` -(gallery-tutorial-field-computation-sensors)= +(examples-tutorial-field-computation-sensors)= ## Using Sensors @@ -126,7 +98,6 @@ The following example shows a moving and rotating sensor with two pixels. At the ```{code-cell} ipython3 import numpy as np - import magpylib as magpy # Reset defaults set in previous example @@ -156,7 +127,7 @@ with magpy.show_context(sensor, coll, animation=True, backend="plotly"): ## Multiple Inputs -When `getBHJM` receive multiple inputs for sources and observers they will compute all possible combinations. It is still beneficial to call the field computation only a single time, because similar sources will be grouped and the computation will be vectorized automatically. +When `getBHJM` receive multiple inputs for sources and observers they will compute all possible combinations. It is still beneficial to call the field computation only a single time, because similar sources will be grouped, and the computation will be vectorized automatically. ```{code-cell} ipython3 import magpylib as magpy @@ -181,6 +152,8 @@ B.shape Select the second cube (first index), the first sensor (second index), pixel 3-4 (index three and four) and the Bz-component of the field (index five) ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + B[1, 0, 2, 3, 2] ``` @@ -188,11 +161,10 @@ A path will add another index. Every higher pixel dimension will add another ind ## Field as Pandas Dataframe -Instead of a Numpy `ndarray`, the field computation can also return a [pandas](https://pandas.pydata.org/).[dataframe](https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe) using the `output='dataframe'` kwarg. +Instead of a NumPy `ndarray`, the field computation can also return a [pandas](https://pandas.pydata.org/).[dataframe](https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe) using the `output='dataframe'` kwarg. ```{code-cell} ipython3 import numpy as np - import magpylib as magpy cube = magpy.magnet.Cuboid( @@ -222,6 +194,8 @@ B Plotting libraries such as [plotly](https://plotly.com/python/plotly-express/) or [seaborn](https://seaborn.pydata.org/introduction.html) can take advantage of this feature, as they can deal with `dataframes` directly. ```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + import plotly.express as px fig = px.line( @@ -236,11 +210,11 @@ fig = px.line( fig.show() ``` -(gallery-tutorial-field-computation-functional-interface)= +(examples-tutorial-field-computation-functional-interface)= ## Functional Interface -All above computations demonstrate the convenient object oriented interface of Magpylib. However, there are instances when it is better to work with the functional interface instead. +All above computations demonstrate the convenient object-oriented interface of Magpylib. However, there are instances when it is better to work with the functional interface instead. 1. Reduce overhead of Python objects 2. Complex computation instances @@ -248,12 +222,11 @@ All above computations demonstrate the convenient object oriented interface of M In the following example we show how complex instances are computed using the functional interface. ```{important} -The functional interface will only outperform the object oriented interface if you use numpy operations for input array creation, such as `tile`, `repeat`, `reshape`, ... ! +The functional interface will only outperform the object oriented interface if you use NumPy operations for input array creation, such as `tile`, `repeat`, `reshape`, ... ! ``` ```{code-cell} ipython3 import numpy as np - import magpylib as magpy # Two different magnet dimensions diff --git a/docs/_pages/gallery/gallery_tutorial_modelling_magnets.md b/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md similarity index 57% rename from docs/_pages/gallery/gallery_tutorial_modelling_magnets.md rename to docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md index b2367a645..fc00f1099 100644 --- a/docs/_pages/gallery/gallery_tutorial_modelling_magnets.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_modeling_magnets.md @@ -1,28 +1,24 @@ ---- -orphan: true ---- +(examples-tutorial-modeling-magnets)= -(gallery-tutorial-modelling-magnets)= +# Modeling a real magnet -# Modelling a real magnet - -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 explaining 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. +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. 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. ## Short summary -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). +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). 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. ## Hysteresis loop -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 size and shape of this volume and what is inside and what is outside. +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. 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: @@ -32,13 +28,13 @@ The B-H curve is called the "normal loop", while J-H and M-H curves are called " ::: :::{grid-item} :columns: 6 - + ::: :::: -**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$. +**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 are able to keep their magnetization $J$ even for strong external fields in the opposite direction. +**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. **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. @@ -48,25 +44,25 @@ Hysteresis in magnetism as presented here is a macroscopic model that is the res 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. - + -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 a 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: +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: - + -Making use of the [streamplot example](gallery-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". +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". ## 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 obtain the correct mean polarization of a magnet we only have to 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. +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. - + 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: - + 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 @@ -88,7 +84,7 @@ Keep in mind that there are still many reasons why your simulation might not fit ::: :::{grid-item} :columns: 6 - + ::: :::: @@ -98,4 +94,4 @@ coming soon: 3. Comparison and discussion **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/ \ No newline at end of file +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/ \ No newline at end of file diff --git a/docs/_pages/gallery/gallery_tutorial_paths.md b/docs/_pages/user_guide/examples/examples_tutorial_paths.md similarity index 90% rename from docs/_pages/gallery/gallery_tutorial_paths.md rename to docs/_pages/user_guide/examples/examples_tutorial_paths.md index eaae4b1e9..0da674d36 100644 --- a/docs/_pages/gallery/gallery_tutorial_paths.md +++ b/docs/_pages/user_guide/examples/examples_tutorial_paths.md @@ -12,11 +12,11 @@ kernelspec: orphan: true --- -(gallery-tutorial-paths)= +(examples-tutorial-paths)= # Working with Paths -The position and orientation attributes are key elements of Magpylib. The documentation section {ref}`docu-position` describes how they work. However, these definitions can seem abstract, but the interface was constructed as intuitive as possible. +The position and orientation attributes are key elements of Magpylib. The documentation section {ref}`docs-position` describes how they work in detail. Wile these definitions can seem abstract, the interface was constructed as intuitively as possible which is demonstrated in this tutorial. ```{important} Always make use of paths when computing with multiple Magpylib object position and orientation instances. This enables vectorized computation. Avoid Python loops at all costs! @@ -31,7 +31,6 @@ Absolute object paths are assigned at initialization or through the object prope ```{code-cell} ipython3 import numpy as np from scipy.spatial.transform import Rotation as R - import magpylib as magpy # Create paths @@ -58,7 +57,6 @@ magpy.show(sensor, cube, animation=True, backend="plotly") ```{code-cell} ipython3 import numpy as np from scipy.spatial.transform import Rotation as R - import magpylib as magpy # Create paths @@ -85,7 +83,6 @@ When the input is a vector, the path is by default appended. ```{code-cell} ipython3 import numpy as np - from magpylib.magnet import Sphere # Create paths @@ -105,11 +102,10 @@ sphere.show() ## Merging paths -Complex paths can be created by merging multiple path operations. This is done with vector input for the `move` and `rotate` methods, and choosing values for `start` that will make the paths overlap. In the following example we combine a linear path with a rotation about self (`anchor=None`) until path index 30. Thereon, a second rotation about the origin is applied, creating a spiral. +Complex paths can be created by merging multiple path operations. This is done with vector input for the `move` and `rotate` methods and choosing values for `start` that will make the paths overlap. In the following example we combine a linear path with a rotation about self (`anchor=None`) until path index 30. Thereon, a second rotation about the origin is applied, creating a spiral. ```{code-cell} ipython3 import numpy as np - from magpylib.magnet import Cuboid # Create cube and set linear path @@ -145,19 +141,17 @@ print(sensor.position) print(sensor.orientation.as_quat()) ``` -(gallery-tutorial-paths-edge-padding-end-slicing)= - +(examples-tutorial-paths-edge-padding-end-slicing)= ## Edge-padding and end-slicing Magpylib will always make sure that object paths are in the right format, i.e., `position` and `orientation` attributes are of the same length. In addition, when objects with different path lengths are combined, e.g., when computing the field, the shorter paths are treated as static beyond their end to make the computation sensible. Internally, Magpylib follows a philosophy of edge-padding and end-slicing when adjusting paths. -The idea behind **edge-padding** is that, whenever path entries beyond the existing path length are needed, the edge-entries of the existing path are returned. This means that the object is considered to be "static" beyond its existing path. +The idea behind **edge-padding** is that, whenever path entries beyond the existing path length are needed, the edge-entries of the existing path are returned. This means that the object is static beyond its existing path. In the following example the orientation attribute is padded by its edge value `(0,0,.2)` as the position attribute length is increased. ```{code-cell} ipython3 from scipy.spatial.transform import Rotation as R - import magpylib as magpy sensor = magpy.Sensor( @@ -185,7 +179,6 @@ The idea behind **end-slicing** is that, whenever a path is automatically reduce ```{code-cell} ipython3 from scipy.spatial.transform import Rotation as R - from magpylib import Sensor sensor = Sensor( diff --git a/docs/_pages/user_guide/examples/examples_vis_animations.md b/docs/_pages/user_guide/examples/examples_vis_animations.md new file mode 100644 index 000000000..87c560685 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_vis_animations.md @@ -0,0 +1,289 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.1 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +orphan: true +--- + +(examples-vis-animations)= + +# Animations + +Magpylib can display the motion of objects along paths in the form of animations. + +```{hint} +1. Animations work best with the [plotly backend](guide-graphic-backends). + +2. If your browser window opens, but your animation does not load, reload the page. + +3. Avoid rendering too many frames. +``` + +Detailed information about how to tune animations can be found in the [graphics documentation](guide-graphic-animations). + +## Simple Animations + +Animations are created with `show` by setting `animation=True`. It is also possible to hand over the animation time with this parameter. + +```{code-cell} ipython3 +import magpylib as magpy +import numpy as np + +# Define magnet with path +magnet = magpy.magnet.Cylinder( + polarization=(1, 0, 0), + dimension=(2, 1), + position=(4, 0, 0), + style_label="magnet", +) +magnet.rotate_from_angax(angle=np.linspace(0, 300, 40), start=0, axis="z", anchor=0) + +# Define sensor with path +sensor = magpy.Sensor( + pixel=[(-0.2, 0, 0), (0.2, 0, 0)], + position=np.linspace((0, 0, -3), (0, 0, 3), 40), + style_label="sensor", +) + +# Display as animation - prefers plotly backend +magpy.show(sensor, magnet, animation=True, backend="plotly") +``` + +(examples-vis-animated-subplots)= + +## Animated Subplots + +[Subplots](examples-vis-subplots) are a powerful tool to see the field along a path while viewing the 3D models at the same time. This is specifically illustrative as an animation where the field at the respective path position is indicated by a marker. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +magpy.show( + dict(objects=[magnet, sensor], output=["Bx", "By", "Bz"], col=1), + dict(objects=[magnet, sensor], output="model3d", col=2), + backend="plotly", + animation=True, +) +``` + +It is also possible to use the [show_context](guide-graphics-show_context) context manager. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +with magpy.show_context([magnet, sensor], backend="plotly", animation=True) as sc: + sc.show(output="Bx", col=1, row=1) + sc.show(output="By", col=1, row=2) + sc.show(output="Bz", col=2, row=1) + sc.show(output="model3d", col=2, row=2) +``` + +(examples-vis-exporting-animations)= + +## Exporting Animations + +Animations are wonderful but can be quite difficult to export when they are needed, for example, in a presentation. Here we show how to creat and export animations using the *.gif format. + +### Built-in export + +The easiest way to export an animation is via the Magpylib built-in command `animation_output` in the `show` function. It works only with the Pyvista backend. The following code will create a file "test4.gif". + +```python +import magpylib as magpy +import numpy as np + +# Create magnets with Paths +path = [(np.sin(t)+1.5,0,0) for t in np.linspace(0, 2*np.pi, 30)] +cube1 = magpy.magnet.Cuboid(dimension=(1,1,1), polarization=(1,0,0), position= path) +cube2 = cube1.copy(position=-np.array(path), polarization=(-1,0,0)) + +# Store gif with animation_output using Pyvista +magpy.show( + cube1, + cube2, + style_legend_show=False, + animation=3, + animation_output="my.gif", + backend="pyvista", + style_path_show=False, +) +``` + +<img src="../../../_static/videos/example_gif1.gif" width=50% align="center"> + +### Custom export Pyvista + +For customizing videos it is best to work directly in the respective graphic backends. Here we show how to transfer the Magpylib graphic objects to a Pyvista plotter, customize the plotting scene, export screen shots, and combine them in a *.gif. The following example also shows how to achieve transparency. + +```python +import magpylib as magpy +import pyvista as pv +from PIL import Image + +def create_gif(images, frame_time, output_file): + """Create a GIF from images""" + frames = [Image.fromarray(img) for img in images] + if frames: + frames[0].save( + output_file, + format="GIF", + append_images=frames[1:], + save_all=True, + duration=frame_time, + loop=0, # Infinite loop + disposal=2, # Remove previous frames for transparency + ) + + +def init_plotter(): + """ Init Pyvista plotter with custom scene layout""" + pl = pv.Plotter(notebook=False, off_screen=True, window_size=[300, 300]) + pl.camera_position = [ + (5, 5, 5), # Position of the camera + (0, 0, 0), # Focal point (what the camera is looking at) + (0, 0, 1) # View up direction + ] + pl.camera.zoom(0.5) + pl.set_background("k") # For better transparency + return pl + + +def create_frames(frames): + """Create frames with Pyvista.""" + + # Create Magpylib objects + mag1 = magpy.magnet.CylinderSegment(dimension=(3, 4, 1, 0, 45), polarization=(0, 0, 1)) + mag2 = magpy.magnet.CylinderSegment(dimension=(2, 3, 1, 0, 45), polarization=(0, 0, -1)) + + images = [] + pl = init_plotter() + for i in range(frames): + + # Modify object positions + mag1.rotate_from_angax(360 / frames, axis='z') + mag2.rotate_from_angax(-360 / frames, axis='z') + + # Transfer Magpylib objects to Pyvista plotter + pl.clear() + magpy.show(mag1, mag2, canvas=pl, style_legend_show=False) + + # Edit figure in Pyvista + pl.add_mesh(pv.Line(mag1.barycenter, mag2.barycenter), color="cyan") + + # Screenshot + print(f"Writing frame {i+1:3d}/{frames}") + ss = pl.screenshot(transparent_background=True, return_img=True) + images.append(ss) + + pl.close() + return images + + +def main(): + frames = 100 + frame_time = 40 + output_file = "my.gif" + + images = create_frames(frames) + create_gif(images, frame_time, output_file) + + +if __name__ == "__main__": + main() +``` + +<img src="../../../_static/videos/example_gif2.gif" width=50% align="center"> + +Notice that when providing a canvas, no update to its layout is performed by Magpylib, unless explicitly specified by setting `canvas_update=True` in `show()`. By default `canvas_update="auto"` only updates the canvas if is not provided by the user. Details can be found in the [graphics documentation](guide-graphics-canvas). + +### Custom export Plotly + +The following examples shows how to work in the Plotly backend. + +```python +import magpylib as magpy +from PIL import Image +import io + + +def create_gif(images, frame_time, output_file): + """Create GIF from frames in the temporary directory.""" + frames = [Image.open(io.BytesIO(data)) for data in images] + if frames: + frames[0].save( + output_file, + format="GIF", + append_images=frames[1:], + save_all=True, + duration=frame_time, + loop=0, # Infinite loop + ) + + +def create_frames(frames): + """Create frames with Pyvista.""" + + # Create Magpylib objects + mag1 = magpy.magnet.CylinderSegment(dimension=(3, 4, 1, 0, 45), polarization=(0, 0, 1)) + mag2 = magpy.magnet.CylinderSegment(dimension=(2, 3, 1, 0, 45), polarization=(0, 0, -1)) + + images = [] + for i in range(frames): + # Set object position + mag1.rotate_from_angax(360 / frames, axis="z") + mag2.rotate_from_angax(-360 / frames, axis="z") + + fig = magpy.show( + mag1, mag2, return_fig=True, backend="plotly", style_legend_show=False + ) + + # Edit figure in Plotly + fig.add_scatter3d( + x=(0, 0, 4, 4, 0), + y=(0, 0, 0, 0, 0), + z=(-2, 2, 2, -2, -2), + mode="lines", + line_color="black", + ) + + # Customize layout + fig.update_layout( + scene=dict( + camera_eye={"x": 1.5, "y": 1.5, "z": 1.5}, + camera_up={"x": 0, "y": 0, "z": 1}, + xaxis_range=(-5, 5), + yaxis_range=(-5, 5), + zaxis_range=(-5, 5), + ), + showlegend=False, + margin=dict(l=0, r=0, t=0, b=0), + ) + + # Screenshot (requires kaleido package) + print(f"Writing frame {i+1:3d}/{frames}") + img = fig.to_image(format="png", width=500, height=500) + images.append(img) + return images + + +def main(): + frames = 50 + frame_time = 50 + output_file = "my.gif" + + images = create_frames(frames) + create_gif(images, frame_time, output_file) + + +if __name__ == "__main__": + main() +``` + +<img src="../../../_static/videos/example_gif3.gif" width=50% align="center"> diff --git a/docs/_pages/user_guide/examples/examples_vis_magnet_colors.md b/docs/_pages/user_guide/examples/examples_vis_magnet_colors.md new file mode 100644 index 000000000..bf0218a58 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_vis_magnet_colors.md @@ -0,0 +1,71 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.14.5 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-vis-magnet-colors)= +# Magnet colors + +The polarization direction of a permanent magnet is often graphically displayed with the help of colors. However, there is no unified color scheme that everyone agrees on. The following image shows some color-examples from the web. + + + +Magpylib uses the DIN Specification 91411 (soon 91479) standard as default setting. The tri-color scheme has the advantage that for multi-pole elements it becomes clear which north is "connected" to which south. + +```{hint} +The color schemes often seem to represent homogeneous polarizations, referred to as "ideal typical" magnets in DIN Specification 91479. However, they often just represent general "pole patterns", i.e. rough sketches where the field goes in and where it comes out, that are not the result of homogeneous polarizations. On this topic review also the examples example {ref}`examples-misc-inhom`, and the tutorial {ref}`examples-tutorial-modeling-magnets`. +``` + +With Magpylib users can easily tune the magnet color schemes. The `style` options are `tricolor` with north, middle and south colors, and `bicolor` with north and south colors. + +```{code-cell} ipython +import magpylib as magpy + +# Create a magnetization style dictionary +mstyle = dict( + mode="color+arrow", + color=dict(north="magenta", middle="white", south="turquoise"), + arrow=dict(width=2, color="k") +) + +# Create magnet and apply style +sphere = magpy.magnet.Sphere( + polarization=(1, 1, 1), + diameter=1, + position=(-1, 1, 0), + style_magnetization=mstyle, +) + +# Create a second magnet with different style +cube = magpy.magnet.Cuboid( + polarization=(1, 0, 0), + dimension=(1, .2, .2), + position=(1, 1, 0), + style_magnetization_color_mode="bicolor", + style_magnetization_color_north="r", + style_magnetization_color_south="g", + style_magnetization_color_transition=0, +) + +# Create a third magnet with different style +cyl = magpy.magnet.CylinderSegment( + polarization=(1, 0, 0), + dimension=(1.7, 2, .3, -145, -35), +) +cyl.style.magnetization.color.north = "cornflowerblue" +cyl.style.magnetization.color.south = "orange" + +# Show all three +magpy.show(sphere, cube, cyl, backend='plotly', style_legend_show=False) +``` + +More information about styles and how to apply them is given in the user-guide [style section](guide-graphic-styles). diff --git a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md b/docs/_pages/user_guide/examples/examples_vis_mpl_streamplot.md similarity index 74% rename from docs/_pages/gallery/gallery_vis_mpl_streamplot.md rename to docs/_pages/user_guide/examples/examples_vis_mpl_streamplot.md index 2cb5d3d02..223a214a6 100644 --- a/docs/_pages/gallery/gallery_vis_mpl_streamplot.md +++ b/docs/_pages/user_guide/examples/examples_vis_mpl_streamplot.md @@ -12,7 +12,7 @@ kernelspec: orphan: true --- -(gallery-vis-mpl-streamplot)= +(examples-vis-mpl-streamplot)= # Matplotlib Streamplot @@ -20,7 +20,7 @@ orphan: true In this example we show the B-field of a cuboid magnet using Matplotlib streamlines. Streamlines are not magnetic field lines in the sense that the field amplitude cannot be derived from their density. However, Matplotlib streamlines can show the field amplitude via color and line thickness. One must be careful that streamlines can only display two components of the field. In the following example the third field component is always zero - but this is generally not the case. -In the example we make use of the [scaling property](docu-api-scale-invariance). We assume that all length inputs are in units of mm, and that the polarization input is in units of millitesla. The resulting `getB` output will also be in millitesla. One must be careful with scaling - the conversion to H would ofc give units of mA/m. +In the example we make use of the [scaling property](guide-docs-io-scale-invariance). We assume that all length inputs are in units of mm, and that the polarization input is in units of millitesla. The resulting `getB` output will also be in millitesla. One must be careful with scaling - the conversion to H would ofc give units of mA/m. ```{code-cell} ipython3 import matplotlib.pyplot as plt @@ -34,19 +34,17 @@ fig, ax = plt.subplots() # Create an observer grid in the xz-symmetry plane ts = np.linspace(-5, 5, 40) grid = np.array([[(x, 0, z) for x in ts] for z in ts]) +X, _, Z = np.moveaxis(grid, 2, 0) # Compute the B-field of a cube magnet on the grid cube = magpy.magnet.Cuboid(polarization=(500,0,500), dimension=(2,2,2)) B = cube.getB(grid) +Bx, _, Bz = np.moveaxis(B, 2, 0) log10_norm_B = np.log10(np.linalg.norm(B, axis=2)) # Display the B-field with streamplot using log10-scaled # color function and linewidth -splt = ax.streamplot( - grid[:, :, 0], - grid[:, :, 2], - B[:, :, 0], - B[:, :, 2], +splt = ax.streamplot(X, Z, Bx, Bz, density=1.5, color=log10_norm_B, linewidth=log10_norm_B, @@ -78,7 +76,7 @@ plt.show() ``` ```{note} -Be aware that the above code is not very performant, but quite readable. The following example creates the grid with numpy commands only instead of Python loops, and uses the {ref}`gallery-tutorial-field-computation-functional-interface` for field computation. +Be aware that the above code is not very performant, but quite readable. The following example creates the grid with NumPy commands only instead of Python loops and uses the {ref}`examples-tutorial-field-computation-functional-interface` for field computation. ``` ## Example 2 - Hollow Cylinder Magnet @@ -95,8 +93,8 @@ import magpylib as magpy fig, ax = plt.subplots() # Create an observer grid in the xy-symmetry plane - using pure numpy -X, Y = np.mgrid[-0.05:0.05:100j, -0.05:0.05:100j].transpose((0, 2, 1)) -grid = np.stack([X, Y, np.zeros((100, 100))], axis=2) +grid = np.mgrid[-.05:.05:100j, -.05:.05:100j, 0:0:1j].T[0] +X, Y, _ = np.moveaxis(grid, 2, 0) # Compute magnetic field on grid - using the functional interface B = magpy.getB( @@ -106,30 +104,15 @@ B = magpy.getB( polarization=(0.1, 0, 0), ) B = B.reshape(grid.shape) +Bx, By, _ = np.moveaxis(B, 2, 0) normB = np.linalg.norm(B, axis=2) -# combine streamplot with contourf -cp = ax.contourf( - X, - Y, - normB*1000, #T->mT - cmap="rainbow", - levels=100, - zorder=1, -) -splt = ax.streamplot( - X, - Y, - B[:, :, 0], - B[:, :, 1], - color="k", - density=1.5, - linewidth=1, - zorder=3, -) +# Combine streamplot with contourf +cp = ax.contourf(X, Y, normB, cmap="rainbow", levels=100) +splt = ax.streamplot(X, Y, Bx, By, color="k", density=1.5, linewidth=1) # Add colorbar -fig.colorbar(cp, ax=ax, label="|B| (mT)") +fig.colorbar(cp, ax=ax, label="|B| (T)") # Outline magnet boundary ts = np.linspace(0, 2 * np.pi, 50) diff --git a/docs/_pages/gallery/gallery_vis_pv_streamlines.md b/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md similarity index 88% rename from docs/_pages/gallery/gallery_vis_pv_streamlines.md rename to docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md index 1b772b2ea..8fc2fd30a 100644 --- a/docs/_pages/gallery/gallery_vis_pv_streamlines.md +++ b/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md @@ -12,9 +12,9 @@ kernelspec: orphan: true --- -(gallery-vis-pv-streamlines)= +(examples-vis-pv-streamlines)= -# Field lines with Pyvista streamlines +# Pyvista 3D field lines Pyvista offers field-line computation and visualization in 3D. In addition to the field computation, Magpylib offers magnet visualization that seamlessly integrates into a Pyvista plotting scene. @@ -48,8 +48,8 @@ strl = grid.streamlines_from_source( # Create a Pyvista plotting scene pl = pv.Plotter() -# Add magnet to scene -magpy.show(magnet, canvas=pl, backend="pyvista") +# Add magnet to scene - streamlines units are assumed to be meters +magpy.show(magnet, canvas=pl, units_length="m", backend="pyvista") # Prepare legend parameters legend_args = { diff --git a/docs/_pages/user_guide/examples/examples_vis_subplots.md b/docs/_pages/user_guide/examples/examples_vis_subplots.md new file mode 100644 index 000000000..9f926ed03 --- /dev/null +++ b/docs/_pages/user_guide/examples/examples_vis_subplots.md @@ -0,0 +1,99 @@ +--- +orphan: true +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.14.5 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(examples-vis-subplots)= + +# Subplots + +It is very illustrative to combine 2D and 3D subplots when viewing the field along paths. Consider the following system of a sensor and a magnet, both endowed with paths. + +```{code-cell} ipython3 +import numpy as np +import magpylib as magpy + +# Define sensor with path +cyl = magpy.magnet.Cylinder( + polarization=(1, 0, 0), + dimension=(2, 1), + position=(4,0,0), +) +cyl.rotate_from_angax(angle=np.linspace(0, 300, 40), start=0, axis="z", anchor=0) + +# Define magnet with path +sens = magpy.Sensor( + pixel=[(-.2, 0, 0), (.2, 0, 0)], + position = np.linspace((0, 0, -3), (0, 0, 3), 40) +) +``` + +In the following, we demonstrate various ways how to generate 2D/3D subplot combinations for this system. + +# Plotting canvas with own figure + +Customization is best done by adding the [Magpylib 3D-model](guide-graphics-show) to your own figure using the `canvas` kwarg. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +import matplotlib.pyplot as plt +fig = plt.figure() +ax1 = fig.add_subplot(121) +ax2 = fig.add_subplot(122, projection='3d') + +# Show pixel1 field on ax1 +B = sens.getB(cyl) +ax1.plot(B[:,0]) + +# Place 3D plot on ax2 +magpy.show(sens, cyl, canvas=ax2) + +plt.show() +``` + +How to add and modify subplots in all three supported backends is demonstrated in the [canvas documentation](guide-graphics-canvas). It is also possible to customize the Magpylib 3D output by returning and editing the respective canvas using the `return_fig` kwarg, see [return figures](guide-graphics-return_fig). + + +# Built-in subplots + +For maximal efficiency, Magpylib offers auto-generated subplots of 3D models and the field along paths by providing the `show` function with proper input dictionaries. + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +magpy.show( + {"objects": [cyl, sens], "output": "Bx", "col": 1}, + {"objects": [cyl, sens], "output": "model3d", "col": 2}, + backend='plotly', +) +``` + +Detailed information on built-in subplots is provided in the [user guide](guide-graphics-subplots). + +# show_context + +With a built-in context manager this functionality can be accessed with maximal ease + +```{code-cell} ipython3 +# Continuation from above - ensure previous code is executed + +with magpy.show_context([cyl, sens], backend='plotly') as sc: + sc.show(output="Bx", col=1, row=1) + sc.show(output="By", col=1, row=2) + sc.show(output="Bz", col=2, row=1) + sc.show(output="model3d", col=2, row=2) +``` + +```{hint} +A very powerful subplot-feature are the built-in [animated subplots](examples-vis-animated-subplots). +``` diff --git a/docs/_pages/user_guide/examples/logo.stl b/docs/_pages/user_guide/examples/logo.stl new file mode 100644 index 000000000..a9905648c Binary files /dev/null and b/docs/_pages/user_guide/examples/logo.stl differ diff --git a/docs/_pages/user_guide/guide_index.md b/docs/_pages/user_guide/guide_index.md new file mode 100644 index 000000000..a63482e3d --- /dev/null +++ b/docs/_pages/user_guide/guide_index.md @@ -0,0 +1,30 @@ +(guide-index)= +# User Guide + +The user guide provides detailed descriptions of the Magpylib API with many interactive examples to help users along the way. + +```{toctree} +:maxdepth: 2 +:caption: Getting Started +guide_start_01_install.md +guide_start_02_fundamentals.md +``` + +```{toctree} +:maxdepth: 2 +:caption: Documentation +docs/docs_units_types.md +docs/docs_classes.md +docs/docs_pos_ori.md +docs/docs_fieldcomp.md +docs/docs_graphics.md +docs/docs_styles.md +docs/docs_magpylib_force.md +``` + +```{toctree} +:maxdepth: 2 +:caption: Resources +guide_resources_01_physics.md +examples/examples_index.md +``` diff --git a/docs/_pages/docu/docu_physics.md b/docs/_pages/user_guide/guide_resources_01_physics.md similarity index 61% rename from docs/_pages/docu/docu_physics.md rename to docs/_pages/user_guide/guide_resources_01_physics.md index 483defaf6..9c0b2821a 100644 --- a/docs/_pages/docu/docu_physics.md +++ b/docs/_pages/user_guide/guide_resources_01_physics.md @@ -1,32 +1,18 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.14.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(docu-physics)= - -# Physics +(guide-ressources-physics)= +# Physics and Computation ## What is implemented ? -The expressions used in Magpylib describe perfectly homogeneous magnets, surface charges, and line currents with natural boundary conditions. Magpylib is at its best when dealing with static air-coils (no eddy currents, no soft-magnetic cores) and high grade permanent magnets (Ferrite, NdFeB, SmCo or similar materials). When **magnet** permeabilities are below $\mu_r < 1.1$ the error typically undercuts few %. Demagnetization factors are not included. The line **current** solutions give the exact same field as outside of a wire that carries a homogeneous current. +The expressions used in Magpylib describe perfectly homogeneous magnets, surface charges, and line currents with natural boundary conditions. Magpylib is at its best when dealing with static air-coils (no eddy currents, no soft-magnetic cores) and high-grade permanent magnets (Ferrite, NdFeB, SmCo or similar materials). When **magnet** permeabilities are below $\mu_r < 1.1$ the error typically undercuts few %. Demagnetization factors are not included. The line **current** solutions give the exact same field as outside of a wire that carries a homogeneous current. ## The analytical solutions Magnetic field computations in Magpylib are based on known analytical solutions (closed form expressions) to permanent magnet and current problems. The Magpylib implementations are based on the following literature references: -- Field of cuboid magnets: \[Yang1999, Engel-Herbert2005, Camacho2013, Cichon2019\] -- Field of cylindrical magnets: \[Furlani1994, Derby2009, Caciagli2018, Slanovc2021\] -- Field of triangular surfaces: \[Guptasarma1999, Janssen2009, Rubeck2013\] -- Field of the current loop: \[Ortner2022\] +- Field of cuboid magnets [^1] [^2] [^3] [^4] +- Field of cylindrical magnets: [^5] [^6] [^7] [^8] +- Field of triangular surfaces: [^9] [^10] [^11] +- Field of the current loop: [^12] - all others derived by hand A short reflection on how these formulas can be achieved: In magnetostatics the magnetic field becomes conservative (Maxwell: $\nabla \times {\bf H} = 0$) and can thus be expressed through the magnetic scalar potential $\Phi_m$: @@ -41,7 +27,7 @@ $$ \Phi_m({\bf r}) = \frac{1}{4\pi}\int_{V'}\frac{\nabla'\cdot {\bf M}({\bf r}')}{|{\bf r}-{\bf r}'|}dV'+\frac{1}{4\pi}\oint_{S'}\frac{{\bf n}'\cdot {\bf M}({\bf r}')}{|{\bf r}-{\bf r}'|}dS' $$ -where ${\bf r}$ denotes the position, $V$ is the magnetized volume with surface $S$ and normal vector ${\bf n}$ onto the surface. This solution is derived in detail e.g. in \[Jackson1999\]. +where ${\bf r}$ denotes the position, $V$ is the magnetized volume with surface $S$ and normal vector ${\bf n}$ onto the surface. This solution is derived in detail e.g. in [^13]. The fields of currents are directly derived using the law of Biot-Savart with the current distribution ${\bf J}({\bf r})$: @@ -49,7 +35,7 @@ $$ {\bf B}({\bf r}) = \frac{\mu_0}{4\pi}\int_{V'} {\bf J}({\bf r}')\times \frac{{\bf r}-{\bf r}'}{|{\bf r}-{\bf r}'|^3} dV' $$ -In some special cases (simple shapes, homogeneous magnetizations and current distributions) the above integrals can be worked out directly to give analytical formulas (or simple, fast converging series). The derivations can be found in the respective references. A noteworthy comparison between the Coulombian approach and the Amperian current model is given in \[Ravaud2009\]. +In some special cases (simple shapes, homogeneous magnetisations, and current distributions) the above integrals can be worked out directly to give analytical formulas (or simple, fast converging series). The derivations can be found in the respective references. A noteworthy comparison between the Coulombian approach and the Amperian current model is given in [^14]. ## Accuracy of the Solutions and Demagnetization @@ -57,11 +43,12 @@ In some special cases (simple shapes, homogeneous magnetizations and current dis The magnetic field of a wire carrying a homogeneous current density is (on the outside) similar to the field of a line current positioned in the center of the wire, which carries the same total current. Current distributions become inhomogeneous at bends of the wire or when eddy currents (finite frequencies) are involved. +(guide-physics-demag)= ### Magnets and Demagnetization The analytical solutions are exact when bodies have a homogeneous magnetization. However, real materials always have a material response which results in an inhomogeneous magnetization even when the initial magnetization is perfectly homogeneous. There is a lot of literature on such [demagnetization effects](https://en.wikipedia.org/wiki/Demagnetizing_field). -Modern high grade permanent magnets (NdFeB, SmCo, Ferrite) have a very weak material responses (local slope of the magnetization curve, remanent permeability) of the order of $\mu_r \approx 1.05$. In this case the analytical solutions provide an excellent approximation with less than 1% error even at close distance from the magnet surface. A detailed error analysis and discussion is presented in the appendix of \[Malago2020\]. +Modern high grade permanent magnets (NdFeB, SmCo, Ferrite) have a very weak material responses (local slope of the magnetization curve, remanent permeability) of the order of $\mu_r \approx 1.05$. In this case the analytical solutions provide an excellent approximation with less than 1% error even at close distance from the magnet surface. A detailed error analysis and discussion is presented in the appendix of [^15]. Error estimation as a result of the material response is evaluated in more detail in the appendix of [Malagรฒ2020](https://www.mdpi.com/1424-8220/20/23/6873). @@ -69,13 +56,13 @@ Demagnetization factors can be used to compensate a large part of the demagnetiz (phys-remanence)= -### Modelling a datasheet magnet +### Modeling a datasheet magnet The material remanence, often found in data sheets, simply corresponds to the material magnetization/polarization when not under the influence of external fields. This can never happen, as the material itself generates a magnetic field. Such self-interactions result in self-demagnetization that can be approximated using the demagnetization factors and the material permeability (or susceptibility). For example, a cube with 1 mm sides has a demagnetization factor is 0.333, see [magpar.net](http://www.magpar.net/static/magpar/doc/html/demagcalc.html). When the remanence field of this cube is 1 T, and its susceptibility is 0.1, the magnetization resulting from self-interaction is reduced to 1 T - 0.3333*0.1 T = 0.9667 T, assuming linear material laws. -A [tutorial](gallery-tutorial-modelling-magnets) explains how to deal with demagnetization effects and how real magnets can be modeled using datasheet values. +A [tutorial](examples-tutorial-modeling-magnets) explains how to deal with demagnetization effects and how real magnets can be modeled using datasheet values. It must be understood that the change in magnetization resulting from self-interaction has a homogeneous contribution which is approximated by the demagnetization factor, and an inhomogeneous contribution which cannot be modeled easily with analytical solutions. The inhomogeneous part, however, is typically an order of magnitude lower than the homogenous part. You can use the Magpylib extension [Magpylib material response](https://github.com/magpylib/magpylib-material-response) to model the self-interactions. @@ -87,7 +74,7 @@ When a magnet lies in front of a soft-magnetic plate, the contribution from the (docu-performance)= -## Computation and Performance +## Computation Performance Magpylib code is fully [vectorized](https://en.wikipedia.org/wiki/Array_programming), written almost completely in numpy native. Magpylib automatically vectorizes the computation with complex inputs (many sources, many observers, paths) and never falls back on using loops. @@ -95,28 +82,42 @@ Magpylib code is fully [vectorized](https://en.wikipedia.org/wiki/Array_programm Maximal performance is achieved when `.getB(sources, observers)` is called only a single time in your program. Try not to use loops. ``` -The object oriented interface comes with an overhead. If you want to achieve maximal performance this overhead can be avoided with {ref}`docu-functional-interface`. +The object-oriented interface comes with an overhead. If you want to achieve maximal performance this overhead can be avoided with {ref}`docs-field-functional`. The analytical solutions provide extreme performance. Single field evaluations take of the order of `100 ยตs`. For large input arrays (e.g. many observer positions or many similar magnets) the computation time can drop below `1 ยตs` per evaluation point on single state-of-the-art x86 mobile cores (tested on `Intel Core i5-8365U @ 1.60GHz`), depending on the source type. ## Numerical stability -Many expressions provided in the literature have very questionable numerical stability. Many of these problems are fixed in Magpylib, but one should be aware that accuracy can be a problem very close to objects, close the z-axis in cylindrical symmetries, at edge extensions, and at large distances. We are working on fixing these problems. Some details can be found in \[Ortner2022\]. +Many expressions provided in the literature have very questionable numerical stability. Many of these problems are fixed in Magpylib, but one should be aware that accuracy can be a problem very close to objects, close the z-axis in cylindrical symmetries, at edge extensions, and at large distances. We are working on fixing these problems. Some details can be found in [^12]. **References** -- \[Yang1999\] Z. J. Yang et al., "Potential and force between a magnet and a bulk Y1Ba2Cu3O7-d superconductor studied by a mechanical pendulum", Superconductor Science and Technology 3(12):591, 1999 -- \[Engel-Herbert2005\] R. Engel-Herbert et al., Journal of Applied Physics 97(7):074504 - 074504-4 (2005) -- \[Camacho2013\] J.M. Camacho and V. Sosa, "Alternative method to calculate the magnetic field of permanent magnets with azimuthal symmetry", Revista Mexicana de Fisica E 59 8โ17, 2013 -- \[Cichon2019\] D. Cichon, R. Psiuk and H. Brauer, "A Hall-Sensor-Based Localization Method With Six Degrees of Freedom Using Unscented Kalman Filter", IEEE Sensors Journal, Vol. 19, No. 7, April 1, 2019. -- \[Furlani1994\] E. P. Furlani, S. Reanik and W. Janson, "A Three-Dimensional Field Solution for Bipolar Cylinders", IEEE Transaction on Magnetics, VOL. 30, NO. 5, 1994 -- \[Derby2009\] N. Derby, "Cylindrical Magnets and Ideal Solenoids", arXiv:0909.3880v1, 2009 -- \[Caciagli2018\] A. Caciagli, R. J. Baars, A. P. Philipse and B. W. M. Kuipers, "Exact expression for the magnetic field of a finite cylinder with arbitrary uniform magnetization", Journal of Magnetism and Magnetic Materials 456 (2018) 423โ432. -- \[Slanovc2022\] F. Slanovc, M. Ortner, M. Moridi, C. Abert and D. Suess, "Full analytical solution for the magnetic field of uniformly magnetized cylinder tiles", submitted to Journal of Magnetism and Magnetic Materials. -- \[Ortner2022\] M. Ortner, S. Slanovc and P. Leitner, "Numerically Stable and Computationally Efficient Expression for the Magnetic Field of a Current Loop", MDPI Magnetism, 3(1), 11-31, 2022. -- \[Guptasarma1911\] D. Guptasarma and B. Singh, "New scheme for computing the magnetic field resulting from a uniformly magnetized arbitrary polyhedron", Geophysics (1999), 64(1):70. -- \[Janssen2009\] J.L.G. Janssen, J.J.H. Paulides and E.A. Lomonova, "3D ANALYTICAL FIELD CALCULATION USING TRIANGULAR MAGNET SEGMENTS APPLIED TO A SKEWED LINEAR PERMANENT MAGNET ACTUATOR", ISEF 2009 - XIV International Symposium on Electromagnetic Fields in Mechatronics, Electrical and Electronic Engineering Arras, France, September 10-12, 2009 -- \[Rubeck2013\] C. Rubeck et al., "Analytical Calculation of Magnet Systems: Magnetic Field Created by Charged Triangles and Polyhedra", IEEE Transactions on Magnetics, VOL. 49, NO. 1, 2013 -- \[Jackson1999\] J. D. Jackson, "Classical Electrodynamics", 1999 Wiley, New York -- \[Ravaud2009\] R. Ravaud and G. Lamarquand, "Comparison of the coulombian and amperian current models for calculating the magnetic field produced by radially magnetized arc-shaped permanent magnets", HAL Id: hal-00412346 -- \[Malago2020\] P. Malagรฒ et al., Magnetic Position System Design Method Applied to Three-Axis Joystick Motion Tracking. Sensors, 2020, 20. Jg., Nr. 23, S. 6873. +[^1]: Z. J. Yang et al., "Potential and force between a magnet and a bulk Y1Ba2Cu3O7-d superconductor studied by a mechanical pendulum", Superconductor Science and Technology 3(12):591, 1990 + +[^2]: R. Engel-Herbert et al., Journal of Applied Physics 97(7):074504 - 074504-4 (2005) + +[^3]: J.M. Camacho and V. Sosa, "Alternative method to calculate the magnetic field of permanent magnets with azimuthal symmetry", Revista Mexicana de Fisica E 59 8โ17, 2013 + +[^4]: D. Cichon, R. Psiuk and H. Brauer, "A Hall-Sensor-Based Localization Method With Six Degrees of Freedom Using Unscented Kalman Filter", IEEE Sensors Journal, Vol. 19, No. 7, April 1, 2019. + +[^5]: E. P. Furlani, S. Reanik and W. Janson, "A Three-Dimensional Field Solution for Bipolar Cylinders", IEEE Transaction on Magnetics, VOL. 30, NO. 5, 1994 + +[^6]: N. Derby, "Cylindrical Magnets and Ideal Solenoids", arXiv:0909.3880v1, 2009 + +[^7]: A. Caciagli, R. J. Baars, A. P. Philipse and B. W. M. Kuipers, "Exact expression for the magnetic field of a finite cylinder with arbitrary uniform magnetization", Journal of Magnetism and Magnetic Materials 456 (2018) 423โ432. + +[^8]: F. Slanovc, M. Ortner, M. Moridi, C. Abert and D. Suess, "Full analytical solution for the magnetic field of uniformly magnetized cylinder tiles", submitted to Journal of Magnetism and Magnetic Materials. + +[^9]: D. Guptasarma and B. Singh, "New scheme for computing the magnetic field resulting from a uniformly magnetized arbitrary polyhedron", Geophysics (1999), 64(1):70. + +[^10]: J.L.G. Janssen, J.J.H. Paulides and E.A. Lomonova, "3D ANALYTICAL FIELD CALCULATION USING TRIANGULAR MAGNET SEGMENTS APPLIED TO A SKEWED LINEAR PERMANENT MAGNET ACTUATOR", ISEF 2009 - XIV International Symposium on Electromagnetic Fields in Mechatronics, Electrical and Electronic Engineering Arras, France, September 10-12, 2009 + +[^11]: C. Rubeck et al., "Analytical Calculation of Magnet Systems: Magnetic Field Created by Charged Triangles and Polyhedra", IEEE Transactions on Magnetics, VOL. 49, NO. 1, 2013 + +[^12]: M. Ortner, S. Slanovc and P. Leitner, "Numerically Stable and Computationally Efficient Expression for the Magnetic Field of a Current Loop", MDPI Magnetism, 3(1), 11-31, 2022. + +[^13]: J. D. Jackson, "Classical Electrodynamics", 1999 Wiley, New York + +[^14]: R. Ravaud and G. Lamarquand, "Comparison of the coulombian and amperian current models for calculating the magnetic field produced by radially magnetized arc-shaped permanent magnets", HAL Id: hal-00412346 + +[^15]: P. Malagรฒ et al., Magnetic Position System Design Method Applied to Three-Axis Joystick Motion Tracking. Sensors, 2020, 20. Jg., Nr. 23, S. 6873. diff --git a/docs/_pages/user_guide/guide_start_01_install.md b/docs/_pages/user_guide/guide_start_01_install.md new file mode 100644 index 000000000..e8f1473e8 --- /dev/null +++ b/docs/_pages/user_guide/guide_start_01_install.md @@ -0,0 +1,29 @@ +# Installation and Dependencies + +## Dependencies and Synergies + +Magpylib supports *Python3.10+* and relies on common scientific computation libraries *NumPy* and *Scipy* for computation, and *Matplotlib* and *Plotly* for graphic outputs. + +Optionally, *Pyvista* is supported as graphical backend, as well as *Pandas* for the field computation output. + +## Installation + +::::{grid} 1 1 2 2 +:margin: 4 4 0 0 +:gutter: 4 + +:::{grid-item-card} Install with pip: +:text-align: center +:shadow: none +```console +pip install magpylib +``` +::: +:::{grid-item-card} Install with conda: +:text-align: center +:shadow: none +```console +conda install -c conda-forge magpylib +``` +::: +:::: diff --git a/docs/_pages/reso_get_started.md b/docs/_pages/user_guide/guide_start_02_fundamentals.md similarity index 57% rename from docs/_pages/reso_get_started.md rename to docs/_pages/user_guide/guide_start_02_fundamentals.md index b0fc5f2db..a8f18ca35 100644 --- a/docs/_pages/reso_get_started.md +++ b/docs/_pages/user_guide/guide_start_02_fundamentals.md @@ -1,49 +1,19 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.0 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -(get-started)= - -# Get Started - -## Installation and Dependencies - -Magpylib supports *Python3.8+* and relies on common scientific computation libraries *Numpy*, *Scipy*, *Matplotlib* and *Plotly*. Optionally, *Pyvista* is recommended as graphical backend. - -::::{grid} 1 1 2 2 -:margin: 4 4 0 0 -:gutter: 4 - -:::{grid-item-card} Install with pip: -:text-align: center -:shadow: none -```console -pip install magpylib -``` -::: -:::{grid-item-card} Install with conda: -:text-align: center -:shadow: none -```console -conda install -c conda-forge magpylib -``` -::: -:::: +(getting-started)= +# The Magpylib fundamentals + +In this section we present the most important Magpylib features, focussing on the intuitive object-oriented interface. -## Magpylib fundamentals +## Basic features + +Learn the Magpylib fundamentals (create magnets, view system, compute field) in 5 minutes. This requires a basic understanding of the Python programming language, the [NumPy array class](https://numpy.org/doc/stable/) and the [Scipy Rotation class](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html). + +```{hint} +Since v5 all Magpylib inputs and ouputs are by default in SI-units. See {ref}`guide-docs-io-scale-invariance` for convenient use. +``` -Learn the Magpylib fundamentals in 5 minutes. This requires a basic understanding of the Python programming language, the [Numpy array class](https://numpy.org/doc/stable/) and the [Scipy Rotation class](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html). +### Create sources and observers as Python objects -### Step 1: Create sources and observers as Python objects +In the object oriented interface sources of the magnetic field (magnets, currents, others) and observers of the magnetic field (sensors) are created as Python objects. ```python import magpylib as magpy @@ -59,7 +29,11 @@ cube = magpy.magnet.Cuboid(polarization=(1,0,0), dimension=(0.01,0.02,0.03)) sensor = magpy.Sensor() ``` -### Step2: Manipulate object position and orientation +Find detailed information on the Magpylib classes [here](docs-classes). + +### Position and orientation + +All Magpylib objects (sources and observers) have position and orientation in a global Cartesian coordinate system that can be manipulated. ```python # By default, the position of a Magpylib object is @@ -88,7 +62,11 @@ print(sensor.position) # -> [-0.01 0. 0. ] print(sensor.orientation.as_rotvec(degrees=True)) # -> [ 0. 0. -45.] ``` -### Step 3: View your system +Find detailed information on position and orientation attributes and how to manipulate them [here](docs-position). + +### 3D view of objects + +In-built 3D graphic output helps to see if all Magpylib objects are positioned properly. The magnet polarization is represented by default by a 3-color scheme, the sensor by an axes cross. ```python # Use the `show` function to view your system @@ -97,35 +75,44 @@ print(sensor.orientation.as_rotvec(degrees=True)) # -> [ 0. 0. -45.] magpy.show(cube, sensor, backend='plotly') ``` -<img src="/_static/images/getting_started_fundamentals1.png" width=50% align="center"> +<img src="../../_static/images/getting_started_fundamentals1.png" width=50% align="center"> + +Detailed information on the graphical output with `show` is given [here](guide-graphics). -### Step 4: Computing the field +### Computing the field + +The field can be computed at sensor objects, or simply by specifying a position of interest. ```python -# Compute the B-field in units of T for some points. +# Compute the B-field for some positions. points = [(0,0,-.01), (0,0,0), (0,0,.01)] # in SI Units (m) B = magpy.getB(cube, points) print(B.round(2)) # -> [[ 0.26 0.07 0.08] - # [ 0.28 0.05 0. ] - # [ 0.26 0.07 -0.08]] # in SI Units (T) + # [ 0.28 0.05 0. ] + # [ 0.26 0.07 -0.08]] # in SI Units (T) -# Compute the H-field in units of A/m at the sensor. +# Compute the H-field at the sensor. H = magpy.getH(cube, sensor) print(H.round()) # -> [51017. 24210. 0.] # in SI Units (A/m) ``` -```{warning} +```{hint} Magpylib makes use of vectorized computation (massive speedup). This requires that you hand over all field computation instances (multiple objects with multiple positions (=paths)) at the same time to `getB`, `getH`, `getJ` and `getM`. Avoid Python loops at all costs !!! ``` -## Other important features +Detailed information on field computation is provided [here](docs-fieldcomp). + +## Advanced features + +While most things can be achieved with the above, the following features will make your live much easier. -:::{dropdown} Paths -Magpylib position and orientation attributes can store multiple values that are referred to as paths. The field will automatically be computed for all path positions. Use this to model objects that move to multiple locations. +### Paths + +Magpylib position and orientation attributes can store multiple values that are referred to as paths. The field will automatically be computed for all path positions. Use this feature to model objects that move to multiple locations. ```python import numpy as np @@ -150,12 +137,11 @@ print(B.round(3)) # ->[[ 0.004 0. -0.001] # [-0.013 0. 0.001] # [-0.004 0. -0.001]] ``` -::: - +More information on paths is provided [here](docs-position). -:::{dropdown} Collections -Magpylib objects can be grouped into Collections. An operation applied to a Collection is applied to every object in it. +### Collections +Magpylib objects can be grouped into Collections. An operation applied to a Collection is applied to every object in it. The Collection itself behaves like a single source object. ```python import magpylib as magpy @@ -175,12 +161,11 @@ coll.move((.001,.002,.003)) print(obj1.position) # -> [0.001 0.002 0.003] print(obj2.position) # -> [0.001 0.002 0.003] ``` -::: +Collections are dicussed in detail [here](guide-docs-classes-collections). - -:::{dropdown} Complex Magnet Shapes -There most convenient way to create a magnet with complex shape is by using the convex hull of a point cloud (= simplest form that includes all given points) and transform it into a triangular surface mesh. +### Complex Magnet Shapes +There most convenient way to create a magnet with complex shape is by using the convex hull of a point cloud (= simplest geometric form that includes all given points) and transform it into a triangular surface mesh. ```python import numpy as np @@ -207,19 +192,18 @@ pyramid = magpy.magnet.TriangularMesh.from_ConvexHull( # Display the magnet graphically pyramid.show() ``` -<img src="../_static/images/getting_started_complex_shapes.png" width=50% align="center"> +<img src="../../_static/images/getting_started_complex_shapes.png" width=50% align="center"> -However, there are several other possibilities to create complex magnet shapes. Some can be found in the [gallery](gallery). -::: +There are several other possibilities to create complex magnet shapes. Some can be found in the [examples](examples-complex-magnet-shapes). -:::{dropdown} Graphic Styles +### Graphic Styles Magpylib offers many ways to customize the graphic output. ```python import magpylib as magpy -# create Cuboid magnet with custom style +# Create Cuboid magnet with custom style cube = magpy.magnet.Cuboid( polarization=(0,0,1), dimension=(.01,.01,.01), @@ -227,7 +211,7 @@ cube = magpy.magnet.Cuboid( style_magnetization_mode='arrow' ) -# create Cylinder magnet with custom style +# Create Cylinder magnet with custom style cyl = magpy.magnet.Cylinder( polarization=(0,0,1), dimension=(.01,.01), @@ -238,13 +222,12 @@ cyl = magpy.magnet.Cylinder( ) magpy.show(cube, cyl) ``` -<img src="../_static/images/getting_started_styles.png" width=50% align="center"> -::: - +<img src="../../_static/images/getting_started_styles.png" width=50% align="center"> +The many options for graphic styling can be found [here](guide-graphic-styles). -:::{dropdown} Animation -Object paths can be animated +### Animation +Object paths can be animated. For this feature the plotly graphic backend is recommended. ```python import numpy as np @@ -264,13 +247,12 @@ cube.rotate_from_angax( # Generate an animation with `show` cube.show(animation=True, backend="plotly") ``` -<img src="../_static/images/getting_started_animation.png" width=50% align="center"> -::: - +<img src="../../_static/images/getting_started_animation.png" width=50% align="center"> +Nice animation examples are shown [here](examples-vis-animations), and a detailed discussion is provided [here](guide-graphic-animations). -:::{dropdown} Functional interface -Magpylib's object oriented interface is convenient to work with but is also slowed down by object initialization and handling. The functional interface bypasses this load and enables field computation for a set of input parameters. +### Functional interface +Magpylib's object oriented interface is convenient to work with but is also slowed down by object initialization and handling. The functional interface bypasses this load and enables fast field computation for an arbitrary set of input parameters. ```python import magpylib as magpy @@ -287,8 +269,5 @@ print(B.round(3)) # -> [[-0.043 0. 0.014] # [ 0. 0. 0.135] # [ 0.043 0. 0.014]] ``` -::: -```{code-cell} ipython3 - -``` +Details on the functional interface are found [here](docs-field-functional). diff --git a/docs/_static/custom.css b/docs/_static/custom.css index d7acfdc57..d699c9c38 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -11,3 +11,29 @@ html[data-theme=dark] .bd-content img:not(.only-dark):not(.dark-light) { .sectiontext { margin-top:-40px; } + + +/* +hide vertical scrollbar - it always appears unwanted when there +is no content +*/ + +.bd-sidebar { + overflow-y: auto; /* Change to auto to only show scrollbar when necessary */ +} + + + +/* Adjust the height to ensure it fits within the viewport */ +/* .bd-sidebar { + max-height: calc(100vh - var(--header-height, 0px)); +} */ + + +/* +Target the logo text +*/ +.navbar-brand { + font-weight: bold; + font-size: 1.6em; +} diff --git a/docs/_static/images/docu_index_icon_magpylib.png b/docs/_static/images/docu_index_icon_magpylib.png deleted file mode 100644 index d6fbc95a2..000000000 Binary files a/docs/_static/images/docu_index_icon_magpylib.png and /dev/null differ diff --git a/docs/_static/images/docu_index_icon_magpylib_show.png b/docs/_static/images/docu_index_icon_magpylib_show.png deleted file mode 100644 index 9e75b3885..000000000 Binary files a/docs/_static/images/docu_index_icon_magpylib_show.png and /dev/null differ diff --git a/docs/_static/images/docu_index_icon_physics.png b/docs/_static/images/docu_index_icon_physics.png deleted file mode 100644 index 363c9723e..000000000 Binary files a/docs/_static/images/docu_index_icon_physics.png and /dev/null differ diff --git a/docs/_static/images/examples_force_floating_coil-magnet.png b/docs/_static/images/examples_force_floating_coil-magnet.png new file mode 100644 index 000000000..9ed8a1e56 Binary files /dev/null and b/docs/_static/images/examples_force_floating_coil-magnet.png differ diff --git a/docs/_static/images/examples_force_floating_ringdown.png b/docs/_static/images/examples_force_floating_ringdown.png new file mode 100644 index 000000000..adf89abeb Binary files /dev/null and b/docs/_static/images/examples_force_floating_ringdown.png differ diff --git a/docs/_static/images/examples_force_holding_force.png b/docs/_static/images/examples_force_holding_force.png new file mode 100644 index 000000000..e6cb65fc1 Binary files /dev/null and b/docs/_static/images/examples_force_holding_force.png differ diff --git a/docs/_static/images/gallery_icon_WIP.png b/docs/_static/images/examples_icon_WIP.png similarity index 100% rename from docs/_static/images/gallery_icon_WIP.png rename to docs/_static/images/examples_icon_WIP.png diff --git a/docs/_static/images/gallery_icon_app_end_of_shaft.png b/docs/_static/images/examples_icon_app_end_of_shaft.png similarity index 100% rename from docs/_static/images/gallery_icon_app_end_of_shaft.png rename to docs/_static/images/examples_icon_app_end_of_shaft.png diff --git a/docs/_static/images/examples_icon_app_halbach.png b/docs/_static/images/examples_icon_app_halbach.png new file mode 100644 index 000000000..ae2317c27 Binary files /dev/null and b/docs/_static/images/examples_icon_app_halbach.png differ diff --git a/docs/_static/images/gallery_icon_app_helmholtz.png b/docs/_static/images/examples_icon_app_helmholtz.png similarity index 100% rename from docs/_static/images/gallery_icon_app_helmholtz.png rename to docs/_static/images/examples_icon_app_helmholtz.png diff --git a/docs/_static/images/gallery_icon_ext_custom_quadrupole.png b/docs/_static/images/examples_icon_ext_custom_quadrupole.png similarity index 100% rename from docs/_static/images/gallery_icon_ext_custom_quadrupole.png rename to docs/_static/images/examples_icon_ext_custom_quadrupole.png diff --git a/docs/_static/images/examples_icon_force_floating.png b/docs/_static/images/examples_icon_force_floating.png new file mode 100644 index 000000000..a16405182 Binary files /dev/null and b/docs/_static/images/examples_icon_force_floating.png differ diff --git a/docs/_static/images/examples_icon_force_force.png b/docs/_static/images/examples_icon_force_force.png new file mode 100644 index 000000000..45304297a Binary files /dev/null and b/docs/_static/images/examples_icon_force_force.png differ diff --git a/docs/_static/images/examples_icon_force_holding_force.png b/docs/_static/images/examples_icon_force_holding_force.png new file mode 100644 index 000000000..ce223bbfe Binary files /dev/null and b/docs/_static/images/examples_icon_force_holding_force.png differ diff --git a/docs/_static/images/gallery_icon_misc_compound.png b/docs/_static/images/examples_icon_misc_compound.png similarity index 100% rename from docs/_static/images/gallery_icon_misc_compound.png rename to docs/_static/images/examples_icon_misc_compound.png diff --git a/docs/_static/images/gallery_icon_misc_field_interpolation.png b/docs/_static/images/examples_icon_misc_field_interpolation.png similarity index 100% rename from docs/_static/images/gallery_icon_misc_field_interpolation.png rename to docs/_static/images/examples_icon_misc_field_interpolation.png diff --git a/docs/_static/images/examples_icon_misc_inhom.png b/docs/_static/images/examples_icon_misc_inhom.png new file mode 100644 index 000000000..18c83294b Binary files /dev/null and b/docs/_static/images/examples_icon_misc_inhom.png differ diff --git a/docs/_static/images/examples_icon_shapes_cad.png b/docs/_static/images/examples_icon_shapes_cad.png new file mode 100644 index 000000000..22b85d83b Binary files /dev/null and b/docs/_static/images/examples_icon_shapes_cad.png differ diff --git a/docs/_static/images/gallery_icon_shapes_convex_hull.png b/docs/_static/images/examples_icon_shapes_convex_hull.png similarity index 100% rename from docs/_static/images/gallery_icon_shapes_convex_hull.png rename to docs/_static/images/examples_icon_shapes_convex_hull.png diff --git a/docs/_static/images/gallery_icon_shapes_pyvista.png b/docs/_static/images/examples_icon_shapes_pyvista.png similarity index 100% rename from docs/_static/images/gallery_icon_shapes_pyvista.png rename to docs/_static/images/examples_icon_shapes_pyvista.png diff --git a/docs/_static/images/gallery_icon_shapes_superpos.png b/docs/_static/images/examples_icon_shapes_superpos.png similarity index 100% rename from docs/_static/images/gallery_icon_shapes_superpos.png rename to docs/_static/images/examples_icon_shapes_superpos.png diff --git a/docs/_static/images/gallery_icon_shapes_triangle.png b/docs/_static/images/examples_icon_shapes_triangle.png similarity index 100% rename from docs/_static/images/gallery_icon_shapes_triangle.png rename to docs/_static/images/examples_icon_shapes_triangle.png diff --git a/docs/_static/images/gallery_icon_tutorial_collection.png b/docs/_static/images/examples_icon_tutorial_collection.png similarity index 100% rename from docs/_static/images/gallery_icon_tutorial_collection.png rename to docs/_static/images/examples_icon_tutorial_collection.png diff --git a/docs/_static/images/gallery_icon_tutorial_custom.png b/docs/_static/images/examples_icon_tutorial_custom.png similarity index 100% rename from docs/_static/images/gallery_icon_tutorial_custom.png rename to docs/_static/images/examples_icon_tutorial_custom.png diff --git a/docs/_static/images/gallery_icon_tutorial_field_computation.png b/docs/_static/images/examples_icon_tutorial_field_computation.png similarity index 100% rename from docs/_static/images/gallery_icon_tutorial_field_computation.png rename to docs/_static/images/examples_icon_tutorial_field_computation.png diff --git a/docs/_static/images/gallery_icon_tutorial_modelling_magnets.png b/docs/_static/images/examples_icon_tutorial_modeling_magnets.png similarity index 100% rename from docs/_static/images/gallery_icon_tutorial_modelling_magnets.png rename to docs/_static/images/examples_icon_tutorial_modeling_magnets.png diff --git a/docs/_static/images/gallery_icon_tutorial_paths.png b/docs/_static/images/examples_icon_tutorial_paths.png similarity index 100% rename from docs/_static/images/gallery_icon_tutorial_paths.png rename to docs/_static/images/examples_icon_tutorial_paths.png diff --git a/docs/_static/images/gallery_icon_viz_animations.png b/docs/_static/images/examples_icon_vis_animations.png similarity index 100% rename from docs/_static/images/gallery_icon_viz_animations.png rename to docs/_static/images/examples_icon_vis_animations.png diff --git a/docs/_static/images/examples_icon_vis_magnet_colors.png b/docs/_static/images/examples_icon_vis_magnet_colors.png new file mode 100644 index 000000000..0bcb52b45 Binary files /dev/null and b/docs/_static/images/examples_icon_vis_magnet_colors.png differ diff --git a/docs/_static/images/gallery_icon_viz_mpl_streamplot.png b/docs/_static/images/examples_icon_vis_mpl_streamplot.png similarity index 100% rename from docs/_static/images/gallery_icon_viz_mpl_streamplot.png rename to docs/_static/images/examples_icon_vis_mpl_streamplot.png diff --git a/docs/_static/images/gallery_icon_viz_pv_streamlines.png b/docs/_static/images/examples_icon_vis_pv_streamlines.png similarity index 100% rename from docs/_static/images/gallery_icon_viz_pv_streamlines.png rename to docs/_static/images/examples_icon_vis_pv_streamlines.png diff --git a/docs/_static/images/examples_icon_vis_subplots.png b/docs/_static/images/examples_icon_vis_subplots.png new file mode 100644 index 000000000..d89f896f6 Binary files /dev/null and b/docs/_static/images/examples_icon_vis_subplots.png differ diff --git a/docs/_static/images/gallery_tutorial_magnet_LDratio.png b/docs/_static/images/examples_tutorial_magnet_LDratio.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_LDratio.png rename to docs/_static/images/examples_tutorial_magnet_LDratio.png diff --git a/docs/_static/images/gallery_tutorial_magnet_datasheet.png b/docs/_static/images/examples_tutorial_magnet_datasheet.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_datasheet.png rename to docs/_static/images/examples_tutorial_magnet_datasheet.png diff --git a/docs/_static/images/gallery_tutorial_magnet_datasheet2.png b/docs/_static/images/examples_tutorial_magnet_datasheet2.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_datasheet2.png rename to docs/_static/images/examples_tutorial_magnet_datasheet2.png diff --git a/docs/_static/images/gallery_tutorial_magnet_fieldcomparison.png b/docs/_static/images/examples_tutorial_magnet_fieldcomparison.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_fieldcomparison.png rename to docs/_static/images/examples_tutorial_magnet_fieldcomparison.png diff --git a/docs/_static/images/gallery_tutorial_magnet_hysteresis.png b/docs/_static/images/examples_tutorial_magnet_hysteresis.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_hysteresis.png rename to docs/_static/images/examples_tutorial_magnet_hysteresis.png diff --git a/docs/_static/images/gallery_tutorial_magnet_table.png b/docs/_static/images/examples_tutorial_magnet_table.png similarity index 100% rename from docs/_static/images/gallery_tutorial_magnet_table.png rename to docs/_static/images/examples_tutorial_magnet_table.png diff --git a/docs/_static/images/examples_vis_magnet_colors.png b/docs/_static/images/examples_vis_magnet_colors.png new file mode 100644 index 000000000..92167a599 Binary files /dev/null and b/docs/_static/images/examples_vis_magnet_colors.png differ diff --git a/docs/_static/images/index_flowchart.png b/docs/_static/images/index_flowchart.png index 4c40eb330..99994606d 100644 Binary files a/docs/_static/images/index_flowchart.png and b/docs/_static/images/index_flowchart.png differ diff --git a/docs/_static/images/index_icon_academic.png b/docs/_static/images/index_icon_academic.png index db8fd4fdf..b8795fb3b 100644 Binary files a/docs/_static/images/index_icon_academic.png and b/docs/_static/images/index_icon_academic.png differ diff --git a/docs/_static/images/index_icon_contributing.png b/docs/_static/images/index_icon_contributing.png deleted file mode 100644 index f6323f80e..000000000 Binary files a/docs/_static/images/index_icon_contributing.png and /dev/null differ diff --git a/docs/_static/images/index_icon_docu.png b/docs/_static/images/index_icon_docu.png deleted file mode 100644 index f4a917285..000000000 Binary files a/docs/_static/images/index_icon_docu.png and /dev/null differ diff --git a/docs/_static/images/index_icon_examples.png b/docs/_static/images/index_icon_examples.png new file mode 100644 index 000000000..6fe95e556 Binary files /dev/null and b/docs/_static/images/index_icon_examples.png differ diff --git a/docs/_static/images/index_icon_gallery.png b/docs/_static/images/index_icon_gallery.png deleted file mode 100644 index 14bc9e159..000000000 Binary files a/docs/_static/images/index_icon_gallery.png and /dev/null differ diff --git a/docs/_static/images/index_icon_get_started.png b/docs/_static/images/index_icon_get_started.png new file mode 100644 index 000000000..0dc85f8dc Binary files /dev/null and b/docs/_static/images/index_icon_get_started.png differ diff --git a/docs/_static/images/index_icon_getting_started.png b/docs/_static/images/index_icon_getting_started.png deleted file mode 100644 index 227965167..000000000 Binary files a/docs/_static/images/index_icon_getting_started.png and /dev/null differ diff --git a/docs/_static/images/index_icon_github.png b/docs/_static/images/index_icon_github.png deleted file mode 100644 index bff6303d4..000000000 Binary files a/docs/_static/images/index_icon_github.png and /dev/null differ diff --git a/docs/_static/switcher.json b/docs/_static/switcher.json new file mode 100644 index 000000000..6fa25f9fd --- /dev/null +++ b/docs/_static/switcher.json @@ -0,0 +1,24 @@ +[ + { + "version": "dev", + "url": "https://magpylib.readthedocs.io/en/latest/" + }, + { + "name": "5.1.1 (stable)", + "version": "5.1.1", + "url": "https://magpylib.readthedocs.io/en/stable", + "preferred": true + }, + { + "version": "4.5.1", + "url": "https://magpylib.readthedocs.io/en/4.5.1/" + }, + { + "version": "3.0.5", + "url": "https://magpylib.readthedocs.io/en/3.0.5/" + }, + { + "version": "2.3.0-beta", + "url": "https://magpylib.readthedocs.io/en/2.3.0-beta/" + } +] \ No newline at end of file diff --git a/docs/_static/videos/axis.mp4 b/docs/_static/videos/axis.mp4 deleted file mode 100644 index 7646fca16..000000000 Binary files a/docs/_static/videos/axis.mp4 and /dev/null differ diff --git a/docs/_static/videos/example_force_floating_ringdown.gif b/docs/_static/videos/example_force_floating_ringdown.gif new file mode 100644 index 000000000..d22c4ff59 Binary files /dev/null and b/docs/_static/videos/example_force_floating_ringdown.gif differ diff --git a/docs/_static/videos/example_gif1.gif b/docs/_static/videos/example_gif1.gif new file mode 100644 index 000000000..602b74469 Binary files /dev/null and b/docs/_static/videos/example_gif1.gif differ diff --git a/docs/_static/videos/example_gif2.gif b/docs/_static/videos/example_gif2.gif new file mode 100644 index 000000000..fdfaa71e0 Binary files /dev/null and b/docs/_static/videos/example_gif2.gif differ diff --git a/docs/_static/videos/example_gif3.gif b/docs/_static/videos/example_gif3.gif new file mode 100644 index 000000000..a1c0c0651 Binary files /dev/null and b/docs/_static/videos/example_gif3.gif differ diff --git a/docs/conf.py b/docs/conf.py index b9c9879c0..88719a64e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,6 @@ os.environ["DISPLAY"] = ":99" os.environ["PYVISTA_OFF_SCREEN"] = "true" os.environ["PYVISTA_USE_IPYVTK"] = "true" - os.environ["MAGPYLIB_MPL_SVG"] = "true" # Location of Sphinx files @@ -60,7 +59,7 @@ def setup(app): # -- Project information ----------------------------------------------------- project = "Magpylib" -copyright = "2022, SAL - Silicon Austria Labs" +copyright = "2019-2024, Magpylib developers, License: BSD 2-clause, Built with Sphinx Pydata-Theme" author = "The Magpylib Project <magpylib@gmail.com>" # The short X.Y version @@ -123,34 +122,52 @@ def setup(app): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_book_theme" +html_theme = "pydata_sphinx_theme" -html_logo = "./_static/images/magpylib_flag.png" +html_logo = "./_static/images/magpylib_logo.png" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # -# announcement = """ -# <p>โ ๏ธ <b>Upcoming Soon: New Version 5 with breaking changes. We recommended to pin your dependencies to magpylib>=4.5<5 to avoid breaking changes! -# <a href="https://github.com/magpylib/magpylib/discussions/647">(see details)</a> -# โ ๏ธ</b></p> -# """ +# Define the json_url for our version switcher. +json_url = "https://magpylib.readthedocs.io/en/latest/_static/switcher.json" + +# Define the version we use for matching in the version switcher. +version_match = os.environ.get("READTHEDOCS_VERSION") + +# If READTHEDOCS_VERSION doesn't exist, we're not on RTD +# If it is an integer, we're in a PR build and the version isn't correct. +# If it's "latest" โ change to "dev" (that's what we want the switcher to call it) +if not version_match or version_match.isdigit() or version_match == "latest": + # For local development, infer the version to match from the package. + if "dev" in release or "rc" in release: + version_match = "dev" + # We want to keep the relative reference if we are in dev mode + # but we want the whole url if we are effectively in a released version + json_url = "_static/switcher.json" + else: + version_match = f"{release}" +elif version_match == "stable": + version_match = f"{release}" + + html_theme_options = { # "announcement": announcement, - "repository_url": "https://github.com/magpylib/magpylib", - "path_to_docs": "docs/", - "repository_branch": release, - "use_repository_button": True, - "use_download_button": True, - "use_source_button": True, - "use_edit_page_button": True, - "use_issues_button": True, - "launch_buttons": { - "binderhub_url": "https://mybinder.org", - "thebe": True, - "notebook_interface": "jupyterlab", + "logo": { + "text": "Magpylib", + "image_dark": "./_static/images/magpylib_logo.png", + }, + "header_links_before_dropdown": 4, + "show_version_warning_banner": True, + "navbar_align": "content", # [left, content, right] For testing that the navbar items align properly + "navbar_center": ["navbar-nav"], + "navbar_persistent": ["version-switcher"], + "switcher": { + "json_url": json_url, + "version_match": version_match, }, + "check_switcher": True, "icon_links": [ { "name": "Github", @@ -158,19 +175,25 @@ def setup(app): "icon": "https://img.shields.io/github/stars/magpylib/magpylib?style=social", "type": "url", }, - { - "name": "PyPI", - "url": "https://pypi.org/project/magpylib/", - "icon": "https://img.shields.io/pypi/v/magpylib", - "type": "url", - }, - { - "name": "Conda", - "url": "https://anaconda.org/conda-forge/magpylib", - "icon": "https://img.shields.io/conda/vn/conda-forge/magpylib", - "type": "url", - }, ], + "navigation_with_keys": False, + "footer_start": ["copyright"], + "footer_end": [], + "use_edit_page_button": True, + "navigation_depth": 3, + "collapse_navigation": False, +} + +# "show_nav_level": 2, # Show navigation up to the second level +# "navigation_depth": 4, # Adjust the depth as needed +# "collapse_navigation": True, # Option to collapse navigation sections + +html_context = { + # "github_url": "https://github.com", # or your GitHub Enterprise site + "github_user": "magpylib", + "github_repo": "magpylib", + "github_version": "main", + "doc_path": "docs/", } # Add any paths that contain custom static files (such as style sheets) here, @@ -187,8 +210,9 @@ def setup(app): # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # -# html_sidebars = {} - +html_sidebars = { + "**": ["search-field.html", "sidebar-nav-bs.html", "sidebar-ethical-ads.html"], +} # -- Options for HTMLHelp output --------------------------------------------- @@ -333,7 +357,8 @@ def setup(app): # import pyvista # pyvista.BUILDING_GALLERY = True -html_last_updated_fmt = "" -html_show_copyright = False -html_show_sphinx = False -show_authors = False +# html_last_updated_fmt = "" +# html_show_copyright = False +# html_show_sphinx = False +# show_authors = False +# html_show_sourcelink = False diff --git a/docs/index.md b/docs/index.md index fb2ed2d8f..677fceb72 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,83 +1,55 @@ -# The Magpylib Documentation +# Magpylib Documentation +Magpylib is an **open-source Python package** for calculating static **magnetic fields** of magnets, currents, and other sources. It uses **analytical expressions**, solutions to macroscopic magnetostatic problems, implemented in **vectorized** form which makes the computation **extremely fast** and leverages the open-source Python ecosystem for spectacular visualizations! -Magpylib is an **open-source Python package** for calculating static **magnetic fields** of magnets, currents and other sources. It uses **explicit expressions**, solutions to the macroscopic magnetostatic problems, implemented in **vectorized** form which makes the computation **extremely fast**. Make use of the open-source Python ecosystem for spectacular visualization. +<h2> Resources </h2> -## How it works - - - -In Magpylib, **sources** (magnets, currents, ...) and **observers** (sensors, position grids, ...) are created as Python objects with position and orientation attributes. These objects can be **grouped** and **moved** around. The system can be **viewed** graphically through various backends. The **magnetic field** is computed in the observer reference frame. Magpylib collects all inputs, and vectorizes the computation for maximal performance. - -## Resources - -::::{grid} 2 3 3 6 +::::{grid} 1 2 3 3 :margin: 4 4 0 0 :gutter: 2 :::{grid-item-card} -:link: get-started +:link: getting-started :link-type: ref :link-alt: link to Getting Started -:img-top: _static/images/index_icon_getting_started.png -:text-align: center -**Get Started** -::: - -:::{grid-item-card} -:link: docu-index -:link-type: ref -:link-alt: link to Documentation -:img-top: _static/images/index_icon_docu.png +:img-top: _static/images/index_icon_get_started.png :text-align: center -**Docs** +**Getting Started** ::: :::{grid-item-card} -:link: gallery +:link: examples :link-type: ref :link-alt: link to Examples -:img-top: _static/images/index_icon_gallery.png +:img-top: _static/images/index_icon_examples.png :text-align: center **Examples** ::: -:::{grid-item-card} -:link: contributing -:link-type: ref -:link-alt: link to Contribution Guide -:img-top: _static/images/index_icon_contributing.png -:text-align: center -**Contribute** -::: - -:::{grid-item-card} -:link: https://github.com/magpylib/magpylib -:link-alt: link to Github -:img-top: _static/images/index_icon_github.png -:text-align: center -**Project** -::: - :::{grid-item-card} :link: https://www.sciencedirect.com/science/article/pii/S2352711020300170 :link-alt: link to Journal :img-top: _static/images/index_icon_academic.png :text-align: center -**Paper** +**Scientific Reference** ::: :::: +<h2> How it works</h2> + + + +In Magpylib, **sources** (magnets, currents, ...) and **observers** (sensors, position grids, ...) are created as Python objects with position and orientation attributes. These objects can be **grouped** and **moved** around. The system can be **viewed** graphically through various backends. The **magnetic field** is computed in the observer reference frame. Magpylib collects all inputs, and vectorizes the computation for maximal performance. + + ```{toctree} :maxdepth: 2 +:hidden: + +_pages/user_guide/guide_index.md +_pages/API_reference.md +_pages/contributing/cont_index.md +_pages/changelog_.md -_pages/reso_get_started.md -_pages/docu/docu_index.md -_pages/gallery/gallery_index.md -_pages/reso_contributing.md -_pages/reso_code_of_conduct.md -_pages/reso_license.md -_pages/reso_changelog.md -_pages/reso_site_notice.md ``` diff --git a/magpylib/__init__.py b/magpylib/__init__.py index f67df2f4d..4481c871a 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -28,7 +28,7 @@ """ # module level dunders -__version__ = "5.0.1" +__version__ = "5.1.1" __author__ = "Michael Ortner & Alexandre Boisselet" __credits__ = "The Magpylib community" __all__ = [ diff --git a/magpylib/_src/display/backend_matplotlib.py b/magpylib/_src/display/backend_matplotlib.py index 3aecfbdcb..a24d781f8 100644 --- a/magpylib/_src/display/backend_matplotlib.py +++ b/magpylib/_src/display/backend_matplotlib.py @@ -4,6 +4,8 @@ # pylint: disable=too-many-statements # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position +# pylint: disable=too-many-positional-arguments + import os from collections import Counter @@ -236,6 +238,7 @@ def display_matplotlib( canvas=None, repeat=False, return_fig=False, + canvas_update="auto", return_animation=False, max_rows=None, max_cols=None, @@ -249,9 +252,10 @@ def display_matplotlib( """Display objects and paths graphically using the matplotlib library.""" frames = data["frames"] ranges = data["ranges"] + labels = data["labels"] + # only update layout if canvas is not provided fig_kwargs = {} if not fig_kwargs else fig_kwargs - fig_kwargs = {"dpi": 80, **fig_kwargs} show_kwargs = {} if not show_kwargs else show_kwargs show_kwargs = {**show_kwargs} @@ -263,16 +267,17 @@ def display_matplotlib( new_data.append(process_extra_trace(model)) fr["data"] = new_data - show_canvas = False + show_canvas = bool(canvas is None) axes = {} - if canvas is None: - show_canvas = True + if canvas_update: + fig_kwargs["dpi"] = fig_kwargs.get("dpi", 80) if fig_kwargs.get("figsize", None) is None: figsize = (8, 8) ratio = subplot_specs.shape[1] / subplot_specs.shape[0] if legend_maxitems != 0: ratio *= 1.5 # extend horizontal ratio if legend is present fig_kwargs["figsize"] = (figsize[0] * ratio, figsize[1]) + if canvas is None: fig = plt.figure(**{"tight_layout": True, **fig_kwargs}) elif isinstance(canvas, matplotlib.axes.Axes): fig = canvas.get_figure() @@ -289,6 +294,9 @@ def display_matplotlib( "The `canvas` parameter must be one of `[None, matplotlib.axes.Axes, " f"matplotlib.figure.Figure]`. Received type {type(canvas)!r} instead" ) + if canvas is not None and canvas_update: + fig.set_size_inches(*fig_kwargs["figsize"], forward=True) + fig.set_dpi(fig_kwargs["dpi"]) if max_rows is None and max_cols is None: if isinstance(canvas, matplotlib.axes.Axes): axes[(1, 1)] = canvas @@ -352,10 +360,11 @@ def draw_frame(frame_ind): for row_col_num, ax in axes.items(): count = count_with_labels.get(row_col_num, 0) if ax.name == "3d": - ax.set( - **{f"{k}label": f"{k} (m)" for k in "xyz"}, - **{f"{k}lim": r for k, r in zip("xyz", ranges)}, - ) + if row_col_num in ranges and canvas_update: + ax.set( + **{f"{k}label": labels[row_col_num][k] for k in "xyz"}, + **{f"{k}lim": r for k, r in zip("xyz", ranges[row_col_num])}, + ) ax.set_box_aspect(aspect=(1, 1, 1)) if 0 < count <= legend_maxitems: lg_kw = {"bbox_to_anchor": (1.04, 1), "loc": "upper left"} diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index 56efb9a97..93dd77caf 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -2,6 +2,8 @@ # pylint: disable=C0302 # pylint: disable=too-many-branches +# pylint: disable=too-many-positional-arguments + import inspect from functools import lru_cache @@ -75,15 +77,17 @@ def match_args(ttype: str): return set(named_args) -def apply_fig_ranges(fig, ranges, apply2d=True): +def apply_fig_ranges(fig, ranges_rc, labels_rc, apply2d=True): """This is a helper function which applies the ranges properties of the provided `fig` object - according to a provided ranges. All three space direction will be equal and match the - maximum of the ranges needed to display all objects, including their paths. + according to a provided ranges for each subplot. All three space direction will be equal and + match the maximum of the ranges needed to display all objects, including their paths. Parameters ---------- - ranges: array of dimension=(3,2) + ranges_rc: dict of arrays of dimension=(3,2) min and max graph range + labels_rc: dict of dicts + contains a dict with 'x', 'y', 'z' keys and respective labels as strings for each subplot apply2d: bool, default = True applies fixed range also on 2d traces @@ -92,15 +96,27 @@ def apply_fig_ranges(fig, ranges, apply2d=True): ------- None: NoneType """ - fig.update_scenes( - **{ - f"{k}axis": {"range": ranges[i], "autorange": False, "title": f"{k} (m)"} - for i, k in enumerate("xyz") - }, - aspectratio={k: 1 for k in "xyz"}, - aspectmode="manual", - camera_eye={"x": 1, "y": -1.5, "z": 1.4}, - ) + for rc, ranges in ranges_rc.items(): + row, col = rc + labels = labels_rc.get(rc, {k: "" for k in "xyz"}) + kwargs = { + **{ + f"{k}axis": { + "range": ranges[i], + "autorange": False, + "title": labels[k], + } + for i, k in enumerate("xyz") + }, + "aspectratio": {k: 1 for k in "xyz"}, + "aspectmode": "manual", + "camera_eye": {"x": 1, "y": -1.5, "z": 1.4}, + } + + # pylint: disable=protected-access + if fig._grid_ref is not None: + kwargs.update({"row": row, "col": col}) + fig.update_scenes(**kwargs) if apply2d: apply_2d_ranges(fig) @@ -144,6 +160,36 @@ def animate_path( maximum of the ranges needed to display all objects, including their paths. """ fps = int(1000 / frame_duration) + + play_dict = { + "args": [ + None, + { + "frame": {"duration": frame_duration}, + "transition": {"duration": 0}, + "fromcurrent": True, + }, + ], + "label": "Play", + "method": "animate", + } + pause_dict = { + "args": [[None], {"frame": {"duration": 0}, "mode": "immediate"}], + "label": "Pause", + "method": "animate", + } + buttons_dict = { + "buttons": [play_dict, pause_dict], + "direction": "left", + "pad": {"r": 10, "t": 20}, + "showactive": False, + "type": "buttons", + "x": 0.1, + "xanchor": "right", + "y": 0, + "yanchor": "top", + } + if animation_slider: sliders_dict = { "active": 0, @@ -161,46 +207,11 @@ def animate_path( "y": 0, "steps": [], } - - buttons_dict = { - "buttons": [ - { - "args": [ - None, - { - "frame": {"duration": frame_duration}, - "transition": {"duration": 0}, - "fromcurrent": True, - }, - ], - "label": "Play", - "method": "animate", - }, - { - "args": [[None], {"frame": {"duration": 0}, "mode": "immediate"}], - "label": "Pause", - "method": "animate", - }, - ], - "direction": "left", - "pad": {"r": 10, "t": 20}, - "showactive": False, - "type": "buttons", - "x": 0.1, - "xanchor": "right", - "y": 0, - "yanchor": "top", - } - - for ind in path_indices: - if animation_slider: + for ind in path_indices: slider_step = { "args": [ [str(ind + 1)], - { - "frame": {"duration": 0, "redraw": True}, - "mode": "immediate", - }, + {"frame": {"duration": 0, "redraw": True}, "mode": "immediate"}, ], "label": str(ind + 1), "method": "animate", @@ -210,20 +221,17 @@ def animate_path( # update fig fig.frames = frames frame0 = fig.frames[0] - fig.add_traces( - frame0.data, - rows=rows, - cols=cols, - ) title = frame0.layout.title.text + fig.add_traces(frame0.data, rows=rows, cols=cols) if update_layout: fig.update_layout( height=None, title=title, ) + sliders = [sliders_dict] if animation_slider else None fig.update_layout( - updatemenus=[buttons_dict], - sliders=[sliders_dict] if animation_slider else None, + updatemenus=[*fig.layout.updatemenus, buttons_dict], + sliders=[*fig.layout.sliders, *sliders], ) @@ -274,11 +282,10 @@ def process_extra_trace(model): def display_plotly( data, - zoom=1, canvas=None, renderer=None, return_fig=False, - update_layout=True, + canvas_update="auto", max_rows=None, max_cols=None, subplot_specs=None, @@ -292,6 +299,7 @@ def display_plotly( show_kwargs = {} if not show_kwargs else show_kwargs show_kwargs = {"renderer": renderer, **show_kwargs} + # only update layout if canvas is not provided fig = canvas show_fig = False extra_data = False @@ -341,15 +349,17 @@ def display_plotly( data["path_indices"], data["frame_duration"], animation_slider=animation_slider, - update_layout=update_layout, + update_layout=canvas_update, rows=rows_list, cols=cols_list, ) - ranges = data["ranges"] - if extra_data: - ranges = get_scene_ranges(*frames[0]["data"], zoom=zoom) - if update_layout: - apply_fig_ranges(fig, ranges, apply2d=isanimation) + if canvas_update: + ranges_rc = data["ranges"] + if extra_data: + ranges_rc = get_scene_ranges(*frames[0]["data"]) + apply_fig_ranges( + fig, ranges_rc, labels_rc=data["labels"], apply2d=isanimation + ) fig.update_layout( legend_itemsizing="constant", # legend_groupclick="toggleitem", diff --git a/magpylib/_src/display/backend_pyvista.py b/magpylib/_src/display/backend_pyvista.py index 2776ff9b2..adcf81c5b 100644 --- a/magpylib/_src/display/backend_pyvista.py +++ b/magpylib/_src/display/backend_pyvista.py @@ -2,6 +2,8 @@ # pylint: disable=too-many-branches # pylint: disable=too-many-statements +# pylint: disable=too-many-positional-arguments + import os import tempfile from functools import lru_cache @@ -211,6 +213,7 @@ def display_pyvista( data, canvas=None, return_fig=False, + canvas_update="auto", jupyter_backend=None, max_rows=None, max_cols=None, @@ -266,27 +269,36 @@ def draw_frame(frame_ind): canvas.subplot(row, col) if subplot_specs[row, col]["type"] == "scene": getattr(canvas, f"add_{typ}")(**tr1) - canvas.show_axes() else: if charts.get((row, col), None) is None: charts_max_ind += 1 charts[(row, col)] = pv.Chart2D() canvas.add_chart(charts[(row, col)]) getattr(charts[(row, col)], typ)(**tr1) - for rowcol, count in count_with_labels.items(): + # in pyvista there is no way to set the bouds so we add corners with + # a transparent scatter plot to set the ranges and zoom correctly + ranges = data["ranges"][row + 1, col + 1] + pts = np.array(np.meshgrid(*ranges)).T.reshape(-1, 3) + canvas.add_mesh(pv.PolyData(pts), opacity=0) + try: + canvas.remove_scalar_bar() + except (StopIteration, IndexError): + # try to remove scalar bar, if none, pass + # needs to happen in the loop otherwise they cummulate + # while the max of 10 is reached and throws a ValueError + pass + + for (row, col), count in count_with_labels.items(): + canvas.subplot(row, col) + # match other backends plotter properties + if canvas_update: + if callable(canvas.show_axes): + canvas.show_axes() + canvas.camera.azimuth = -90 + canvas.set_background("gray", top="white") if 0 < count <= legend_maxitems: - row, col = rowcol - canvas.subplot(row, col) if subplot_specs[row, col]["type"] == "scene": canvas.add_legend(bcolor=None) - # match other backends plotter properties - canvas.set_background("gray", top="white") - canvas.camera.azimuth = -90 - try: - canvas.remove_scalar_bar() - except IndexError: - # try to remove scalar bar, if none, pass - pass def run_animation(filename, embed=True): # embed=True, embeds the animation into the notebook page and is necessary when using diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 946144312..66098cb6b 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -1,4 +1,4 @@ -""" Display function codes""" +"""Display function codes""" import warnings from contextlib import contextmanager @@ -11,14 +11,16 @@ from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.display.traces_generic import MagpyMarkers from magpylib._src.display.traces_generic import get_frames +from magpylib._src.display.traces_utility import DEFAULT_ROW_COL_PARAMS +from magpylib._src.display.traces_utility import linearize_dict from magpylib._src.display.traces_utility import process_show_input_objs from magpylib._src.input_checks import check_format_input_backend from magpylib._src.input_checks import check_format_input_vector from magpylib._src.input_checks import check_input_animation -from magpylib._src.input_checks import check_input_zoom +from magpylib._src.input_checks import check_input_canvas_update from magpylib._src.utility import check_path_format -disp_args = get_defaults_dict("display").keys() +disp_args = set(get_defaults_dict("display")) class RegisteredBackend: @@ -30,14 +32,14 @@ def __init__( self, *, name, - show_func_getter, + show_func, supports_animation, supports_subplots, supports_colorgradient, supports_animation_output, ): self.name = name - self.show_func_getter = show_func_getter + self.show_func = show_func self.supports = { "animation": supports_animation, "subplots": supports_subplots, @@ -54,7 +56,6 @@ def show( cls, *objs, backend, - zoom=0, title=None, max_rows=None, max_cols=None, @@ -83,12 +84,18 @@ def show( f"\nFalling back to: {params}" ) kwargs.update(params) - frame_kwargs = { + display_kwargs = { k: v for k, v in kwargs.items() - if any(k.startswith(arg) for arg in disp_args) + if any(k.startswith(arg) for arg in disp_args - {"style"}) + } + style_kwargs = {k: v for k, v in kwargs.items() if k.startswith("style")} + style_kwargs = linearize_dict(style_kwargs, separator="_") + kwargs = { + k: v + for k, v in kwargs.items() + if (k not in display_kwargs and k not in style_kwargs) } - kwargs = {k: v for k, v in kwargs.items() if k not in frame_kwargs} backend_kwargs = { k[len(backend) + 1 :]: v for k, v in kwargs.items() @@ -117,13 +124,12 @@ def show( objs, supports_colorgradient=self.supports["colorgradient"], backend=backend, - zoom=zoom, title=title, - **frame_kwargs, + style_kwargs=style_kwargs, + **display_kwargs, ) - return self.show_func_getter()( + return self.show_func( data, - zoom=zoom, max_rows=max_rows, max_cols=max_cols, subplot_specs=subplot_specs, @@ -136,12 +142,9 @@ def show( def get_show_func(backend): """Return the backend show function""" # defer import to show call. Importerror should only fail if unavalaible backend is called - return lambda: getattr( + return lambda *args, backend=backend, **kwargs: getattr( import_module(f"magpylib._src.display.backend_{backend}"), f"display_{backend}" - ) - - -ROW_COL_SPECIFIC_NAMES = ("row", "col", "output", "sumup", "pixel_agg", "in_out") + )(*args, **kwargs) def infer_backend(canvas): @@ -180,10 +183,11 @@ def infer_backend(canvas): def _show( *objects, - backend=None, animation=False, - zoom=0, markers=None, + canvas=None, + canvas_update=None, + backend=None, **kwargs, ): """Display objects and paths graphically. @@ -193,18 +197,16 @@ def _show( # process input objs objects, obj_list_flat, max_rows, max_cols, subplot_specs = process_show_input_objs( - objects, **{k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + objects, + **{k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS}, ) - kwargs = {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} - kwargs["max_rows"], kwargs["max_cols"] = max_rows, max_cols - kwargs["subplot_specs"] = subplot_specs - + kwargs = {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} + canvas_update = check_input_canvas_update(canvas_update, canvas) # test if every individual obj_path is good check_path_format(obj_list_flat) # input checks backend = check_format_input_backend(backend) - check_input_zoom(zoom) check_input_animation(animation) check_format_input_vector( markers, @@ -216,24 +218,20 @@ def _show( ) if markers: - objects = [ - *objects, - { - "objects": [MagpyMarkers(*markers)], - "row": 1, - "col": 1, - "output": "model3d", - }, - ] + objects.append({"objects": [MagpyMarkers(*markers)], **DEFAULT_ROW_COL_PARAMS}) if backend == "auto": - backend = infer_backend(kwargs.get("canvas", None)) + backend = infer_backend(canvas) return RegisteredBackend.show( backend=backend, *objects, - zoom=zoom, animation=animation, + canvas=canvas, + canvas_update=canvas_update, + subplot_specs=subplot_specs, + max_rows=max_rows, + max_cols=max_cols, **kwargs, ) @@ -247,6 +245,7 @@ def show( zoom=_DefaultValue, markers=_DefaultValue, return_fig=_DefaultValue, + canvas_update=_DefaultValue, row=_DefaultValue, col=_DefaultValue, output=_DefaultValue, @@ -298,6 +297,12 @@ def show( - with plotly: `plotly.graph_objects.Figure` or `plotly.graph_objects.FigureWidget`. - with pyvista: `pyvista.Plotter`. + canvas_update: bool, default="auto". + When no canvas is provided, Magpylib creates one and sets the layout to internally defined + settings (e.g. camera angle, aspect ratio). If a canvas is provided, no changes to the + layout are made. One can however explicitly force a behavior by setting `canvas_update` + to True or False. + row: int or None, If provided specifies the row in which the objects will be displayed. @@ -407,9 +412,9 @@ def show( } ) if ctx.isrunning: - rco = {k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + rco = {k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS} ctx.kwargs.update( - {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} + {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} ) ctx_objects = tuple({**o, **rco} for o in ctx.objects_from_ctx) objects, *_ = process_show_input_objs(ctx_objects + objects, **rco) @@ -428,6 +433,7 @@ def show_context( zoom=_DefaultValue, markers=_DefaultValue, return_fig=_DefaultValue, + canvas_update=_DefaultValue, row=_DefaultValue, col=_DefaultValue, output=_DefaultValue, @@ -452,11 +458,11 @@ def show_context( ) try: ctx.isrunning = True - rco = {k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + rco = {k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS} objects, *_ = process_show_input_objs(objects, **rco) ctx.objects_from_ctx += tuple(objects) ctx.kwargs.update( - {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} + {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} ) yield ctx ctx.show_return_value = _show(*ctx.objects, **ctx.kwargs) @@ -491,7 +497,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="matplotlib", - show_func_getter=get_show_func("matplotlib"), + show_func=get_show_func("matplotlib"), supports_animation=True, supports_subplots=True, supports_colorgradient=False, @@ -501,7 +507,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="plotly", - show_func_getter=get_show_func("plotly"), + show_func=get_show_func("plotly"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, @@ -510,7 +516,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="pyvista", - show_func_getter=get_show_func("pyvista"), + show_func=get_show_func("pyvista"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, diff --git a/magpylib/_src/display/sensor_mesh.py b/magpylib/_src/display/sensor_mesh.py index c174fc94f..0095b4436 100644 --- a/magpylib/_src/display/sensor_mesh.py +++ b/magpylib/_src/display/sensor_mesh.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + import numpy as np from scipy.spatial.transform import Rotation as RotScipy diff --git a/magpylib/_src/display/traces_base.py b/magpylib/_src/display/traces_base.py index baa4e4115..e752c65f6 100644 --- a/magpylib/_src/display/traces_base.py +++ b/magpylib/_src/display/traces_base.py @@ -1,5 +1,7 @@ """base traces building functions""" +# pylint: disable=too-many-positional-arguments + from functools import partial import numpy as np diff --git a/magpylib/_src/display/traces_core.py b/magpylib/_src/display/traces_core.py index ede159519..d3c8cd15a 100644 --- a/magpylib/_src/display/traces_core.py +++ b/magpylib/_src/display/traces_core.py @@ -318,6 +318,7 @@ def make_triangle_orientations(obj, **kwargs) -> Dict[str, Any]: def get_closest_vertices(faces_subsets, vertices): """Get closest pairs of points between disconnected subsets of faces indices""" + # pylint: disable=used-before-assignment nparts = len(faces_subsets) inds_subsets = [np.unique(v) for v in faces_subsets] closest_verts_list = [] diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 5f3cc21a8..21a301ea4 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -5,6 +5,8 @@ # pylint: disable=too-many-statements # pylint: disable=too-many-nested-blocks # pylint: disable=cyclic-import +# pylint: disable=too-many-positional-arguments + import numbers import warnings from collections import Counter @@ -20,17 +22,21 @@ from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS from magpylib._src.defaults.defaults_utility import linearize_dict from magpylib._src.display.traces_utility import draw_arrowed_line -from magpylib._src.display.traces_utility import get_flatten_objects_properties from magpylib._src.display.traces_utility import get_legend_label +from magpylib._src.display.traces_utility import get_objects_props_by_row_col from magpylib._src.display.traces_utility import get_rot_pos_from_path from magpylib._src.display.traces_utility import get_scene_ranges from magpylib._src.display.traces_utility import getColorscale from magpylib._src.display.traces_utility import getIntensity from magpylib._src.display.traces_utility import group_traces from magpylib._src.display.traces_utility import place_and_orient_model3d +from magpylib._src.display.traces_utility import rescale_traces from magpylib._src.display.traces_utility import slice_mesh_from_colorscale from magpylib._src.style import DefaultMarkers from magpylib._src.utility import format_obj_input +from magpylib._src.utility import get_unit_factor +from magpylib._src.utility import style_temp_edit +from magpylib._src.utility import unit_prefix class MagpyMarkers: @@ -63,6 +69,7 @@ def get_trace(self, **kwargs): "y": y, "z": z, "mode": "markers", + "showlegend": style.legend.show, # pylint: disable=no-member **marker_kwargs, **kwargs, } @@ -147,6 +154,8 @@ def make_mag_arrows(obj): mag = obj.magnetization # collect all draw positions and directions pos = getattr(obj, "_barycenter", obj._position)[0] - obj._position[0] + # we need initial relative barycenter, arrow gets orientated later + pos = obj._orientation[0].inv().apply(pos) direc = mag / (np.linalg.norm(mag) + 1e-6) * length x, y, z = draw_arrowed_line( direc, pos, sign=1, arrow_pos=arrow.offset, pivot="tail" @@ -201,13 +210,12 @@ def get_trace2D_dict( field_str, coords_str, obj_lst_str, - frame_focus_inds, + focus_inds, frames_indices, mode, label_suff, - color, - symbol, - linestyle, + units_polarization, + units_magnetization, **kwargs, ): """return a 2d trace based on field and parameters""" @@ -217,9 +225,14 @@ def get_trace2D_dict( y = y[0] else: y = np.linalg.norm(y, axis=0) - marker_size = np.array([2] * len(frames_indices)) - marker_size[frame_focus_inds] = 15 + marker_size = np.array([3] * len(frames_indices)) + marker_size[focus_inds] = 15 title = f"{field_str}{''.join(coords_str)}" + unit = ( + units_polarization + if field_str in "BJ" + else units_magnetization if field_str in "HM" else "" + ) trace = { "mode": "lines+markers", "legendgrouptitle_text": f"{title}" @@ -227,35 +240,35 @@ def get_trace2D_dict( "text": mode, "hovertemplate": ( "<b>Path index</b>: %{x} " - f"<b>{title}</b>: " + "%{y:.3s}T<br>" + f"<b>{title}</b>: %{{y:.3s}}{unit}<br>" f"<b>{'sources'}</b>:<br>{obj_lst_str['sources']}<br>" f"<b>{'sensors'}</b>:<br>{obj_lst_str['sensors']}" # "<extra></extra>", ), "x": frames_indices, "y": y[frames_indices], - "line_dash": linestyle, - "line_color": color, "marker_size": marker_size, - "marker_color": color, - "marker_symbol": symbol, "showlegend": True, "legendgroup": f"{title}{label_suff}", - **kwargs, } + trace.update(kwargs) return trace -def get_generic_traces_2D( - *, - objects, +def get_traces_2D( + *objects, output=("Bx", "By", "Bz"), row=None, col=None, sumup=True, pixel_agg=None, in_out="auto", - style_path_frames=None, + styles=None, + units_polarization="T", + units_magnetization="A/m", + # pylint: disable=unused-argument + units_length="m", + zoom=0, ): """draws and animates sensor values over a path in a subplot""" # pylint: disable=import-outside-toplevel @@ -277,6 +290,7 @@ def get_generic_traces_2D( if not isinstance(output, (list, tuple)): output = [output] output_params = {} + field_str_list = [] for out, linestyle in zip(output, cycle(ALLOWED_LINESTYLES[:6])): field_str, *coords_str = out if not coords_str: @@ -284,31 +298,44 @@ def get_generic_traces_2D( if field_str not in "BHMJ" and set(coords_str).difference(set("xyz")): raise ValueError( "The `output` parameter must start with 'B', 'H', 'M', 'J' " - "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Hz') )" + "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Bz') )" f"\nreceived {out!r} instead" ) + field_str_list.append(field_str) output_params[out] = { "field_str": field_str, "coords_str": coords_str, - "linestyle": linestyle, + "line_dash": linestyle, } - BH_array = getBH_level2( - sources, - sensors, - sumup=sumup, - squeeze=False, - field=field_str, - pixel_agg=pixel_agg, - output="ndarray", - in_out=in_out, - ) - BH_array = BH_array.swapaxes(1, 2) # swap axes to have sensors first, path second - - frames_indices = np.arange(0, BH_array.shape[2]) - frame_focus_inds = [-1] if style_path_frames is None else style_path_frames - if isinstance(frame_focus_inds, numbers.Number): - # pylint: disable=invalid-unary-operand-type - frame_focus_inds = frames_indices[::-style_path_frames] + field_str_list = list(dict.fromkeys(field_str_list)) + BH_array = {} + for field_str in field_str_list: + BH_array[field_str] = getBH_level2( + sources, + sensors, + sumup=sumup, + squeeze=False, + field=field_str, + pixel_agg=pixel_agg, + output="ndarray", + in_out=in_out, + ) + # swap axes to have sensors first, path second + BH_array[field_str] = BH_array[field_str].swapaxes(1, 2) + frames_indices = np.arange(0, BH_array[field_str_list[0]].shape[2]) + + def get_focus_inds(*objs): + focus_inds = [] + for obj in objs: + style = styles.get(obj, obj.style) + frames = style.path.frames + inds = [] if frames is None else frames + if isinstance(inds, numbers.Number): + # pylint: disable=invalid-unary-operand-type + inds = frames_indices[::-frames] + focus_inds.extend(inds) + focus_inds = list(dict.fromkeys(focus_inds)) + return focus_inds if focus_inds else [-1] def get_obj_list_str(objs): if len(objs) < 8: @@ -319,7 +346,8 @@ def get_obj_list_str(objs): return obj_lst_str def get_label_and_color(obj): - style = obj.style + style = styles.get(obj, None) + style = obj.style if style is None else style label = get_legend_label(obj, style=style) color = getattr(style, "color", None) return label, color @@ -337,15 +365,18 @@ def get_label_and_color(obj): label_src, color_src = get_label_and_color(src) symbols = cycle(ALLOWED_SYMBOLS[:6]) for sens_ind, sens in enumerate(sensors): + focus_inds = get_focus_inds(src, sens) label_sens, color_sens = get_label_and_color(sens) label_suff = label_sens - if mode == "sensors": - label, color = label_src, color_src - else: + label = label_src + line_color = color_src + marker_color = color_sens if len(sensors) > 1 else None + if sumup: + line_color = color_sens + label = label_sens label_suff = ( f"{label_src}" if len(sources) == 1 else f"{len(sources)} sources" ) - label, color = label_sens, color_sens num_of_pix = ( len(sens.pixel.reshape(-1, 3)) if (not isinstance(sens, magpy.Collection)) @@ -356,29 +387,33 @@ def get_label_and_color(obj): pix_suff = "" num_of_pix_to_show = 1 if pixel_agg else num_of_pix for pix_ind in range(num_of_pix_to_show): - symbol = next(symbols) - BH = BH_array[src_ind, sens_ind, :, pix_ind] + marker_symbol = next(symbols) if num_of_pix > 1: if pixel_agg: - pix_suff = f" ({num_of_pix} pixels {pixel_agg})" + pix_suff = f" - {num_of_pix} pixels {pixel_agg}" else: - pix_suff = f" (pixel {pix_ind})" + pix_suff = f" - pixel {pix_ind}" for param in output_params.values(): + BH = BH_array[param["field_str"]][src_ind, sens_ind, :, pix_ind] traces.append( get_trace2D_dict( BH, **param, obj_lst_str=obj_lst_str, - frame_focus_inds=frame_focus_inds, + focus_inds=focus_inds, frames_indices=frames_indices, mode=mode, label_suff=label_suff, name=f"{label}{pix_suff}", - color=color, - symbol=symbol, + line_color=line_color, + marker_color=marker_color, + marker_line_color=marker_color, + marker_symbol=marker_symbol, type="scatter", row=row, col=col, + units_polarization=units_polarization, + units_magnetization=units_magnetization, ) ) return traces @@ -393,9 +428,10 @@ def process_extra_trace(model): "constructor": extr.constructor, "kwargs": model_kwargs, "args": model_args, + "coordsargs": extr.coordsargs, "kwargs_extra": model["kwargs_extra"], } - kwargs, args = place_and_orient_model3d( + kwargs, args, coordsargs = place_and_orient_model3d( model_kwargs=model_kwargs, model_args=model_args, orientation=model["orientation"], @@ -403,13 +439,15 @@ def process_extra_trace(model): coordsargs=extr.coordsargs, scale=extr.scale, return_model_args=True, + return_coordsargs=True, ) + trace3d["coordsargs"] = coordsargs trace3d["kwargs"].update(kwargs) trace3d["args"] = args return trace3d -def get_generic_traces( +def get_generic_traces3D( input_obj, autosize=None, legendgroup=None, @@ -440,7 +478,6 @@ def get_generic_traces( # pylint: disable=too-many-nested-blocks # pylint: disable=protected-access # pylint: disable=import-outside-toplevel - style = input_obj.style is_mag_arrows = False is_mag = hasattr(input_obj, "magnetization") and hasattr(style, "magnetization") @@ -496,24 +533,24 @@ def get_generic_traces( extr.update(extr.updatefunc()) # update before checking backend if extr.backend == "generic": extr.update(extr.updatefunc()) - tr_generic = {"opacity": style.opacity} + tr_non_generic = {"opacity": style.opacity} ttype = extr.constructor.lower() obj_extr_trace = extr.kwargs() if callable(extr.kwargs) else extr.kwargs obj_extr_trace = {"type": ttype, **obj_extr_trace} if ttype == "scatter3d": for k in ("marker", "line"): - tr_generic[f"{k}_color"] = tr_generic.get( + tr_non_generic[f"{k}_color"] = tr_non_generic.get( f"{k}_color", style.color ) elif ttype == "mesh3d": - tr_generic["showscale"] = tr_generic.get("showscale", False) - tr_generic["color"] = tr_generic.get("color", style.color) + tr_non_generic["showscale"] = tr_non_generic.get("showscale", False) + tr_non_generic["color"] = tr_non_generic.get("color", style.color) else: # pragma: no cover raise ValueError( f"{ttype} is not supported, only 'scatter3d' and 'mesh3d' are" ) - tr_generic.update(linearize_dict(obj_extr_trace, separator="_")) - traces_generic.append(tr_generic) + tr_non_generic.update(linearize_dict(obj_extr_trace, separator="_")) + traces_generic.append(tr_non_generic) if is_mag_arrows: mag = input_obj.magnetization @@ -543,6 +580,7 @@ def get_generic_traces( path_traces_generic = group_traces(*path_traces_generic) for tr in path_traces_generic: + tr.update(place_and_orient_model3d(tr)) tr.update(row=row, col=col) if tr.get("opacity", None) is None: tr["opacity"] = style.opacity @@ -572,7 +610,7 @@ def get_generic_traces( extr.update(extr.updatefunc()) # update before checking backend if extr.backend == extra_backend: for orient, pos in zip(orientations, positions): - tr_generic = { + tr_non_generic = { "model3d": extr, "position": pos, "orientation": orient, @@ -590,8 +628,8 @@ def get_generic_traces( "col": col, }, } - tr_generic = process_extra_trace(tr_generic) - path_traces_extra_non_generic_backend.append(tr_generic) + tr_non_generic = process_extra_trace(tr_non_generic) + path_traces_extra_non_generic_backend.append(tr_non_generic) out.update({extra_backend: path_traces_extra_non_generic_backend}) return out @@ -716,7 +754,34 @@ def extract_animation_properties( return path_indices, exp, frame_duration -def draw_frame(objs, colorsequence=None, zoom=0.0, autosize=None, **kwargs) -> Tuple: +def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs): + """Return traces, traces to resize and extra_backend_traces""" + extra_backend_traces = [] + traces_dict = {} + for obj, params in flat_objs_props.items(): + params = {**params, **kwargs} + if autosize is None and getattr(obj, "_autosize", False): + # temporary coordinates to be able to calculate ranges + # pylint: disable=protected-access + x, y, z = obj._position.T + rc_dict = {k: v for k, v in params.items() if k in ("row", "col")} + traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True, **rc_dict}] + else: + traces_dict[obj] = [] + with style_temp_edit(obj, style_temp=params.pop("style", None), copy=True): + out_traces = get_generic_traces3D( + obj, + extra_backend=extra_backend, + autosize=autosize, + **params, + ) + if extra_backend: + extra_backend_traces.extend(out_traces.get(extra_backend, [])) + traces_dict[obj].extend(out_traces["generic"]) + return traces_dict, extra_backend_traces + + +def draw_frame(objs, *, colorsequence, rc_params, style_kwargs, **kwargs) -> Tuple: """ Creates traces from input `objs` and provided parameters, updates the size of objects like Sensors and Dipoles in `kwargs` depending on the canvas size. @@ -731,84 +796,73 @@ def draw_frame(objs, colorsequence=None, zoom=0.0, autosize=None, **kwargs) -> T colorsequence = default_settings.display.colorsequence # dipoles and sensors use autosize, the trace building has to be put at the back of the queue. # autosize is calculated from the other traces overall scene range - - style_path_frames = kwargs.get( - "style_path_frames", [-1] - ) # get before next func strips style - flat_objs_props, kwargs = get_flatten_objects_properties( - *objs, colorsequence=colorsequence, **kwargs - ) - traces_dict, traces_to_resize_dict, extra_backend_traces = get_row_col_traces( - flat_objs_props, **kwargs - ) - traces = [t for tr in traces_dict.values() for t in tr] - ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) - if autosize is None or autosize == "return": - # pylint: disable=no-member - autosize = np.mean(np.diff(ranges)) / default_settings.display.autosizefactor - - traces_dict_2, _, extra_backend_traces2 = get_row_col_traces( - traces_to_resize_dict, autosize=autosize, **kwargs + objs_rc = get_objects_props_by_row_col( + *objs, + colorsequence=colorsequence, + style_kwargs=style_kwargs, ) - traces_dict.update(traces_dict_2) - extra_backend_traces.extend(extra_backend_traces2) + traces_dict = {} + extra_backend_traces = [] + rc_params = {} if rc_params is None else rc_params + for rc, props in objs_rc.items(): + if props["rc_params"]["output"] == "model3d": + rc_params[rc] = rc_params.get(rc, {}) + rc_params[rc]["units_length"] = props["rc_params"]["units_length"] + rc_keys = ("row", "col") + rc_kwargs = {k: v for k, v in props["rc_params"].items() if k in rc_keys} + traces_d1, traces_ex1 = get_traces_3D( + props["objects"], **rc_kwargs, **kwargs + ) + rc_params[rc]["autosize"] = rc_params.get(rc, {}).get("autosize", None) + if rc_params[rc]["autosize"] is None: + zoom = rc_params[rc]["zoom"] = props["rc_params"]["zoom"] + traces = [t for tr in traces_d1.values() for t in tr] + ranges_rc = get_scene_ranges(*traces, *traces_ex1, zoom=zoom) + # pylint: disable=no-member + factor = default_settings.display.autosizefactor + rc_params[rc]["autosize"] = np.mean(np.diff(ranges_rc[rc])) / factor + to_resize_keys = { + k for k, v in traces_d1.items() if v and "_autosize" in v[0] + } + flat_objs_props = { + k: v for k, v in props["objects"].items() if k in to_resize_keys + } + traces_d2, traces_ex2 = get_traces_3D( + flat_objs_props, + autosize=rc_params[rc]["autosize"], + **rc_kwargs, + **kwargs, + ) + traces_dict.update( + {(k, *rc): v for k, v in {**traces_d1, **traces_d2}.items()} + ) + extra_backend_traces.extend([*traces_ex1, *traces_ex2]) traces = group_traces(*[t for tr in traces_dict.values() for t in tr]) - obj_list_2d = [o for o in objs if o["output"] != "model3d"] - for objs_2d in obj_list_2d: - traces2d = get_generic_traces_2D( - **objs_2d, - style_path_frames=style_path_frames, - ) - traces.extend(traces2d) - return traces, autosize, ranges, extra_backend_traces - -def get_row_col_traces(flat_objs_props, extra_backend=False, autosize=None, **kwargs): - """Return traces, traces to resize and extra_backend_traces""" - # pylint: disable=protected-access - extra_backend_traces = [] - traces_dict = {} - traces_to_resize_dict = {} - for obj, params in flat_objs_props.items(): - params.update(kwargs) - if autosize is None and getattr(obj, "_autosize", False): - traces_to_resize_dict[obj] = {**params} - # temporary coordinates to be able to calculate ranges - x, y, z = obj._position.T - traces_dict[obj] = [{"x": x, "y": y, "z": z}] - else: - traces_dict[obj] = [] - rco_obj = params.pop("row_cols") - for rco in rco_obj: - params["row"], params["col"], output_typ = rco - if output_typ == "model3d": - orig_style = None - try: - # temporary replace style attribute - orig_style = obj.style - obj._style = params.pop("style", None) - out_traces = get_generic_traces( - obj, - extra_backend=extra_backend, - autosize=autosize, - **params, - ) - finally: - obj._style = orig_style - if extra_backend: - extra_backend_traces.extend(out_traces.get(extra_backend, [])) - traces_dict[obj].extend(out_traces["generic"]) - return traces_dict, traces_to_resize_dict, extra_backend_traces + styles = { + obj: params.get("style", None) + for o_rc in objs_rc.values() + for obj, params in o_rc["objects"].items() + } + for props in objs_rc.values(): + if props["rc_params"]["output"] != "model3d": + traces2d = get_traces_2D( + *props["objects"], + **props["rc_params"], + styles=styles, + ) + traces.extend(traces2d) + return traces, extra_backend_traces, rc_params def get_frames( objs, colorsequence=None, - zoom=1, title=None, animation=False, supports_colorgradient=True, backend="generic", + style_kwargs=None, **kwargs, ): """This is a helper function which generates frames with generic traces to be provided to @@ -838,25 +892,26 @@ def get_frames( ) # create frame for each path index or downsampled path index frames = [] - autosize = "return" + title_str = title + rc_params = {} for i, ind in enumerate(path_indices): extra_backend_traces = [] if animation: - kwargs["style_path_frames"] = [ind] + style_kwargs["style_path_frames"] = [ind] title = "Animation 3D - " if title is None else title title_str = f"""{title}path index: {ind+1:0{exp}d}""" - traces, autosize_init, ranges, extra_backend_traces = draw_frame( + traces, extra_backend_traces, rc_params_temp = draw_frame( objs, - colorsequence, - zoom, - autosize=autosize, + colorsequence=colorsequence, + rc_params=rc_params, supports_colorgradient=supports_colorgradient, extra_backend=backend, + style_kwargs=style_kwargs, **kwargs, ) if i == 0: # get the dipoles and sensors autosize from first frame - autosize = autosize_init + rc_params = rc_params_temp frames.append( { "data": traces, @@ -865,13 +920,30 @@ def get_frames( "extra_backend_traces": extra_backend_traces, } ) - clean_legendgroups(frames) traces = [t for frame in frames for t in frame["data"]] - ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) + zoom = {rc: v["zoom"] for rc, v in rc_params.items()} + ranges_rc = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) + labels_rc = {(1, 1): {k: "" for k in "xyz"}} + scale_factors_rc = {} + for rc, params in rc_params.items(): + units_length = params["units_length"] + if units_length == "auto": + rmax = np.amax(np.abs(ranges_rc[rc])) + units_length = f"{unit_prefix(rmax, as_tuple=True)[2]}m" + unit_str = "" if not (units_length) else f" ({units_length})" + labels_rc[rc] = {k: f"{k}{unit_str}" for k in "xyz"} + scale_factors_rc[rc] = get_unit_factor(units_length, target_unit="m") + ranges_rc[rc] *= scale_factors_rc[rc] + + for frame in frames: + for key in ("data", "extra_backend_traces"): + frame[key] = rescale_traces(frame[key], factors=scale_factors_rc) + out = { "frames": frames, - "ranges": ranges, + "ranges": ranges_rc, + "labels": labels_rc, "input_kwargs": {**kwargs, **animation_kwargs}, } if animation: diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 9ff442d24..d1dd5b531 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -1,8 +1,11 @@ -""" Display function codes""" +"""Display function codes""" # pylint: disable=too-many-branches +# pylint: disable=too-many-positional-arguments + from collections import defaultdict from functools import lru_cache +from itertools import chain from itertools import cycle from typing import Tuple @@ -11,8 +14,21 @@ from magpylib._src.defaults.defaults_classes import default_settings from magpylib._src.defaults.defaults_utility import linearize_dict +from magpylib._src.input_checks import check_input_zoom from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input +from magpylib._src.utility import merge_dicts_with_conflict_check + +DEFAULT_ROW_COL_PARAMS = { + "row": 1, + "col": 1, + "output": "model3d", + "sumup": True, + "pixel_agg": "mean", + "in_out": "auto", + "zoom": 0, + "units_length": "auto", +} def get_legend_label(obj, style=None, suffix=True): @@ -34,49 +50,55 @@ def get_legend_label(obj, style=None, suffix=True): def place_and_orient_model3d( model_kwargs, + *, model_args=None, orientation=None, position=None, coordsargs=None, scale=1, return_model_args=False, + return_coordsargs=False, + length_factor=1, **kwargs, ): """places and orients mesh3d dict""" - if orientation is None and position is None: - return {**model_kwargs, **kwargs} - position = (0.0, 0.0, 0.0) if position is None else position - position = np.array(position, dtype=float) - new_model_dict = {} - if model_args is None: - model_args = () - new_model_args = list(model_args) - vertices, coordsargs, useargs = get_vertices_from_model( - model_kwargs, model_args, coordsargs - ) - - # sometimes traces come as (n,m,3) shape - vert_shape = vertices.shape - vertices = np.reshape(vertices, (3, -1)) - - vertices = vertices.T - - if orientation is not None: - vertices = orientation.apply(vertices) - new_vertices = (vertices * scale + position).T - new_vertices = np.reshape(new_vertices, vert_shape) - for i, k in enumerate("xyz"): - key = coordsargs[k] - if useargs: - ind = int(key[5]) - new_model_args[ind] = new_vertices[i] - else: - new_model_dict[key] = new_vertices[i] - new_model_kwargs = {**model_kwargs, **new_model_dict, **kwargs} + if orientation is None and position is None and length_factor == 1: + new_model_kwargs = {**model_kwargs, **kwargs} + new_model_args = model_args + else: + position = (0.0, 0.0, 0.0) if position is None else position + position = np.array(position, dtype=float) + new_model_dict = {} + if model_args is None: + model_args = () + new_model_args = list(model_args) + vertices, coordsargs, useargs = get_vertices_from_model( + model_kwargs, model_args, coordsargs + ) + # sometimes traces come as (n,m,3) shape + vert_shape = vertices.shape + vertices = np.reshape(vertices.astype(float), (3, -1)) + + vertices = vertices.T + + if orientation is not None: + vertices = orientation.apply(vertices) + new_vertices = (vertices * scale + position).T * length_factor + new_vertices = np.reshape(new_vertices, vert_shape) + for i, k in enumerate("xyz"): + key = coordsargs[k] + if useargs: + ind = int(key[5]) + new_model_args[ind] = new_vertices[i] + else: + new_model_dict[key] = new_vertices[i] + new_model_kwargs = {**model_kwargs, **new_model_dict, **kwargs} out = (new_model_kwargs,) if return_model_args: out += (new_model_args,) + if return_coordsargs: + out += (coordsargs,) return out[0] if len(out) == 1 else out @@ -244,42 +266,52 @@ def get_rot_pos_from_path(obj, show_path=None): return rots, poss, inds -def get_flatten_objects_properties(*objs, colorsequence, **kwargs): +def get_objects_props_by_row_col(*objs, colorsequence, style_kwargs): """Return flat dict with objs as keys object properties as values. Properties include: row_cols, style, legendgroup, legendtext""" - flat_objs = {} + flat_objs_rc = {} + rc_params_by_obj = {} + obj_list_semi_flat = [o for obj in objs for o in obj["objects"]] for obj in objs: - flat_sub_objs = get_flatten_objects_properties_recursive( - *obj["objects"], colorsequence=colorsequence, **kwargs - ) - for subobj, props in flat_sub_objs.items(): - if subobj in flat_objs: - props["row_cols"] = flat_objs[subobj]["row_cols"] - elif "row_cols" not in props: - props["row_cols"] = [] - props["row_cols"].extend([(obj["row"], obj["col"], obj["output"])]) - flat_objs.update(flat_sub_objs) - kwargs = {k: v for k, v in kwargs.items() if not k.startswith("style")} - return flat_objs, kwargs + rc_params = {k: v for k, v in obj.items() if k != "objects"} + for subobj in obj["objects"]: + children = getattr(subobj, "children_all", []) + for child in chain([subobj], children): + if child not in rc_params_by_obj: + rc_params_by_obj[child] = [] + rc_params_by_obj[child].append(rc_params) + flat_sub_objs = get_flatten_objects_properties_recursive( + *obj_list_semi_flat, + style_kwargs=style_kwargs, + colorsequence=colorsequence, + ) + for obj, rc_params_list in rc_params_by_obj.items(): + for rc_params in rc_params_list: + rc = rc_params["row"], rc_params["col"] + if rc not in flat_objs_rc: + flat_objs_rc[rc] = {"objects": {}, "rc_params": rc_params} + flat_objs_rc[rc]["objects"][obj] = flat_sub_objs[obj] + return flat_objs_rc def get_flatten_objects_properties_recursive( *obj_list_semi_flat, + style_kwargs=None, colorsequence=None, color_cycle=None, parent_legendgroup=None, parent_color=None, parent_label=None, parent_showlegend=None, - **kwargs, ): """returns a flat dict -> (obj: display_props, ...) from nested collections""" if color_cycle is None: color_cycle = cycle(colorsequence) flat_objs = {} - for subobj in obj_list_semi_flat: + for subobj in dict.fromkeys(obj_list_semi_flat): isCollection = getattr(subobj, "children", None) is not None - style = get_style(subobj, default_settings, **kwargs) + style_kwargs = {} if style_kwargs is None else style_kwargs + style = get_style(subobj, default_settings, **style_kwargs) if style.label is None: style.label = str(type(subobj).__name__) legendgroup = f"{subobj}" if parent_legendgroup is None else parent_legendgroup @@ -299,18 +331,17 @@ def get_flatten_objects_properties_recursive( "showlegend": parent_showlegend, } if isCollection: - flat_objs.update( - get_flatten_objects_properties_recursive( - *subobj.children, - colorsequence=colorsequence, - color_cycle=color_cycle, - parent_legendgroup=legendgroup, - parent_color=style.color, - parent_label=label, - parent_showlegend=style.legend.show, - **kwargs, - ) + new_ojbs = get_flatten_objects_properties_recursive( + *subobj.children, + colorsequence=colorsequence, + color_cycle=color_cycle, + parent_legendgroup=legendgroup, + parent_color=style.color, + parent_label=label, + parent_showlegend=style.legend.show, + style_kwargs=style_kwargs, ) + flat_objs = {**new_ojbs, **flat_objs} return flat_objs @@ -467,47 +498,82 @@ def getColorscale( return colorscale -def get_scene_ranges(*traces, zoom=1) -> np.ndarray: +def get_scene_ranges(*traces, zoom=0) -> np.ndarray: """ Returns 3x2 array of the min and max ranges in x,y,z directions of input traces. Traces can be any plotly trace object or a dict, with x,y,z numbered parameters. """ - trace3d_found = False - if traces: - ranges = {k: [] for k in "xyz"} - for tr in traces: - coords = "xyz" - if "constructor" in tr: - verts, *_ = get_vertices_from_model( - model_args=tr.get("args", None), - model_kwargs=tr.get("kwargs", None), - coordsargs=tr.get("coordsargs", None), - ) - tr = dict(zip("xyz", verts)) - if "z" in tr: # only extend range for 3d traces - trace3d_found = True - pts = np.array([tr[k] for k in coords], dtype="float64").T - try: # for mesh3d, use only vertices part of faces for range calculation - inds = np.array([tr[k] for k in "ijk"], dtype="int64").T - pts = pts[inds] - except KeyError: - # for 2d meshes, nothing special needed - pass - pts = pts.reshape(-1, 3) - if pts.size != 0: - min_max = np.nanmin(pts, axis=0), np.nanmax(pts, axis=0) - for v, min_, max_ in zip(ranges.values(), *min_max): - v.extend([min_, max_]) - if trace3d_found: + ranges_rc = {} + tr_dim_count = {} + for tr in traces: + coords = "xyz" + rc = tr.get("row", 1), tr.get("col", 1) + if "constructor" in tr: + verts, *_ = get_vertices_from_model( + model_args=tr.get("args", None), + model_kwargs=tr.get("kwargs", None), + coordsargs=tr.get("coordsargs", None), + ) + kwex = tr["kwargs_extra"] + tr = dict(zip("xyz", verts)) + rc = kwex["row"], kwex["col"] + if rc not in ranges_rc: + ranges_rc[rc] = {k: [] for k in "xyz"} + tr_dim_count[rc] = {"2D": 0, "3D": 0} + if "z" not in tr: # only extend range for 3d traces + tr_dim_count[rc]["2D"] += 1 + else: + tr_dim_count[rc]["3D"] += 1 + pts = np.array([tr[k] for k in coords], dtype="float64").T + try: # for mesh3d, use only vertices part of faces for range calculation + inds = np.array([tr[k] for k in "ijk"], dtype="int64").T + pts = pts[inds] + except KeyError: + # for 2d meshes, nothing special needed + pass + pts = pts.reshape(-1, 3) + if pts.size != 0: + min_max = np.nanmin(pts, axis=0), np.nanmax(pts, axis=0) + for v, min_, max_ in zip(ranges_rc[rc].values(), *min_max): + v.extend([min_, max_]) + for rc, ranges in ranges_rc.items(): + if tr_dim_count[rc]["3D"]: + zo = zoom[rc] if isinstance(zoom, dict) else zoom + # SET 3D PLOT BOUNDARIES + # collect min/max from all elements r = np.array([[np.nanmin(v), np.nanmax(v)] for v in ranges.values()]) size = np.diff(r, axis=1) - size[size == 0] = 1 m = size.max() / 2 + m = 1 if m == 0 else m center = r.mean(axis=1) - ranges = np.array([center - m * (1 + zoom), center + m * (1 + zoom)]).T - if not traces or not trace3d_found: - ranges = np.array([[-1.0, 1.0]] * 3) - return ranges + ranges = np.array([center - m * (1 + zo), center + m * (1 + zo)]).T + else: + ranges = np.array([[-1.0, 1.0]] * 3) + ranges_rc[rc] = ranges + if not ranges_rc: + ranges_rc[(1, 1)] = np.array([[-1.0, 1.0]] * 3) + return ranges_rc + + +def rescale_traces(traces, factors): + """Rescale traces based on scale factors by (row,col) index""" + for ind, tr in enumerate(traces): + if "constructor" in tr: + kwex = tr["kwargs_extra"] + rc = kwex["row"], kwex["col"] + kwargs, args = place_and_orient_model3d( + model_kwargs=tr.get("kwargs", None), + model_args=tr.get("args", None), + coordsargs=tr.get("coordsargs", None), + length_factor=factors[rc], + return_model_args=True, + ) + tr["kwargs"].update(kwargs) + tr["args"] = args + if "z" in tr: # rescale only 3d traces + rc = tr.get("row", 1), tr.get("col", 1) + traces[ind] = place_and_orient_model3d(tr, length_factor=factors[rc]) + return traces def group_traces(*traces): @@ -576,57 +642,52 @@ def subdivide_mesh_by_facecolor(trace): def process_show_input_objs(objs, **kwargs): """Extract max_rows and max_cols from obj list of dicts""" - defaults = { - "row": 1, - "col": 1, - "output": "model3d", - "sumup": True, - "pixel_agg": "mean", - "in_out": "auto", - } - max_rows = max_cols = 1 - flat_objs = [] - new_objs = {} - subplot_specs = {} + defaults = DEFAULT_ROW_COL_PARAMS.copy() + identifiers = ("row", "col") + unique_fields = tuple(k for k in defaults if k not in identifiers) + sources_and_sensors_only = [] + new_objs = [] for obj in objs: + # add missing kwargs if isinstance(obj, dict): obj = {**defaults, **obj, **kwargs} else: obj = {**defaults, "objects": obj, **kwargs} + # extend objects list obj["objects"] = format_obj_input( obj["objects"], allow="sources+sensors+collections" ) - flat_objs.extend(format_obj_input(obj["objects"], allow="sources+sensors")) - if obj["row"] is not None: - max_rows = max(max_rows, obj["row"]) - if obj["col"] is not None: - max_cols = max(max_cols, obj["col"]) - out = obj["output"] - key = (obj["row"], obj["col"], out if isinstance(out, str) else tuple(out)) - if key not in new_objs: - new_objs[key] = obj - else: - new_objs[key]["objects"] = list( - dict.fromkeys(new_objs[key]["objects"] + obj["objects"]) - ) - current_subplot_specs = subplot_specs.get(key[:2], obj["output"]) - if current_subplot_specs != obj["output"]: - raise ValueError( - f"Row/Col {key[:2]}, received conflicting output types " - f"{current_subplot_specs!r} vs {obj['output']!r}" - ) - subplot_specs[key[:2]] = obj["output"] + sources_and_sensors_only.extend( + format_obj_input(obj["objects"], allow="sources+sensors") + ) + new_objs.append(obj) + row_col_dict = merge_dicts_with_conflict_check( + new_objs, + target="objects", + identifiers=identifiers, + unique_fields=unique_fields, + ) + + # create subplot specs grid + row_cols = [*row_col_dict] + max_rows, max_cols = np.max(row_cols, axis=0).astype(int) if row_cols else (1, 1) + # convert to int to avoid np.int32 type conflicting with plolty subplot specs + max_rows, max_cols = int(max_rows), int(max_cols) specs = np.array([[{"type": "scene"}] * max_cols] * max_rows) - for inds, out in subplot_specs.items(): - if out != "model3d": - specs[inds[0] - 1, inds[1] - 1] = {"type": "xy"} + for rc, obj in row_col_dict.items(): + if obj["output"] != "model3d": + specs[rc[0] - 1, rc[1] - 1] = {"type": "xy"} if max_rows == 1 and max_cols == 1: max_rows = max_cols = None + + for obj in row_col_dict.values(): + check_input_zoom(obj.get("zoom", None)) + return ( - list(new_objs.values()), - list(dict.fromkeys(flat_objs)), + list(row_col_dict.values()), + list(dict.fromkeys(sources_and_sensors_only)), max_rows, max_cols, specs, diff --git a/magpylib/_src/exceptions.py b/magpylib/_src/exceptions.py index 80aeb5f93..459b9d740 100644 --- a/magpylib/_src/exceptions.py +++ b/magpylib/_src/exceptions.py @@ -1,4 +1,4 @@ -""" Definition of custom exceptions""" +"""Definition of custom exceptions""" class MagpylibBadUserInput(Exception): diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index da82a8a99..43580e1db 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -41,8 +41,24 @@ def current_circle_Hfield( Returns ------- - B-field: ndarray, shape (n,3) - B-field generated by Loops at observer positions. + B-field: ndarray, shape (3,n) + B-field generated by Loops at observer positions in cylinder coordinates. + + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> H = magpy.core.current_circle_Hfield( + ... r0=np.array([1,2]), + ... r =np.array([1,1]), + ... z =np.array([1,2]), + ... i0=np.array([1,3]), + ... ) + >>> with np.printoptions(precision=3): + ... print(H) + [[0.091 0.094] + [0. 0. ] + [0.077 0.226]] Notes ----- @@ -78,7 +94,7 @@ def current_circle_Hfield( Hz = -pf * cel_iter(q, p, np.ones(n5), cc, ss, p, q) # input is I -> output must be H-field - return np.row_stack((Hr, np.zeros(n5), Hz)) * 795774.7154594767 # *1e7/4/np.pi + return np.vstack((Hr, np.zeros(n5), Hz)) * 795774.7154594767 # *1e7/4/np.pi def BHJM_circle( diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 19365d78a..2204187b2 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -39,12 +39,26 @@ def magnet_cuboid_Bfield( B-Field: ndarray, shape (n,3) B-field generated by Cuboids at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.magnet_cuboid_Bfield( + ... observers=np.array([(1,1,1), (2,2,2)]), + ... dimensions=np.array([(1,1,1), (1,2,3)]), + ... polarizations=np.array([(0,0,1), (.5,.5,0)]), + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[ 1.561e-02 1.561e-02 -3.534e-17] + [ 7.732e-03 6.544e-03 1.048e-02]] + Notes ----- Field computations via magnetic surface charge density. Published several times with similar expressions: - Yang: Superconductor Science and Technology 3(12):591 (1999) + Yang: Superconductor Science and Technology 3(12):591 (1990) Engel-Herbert: Journal of Applied Physics 97(7):074504 - 074504-4 (2005) diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index e71eea7d0..7f3c826d5 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -4,6 +4,7 @@ """ # pylint: disable = no-name-in-module + import numpy as np from scipy.constants import mu_0 as MU0 from scipy.special import ellipe @@ -39,6 +40,21 @@ def magnet_cylinder_axial_Bfield(z0: np.ndarray, r: np.ndarray, z: np.ndarray) - B-field: ndarray, (Br, Bphi, Bz) B-field generated by Cylinders at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.magnet_cylinder_axial_Bfield( + ... z0=np.array([1,2]), + ... r =np.array([1,2]), + ... z =np.array([2,3]), + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[0.056 0.041] + [0. 0. ] + [0.067 0.018]] + Notes ----- Implementation based on Derby, American Journal of Physics 78.3 (2010): 229-235. @@ -71,7 +87,7 @@ def magnet_cylinder_axial_Bfield(z0: np.ndarray, r: np.ndarray, z: np.ndarray) - / np.pi ) - return np.row_stack((Br, np.zeros(n), Bz)) + return np.vstack((Br, np.zeros(n), Bz)) # CORE @@ -90,22 +106,39 @@ def magnet_cylinder_diametral_Hfield( Parameters ---------- - z0: ndarray, shape (n) + z0: ndarray, shape (n,) Ratios of cylinder heights over cylinder radii. - r: ndarray, shape (n) + r: ndarray, shape (n,) Ratios of radial observer positions over cylinder radii. - z: ndarray, shape (n) + z: ndarray, shape (n,) Ratios of axial observer positions over cylinder radii. - phi: Azimuth angles between observers and magnetization directions. + phi: ndarray, shape(n,), unit rad + Azimuth angles between observers and magnetization directions. Returns ------- H-Field: np.ndarray, (Hr, Hphi, Hz) H-field generated by Cylinders at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.magnet_cylinder_diametral_Hfield( + ... z0=np.array([1,2]), + ... r =np.array([1,2]), + ... z =np.array([2,3]), + ... phi=np.array([.1,np.pi/4]), + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[-0.021 0.007] + [ 0.005 0.02 ] + [ 0.055 0.029]] + Notes ----- Implementation partially based on Caciagli: Journal of Magnetism and @@ -257,7 +290,7 @@ def magnet_cylinder_diametral_Hfield( ) ) - return np.row_stack((Hr, Hphi, Hz)) + return np.vstack((Hr, Hphi, Hz)) def BHJM_magnet_cylinder( diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 1a0f289e2..f09a7d8e9 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -3,6 +3,8 @@ # pylint: disable=missing-function-docstring # pylint: disable=no-name-in-module # pylint: disable=too-many-statements +# pylint: disable=too-many-positional-arguments + import numpy as np from scipy.constants import mu_0 as MU0 from scipy.special import ellipeinc @@ -2139,6 +2141,20 @@ def magnet_cylinder_segment_Hfield( H-field: ndarray, shape (n,3) H-field generated by Cylinder Segments at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.magnet_cylinder_segment_Hfield( + ... observers=np.array([(1,1,2), (0,0,0)]), + ... dimensions=np.array([(1,2,.1,.2,-1,1), (1,2,.3,.9,0,1)]), + ... magnetizations=np.array([(1e7,.1,.2), (1e6,1.1,2.2)]), + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[-1948.144 32319.944 17616.886] + [14167.65 1419.941 17921.646]] + Notes ----- Implementation based on F.Slanovc, Journal of Magnetism and Magnetic @@ -2279,7 +2295,7 @@ def magnet_cylinder_segment_Hfield( result = np.sum(result[:, (1, 2, 4, 7)] - result[:, (0, 3, 5, 6)], axis=(1, 3)) # multiply with magnetization amplitude - result = result.T * magnetizations[:, 0] * 1e-7 + result = result.T * magnetizations[:, 0] * 1e-7 / MU0 return result.T @@ -2430,7 +2446,7 @@ def BHJM_cylinder_segment( Hr, Hphi, Hz = H_cy.T Hx = Hr * np.cos(phi) - Hphi * np.sin(phi) Hy = Hr * np.sin(phi) + Hphi * np.cos(phi) - BHJM[mask_not_on_surf] = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T / MU0 + BHJM[mask_not_on_surf] = np.concatenate(((Hx,), (Hy,), (Hz,)), axis=0).T if field == "H": return BHJM diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index ac7455d6a..7b377336a 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -34,6 +34,19 @@ def dipole_Hfield( H-field: ndarray, shape (n,3) H-field of Dipole in Cartesian coordinates. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> H = magpy.core.dipole_Hfield( + ... observers=np.array([(1,1,1), (2,2,2)]), + ... moments=np.array([(1e5,0,0), (0,0,1e5)]), + ... ) + >>> with np.printoptions(precision=3): + ... print(H) + [[2.895e-13 1.531e+03 1.531e+03] + [1.914e+02 1.914e+02 3.619e-14]] + Notes ----- The moment of a magnet is given by its volume*magnetization. @@ -58,7 +71,7 @@ def dipole_Hfield( if np.any(mask1): with np.errstate(divide="ignore", invalid="ignore"): H[mask1] = moments[mask1] / 0.0 - np.nan_to_num(H, copy=False, posinf=np.inf, neginf=np.NINF) + np.nan_to_num(H, copy=False, posinf=np.inf, neginf=-np.inf) return H diff --git a/magpylib/_src/fields/field_BH_polyline.py b/magpylib/_src/fields/field_BH_polyline.py index 47005a2b3..67b1a875a 100644 --- a/magpylib/_src/fields/field_BH_polyline.py +++ b/magpylib/_src/fields/field_BH_polyline.py @@ -2,6 +2,8 @@ Implementations of analytical expressions of line current segments """ +# pylint: disable=too-many-positional-arguments + import numpy as np from numpy.linalg import norm from scipy.constants import mu_0 as MU0 @@ -98,10 +100,28 @@ def current_polyline_Hfield( B-Field: ndarray, shape (n,3) B-field generated by current segments at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> H = magpy.core.current_polyline_Hfield( + ... observers=np.array([(1,1,1), (2,2,2)]), + ... segments_start=np.array([(0,0,0), (0,0,0)]), + ... segments_end=np.array([(1,0,0), (-1,0,0)]), + ... currents=np.array([100,200]), + ... ) + >>> with np.printoptions(precision=3): + ... print(H) + [[ 0. -2.297 2.297] + [ 0. 0.598 -0.598]] + Notes ----- Field computation via law of Biot Savart. See also countless online resources. eg. http://www.phys.uri.edu/gerhard/PHY204/tsl216.pdf + + Be careful with magnetic fields of discontinued segements. They are + unphysical and can lead to unphysical effects. """ # rename p1, p2, po = segments_start, segments_end, observers diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 379d761cf..f3d58d3f5 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -37,6 +37,20 @@ def magnet_sphere_Bfield( B-field: ndarray, shape (n,3) B-field generated by Spheres at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.magnet_sphere_Bfield( + ... observers=np.array([(1,1,1), (2,2,2)]), + ... diameters=np.array([1,2]), + ... polarizations=np.array([(1,0,0), (1,1,0)]), + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[1.187e-18 8.019e-03 8.019e-03] + [8.019e-03 8.019e-03 1.604e-02]] + Notes ----- The field corresponds to a dipole field on the outside and is 2/3*mag diff --git a/magpylib/_src/fields/field_BH_triangle.py b/magpylib/_src/fields/field_BH_triangle.py index 3d96a98a6..ff23601ba 100644 --- a/magpylib/_src/fields/field_BH_triangle.py +++ b/magpylib/_src/fields/field_BH_triangle.py @@ -107,6 +107,20 @@ def triangle_Bfield( B-field: ndarray, shape (n,3) B-field generated by Triangles at observer positions. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> B = magpy.core.triangle_Bfield( + ... observers=np.array([(2,1,1), (2,2,2)]), + ... vertices=np.array([[(0,0,0),(0,0,1),(1,0,0)], [(0,0,0),(0,0,1),(1,0,0)]]), + ... polarizations=np.array([(1,1,1), (1,1,0)])*1e3, + ... ) + >>> with np.printoptions(precision=3): + ... print(B) + [[7.452 4.62 3.136] + [2.213 2.677 2.213]] + Notes ----- Field computations implemented from Guptasarma, Geophysics, 1999, 64:1, 70-74. diff --git a/magpylib/_src/fields/field_wrap_BH.py b/magpylib/_src/fields/field_wrap_BH.py index 6d81b78de..a4209f0b6 100644 --- a/magpylib/_src/fields/field_wrap_BH.py +++ b/magpylib/_src/fields/field_wrap_BH.py @@ -1,5 +1,7 @@ # pylint: disable=cyclic-import # pylint: disable=too-many-lines +# pylint: disable=too-many-positional-arguments + """Field computation structure: level_core:(field_BH_XXX.py files) @@ -689,33 +691,37 @@ def getB( In this example we compute the B-field in T of a spherical magnet and a current loop at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src1 = magpy.current.Circle(current=100, diameter=.002) >>> src2 = magpy.magnet.Sphere(polarization=(0,0,.1), diameter=.001) >>> B = magpy.getB([src1, src2], (.01,.01,.01)) - >>> print(B) - [[ 6.05434592e-06 6.05434592e-06 2.35680448e-08] - [ 8.01875374e-07 8.01875374e-07 -9.05619815e-23]] + >>> with np.printoptions(precision=3): + ... print(B) + [[ 6.054e-06 6.054e-06 2.357e-08] + [ 8.019e-07 8.019e-07 -9.056e-23]] We can also use sensor objects as observers input: >>> sens1 = magpy.Sensor(position=(.01,.01,.01)) >>> sens2 = sens1.copy(position=(.01,.01,-.01)) >>> B = magpy.getB([src1, src2], [sens1, sens2]) - >>> print(B) - [[[ 6.05434592e-06 6.05434592e-06 2.35680448e-08] - [-6.05434592e-06 -6.05434592e-06 2.35680448e-08]] + >>> with np.printoptions(precision=3): + ... print(B) + [[[ 6.054e-06 6.054e-06 2.357e-08] + [-6.054e-06 -6.054e-06 2.357e-08]] <BLANKLINE> - [[ 8.01875374e-07 8.01875374e-07 -9.05619815e-23] - [-8.01875374e-07 -8.01875374e-07 -9.05619815e-23]]] + [[ 8.019e-07 8.019e-07 -9.056e-23] + [-8.019e-07 -8.019e-07 -9.056e-23]]] Through the functional interface we can compute the same fields for the loop as: >>> obs = [(.01,.01,.01), (.01,.01,-.01)] >>> B = magpy.getB('Circle', obs, current=100, diameter=.002) - >>> print(B) - [[ 6.05434592e-06 6.05434592e-06 2.35680448e-08] - [-6.05434592e-06 -6.05434592e-06 2.35680448e-08]] + >>> with np.printoptions(precision=3): + ... print(B) + [[ 6.054e-06 6.054e-06 2.357e-08] + [-6.054e-06 -6.054e-06 2.357e-08]] But also for a set of four completely different instances: @@ -726,11 +732,12 @@ def getB( ... diameter=(.001, .002, .003, .004), ... position=((0,0,0), (0,0,.01), (0,0,.02), (0,0,.03)), ... ) - >>> print(B) - [[ 1.66322588e-07 1.66322588e-07 1.61742625e-10] - [-4.69451598e-07 -4.69451598e-07 4.70690813e-07] - [ 7.96993186e-07 1.59398637e-06 -7.91258466e-07] - [-1.37369334e-06 -1.37369334e-06 -1.36554287e-06]] + >>> with np.printoptions(precision=3): + ... print(B) + [[ 1.663e-07 1.663e-07 1.617e-10] + [-4.695e-07 -4.695e-07 4.707e-07] + [ 7.970e-07 1.594e-06 -7.913e-07] + [-1.374e-06 -1.374e-06 -1.366e-06]] """ return getBH_level2( sources, @@ -875,33 +882,37 @@ def getH( In this example we compute the H-field in A/m of a spherical magnet and a current loop at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src1 = magpy.current.Circle(current=100, diameter=.002) >>> src2 = magpy.magnet.Sphere(polarization=(0,0,.1), diameter=.001) >>> H = magpy.getH([src1, src2], (.01,.01,.01)) - >>> print(H) - [[ 4.81789540e+00 4.81789540e+00 1.87548541e-02] - [ 6.38112147e-01 6.38112147e-01 -7.20669350e-17]] + >>> with np.printoptions(precision=3): + ... print(H) + [[ 4.818e+00 4.818e+00 1.875e-02] + [ 6.381e-01 6.381e-01 -7.207e-17]] We can also use sensor objects as observers input: >>> sens1 = magpy.Sensor(position=(.01,.01,.01)) >>> sens2 = sens1.copy(position=(.01,.01,-.01)) >>> H = magpy.getH([src1, src2], [sens1, sens2]) - >>> print(H) - [[[ 4.81789540e+00 4.81789540e+00 1.87548541e-02] - [-4.81789540e+00 -4.81789540e+00 1.87548541e-02]] + >>> with np.printoptions(precision=3): + ... print(H) + [[[ 4.818e+00 4.818e+00 1.875e-02] + [-4.818e+00 -4.818e+00 1.875e-02]] <BLANKLINE> - [[ 6.38112147e-01 6.38112147e-01 -7.20669350e-17] - [-6.38112147e-01 -6.38112147e-01 -7.20669350e-17]]] + [[ 6.381e-01 6.381e-01 -7.207e-17] + [-6.381e-01 -6.381e-01 -7.207e-17]]] Through the functional interface we can compute the same fields for the loop as: >>> obs = [(.01,.01,.01), (.01,.01,-.01)] >>> H = magpy.getH('Circle', obs, current=100, diameter=.002) - >>> print(H) - [[ 4.8178954 4.8178954 0.01875485] - [-4.8178954 -4.8178954 0.01875485]] + >>> with np.printoptions(precision=3): + ... print(H) + [[ 4.818 4.818 0.019] + [-4.818 -4.818 0.019]] But also for a set of four completely different instances: @@ -912,11 +923,12 @@ def getH( ... diameter=(.001, .002, .003, .004), ... position=((0,0,0), (0,0,.01), (0,0,.02), (0,0,.03)), ... ) - >>> print(H) - [[ 1.32355310e-01 1.32355310e-01 1.28710691e-04] - [-3.73577711e-01 -3.73577711e-01 3.74563848e-01] - [ 6.34227026e-01 1.26845405e+00 -6.29663481e-01] - [-1.09315042e+00 -1.09315042e+00 -1.08666449e+00]] + >>> with np.printoptions(precision=3): + ... print(H) + [[ 1.324e-01 1.324e-01 1.287e-04] + [-3.736e-01 -3.736e-01 3.746e-01] + [ 6.342e-01 1.268e+00 -6.297e-01] + [-1.093e+00 -1.093e+00 -1.087e+00]] """ return getBH_level2( sources, @@ -1050,6 +1062,21 @@ def getM( Functional interface: ndarray, shape (n,3) M-field for every parameter set in units of A/m. + Examples + -------- + In this example we test the magnetization at an observer point. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> M = cube.getM((3,3,0)) + >>> with np.printoptions(precision=0): + ... print(M) + [562698. 562698. 0.] + Notes ----- This function automatically joins all sensor and position inputs together and groups @@ -1188,6 +1215,32 @@ def getJ( Functional interface: ndarray, shape (n,3) J-field for every parameter set in units of T. + Returns + ------- + M-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame + M-field at each path position (index m) for each sensor (index k) and each sensor pixel + position (indices n1, n2, ...) in units of A/m. Sensor pixel positions are equivalent + to simple observer positions. Paths of objects that are shorter than index m are + considered as static beyond their end. + + Functional interface: ndarray, shape (n,3) + M-field for every parameter set in units of A/m. + + Examples + -------- + In this example we test the polarization at an observer point. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> J = cube.getJ((3,3,0)) + >>> with np.printoptions(precision=3): + ... print(J) + [0.707 0.707 0. ] + Notes ----- This function automatically joins all sensor and position inputs together and groups diff --git a/magpylib/_src/fields/special_cel.py b/magpylib/_src/fields/special_cel.py index f9690fd4a..71bf2ca26 100644 --- a/magpylib/_src/fields/special_cel.py +++ b/magpylib/_src/fields/special_cel.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + import math as m import numpy as np @@ -96,7 +98,7 @@ def celv(kc, p, c, s): mask = np.ones(n, dtype=bool) while np.any(mask): k[mask] = 2 * np.sqrt(kk[mask]) - kk[mask] = np.copy(k[mask] * em[mask]) + kk[mask] = k[mask] * em[mask] f[mask] = cc[mask] cc[mask] = cc[mask] + ss[mask] / pp[mask] g[mask] = kk[mask] / pp[mask] diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index b4b0a6d20..0ed91df4f 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -1,7 +1,9 @@ -""" input checks code""" +"""input checks code""" # pylint: disable=import-outside-toplevel # pylint: disable=cyclic-import +# pylint: disable=too-many-positional-arguments + import inspect import numbers @@ -69,15 +71,10 @@ def check_array_shape(inp: np.ndarray, dims: tuple, shape_m1: int, length=None, def check_input_zoom(inp): """check show zoom input""" - if not isinstance(inp, numbers.Number): - raise MagpylibBadUserInput( - "Input parameter `zoom` must be a number `zoom>=0`.\n" - f"Instead received {inp}." - ) - if inp < 0: + if not (isinstance(inp, numbers.Number) and inp >= 0): raise MagpylibBadUserInput( - "Input parameter `zoom` must be a number `zoom>=0`.\n" - f"Instead received {inp}." + "Input parameter `zoom` must be a positive number or zero.\n" + f"Instead received {inp!r}." ) @@ -85,7 +82,7 @@ def check_input_animation(inp): """check show animation input""" ERR_MSG = ( "Input parameter `animation` must be boolean or a positive number.\n" - f"Instead received {inp}." + f"Instead received {inp!r}." ) if not isinstance(inp, numbers.Number): raise MagpylibBadUserInput(ERR_MSG) @@ -105,7 +102,7 @@ def check_start_type(inp): ): raise MagpylibBadUserInput( f"Input parameter `start` must be integer value or 'auto'.\n" - f"Instead received {repr(inp)}." + f"Instead received {inp!r}." ) @@ -114,7 +111,7 @@ def check_degree_type(inp): if not isinstance(inp, bool): raise MagpylibBadUserInput( "Input parameter `degrees` must be boolean (`True` or `False`).\n" - f"Instead received {repr(inp)}." + f"Instead received {inp!r}." ) @@ -124,7 +121,7 @@ def check_field_input(inp): if not (isinstance(inp, str) and inp in allowed): raise MagpylibBadUserInput( f"`field` input can only be one of {allowed}.\n" - f"Instead received {repr(inp)}." + f"Instead received {inp!r}." ) @@ -139,7 +136,7 @@ def validate_field_func(val): if not callable(val): raise MagpylibBadUserInput( "Input parameter `field_func` must be a callable.\n" - f"Instead received {type(val).__name__}." + f"Instead received {type(val).__name__!r}." ) fn_args = inspect.getfullargspec(val).args @@ -147,7 +144,7 @@ def validate_field_func(val): raise MagpylibBadUserInput( "Input parameter `field_func` must have two positional args" " called 'field' and 'observers'.\n" - f"Instead received a callable where the first two args are: {fn_args[:2]}" + f"Instead received a callable where the first two args are: {fn_args[:2]!r}" ) for field in ["B", "H"]: @@ -157,13 +154,14 @@ def validate_field_func(val): raise MagpylibBadUserInput( "Input parameter `field_func` must be a callable that returns B- and H-field" " as numpy ndarray.\n" - f"Instead it returns type {type(out)} for {field}-field." + f"Instead it returns type {type(out)!r} for {field}-field." ) if out.shape != (2, 3): raise MagpylibBadUserInput( "Input parameter `field_func` must be a callable that returns B- and H-field" " as numpy ndarray with shape (n,3), when `observers` input is shape (n,3).\n" - f"Instead it returns shape {out.shape} for {field}-field for input shape (2,3)" + f"Instead it returns shape {out.shape} for {field}-field for input shape " + "(2,3)" ) return None @@ -190,7 +188,7 @@ def check_format_input_orientation(inp, init_format=False): if not isinstance(inp, (Rotation, type(None))): raise MagpylibBadUserInput( f"Input parameter `orientation` must be `None` or scipy `Rotation` object.\n" - f"Instead received type {type(inp)}." + f"Instead received type {type(inp)!r}." ) # handle None input and compute inpQ if inp is None: @@ -239,7 +237,7 @@ def check_format_input_axis(inp): return np.array((0, 0, 1)) raise MagpylibBadUserInput( "Input parameter `axis` must be array_like shape (3,) or one of ['x', 'y', 'z'].\n" - f"Instead received string {inp}.\n" + f"Instead received string {inp!r}.\n" ) inp = check_format_input_vector( @@ -291,7 +289,7 @@ def check_format_input_scalar( ERR_MSG = ( f"Input parameter `{sig_name}` must be {sig_type}.\n" - f"Instead received {repr(inp)}." + f"Instead received {inp!r}." ) if not isinstance(inp, numbers.Number): @@ -332,7 +330,7 @@ def check_format_input_vector( is_array_like( inp, f"Input parameter `{sig_name}` must be {sig_type}.\n" - f"Instead received type {type(inp)}.", + f"Instead received type {type(inp)!r}.", ) inp = make_float_array( inp, @@ -372,7 +370,7 @@ def check_format_input_vector2( is_array_like( inp, f"Input parameter `{param_name}` must be array_like.\n" - f"Instead received type {type(inp)}.", + f"Instead received type {type(inp)!r}.", ) inp = make_float_array( inp, @@ -436,8 +434,8 @@ def check_format_input_cylinder_segment(inp): if case2 | case3 | case4 | case5: raise MagpylibBadUserInput( f"Input parameter `CylinderSegment.dimension` must be array_like of the form" - f" (r1, r2, h, phi1, phi2) with 0<=r1<r2, h>0, phi1<phi2 and phi2-phi1<=360,\n" - f"but received {inp} instead." + f" (r1, r2, h, phi1, phi2) with 0<=r1<r2, h>0, phi1<phi2 and phi2-phi1<=360," + f"\nInstead received {inp!r}." ) return inp @@ -450,8 +448,8 @@ def check_format_input_backend(inp): if inp in backends: return inp raise MagpylibBadUserInput( - f"Input parameter `backend` must be one of `{backends+[None]}`.\n" - f"Instead received {inp}." + f"Input parameter `backend` must be one of `{backends+[None]}`." + f"\nInstead received {inp!r}." ) @@ -545,9 +543,6 @@ def check_format_input_obj( if "collections" in allow.split("+"): wanted_types += (Collection,) - if typechecks: - all_types = (BaseSource, Sensor, Collection) - obj_list = [] for obj in inp: # add to list if wanted type @@ -564,10 +559,11 @@ def check_format_input_obj( ) # typechecks - if typechecks and not isinstance(obj, all_types): + # pylint disable possibly-used-before-assignment + if typechecks and not isinstance(obj, (BaseSource, Sensor, Collection)): raise MagpylibBadUserInput( f"Input objects must be {allow} or a flat list thereof.\n" - f"Instead received {type(obj)}." + f"Instead received {type(obj)!r}." ) return obj_list @@ -610,8 +606,8 @@ def check_format_pixel_agg(pixel_agg): PIXEL_AGG_ERR_MSG = ( "Input `pixel_agg` must be a reference to a numpy callable that reduces" - + " an array shape like 'mean', 'std', 'median', 'min', ...\n" - + f"Instead received {pixel_agg}." + " an array shape like 'mean', 'std', 'median', 'min', ..." + f"\nInstead received {pixel_agg!r}." ) if pixel_agg is None: @@ -637,7 +633,7 @@ def check_getBH_output_type(output): if output not in acceptable: raise ValueError( f"The `output` argument must be one of {acceptable}." - f"\nInstead received {output}." + f"\nInstead received {output!r}." ) if output == "dataframe": try: @@ -652,3 +648,14 @@ def check_getBH_output_type(output): ) from missing_module return output + + +def check_input_canvas_update(canvas_update, canvas): + """chekc if canvas_update is acceptable also depending on canvas input""" + acceptable = (True, False, "auto", None) + if canvas_update not in acceptable: + raise ValueError( + f"The `canvas_update` must be one of {acceptable}" + f"\nInstead received {canvas_update!r}." + ) + return canvas is None if canvas_update in (None, "auto") else canvas_update diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index e6d5e1733..f9b4ada82 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -1,6 +1,8 @@ """BaseHomMag class code""" # pylint: disable=cyclic-import +# pylint: disable=too-many-positional-arguments + import warnings import numpy as np @@ -107,25 +109,28 @@ def getB( -------- Compute the B-field of a spherical magnet at three positions: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> B = src.getB(((0,0,0), (1,0,0), (2,0,0))) - >>> print(B) - [[ 0. 0. 0.66666667] - [ 0. 0. -0.04166667] - [ 0. 0. -0.00520833]] + >>> with np.printoptions(precision=3): + ... print(B) + [[ 0. 0. 0.667] + [ 0. 0. -0.042] + [ 0. 0. -0.005]] Compute the B-field at two sensors, each one with two pixels >>> sens1 = magpy.Sensor(position=(1,0,0), pixel=((0,0,.1), (0,0,-.1))) >>> sens2 = sens1.copy(position=(2,0,0)) >>> B = src.getB(sens1, sens2) - >>> print(B) - [[[ 0.01219289 0. -0.0398301 ] - [-0.01219289 0. -0.0398301 ]] + >>> with np.printoptions(precision=3): + ... print(B) + [[[ 0.012 0. -0.04 ] + [-0.012 0. -0.04 ]] <BLANKLINE> - [[ 0.00077639 0. -0.00515004] - [-0.00077639 0. -0.00515004]]] + [[ 0.001 0. -0.005] + [-0.001 0. -0.005]]] """ observers = format_star_input(observers) return getBH_level2( @@ -192,27 +197,29 @@ def getH( -------- Compute the H-field of a spherical magnet at three positions: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> H = src.getH(((0,0,0), (1,0,0), (2,0,0))) - >>> print(H) - [[ 0. 0. -265258.23834209] - [ 0. 0. -33157.27979276] - [ 0. 0. -4144.6599741 ]] + >>> with np.printoptions(precision=0): + ... print(H) + [[ 0. 0. -265258.] + [ 0. 0. -33157.] + [ 0. 0. -4145.]] Compute the H-field at two sensors, each one with two pixels >>> sens1 = magpy.Sensor(position=(1,0,0), pixel=((0,0,.1), (0,0,-.1))) >>> sens2 = sens1.copy(position=(2,0,0)) >>> H = src.getH(sens1, sens2) - >>> print(H) - [[[ 9702.79184001 0. -31695.78667738] - [ -9702.79184001 0. -31695.78667738]] + >>> with np.printoptions(precision=0): + ... print(H) + [[[ 9703. 0. -31696.] + [ -9703. 0. -31696.]] <BLANKLINE> - [[ 617.83031344 0. -4098.27441249] - [ -617.83031344 0. -4098.27441249]]] - + [[ 618. 0. -4098.] + [ -618. 0. -4098.]]] """ observers = format_star_input(observers) return getBH_level2( @@ -274,6 +281,21 @@ def getM( pixel position (indices n1,n2,...) in units of A/m. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than index m will be considered as static beyond their end. + + Examples + -------- + In this example we test the magnetization at an observer point. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> M = cube.getM((3,3,0)) + >>> with np.printoptions(precision=0): + ... print(M) + [562698. 562698. 0.] """ observers = format_star_input(observers) return getBH_level2( @@ -337,6 +359,21 @@ def getJ( pixel position (indices n1,n2,...) in units of T. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than index m will be considered as static beyond their end. + + Examples + -------- + In this example we test the polarization at an observer point. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> J = cube.getJ((3,3,0)) + >>> with np.printoptions(precision=3): + ... print(J) + [0.707 0.707 0. ] """ observers = format_star_input(observers) return getBH_level2( diff --git a/magpylib/_src/obj_classes/class_BaseTransform.py b/magpylib/_src/obj_classes/class_BaseTransform.py index ecabe5852..9b0c1875f 100644 --- a/magpylib/_src/obj_classes/class_BaseTransform.py +++ b/magpylib/_src/obj_classes/class_BaseTransform.py @@ -1,6 +1,8 @@ """BaseTransform class code""" # pylint: disable=protected-access +# pylint: disable=too-many-positional-arguments + import numbers import numpy as np diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 0e79fa38b..a880385a8 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -2,6 +2,8 @@ # pylint: disable=redefined-builtin # pylint: disable=import-outside-toplevel +# pylint: disable=too-many-positional-arguments + from collections import Counter from magpylib._src.defaults.defaults_utility import validate_style_keys @@ -506,6 +508,7 @@ def _validate_getBH_inputs(self, *inputs): """ # pylint: disable=protected-access # pylint: disable=too-many-branches + # pylint: disable=possibly-used-before-assignment current_sources = format_obj_input(self, allow="sources") current_sensors = format_obj_input(self, allow="sensors") @@ -531,9 +534,7 @@ def _validate_getBH_inputs(self, *inputs): return sources, sensors - def getB( - self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" - ): + def getB(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): """Compute B-field for given sources and observers. SI units are used for all inputs and outputs. @@ -560,19 +561,6 @@ def getB( `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). - in_out: {'auto', 'inside', 'outside'} - This parameter only applies for magnet bodies. It specifies the location of the - observers relative to the magnet body, affecting the calculation of the magnetic field. - The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for - each observer. - - 'inside': All observers are considered to be inside the cuboid; use this for - performance optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for - performance optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. - Returns ------- B-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame @@ -585,26 +573,27 @@ def getB( -------- In this example we create a collection from two sources and two sensors: + >>> import numpy as np >>> import magpylib as magpy + >>> # Create Collection with two sensors and two magnets >>> src1 = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> src2 = src1.copy() - >>> sens1 = magpy.Sensor(position=(0,0,1)) + >>> sens1 = magpy.Sensor(position=(0,0,.6)) >>> sens2 = sens1.copy() >>> col = src1 + src2 + sens1 + sens2 - - The following computations all give the same result: - + >>> # The following computations al give the same result >>> B = col.getB() >>> B = magpy.getB(col, col) >>> B = magpy.getB(col, [sens1, sens2]) >>> B = magpy.getB([src1, src2], col) >>> B = magpy.getB([src1, src2], [sens1, sens2]) - >>> print(B) - [[[0. 0. 0.08333333] - [0. 0. 0.08333333]] + >>> with np.printoptions(precision=3): + ... print(B) + [[[0. 0. 0.386] + [0. 0. 0.386]] <BLANKLINE> - [[0. 0. 0.08333333] - [0. 0. 0.08333333]]] + [[0. 0. 0.386] + [0. 0. 0.386]]] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -616,12 +605,10 @@ def getB( squeeze=squeeze, pixel_agg=pixel_agg, output=output, - in_out=in_out, + in_out="auto", ) - def getH( - self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" - ): + def getH(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): """Compute H-field for given sources and observers. SI units are used for all inputs and outputs. @@ -656,43 +643,31 @@ def getH( equivalent to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. - in_out: {'auto', 'inside', 'outside'} - This parameter only applies for magnet bodies. It specifies the location of the - observers relative to the magnet body, affecting the calculation of the magnetic field. - The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for - each observer. - - 'inside': All observers are considered to be inside the cuboid; use this for - performance optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for - performance optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. - Examples -------- In this example we create a collection from two sources and two sensors: + >>> import numpy as np >>> import magpylib as magpy + >>> # Create Collection with two sensors and two magnets >>> src1 = magpy.magnet.Sphere(polarization=(0,0,1.), diameter=1) >>> src2 = src1.copy() >>> sens1 = magpy.Sensor(position=(0,0,1)) >>> sens2 = sens1.copy() >>> col = src1 + src2 + sens1 + sens2 - - The following computations all give the same result: - + >>> # The following computations al give the same result >>> H = col.getH() >>> H = magpy.getH(col, col) >>> H = magpy.getH(col, [sens1, sens2]) >>> H = magpy.getH([src1, src2], col) >>> H = magpy.getH([src1, src2], [sens1, sens2]) - >>> print(H) - [[[ 0. 0. 66314.55958552] - [ 0. 0. 66314.55958552]] + >>> with np.printoptions(precision=3): + ... print(H) + [[[ 0. 0. 66314.56] + [ 0. 0. 66314.56]] <BLANKLINE> - [[ 0. 0. 66314.55958552] - [ 0. 0. 66314.55958552]]] + [[ 0. 0. 66314.56] + [ 0. 0. 66314.56]]] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -705,12 +680,10 @@ def getH( squeeze=squeeze, pixel_agg=pixel_agg, output=output, - in_out=in_out, + in_out="auto", ) - def getM( - self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" - ): + def getM(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): """Compute M-field for given sources and observers. SI units are used for all inputs and outputs. @@ -745,18 +718,19 @@ def getM( equivalent to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. - in_out: {'auto', 'inside', 'outside'} - This parameter only applies for magnet bodies. It specifies the location of the - observers relative to the magnet body, affecting the calculation of the magnetic field. - The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for - each observer. - - 'inside': All observers are considered to be inside the cuboid; use this for - performance optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for - performance optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> coll = magpy.Collection(cube) + >>> M = coll.getM((3,3,0)) + >>> with np.printoptions(precision=0): + ... print(M) + [562698. 562698. 0.] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -769,12 +743,10 @@ def getM( squeeze=squeeze, pixel_agg=pixel_agg, output=output, - in_out=in_out, + in_out="auto", ) - def getJ( - self, *inputs, squeeze=True, pixel_agg=None, output="ndarray", in_out="auto" - ): + def getJ(self, *inputs, squeeze=True, pixel_agg=None, output="ndarray"): """Compute J-field for given sources and observers. SI units are used for all inputs and outputs. @@ -801,19 +773,6 @@ def getJ( `numpy.ndarray` object is returned. If 'dataframe' is chosen, a `pandas.DataFrame` object is returned (the Pandas library must be installed). - in_out: {'auto', 'inside', 'outside'} - This parameter only applies for magnet bodies. It specifies the location of the - observers relative to the magnet body, affecting the calculation of the magnetic field. - The options are: - - 'auto': The location (inside or outside the cuboid) is determined automatically for - each observer. - - 'inside': All observers are considered to be inside the cuboid; use this for - performance optimization if applicable. - - 'outside': All observers are considered to be outside the cuboid; use this for - performance optimization if applicable. - Choosing 'auto' is fail-safe but may be computationally intensive if the mix of observer - locations is unknown. - Returns ------- J-field: ndarray, shape squeeze(m, k, n1, n2, ..., 3) or DataFrame @@ -821,6 +780,20 @@ def getJ( sensor pixel position (indices n1,n2,...) in units of T. Sensor pixel positions are equivalent to simple observer positions. Paths of objects that are shorter than index m are considered as static beyond their end. + + Examples + -------- + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ).rotate_from_angax(45,'z') + >>> coll = magpy.Collection(cube) + >>> J = coll.getJ((3,3,0)) + >>> with np.printoptions(precision=3): + ... print(J) + [0.707 0.707 0. ] """ sources, sensors = self._validate_getBH_inputs(*inputs) @@ -833,7 +806,7 @@ def getJ( squeeze=squeeze, pixel_agg=pixel_agg, output=output, - in_out=in_out, + in_out="auto", ) @property @@ -914,6 +887,7 @@ class Collection(BaseGeo, BaseCollection): Collections function as groups of multiple magpylib objects. In this example we create a collection with two sources and move the whole collection: + >>> import numpy as np >>> import magpylib as magpy >>> src1 = magpy.magnet.Sphere(position=(2,0,0), diameter=1,polarization=(.1,.2,.3)) >>> src2 = magpy.current.Circle(position=(-2,0,0), diameter=1, current=1) @@ -958,8 +932,9 @@ class Collection(BaseGeo, BaseCollection): a single command: >>> B = col.getB() - >>> print(B) - [ 2.32922681e-04 -9.31694991e-05 -3.44484717e-10] + >>> with np.printoptions(precision=3): + ... print(B) + [ 2.329e-04 -9.317e-05 -3.445e-10] """ def __init__( diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index dd6a60121..aadf33aea 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Sensor class code""" import numpy as np @@ -60,29 +62,33 @@ class Sensor(BaseGeo, BaseDisplayRepr): `Sensor` objects are observers for magnetic field computation. In this example we compute the B-field in units of T as seen by the sensor in the center of a circular current loop: + >>> import numpy as np >>> import magpylib as magpy >>> sens = magpy.Sensor() >>> loop = magpy.current.Circle(current=1, diameter=0.01) >>> B = sens.getB(loop) - >>> print(B) - [0. 0. 0.00012566] + >>> with np.printoptions(precision=3): + ... print(B*1000) + [0. 0. 0.126] We rotate the sensor by 45 degrees and compute the field again: >>> sens.rotate_from_rotvec((45,0,0)) Sensor(id=...) >>> B = sens.getB(loop) - >>> print(B) - [0.00000000e+00 8.88576588e-05 8.88576588e-05] + >>> with np.printoptions(precision=3): + ... print(B) + [0.000e+00 8.886e-05 8.886e-05] Finally we set some sensor pixels and compute the field again: >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> B = sens.getB(loop) - >>> print(B) - [[0.00000000e+00 8.88576588e-05 8.88576588e-05] - [0.00000000e+00 9.16274003e-05 9.16274003e-05] - [0.00000000e+00 1.01415384e-04 1.01415384e-04]] + >>> with np.printoptions(precision=3): + ... print(B) + [[0.000e+00 8.886e-05 8.886e-05] + [0.000e+00 9.163e-05 9.163e-05] + [0.000e+00 1.014e-04 1.014e-04]] """ _style_class = SensorStyle @@ -201,29 +207,33 @@ def getB( Sensors are observers for magnetic field computation. In this example we compute the B-field in T as seen by the sensor in the center of a circular current loop: + >>> import numpy as np >>> import magpylib as magpy >>> sens = magpy.Sensor() >>> loop = magpy.current.Circle(current=1, diameter=.01) >>> B = sens.getB(loop) - >>> print(B) - [0. 0. 0.00012566] + >>> with np.printoptions(precision=3): + ... print(B*1000) + [0. 0. 0.126] Then we rotate the sensor by 45 degrees and compute the field again: >>> sens.rotate_from_rotvec((45,0,0)) Sensor(id=...) >>> B = sens.getB(loop) - >>> print(B) - [0.00000000e+00 8.88576588e-05 8.88576588e-05] + >>> with np.printoptions(precision=3): + ... print(B) + [0.000e+00 8.886e-05 8.886e-05] Finally we set some sensor pixels and compute the field again: >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> B = sens.getB(loop) - >>> print(B) - [[0.00000000e+00 8.88576588e-05 8.88576588e-05] - [0.00000000e+00 9.16274003e-05 9.16274003e-05] - [0.00000000e+00 1.01415384e-04 1.01415384e-04]] + >>> with np.printoptions(precision=3): + ... print(B) + [[0.000e+00 8.886e-05 8.886e-05] + [0.000e+00 9.163e-05 9.163e-05] + [0.000e+00 1.014e-04 1.014e-04]] """ sources = format_star_input(sources) return getBH_level2( @@ -296,11 +306,13 @@ def getH( Sensors are observers for magnetic field computation. In this example we compute the B-field in T as seen by the sensor in the center of a circular current loop: + >>> import numpy as np >>> import magpylib as magpy >>> sens = magpy.Sensor() >>> loop = magpy.current.Circle(current=1, diameter=.01) >>> H = sens.getH(loop) - >>> print(H) + >>> with np.printoptions(precision=3): + ... print(H) [ 0. 0. 100.] Then we rotate the sensor by 45 degrees and compute the field again: @@ -308,17 +320,19 @@ def getH( >>> sens.rotate_from_rotvec((45,0,0)) Sensor(id=...) >>> H = sens.getH(loop) - >>> print(H) - [ 0. 70.71067812 70.71067812] + >>> with np.printoptions(precision=3): + ... print(H) + [ 0. 70.711 70.711] Finally we set some sensor pixels and compute the field again: >>> sens.pixel=((0,0,0), (.001,0,0), (.002,0,0)) >>> H = sens.getH(loop) - >>> print(H) - [[ 0. 70.71067812 70.71067812] - [ 0. 72.9147684 72.9147684 ] - [ 0. 80.7037979 80.7037979 ]] + >>> with np.printoptions(precision=3): + ... print(H) + [[ 0. 70.711 70.711] + [ 0. 72.915 72.915] + [ 0. 80.704 80.704]] """ sources = format_star_input(sources) return getBH_level2( @@ -385,6 +399,22 @@ def getM( M-field of each source (index l) at each path position (index m) and each sensor pixel position (indices n1,n2,...) in units of A/m. Paths of objects that are shorter than index m are considered as static beyond their end. + + Examples + -------- + Test if there is magnetization at the location of the sensor. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ) + >>> sens = magpy.Sensor() + >>> M = sens.getM(cube) + >>> with np.printoptions(precision=0): + ... print(M) + [795775. 0. 0.] """ sources = format_star_input(sources) return getBH_level2( @@ -451,6 +481,22 @@ def getJ( J-field of each source (index l) at each path position (index m) and each sensor pixel position (indices n1,n2,...) in units of T. Paths of objects that are shorter than index m are considered as static beyond their end. + + Examples + -------- + Test if there is polarization at the location of the sensor. + + >>> import numpy as np + >>> import magpylib as magpy + >>> cube = magpy.magnet.Cuboid( + ... dimension=(10,1,1), + ... polarization=(1,0,0) + ... ) + >>> sens = magpy.Sensor() + >>> J = sens.getJ(cube) + >>> with np.printoptions(precision=0): + ... print(J) + [1. 0. 0.] """ sources = format_star_input(sources) return getBH_level2( diff --git a/magpylib/_src/obj_classes/class_current_Circle.py b/magpylib/_src/obj_classes/class_current_Circle.py index 6b65fbc60..8b30e81f0 100644 --- a/magpylib/_src/obj_classes/class_current_Circle.py +++ b/magpylib/_src/obj_classes/class_current_Circle.py @@ -55,33 +55,13 @@ class Circle(BaseCurrent): of such a current loop with 100 A current and a diameter of 2 meters at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.current.Circle(current=100, diameter=2) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [7.50093701e-03 7.50093701e-03 4.99999967e+01] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Circle(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[-1.63585841e-24 -4.44388287e-05 4.44388287e-05] - [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] - [-9.85948765e-24 -4.45190261e-05 4.45190261e-05]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). This time we use a `Sensor` object as observer. - - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Circle(id=...) - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> B = src.getB(sens) - >>> print(B) - [[-1.63585841e-24 -4.44388287e-05 4.44388287e-05] - [-6.55449367e-24 -4.44688604e-05 4.44688604e-05] - [-9.85948765e-24 -4.45190261e-05 4.45190261e-05]] + >>> with np.printoptions(precision=3): + ... print(H) + [7.501e-03 7.501e-03 5.000e+01] """ _field_func = staticmethod(BHJM_circle) diff --git a/magpylib/_src/obj_classes/class_current_Polyline.py b/magpylib/_src/obj_classes/class_current_Polyline.py index b4624d5f2..a172bc053 100644 --- a/magpylib/_src/obj_classes/class_current_Polyline.py +++ b/magpylib/_src/obj_classes/class_current_Polyline.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Polyline current class code""" import warnings @@ -56,36 +58,17 @@ class Polyline(BaseCurrent): of a square-shaped line-current with 1 A current at the observer position (1,1,1) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.current.Polyline( ... current=1, ... vertices=((.01,0,0), (0,.01,0), (-.01,0,0), (0,-.01,0), (.01,0,0)), ... ) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [3.16063859 3.16063859 0.76687556] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Polyline(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[-1.04529728e-21 3.50341393e-06 -3.50341393e-06] - [-9.28140349e-23 3.62181325e-07 -3.62181325e-07] - [-1.72744075e-23 1.03643004e-07 -1.03643004e-07]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). This time we use a `Sensor` object as observer. - - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Polyline(id=...) - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> B = src.getB(sens) - >>> print(B) - [[-1.04529728e-21 3.50341393e-06 -3.50341393e-06] - [-9.28140349e-23 3.62181325e-07 -3.62181325e-07] - [-1.72744075e-23 1.03643004e-07 -1.03643004e-07]] + >>> with np.printoptions(precision=3): + ... print(H) + [3.161 3.161 0.767] + """ # pylint: disable=dangerous-default-value diff --git a/magpylib/_src/obj_classes/class_magnet_Cuboid.py b/magpylib/_src/obj_classes/class_magnet_Cuboid.py index 4ccf52ef7..d9c5faf9e 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cuboid.py +++ b/magpylib/_src/obj_classes/class_magnet_Cuboid.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Magnet Cuboid class code""" from magpylib._src.display.traces_core import make_Cuboid @@ -58,21 +60,13 @@ class Cuboid(BaseMagnet): cubical magnet with magnetic polarization of (0.5,0.6,0.7) in units of T and 0.01 meter sides at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.Cuboid(polarization=(.5,.6,.7), dimension=(.01,.01,.01)) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [16149.04135639 14906.8074059 13664.57345541] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Cuboid(id=...) - >>> B = src.getB([(.01,0,0), (0,.01,0), (0,0,.01)]) - >>> print(B) - [[ 0.06739119 0.00476528 -0.0619486 ] - [-0.03557183 -0.01149497 -0.08403664] - [-0.03557183 0.00646436 0.14943466]] + >>> with np.printoptions(precision=0): + ... print(H) + [16149. 14907. 13665.] """ _field_func = staticmethod(BHJM_magnet_cuboid) diff --git a/magpylib/_src/obj_classes/class_magnet_Cylinder.py b/magpylib/_src/obj_classes/class_magnet_Cylinder.py index 7d0f7c67a..ade5907f3 100644 --- a/magpylib/_src/obj_classes/class_magnet_Cylinder.py +++ b/magpylib/_src/obj_classes/class_magnet_Cylinder.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Magnet Cylinder class code""" from magpylib._src.display.traces_core import make_Cylinder @@ -57,33 +59,13 @@ class Cylinder(BaseMagnet): cylinder magnet with polarization (.1,.2,.3) in units of T and 0.01 meter diameter and height at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.Cylinder(polarization=(.1,.2,.3), dimension=(.01,.01)) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [4849.91343121 3883.17815517 2739.73202237] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Cylinder(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[3.31419501e-03 5.26683023e-03 3.77670149e-04] - [4.22984050e-04 6.77105357e-04 4.46493154e-05] - [1.25715233e-04 2.01445027e-04 1.31238931e-05]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (0.01,0.01,0.01). Here we use a `Sensor` object as observer. - - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Cylinder(id=...) - >>> B = src.getB(sens) - >>> print(B) - [[3.31419501e-03 5.26683023e-03 3.77670149e-04] - [4.22984050e-04 6.77105357e-04 4.46493154e-05] - [1.25715233e-04 2.01445027e-04 1.31238931e-05]] + >>> with np.printoptions(precision=3): + ... print(H) + [4849.913 3883.178 2739.732] """ _field_func = staticmethod(BHJM_magnet_cylinder) diff --git a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py index b4703c3e8..07108a8f9 100644 --- a/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py +++ b/magpylib/_src/obj_classes/class_magnet_CylinderSegment.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Magnet Cylinder class code""" import numpy as np @@ -72,33 +74,13 @@ class CylinderSegment(BaseMagnet): in units of T, inner radius 0.01 meter, outer radius 0.02 meter, height 0.01 meter, and section angles 0 and 45 deg at the observer position (0.02,0.02,0.02) in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.CylinderSegment(polarization=(.1,.2,.3), dimension=(.01,.02,.01,0,45)) >>> H = src.getH((.02,.02,.02)) - >>> print(H) - [ 807.84692247 1934.22812757 2741.16804414] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - CylinderSegment(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[-0.0328285 0.03015882 -0.01632886] - [ 0.00062876 0.00397579 0.00073298] - [ 0.00025439 0.00074332 0.00011683]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (.01,.01,.01). Here we use a `Sensor` object as observer. - - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - CylinderSegment(id=...) - >>> B = src.getB(sens) - >>> print(B) - [[-0.0328285 0.03015882 -0.01632886] - [ 0.00062876 0.00397579 0.00073298] - [ 0.00025439 0.00074332 0.00011683]] + >>> with np.printoptions(precision=3): + ... print(H) + [ 807.847 1934.228 2741.168] """ _field_func = staticmethod(BHJM_cylinder_segment_internal) diff --git a/magpylib/_src/obj_classes/class_magnet_Sphere.py b/magpylib/_src/obj_classes/class_magnet_Sphere.py index 8885d6988..bed7da0bb 100644 --- a/magpylib/_src/obj_classes/class_magnet_Sphere.py +++ b/magpylib/_src/obj_classes/class_magnet_Sphere.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Magnet Sphere class code""" from magpylib._src.display.traces_core import make_Sphere @@ -57,33 +59,13 @@ class Sphere(BaseMagnet): of a spherical magnet with polarization (0.1,0.2,0.3) in units of T and diameter of 0.01 meter at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.magnet.Sphere(polarization=(.1,.2,.3), diameter=.01) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [3190.56073566 2552.44858853 1914.33644139] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Sphere(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[2.26804606e-03 3.63693295e-03 2.34863859e-04] - [2.83505757e-04 4.54616618e-04 2.93579824e-05] - [8.40017059e-05 1.34701220e-04 8.69866146e-06]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (.01,.01,.01). This time we use a `Sensor` object as observer. - - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Sphere(id=...) - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> B = src.getB(sens) - >>> print(B) - [[2.26804606e-03 3.63693295e-03 2.34863859e-04] - [2.83505757e-04 4.54616618e-04 2.93579824e-05] - [8.40017059e-05 1.34701220e-04 8.69866146e-06]] + >>> with np.printoptions(precision=3): + ... print(H) + [3190.561 2552.449 1914.336] """ _field_func = staticmethod(BHJM_magnet_sphere) diff --git a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py index 5970df551..924513ca0 100644 --- a/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py +++ b/magpylib/_src/obj_classes/class_magnet_Tetrahedron.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-positional-arguments + """Magnet Tetrahedron class code""" import numpy as np @@ -68,34 +70,14 @@ class Tetrahedron(BaseMagnet): through the vertices (0,0,0), (.01,0,0), (0,.01,0) and (0,0,.01) in units of m at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> verts = [(0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)] >>> src = magpy.magnet.Tetrahedron(polarization=(.1,.2,.3), vertices=verts) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [2070.8978262 1656.71826096 1242.53869572] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Tetrahedron(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] - [ 1.01357229e-04 1.93731796e-04 -1.59677364e-05] - [ 2.90426931e-05 5.22556994e-05 -1.70596096e-06]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). Here we use a `Sensor` object as observer. - - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Tetrahedron(id=...) - >>> B = src.getB(sens) - >>> print(B) - [[ 8.68006559e-04 2.00895792e-03 -5.03469140e-04] - [ 1.01357229e-04 1.93731796e-04 -1.59677364e-05] - [ 2.90426931e-05 5.22556994e-05 -1.70596096e-06]] + >>> with np.printoptions(precision=3): + ... print(H) + [2070.898 1656.718 1242.539] """ _field_func = staticmethod(BHJM_magnet_tetrahedron) diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 81fa877cd..ccb7694d8 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -22,6 +22,7 @@ # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods +# pylint: disable=too-many-positional-arguments class TriangularMesh(BaseMagnet): @@ -105,12 +106,14 @@ class TriangularMesh(BaseMagnet): with polarization (0.1,0.2,0.3) in units of T at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> vv = ((0,0,0), (.01,0,0), (0,.01,0), (0,0,.01)) >>> tt = ((0,1,2), (0,1,3), (0,2,3), (1,2,3)) >>> trim = magpy.magnet.TriangularMesh(polarization=(.1,.2,.3), vertices=vv, faces=tt) - >>> print(trim.getB((.01,.01,.01))) - [0.00260237 0.00208189 0.00156142] + >>> with np.printoptions(precision=3): + ... print(trim.getB((.01,.01,.01))*1000) + [2.602 2.082 1.561] """ _field_func = staticmethod(BHJM_magnet_trimesh) @@ -599,9 +602,6 @@ def from_ConvexHull( Returns ------- magnet source: `TriangularMesh` object - - Examples - -------- """ return cls( position=position, @@ -692,9 +692,6 @@ def from_pyvista( Returns ------- magnet source: `TriangularMesh` object - - Examples - -------- """ # pylint: disable=import-outside-toplevel try: @@ -802,9 +799,6 @@ def from_triangles( Returns ------- magnet source: `TriangularMesh` object - - Examples - -------- """ if not isinstance(triangles, (list, Collection)): raise TypeError( @@ -910,9 +904,6 @@ def from_mesh( Returns ------- magnet source: `TriangularMesh` object - - Examples - -------- """ mesh = check_format_input_vector2( mesh, diff --git a/magpylib/_src/obj_classes/class_misc_CustomSource.py b/magpylib/_src/obj_classes/class_misc_CustomSource.py index 4637e4622..4692ca264 100644 --- a/magpylib/_src/obj_classes/class_misc_CustomSource.py +++ b/magpylib/_src/obj_classes/class_misc_CustomSource.py @@ -1,4 +1,4 @@ -"""Custom class code """ +"""Custom class code""" from magpylib._src.obj_classes.class_BaseExcitations import BaseSource @@ -58,28 +58,6 @@ class CustomSource(BaseSource): >>> H = src.getH((.01,.01,.01)) >>> print(H) [0.08 0. 0. ] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'z') - CustomSource(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[0.00707107 0.00707107 0. ] - [0.00707107 0.00707107 0. ] - [0.00707107 0.00707107 0. ]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (0.01,0.01,0.01). This time we use a `Sensor` object as observer. - - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - CustomSource(id=...) - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> B = src.getB(sens) - >>> print(B) - [[0.00707107 0.00707107 0. ] - [0.00707107 0.00707107 0. ] - [0.00707107 0.00707107 0. ]] """ _editable_field_func = True diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index 63872b45c..bad48f7fb 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -53,33 +53,13 @@ class Dipole(BaseSource): of such a magnetic dipole with a moment of (100,100,100) in units of Aยทmยฒ at an observer position (.01,.01,.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> src = magpy.misc.Dipole(moment=(10,10,10)) >>> H = src.getH((.01,.01,.01)) - >>> print(H) - [306293.83078988 306293.83078988 306293.83078988] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Dipole(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[0.27216553 0.46461562 0.19245009] - [0.03402069 0.05807695 0.02405626] - [0.0100802 0.01720799 0.00712778]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (1,1,1). This time we use a `Sensor` object as observer. - - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Dipole(id=...) - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> B = src.getB(sens) - >>> print(B) - [[0.27216553 0.46461562 0.19245009] - [0.03402069 0.05807695 0.02405626] - [0.0100802 0.01720799 0.00712778]] + >>> with np.printoptions(precision=0): + ... print(H) + [306294. 306294. 306294.] """ _field_func = staticmethod(BHJM_dipole) diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index fe4d1d850..d2e479a9f 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -1,5 +1,6 @@ -"""Magnet Triangle class -""" +# pylint: disable=too-many-positional-arguments + +"""Magnet Triangle class""" import numpy as np @@ -71,34 +72,14 @@ class Triangle(BaseMagnet): through the vertices (0,0,0), (0.01,0,0) and (0,0.01,0) in units of m at the observer position (0.01,0.01,0.01) given in units of m: + >>> import numpy as np >>> import magpylib as magpy >>> verts = [(0,0,0), (.01,0,0), (0,.01,0)] >>> src = magpy.misc.Triangle(polarization=(.1,.2,.3), vertices=verts) >>> H = src.getH((.1,.1,.1)) - >>> print(H) - [18.8886983 18.8886983 19.54560636] - - We rotate the source object, and compute the B-field, this time at a set of observer positions: - - >>> src.rotate_from_angax(45, 'x') - Triangle(id=...) - >>> B = src.getB([(.01,.01,.01), (.02,.02,.02), (.03,.03,.03)]) - >>> print(B) - [[0.00394659 0.00421773 0.00421773] - [0.00073746 0.00077326 0.00077326] - [0.00030049 0.00031044 0.00031044]] - - The same result is obtained when the rotated source moves along a path away from an - observer at position (0.01,0.01,0.01). Here we use a `Sensor` object as observer. - - >>> sens = magpy.Sensor(position=(.01,.01,.01)) - >>> src.move([(-.01,-.01,-.01), (-.02,-.02,-.02)]) - Triangle(id=...) - >>> B = src.getB(sens) - >>> print(B) - [[0.00394659 0.00421773 0.00421773] - [0.00073746 0.00077326 0.00077326] - [0.00030049 0.00031044 0.00031044]] + >>> with np.printoptions(precision=3): + ... print(H) + [18.889 18.889 19.546] """ _field_func = staticmethod(BHJM_triangle) diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py index 9c8c2127e..901b91b30 100644 --- a/magpylib/_src/style.py +++ b/magpylib/_src/style.py @@ -3,6 +3,8 @@ # pylint: disable=C0302 # pylint: disable=too-many-instance-attributes # pylint: disable=cyclic-import +# pylint: disable=too-many-positional-arguments + import numpy as np from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index d62d377c1..544628e08 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -1,8 +1,9 @@ -""" some utility functions""" +"""some utility functions""" # pylint: disable=import-outside-toplevel # pylint: disable=cyclic-import # import numbers +from contextlib import contextmanager from functools import lru_cache from inspect import signature from math import log10 @@ -237,8 +238,36 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True): 24: "Y", # yotta } +_UNIT_PREFIX_REVERSED = {v: k for k, v in _UNIT_PREFIX.items()} -def unit_prefix(number, unit="", precision=3, char_between="") -> str: + +@lru_cache(maxsize=None) +def get_unit_factor(unit_input, *, target_unit, deci_centi=True): + """return unit factor based on input and target unit""" + if unit_input is None or unit_input == target_unit: + return 1 + pref, suff, factor_power = "", "", None + prefs = _UNIT_PREFIX_REVERSED + if deci_centi: + prefs = {**_UNIT_PREFIX_REVERSED, "d": -1, "c": -2} + unit_input_str = str(unit_input) + if unit_input_str: + if len(unit_input_str) >= 2: + pref, *suff = unit_input_str + suff = "".join(suff) + if suff == target_unit: + factor_power = prefs.get(pref, None) + + if factor_power is None or len(unit_input_str) > 2: + valid_inputs = [f"{k}{target_unit}" for k in prefs] + raise ValueError( + f"Invalid unit input ({unit_input!r}), must be one of {valid_inputs}" + ) + factor = 1 / (10**factor_power) + return factor + + +def unit_prefix(number, unit="", precision=3, char_between="", as_tuple=False) -> str: """ displays a number with given unit and precision and uses unit prefixes for the exponents from yotta (y) to Yocto (Y). If the exponent is smaller or bigger, falls back to scientific notation. @@ -253,10 +282,13 @@ def unit_prefix(number, unit="", precision=3, char_between="") -> str: char_between : str, optional character to insert between number of prefix. Can be " " or any string, if a space is wanted before the unit symbol , by default "" + as_tuple: bool, optional + if True returns (new_number_str, char_between, prefix, unit) tuple + else returns the joined string Returns ------- - str - returns formatted number as string + str or tuple + returns formatted number as string or tuple """ digits = int(log10(abs(number))) // 3 * 3 if number != 0 else 0 prefix = _UNIT_PREFIX.get(digits, "") @@ -264,7 +296,10 @@ def unit_prefix(number, unit="", precision=3, char_between="") -> str: if prefix == "": digits = 0 new_number_str = f"{number / 10 ** digits:.{precision}g}" - return f"{new_number_str}{char_between}{prefix}{unit}" + res = (new_number_str, char_between, prefix, unit) + if as_tuple: + return res + return "".join(f"{v}" for v in res) def add_iteration_suffix(name): @@ -398,3 +433,80 @@ def has_parameter(func: Callable, param_name: str) -> bool: """Check if input function has a specific parameter""" sig = signature(func) return param_name in sig.parameters + + +def merge_dicts_with_conflict_check(objs, *, target, identifiers, unique_fields): + """ + Merge dictionaries ensuring unique identifier fields don't lead to conflict. + + Parameters + ---------- + objs : list of dicts + List of dictionaries to be merged based on identifier fields. + target : str + The key in the dictionaries whose values are lists to be merged. + identifiers : list of str + Keys used to identify a unique dictionary. + unique_fields : list of str + Additional keys that must not conflict across merged dictionaries. + + Returns + ------- + dict of dicts + Merged dictionaries with combined `target` lists, ensuring no conflicts + in `unique_fields`. + + Raises + ------ + ValueError + If a conflict is detected in `unique_fields` for any `identifiers`. + + Notes + ----- + `objs` should be a list of dictionaries. Identifiers determine uniqueness, + and merging is done by extending the lists in the `target` key. If any of + the `unique_fields` conflict with previously tracked identifiers, a + `ValueError` is raised detailing the conflict. + + """ + merged_dict = {} + tracker = {} + for obj in objs: + key_dict = {k: obj[k] for k in identifiers} + key = tuple(key_dict.values()) + tracker_previous = tracker.get(key, None) + tracker_actual = tuple(obj[field] for field in unique_fields) + if key in tracker and tracker_previous != tracker_actual: + diff = [ + f"{f!r} first got {a!r} then {t!r}" + for f, a, t in zip(unique_fields, tracker_actual, tracker_previous) + if a != t + ] + raise ValueError( + f"Conflicting parameters detected for {key_dict}: {', '.join(diff)}." + ) + tracker[key] = tracker_actual + + if key not in merged_dict: + merged_dict[key] = obj + else: + merged_dict[key][target] = list( + dict.fromkeys([*merged_dict[key][target], *obj[target]]) + ) + return merged_dict + + +@contextmanager +def style_temp_edit(obj, style_temp, copy=True): + """Temporary replace style to allow edits before returning to original state""" + # pylint: disable=protected-access + orig_style = getattr(obj, "_style", None) + try: + # temporary replace style attribute + obj._style = style_temp + if style_temp and copy: + # deepcopy style only if obj is in multiple subplots. + obj._style = style_temp.copy() + yield + finally: + obj._style = orig_style diff --git a/pyproject.toml b/pyproject.toml index 370f1e127..50c8b07ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ build-backend = "flit_core.buildapi" name = "magpylib" dynamic = ["version"] dependencies = [ - "numpy>=1.20", - "scipy>=1.7", - "matplotlib>=3.3", - "plotly>=5.3", + "numpy>=1.23", + "scipy>=1.8", + "matplotlib>=3.6", + "plotly>=5.16", ] -requires-python = ">=3.8" +requires-python = ">=3.10" authors = [ {name = "Michael Ortner", email = "magpylib@gmail.com"}, ] @@ -25,10 +25,9 @@ keywords = ["magnetism", "physics", "analytical", "electromagnetic", "magnetic-f classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: Education", @@ -40,17 +39,19 @@ code_style = [ "pre-commit", ] docs = [ - "sphinx==7.2", + "pydata-sphinx-theme", + "sphinx==7.3", "sphinx-design", "sphinx-thebe", "sphinx-favicon", "sphinx-gallery", "sphinx-copybutton", - "sphinx-book-theme", "myst-nb", "pandas", "numpy-stl", "pyvista", + "magpylib-material-response", + "magpylib-force", ] test = [ "tox>=4.11", @@ -62,6 +63,7 @@ test = [ "ipywidgets", # for plotly FigureWidget "imageio[tifffile, ffmpeg]", "jupyterlab", + "anywidget", ] binder = [ "jupytext", diff --git a/tests/__init__.py b/tests/__init__.py index 4c1867059..30ea4534b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -""" tests """ +"""tests""" diff --git a/tests/test_BaseTransform.py b/tests/test_BaseTransform.py index b96908c93..38265a688 100644 --- a/tests/test_BaseTransform.py +++ b/tests/test_BaseTransform.py @@ -6,6 +6,8 @@ from magpylib._src.obj_classes.class_BaseTransform import apply_move from magpylib._src.obj_classes.class_BaseTransform import apply_rotation +# pylint: disable=too-many-positional-arguments + @pytest.mark.parametrize( ( diff --git a/tests/test_core_physics_consistency.py b/tests/test_core_physics_consistency.py index 3a6249012..61669136d 100644 --- a/tests/test_core_physics_consistency.py +++ b/tests/test_core_physics_consistency.py @@ -355,7 +355,9 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dimension=dim, polarization=pol, ) - np.testing.assert_allclose(Bdip, Bcub) + # np.testing.assert_allclose(Bdip, Bcub) + err = np.linalg.norm(Bdip - Bcub) / np.linalg.norm(Bdip) + assert err < 1e-6 dim = np.array([(0.5, 0.5)] * 2) vol = 0.25**2 * np.pi * 0.5 @@ -366,7 +368,9 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dimension=dim, polarization=pol, ) - np.testing.assert_allclose(Bdip, Bcyl) + # np.testing.assert_allclose(Bdip, Bcyl, rtol=1e-6) + err = np.linalg.norm(Bdip - Bcyl) / np.linalg.norm(Bdip) + assert err < 1e-6 vert = np.array([[(0, 0, 0), (0, 0, 0.1), (0.1, 0, 0), (0, 0.1, 0)]] * 2) vol = 1 / 6 * 1e-3 @@ -377,7 +381,9 @@ def test_core_physics_dipole_approximation_magnet_far_field(): vertices=vert, polarization=pol, ) - np.testing.assert_allclose(Bdip, Btetra, rtol=1e-3) + # np.testing.assert_allclose(Bdip, Btetra, rtol=1e-3) + err = np.linalg.norm(Bdip - Btetra) / np.linalg.norm(Bdip) + assert err < 1e-3 dim = np.array([(0.1, 0.2, 0.1, -25, 25)] * 2) vol = 3 * np.pi * (50 / 360) * 1e-3 @@ -388,7 +394,9 @@ def test_core_physics_dipole_approximation_magnet_far_field(): dimension=dim, polarization=pol, ) - np.testing.assert_allclose(Bdip, Bcys, rtol=1e-4) + # np.testing.assert_allclose(Bdip, Bcys, rtol=1e-4) + err = np.linalg.norm(Bdip - Bcys) / np.linalg.norm(Bdip) + assert err < 1e-4 # --> Circle diff --git a/tests/test_defaults.py b/tests/test_defaults.py index 6a2c99b57..cf43825e2 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -74,6 +74,7 @@ def get_bad_test_data(): """create parametrized bad style test data""" + # pylint: disable=possibly-used-before-assignment bad_test_data = [] for k, tup in bad_inputs.items(): for v in tup: diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 8a4fc464a..890aabfbd 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -422,8 +422,7 @@ def test_matplotlib_model3d_extra_updatefunc(): def test_empty_display(): """should not fail if nothing to display""" - ax = plt.subplot(projection="3d") - magpy.show(canvas=ax, backend="matplotlib", return_fig=True) + magpy.show(backend="matplotlib", return_fig=True) def test_graphics_model_mpl(): @@ -523,7 +522,10 @@ def test_bad_show_inputs(): ) with pytest.raises( ValueError, - match=r"Row/Col .* received conflicting output types.*", + match=( + r"Conflicting parameters detected for {'row': 1, 'col': 1}:" + r" 'output' first got 'model3d' then 'Bx'." + ), ): with magpy.show_context(animation=False, sumup=True, pixel_agg="mean") as s: s.show(cyl1, sensor, col=1, output="Bx") @@ -615,3 +617,13 @@ def test_show_legend(): s2.style.legend = "full legend replace" s3.style.description = "description replace only" magpy.show(s1, s2, s3, return_fig=True) + + +@pytest.mark.parametrize("units_length", ["mT", "inch", "dam", "e"]) +def test_bad_units_length(units_length): + """test units lenghts""" + + c = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + + with pytest.raises(ValueError, match=r"Invalid unit input.*"): + c.show(units_length=units_length, return_fig=True, backend="matplotlib") diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index b9f51abfd..eabccc766 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -4,6 +4,7 @@ import magpylib as magpy from magpylib._src.exceptions import MagpylibBadUserInput +from magpylib._src.utility import get_unit_factor # pylint: disable=assignment-from-no-return # pylint: disable=no-member @@ -383,3 +384,105 @@ def test_legends(): ) assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] * 4 assert [t.showlegend for t in fig.data] == [True, False, False, False] + + fig = magpy.show( + markers=[(0, 0, 0)], + backend="plotly", + style_legend_show=False, + return_fig=True, + ) + + assert [t.name for t in fig.data] == ["Marker"] + assert [t.showlegend for t in fig.data] == [False] + + +def test_color_precedence(): + """Test if color precedence is respected when calling in nested collections""" + c1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + c2 = c1.copy(position=(1, 0, 0)) + c3 = c1.copy(position=(2, 0, 0)) + coll = magpy.Collection(c1, magpy.Collection(c2, c3)) + kw = { + "backend": "plotly", + "style_magnetization_show": False, + "colorsequence": ["red", "blue", "green"], + "return_fig": True, + } + fig = magpy.show(coll, **kw) + assert [tr["color"] for tr in fig.data] == ["red"] + + fig = magpy.show(*coll, **kw) + assert [tr["color"] for tr in fig.data] == ["red", "blue"] + + fig = magpy.show(*coll.sources_all, **kw) + assert [tr["color"] for tr in fig.data] == ["red", "blue", "green"] + + fig = magpy.show({"objects": c1, "col": 1}, {"objects": c1, "col": 2}, **kw) + # sane obj in different subplot should have same color + assert [tr["color"] for tr in fig.data] == ["red", "red"] + + +def test_colors_output2d(): + """Tests if lines have objects corresponding colors in ouptut=Bx, By...""" + l1 = magpy.current.Circle( + current=1, + diameter=1, + style_label="L1", + style_arrow_show=False, + ) + l2 = l1.copy(diameter=2) + s1 = magpy.Sensor( + pixel=[[0, 0, 0], [0, 1, 0]], + position=np.linspace((-1, 0, 1), (1, 0, 1), 10), + style_label="S", + style_model3d_showdefault=False, + ) + s2 = s1.copy().move((0, 0, 1)) + objs = {"objects": [l1, l2, s1, s2]} + kw = { + "backend": "plotly", + "return_fig": True, + "colorsequence": ["red", "blue", "green", "cyan"], + } + kw2d = {"output": "Bx", "col": 2} + + def get_scatters2d(fig): + return [t.line.color for t in fig.data if t.type == "scatter"] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": True}, **kw) + assert get_scatters2d(fig) == ["green", "cyan"] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": True, "pixel_agg": None}, **kw) + assert get_scatters2d(fig) == [*["green"] * 2, *["cyan"] * 2] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": False}, **kw) + assert get_scatters2d(fig) == [*["red"] * 2, *["blue"] * 2] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": False, "pixel_agg": None}, **kw) + assert get_scatters2d(fig) == [*["red"] * 4, *["blue"] * 4] + + +def test_units_length(): + """test units lenghts""" + + dims = (1, 2, 3) + c1 = magpy.magnet.Cuboid(dimension=dims, polarization=(1, 2, 3)) + inputs = [ + {"objects": c1, "row": 1, "col": 1, "units_length": "m", "zoom": 3}, + {"objects": c1, "row": 1, "col": 2, "units_length": "dm", "zoom": 2}, + {"objects": c1, "row": 2, "col": 1, "units_length": "cm", "zoom": 1}, + {"objects": c1, "row": 2, "col": 2, "units_length": "mm", "zoom": 0}, + ] + fig = magpy.show( + *inputs, + backend="plotly", + return_fig=True, + ) + for ind, inp in enumerate(inputs): + scene = getattr(fig.layout, f"scene{'' if ind==0 else ind+1}") + for k in "xyz": + ax = getattr(scene, f"{k}axis") + assert ax.title.text == f"{k} ({inp['units_length']})" + factor = get_unit_factor(inp["units_length"], target_unit="m") + r = (inp["zoom"] + 1) / 2 * factor * max(dims) + assert ax.range == (-r, r) diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index c0fe6bc50..dec240b40 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -204,9 +204,7 @@ def test_cylinder_tile_slanovc(inputs, H_expected): "observers": inputs["obs_pos"], "dimensions": inputs["dim"], } - H = ( - magnet_cylinder_segment_Hfield(**inputs_mod) / 4 / np.pi * 1e7 - ) # factors come from B <->H change + H = magnet_cylinder_segment_Hfield(**inputs_mod) # factors come from B <->H change np.testing.assert_allclose(H, H_expected) @@ -219,7 +217,7 @@ def test_cylinder_field1(): nulll = np.zeros(N) eins = np.ones(N) - d, h, _ = dim.T + d, h, _ = dim.T # pylint: disable=no-member dim5 = np.array([nulll, d / 2, h, nulll, eins * 360]).T B1 = BHJM_cylinder_segment( field="B", observers=poso, polarization=magg, dimension=dim5 @@ -400,6 +398,7 @@ def test_cylinder_tile_vs_fem(): amp3 = np.linalg.norm(B3, axis=1) amp4 = np.linalg.norm(B4, axis=1) + # pylint: disable=unsubscriptable-object assert np.amax((fd1[:, 1:] * 1000 - B1).T / amp1) < 0.05 assert np.amax((fd2[5:-5, 1:] * 1000 - B2[5:-5]).T / amp2[5:-5]) < 0.05 assert np.amax((fd3[:, 1:] * 1000 - B3).T / amp3) < 0.05 diff --git a/tests/test_getBH_level2.py b/tests/test_getBH_level2.py index 79de8a1fe..a5ca5e114 100644 --- a/tests/test_getBH_level2.py +++ b/tests/test_getBH_level2.py @@ -244,24 +244,30 @@ def test_sensor_rotation2(): B = magpy.getB(src, poss, squeeze=True) Btest = x1 np.testing.assert_allclose( - np.around(B, decimals=5), + B, Btest, + rtol=1e-4, + atol=1e-5, err_msg="FAIL: mag + pos", ) B = magpy.getB([src], [sens], squeeze=True) Btest = np.array([x1, x2, x3]) np.testing.assert_allclose( - np.around(B, decimals=5), + B, Btest, + rtol=1e-4, + atol=1e-5, err_msg="FAIL: mag + sens_rot_path", ) B = magpy.getB([src], [sens, poss], squeeze=True) Btest = np.array([[x1, x1], [x2, x1], [x3, x1]]) np.testing.assert_allclose( - np.around(B, decimals=5), + B, Btest, + rtol=1e-4, + atol=1e-5, err_msg="FAIL: mag + sens_rot_path, pos", ) @@ -270,8 +276,10 @@ def test_sensor_rotation2(): [[[x1, x1], [x2, x1], [x3, x1]], [[x1b, x1b], [x2b, x1b], [x3b, x1b]]] ) np.testing.assert_allclose( - np.around(B, decimals=5), + B, Btest, + rtol=1e-4, + atol=1e-5, err_msg="FAIL: mag,col + sens_rot_path, pos", ) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 68024e801..b92c048b9 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -553,7 +553,7 @@ def test_input_show_zoom_bad(zoom): """bad show zoom inputs""" x = magpy.Sensor() with pytest.raises(MagpylibBadUserInput): - magpy.show(x, zoom=zoom) + magpy.show(x, zoom=zoom, return_fig=True, backend="plotly") @pytest.mark.parametrize( diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index e6707cb49..265f91474 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -74,8 +74,19 @@ def test_BaseGeo_basics(): poss = np.array(poss) rots = np.array(rots) - np.testing.assert_allclose(poss, ptest, err_msg="test_BaseGeo bad position") - np.testing.assert_allclose(rots, otest, err_msg="test_BaseGeo bad orientation") + # avoid generating different zeros in macos CI tests (atol=1e-6) + np.testing.assert_allclose( + poss, + ptest, + atol=1e-6, + err_msg="test_BaseGeo bad position", + ) + np.testing.assert_allclose( + rots, + otest, + atol=1e-6, + err_msg="test_BaseGeo bad orientation", + ) def test_rotate_vs_rotate_from(): diff --git a/tests/test_obj_BaseGeo_v4motion.py b/tests/test_obj_BaseGeo_v4motion.py index 6bb861778..25d0ed1c0 100644 --- a/tests/test_obj_BaseGeo_v4motion.py +++ b/tests/test_obj_BaseGeo_v4motion.py @@ -4,6 +4,8 @@ import magpylib as magpy +# pylint: disable=too-many-positional-arguments + ############################################################################### ############################################################################### # NEW BASE GEO TESTS FROM v4 diff --git a/tests/test_obj_Collection_v4motion.py b/tests/test_obj_Collection_v4motion.py index fb6585fec..b22607675 100644 --- a/tests/test_obj_Collection_v4motion.py +++ b/tests/test_obj_Collection_v4motion.py @@ -4,6 +4,8 @@ import magpylib as magpy +# pylint: disable=too-many-positional-arguments + ############################################################################### ############################################################################### # NEW COLLECTION POS/ORI TESTS FROM v4 diff --git a/tox.ini b/tox.ini index e2efcccdd..62ad23d13 100644 --- a/tox.ini +++ b/tox.ini @@ -8,8 +8,6 @@ python = 3.12 = py312 3.11 = py311 #, type 3.10 = py310 - 3.9 = py39 - 3.8 = py38 [testenv] description = run the tests with pytest