├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASE.md ├── conversion_notes.md ├── data ├── LICENSE.txt ├── README.md ├── pyCoilGenData │ ├── Geometry_Data │ │ ├── Cone_shaped_self_shielding_surface.stl │ │ ├── Cone_shaped_self_shielding_surface2.stl │ │ ├── Double_coaxial_open_cylinder_r1_400mm_r2_600_length_1500mm.stl │ │ ├── Open_cylinder_r750mm_length_1500mm.stl │ │ ├── Primary_r500mm_l1500mm_Shield_r750mm_l1750mm.stl │ │ ├── bi_planer_rectangles_width_1000mm_distance_500mm.stl │ │ ├── closed_cylinder_length_300mm_radius_150mm.stl │ │ ├── closed_cylinder_radius10mm_length40mm.stl │ │ ├── cylinder_radius500mm_length1500mm.stl │ │ ├── cylinder_radius500mm_length1500mm_90deg_rotated.stl │ │ ├── cylinder_radius500mm_length1500mm_holes_250mm.stl │ │ ├── cylinder_radius500mm_length1500mm_regular_holes.stl │ │ ├── cylinder_radius50mm_length350mm.stl │ │ ├── cylinder_radius600mm_length_1500mm_regular.stl │ │ ├── cylinder_radius900mm_length_1900mm_regular.stl │ │ ├── dental_ccs_bionic.stl │ │ ├── dental_ccs_bionic_shift_1cm_in_y.stl │ │ ├── dental_extraoral_ccs.stl │ │ ├── dental_extraoral_ccs2_shifted_2cm_in_y.stl │ │ ├── dental_gradient_ccs.stl │ │ ├── dental_gradient_ccs_single_low.stl │ │ ├── dental_gradient_target_region.stl │ │ ├── dental_gradient_target_region_single_low.stl │ │ ├── dental_oral_target.stl │ │ ├── dental_oral_target_bionic.stl │ │ ├── dental_oral_target_shifted_1cm_in_y.stl │ │ ├── diamond_probe_gradient_css.stl │ │ ├── diamond_probe_target_surface.stl │ │ ├── half_sphere_shape.stl │ │ ├── rectangular_plane_500mm.stl │ │ ├── sphere_radius150mm.stl │ │ ├── sphere_radius300mm.stl │ │ ├── sphere_radius_25mm.stl │ │ └── sphere_radius_5mm.stl │ ├── Pre_Optimized_Solutions │ │ ├── source_data_SVD_coil.npy │ │ └── source_data_breast_coil.npy │ ├── __init__.py │ └── target_fields │ │ └── intraoral_dental_target_field.npy └── pyproject.toml ├── docs ├── .readthedocs.yaml ├── Makefile ├── README.md ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── configuration.md │ ├── figures │ ├── 3D_axes.png │ ├── Logo_large.png │ ├── Logo_small.png │ ├── builder_biplanar_mesh.png │ ├── builder_circular_mesh.png │ ├── builder_cylindrical_mesh.png │ ├── builder_planar_mesh.png │ ├── flow_chart_algorithm_revised.png │ ├── illustration_cut_plane_definition.png │ ├── loop_cut_width.png │ ├── mesh_example_gradient_3D.png │ ├── mesh_s2_shim_swept_3D_copper.png │ ├── mesh_shielded_ygradient_swept_3D_copper.png │ ├── plot_errors_example_ygradient.png │ ├── plot_errors_s2_shim_coil.png │ ├── plot_example_ygradient_2D.png │ ├── plot_example_ygradient_3D.png │ ├── plot_s2_shim_coil_2D.png │ ├── plot_s2_shim_coil_3D.png │ ├── plot_s2_shim_coil_Target_Field_Error_XY.png │ ├── plot_s2_shim_coil_Target_Field_XY.png │ ├── plot_shielded_ygradient_coil_2D.png │ ├── plot_solutions_Halbach_study_Tikhonov05.png │ └── plot_solutions_Halbach_study_Tikhonov10.png │ ├── glossary.md │ ├── index.rst │ ├── installation.md │ ├── overview.md │ ├── quick_start.md │ └── results.md ├── examples ├── biplanar_xgradient.py ├── halbach_gradient_x.py ├── plotting_s2_shim.py ├── preoptimzed_breast_coil.py ├── preoptimzed_svd_coil.py ├── quick_check.py ├── s2_shim_coil_with_surface_openings.py ├── shielded_ygradient_coil.py └── ygradient_coil.py ├── pyCoilGen ├── __init__.py ├── __main__.py ├── export_factory │ ├── __init__.py │ └── export_cad_file.py ├── helpers │ ├── __init__.py │ ├── common.py │ ├── convert_matlabdata_to_numpy.py │ ├── extraction.py │ ├── persistence.py │ ├── pyshull.py │ ├── timing.py │ ├── triangulation.py │ └── visualisation.py ├── mesh_factory │ ├── __init__.py │ ├── build_biplanar_mesh.py │ ├── build_circular_mesh.py │ ├── build_cut_circle.py │ ├── build_cut_rectangle.py │ ├── build_cylinder_mesh.py │ ├── build_planar_mesh.py │ └── create_stl_mesh.py ├── plotting │ ├── __init__.py │ ├── plot_2D_contours_with_sf.py │ ├── plot_3D_contours_with_sf.py │ ├── plot_contours_with_field.py │ ├── plot_error_different_solutions.py │ ├── plot_various_error_metrics.py │ └── plot_vector_field.py ├── pyCoilGen_develop.py ├── pyCoilGen_release.py └── sub_functions │ ├── __init__.py │ ├── calc_3d_rotation_matrix_by_vector.py │ ├── calc_contours_by_triangular_potential_cuts.py │ ├── calc_gradient_along_vector.py │ ├── calc_local_opening_gab.py │ ├── calc_mean_loop_normal.py │ ├── calc_plane_line_intersection.py │ ├── calc_potential_levels.py │ ├── calculate_basis_functions.py │ ├── calculate_boundary_criteria_matrix.py │ ├── calculate_force_and_torque_matrix.py │ ├── calculate_gradient.py │ ├── calculate_gradient_sensitivity_matrix.py │ ├── calculate_group_centers.py │ ├── calculate_inductance_by_coil_layout.py │ ├── calculate_one_ring_by_mesh.py │ ├── calculate_resistance_matrix.py │ ├── calculate_sensitivity_matrix.py │ ├── check_mutual_loop_inclusion.py │ ├── constants.py │ ├── create_sweep_along_surface.py │ ├── data_structures.py │ ├── define_target_field.py │ ├── evaluate_field_errors.py │ ├── export_data.py │ ├── find_group_cut_position.py │ ├── find_min_mutual_loop_distance.py │ ├── find_minimal_contour_distance.py │ ├── find_segment_intersections.py │ ├── gauss_legendre_integration_points_triangle.py │ ├── generate_cylindrical_pcb_print.py │ ├── interconnect_among_groups.py │ ├── interconnect_within_groups.py │ ├── load_preoptimized_data.py │ ├── matlab_internal.py │ ├── mesh_parameterization_iterative.py │ ├── open_loop_with_3d_sphere.py │ ├── parameterize_mesh.py │ ├── parse_input.py │ ├── plane_line_intersect.py │ ├── process_raw_loops.py │ ├── read_mesh.py │ ├── refine_mesh.py │ ├── remove_points_from_loop.py │ ├── shift_return_paths.py │ ├── smooth_track_by_folding.py │ ├── split_disconnected_mesh.py │ ├── stream_function_optimization.py │ ├── temp_evaluation.py │ ├── topological_loop_grouping.py │ └── uv_to_xyz.py ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── scratchpad ├── checking1.py ├── checking2.py ├── checking3.py ├── compare1.py ├── convert.py ├── debug.py ├── figures_for_docs.py ├── issue70.py └── plotting.py ├── tests ├── regression_issue70.py ├── test_biot_savart_calc_b.py ├── test_build_biplanar_mesh.py ├── test_build_circular_mesh.py ├── test_build_cylinder_mesh.py ├── test_build_planar_mesh.py ├── test_calc_3d_rotation_matrix_by_vector.py ├── test_data │ ├── biplanar_mesh.json │ ├── planar_mesh.json │ ├── point_locations3.json │ ├── point_locations4.json │ ├── point_locations5.json │ ├── test_add_nearest_ref_point_to_curve1.npy │ ├── test_open_loop_with_3d_sphere1.npy │ └── test_remove_points_from_loop1.npy ├── test_gauss_legendre_integration_points_triangle.py ├── test_mesh.py ├── test_mesh_factory.py ├── test_open_loop_with_3d_sphere.py ├── test_process_raw_loops.py ├── test_read_mesh.py ├── test_remove_points_from_loop.py ├── test_save_preoptimised_data.py ├── test_smooth_track_by_folding.py ├── test_split_disconnected_mesh.py ├── test_symbolic_calculation_of_gradient.py ├── test_uv_to_xyz.py └── test_visualisation.py └── utilities ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── pyCoilGenUtils ├── __init__.py └── stl_asc2bin.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | 3 | *.pyc 4 | __pycache__/ 5 | *.tar 6 | 7 | instance/ 8 | 9 | .pytest_cache/ 10 | .coverage 11 | htmlcov/ 12 | 13 | dist/ 14 | build/ 15 | *.egg-info/ 16 | 17 | .vscode 18 | workspace.code-workspace 19 | matlab/ 20 | images/ 21 | debug/ 22 | 23 | # FastHenry files 24 | output.log 25 | coil_track_FH2_input.inp 26 | Zc*.mat 27 | 28 | # Output files in top directory 29 | *.stl 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.3 (2025-01-16) 4 | 5 | #### Fixes 6 | 7 | * (docs): Improving documentation 8 | * (docs): Adding additional text to the documentation to improve chances of outward normals in rendered wirepath, as reported in issue [#75](https://github.com/kev-m/pyCoilGen/issues/75) 9 | 10 | Full set of changes: [`0.2.2...0.2.3`](https://github.com/kev-m/pyCoilGen/compare/0.2.2...0.2.3) 11 | 12 | ## 0.2.2 (2024-06-24) 13 | 14 | #### Fixes 15 | 16 | * fixes the bug reported in issue [#70](https://github.com/kev-m/pyCoilGen/issues/70) ([#71](https://github.com/kev-m/pyCoilGen/issues/71)) 17 | 18 | Full set of changes: [`0.2.1...0.2.2`](https://github.com/kev-m/pyCoilGen/compare/0.2.1...0.2.2) 19 | 20 | ## 0.2.1 (2024-03-18) 21 | 22 | #### Fixes 23 | 24 | * Fix runtime error due to numpy deprecation. ([#69](https://github.com/kev-m/pyCoilGen/issues/69)) 25 | * Fix runtime errors due to deprecated np.warnings 26 | 27 | Full set of changes: [`0.2.0...0.2.1`](https://github.com/kev-m/pyCoilGen/compare/0.2.0...0.2.1) 28 | 29 | ## 0.2.0 (2023-11-07) 30 | 31 | #### New Features 32 | 33 | * (topology): Expose cut_plane_definition to CLI. 34 | #### Fixes 35 | 36 | * (postprocessing): Do not calculate inductance if skip_postprocessing is True. 37 | 38 | Full set of changes: [`0.1.3...0.2.0`](https://github.com/kev-m/pyCoilGen/compare/0.1.3...0.2.0) 39 | 40 | ## 0.1.3 (2023-10-11) 41 | 42 | #### Fixes 43 | 44 | * Implemented bugfix from CoilGen 45 | 46 | Full set of changes: [`0.1.2...0.1.3`](https://github.com/kev-m/pyCoilGen/compare/0.1.2...0.1.3) 47 | 48 | ## 0.1.2 (2023-10-09) 49 | 50 | #### Fixes 51 | 52 | * (installation): Relaxing dependencies. 53 | 54 | Full set of changes: [`0.1.1...0.1.2`](https://github.com/kev-m/pyCoilGen/compare/0.1.1...0.1.2) 55 | 56 | ## 0.1.1 (2023-10-04) 57 | 58 | #### Fixes 59 | 60 | * (meshes): Fix bi-planar mesh so that the normals point outwards. 61 | 62 | Full set of changes: [`0.1.0...0.1.1`](https://github.com/kev-m/pyCoilGen/compare/0.1.0...0.1.1) 63 | 64 | ## 0.1.0 (2023-10-03) 65 | 66 | #### New Features 67 | 68 | * (exporter): Extending the list of supported export types. 69 | * (meshes): Using the mesh factory for coil, target and shield meshes. 70 | * (meshes): Adding 'create circular mesh' to the mesh factory. 71 | * (meshes): Using auto-discovery to discover mesh builders. ([#55](https://github.com/kev-m/pyCoilGen/issues/55)) 72 | #### Fixes 73 | 74 | * Bugfix with trying to access invalid function. 75 | * (meshes): Using stl_mesh_filename and coil_mesh_file. 76 | * (meshes): Supporting int and float parameters. 77 | 78 | Full set of changes: [`0.0.11...0.1.0`](https://github.com/kev-m/pyCoilGen/compare/0.0.11...0.1.0) 79 | 80 | ## 0.0.11 (2023-09-28) 81 | 82 | #### Fixes 83 | 84 | * Fixing exception when skip_inductance_calculation is True. 85 | * Fixing exception when skip_postprocessing is True. 86 | * (build_cylinder_mesh): Generated cylinder mesh exactly matches input dimensions. 87 | #### Docs 88 | 89 | * Moving the release procedure to its own file. ([#51](https://github.com/kev-m/pyCoilGen/issues/51)) 90 | 91 | Full set of changes: [`0.0.10...0.0.11`](https://github.com/kev-m/pyCoilGen/compare/0.0.10...0.0.11) 92 | 93 | ## 0.0.10 (2023-09-26) 94 | 95 | #### Docs 96 | 97 | * Fixing URL in pyproject.toml 98 | 99 | Full set of changes: [`0.0.9...0.0.10`](https://github.com/kev-m/pyCoilGen/compare/0.0.9...0.0.10) 100 | 101 | ## 0.0.9 (2023-09-26) 102 | 103 | #### Docs 104 | 105 | * Updating release procedure. 106 | #### Others 107 | 108 | * Globally reformatted sources. 109 | 110 | Full set of changes: [`0.0.8...0.0.9`](https://github.com/kev-m/pyCoilGen/compare/0.0.8...0.0.9) 111 | 112 | ## 0.0.8 (2023-09-25) 113 | 114 | #### New Features 115 | 116 | * Initial release candidate. 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behaviour that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behaviour by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at [kevin@kmz.co.za]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from version 2.0 of the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html) from [Contributor Covenant](https://www.contributor-covenant.org). 44 | 45 | For answers to common questions about this code of conduct, see the [Contributor Covenant FAQ](https://www.contributor-covenant.org/faq). 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyCoilGen 2 | [![GitHub license](https://img.shields.io/github/license/kev-m/pyCoilGen)](https://github.com/kev-m/pyCoilGen/blob/main/LICENSE) 3 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycoilgen?logo=pypi)](https://pypi.org/project/pycoilgen/) 4 | [![semver](https://img.shields.io/badge/semver-2.0.0-blue)](https://semver.org/) 5 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/kev-m/pyCoilGen?sort=semver)](https://github.com/kev-m/pyCoilGen/releases) 6 | [![Code style: autopep8](https://img.shields.io/badge/code%20style-autopep8-000000.svg)](https://pypi.org/project/autopep8/) 7 | 8 | ![pyCoilGen logo](https://github.com/kev-m/pyCoilGen/blob/master/docs/source/figures/Logo_small.png) 9 | 10 | The **pyCoilGen** project is an open source tool for generating coil winding layouts, such as 11 | [gradient field coils](https://mriquestions.com/gradient-coils.html), within the 12 | [MRI](https://en.wikipedia.org/wiki/Magnetic_resonance_imaging) and 13 | [NMR](https://en.wikipedia.org/wiki/Nuclear_magnetic_resonance) environments. **pyCoilGen** is based on a boundary element method and generates interconnected non-overlapping wire-tracks on 3D support structures. 14 | 15 | This Python project is a port of the MATLAB [CoilGen code](https://github.com/Philipp-MR/CoilGen) developed by Philipp Amrein. 16 | 17 | For detailed documentation, refer to the [pyCoilGen Documentation](https://pycoilgen.readthedocs.io/). 18 | 19 | ## Installation 20 | 21 | Refer to the [Installation Guide](https://pycoilgen.readthedocs.io/en/latest/installation.html) for detailed instructions on how to install and set up **pyCoilGen**. 22 | 23 | ## Examples 24 | 25 | The [`examples`](https://github.com/kev-m/pyCoilGen/blob/master/examples) directory contains several examples for how to use **pyCoilGen**. These examples demonstrate different scenarios and configurations for generating coil layouts. 26 | 27 | ## Acknowledgements 28 | 29 | The porting of the code from MATLAB to Python was facilitated by [ChatGPT, May 24 through August 3 Version](https://chat.openai.com) with manual corrections. 30 | 31 | Additional cross-checking was done using [MATLAB Online](https://www.mathworks.com/products/matlab-online.html) provided by MathWorks. 32 | 33 | ## Contributing 34 | 35 | If you'd like to contribute to **pyCoilGen**, follow the guidelines outlined in the [Contributing Guide](https://github.com/kev-m/pyCoilGen/blob/master/CONTRIBUTING.md). 36 | 37 | ## License 38 | 39 | See [`LICENSE.txt`](https://github.com/kev-m/pyCoilGen/blob/master/LICENSE.txt) for more information. 40 | 41 | ## Contact 42 | 43 | For inquiries and discussion, use [pyCoilGen Discussions](https://github.com/kev-m/pyCoilGen/discussions). 44 | 45 | ## Issues 46 | 47 | For issues related to this Python implementation, visit the [Issues](https://github.com/kev-m/pyCoilGen/issues) page. 48 | 49 | ## Citation 50 | 51 | Use the following publication, if you need to cite this work: 52 | 53 | - [Amrein, P., Jia, F., Zaitsev, M., & Littin, S. (2022). CoilGen: Open-source MR coil layout generator. Magnetic Resonance in Medicine, 88(3), 1465-1479.](https://onlinelibrary.wiley.com/doi/10.1002/mrm.29294) 54 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Procedure 2 | The basic procedure for releasing a new version of **pyCoilGen** consists of: 3 | - Running the unit tests. 4 | - Checking the documentation 5 | - Create a tag and update the change log 6 | - Build and Publish the project 7 | 8 | ## Check the Unit Tests 9 | 10 | Run the unit tests from the project top level directory: 11 | ```bash 12 | pytest 13 | ``` 14 | 15 | ## Check the Documentation 16 | 17 | Build and check the documentation: 18 | ```bash 19 | cd docs 20 | make clean html 21 | ``` 22 | 23 | Load the `docs/build/html/index.html`. 24 | 25 | ## Create a Tag 26 | 27 | **pyCoilGen** uses semantic versioning. Update the version number in [pyCoilGen/__init__.py](pyCoilGen/__init__.py) according to changes since the previous tag. 28 | 29 | Create a tag with only the current number, e.g. `0.0.9`. 30 | ```bash 31 | git tag 0.0.9 32 | ``` 33 | 34 | ## Update the ChangeLog 35 | 36 | **pyCoilGen** uses `auto-changelog` to parse git commit messages and generate the `CHANGELOG.md`. 37 | 38 | ```bash 39 | auto-changelog 40 | git add CHANGELOG.md 41 | git commit -m "Updating CHANGELOG" 42 | git push 43 | git push --tags 44 | ``` 45 | 46 | ## Building the Package 47 | 48 | The sources are published as two packages using `flit` to build and publish the artifacts. 49 | 50 | The project details are defined in the `pyproject.toml` files. The version and description are defined in the top-level `__init__.py` file for each package. 51 | 52 | This project uses [semantic versioning](https://semver.org/). 53 | 54 | Build and publish the main artifact: 55 | ```bash 56 | $ flit build 57 | $ flit publish 58 | ``` 59 | 60 | Build and publish the data artifact if it has changed: 61 | ```bash 62 | $ cd data 63 | $ flit build 64 | $ flit publish 65 | ``` 66 | ## Make a GitHub Release 67 | 68 | Go to the GitHub project administration page and [publish a release](https://github.com/kev-m/pyCoilGen/releases/new) using the tag created, above. 69 | 70 | Update the `release` branch: 71 | ```bash 72 | git checkout release 73 | git rebase master 74 | git push 75 | ``` -------------------------------------------------------------------------------- /conversion_notes.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | ## SciPiy and Dependencies 3 | SciPy might actually not be needed for the release version. To be checked!! 4 | Note: Also need BLAS and gfortran to install scipy: 5 | ```bash 6 | $ sudo apt-get install libopenblas-dev 7 | $ sudo apt install gfortran 8 | ``` 9 | 10 | ## Trimesh Dependencies 11 | Need to manually install Trimesh dependencies. 12 | 1. rtree (for nearest.on_surface) 13 | 14 | I needed to manually install libspatialindex library (for rtree). 15 | ```bash 16 | $ sudo apt-get install libspatialindex-dev 17 | ``` 18 | 19 | ## FastHenry2 20 | The `FastHenry2` application needs to downloaded and installed. 21 | 22 | ### Windows 23 | Go to the [download](https://www.fastfieldsolvers.com/download.htm) page, fill out the form, then download the 24 | `FastFieldSolvers` bundle, e.g. FastFieldSolvers Software Bundle Version 5.2.0 25 | 26 | Under Linux systems, the project should be cloned from [GitHub](https://github.com/ediloren/FastHenry2) and compiled. 27 | ### Linux 28 | ```bash 29 | $ git clone https://github.com/ediloren/FastHenry2.git 30 | $ cd FastHenry2/src 31 | $ make 32 | ``` 33 | 34 | # Conversion Notes 35 | ## Indexing 36 | Note that MATLAB uses base index of 1 for arrays, whereas NumPy uses 0. Adjust all functions that create `faces` arrays accordingly. 37 | 38 | ## Mesh Geometry 39 | Confirm: Are mesh normals computed according to the right-hand rule? i.e. defined using the "counter-clockwise" or "anti-clockwise" 40 | vertex ordering, where the vertices of the face are specified in a counter-clockwise direction when viewed from the outside of the mesh. 41 | 42 | ## Other Weirdnesses 43 | ### build_cylinder_mesh.py 44 | I think there may be a fence-post bug in the original MATLAB code. The height of the resulting 45 | cylinder does not match the cylinder_height parameter. This is especially noticable for small 46 | values of num_longitudinal_divisions. See test_build_cylinder_mesh.py. 47 | 48 | ### define_target_field.py 49 | In define_target_field, line 104, I have to swap y and z coords to match MATLAB: 50 | ```python 51 | target_points = np.vstack((target_grid_x.ravel(), target_grid_z.ravel(), target_grid_y.ravel())) 52 | ``` 53 | 54 | 55 | 56 | ## Notes per Sub_Function 57 | ### calc_gradient_along_vector(field, field_coords, target_endcoding_function) 58 | The target_endcoding_function needs to be converted, too. The Python implementation uses `eval` 59 | whereas the original MATLAB uses `my_fun=str2func("@(x,y,z)"+target_endcoding_function);`. 60 | 61 | TODO: Check caller implementations and convert appropriately. 62 | 63 | # Runtime Errors 64 | When target_region_resolution is 10. 65 | ``` bash 66 | File "/home/kevin/Dev/CoilGen-Python/sub_functions/interconnect_within_groups.py", line 77, in interconnect_within_groups 67 | part_group.opened_loop = part_group.loops.uv 68 | AttributeError: 'list' object has no attribute 'uv' 69 | ``` -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # pyCoilGen Data 2 | 3 | [pyCoilGen](https://github.com/kev-m/pyCoilGen) is an application for generating gradient field coil layouts within the MRI/NMR environment. 4 | 5 | The tool is based on a boundary element method and creates interconnected, non-overlapping wire-tracks on 3D support structures. 6 | 7 | This package provides optional extra data for **pyCoilGen** that provides coil mesh surface `.stl` files, pre-calculated target fields and solutions. 8 | 9 | For detailed documentation, refer to the [pyCoilGen Documentation](https://pycoilgen.readthedocs.io/). 10 | 11 | ## Installation 12 | 13 | Install **pyCoilGen** using pip: 14 | ```bash 15 | $ pip install pycoilgen 16 | ``` 17 | 18 | Install this **pyCoilGen Data** using pip: 19 | ```bash 20 | $ pip install pycoilgen_data 21 | ``` 22 | 23 | ## License 24 | 25 | See [`LICENSE.txt`](https://github.com/kev-m/pyCoilGen/blob/master/LICENSE.txt) for more information. 26 | 27 | -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/Cone_shaped_self_shielding_surface.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/Cone_shaped_self_shielding_surface.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/Cone_shaped_self_shielding_surface2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/Cone_shaped_self_shielding_surface2.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/Double_coaxial_open_cylinder_r1_400mm_r2_600_length_1500mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/Double_coaxial_open_cylinder_r1_400mm_r2_600_length_1500mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/Open_cylinder_r750mm_length_1500mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/Open_cylinder_r750mm_length_1500mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/Primary_r500mm_l1500mm_Shield_r750mm_l1750mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/Primary_r500mm_l1500mm_Shield_r750mm_l1750mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/bi_planer_rectangles_width_1000mm_distance_500mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/bi_planer_rectangles_width_1000mm_distance_500mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/closed_cylinder_length_300mm_radius_150mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/closed_cylinder_length_300mm_radius_150mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/closed_cylinder_radius10mm_length40mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/closed_cylinder_radius10mm_length40mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_90deg_rotated.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_90deg_rotated.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_holes_250mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_holes_250mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_regular_holes.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius500mm_length1500mm_regular_holes.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius50mm_length350mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius50mm_length350mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius600mm_length_1500mm_regular.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius600mm_length_1500mm_regular.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/cylinder_radius900mm_length_1900mm_regular.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/cylinder_radius900mm_length_1900mm_regular.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_ccs_bionic.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_ccs_bionic.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_ccs_bionic_shift_1cm_in_y.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_ccs_bionic_shift_1cm_in_y.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_extraoral_ccs.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_extraoral_ccs.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_extraoral_ccs2_shifted_2cm_in_y.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_extraoral_ccs2_shifted_2cm_in_y.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_gradient_ccs.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_gradient_ccs.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_gradient_ccs_single_low.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_gradient_ccs_single_low.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_gradient_target_region.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_gradient_target_region.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_gradient_target_region_single_low.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_gradient_target_region_single_low.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_oral_target.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_oral_target.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_oral_target_bionic.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_oral_target_bionic.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/dental_oral_target_shifted_1cm_in_y.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/dental_oral_target_shifted_1cm_in_y.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/diamond_probe_gradient_css.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/diamond_probe_gradient_css.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/diamond_probe_target_surface.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/diamond_probe_target_surface.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/half_sphere_shape.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/half_sphere_shape.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/rectangular_plane_500mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/rectangular_plane_500mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/sphere_radius150mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/sphere_radius150mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/sphere_radius300mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/sphere_radius300mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/sphere_radius_25mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/sphere_radius_25mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Geometry_Data/sphere_radius_5mm.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Geometry_Data/sphere_radius_5mm.stl -------------------------------------------------------------------------------- /data/pyCoilGenData/Pre_Optimized_Solutions/source_data_SVD_coil.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Pre_Optimized_Solutions/source_data_SVD_coil.npy -------------------------------------------------------------------------------- /data/pyCoilGenData/Pre_Optimized_Solutions/source_data_breast_coil.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/Pre_Optimized_Solutions/source_data_breast_coil.npy -------------------------------------------------------------------------------- /data/pyCoilGenData/__init__.py: -------------------------------------------------------------------------------- 1 | """Extra data for pyCoilGen, the Open source Magnetic Resonance Coil Generator.""" 2 | __version__ = "0.0.4" 3 | 4 | from os import path 5 | 6 | __data_directory = __file__[:-(len('__init.py__'))] 7 | 8 | def data_directory(): 9 | """Get the installation directory of pyCoilGenData""" 10 | return __data_directory -------------------------------------------------------------------------------- /data/pyCoilGenData/target_fields/intraoral_dental_target_field.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/data/pyCoilGenData/target_fields/intraoral_dental_target_field.npy -------------------------------------------------------------------------------- /data/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pycoilgen_data" 7 | authors = [ 8 | {name = "Kevin Meyer", email = "kevin@kmz.co.za"}, 9 | {name = "Philipp Amrein", email="none@noreply.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE.txt"} 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Environment :: Console", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: Healthcare Industry", 18 | "Intended Audience :: Science/Research", 19 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Topic :: Scientific/Engineering", 30 | ] 31 | 32 | requires-python = ">=3.6" 33 | keywords = [ 34 | "MRI", 35 | "Magnetic Resonance Imaging", 36 | "NMR", 37 | "Nuclear Resonance Imaging", 38 | "Target Field", 39 | "Gradient Field", 40 | "Physics", 41 | "Coil", 42 | ] 43 | dependencies = [ "pycoilgen" ] 44 | dynamic = ["version", "description"] 45 | 46 | [project.urls] 47 | Home = "https://github.com/kev-m/pyCoilGen" 48 | Documentation = "https://pycoilgen.readthedocs.io/" 49 | Source = "https://github.com/kev-m/pyCoilGen" 50 | "Code of Conduct" = "https://github.com/kev-m/pyCoilGen/blob/master/CODE_OF_CONDUCT.md" 51 | "Bug tracker" = "https://github.com/kev-m/pyCoilGen/issues" 52 | Changelog = "https://github.com/kev-m/pyCoilGen/blob/master/CHANGELOG.md" 53 | Contributing = "https://github.com/kev-m/pyCoilGen/blob/master/CONTRIBUTING.md" 54 | 55 | 56 | [tool.flit.module] 57 | name = "pyCoilGenData" 58 | -------------------------------------------------------------------------------- /docs/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.11" 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | 11 | # Dependencies required to build your docs 12 | python: 13 | install: 14 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # pyCoilGen Documentation 2 | 3 | The documentation is written in [MarkDown](https://commonmark.org/help/) and built with Sphinx and the 4 | [`myst-parser` extension](https://myst-parser.readthedocs.io/en/latest/index.html). 5 | 6 | The documentation structure is maintained in [source/index.rst](source/index.rst). If a new file is added, it must be 7 | manually added to the index in the appropriate place. 8 | 9 | ## Installation 10 | 11 | To build the documentation locally, install the documentation dependencies using pip: 12 | ```bash 13 | $ pip install -r requirements.txt 14 | ``` 15 | 16 | ## Building 17 | 18 | Build the documentation using `make`: 19 | ```bash 20 | $ make clean html 21 | ``` 22 | 23 | The documentation can then be previewed by loading [build/html/index.html](build/html/index.html). 24 | 25 | ## Publishing 26 | 27 | The documentation is automatically published to [ReadTheDocs](https://pycoilgen.readthedocs.io/) when the `master` branch is updated. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements only to build the documentation 2 | sphinx==5.3.0 3 | myst-parser==1.0.0 4 | sphinx_rtd_theme 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | import os 3 | import sys 4 | sys.path.insert(0, os.path.abspath('../..')) 5 | 6 | from pyCoilGen import __version__ 7 | # 8 | # For the full list of built-in configuration values, see the documentation: 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 10 | 11 | # -- Project information ----------------------------------------------------- 12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 13 | 14 | project = 'pyCoilGen User Guide' 15 | copyright = '2023, Kevin Meyer, Philipp Amrein' 16 | author = 'Kevin Meyer, Philipp Amrein' 17 | version = __version__ 18 | release = __version__ 19 | 20 | # -- General configuration --------------------------------------------------- 21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 22 | source_dir = os.path.abspath(os.path.dirname(__file__)) 23 | 24 | extensions = [ 25 | # Using 'myst_parser' for MD parsing, as per https://docs.readthedocs.io/en/stable/intro/getting-started-with-sphinx.html 26 | 'myst_parser', 27 | ] 28 | # Specify the source suffix for Markdown files 29 | source_suffix = { 30 | '.rst': 'restructuredtext', 31 | '.txt': 'markdown', 32 | '.md': 'markdown', 33 | } 34 | 35 | # Myst Extensions 36 | myst_enable_extensions = [ 37 | 'deflist', 38 | ] 39 | myst_heading_anchors = 3 40 | 41 | templates_path = ['_templates'] 42 | exclude_patterns = [ 43 | '_build', 'Thumbs.db', '.DS_Store', 44 | 'requirements.txt', 45 | ] 46 | 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 51 | 52 | # html_theme = 'latest' 53 | html_theme = "sphinx_rtd_theme" 54 | html_static_path = ['_static'] 55 | -------------------------------------------------------------------------------- /docs/source/figures/3D_axes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/3D_axes.png -------------------------------------------------------------------------------- /docs/source/figures/Logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/Logo_large.png -------------------------------------------------------------------------------- /docs/source/figures/Logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/Logo_small.png -------------------------------------------------------------------------------- /docs/source/figures/builder_biplanar_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/builder_biplanar_mesh.png -------------------------------------------------------------------------------- /docs/source/figures/builder_circular_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/builder_circular_mesh.png -------------------------------------------------------------------------------- /docs/source/figures/builder_cylindrical_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/builder_cylindrical_mesh.png -------------------------------------------------------------------------------- /docs/source/figures/builder_planar_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/builder_planar_mesh.png -------------------------------------------------------------------------------- /docs/source/figures/flow_chart_algorithm_revised.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/flow_chart_algorithm_revised.png -------------------------------------------------------------------------------- /docs/source/figures/illustration_cut_plane_definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/illustration_cut_plane_definition.png -------------------------------------------------------------------------------- /docs/source/figures/loop_cut_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/loop_cut_width.png -------------------------------------------------------------------------------- /docs/source/figures/mesh_example_gradient_3D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/mesh_example_gradient_3D.png -------------------------------------------------------------------------------- /docs/source/figures/mesh_s2_shim_swept_3D_copper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/mesh_s2_shim_swept_3D_copper.png -------------------------------------------------------------------------------- /docs/source/figures/mesh_shielded_ygradient_swept_3D_copper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/mesh_shielded_ygradient_swept_3D_copper.png -------------------------------------------------------------------------------- /docs/source/figures/plot_errors_example_ygradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_errors_example_ygradient.png -------------------------------------------------------------------------------- /docs/source/figures/plot_errors_s2_shim_coil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_errors_s2_shim_coil.png -------------------------------------------------------------------------------- /docs/source/figures/plot_example_ygradient_2D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_example_ygradient_2D.png -------------------------------------------------------------------------------- /docs/source/figures/plot_example_ygradient_3D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_example_ygradient_3D.png -------------------------------------------------------------------------------- /docs/source/figures/plot_s2_shim_coil_2D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_s2_shim_coil_2D.png -------------------------------------------------------------------------------- /docs/source/figures/plot_s2_shim_coil_3D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_s2_shim_coil_3D.png -------------------------------------------------------------------------------- /docs/source/figures/plot_s2_shim_coil_Target_Field_Error_XY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_s2_shim_coil_Target_Field_Error_XY.png -------------------------------------------------------------------------------- /docs/source/figures/plot_s2_shim_coil_Target_Field_XY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_s2_shim_coil_Target_Field_XY.png -------------------------------------------------------------------------------- /docs/source/figures/plot_shielded_ygradient_coil_2D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_shielded_ygradient_coil_2D.png -------------------------------------------------------------------------------- /docs/source/figures/plot_solutions_Halbach_study_Tikhonov05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_solutions_Halbach_study_Tikhonov05.png -------------------------------------------------------------------------------- /docs/source/figures/plot_solutions_Halbach_study_Tikhonov10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/docs/source/figures/plot_solutions_Halbach_study_Tikhonov10.png -------------------------------------------------------------------------------- /docs/source/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | stl 4 | : A [file format for stereolithography CAD files](https://en.wikipedia.org/wiki/STL_(file_format)) that describes a 5 | triangulated surface consisting of facets and vertices. 6 | 7 | Gradient Field 8 | : A magnetic field in the target volume that has a [specific gradient](https://mriquestions.com/gradient-coils.html). 9 | 10 | Target Field 11 | : A desired vector field in the desired target volume. -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyCoilGen documentation master file, created by 2 | sphinx-quickstart on Tue Sep 19 14:51:08 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | pyCoilGen User Guide 7 | ==================== 8 | |licence| |pypi| |semver| |version| |autopep8| 9 | 10 | .. |licence| image:: https://img.shields.io/pypi/l/pycoilgen 11 | :target: https://github.com/kev-m/pyCoilGen/blob/main/LICENSE 12 | :alt: PyPI - License 13 | .. |pypi| image:: https://img.shields.io/pypi/v/pycoilgen 14 | :target: https://pypi.org/project/pycoilgen/ 15 | :alt: PyPI 16 | .. |semver| image:: https://img.shields.io/badge/semver-2.0.0-blue 17 | :target: https://semver.org/ 18 | :alt: SemVer 2.0.0 19 | .. |version| image:: https://img.shields.io/pypi/pyversions/pycoilgen 20 | :alt: PyPI - Python Version 21 | .. |autopep8| image:: https://img.shields.io/badge/code%20style-autopep8-000000.svg 22 | :target: https://pypi.org/project/autopep8/ 23 | :alt: Code style - Black 24 | 25 | .. image:: figures/Logo_small.png 26 | :alt: pyCoilGen logo, a cylinder lying on its side with a complex wire wrapped around it. The word pyCoilGen written underneath. 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | :numbered: 4 31 | 32 | overview.md 33 | installation.md 34 | quick_start.md 35 | configuration.md 36 | results.md 37 | glossary.md -------------------------------------------------------------------------------- /docs/source/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > **Note:** Ensure that you have the necessary permissions to install packages on your system. Consider using a [Python virtual environment](https://docs.python.org/3/library/venv.html) to manage your dependencies. 4 | 5 | To use **pyCoilGen**, you need to: 6 | 7 | * [install a supported version of Python](#install-python) 8 | * [install the `pycoilgen` package](#install-pycoilgen) 9 | * [install missing dependencies](#scipy-installation-issues-on-linux), if these are not already on your system. 10 | 11 | Optionally, you can install: 12 | 13 | * the [**pyCoilGen** data package](#install-pycoilgen-data-package) 14 | * [FastHenry2](#fasthenry2) 15 | 16 | 17 | ## Install Python 18 | 19 | **pyCoilGen** depends on Python >= 3.6. 20 | 21 | Please follow the instructions for your operating system to [install Python](https://www.python.org/downloads/). 22 | 23 | ## Install pyCoilGen 24 | 25 | To install **pyCoilGen**, you can use `pip`, the Python package manager. 26 | 27 | ```bash 28 | $ pip install pycoilgen 29 | ``` 30 | 31 | ## SciPy Installation Issues on Linux 32 | 33 | Some Linux users have reported issues when installing SciPy. In order to complete the SciPy installation, it was necessary to install 34 | [BLAS](https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms) and gfortran. 35 | ```bash 36 | $ sudo apt-get install libopenblas-dev gfortran 37 | ``` 38 | 39 | ## Optional packages 40 | 41 | ### Install pyCoilGen Data Package 42 | 43 | There is an optional data package for **pyCoilGen** that provides 34 mesh `.stl` files of various shapes and sizes, one pre-calculated target field and two pre-optimised solutions. This package can be installed with `pip`. 44 | 45 | ```bash 46 | $ pip install pycoilgen_data 47 | ``` 48 | 49 | These files will be automatically detected by **pyCoilGen**. 50 | 51 | 52 | ### FastHenry2 53 | The `FastHenry2` application is used to calculate the resistance and inductance of the coil winding. If these values 54 | are important to your coil project, this application must be downloaded and installed. 55 | 56 | #### Windows 57 | Go to the [download](https://www.fastfieldsolvers.com/download.htm) page, fill out the form, and then download and install 58 | the `FastFieldSolvers` bundle, e.g. FastFieldSolvers Software Bundle Version 5.2.0. 59 | 60 | #### Linux 61 | 62 | Clone the `FastHenry2` repository from [GitHub](https://github.com/ediloren/FastHenry2) and compile it: 63 | 64 | ```bash 65 | $ git clone https://github.com/ediloren/FastHenry2.git 66 | $ cd FastHenry2/src 67 | $ make 68 | ``` 69 | Thereafter you can manually copy the binary executable file `bin/fasthenry` to the `/usr/bin` directory or use it in 70 | place by setting the `fasthenry_bin` [configuration parameter](./configuration.md#calculate-inductance) to the location 71 | of the binary. -------------------------------------------------------------------------------- /docs/source/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | **pyCoilGen** is an open source tool for generating coil winding layouts, such as 4 | [gradient field coils](https://mriquestions.com/gradient-coils.html), within the 5 | [MRI](https://en.wikipedia.org/wiki/Magnetic_resonance_imaging) and 6 | [NMR](https://en.wikipedia.org/wiki/Nuclear_magnetic_resonance) environments. 7 | 8 | **pyCoilGen** is based on a boundary element method and generates interconnected non-overlapping wire-tracks on 3D support structures. 9 | 10 | The source code for **pyCoilGen** is available on [GitHub](https://github.com/kev-m/pyCoilGen). 11 | 12 | ```{figure} figures/mesh_shielded_ygradient_swept_3D_copper.png 13 | :scale: 50 % 14 | :align: center 15 | :alt: A 3D rendered view of the `.stl` swept output. 16 | 17 | A 3D rendering of the `.stl` output for the `shielded_ygradient_coil.py` example. 18 | ``` 19 | ```{figure} figures/plot_shielded_ygradient_coil_2D.png 20 | :scale: 50 % 21 | :align: center 22 | :alt: A colour plot showing the stream function and the corresponding contour groups. 23 | 24 | A colour plot showing the 2D stream function and the corresponding contour groups for the `shielded_ygradient_coil.py` example. 25 | ``` 26 | 27 | ## Features 28 | 29 | With **pyCoilGen**, you can: 30 | 31 | - Specify a target field (e.g., `bz(x,y,z)=y`) and a surface mesh geometry. 32 | - Use built-in surface mesh geometries or 3D meshes defined in `.stl` files. 33 | - Generate a coil layout in the form of a non-overlapping, interconnected wire track to achieve the desired field, exported as an `.stl` file. 34 | 35 | For a detailed description of the algorithm, refer to the research paper [CoilGen: Open-source MR coil layout generator](https://onlinelibrary.wiley.com/doi/10.1002/mrm.29294). 36 | 37 | ## Examples 38 | 39 | The [`examples`](https://github.com/kev-m/pyCoilGen/blob/master/examples) directory in the GitHub repository contains several usage examples for **pyCoilGen**. These examples demonstrate different scenarios and configurations for generating coil layouts. 40 | 41 | ## Citation 42 | 43 | Use the following publication if you need to cite this work: 44 | 45 | - [Amrein, P., Jia, F., Zaitsev, M., & Littin, S. (2022). CoilGen: Open-source MR coil layout generator. Magnetic Resonance in Medicine, 88(3), 1465-1479.](https://onlinelibrary.wiley.com/doi/10.1002/mrm.29294) 46 | -------------------------------------------------------------------------------- /examples/biplanar_xgradient.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | # Logging 5 | import logging 6 | 7 | # Local imports 8 | from pyCoilGen.pyCoilGen_release import pyCoilGen 9 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 10 | 11 | if __name__ == '__main__': 12 | # Set up logging 13 | log = logging.getLogger(__name__) 14 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 15 | # logging.basicConfig(level=logging.INFO) 16 | 17 | arg_dict = { 18 | 'field_shape_function': 'x', # definition of the target field 19 | 'coil_mesh_file': 'bi_planer_rectangles_width_1000mm_distance_500mm.stl', 20 | 'target_mesh_file': 'none', 21 | 'secondary_target_mesh_file': 'none', 22 | 'secondary_target_weight': 0.5, 23 | 'target_region_radius': 0.1, # in meter 24 | # 'target_region_resolution': 10, # MATLAB 10 is the default 25 | 'use_only_target_mesh_verts': False, 26 | 'sf_source_file': 'none', 27 | # the number of potential steps that determines the later number of windings (Stream function discretization) 28 | 'levels': 14, 29 | # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 30 | 'pot_offset_factor': 0.25, 31 | 'surface_is_cylinder_flag': True, 32 | # the width for the interconnections are interconnected; in meter 33 | 'interconnection_cut_width': 0.05, 34 | # the length for which overlapping return paths will be shifted along the surface normals; in meter 35 | 'normal_shift_length': 0.01, 36 | 'iteration_num_mesh_refinement': 1, # the number of refinements for the mesh; 37 | 'set_roi_into_mesh_center': True, 38 | 'force_cut_selection': ['high'], 39 | # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 40 | 'level_set_method': 'primary', 41 | 'skip_postprocessing': False, 42 | 'skip_inductance_calculation': False, 43 | 'tikhonov_reg_factor': 10, # Tikhonov regularization factor for the SF optimization 44 | 45 | 'output_directory': 'images', # [Current directory] 46 | 'project_name': 'biplanar_xgradient', 47 | 'persistence_dir': 'debug', 48 | 'debug': DEBUG_BASIC, 49 | } 50 | 51 | result = pyCoilGen(log, arg_dict) 52 | -------------------------------------------------------------------------------- /examples/plotting_s2_shim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Kevin Meyer 3 | Bela Pena s.p. 4 | September 2023 5 | 6 | Demonstrate using the plotting routines to visualise data from the s2_shim_coil example. 7 | """ 8 | 9 | from os import makedirs 10 | 11 | import matplotlib.pyplot as plt 12 | from pyCoilGen.helpers.persistence import load 13 | import pyCoilGen.plotting as pcg_plt 14 | 15 | solution = load('debug', 's2_shim_coil', 'final') 16 | which = solution.input_args.project_name 17 | save_dir = f'{solution.input_args.output_directory}' 18 | makedirs(save_dir, exist_ok=True) 19 | 20 | coil_solutions = [solution] 21 | 22 | # Plot a multi-plot summary of the solution 23 | pcg_plt.plot_various_error_metrics(coil_solutions, 0, f'{which}', save_dir=save_dir) 24 | 25 | # Plot the 2D projection of stream function contour loops. 26 | pcg_plt.plot_2D_contours_with_sf(coil_solutions, 0, f'{which} 2D', save_dir=save_dir) 27 | pcg_plt.plot_3D_contours_with_sf(coil_solutions, 0, f'{which} 3D', save_dir=save_dir) 28 | 29 | # Plot the vector fields 30 | coords = solution.target_field.coords 31 | 32 | # Plot the computed target field. 33 | plot_title = f'{which} Target Field ' 34 | field = solution.solution_errors.combined_field_layout 35 | pcg_plt.plot_vector_field_xy(coords, field, plot_title=plot_title, save_dir=save_dir) 36 | 37 | # Plot the difference between the computed target field and the input target field. 38 | plot_title = f'{which} Target Field Error ' 39 | field = solution.solution_errors.combined_field_layout - solution.target_field.b 40 | pcg_plt.plot_vector_field_xy(coords, field, plot_title=plot_title, save_dir=save_dir) 41 | -------------------------------------------------------------------------------- /examples/preoptimzed_breast_coil.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | import numpy as np 5 | 6 | # Logging 7 | import logging 8 | 9 | 10 | # Local imports 11 | from pyCoilGen.pyCoilGen_release import pyCoilGen 12 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 13 | 14 | 15 | """ 16 | Author: Philipp Amrein, University Freiburg, Medical Center, Radiology, 17 | Medical Physics 18 | February 2022 19 | 20 | This generates a diffusion weighting MR gradient coil for the female breast. An already optimized solution for the 21 | stream function is loaded. 22 | 23 | For the background of this project refer to: Jia F, Littin S, Amrein P, Yu H, Magill AW, Kuder TA, Bickelhaupt 24 | S, Laun F, Ladd ME, Zaitsev M. Design of a high-performance non-linear gradient coil for diffusion weighted MRI 25 | of the breast. J Magn Reson. 2021 Oct;331:107052. doi: 10.1016/j.jmr.2021.107052. Epub 2021 Aug 14. PMID: 34478997. 26 | """ 27 | 28 | if __name__ == '__main__': 29 | # Set up logging 30 | log = logging.getLogger(__name__) 31 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 32 | # logging.basicConfig(level=logging.INFO) 33 | 34 | theta = np.arange(0, 2 * np.pi + (2 * np.pi) / (10 - 1), (2 * np.pi) / (10 - 1)) 35 | cross_section = np.vstack((np.sin(theta), np.cos(theta))) 36 | cross_section *= np.array([[0.002], [0.002]]) 37 | 38 | arg_dict = { 39 | 'field_shape_function': 'none', # definition of the target field 40 | 'coil_mesh_file': 'none', 41 | # 'min_loop_significance':1, 42 | 'use_only_target_mesh_verts': False, 43 | 'sf_source_file': 'source_data_breast_coil.npy', 44 | # the number of potential steps that determines the later number of windings (Stream function discretization) 45 | 'levels': 14, 46 | 'pot_offset_factor': 0.25, # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 47 | 'surface_is_cylinder_flag': False, 48 | 'interconnection_cut_width': 0.01, # the width for the interconnections are interconnected; in meter 49 | 'normal_shift_length': 0.01, # the length for which overlapping return paths will be shifted along the surface normals; in meter 50 | 'force_cut_selection': ['high'], 51 | 'level_set_method': 'primary', # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 52 | 'skip_postprocessing': False, 53 | 'cross_sectional_points': cross_section, 54 | 'skip_sweep': False, 55 | 'skip_inductance_calculation': False, 56 | 57 | 'project_name': 'Preoptimzed_Breast_Coil', 58 | 'persistence_dir': 'debug', 59 | 'output_directory': 'images', # [Current directory] 60 | 'debug': DEBUG_BASIC, 61 | } 62 | 63 | result = pyCoilGen(log, arg_dict) 64 | -------------------------------------------------------------------------------- /examples/preoptimzed_svd_coil.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import numpy as np 3 | import sys 4 | 5 | 6 | # Logging 7 | import logging 8 | 9 | 10 | # Local imports 11 | from pyCoilGen.pyCoilGen_release import pyCoilGen 12 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 13 | 14 | """ 15 | Author: Philipp Amrein, University Freiburg, Medical Center, Radiology, 16 | Medical Physics 17 | February 2022 18 | 19 | This generates a targeted SVD coil for the human brain. An already optimized solution for the stream function is 20 | loaded. 21 | 22 | For the background of this project refer to: Design of a shim coil array matched to the human brain anatomy 23 | Feng Jia, Hatem Elshatlawy, Ali Aghaeifar, Ying-Hua Chu, Yi-Cheng Hsu, Sebastian Littin, Stefan Kroboth, Huijun Yu, 24 | Philipp Amrein, Xiang Gao, Wenchao Yang, Pierre LeVan, Klaus Scheffler, Maxim Zaitsev 25 | 26 | """ 27 | 28 | if __name__ == '__main__': 29 | # Set up logging 30 | log = logging.getLogger(__name__) 31 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 32 | # logging.basicConfig(level=logging.INFO) 33 | 34 | arg_dict = { 35 | 'field_shape_function': 'none', # definition of the target field 36 | 'coil_mesh_file': 'none', 37 | 'use_only_target_mesh_verts': False, 38 | 'sf_source_file': 'source_data_SVD_coil.npy', 39 | # the number of potential steps that determines the later number of windings (Stream function discretization) 40 | 'levels': 30, 41 | 'min_loop_significance': 5, 42 | 'pot_offset_factor': 0.25, # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 43 | 'surface_is_cylinder_flag': True, 44 | 'interconnection_cut_width': 0.01, # the width for the interconnections are interconnected; in meter 45 | 'normal_shift_length': 0.01, # the length for which overlapping return paths will be shifted along the surface normals; in meter 46 | 'level_set_method': 'primary', # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 47 | 'skip_postprocessing': False, 48 | 'skip_inductance_calculation': False, 49 | 50 | 'project_name': 'Preoptimzed_SVD_Coil', 51 | 'persistence_dir': 'debug', 52 | 'output_directory': 'images', # [Current directory] 53 | 'debug': DEBUG_BASIC, 54 | } 55 | 56 | result = pyCoilGen(log, arg_dict) 57 | -------------------------------------------------------------------------------- /examples/s2_shim_coil_with_surface_openings.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | # Logging 5 | import logging 6 | 7 | 8 | # Local imports 9 | from pyCoilGen.pyCoilGen_release import pyCoilGen 10 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 11 | 12 | 13 | """ 14 | Author: Philipp Amrein, University Freiburg, Medical Center, Radiology, 15 | Medical Physics 16 | February 2022 17 | 18 | This scripts generates a "S2" shimming coil on a cylindrical support with 19 | four rectangular openings 20 | """ 21 | 22 | if __name__ == '__main__': 23 | # Set up logging 24 | log = logging.getLogger(__name__) 25 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 26 | # logging.basicConfig(level=logging.INFO) 27 | 28 | arg_dict = { 29 | 'field_shape_function': '2*x*y', # definition of the target field 30 | 'coil_mesh_file': 'cylinder_radius500mm_length1500mm_regular_holes.stl', 31 | 'target_mesh_file': 'none', 32 | 'secondary_target_mesh_file': 'none', 33 | 'secondary_target_weight': 0.5, 34 | 'target_region_radius': 0.15, # in meter 35 | 'use_only_target_mesh_verts': False, 36 | 'sf_source_file': 'none', 37 | # the number of potential steps that determines the later number of windings (Stream function discretization) 38 | 'levels': 14, 39 | 'pot_offset_factor': 0.25, # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 40 | 'surface_is_cylinder_flag': True, 41 | 'interconnection_cut_width': 0.05, # the width for the interconnections are interconnected; in meter 42 | 'normal_shift_length': 0.01, # the length for which overlapping return paths will be shifted along the surface normals; in meter 43 | 'iteration_num_mesh_refinement': 0, # the number of refinements for the mesh; 44 | 'set_roi_into_mesh_center': True, 45 | 'skip_normal_shift': False, 46 | 'force_cut_selection': ['high', 'high', 'high', 'high', 'low', 'low', 'low', 'low'], 47 | 'level_set_method': 'primary', # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 48 | 'skip_postprocessing': False, 49 | 'skip_inductance_calculation': False, 50 | 'conductor_thickness': 0.01, 51 | 'tikhonov_reg_factor': 10, # Tikhonov regularization factor for the SF optimization 52 | 53 | 54 | 'output_directory': 'images', # [Current directory] 55 | 'project_name': 's2_shim_coil', 56 | 'persistence_dir': 'debug', 57 | 'debug': DEBUG_BASIC, 58 | } 59 | 60 | result = pyCoilGen(log, arg_dict) 61 | -------------------------------------------------------------------------------- /examples/shielded_ygradient_coil.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | # Logging 5 | import logging 6 | 7 | # Local imports 8 | from pyCoilGen.pyCoilGen_release import pyCoilGen 9 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 10 | 11 | 12 | """ 13 | Author: Philipp Amrein, University Freiburg, Medical Center, Radiology, 14 | Medical Physics 15 | February 2022 16 | 17 | This scripts generates a shielded y gradient coil 18 | """ 19 | 20 | if __name__ == '__main__': 21 | # Set up logging 22 | log = logging.getLogger(__name__) 23 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 24 | # logging.basicConfig(level=logging.INFO) 25 | 26 | arg_dict = { 27 | 'field_shape_function': 'y', # definition of the target field 28 | 'coil_mesh_file': 'Double_coaxial_open_cylinder_r1_400mm_r2_600_length_1500mm.stl', 29 | 'target_mesh_file': 'none', 30 | 'target_region_resolution': 10, # MATLAB 10 is the default 31 | 'secondary_target_mesh_file': 'Open_cylinder_r750mm_length_1500mm.stl', 32 | 'secondary_target_weight': 0.5, 33 | 'target_region_radius': 0.15, # in meter 34 | 'use_only_target_mesh_verts': False, 35 | 'sf_source_file': 'none', 36 | # the number of potential steps that determines the later number of windings (Stream function discretization) 37 | 'levels': 26, 38 | 'pot_offset_factor': 0.25, # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 39 | 'surface_is_cylinder_flag': True, 40 | 'interconnection_cut_width': 0.05, # the width for the interconnections are interconnected; in meter 41 | 'normal_shift_length': 0.01, # the length for which overlapping return paths will be shifted along the surface normals; in meter 42 | 'iteration_num_mesh_refinement': 1, # the number of refinements for the mesh (Was: 1) 43 | 'set_roi_into_mesh_center': True, 44 | 'force_cut_selection': ['high'], 45 | # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 46 | 'level_set_method': 'primary', 47 | 'skip_postprocessing': False, 48 | 'skip_inductance_calculation': False, 49 | 'tikhonov_reg_factor': 10, # Tikhonov regularization factor for the SF optimization 50 | 51 | 'output_directory': 'images', # [Current directory] 52 | 'project_name': 'shielded_ygradient_coil', 53 | 'persistence_dir': 'debug', 54 | 'debug': DEBUG_BASIC, 55 | } 56 | 57 | result = pyCoilGen(log, arg_dict) 58 | -------------------------------------------------------------------------------- /examples/ygradient_coil.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | import numpy as np 4 | 5 | # Logging 6 | import logging 7 | 8 | 9 | # Local imports 10 | from pyCoilGen.pyCoilGen_release import pyCoilGen 11 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 12 | 13 | 14 | if __name__ == '__main__': 15 | # Set up logging 16 | log = logging.getLogger(__name__) 17 | logging.basicConfig(level=logging.DEBUG) 18 | # logging.basicConfig(level=logging.INFO) 19 | 20 | arg_dict = { 21 | 'field_shape_function': 'y', # % definition of the target field ['x'] 22 | 'coil_mesh_file': 'cylinder_radius500mm_length1500mm.stl', 23 | 'secondary_target_weight': 0.5, # [1.0] 24 | 'target_region_resolution': 10, # MATLAB 10 is the default 25 | 'levels': 20, # The number of potential steps, determines the number of windings [10] 26 | # a potential offset value for the minimal and maximal contour potential [0.5] 27 | 'pot_offset_factor': 0.25, 28 | 'interconnection_cut_width': 0.1, # Width cut used when cutting and joining wire paths; in metres [0.01] 29 | # Displacement that overlapping return paths will be shifted along the surface normals; in meter [0.001] 30 | 'normal_shift_length': 0.025, 31 | 'iteration_num_mesh_refinement': 1, # % the number of refinements for the mesh; [0] 32 | 'set_roi_into_mesh_center': True, # [False] 33 | 'force_cut_selection': ['high'], # [] 34 | 'make_cylindrical_pcb': True, # [False] 35 | 'conductor_cross_section_width': 0.015, # [0.002] 36 | 'cross_sectional_points': np.array([np.sin(np.linspace(0, 2 * np.pi, 10)), 37 | np.cos(np.linspace(0, 2 * np.pi, 10))]) * 0.01, 38 | 'tikhonov_reg_factor': 100, # Tikhonov regularization factor for the SF optimization [1] 39 | 40 | 'output_directory': 'images', # [Current directory] 41 | 'project_name': 'ygradient_coil', # ['CoilGen'] 42 | 'persistence_dir': 'debug', # [debug] 43 | # 'debug': DEBUG_VERBOSE, 44 | 'debug': DEBUG_BASIC, # [0 = NONE] 45 | } 46 | 47 | result = pyCoilGen(log, arg_dict) 48 | -------------------------------------------------------------------------------- /pyCoilGen/__init__.py: -------------------------------------------------------------------------------- 1 | """Magnetic Field Coil Generator for Python.""" 2 | # Semantic Versioning according to https://semver.org/spec/v2.0.0.html 3 | __version__ = "0.2.3" # Fix level, due to documentation for issue #75 4 | -------------------------------------------------------------------------------- /pyCoilGen/__main__.py: -------------------------------------------------------------------------------- 1 | """Magnetic Field Coil Generator for Python.""" 2 | 3 | import logging 4 | 5 | from pyCoilGen.pyCoilGen_release import pyCoilGen 6 | 7 | 8 | def main(): 9 | # Set up logging 10 | log = logging.getLogger(__name__) 11 | logging.basicConfig(level=logging.INFO) 12 | # Fetch parameters from the command-line 13 | pyCoilGen(log) 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /pyCoilGen/export_factory/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize the export_factory package. 2 | 3 | This module provides functions to dynamically load plugins for exporting pyCoilGen artifacts. 4 | """ 5 | 6 | import os 7 | import importlib 8 | 9 | __exporter_plugins__ = [] 10 | 11 | 12 | def load_exporter_plugins(): 13 | """Load all available export plugins. 14 | 15 | This function dynamically discovers and imports all Python files in the 16 | export_factory directory (excluding this file), treating them as plugins. 17 | It returns a list of imported modules. 18 | 19 | Every plugin must be a module that exposes the following functions: 20 | 21 | - get_name()-> str : Return the name of the mesh builder instruction. 22 | - get_parameters()->list : Return a list of tuples of the parameter names and default values. 23 | - register_args(parser) : Called to register any required parameters with ArgParse. 24 | 25 | In addition, it must also provide an export function that matches the value returned by `get_name()`, e.g.: 26 | - export_stl_mesh(solution) 27 | 28 | Returns: 29 | list: A list of imported plugin modules. 30 | 31 | """ 32 | if len(__exporter_plugins__) == 0: 33 | # Load all .py files in the export_factory directory 34 | for file_name in os.listdir(os.path.dirname(__file__)): 35 | if file_name.endswith(".py") and file_name != "__init__.py": 36 | module_name = f"pyCoilGen.export_factory.{file_name[:-3]}" 37 | module = importlib.import_module(module_name) 38 | __exporter_plugins__.append(module) 39 | 40 | return __exporter_plugins__ 41 | -------------------------------------------------------------------------------- /pyCoilGen/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/pyCoilGen/helpers/__init__.py -------------------------------------------------------------------------------- /pyCoilGen/helpers/common.py: -------------------------------------------------------------------------------- 1 | from numpy import sum, ndarray, zeros 2 | from os import path 3 | 4 | # Logging 5 | import logging 6 | 7 | 8 | from pyCoilGen.sub_functions.constants import get_level, DEBUG_NONE 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def nearest_approaches(point: ndarray, starts: ndarray, ends: ndarray): 14 | """ 15 | Calculate the nearest approach of a point to arrays of line segments. 16 | 17 | NOTE: Uses MATLAB shape conventions 18 | 19 | Args: 20 | point (ndarray): The point of interest (3D coordinates) (1,3). 21 | starts (ndarray): The line segment starting positions (m,3) 22 | ends (ndarray): The line segment ending positions (m,3) 23 | 24 | Returns: 25 | distances, diffs (ndarray, ndarray): The nearest approach distances and the diffs array for re-use. 26 | """ 27 | diffs = ends - starts 28 | vec_targets2 = point - starts 29 | t1 = sum(vec_targets2 * diffs, axis=0) / sum(diffs * diffs, axis=0) 30 | return t1, diffs 31 | 32 | 33 | def blkdiag(arr1: ndarray, arr2: ndarray) -> ndarray: 34 | """ 35 | Compute the block diagonal matrix created by aligning the input matrices along the diagonal. 36 | 37 | Args: 38 | arr1 (ndarray): The first input matrix. 39 | arr2 (ndarray): The second input matrix. 40 | 41 | Returns: 42 | ndarray: The block diagonal matrix created by aligning arr1 and arr2 along the diagonal. 43 | """ 44 | # Get the dimensions of the arrays 45 | rows1, cols1 = arr1.shape 46 | rows2, cols2 = arr2.shape 47 | 48 | # Create a larger zero-filled array with the final desired size 49 | result = zeros((rows1 + rows2, cols1 + cols2)) 50 | 51 | # Place the smaller array within the larger one 52 | result[:rows1, :cols1] = arr1 53 | result[rows1:, cols1:] = arr2 54 | 55 | return result 56 | 57 | 58 | # A list of possible paths to try: 'data' in both the local and site-packages installed directories. 59 | __directory_list = [ 60 | path.join('data', 'pyCoilGenData'), 61 | ] 62 | 63 | 64 | def __add_pyCoilGenData(to_list: list): 65 | try: 66 | from pyCoilGenData import data_directory 67 | data_directory_str = data_directory() 68 | to_list.append(data_directory_str) 69 | log.debug("Adding '%s' to data search path", data_directory_str) 70 | except ImportError: 71 | log.debug("Package 'pyCoilGenData' is not installed. Unable to retrieve the data directory. Install with 'pip install pycoilgen_data'") 72 | 73 | 74 | def find_file(file_directory: str, file_name: str) -> str: 75 | """ 76 | Iterates through candidate paths to find a file on the file system. 77 | 78 | Args: 79 | file_directory (str): The default directory to search in. 80 | file_name (str): The filename to load. 81 | 82 | Returns: 83 | new_file_name (str): The actual file name, if it has been found. 84 | 85 | Raises: 86 | FileNotFoundError: If the file can not be found anywhere. 87 | """ 88 | path_list = __directory_list.copy() 89 | __add_pyCoilGenData(path_list) 90 | dir_path = path.join(file_directory, file_name) 91 | if path.exists(dir_path): 92 | log.debug("Found '%s'", dir_path) 93 | return dir_path 94 | for new_path in path_list: 95 | new_file_name = path.join(new_path, dir_path) 96 | if path.exists(new_file_name): 97 | log.debug("Found '%s'", new_file_name) 98 | return new_file_name 99 | raise FileNotFoundError(f"Unable to find {dir_path} in local path or {__directory_list}") 100 | 101 | 102 | def title_to_filename(title_str: str): 103 | """Convert a title string into a valid filename string.""" 104 | result = title_str 105 | for char in "\n.:\\/ ": 106 | result = result.replace(char, '_') 107 | return result 108 | 109 | 110 | def int_or_float(value): 111 | """Return a value as integer if possible, else float.""" 112 | try: 113 | return int(value) 114 | except ValueError: 115 | return float(value) 116 | -------------------------------------------------------------------------------- /pyCoilGen/helpers/convert_matlabdata_to_numpy.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/874461/read-mat-files-in-python 2 | # Note: The CoilGen matlab files have the following header: 3 | # MATLAB 5.0 MAT-file, Platform: PCWIN64, Created on: Fri Oct 28 15:47:12 2022 4 | 5 | import scipy.io 6 | import numpy as np 7 | 8 | # Logging 9 | import logging 10 | 11 | 12 | def load_matlab(filename): 13 | mat = scipy.io.loadmat(filename+'.mat') 14 | return mat 15 | 16 | 17 | def save_numpy(filename, data): 18 | result = np.save(filename+'.npy', data) 19 | return result 20 | 21 | 22 | if __name__ == "__main__": 23 | # Set up logging 24 | log = logging.getLogger(__name__) 25 | logging.basicConfig(level=logging.DEBUG) 26 | # logging.basicConfig(level=logging.INFO) 27 | 28 | # mat = load_matlab('../CoilGen/target_fields/intraoral_dental_target_field') 29 | # log.debug(" Loaded: %s", mat) 30 | # result = save_numpy('target_fields/intraoral_dental_target_field', mat) 31 | 32 | mat = load_matlab('debug/result_y_gradient') 33 | log.debug(" Loaded: %s", mat) 34 | result = save_numpy('debug/result_y_gradient', mat) 35 | -------------------------------------------------------------------------------- /pyCoilGen/helpers/persistence.py: -------------------------------------------------------------------------------- 1 | from numpy import save as np_save, load as np_load, asarray, concatenate 2 | from os import path, makedirs 3 | 4 | # Logging 5 | import logging 6 | 7 | from pyCoilGen.sub_functions.constants import get_level, DEBUG_NONE 8 | from pyCoilGen.sub_functions.data_structures import CoilSolution, DataStructure 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def save(output_dir: str, project_name: str, tag: str, solution) -> str: 14 | """ 15 | Save the solution to the output directory, with a name based on the project_name and tag. 16 | 17 | Creates the output_dir if it does not already exist. 18 | 19 | Args: 20 | output_dir (str): The default directory to write to. 21 | project_name (str): The project name. 22 | tag (str): A tag to distinguish this save from any others. 23 | solution (CoilSolution): The solution to save. 24 | 25 | Returns: 26 | filename (str): The actual filename that the solution has been saved to. 27 | """ 28 | # Create the output_dir if it does not exist 29 | makedirs(output_dir, exist_ok=True) 30 | filename = f'{path.join(output_dir,project_name)}_{tag}.npy' 31 | if get_level() > DEBUG_NONE: 32 | log.debug("Saving solution to '%s'", filename) 33 | np_save(filename, asarray([solution], dtype=object)) 34 | 35 | return filename 36 | 37 | 38 | def load(output_dir: str, project_name: str, tag: str) -> CoilSolution: 39 | """ 40 | Load the solution from the output directory, with a name based on the project_name and tag. 41 | 42 | Args: 43 | output_dir (str): The default directory to write to. 44 | project_name (str): The project name. 45 | tag (str): A tag to distinguish this save from any others. 46 | 47 | Returns: 48 | solution (CoilSolution): The coil solution. 49 | """ 50 | # Create the output_dir if it does not exist 51 | filename = f'{path.join(output_dir,project_name)}_{tag}.npy' 52 | if get_level() > DEBUG_NONE: 53 | log.debug("Loading solution from '%s'", filename) 54 | [solution] = np_load(filename, allow_pickle=True) 55 | 56 | return solution 57 | 58 | 59 | def save_preoptimised_data(solution: CoilSolution, default_dir='Pre_Optimized_Solutions') -> str: 60 | """ 61 | Writes out the combined coil mesh, stream function and target field data for re-use. 62 | 63 | Load the data with the `--sf_source_file` parameter. 64 | 65 | Depends on the following properties of the CoilSolution: 66 | - target_field.coords, target_field.b 67 | - coil_parts[n].stream_function 68 | - combined_mesh.vertices, combined_mesh.faces 69 | - input_args.sf_dest_file 70 | 71 | Args: 72 | solution (CoilSolution): The solution data. 73 | default_dir (str, optional): Default directory to search first. Defaults to 'Pre_Optimized_Solutions' 74 | 75 | Returns: 76 | filename (str): The filename written to. 77 | 78 | Raises: 79 | FileNotFoundError: If the file can not be created, e.g. if the directory does not exist. 80 | """ 81 | if solution.input_args.sf_dest_file != 'none': 82 | # Extract the TargetField coords and b-field in Python (m,3) format 83 | target_field = DataStructure(coords=solution.target_field.coords.T, b=solution.target_field.b.T) 84 | 85 | # Create the stream_function from the coil_parts 86 | stream_function = solution.coil_parts[0].stream_function 87 | for i in range(1, len(solution.coil_parts)): 88 | stream_function = concatenate((stream_function, solution.coil_parts[i].stream_function)) 89 | 90 | # Extract the vertices and faces of the combined mesh in (m,3) format 91 | combined_mesh = DataStructure(vertices=solution.combined_mesh.vertices, faces=solution.combined_mesh.faces) 92 | 93 | # Create the carrier data structure 94 | data = DataStructure(coil_mesh=combined_mesh, target_field=target_field, stream_function=stream_function) 95 | 96 | target_file = solution.input_args.sf_dest_file 97 | if '/' in target_file or '\\' in target_file: 98 | filename = f'{target_file}.npy' 99 | else: 100 | makedirs(default_dir, exist_ok=True) 101 | filename = f'{path.join(default_dir, target_file)}.npy' 102 | log.info("Writing pre-optimised data to '%s'", filename) 103 | np_save(filename, [data], allow_pickle=True) 104 | 105 | return filename 106 | -------------------------------------------------------------------------------- /pyCoilGen/helpers/timing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | class Timing: 8 | def __init__(self): 9 | self.start_times = [] 10 | 11 | def start(self): 12 | self.start_times.append(time.time()) 13 | 14 | def stop(self): 15 | if self.start_times: 16 | elapsed_time = time.time() - self.start_times.pop() 17 | if len(self.start_times) == 0: 18 | log.info(f"Total elapsed time: {elapsed_time:.6f} seconds") 19 | else: 20 | log.debug(f"Elapsed time: {elapsed_time:.6f} seconds") 21 | else: 22 | log.warning("No active timer to stop.") 23 | 24 | 25 | # Usage example 26 | if __name__ == "__main__": 27 | logging.basicConfig(level=logging.DEBUG) 28 | 29 | timer = Timing() 30 | 31 | timer.start() # Start total timer 32 | timer.start() # Start task 1 timer 33 | time.sleep(1) # Simulate task 1 34 | timer.stop() # Measure time of task 1 and write time to log.debug 35 | 36 | timer.start() # Start task 2 timer 37 | time.sleep(2) # Simulate task 2 38 | timer.stop() # Measure time of task 2 and write time to log.debug 39 | 40 | timer.stop() # Measure and write total time to log.debug 41 | -------------------------------------------------------------------------------- /pyCoilGen/helpers/triangulation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pyCoilGen.helpers.pyshull import PySHull 3 | 4 | # Logging 5 | import logging 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class Triangulate(): 11 | """ 12 | A wrapper that provides Delaunay functionality, hiding the implementation. 13 | """ 14 | 15 | def __init__(self, vertices: np.ndarray) -> None: 16 | """ 17 | Create an instance of Delaunay triangulation from the provided vertices. 18 | 19 | """ 20 | self._vertices = vertices.copy() 21 | self._triangles = PySHull(vertices) 22 | 23 | def get_triangles(self): 24 | return self._triangles 25 | 26 | def get_vertices(self): 27 | return self._vertices 28 | 29 | 30 | if __name__ == "__main__": 31 | # Set up logging 32 | log = logging.getLogger(__name__) 33 | logging.basicConfig(level=logging.DEBUG) 34 | 35 | points = [[0.0, 0.006427876096865392, 0.00984807753012208, 0.008660254037844387, 0.0034202014332566887, -0.0034202014332566865, -0.008660254037844388, -0.009848077530122082, -0.006427876096865396, -2.4492935982947064e-18], 36 | [0.01, 0.007660444431189781, 0.0017364817766693042, -0.0049999999999999975, -0.009396926207859084, -0.009396926207859084, -0.004999999999999997, 0.0017364817766692998, 0.007660444431189778, 0.01]] 37 | 38 | vertices = np.array(points).T 39 | d = Triangulate(vertices) 40 | log.debug(" edges: %s", d.get_triangles()) 41 | for edge in d.get_triangles(): 42 | log.debug(" edges: %s", edge) 43 | -------------------------------------------------------------------------------- /pyCoilGen/mesh_factory/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize the mesh_factory package. 2 | 3 | This module provides functions to dynamically load plugins for mesh creation. 4 | """ 5 | 6 | import os 7 | import importlib 8 | 9 | def load_mesh_factory_plugins(): 10 | """Load all available mesh creation plugins. 11 | 12 | This function dynamically discovers and imports all Python files in the 13 | mesh_factory directory (excluding this file), treating them as plugins. 14 | It returns a list of imported modules. 15 | 16 | Every plugin must be a module that exposes the following functions: 17 | 18 | - get_name()-> str : Return the name of the mesh builder instruction. 19 | - get_parameters()->list : Return a list of tuples of the parameter names and default values. 20 | - register_args(parser) : Called to register any required parameters with ArgParse. 21 | 22 | In addition, it must also provide a creator function that matches the value returned by `get_name()`, e.g.: 23 | - create_planar_mesh(input_args: argparse.Namespace) : Mesh or DataStructure(vertices, faces, normal) 24 | 25 | Returns: 26 | list: A list of imported plugin modules. 27 | 28 | """ 29 | plugins = [] 30 | 31 | # Load all .py files in the mesh_factory directory 32 | for file_name in os.listdir(os.path.dirname(__file__)): 33 | if file_name.endswith(".py") and file_name != "__init__.py": 34 | module_name = f"pyCoilGen.mesh_factory.{file_name[:-3]}" 35 | module = importlib.import_module(module_name) 36 | plugins.append(module) 37 | 38 | return plugins 39 | -------------------------------------------------------------------------------- /pyCoilGen/mesh_factory/build_cut_circle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def build_cut_circle(center_point, cut_width): 5 | """ 6 | Build a rectangular cut shape in the form of a circular opening. 7 | 8 | Args: 9 | center_point (ndarray): Array containing the x and y coordinates of the center point. 10 | cut_width (float): Width of the cut. 11 | 12 | Returns: 13 | cut_circle (ndarray): Array containing the x and y coordinates of the circular cut shape. 14 | """ 15 | 16 | circular_resolution = 10 17 | 18 | # Build circular cut shapes 19 | opening_angles = np.linspace(0, 2*np.pi, circular_resolution) 20 | opening_circle = np.column_stack((np.sin(opening_angles), np.cos(opening_angles))) 21 | 22 | # Create a circular opening cut 23 | cut_circle = opening_circle * (cut_width / 2) + np.tile(center_point, (circular_resolution, 1)) 24 | 25 | return cut_circle 26 | -------------------------------------------------------------------------------- /pyCoilGen/mesh_factory/build_cut_rectangle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def build_cut_rectangle(loop, center_point, segment_ind, cut_width, cut_height_ratio): 5 | """ 6 | Build a rectangular cut shape. 7 | 8 | Args: 9 | loop (ndarray): Array of loop coordinates. 10 | center_point: Center point of the rectangle 11 | segment_ind: Index of the segment within the loop 12 | cut_width: Width of the cut rectangle 13 | cut_height_ratio: Height ratio of the cut rectangle 14 | 15 | Returns: 16 | cut_rectangle: Array containing the coordinates of the rectangular cut shape 17 | """ 18 | 19 | cut_points_left = loop[:, segment_ind + 1] 20 | cut_points_right = loop[:, segment_ind] 21 | 22 | longitudinal_vector = cut_points_left - cut_points_right 23 | longitudinal_vector = longitudinal_vector / np.linalg.norm(longitudinal_vector) 24 | orthogonal_vector = np.array([longitudinal_vector[1], -longitudinal_vector[0]]) 25 | orthogonal_vector = orthogonal_vector / np.linalg.norm(orthogonal_vector) 26 | 27 | # Scale the vectors to the targeted width and height 28 | orthogonal_vector = orthogonal_vector * (cut_width * cut_height_ratio) 29 | longitudinal_vector = longitudinal_vector * cut_width 30 | 31 | # Create the rectangular points 32 | cut_rectangle = np.array([center_point + longitudinal_vector/2 + orthogonal_vector/2, 33 | center_point + longitudinal_vector/2 - orthogonal_vector/2, 34 | center_point - longitudinal_vector/2 - orthogonal_vector/2, 35 | center_point - longitudinal_vector/2 + orthogonal_vector/2, 36 | center_point + longitudinal_vector/2 + orthogonal_vector/2]) 37 | 38 | return cut_rectangle 39 | -------------------------------------------------------------------------------- /pyCoilGen/mesh_factory/create_stl_mesh.py: -------------------------------------------------------------------------------- 1 | """Module for creating STL meshes. 2 | 3 | This module provides functions for creating STL meshes based on input arguments. 4 | """ 5 | 6 | # System 7 | import numpy as np 8 | 9 | # Logging 10 | import logging 11 | 12 | # Local imports 13 | from pyCoilGen.sub_functions.data_structures import Mesh 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | def create_stl_mesh(input_args): 19 | """Create an STL mesh based on input arguments. 20 | 21 | This function loads an STL file specified in the input arguments, reads 22 | the coil mesh surface, and returns a Mesh object. It assumes a representative 23 | normal of [0,0,1] if not specified. 24 | 25 | Args: 26 | input_args (argparse.Namespace): Input arguments containing file paths and settings. 27 | 28 | Returns: 29 | Mesh: A Mesh object representing the coil. 30 | 31 | Example: 32 | >>> input_args = argparse.Namespace( 33 | ... geometry_source_path='/path/to/geometry/', 34 | ... coil_mesh_file='coil_mesh.stl' 35 | ... ) 36 | >>> coil_mesh = create_stl_mesh(input_args) 37 | """ 38 | # Support both stl_mesh_filename and coil_mesh_file 39 | mesh_file = input_args.stl_mesh_filename 40 | if mesh_file == 'none': 41 | mesh_file = input_args.coil_mesh_file 42 | if mesh_file == 'none': 43 | return None 44 | log.debug("Loading STL from %s", mesh_file) 45 | # Load the stl file; read the coil mesh surface 46 | coil_mesh = Mesh.load_from_file(input_args.geometry_source_path, mesh_file) 47 | log.info(" Loaded mesh from %s/%s.", input_args.geometry_source_path, mesh_file) 48 | coil_mesh.normal_rep = np.array([0.0, 0.0, 1.0]) 49 | return coil_mesh 50 | 51 | def get_name(): 52 | """ 53 | Template function to retrieve the plugin builder name. 54 | 55 | Returns: 56 | builder_name (str): The builder name, given to 'coil_mesh'. 57 | """ 58 | return 'create stl mesh' 59 | 60 | def get_parameters()->list: 61 | """ 62 | Template function to retrieve the supported parameters and default values as strings. 63 | 64 | Returns: 65 | list of tuples of parameter name and default value: The additional parameters provided by this builder 66 | """ 67 | return [('stl_mesh_filename', 'none')] 68 | 69 | def register_args(parser): 70 | """Register arguments specific to STL mesh creation. 71 | 72 | This function adds command-line arguments to the provided parser that are 73 | specific to STL mesh creation. 74 | 75 | Args: 76 | parser (argparse.ArgumentParser): The parser to which arguments will be added. 77 | """ 78 | # Add arguments specific to STL mesh creation 79 | parser.add_argument('--stl_mesh_filename', type=str, default='none', 80 | help="File of the mesh. Supports STL, GLB, PLY, 3MF, XAML, etc.") 81 | # Add legacy parameter 82 | # Version 0.x.x uses 'coil_mesh_file' to specify the primary mesh or a mesh builder. 83 | parser.add_argument('--coil_mesh_file', type=str, default='none', 84 | help="File of the coil mesh or a mesh builder instruction") 85 | -------------------------------------------------------------------------------- /pyCoilGen/plotting/__init__.py: -------------------------------------------------------------------------------- 1 | from .plot_error_different_solutions import plot_error_different_solutions 2 | from .plot_various_error_metrics import plot_various_error_metrics 3 | from .plot_2D_contours_with_sf import plot_2D_contours_with_sf 4 | from .plot_3D_contours_with_sf import plot_3D_contours_with_sf 5 | from .plot_vector_field import plot_vector_field_xy, plot_vector_field_yz, plot_vector_field_xz, plot_vector_field 6 | -------------------------------------------------------------------------------- /pyCoilGen/plotting/plot_3D_contours_with_sf.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection # Import Poly3DCollection 4 | from typing import List 5 | 6 | # Logging 7 | import logging 8 | 9 | # Local imports 10 | from pyCoilGen.sub_functions.data_structures import CoilSolution 11 | from pyCoilGen.helpers.common import title_to_filename 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | _default_colours = ['blue', 'green', 'red', 'purple', 'orange', 'brown', 'pink', 'gray', 'cyan', 'magenta'] 16 | 17 | 18 | def plot_3D_contours_with_sf(coil_layout: List[CoilSolution], single_ind_to_plot: int, plot_title: str, group_colours=_default_colours, save_dir=None, dpi=100): 19 | """ 20 | Plot the stream function interpolated on a triangular mesh. 21 | 22 | Args: 23 | coil_layout (list[CoilSolution]): List of CoilSolution objects. 24 | single_ind_to_plot (int): Index of the solution to plot. 25 | plot_title (str): Title of the plot. 26 | group_colours (list of colour strings, optional): A list of colours to use when plotting group contours. 27 | save_dir (str, optional): If specified, saves the plot to the directory, else plots it. 28 | dpi (int, optional): The dots-per-inch (DPI) to use when saving the figure. 29 | 30 | Returns: 31 | None 32 | """ 33 | # Plot the stream function interpolated on triangular mesh 34 | fig = plt.figure(figsize=(5, 6)) 35 | ax = fig.add_subplot(111, projection='3d') 36 | plt.title(plot_title + '\nStream function by optimization and target Bz') 37 | 38 | # Set axis limits 39 | combined_mesh = coil_layout[single_ind_to_plot].combined_mesh.vertices 40 | min_values = np.min(combined_mesh, axis=0) 41 | max_values = np.max(combined_mesh, axis=0) 42 | 43 | ax.set_xlim(min_values[0], max_values[0]) 44 | ax.set_ylim(min_values[1], max_values[1]) 45 | ax.set_zlim(min_values[2], max_values[2]) 46 | 47 | for part_ind in range(len(coil_layout[single_ind_to_plot].coil_parts)): 48 | coil_part = coil_layout[single_ind_to_plot].coil_parts[part_ind] 49 | normed_sf = coil_part.stream_function - np.min(coil_part.stream_function) 50 | normed_sf /= np.max(normed_sf) 51 | 52 | # Create a list of vertices and faces for Poly3DCollection 53 | vertices = coil_part.coil_mesh.v 54 | faces = coil_part.coil_mesh.f # Get all faces 55 | face_vertices = vertices[faces] 56 | 57 | # Create a custom colormap using 'viridis' 58 | sf_face_colours = [np.mean(normed_sf[face]) for face in faces] 59 | face_colors = plt.cm.viridis(sf_face_colours) 60 | 61 | # Create Poly3DCollection object and add it to the plot 62 | poly = Poly3DCollection(face_vertices, facecolors=face_colors, alpha=0.6) 63 | ax.add_collection3d(poly) 64 | 65 | if coil_part.groups is not None: # Attribute is always present, but not always initialised 66 | group_ind = 0 67 | for group in coil_part.groups: 68 | group_color = group_colours[group_ind % len(group_colours)] 69 | for contour in group.loops: 70 | ax.plot(contour.v[0, :], contour.v[1, :], contour.v[2, :], color=group_color, linewidth=2) 71 | group_ind += 1 72 | 73 | # Customize the plot further if needed 74 | plt.xlabel('x[m]') 75 | plt.ylabel('y[m]') 76 | ax.set_zlabel('z[m]') 77 | plt.gca().set_box_aspect([1, 1, 1]) # Set the aspect ratio to be equal 78 | 79 | # Save the figure if specified 80 | if save_dir is not None: 81 | plt.savefig(f'{save_dir}/plot_{title_to_filename(plot_title)}.png', dpi=dpi) 82 | else: 83 | plt.show() 84 | -------------------------------------------------------------------------------- /pyCoilGen/plotting/plot_contours_with_field.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import numpy as np 4 | from typing import List 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | from pyCoilGen.sub_functions.data_structures import CoilSolution, SolutionErrors 9 | from pyCoilGen.helpers.common import title_to_filename 10 | 11 | # Configure logging 12 | log = logging.getLogger(__name__) 13 | 14 | def plot_contours_with_field(coil_layout: List[CoilSolution], single_ind_to_plot: int, plot_title: str, save_dir=None, dpi=100): 15 | """ 16 | Plot the stream function contours, the calculated field, and the sweep path in a single 3D plot. 17 | 18 | Args: 19 | coil_layout (List[CoilSolution]): List of CoilSolution objects. 20 | single_ind_to_plot (int): Index of the solution to plot. 21 | plot_title (str): Title of the plot. 22 | save_dir (str, optional): Directory to save the plot. If None, the plot is only displayed. 23 | dpi (int, optional): Resolution of the saved plot. 24 | 25 | Returns: 26 | None 27 | """ 28 | dot_size = 200 29 | 30 | # Extract relevant data from the CoilSolution 31 | coil_solution: CoilSolution = coil_layout[single_ind_to_plot] 32 | errors: SolutionErrors = coil_solution.solution_errors 33 | 34 | layout_c = errors.combined_field_layout[2] # Calculated field 35 | pos_data = coil_solution.target_field.coords # Coordinates (3, N) 36 | 37 | fig = plt.figure(figsize=(10, 8)) 38 | ax = fig.add_subplot(111, projection='3d') 39 | fig.suptitle(plot_title, fontsize=16) 40 | 41 | # Plot the calculated field as a scatter plot 42 | scatter = ax.scatter(pos_data[0], pos_data[1], pos_data[2], 43 | c=layout_c, s=dot_size, cmap='viridis', label='Calculated Field') 44 | 45 | # Add a color bar for the calculated field 46 | cbar = fig.colorbar(scatter, ax=ax, pad=0.1) 47 | cbar.set_label('Field [mT/A]') 48 | 49 | # Plot the contours from the stream function 50 | for part_ind, coil_part in enumerate(coil_solution.coil_parts): 51 | 52 | # Plot the sweep path (wire_path.v) for the current coil part 53 | if coil_part.wire_path is not None and hasattr(coil_part.wire_path, 'v'): 54 | wire_path_v = coil_part.wire_path.v # Extract the sweep path 55 | ax.plot(wire_path_v[0, :], wire_path_v[1, :], wire_path_v[2, :],color='blue') 56 | 57 | # Customize plot appearance 58 | ax.set_xlabel('X [m]') 59 | ax.set_ylabel('Y [m]') 60 | ax.set_zlabel('Z [m]') 61 | ax.legend(loc='upper right') 62 | 63 | # Set equal aspect ratio 64 | combined_mesh = coil_solution.combined_mesh.vertices 65 | min_values = np.min(combined_mesh, axis=0) 66 | max_values = np.max(combined_mesh, axis=0) 67 | ax.set_xlim(min_values[0], max_values[0]) 68 | ax.set_ylim(min_values[1], max_values[1]) 69 | ax.set_zlim(min_values[2], max_values[2]) 70 | 71 | plt.gca().set_box_aspect(max_values-min_values) # Set the aspect ratio to be equal 72 | plt.tight_layout() 73 | 74 | # Save the plot if save_dir is provided 75 | if save_dir is not None: 76 | plt.savefig(f'{save_dir}/plot_contours_with_field_{title_to_filename(plot_title)}.png', dpi=dpi) 77 | log.info(f'Plot saved to {save_dir}/plot_contours_with_field_{title_to_filename(plot_title)}.png') 78 | 79 | # Display the plot 80 | plt.show() 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /pyCoilGen/plotting/plot_error_different_solutions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | 4 | import matplotlib.pyplot as plt 5 | 6 | from pyCoilGen.sub_functions.data_structures import CoilSolution 7 | from pyCoilGen.helpers.common import title_to_filename 8 | 9 | 10 | def plot_error_different_solutions(coil_solutions: List[CoilSolution], solutions_to_plot: List[int], plot_title: str, 11 | x_ticks: dict = None, save_dir=None, dpi=100): 12 | """ 13 | Plots error metrics for different coil solutions. 14 | 15 | If requested, with save_figure, images are saved to an 'images' subdirectory. 16 | 17 | Args: 18 | coil_solutions (List[CoilSolution]): List of CoilSolution objects. 19 | solutions_to_plot (List[int]): List of indices indicating which solutions to plot. 20 | plot_title (str): Title of the plot. 21 | x_ticks (dict): A single key-values dictionary to specify custom x-ticks. The key is used to label the x-axis. 22 | save_dir (str, optional): If specified, saves the plot to the directory, else plots it. 23 | dpi (int, optional): The dots-per-inch (DPI) to use when saving the figure. 24 | 25 | Returns: 26 | None 27 | """ 28 | max_rel_error_layout_vs_target = np.array( 29 | [coil_solutions[x].solution_errors.field_error_vals.max_rel_error_layout_vs_target for x in solutions_to_plot]) 30 | mean_rel_error_layout_vs_target = np.array( 31 | [coil_solutions[x].solution_errors.field_error_vals.mean_rel_error_layout_vs_target for x in solutions_to_plot]) 32 | max_rel_error_loops_vs_target = np.array( 33 | [coil_solutions[x].solution_errors.field_error_vals.max_rel_error_unconnected_contours_vs_target for x in solutions_to_plot]) 34 | mean_rel_error_loops_vs_target = np.array( 35 | [coil_solutions[x].solution_errors.field_error_vals.mean_rel_error_unconnected_contours_vs_target for x in solutions_to_plot]) 36 | max_rel_error_layout_vs_sf = np.array( 37 | [coil_solutions[x].solution_errors.field_error_vals.max_rel_error_layout_vs_stream_function_field for x in solutions_to_plot]) 38 | mean_rel_error_layout_vs_sf = np.array( 39 | [coil_solutions[x].solution_errors.field_error_vals.mean_rel_error_layout_vs_stream_function_field for x in solutions_to_plot]) 40 | max_rel_error_loops_vs_sf = np.array( 41 | [coil_solutions[x].solution_errors.field_error_vals.max_rel_error_unconnected_contours_vs_stream_function_field for x in solutions_to_plot]) 42 | mean_rel_error_loops_vs_sf = np.array( 43 | [coil_solutions[x].solution_errors.field_error_vals.mean_rel_error_unconnected_contours_vs_stream_function_field for x in solutions_to_plot]) 44 | 45 | plt.figure(figsize=(10, 6)) 46 | 47 | p1 = plt.plot(solutions_to_plot, max_rel_error_loops_vs_sf, 'o-b', linewidth=2) 48 | p2 = plt.plot(solutions_to_plot, max_rel_error_layout_vs_sf, 'o-r', linewidth=2) 49 | p3 = plt.plot(solutions_to_plot, mean_rel_error_loops_vs_sf, '*-b', linewidth=2) 50 | p4 = plt.plot(solutions_to_plot, mean_rel_error_layout_vs_sf, '*-r', linewidth=2) 51 | p5 = plt.plot(solutions_to_plot, max_rel_error_loops_vs_target, 'o-y', linewidth=2) 52 | p6 = plt.plot(solutions_to_plot, max_rel_error_layout_vs_target, 'o-m', linewidth=2) 53 | p7 = plt.plot(solutions_to_plot, mean_rel_error_loops_vs_target, '*-', 54 | linewidth=2, color=[0, 0.5, 0], markerfacecolor=[0, 0.5, 0]) 55 | p8 = plt.plot(solutions_to_plot, mean_rel_error_layout_vs_target, '*-c', linewidth=2) 56 | 57 | plt.legend(['max_rel_error_loops_vs_sf', 'max_rel_error_layout_vs_sf', 'mean_rel_error_loops_vs_sf', 'mean_rel_error_layout_vs_sf', 58 | 'max_rel_error_loops_vs_target', 'max_rel_error_layout_vs_target', 'mean_rel_error_loops_vs_target', 'mean_rel_error_layout_vs_target']) 59 | if x_ticks is not None: 60 | x_tick = next(iter(x_ticks.keys())) 61 | x_labels = [str(tick) for tick in x_ticks[x_tick]] 62 | plt.xlabel(x_tick) 63 | plt.xticks(solutions_to_plot, x_labels) 64 | else: 65 | plt.xlabel('solutions_to_plot') 66 | plt.ylabel('Error Values') 67 | plt.title(plot_title) 68 | plt.grid(True) 69 | if save_dir is not None: 70 | plt.savefig(f'{save_dir}/plot_solutions_{title_to_filename(plot_title)}.png', dpi=dpi) 71 | else: 72 | plt.show() 73 | -------------------------------------------------------------------------------- /pyCoilGen/plotting/plot_various_error_metrics.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | from typing import List 4 | 5 | import matplotlib.pyplot as plt 6 | 7 | from pyCoilGen.sub_functions.data_structures import CoilSolution, SolutionErrors, FieldErrors 8 | from pyCoilGen.helpers.common import title_to_filename 9 | 10 | 11 | def plot_various_error_metrics(coil_layouts: List[CoilSolution], single_ind_to_plot: int, plot_title: str, save_dir=None, dpi=100): 12 | """ 13 | Plots various error metrics for a specific coil layout. 14 | 15 | If requested, with save_figure, images are saved to an 'images' subdirectory. 16 | 17 | Args: 18 | coil_layouts (List[CoilSolution]): List of coil solutions. 19 | single_ind_to_plot (int): Index of the coil layout to be plotted. 20 | plot_title (str): Title of the plot. 21 | save_dir (str, optional): If specified, saves the plot to the directory, else plots it. 22 | dpi (int, optional): The dots-per-inch (DPI) to use when saving the figure. 23 | 24 | Returns: 25 | None 26 | """ 27 | dot_size = 200 28 | 29 | coil_solution: CoilSolution = coil_layouts[single_ind_to_plot] 30 | errors: SolutionErrors = coil_solution.solution_errors 31 | 32 | layout_c = errors.combined_field_layout[2] # field_by_layout (3,257) 33 | sf_c = coil_solution.sf_b_field[:, 2] # b_field_opt_sf (257,3) 34 | loops_c = errors.combined_field_loops[2] # field_by_unconnected_loops (3,257) 35 | target_c = coil_solution.target_field.b[2] # target_field.b (3,257) 36 | # loops_c_1A = errors.combined_field_loops_per1Amp[2] # field_loops_per1Amp (3,257) 37 | # layout_c_1A = errors.combined_field_layout_per1Amp[2] # field_layout_per1Amp (3,257) 38 | pos_data = coil_solution.target_field.coords # (3,257) 39 | 40 | fig = plt.figure(figsize=(15, 15)) 41 | fig.suptitle(plot_title, fontsize=16) 42 | 43 | # Create subplots 44 | ax1 = fig.add_subplot(331, projection='3d') 45 | ax2 = fig.add_subplot(332, projection='3d') 46 | ax3 = fig.add_subplot(333, projection='3d') 47 | ax4 = fig.add_subplot(334, projection='3d') 48 | ax5 = fig.add_subplot(335, projection='3d') 49 | ax6 = fig.add_subplot(336, projection='3d') 50 | ax7 = fig.add_subplot(337, projection='3d') 51 | ax8 = fig.add_subplot(338, projection='3d') 52 | ax9 = fig.add_subplot(339, projection='3d') 53 | 54 | plot_data = [ 55 | (ax1, target_c, 'Target Bz, [mT/A]'), 56 | (ax2, sf_c, 'Bz by stream function, [mT/A]'), 57 | (ax3, layout_c, 'Layout Bz, [mT/A]'), 58 | (ax4, loops_c, 'Unconnected Contour Bz, [mT/A]'), 59 | (ax5, abs(sf_c - target_c) / np.max(np.abs(target_c)) * 100, 'Relative SF error, [%]'), 60 | (ax6, abs(layout_c - target_c) / np.max(np.abs(target_c)) * 100, 'Relative error\n layout vs. target, [%]'), 61 | (ax7, abs(layout_c - sf_c) / np.max(np.abs(sf_c)) * 100, 'Relative error\n layout vs. sf field, [%]'), 62 | (ax8, abs(loops_c - target_c) / np.max(np.abs(target_c)) * 63 | 100, 'Relative error\n unconnected contours vs. target, [%]'), 64 | (ax9, abs(loops_c - layout_c) / np.max(np.abs(target_c)) * 100, 65 | 'Field difference between unconnected contours\n and final layout, [%]') 66 | ] 67 | 68 | for ax, plot_color, title in plot_data: 69 | ax.scatter(pos_data[0], pos_data[1], pos_data[2], c=plot_color, s=dot_size, cmap='viridis') 70 | ax.set_title(title) 71 | ax.set_xlabel('x[m]') 72 | ax.set_ylabel('y[m]') 73 | ax.set_zlabel('z[m]') 74 | fig.colorbar(ax.scatter(pos_data[0], pos_data[1], pos_data[2], c=plot_color, 75 | s=dot_size, cmap='viridis'), ax=ax, label='Error %', pad=0.10) 76 | 77 | plt.tight_layout(rect=[0, 0, 1, 0.95]) 78 | if save_dir is not None: 79 | plt.savefig(f'{save_dir}/plot_errors_{title_to_filename(plot_title)}.png', dpi=dpi) 80 | else: 81 | plt.show() 82 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/pyCoilGen/sub_functions/__init__.py -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calc_3d_rotation_matrix_by_vector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_3d_rotation_matrix_by_vector(rot_vec, rot_angle): 5 | """ 6 | Calculate the 3D rotation matrix around a rotation axis given by a vector and an angle. 7 | 8 | Args: 9 | rot_vec (numpy.ndarray): Rotation axis vector. 10 | rot_angle (float): Rotation angle in radians. 11 | 12 | Returns: 13 | numpy.ndarray: 3x3 rotation matrix. 14 | """ 15 | rot_vec = rot_vec / np.linalg.norm(rot_vec) # Normalize rotation vector 16 | 17 | u_x = rot_vec[0] 18 | u_y = rot_vec[1] 19 | u_z = rot_vec[2] 20 | 21 | tmp1 = np.sin(rot_angle) 22 | tmp2 = np.cos(rot_angle) 23 | tmp3 = 1 - np.cos(rot_angle) 24 | 25 | rot_mat_out = np.zeros((3, 3)) 26 | 27 | rot_mat_out[0, 0] = tmp2 + u_x * u_x * tmp3 28 | rot_mat_out[1, 0] = u_x * u_y * tmp3 - u_z * tmp1 29 | rot_mat_out[2, 0] = u_x * u_z * tmp3 + u_y * tmp1 30 | 31 | rot_mat_out[0, 1] = u_y * u_x * tmp3 + u_z * tmp1 32 | rot_mat_out[1, 1] = tmp2 + u_y * u_y * tmp3 33 | rot_mat_out[2, 1] = u_y * u_z * tmp3 - u_x * tmp1 34 | 35 | rot_mat_out[0, 2] = u_z * u_x * tmp3 - u_y * tmp1 36 | rot_mat_out[1, 2] = u_z * u_y * tmp3 + u_x * tmp1 37 | rot_mat_out[2, 2] = tmp2 + u_z * u_z * tmp3 38 | 39 | return rot_mat_out 40 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calc_gradient_along_vector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_gradient_along_vector(field, field_coords, target_endcoding_function): 5 | """ 6 | Calculate the mean gradient in a given direction and angle. 7 | 8 | Args: 9 | field (array-like): Field values. 10 | field_coords (array-like): Field coordinates. 11 | target_endcoding_function (str): Target encoding function for coordinate transformation. 12 | 13 | Returns: 14 | mean_gradient_strength (float): Mean gradient strength. 15 | gradient_out (array): Gradient values. 16 | """ 17 | def my_fun(x, y, z): 18 | # Define the target encoding function 19 | return eval(target_endcoding_function) 20 | 21 | norm_dir_x = my_fun(1, 0, 0) 22 | norm_dir_y = my_fun(0, 1, 0) 23 | norm_dir_z = my_fun(0, 0, 1) 24 | 25 | target_direction = np.array([0, 0, 1]) 26 | gradient_direction = np.array([norm_dir_x, norm_dir_y, norm_dir_z]) 27 | gradient_direction /= np.linalg.norm(gradient_direction) 28 | 29 | if np.linalg.norm(np.cross(gradient_direction, target_direction)) != 0: 30 | rot_vector = np.cross(gradient_direction, target_direction) / \ 31 | np.linalg.norm(np.cross(gradient_direction, target_direction)) 32 | rot_angle = np.arcsin(np.linalg.norm(np.cross(gradient_direction, target_direction)) / 33 | (np.linalg.norm(target_direction) * np.linalg.norm(gradient_direction))) 34 | else: 35 | rot_vector = np.array([1, 0, 0]) 36 | rot_angle = 0 37 | 38 | rot_mat_out = calc_3d_rotation_matrix(rot_vector.reshape(-1, 1), rot_angle) 39 | rotated_field_coords = np.dot(rot_mat_out, (field_coords - np.mean(field_coords, axis=1).reshape(-1, 1))) 40 | gradient_out = field[2, :] / rotated_field_coords[2, :] 41 | gradient_out[np.abs(rotated_field_coords[2, :]) < 1e-6] = np.nan 42 | mean_gradient_strength = np.nanmean(gradient_out) 43 | 44 | return mean_gradient_strength, gradient_out 45 | 46 | 47 | def calc_3d_rotation_matrix(rot_vec, rot_angle): 48 | """ 49 | Calculate the 3D rotation matrix around a rotation axis given by a vector and an angle. 50 | 51 | Args: 52 | rot_vec (array-like): Rotation vector. 53 | rot_angle (float): Rotation angle. 54 | 55 | Returns: 56 | rot_mat_out (array): 3D rotation matrix. 57 | 58 | Raises: 59 | None 60 | """ 61 | rot_vec = rot_vec / np.linalg.norm(rot_vec) # normalize rotation vector 62 | u_x = rot_vec[0] 63 | u_y = rot_vec[1] 64 | u_z = rot_vec[2] 65 | tmp1 = np.sin(rot_angle) 66 | tmp2 = np.cos(rot_angle) 67 | tmp3 = 1 - np.cos(rot_angle) 68 | rot_mat_out = np.zeros((3, 3)) 69 | rot_mat_out[0, 0] = tmp2 + u_x * u_x * tmp3 70 | rot_mat_out[0, 1] = u_x * u_y * tmp3 - u_z * tmp1 71 | rot_mat_out[0, 2] = u_x * u_z * tmp3 + u_y * tmp1 72 | rot_mat_out[1, 0] = u_y * u_x * tmp3 + u_z * tmp1 73 | rot_mat_out[1, 1] = tmp2 + u_y * u_y * tmp3 74 | rot_mat_out[1, 2] = u_y * u_z * tmp3 - u_x * tmp1 75 | rot_mat_out[2, 0] = u_z * u_x * tmp3 - u_y * tmp1 76 | rot_mat_out[2, 1] = u_z * u_y * tmp3 + u_x * tmp1 77 | rot_mat_out[2, 2] = tmp2 + u_z * u_z * tmp3 78 | 79 | return rot_mat_out 80 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calc_local_opening_gab.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_local_opening_gab(loop, point_1, point_2, opening_gab): 5 | """ 6 | Calculate the local opening gap based on the given inputs. 7 | 8 | Args: 9 | loop (object): Loop object. 10 | point_1 (int): Index of the first point. 11 | point_2 (int): Index of the second point. 12 | opening_gab (float): Opening gap value. 13 | 14 | Returns: 15 | local_opening_gab (float): Local opening gap value. 16 | """ 17 | if point_2 is not None: # Two points are specified 18 | uv_distance = np.linalg.norm(loop.uv[:, point_2] - loop.uv[:, point_1]) 19 | v_distance = np.linalg.norm(loop.v[:, point_2] - loop.v[:, point_1]) 20 | local_opening_gab = opening_gab * uv_distance / v_distance 21 | else: # Only one point is specified, find the other to build the direction 22 | min_ind_2 = np.argmin(np.linalg.norm(loop.uv - point_1, axis=1)) 23 | min_ind_1 = min_ind_2 + 2 24 | if min_ind_1 < 0: 25 | min_ind_1 += loop.uv.shape[1] 26 | if min_ind_1 > loop.uv.shape[1]: 27 | min_ind_1 -= loop.uv.shape[1] 28 | uv_distance = np.linalg.norm(loop.uv[:, min_ind_1] + (loop.uv[:, min_ind_2] - loop.uv[:, min_ind_1]) / 1000) 29 | v_distance = np.linalg.norm(loop.v[:, min_ind_1] + (loop.v[:, min_ind_2] - loop.v[:, min_ind_1]) / 1000) 30 | local_opening_gab = opening_gab * uv_distance / v_distance 31 | 32 | return local_opening_gab 33 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calc_mean_loop_normal.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from typing import List 4 | 5 | from .data_structures import TopoGroup, Mesh 6 | 7 | 8 | def calc_mean_loop_normal(group: TopoGroup, coil_mesh: Mesh): 9 | """ 10 | Calculate the mean loop normal for the given group. 11 | 12 | Args: 13 | group (Group): The group containing loops. 14 | coil_mesh (Mesh): The coil mesh object. 15 | 16 | Returns: 17 | ndarray: The mean loop normal. 18 | """ 19 | 20 | # Initialize an array to store all the loop normals 21 | all_loop_normals = np.zeros((3, len(group.loops))) 22 | 23 | # Calculate loop normals for each loop in the group 24 | for loop_ind in range(len(group.loops)): 25 | group_center = np.mean(group.loops[loop_ind].v, axis=1) 26 | loop_vecs = group.loops[loop_ind].v[:, 1:] - group.loops[loop_ind].v[:, :-1] 27 | center_vecs = group.loops[loop_ind].v[:, :-1] - group_center[:, np.newaxis] 28 | 29 | # Calculate cross products to get loop normals 30 | loop_normals = np.cross(loop_vecs, center_vecs, axis=0) 31 | 32 | # Calculate mean loop normal 33 | all_loop_normals[:, loop_ind] = np.mean(loop_normals, axis=1) 34 | 35 | # Calculate the mean loop normal 36 | loop_normal = np.mean(all_loop_normals, axis=1) 37 | loop_normal /= np.linalg.norm(loop_normal) 38 | 39 | # Make sure the loop normal points outwards seen from the coordinate center 40 | if np.sum(loop_normal * group_center) < 0: 41 | loop_normal *= -1 42 | 43 | return loop_normal 44 | 45 | 46 | """ 47 | Please note that in the above code, we are assuming that Group is a data structure that holds the loop information and 48 | Mesh is a data structure that holds the coil mesh information. Also, the loop_normal calculated in the Python code will 49 | be an ndarray representing the mean loop normal. 50 | """ 51 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calc_plane_line_intersection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_plane_line_intersection(n, V0, P0, P1): 5 | """ 6 | Calculate the intersection point between a plane and a line segment. 7 | 8 | Args: 9 | n (numpy.ndarray): Plane normal vector. 10 | V0 (numpy.ndarray): Point on the plane. 11 | P0 (numpy.ndarray): Start point of the line segment. 12 | P1 (numpy.ndarray): End point of the line segment. 13 | 14 | Returns: 15 | numpy.ndarray: Intersection point. 16 | int: Intersection check code (0 - no intersection, 1 - successful intersection, 17 | 2 - line segment lies in plane, 3 - intersection lies outside the segment). 18 | """ 19 | I = np.zeros(3) 20 | u = P1 - P0 21 | w = P0 - V0 22 | D = np.dot(n, u) 23 | N = -np.dot(n, w) 24 | check = 0 25 | 26 | if np.abs(D) < 1e-7: # The segment is parallel to the plane 27 | if N == 0: # The segment lies in the plane 28 | check = 2 29 | return I, check 30 | else: 31 | check = 0 # No intersection 32 | return I, check 33 | 34 | # Compute the intersection parameter 35 | sI = N / D 36 | I = P0 + sI * u 37 | 38 | if sI < 0 or sI > 1: 39 | check = 3 # The intersection point lies outside the segment 40 | else: 41 | check = 1 42 | 43 | return I, check 44 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calculate_boundary_criteria_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calculate_boundary_criteria_matrix(coil_mesh, basis_elements): 5 | """ 6 | Calculate the boundary_criteria_matrix which enforces a constant stream function on the boundary nodes. 7 | 8 | Args: 9 | coil_mesh: The mesh representing the coil geometry. 10 | basis_elements: The basis elements containing information about the triangles and current densities. 11 | 12 | Returns: 13 | boundary_criteria_matrix: The matrix that enforces the boundary conditions. 14 | """ 15 | num_nodes = coil_mesh.vertices.shape[1] 16 | boundary_criteria_matrix = np.zeros((num_nodes, num_nodes)) 17 | 18 | # Mark the nodes that are boundary nodes 19 | boundary_nodes = [] 20 | for boundary_ind in range(len(coil_mesh.boundary)): 21 | boundary_nodes.extend(coil_mesh.boundary[boundary_ind]) 22 | is_boundary = np.isin(np.arange(1, num_nodes+1), boundary_nodes) 23 | 24 | # Define the criteria that currents do not leave the surface 25 | boundary_criteria = [] 26 | for boundary_ind in range(len(coil_mesh.boundary)): 27 | boundary_criteria.extend(zip(coil_mesh.boundary[boundary_ind][1:], coil_mesh.boundary[boundary_ind][:-1])) 28 | 29 | for hhhh in range(len(boundary_criteria)): 30 | boundary_criteria_matrix[hhhh, boundary_criteria[hhhh][0]-1] = 1 31 | boundary_criteria_matrix[hhhh, boundary_criteria[hhhh][1]-1] = -1 32 | 33 | return boundary_criteria_matrix 34 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calculate_force_and_torque_matrix.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import numpy as np 3 | 4 | 5 | def calculate_force_and_torque_matrix(coil_parts, gauss_order, conductor_thickness, specific_conductivity_copper): 6 | """ 7 | Calculate the force and torque matrix for each coil part. 8 | 9 | Args: 10 | coil_parts (list): List of CoilPart objects representing different parts of the coil. 11 | gauss_order (int): Gauss order for numerical integration. 12 | conductor_thickness (float): Thickness of the conductor. 13 | specific_conductivity_copper (float): Specific conductivity of copper. 14 | 15 | Returns: 16 | list: Updated list of CoilPart objects with force and torque matrices calculated. 17 | """ 18 | material_factor = specific_conductivity_copper / conductor_thickness 19 | 20 | for part in coil_parts: 21 | num_nodes = len(part.basis_elements) 22 | 23 | # Calculate the adjacency matrix to mark mesh node neighbors 24 | node_adjacency_mat = np.zeros((num_nodes, num_nodes), dtype=bool) 25 | for tri_ind in range(part.coil_mesh.faces.shape[1]): 26 | node_adjacency_mat[part.coil_mesh.faces[0, tri_ind], 27 | part.coil_mesh.faces[1, tri_ind]] = True 28 | node_adjacency_mat[part.coil_mesh.faces[1, tri_ind], 29 | part.coil_mesh.faces[2, tri_ind]] = True 30 | node_adjacency_mat[part.coil_mesh.faces[2, tri_ind], 31 | part.coil_mesh.faces[0, tri_ind]] = True 32 | 33 | vert1, vert2 = np.where(node_adjacency_mat) 34 | mesh_edges = np.column_stack((vert1, vert2)) 35 | mesh_edges_non_unique = np.vstack((np.arange(num_nodes), mesh_edges[:, 0]), 36 | np.vstack((np.arange(num_nodes), mesh_edges[:, 1]))).T 37 | 38 | node_adjacency_mat = np.logical_or( 39 | node_adjacency_mat, node_adjacency_mat.T) 40 | part.node_adjacency_mat = node_adjacency_mat 41 | 42 | # Calculate the matrix of spatial distances for neighboring vertices 43 | nodal_neighbor_distances = np.linalg.norm(np.repeat(part.coil_mesh.vertices[:, :, np.newaxis], 44 | part.coil_mesh.vertices.shape[2], axis=2) 45 | - np.flip(np.repeat(part.coil_mesh.vertices[:, :, np.newaxis], 46 | part.coil_mesh.vertices.shape[2], axis=2), axis=1), 47 | ord=2, axis=1) 48 | part.nodal_neighbor_distances = np.squeeze( 49 | nodal_neighbor_distances) * node_adjacency_mat 50 | 51 | resistance_matrix = np.zeros((num_nodes, num_nodes)) 52 | for edge_ind in range(mesh_edges_non_unique.shape[0]): 53 | node_ind1 = mesh_edges_non_unique[edge_ind, 0] 54 | node_ind2 = mesh_edges_non_unique[edge_ind, 1] 55 | overlapping_triangles = np.intersect1d(part.basis_elements[node_ind1].triangles, 56 | part.basis_elements[node_ind2].triangles) 57 | resistance_sum = 0 58 | if overlapping_triangles.size > 0: 59 | for overlapp_tri_ind in overlapping_triangles: 60 | first_node_triangle_position = part.basis_elements[ 61 | node_ind1].triangles == overlapp_tri_ind 62 | second_node_triangle_position = part.basis_elements[ 63 | node_ind2].triangles == overlapp_tri_ind 64 | triangle_area = part.basis_elements[node_ind1].area[first_node_triangle_position] 65 | primary_current = part.basis_elements[node_ind1].current[first_node_triangle_position, :] 66 | secondary_current = part.basis_elements[node_ind2].current[second_node_triangle_position, :] 67 | resistance_sum += np.dot(primary_current, 68 | secondary_current) * (triangle_area ** 2) 69 | resistance_matrix[node_ind1, node_ind2] = resistance_sum 70 | 71 | resistance_matrix += resistance_matrix.T 72 | resistance_matrix *= material_factor 73 | part.resistance_matrix = resistance_matrix 74 | 75 | return coil_parts 76 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calculate_one_ring_by_mesh.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | from typing import List 3 | import numpy as np 4 | 5 | # Logging 6 | import logging 7 | 8 | # Local imports 9 | from .data_structures import CoilPart 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def calculate_one_ring_by_mesh(coil_parts: List[CoilPart]): 15 | """ 16 | Calculate the one-ring neighborhood of vertices in the coil mesh. 17 | 18 | Initialises the following properties of a CoilPart: 19 | - one_ring_list 20 | - node_triangles 21 | - node_triangle_mat 22 | 23 | Depends on the following input_args: 24 | - None 25 | 26 | Updates the following properties of a CoilPart: 27 | - None 28 | 29 | Args: 30 | coil_parts (List[CoilPart]): List of coil parts. 31 | 32 | Returns: 33 | List[CoilPart]: Updated list of coil parts with calculated one ring information. 34 | """ 35 | for part_ind in range(len(coil_parts)): 36 | # Extract the intermediate variables 37 | coil_part = coil_parts[part_ind] 38 | part_mesh = coil_part.coil_mesh # Get the Mesh instance 39 | part_vertices = part_mesh.get_vertices() # Get the vertices for the coil part. 40 | part_faces = part_mesh.get_faces() 41 | num_vertices = part_vertices.shape[0] 42 | # NOTE: Not calculated the same as MATLAB 43 | node_triangles = part_mesh.vertex_faces() 44 | 45 | # Extract the indices of the corners of the connected triangles 46 | node_triangles_corners = [ 47 | part_faces[x, :] for x in node_triangles 48 | ] # Get triangle corners for each node 49 | 50 | # Create array with the vertex indices of neighboring vertices for each vertex in the mesh. 51 | one_ring_list = np.empty(num_vertices, dtype=object) # [] 52 | for node_ind in range(num_vertices): 53 | single_cell = node_triangles_corners[node_ind] # Arrays of corners, including current index 54 | neighbor_faces = np.asarray([x[x != node_ind] for x in single_cell]) 55 | one_ring_list[node_ind] = neighbor_faces # m (vertices) x n_m (neighbours) array of neighbouring vertices 56 | 57 | # Make sure that the current orientation is uniform for all elements 58 | for node_ind in range(num_vertices): 59 | for face_ind in range(len(one_ring_list[node_ind])): 60 | point_aa = part_vertices[one_ring_list[node_ind][face_ind][0]] 61 | point_bb = part_vertices[one_ring_list[node_ind][face_ind][1]] 62 | point_cc = part_vertices[node_ind] 63 | cross_vec = np.cross(point_bb - point_aa, point_aa - point_cc) 64 | 65 | if np.sign(np.dot(part_mesh.n[node_ind], cross_vec)) > 0: 66 | one_ring_list[node_ind][face_ind] = np.flipud( 67 | one_ring_list[node_ind][face_ind] 68 | ) 69 | 70 | # Update the coil_part with one-ring neighborhood information 71 | node_triangle_mat = np.zeros((num_vertices, part_faces.shape[0]), dtype=int) 72 | 73 | coil_part.one_ring_list = one_ring_list 74 | coil_part.node_triangles = node_triangles 75 | coil_part.node_triangle_mat = node_triangle_mat 76 | 77 | return coil_parts 78 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/calculate_sensitivity_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | 4 | # Logging 5 | import logging 6 | 7 | # Local imports 8 | from .data_structures import CoilSolution, BasisElement, CoilPart 9 | from .gauss_legendre_integration_points_triangle import gauss_legendre_integration_points_triangle 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def calculate_sensitivity_matrix(coil_parts: List[CoilPart], target_field, input_args) -> List[CoilPart]: 15 | """ 16 | Calculate the sensitivity matrix. 17 | 18 | Initialises the following properties of a CoilPart: 19 | - sensitivity_matrix: (3, m, num vertices) 20 | 21 | Updates the following properties of a CoilPart: 22 | - None 23 | 24 | Args: 25 | coil_parts (List[CoilPart]): List of coil parts. 26 | target_field: The target field. 27 | 28 | Returns: 29 | List[CoilPart]: Updated list of coil parts with sensitivity matrix. 30 | 31 | """ 32 | for part_ind in range(len(coil_parts)): 33 | coil_part = coil_parts[part_ind] 34 | 35 | target_points = target_field.coords 36 | gauss_order = input_args.gauss_order 37 | 38 | # Calculate the weights and the test point for the Gauss-Legendre integration on each triangle 39 | u_coord, v_coord, gauss_weight = gauss_legendre_integration_points_triangle(gauss_order) 40 | num_gauss_points = len(gauss_weight) 41 | biot_savart_coeff = 1e-7 42 | num_nodes = len(coil_part.basis_elements) 43 | num_target_points = target_points.shape[1] 44 | sensitivity_matrix = np.zeros((3, num_target_points, num_nodes)) 45 | 46 | for node_ind in range(num_nodes): 47 | basis_element = coil_part.basis_elements[node_ind] 48 | 49 | dCx = np.zeros(num_target_points) 50 | dCy = np.zeros(num_target_points) 51 | dCz = np.zeros(num_target_points) 52 | 53 | for tri_ind in range(len(basis_element.area)): 54 | node_point = basis_element.triangle_points_ABC[tri_ind, :, 0] 55 | point_b = basis_element.triangle_points_ABC[tri_ind, :, 1] 56 | point_c = basis_element.triangle_points_ABC[tri_ind, :, 2] 57 | 58 | x1, y1, z1 = node_point 59 | x2, y2, z2 = point_b 60 | x3, y3, z3 = point_c 61 | 62 | vx, vy, vz = basis_element.current[tri_ind] 63 | 64 | for gauss_ind in range(num_gauss_points): 65 | xgauss_in_uv = x1 * u_coord[gauss_ind] + x2 * v_coord[gauss_ind] + \ 66 | x3 * (1 - u_coord[gauss_ind] - v_coord[gauss_ind]) 67 | ygauss_in_uv = y1 * u_coord[gauss_ind] + y2 * v_coord[gauss_ind] + \ 68 | y3 * (1 - u_coord[gauss_ind] - v_coord[gauss_ind]) 69 | zgauss_in_uv = z1 * u_coord[gauss_ind] + z2 * v_coord[gauss_ind] + \ 70 | z3 * (1 - u_coord[gauss_ind] - v_coord[gauss_ind]) 71 | 72 | distance_norm = ( 73 | (xgauss_in_uv - target_points[0])**2 + (ygauss_in_uv - target_points[1])**2 + (zgauss_in_uv - target_points[2])**2)**(-3/2) 74 | 75 | dCx += ((-1) * vz * (target_points[1] - ygauss_in_uv) + vy * (target_points[2] - zgauss_in_uv) 76 | ) * distance_norm * 2 * basis_element.area[tri_ind] * gauss_weight[gauss_ind] 77 | dCy += ((-1) * vx * (target_points[2] - zgauss_in_uv) + vz * (target_points[0] - xgauss_in_uv) 78 | ) * distance_norm * 2 * basis_element.area[tri_ind] * gauss_weight[gauss_ind] 79 | dCz += ((-1) * vy * (target_points[0] - xgauss_in_uv) + vx * (target_points[1] - ygauss_in_uv) 80 | ) * distance_norm * 2 * basis_element.area[tri_ind] * gauss_weight[gauss_ind] 81 | 82 | sensitivity_matrix[:, :, node_ind] = np.array([dCx, dCy, dCz]) * biot_savart_coeff 83 | 84 | coil_part.sensitivity_matrix = sensitivity_matrix 85 | 86 | return coil_parts 87 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/check_mutual_loop_inclusion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | 4 | 5 | def check_mutual_loop_inclusion(test_poly: np.ndarray, target_poly: np.ndarray) -> bool: 6 | """ 7 | Check if the test polygon lies fully enclosed within the second polygon. 8 | 9 | This check is done with the winding number algorithm test for each vertex towards the second polygon. 10 | 11 | Args: 12 | test_poly (np.ndarray): Array representing the test polygon's 2D coordinates (shape: (2, num_vertices)). 13 | target_poly (np.ndarray): Array representing the target polygon's 2D coordinates (shape: (2, num_vertices)). 14 | 15 | Returns: 16 | bool: True if the test polygon is fully enclosed within the target polygon, False otherwise. 17 | """ 18 | 19 | if len(test_poly.shape) == 1: 20 | test_poly = test_poly.reshape((2, 1)) 21 | num_entries = test_poly.shape[1] 22 | winding_numbers = np.zeros(num_entries) 23 | for point_ind in range(num_entries): 24 | A = np.tile(test_poly[:, point_ind, np.newaxis], (1, target_poly.shape[1] - 1)) 25 | B = target_poly[:, 1:] 26 | C = target_poly[:, :-1] 27 | 28 | vec1 = C - A 29 | vec2 = B - A 30 | 31 | angle = np.arctan2(vec1[0, :] * vec2[1, :] - vec1[1, :] * vec2[0, :], 32 | vec1[0, :] * vec2[0, :] + vec1[1, :] * vec2[1, :]) 33 | 34 | winding_numbers[point_ind] = round(abs(np.sum(angle) / (2 * np.pi))) 35 | 36 | inside_flag = np.all(winding_numbers == 1) 37 | 38 | return inside_flag 39 | 40 | 41 | """ 42 | In this Python implementation, the function check_mutual_loop_inclusion takes two 2D arrays, test_poly and target_poly, 43 | which represent the coordinates of the test polygon and the target polygon, respectively. The function calculates the 44 | winding numbers for each vertex of the test polygon with respect to the target polygon using the winding number 45 | algorithm. If all winding numbers are equal to 1, it indicates that the test polygon is fully enclosed within the 46 | target polygon, and the function returns True; otherwise, it returns False. 47 | """ 48 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/constants.py: -------------------------------------------------------------------------------- 1 | # Debug levels 2 | DEBUG_NONE = 0 3 | DEBUG_BASIC = 1 4 | DEBUG_VERBOSE = 2 5 | 6 | _shared_data = {'CURRENT_LEVEL': DEBUG_NONE} 7 | 8 | 9 | def set_level(level): 10 | _shared_data['CURRENT_LEVEL'] = level 11 | 12 | 13 | def get_level(): 14 | return _shared_data['CURRENT_LEVEL'] 15 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/export_data.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import os 3 | from argparse import Namespace 4 | 5 | # Logging 6 | import logging 7 | 8 | # Local imports 9 | from pyCoilGen.export_factory import load_exporter_plugins 10 | 11 | from .data_structures import CoilSolution 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def check_exporter_help(input_args: Namespace, print_function=print): 17 | """ 18 | Print the help, if the exporter is 'help'. 19 | 20 | Args: 21 | input_args (Namespace): The project input arguments. 22 | print_function(function): The function used to write out the help. 23 | 24 | Returns: 25 | bool: True if help was requested. 26 | """ 27 | plugin_name = input_args.exporter 28 | if plugin_name == 'help': 29 | print_function('Available exporter plugins are:') 30 | exporter_plugins = load_exporter_plugins() 31 | for plugin in exporter_plugins: 32 | name_function = getattr(plugin, 'get_name', None) 33 | parameters_function = getattr(plugin, 'get_parameters', None) 34 | if name_function: 35 | name = name_function() 36 | if parameters_function: 37 | parameters = parameters_function() 38 | parameter_name, default_value = parameters[0] 39 | print_function(f"'{name}', Parameter: '{parameter_name}', Default values: {default_value}") 40 | for i in range(1, len(parameters)): 41 | print(f"\t\tParameter: '{parameter_name}', Default values: '{default_value}'") 42 | 43 | else: 44 | print_function(f"'{name}', no parameters") 45 | return True 46 | return False 47 | 48 | 49 | def export_data(solution: CoilSolution): 50 | """ 51 | Use an exporter to save export data from the solution. 52 | 53 | The exporter is specified using the `exporter` parameter. 54 | 55 | Args: 56 | solution (CoilSolution): The current coil solution. 57 | 58 | Returns: 59 | None 60 | 61 | Raises: 62 | ValueError if the export function can not be found. 63 | """ 64 | input_args = solution.input_args 65 | 66 | # Read the list of exporters 67 | exporter_plugins = load_exporter_plugins() 68 | 69 | exporter = input_args.exporter 70 | 71 | plugin_name = exporter.replace(' ', '_').replace('-', '_') 72 | 73 | # Exit early if no exporter is specified. 74 | if plugin_name == 'none': 75 | log.debug("Exporter is 'none', exiting...") 76 | return 77 | 78 | print("Using exporter plugin: ", plugin_name) 79 | 80 | found = False 81 | for plugin in exporter_plugins: 82 | exporter_function = getattr(plugin, plugin_name, None) 83 | if exporter_function: 84 | log.debug("Calling exporter: %s", plugin_name) 85 | exporter_function(solution) 86 | found = True 87 | break 88 | 89 | if found == False: 90 | raise ValueError(f"Function {plugin_name} was not found in {input_args.exporter}") 91 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/find_minimal_contour_distance.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from typing import List 4 | 5 | # Logging 6 | import logging 7 | 8 | # Local imports 9 | from .data_structures import CoilPart 10 | from .find_min_mutual_loop_distance import find_min_mutual_loop_distance 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def find_minimal_contour_distance(coil_parts: List[CoilPart], input_args): 16 | """ 17 | Find the minimal distance in the xyz domain between contours to assign a proper conductor width later. 18 | 19 | Initialises the following properties of a CoilPart: 20 | - None 21 | 22 | Depends on the following properties of the CoilParts: 23 | - contour_lines 24 | 25 | Depends on the following input_args: 26 | - skip_calculation_min_winding_distance 27 | 28 | Updates the following properties of a CoilPart: 29 | - pcb_track_width 30 | 31 | Args: 32 | coil_parts (List[CoilPart]): List of CoilPart structures. 33 | input_args (DataStructure): The application command-line arguments. 34 | 35 | Returns: 36 | List[CoilPart]: List of CoilPart structures with the 'pcb_track_width' attribute updated. 37 | """ 38 | for part_ind in range(len(coil_parts)): 39 | coil_part = coil_parts[part_ind] 40 | if not input_args.skip_calculation_min_winding_distance: 41 | min_vals = [] 42 | for ind_1 in range(len(coil_part.contour_lines)): 43 | for ind_2 in range(ind_1, len(coil_part.contour_lines)): 44 | if ind_1 != ind_2: 45 | min_dist = find_min_mutual_loop_distance(coil_part.contour_lines[ind_1], 46 | coil_part.contour_lines[ind_2], 47 | False, only_min_dist=True) 48 | min_vals.append(min_dist) 49 | coil_part.pcb_track_width = min(min_vals) 50 | else: 51 | coil_part.pcb_track_width = 0 52 | 53 | return coil_parts 54 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/find_segment_intersections.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | 4 | # local imports 5 | from .data_structures import DataStructure 6 | 7 | 8 | def find_segment_intersections(loop: np.ndarray, test_polygon: np.ndarray): 9 | """ 10 | Find intersection points between a loop and a polygon (2D). 11 | 12 | Args: 13 | loop (np.ndarray): Array representing the loop's 2D coordinates (shape: (2, num_vertices)). 14 | test_polygon (np.ndarray): Array representing the polygon's 2D coordinates (shape: (2, num_vertices)). 15 | 16 | Returns: 17 | List[dict]: A list of dictionaries, each containing 'segment_inds' and 'uv' keys. 18 | 'segment_inds' holds indices of the segments where intersections occur. 19 | 'uv' holds the intersection points as a 2xN array. 20 | Values contain np.nan if there is no intersection. 21 | """ 22 | 23 | intersection_points = [] 24 | num_segments = test_polygon.shape[1] - 1 25 | 26 | for seg_ind in range(num_segments): 27 | x1 = np.tile(test_polygon[0, seg_ind], loop.shape[1] - 1) 28 | x2 = np.tile(test_polygon[0, seg_ind + 1], loop.shape[1] - 1) 29 | x3 = loop[0, :-1] 30 | x4 = loop[0, 1:] 31 | 32 | y1 = np.tile(test_polygon[1, seg_ind], loop.shape[1] - 1) 33 | y2 = np.tile(test_polygon[1, seg_ind + 1], loop.shape[1] - 1) 34 | y3 = loop[1, :-1] 35 | y4 = loop[1, 1:] 36 | 37 | d1x = x2 - x1 38 | d1y = y2 - y1 39 | d2x = x4 - x3 40 | d2y = y4 - y3 41 | 42 | s = (-d1y * (x1 - x3) + d1x * (y1 - y3)) / (-d2x * d1y + d1x * d2y) 43 | t = (d2x * (y1 - y3) - d2y * (x1 - x3)) / (-d2x * d1y + d1x * d2y) 44 | 45 | intersection_segment_inds = np.where((s >= 0) & (s <= 1) & (t >= 0) & (t <= 1))[0] 46 | 47 | if len(intersection_segment_inds) > 0: 48 | x_out = x1[intersection_segment_inds] + (t[intersection_segment_inds] * d1x[intersection_segment_inds]) 49 | y_out = y1[intersection_segment_inds] + (t[intersection_segment_inds] * d1y[intersection_segment_inds]) 50 | 51 | new_intersection = DataStructure(segment_inds=intersection_segment_inds, uv=np.vstack((x_out, y_out))) 52 | else: 53 | new_intersection = DataStructure(segment_inds=np.nan, uv=np.vstack((np.nan, np.nan))) 54 | intersection_points.append(new_intersection) 55 | 56 | return intersection_points 57 | 58 | 59 | """ 60 | In this Python implementation, the function find_segment_intersections takes two 2D arrays, loop and test_polygon, 61 | which represent the coordinates of a loop and a test polygon, respectively. The function then finds the intersection 62 | points between each segment of the test polygon and the loop. The result is returned as a list of dictionaries, where 63 | each dictionary contains the indices of the segments with intersections and the corresponding 2D intersection points. 64 | 65 | Please note that the implementation might not directly translate to NumPy broadcasting due to the nature of the loop in 66 | the original Matlab code. Instead, the use of NumPy functions like np.tile, np.where, and array slicing helps achieve 67 | the same functionality. 68 | """ 69 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/gauss_legendre_integration_points_triangle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def gauss_legendre_integration_points_triangle(n): 5 | """ 6 | Calculate the coordinates and weighting coefficients for Gauss-Legendre integration on a triangle. 7 | 8 | Args: 9 | n (int): Order of the integration. 10 | 11 | Returns: 12 | u (ndarray): U coordinates of the integration points. 13 | v (ndarray): V coordinates of the integration points. 14 | ck (ndarray): Weighting coefficients. 15 | 16 | """ 17 | 18 | eta, w = calc_weights_gauss(n) 19 | num_points = eta.shape[0] * eta.shape[0] 20 | u = np.zeros((num_points, 1)) 21 | v = np.zeros((num_points, 1)) 22 | ck = np.zeros((num_points, 1)) 23 | 24 | k = 0 25 | for i in range(eta.shape[0]): 26 | for j in range(eta.shape[0]): 27 | u[k, 0] = (1 + eta[i]) / 2 28 | v[k, 0] = (1 - eta[i]) * (1 + eta[j]) / 4 29 | ck[k, 0] = ((1 - eta[i]) / 8) * w[i] * w[j] 30 | k += 1 31 | 32 | return u, v, ck 33 | 34 | 35 | def calc_weights_gauss(n): 36 | """ 37 | Generate the abscissa and weights for Gauss-Legendre quadrature. 38 | 39 | Args: 40 | n (int): Number of points. 41 | 42 | Returns: 43 | g_abscissa (ndarray): Abscissa values. 44 | g_weights (ndarray): Weighting coefficients. 45 | 46 | """ 47 | 48 | g_abscissa = np.zeros(n) # Preallocations. 49 | g_weights = np.zeros(n) 50 | m = int((n + 1) / 2) 51 | 52 | for ii in range(1, m + 1): 53 | z = np.cos(np.pi * (ii - 0.25) / (n + 0.5)) # Initial estimate. 54 | z1 = z + 1.0 55 | while abs(z - z1) > np.finfo(float).eps: 56 | p1 = 1 57 | p2 = 0 58 | 59 | for jj in range(1, n + 1): 60 | p3 = p2 61 | p2 = p1 62 | p1 = ((2 * jj - 1) * z * p2 - (jj - 1) * p3) / jj # The Legendre polynomial. 63 | 64 | pp = n * (z * p1 - p2) / (z ** 2 - 1) # The L.P. derivative. 65 | z1 = z 66 | z = z1 - p1 / pp 67 | 68 | g_abscissa[ii - 1] = -z # Build up the abscissas. 69 | g_abscissa[n - ii] = z 70 | g_weights[ii - 1] = 2 / ((1 - z ** 2) * (pp ** 2)) # Build up the weights. 71 | g_weights[n - ii] = g_weights[ii - 1] 72 | 73 | return g_abscissa, g_weights 74 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/load_preoptimized_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from typing import List 4 | import logging 5 | 6 | from os import path 7 | 8 | 9 | # Local imports 10 | from .constants import * 11 | from .data_structures import CoilSolution, TargetField, Mesh, DataStructure 12 | from .split_disconnected_mesh import split_disconnected_mesh 13 | from .parameterize_mesh import parameterize_mesh 14 | from .stream_function_optimization import generate_combined_mesh 15 | 16 | # For timing 17 | from pyCoilGen.helpers.timing import Timing 18 | 19 | # For loading data files 20 | from pyCoilGen.helpers.common import find_file 21 | 22 | 23 | # Logging 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | def load_preoptimized_data(input_args, default_dir='Pre_Optimized_Solutions') -> CoilSolution: 28 | """ 29 | Load pre-calculated data from a previous run. 30 | 31 | Initialises the following properties of the CoilParts: 32 | - v,n (np.ndarray) : vertices and vertex normals (m,3), (m,3) 33 | - f,fn (np.ndarray) : faces and face normals (n,2), (n,3) 34 | - uv (np.ndarray) : 2D project of mesh (m,2) 35 | - boundary (int) : list of lists boundary vertex indices (n, variable) 36 | 37 | Depends on the following properties of the CoilParts: 38 | - None 39 | 40 | Depends on the following input_args: 41 | - sf_source_file 42 | 43 | Updates the following properties of a CoilPart: 44 | - None 45 | 46 | Args: 47 | input_args (any): Input arguments for loading pre-optimised data. 48 | default_dir (str, optional): Default directory to search first. Defaults to 'Pre_Optimized_Solutions' 49 | 50 | Returns: 51 | coilSolution (CoilSolution): Pre-optimised coil solution containing mesh and stream function information. 52 | """ 53 | # Load pre-optimised data 54 | source_file = f'{input_args.sf_source_file}.npy' 55 | if '/' in source_file or '\\' in source_file: 56 | filename = source_file 57 | else: 58 | filename = find_file(default_dir, source_file) 59 | 60 | # Load data from load_path 61 | log.info("Loading pre-optimised data from '%s'", filename) 62 | loaded_data = np.load(filename, allow_pickle=True)[0] 63 | 64 | # Extract loaded data 65 | coil_mesh = loaded_data.coil_mesh 66 | # Transpose because data is saved in Python (m,3) format 67 | target_field = TargetField(b=loaded_data.target_field.b.T, coords=loaded_data.target_field.coords.T) 68 | stream_function = loaded_data.stream_function 69 | 70 | timer = Timing() 71 | 72 | secondary_target_mesh = None 73 | 74 | # Split the mesh and the stream function into disconnected pieces 75 | timer.start() 76 | log.info('Split the mesh and the stream function into disconnected pieces.') 77 | combined_mesh = Mesh(vertices=coil_mesh.vertices, faces=coil_mesh.faces) 78 | combined_mesh.normal_rep = [0.0, 0.0, 0.0] # Invalid value, fix this later if needed 79 | coil_parts = split_disconnected_mesh(combined_mesh) 80 | timer.stop() 81 | 82 | # Parameterize the mesh 83 | timer.start() 84 | log.info('Parameterise the mesh:') 85 | coil_parts = parameterize_mesh(coil_parts, input_args) 86 | timer.stop() 87 | 88 | # Update additional target field properties 89 | target_field.weights = np.ones(target_field.b.shape[1]) 90 | target_field.target_field_group_inds = np.ones(target_field.b.shape[1]) 91 | is_suppressed_point = np.zeros(target_field.b.shape[1]) 92 | sf_b_field = loaded_data.target_field.b # MATLAB Shape 93 | 94 | # Generate a combined mesh container 95 | # TODO: Umm?? Why recreate the combined mesh, when it was created above? 96 | combined_mesh = generate_combined_mesh(coil_parts) 97 | 98 | # Assign the stream function to the different mesh parts 99 | for part_ind in range(len(coil_parts)): 100 | unique_vert_inds = coil_parts[part_ind].coil_mesh.unique_vert_inds 101 | coil_parts[part_ind].stream_function = stream_function[unique_vert_inds] 102 | 103 | # Return the CoilSolution instance with the pre-optimised data 104 | return CoilSolution(input_args=input_args, coil_parts=coil_parts, target_field=target_field, 105 | is_suppressed_point=is_suppressed_point, combined_mesh=combined_mesh, 106 | sf_b_field=sf_b_field) 107 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/plane_line_intersect.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Logging 4 | import logging 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | def plane_line_intersect(plane_normal: np.ndarray, plane_pos: np.ndarray, point_0: np.ndarray, point_1: np.ndarray): 10 | """ 11 | Compute the intersection point between a plane and a line segment defined by two points. 12 | 13 | Args: 14 | plane_normal (np.ndarray): The normal vector of the plane. 15 | plane_pos (np.ndarray): A point on the plane. 16 | point_0 (np.ndarray): The first point of the line segment. 17 | point_1 (np.ndarray): The second point of the line segment. 18 | 19 | Returns: 20 | intersec_point (np.ndarray): The intersection point. 21 | cut_flag (int): A flag indicating the type of intersection. 22 | - 1: The intersection point is within the line segment. 23 | - 2: The line segment lies within the plane. 24 | - 3: The intersection point is outside the line segment. 25 | 26 | """ 27 | intersec_point = np.zeros(3) 28 | line_vec = point_1 - point_0 29 | diff_vec = point_0 - plane_pos 30 | D = np.dot(plane_normal, line_vec) 31 | N = -np.dot(plane_normal, diff_vec) 32 | cut_flag = 0 33 | 34 | if abs(D) < 10**-7: # The segment is parallel to the plane 35 | if N == 0: # The segment lies in the plane 36 | cut_flag = 2 37 | return intersec_point, cut_flag 38 | else: 39 | cut_flag = 0 # no intersection 40 | return intersec_point, cut_flag 41 | 42 | # Compute the intersection parameter 43 | sI = N / D 44 | intersec_point = point_0 + sI * line_vec 45 | 46 | if (sI < -0.0000001 or sI > 1.0000001): 47 | cut_flag = 3 # The intersection point lies outside the segment, so there is no intersection 48 | else: 49 | cut_flag = 1 50 | 51 | return intersec_point, cut_flag 52 | 53 | 54 | """ 55 | In this Python version, the function plane_line_intersect takes in the required parameters as NumPy arrays and returns 56 | the intersection point and a cut flag indicating the type of intersection. The rest of the code remains similar to the 57 | MATLAB implementation, but with appropriate NumPy syntax for array operations and comparisons. 58 | """ 59 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/remove_points_from_loop.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from typing import List 4 | 5 | # Logging 6 | import logging 7 | 8 | # Local imports 9 | from .data_structures import Shape3D 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def remove_points_from_loop(loop: Shape3D, points_to_remove: np.ndarray, boundary_threshold: int): 15 | """ 16 | Remove points with identical uv coordinates from a loop, even with some additional more points around. 17 | 18 | Args: 19 | loop (Shape3D): The loop data containing 'uv' and 'v'. 20 | points_to_remove (np.ndarray): The points to be removed (shape: (2, num_points)). 21 | boundary_threshold (int): The number of additional points around each identical point to be removed. 22 | 23 | Returns: 24 | loop_out_uv (Shape2D): The updated 2D loop data after removing the specified points. 25 | loop_out_v (Shape3D): The updated 3D loop data after removing the specified points. 26 | """ 27 | 28 | rep_u = np.tile(loop.uv[0, :], (points_to_remove.shape[1], 1)) 29 | rep_v = np.tile(loop.uv[1, :], (points_to_remove.shape[1], 1)) 30 | 31 | rep_u2 = np.tile(points_to_remove[0, :], (loop.uv.shape[1], 1)) 32 | rep_v2 = np.tile(points_to_remove[1, :], (loop.uv.shape[1], 1)) 33 | 34 | arr1 = rep_u == rep_u2.T 35 | arr2 = rep_v == rep_v2.T 36 | arr3 = arr1 & arr2 37 | 38 | identical_point_inds1 = np.where(arr3) 39 | identical_point_inds = identical_point_inds1[1] # Magic number chose to reproduce MATLAB results 40 | 41 | if len(identical_point_inds) > 0: 42 | below_inds = np.arange(min(identical_point_inds) - boundary_threshold, min(identical_point_inds)+1) 43 | below_inds[below_inds < 0] = below_inds[below_inds < 0] + loop.uv.shape[1] 44 | 45 | abow_inds = np.arange(max(identical_point_inds), max(identical_point_inds) + boundary_threshold + 1) 46 | abow_inds[abow_inds >= loop.uv.shape[1]] = abow_inds[abow_inds >= loop.uv.shape[1]] - loop.uv.shape[1] 47 | 48 | # Add more points as a "boundary threshold" 49 | full_point_inds_to_remove = np.concatenate((below_inds, identical_point_inds, abow_inds)) 50 | 51 | # Use np.in1d to find the indices to keep 52 | inds_to_keep = np.arange(loop.uv.shape[1])[~np.in1d(np.arange(loop.uv.shape[1]), full_point_inds_to_remove)] 53 | 54 | # Extract the loop with the remaining points 55 | loop_out_uv = loop.uv[:, inds_to_keep] 56 | loop_out_v = loop.v[:, inds_to_keep] 57 | else: 58 | loop_out_uv = loop.uv 59 | loop_out_v = loop.v 60 | 61 | return loop_out_uv, loop_out_v 62 | 63 | 64 | """ 65 | Note: The code assumes that the data structure Shape3D is already defined elsewhere or imported. Additionally, the 66 | function returns the updated loop data (loop_out_uv and loop_out_v) rather than modifying the original data in-place, 67 | as it is not possible to modify the input loop data due to the immutability of NumPy arrays. The logic and 68 | functionality of the MATLAB function have been retained in the Python equivalent. 69 | """ 70 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/smooth_track_by_folding.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def smooth_track_by_folding(track_in, smoothing_length): 5 | """ 6 | Smooth a track by folding its data. 7 | 8 | Args: 9 | track_in (ndarray): The input track data with shape (2, M). 10 | smoothing_length (int): The length of smoothing. 11 | 12 | Returns: 13 | ndarray: The smoothed track with shape (2, M). 14 | """ 15 | 16 | if smoothing_length > 1: 17 | track_out = track_in.copy() 18 | 19 | # Extend the track by repeating the first and last points for smoothing 20 | extended_track = np.hstack(( 21 | np.tile(track_in[:, 0].reshape(-1, 1), (1, smoothing_length)), 22 | track_in[:, 1:-1], 23 | np.tile(track_in[:, -1].reshape(-1, 1), (1, smoothing_length)) 24 | )) 25 | 26 | for shift_ind in range(-(smoothing_length - 1), 0): 27 | add_track = np.roll(extended_track, shift_ind, axis=1) 28 | add_track = add_track[:, smoothing_length-1:(-smoothing_length+1)] 29 | track_out = track_out + add_track 30 | 31 | for shift_ind in range(1, smoothing_length): 32 | add_track = np.roll(extended_track, shift_ind, axis=1) 33 | add_track = add_track[:, smoothing_length-1:(-smoothing_length+1)] 34 | track_out = track_out + add_track 35 | 36 | # Calculate the average of the overlapping segments 37 | track_out /= (2 * smoothing_length - 1) 38 | 39 | return track_out 40 | else: 41 | return track_in 42 | -------------------------------------------------------------------------------- /pyCoilGen/sub_functions/temp_evaluation.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from sys import getsizeof 3 | import struct 4 | 5 | # Logging 6 | import logging 7 | 8 | # Local imports 9 | from .data_structures import CoilSolution 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def temp_evaluation(coil_solution: CoilSolution, input, target_field): 15 | """ 16 | Evaluates whether pre-calculated values can be used from previous calculations. 17 | 18 | Args: 19 | CoilSolution 20 | input (object): Input parameters. 21 | target_field (object): Target field data. 22 | 23 | Returns: 24 | object: Updated input parameters. 25 | """ 26 | preoptimization_input_hash = generate_DataHash([input.coil_mesh_file, input.iteration_num_mesh_refinement, 27 | input.surface_is_cylinder_flag, target_field]) 28 | optimized_input_hash = generate_DataHash([input.sf_opt_method, input.tikhonov_reg_factor, input.fmincon_parameter]) 29 | 30 | coil_solution.optimisation.use_preoptimization_temp = False 31 | coil_solution.optimisation.use_optimized_temp = False 32 | 33 | # Initialize values if not existing 34 | if not hasattr(coil_solution.optimisation, 'preoptimization_hash'): 35 | coil_solution.optimisation.preoptimization_hash = 'none' 36 | 37 | if not hasattr(coil_solution.optimisation, 'optimized_hash'): 38 | coil_solution.optimisation.optimized_hash = 'none' 39 | 40 | if preoptimization_input_hash == coil_solution.optimisation.preoptimization_hash: 41 | coil_solution.optimisation.use_preoptimization_temp = True 42 | 43 | if optimized_input_hash == coil_solution.optimisation.optimized_hash: 44 | coil_solution.optimisation.use_optimized_temp = True 45 | 46 | # Assign the new hash to temp 47 | coil_solution.optimisation.preoptimization_hash = preoptimization_input_hash 48 | coil_solution.optimisation.optimized_hash = optimized_input_hash 49 | 50 | log.debug(" - preoptimization_hash: %s", preoptimization_input_hash) 51 | log.debug(" - optimized_hash: %s", optimized_input_hash) 52 | 53 | return input 54 | 55 | 56 | def generate_DataHash(Data): 57 | """ 58 | Generates the MD5 hash of the provided data. 59 | 60 | Args: 61 | Data: Data to be hashed. 62 | 63 | Returns: 64 | str: MD5 hash of the data. 65 | """ 66 | Engine = hashlib.md5() 67 | H = CoreHash(Data, Engine) 68 | H = H.hexdigest() # To hex string 69 | return H 70 | 71 | 72 | def CoreHash(Data, Engine): 73 | """ 74 | Core hashing function for generating MD5 hash recursively. 75 | 76 | Args: 77 | Data: Data to be hashed. 78 | Engine: MD5 engine. 79 | 80 | Returns: 81 | hashlib.md5: MD5 hash object. 82 | """ 83 | # Consider the type of empty arrays 84 | S = f"{type(Data).__name__} {Data.shape}" if hasattr(Data, 'shape') else f"{type(Data).__name__}" 85 | Engine.update(S.encode('utf-8')) 86 | H = hashlib.md5(Engine.digest()) 87 | 88 | if isinstance(Data, dict): 89 | for key in sorted(Data.keys()): 90 | H.update(CoreHash(Data[key], hashlib.md5()).digest()) 91 | 92 | elif isinstance(Data, (list, tuple)): 93 | for item in Data: 94 | H.update(CoreHash(item, hashlib.md5()).digest()) 95 | 96 | elif isinstance(Data, (str, bytes)): 97 | H.update(Data.encode('utf-8')) 98 | 99 | elif isinstance(Data, bool): 100 | H.update(int(Data).to_bytes(1, 'big')) 101 | 102 | elif isinstance(Data, int): 103 | H.update(int(Data).to_bytes(getsizeof(int), 'big')) 104 | 105 | elif isinstance(Data, float): 106 | H.update(bytearray(struct.pack("f", float(Data)))) 107 | 108 | elif callable(Data): 109 | H.update(CoreHash(Data.__code__, hashlib.md5()).digest()) 110 | 111 | else: 112 | log.warning(f"Type of variable not considered: {type(Data).__name__}") 113 | 114 | return H 115 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pycoilgen" 7 | authors = [ 8 | {name = "Kevin Meyer", email = "kevin@kmz.co.za"}, 9 | {name = "Philipp Amrein", email="none@noreply.com"}, 10 | ] 11 | readme = "README.md" 12 | license = {file = "LICENSE.txt"} 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Environment :: Console", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: Healthcare Industry", 18 | "Intended Audience :: Science/Research", 19 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Topic :: Scientific/Engineering", 30 | ] 31 | 32 | requires-python = ">=3.6" 33 | keywords = [ 34 | "MRI", 35 | "Magnetic Resonance Imaging", 36 | "NMR", 37 | "Nuclear Resonance Imaging", 38 | "Target Field", 39 | "Gradient Field", 40 | "Physics", 41 | "Coil", 42 | ] 43 | dependencies = [ 44 | "numpy==1.*", 45 | "scipy==1.*", 46 | "trimesh==3.*", 47 | "sympy==1.*", 48 | "pillow<=9.5", 49 | "matplotlib==3.*", 50 | ] 51 | dynamic = ["version", "description"] 52 | 53 | [project.urls] 54 | Home = "https://github.com/kev-m/pyCoilGen" 55 | Documentation = "https://pycoilgen.readthedocs.io/" 56 | Source = "https://github.com/kev-m/pyCoilGen" 57 | "Code of Conduct" = "https://github.com/kev-m/pyCoilGen/blob/release/CODE_OF_CONDUCT.md" 58 | "Bug tracker" = "https://github.com/kev-m/pyCoilGen/issues" 59 | Changelog = "https://github.com/kev-m/pyCoilGen/blob/release/CHANGELOG.md" 60 | Contributing = "https://github.com/kev-m/pyCoilGen/blob/release/CONTRIBUTING.md" 61 | 62 | [project.scripts] 63 | pyCoilGen = "pyCoilGen:__main__.main" 64 | 65 | [tool.flit.module] 66 | name = "pyCoilGen" 67 | 68 | [tool.flit.sdist] 69 | include = [ 70 | ] 71 | exclude = [ 72 | "docs", 73 | "examples", 74 | "utilities", 75 | "scratchpad", 76 | "tests", 77 | "data", 78 | "pyCoilGen/pyCoilGen_develop.py", 79 | ] 80 | 81 | [tool.autopep8] 82 | max_line_length = 120 83 | aggressive = 0 -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . 3 | python_files = test_*.py regression_*.py 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ################################## 2 | autopep8==2.0.2 3 | 4 | # For testing 5 | pytest==7.4.0 6 | 7 | # For packaging 8 | flit==3.9.0 9 | 10 | # For mesh visualisation (optional) 11 | pyglet<2 12 | 13 | # For ChangeLog generation 14 | auto-changelog -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.* 2 | 3 | # Note: Also need BLAS and gfortran to install scipy 4 | # sudo apt-get install libopenblas-dev 5 | # sudo apt install gfortran 6 | scipy==1.* 7 | 8 | # For Mesh support 9 | trimesh==3.* 10 | # Missing Trimesh dependency 11 | networkx==2.* 12 | 13 | # For target field calculations 14 | sympy==1.* 15 | 16 | # For visualisation of matrices 17 | pillow<=9.5 18 | matplotlib==3.* -------------------------------------------------------------------------------- /scratchpad/checking1.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # System imports 4 | import sys 5 | 6 | # Logging 7 | import logging 8 | 9 | # Local imports 10 | from pyCoilGen.pyCoilGen_release import pyCoilGen 11 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 12 | 13 | if __name__ == '__main__': 14 | # Set up logging 15 | log = logging.getLogger(__name__) 16 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 17 | # logging.basicConfig(level=logging.INFO) 18 | 19 | 20 | tikhonov_factor = 10000 21 | num_levels = 30 22 | pcb_width = 0.002 23 | cut_width = 0.025 24 | normal_shift = 0.006 25 | min_loop_significance = 3 26 | 27 | circular_resolution = 10 28 | conductor_width = 0.0015 29 | theta = np.linspace(0, 2*np.pi, circular_resolution) 30 | cross_sectional_points = np.array([np.sin(theta), np.cos(theta)]) * conductor_width 31 | normal_shift_smooth_factors = [5, 5, 5] 32 | 33 | # Define the parameters as a dictionary 34 | parameters = { 35 | 'field_shape_function': '0.2500000000000001*x + 0.7694208842938134*y + 0.5877852522924731*z', 36 | 'coil_mesh_file': 'create cylinder mesh', 37 | 'cylinder_mesh_parameter_list': [0.4913, 0.154, 50, 50, 0, 1, 0, np.pi/2], 38 | 'surface_is_cylinder_flag': True, 39 | 'min_loop_significance': min_loop_significance, 40 | 'target_region_radius': 0.1, 41 | 'levels': num_levels, 42 | 'pot_offset_factor': 0.25, 43 | 'interconnection_cut_width': cut_width, 44 | 'conductor_cross_section_width': pcb_width, 45 | 'normal_shift_length': normal_shift, 46 | 'skip_postprocessing': False, 47 | 'make_cylindrical_pcb': True, 48 | 'skip_inductance_calculation': False, 49 | 'cross_sectional_points': cross_sectional_points, 50 | 'normal_shift_smooth_factors': normal_shift_smooth_factors, 51 | # 'smooth_flag': True, 52 | 'smooth_factor': 2, 53 | 'save_stl_flag': True, 54 | 'tikhonov_reg_factor': tikhonov_factor, 55 | 56 | 'output_directory': 'images', # [Current directory] 57 | 'project_name': 'find_group_cut_test', # See https://github.com/kev-m/pyCoilGen/issues/60 58 | 'persistence_dir': 'debug', 59 | 'debug': DEBUG_BASIC, 60 | 61 | 'sf_dest_file' : 'test_find_group_cut', 62 | 'sf_source_file' : 'test_find_group_cut' 63 | 64 | } 65 | 66 | # Run the algorithm with the given parameters (CoilGen function is not provided here) 67 | result = pyCoilGen(log, parameters) -------------------------------------------------------------------------------- /scratchpad/checking2.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | # Logging 5 | import logging 6 | 7 | # Local imports 8 | from pyCoilGen.pyCoilGen_release import pyCoilGen 9 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 10 | 11 | if __name__ == '__main__': 12 | # Set up logging 13 | log = logging.getLogger(__name__) 14 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 15 | # logging.basicConfig(level=logging.INFO) 16 | 17 | arg_dict1 = { 18 | 'field_shape_function': 'x**2 + y**2', # definition of the target field 19 | 'coil_mesh_file': 'bi_planer_rectangles_width_1000mm_distance_500mm.stl', 20 | 'target_region_radius': 0.1, # in meter 21 | # 'target_region_resolution': 10, # MATLAB 10 is the default 22 | 'use_only_target_mesh_verts': False, 23 | 'sf_source_file': 'none', 24 | # the number of potential steps that determines the later number of windings (Stream function discretization) 25 | 'levels': 30, 26 | # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 27 | 'pot_offset_factor': 0.25, 28 | 'surface_is_cylinder_flag': True, 29 | # the width for the interconnections are interconnected; in meter 30 | 'interconnection_cut_width': 0.05, 31 | # the length for which overlapping return paths will be shifted along the surface normals; in meter 32 | 'normal_shift_length': 0.01, 33 | # 'iteration_num_mesh_refinement': 1, # the number of refinements for the mesh; 34 | 'set_roi_into_mesh_center': True, 35 | 'force_cut_selection': ['high'], 36 | # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 37 | 'level_set_method': 'primary', 38 | 'skip_postprocessing': False, 39 | 'skip_inductance_calculation': False, 40 | 'tikhonov_reg_factor': 10, # Tikhonov regularization factor for the SF optimization 41 | 42 | 'sf_dest_file': 'images/loop_opening_exc/solution', # Save pre-optimised solution 43 | 44 | 'output_directory': 'images/loop_opening_exc', # [Current directory] 45 | 'project_name': 'loop_opening_exception', 46 | 'persistence_dir': 'debug', 47 | 'debug': DEBUG_BASIC, 48 | } 49 | 50 | arg_dict = { 51 | 'field_shape_function': 'x', # definition of the target field ['x'] 52 | 'coil_mesh': 'create bi-planar mesh', 53 | 'biplanar_mesh_parameter_list': [1, 1, 30, 30, 0, 1, 0, 0, 0, 0, 0.5], 54 | 'min_loop_significance': 3, # [1] Remove loops if they contribute less than 3% to the target field. 55 | 'target_region_radius': 0.125, # [0.15] in meter 56 | 'pot_offset_factor': 0.25, # [0.5] a potential offset value for the minimal and maximal contour potential 57 | 'interconnection_cut_width': 0.005, # [0.01] the width for the interconnections are interconnected; in meter 58 | # the length for which overlapping return paths will be shifted along the surface normals; in meter 59 | 'surface_is_cylinder_flag': True, 60 | 'normal_shift_length': 0.01, # [0.001] 61 | 'make_cylindrical_pcb': False, # [False] 62 | 'save_stl_flag': True, 63 | 'smooth_factor': 1, 64 | 65 | # 'tikhonov_reg_factor': 1000, # Tikhonov regularization factor for the SF optimization 66 | # 'cut_plane_definition' : 'B0', 67 | 'skip_postprocessing' : True, 68 | 69 | 'output_directory': 'images/loop_opening_exc', # [Current directory] 70 | 'project_name': 'loop_opening_exception', 71 | 'persistence_dir': 'debug', 72 | 'debug': DEBUG_BASIC, 73 | } 74 | result = pyCoilGen(log, arg_dict) 75 | -------------------------------------------------------------------------------- /scratchpad/checking3.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | from pathlib import Path 4 | import numpy as np 5 | 6 | # Trimesh 7 | import trimesh 8 | 9 | # Logging 10 | import logging 11 | 12 | # Local imports 13 | # Add the sub_functions directory to the Python module search path 14 | sub_functions_path = Path(__file__).resolve().parent / '../sub_functions' 15 | sys.path.append(str(sub_functions_path)) 16 | 17 | from build_planar_mesh import build_planar_mesh 18 | from build_biplanar_mesh import build_biplanar_mesh 19 | from data_structures import Mesh 20 | 21 | # Import the required modules from sub_functions directory 22 | 23 | if __name__ == "__main__": 24 | # Set up logging 25 | log = logging.getLogger(__name__) 26 | logging.basicConfig(level=logging.DEBUG) 27 | # logging.basicConfig(level=logging.INFO) 28 | 29 | # attach to logger so trimesh messages will be printed to console 30 | trimesh.util.attach_to_log() 31 | 32 | # mesh = trimesh.load('Geometry_Data/dental_gradient_ccs_single_low.stl') 33 | # 34 | # log.debug(" coil_mesh: %s", mesh) 35 | 36 | planar_height = 0.5 37 | planar_width = 0.75 38 | num_lateral_divisions = 4 39 | num_longitudinal_divisions = 4 40 | rotation_vector_x = 0.0 41 | rotation_vector_y = 0.0 42 | rotation_vector_z = 1.0 43 | rotation_angle = np.pi/8.0 44 | center_position_x = 0.0 45 | center_position_y = 0.0 46 | center_position_z = 0.0 47 | data_mesh = build_planar_mesh(planar_height, planar_width, num_lateral_divisions, num_longitudinal_divisions, 48 | rotation_vector_x, rotation_vector_y, rotation_vector_z, rotation_angle, 49 | center_position_x, center_position_y, center_position_z) 50 | coil_mesh = Mesh(vertices=data_mesh.vertices, faces=data_mesh.faces) 51 | 52 | ## print(coil_mesh.get_vertices()) 53 | #log.debug(" vertices: %s", coil_mesh.get_vertices()) 54 | # DEBUG (checking2.py: 44) shape vertices: (114, 3) 55 | log.debug(" shape vertices: %s", coil_mesh.get_vertices().shape) 56 | # DEBUG:__main__: shape faces: (182, 3) 57 | log.debug(" shape faces: %s", coil_mesh.get_faces().shape) 58 | log.debug(" faces min: %d, max: %s", np.min( 59 | coil_mesh.get_faces()), np.max(coil_mesh.get_faces())) 60 | 61 | parts = coil_mesh.separate_into_get_parts() 62 | log.debug("Parts: %d", len(parts)) 63 | 64 | # Access the Trimesh implementation 65 | mesh = coil_mesh.trimesh_obj 66 | 67 | # is the current mesh watertight? 68 | log.debug("mesh.is_watertight: %s", mesh.is_watertight) 69 | 70 | # what's the euler number for the mesh? 71 | log.debug("mesh.euler_number: %s", mesh.euler_number) 72 | 73 | #mesh.show() 74 | 75 | planar_height = 0.5 76 | planar_width = 0.75 77 | num_lateral_divisions = 4 78 | num_longitudinal_divisions = 4 79 | target_normal_x = 0.0 80 | target_normal_y = 0.0 81 | target_normal_z = 1.0 82 | center_position_x = 0.0 83 | center_position_y = 0.0 84 | center_position_z = 0.0 85 | plane_distance = 0.25 86 | data_mesh2 = build_biplanar_mesh(planar_height, planar_width, 87 | num_lateral_divisions, num_longitudinal_divisions, 88 | target_normal_x, target_normal_y, target_normal_z, 89 | center_position_x, center_position_y, center_position_z, 90 | plane_distance) 91 | coil_mesh2 = Mesh(vertices=data_mesh2.vertices, faces=data_mesh2.faces) 92 | 93 | #print("vertices = ", coil_mesh2.get_vertices()) 94 | #print("faces = ", coil_mesh2.get_faces()) 95 | print("faces shape: ", np.min(coil_mesh2.get_faces()), np.max(coil_mesh2.get_faces())) 96 | 97 | parts2 = coil_mesh2.separate_into_get_parts() 98 | log.debug("Parts: %d", len(parts2)) 99 | 100 | # Access the Trimesh implementation 101 | mesh2 = coil_mesh2.trimesh_obj 102 | 103 | # is the current mesh watertight? 104 | log.debug("mesh.is_watertight: %s", mesh2.is_watertight) 105 | 106 | # what's the euler number for the mesh? 107 | log.debug("mesh.euler_number: %s", mesh2.euler_number) 108 | 109 | mesh2.show() 110 | 111 | -------------------------------------------------------------------------------- /scratchpad/figures_for_docs.py: -------------------------------------------------------------------------------- 1 | """Utility class that generates figures for the documentation.""" 2 | 3 | #from pyCoilGen.mesh_factory import build_planar_mesh, build_biplanar_mesh, build_circular_mesh, build_cylinder_mesh 4 | import importlib 5 | 6 | from pyCoilGen.sub_functions.data_structures import Mesh 7 | 8 | for which in ['build_planar_mesh', 'build_biplanar_mesh', 'build_circular_mesh', 'build_cylinder_mesh']: 9 | module_name = f'pyCoilGen.mesh_factory.{which}' 10 | 11 | # Dynamically import the module 12 | module = importlib.import_module(module_name) 13 | 14 | # Call the builder with the default value 15 | built = getattr(module, which)(*getattr(module, '__default_value__')) 16 | mesh = Mesh(vertices=built.vertices, faces=built.faces) 17 | mesh.export(f'images/figures/{which}.stl') -------------------------------------------------------------------------------- /scratchpad/issue70.py: -------------------------------------------------------------------------------- 1 | # System imports 2 | import sys 3 | 4 | # Logging 5 | import logging 6 | 7 | # Debugging imports 8 | # Add the sub_functions directory to the Python module search path 9 | from pathlib import Path 10 | sub_functions_path = Path(__file__).resolve().parent / '..' 11 | sys.path.append(str(sub_functions_path)) 12 | 13 | # Local imports 14 | from pyCoilGen.pyCoilGen_release import pyCoilGen 15 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC, DEBUG_VERBOSE 16 | 17 | if __name__ == '__main__': 18 | # Set up logging 19 | log = logging.getLogger(__name__) 20 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 21 | # logging.basicConfig(level=logging.INFO) 22 | 23 | arg_dict = { 24 | 'field_shape_function': 'x', # definition of the target field 25 | 26 | # 'coil_mesh_file': 'bi_planer_rectangles_width_1000mm_distance_500mm.stl', 27 | 'coil_mesh': 'create bi-planar mesh', 28 | 'biplanar_mesh_parameter_list': [1.0, 1.0, # planar_height, planar_width of the planar mesh. 29 | 20, 20, # num_lateral_divisions, num_longitudinal_divisions 30 | 0.0, 1.0, 0.0, # target_normal_x, target_normal_y, target_normal_z 31 | 0, 0, 0, # center_position_x, center_position_y, center_position_z 32 | 0.5], # plane_distance (`float`): Distance between the two planes. 33 | 34 | 'target_mesh_file': 'none', 35 | 'secondary_target_mesh_file': 'none', 36 | 'secondary_target_weight': 0.5, 37 | 'target_region_radius': 0.1, # in meter 38 | # 'target_region_resolution': 10, # MATLAB 10 is the default 39 | 'use_only_target_mesh_verts': False, 40 | 'sf_source_file': 'none', 41 | # the number of potential steps that determines the later number of windings (Stream function discretization) 42 | 'levels': 14, 43 | # a potential offset value for the minimal and maximal contour potential ; must be between 0 and 1 44 | 'pot_offset_factor': 0.25, 45 | 'surface_is_cylinder_flag': True, 46 | # the width for the interconnections are interconnected; in meter 47 | 'interconnection_cut_width': 0.05, 48 | # the length for which overlapping return paths will be shifted along the surface normals; in meter 49 | 'normal_shift_length': 0.01, 50 | 'iteration_num_mesh_refinement': 0, # 1, # the number of refinements for the mesh; 51 | 'set_roi_into_mesh_center': True, 52 | 'force_cut_selection': ['high'], 53 | # Specify one of the three ways the level sets are calculated: "primary","combined", or "independent" 54 | 'level_set_method': 'primary', 55 | 'skip_postprocessing': False, 56 | 'skip_inductance_calculation': False, 57 | 'tikhonov_reg_factor': 10, # Tikhonov regularization factor for the SF optimization 58 | 59 | 'output_directory': 'images/issue70', # [Current directory] 60 | # 'project_name': 'issue70', 61 | 'project_name': 'biplanar_xgradient_Tj', 62 | 'persistence_dir': 'debug', 63 | 'debug': DEBUG_BASIC, 64 | } 65 | 66 | result = pyCoilGen(log, arg_dict) 67 | -------------------------------------------------------------------------------- /scratchpad/plotting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | from typing import List 4 | from os import makedirs 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | from pyCoilGen.pyCoilGen_release import pyCoilGen 9 | from pyCoilGen.sub_functions.constants import DEBUG_BASIC 10 | from pyCoilGen.sub_functions.data_structures import CoilSolution, SolutionErrors, FieldErrors, TargetField 11 | from pyCoilGen.helpers.persistence import load 12 | 13 | # Plotting 14 | import pyCoilGen.plotting as pcg_plt 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | # logging.basicConfig(level=logging.DEBUG) 19 | logging.basicConfig(level=logging.INFO) 20 | 21 | # Change the default values to suit your application 22 | arg_dict = { 23 | 'field_shape_function': 'y', # % definition of the target field ['x'] 24 | 'coil_mesh_file': 'cylinder_radius500mm_length1500mm.stl', 25 | 'secondary_target_weight': 0.5, # [1.0] 26 | 'target_region_resolution': 10, # MATLAB 10 is the default 27 | 'levels': 20, # The number of potential steps, determines the number of windings [10] 28 | # a potential offset value for the minimal and maximal contour potential [0.5] 29 | 'pot_offset_factor': 0.25, 30 | 'interconnection_cut_width': 0.1, # Width cut used when cutting and joining wire paths; in metres [0.01] 31 | # Displacement that overlapping return paths will be shifted along the surface normals; in meter [0.001] 32 | 'normal_shift_length': 0.025, 33 | 'iteration_num_mesh_refinement': 1, # % the number of refinements for the mesh; [0] 34 | 'set_roi_into_mesh_center': True, # [False] 35 | 'force_cut_selection': ['high'], # [] 36 | 'make_cylindrical_pcb': True, # [False] 37 | 'conductor_cross_section_width': 0.015, # [0.002] 38 | 'tikhonov_reg_factor': 100, # Tikhonov regularization factor for the SF optimization [1] 39 | 40 | 'output_directory': 'images', # [Current directory] 41 | 'project_name': 'ygradient_cylinder', 42 | 'fasthenry_bin': '../FastHenry2/bin/fasthenry', # [/usr/bin/fasthenry'] 43 | 'persistence_dir': 'debug', # [debug] 44 | # 'debug': DEBUG_VERBOSE, 45 | 'debug': DEBUG_BASIC, # [0 = NONE] 46 | } 47 | 48 | # solution = pyCoilGen(log, arg_dict) # Calculate the solution 49 | # which = arg_dict['project_name'] 50 | # Calculate the errors 51 | # [loaded] = oad('debug', which, 'final') 52 | # solution = load('debug', 'biplanar_xgradient', 'final') 53 | # solution = load('debug', 'Preoptimzed_SVD_Coil', 'final') 54 | # solution = load('debug', 'Preoptimzed_Breast_Coil', 'final') 55 | # solution = load('debug', 's2_shim_coil', 'final') 56 | solution = load('debug', 'shielded_ygradient_coil', 'final') 57 | # print(solution.input_args) 58 | which = solution.input_args.project_name 59 | save_dir = f'{solution.input_args.output_directory}' 60 | makedirs(save_dir, exist_ok=True) 61 | 62 | coil_solutions = [solution] 63 | # pcg_plt.plot_error_different_solutions(coil_solutions, [0], 'gradient study') 64 | pcg_plt.plot_various_error_metrics(coil_solutions, 0, f'{which}', save_dir=save_dir) 65 | pcg_plt.plot_2D_contours_with_sf(coil_solutions, 0, f'{which} 2D', save_dir=save_dir) 66 | pcg_plt.plot_3D_contours_with_sf(coil_solutions, 0, f'{which} 3D', save_dir=save_dir) 67 | 68 | # Plot vector fields 69 | coords = solution.target_field.coords 70 | 71 | plot_title=f'{which} Target Field ' 72 | field = solution.solution_errors.combined_field_layout 73 | pcg_plt.plot_vector_field_xy(coords, field, plot_title=plot_title, save_dir=save_dir) 74 | # pcg_plt.plot_vector_field_yz(coords, field, plot_title=plot_title, save_dir=save_dir) 75 | # pcg_plt.plot_vector_field_xz(coords, field, plot_title=plot_title, save_dir=save_dir) 76 | 77 | plot_title=f'{which} Target Field Error ' 78 | field = solution.solution_errors.combined_field_layout - solution.target_field.b 79 | pcg_plt.plot_vector_field_xy(coords, field, plot_title=plot_title, save_dir=save_dir) 80 | # pcg_plt.plot_vector_field_yz(coords, field, plot_title=plot_title, save_dir=save_dir) 81 | # pcg_plt.plot_vector_field_xz(coords, field, plot_title=plot_title, save_dir=save_dir) 82 | -------------------------------------------------------------------------------- /tests/regression_issue70.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Test support 4 | from pytest import approx 5 | 6 | # Local import 7 | from pyCoilGen.sub_functions.data_structures import DataStructure 8 | 9 | # Function under test 10 | from pyCoilGen.mesh_factory import load_mesh_factory_plugins 11 | 12 | 13 | def test_issue70(): 14 | 15 | planar_height = 0.75 16 | planar_width = 0.85 17 | num_lateral_divisions = 20 18 | num_longitudinal_divisions = 25 19 | target_normal_x = 0.0 20 | target_normal_y = 1.0 21 | target_normal_z = 0.0 22 | center_position_x = 0 23 | center_position_y = 0 24 | center_position_z = 0 25 | plane_distance = 0.5 26 | 27 | 28 | input_args = DataStructure(coil_mesh='create bi-planar mesh', 29 | biplanar_mesh_parameter_list=[planar_height, planar_width, 30 | num_lateral_divisions, num_longitudinal_divisions, 31 | target_normal_x, target_normal_y, target_normal_z, 32 | center_position_x, center_position_y, center_position_z, 33 | plane_distance], 34 | ) 35 | 36 | print(f"input_args.coil_mesh => {input_args.coil_mesh}") 37 | plugin_name = input_args.coil_mesh.replace(' ', '_').replace('-', '_') 38 | plugins = load_mesh_factory_plugins() 39 | found = False 40 | for plugin in plugins: 41 | mesh_creation_function = getattr(plugin, plugin_name, None) 42 | if mesh_creation_function: 43 | coil_mesh = mesh_creation_function(input_args) 44 | found = True 45 | break 46 | 47 | assert found 48 | 49 | # Min x is -0.5 width 50 | assert np.min(coil_mesh.v[:, 0]) == approx(-planar_width/2.0) 51 | # Max x is 0.5 width 52 | assert np.max(coil_mesh.v[:, 0]) == approx(planar_width/2.0) 53 | 54 | # Min y is -0.5 height 55 | assert np.min(coil_mesh.v[:, 1]) == approx(-plane_distance/2.0) 56 | # Max y is 0.5 height 57 | assert np.max(coil_mesh.v[:, 1]) == approx(plane_distance/2.0) 58 | 59 | # Min z is 0 60 | assert np.min(coil_mesh.v[:, 2]) == -planar_height/2.0 61 | # Max z is 0 62 | assert np.max(coil_mesh.v[:, 2]) == planar_height/2.0 63 | -------------------------------------------------------------------------------- /tests/test_biot_savart_calc_b.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pyCoilGen.sub_functions.data_structures import TargetField 4 | # Code under test 5 | from pyCoilGen.sub_functions.process_raw_loops import biot_savart_calc_b 6 | 7 | def test_biot_savart_calc_b_trivial(): 8 | wire_path = np.asarray(([-0.5, 0.5], [0.0, 0.0], [0.0, 0.0])).reshape(3,2) 9 | coords = np.asarray(([0.0], [1.0], [0.0])).reshape(3,1) 10 | target_field = TargetField(coords=coords) 11 | 12 | ################################################################################### 13 | # Function under test 14 | result = biot_savart_calc_b(wire_path, target_field) 15 | ################################################################################### 16 | expected = np.asarray(([0.000000e+00, 0.000000e+00, 1.000000e-07])) 17 | assert np.all(expected == result.T) 18 | 19 | def test_biot_savart_calc_b_arrays(): 20 | # Less than 1000 elements, wire_path is processed in one go 21 | elements = 501 # 500 segments 22 | span = np.arange(0.0, 1.0, 1.0/elements, dtype=np.float64) 23 | span /= np.max(span) # 0 -> 1.0 24 | span -= 0.5 # -0.5 -> 0.5 25 | zero_arr = np.zeros_like(span) 26 | wire_path = np.asarray((span, zero_arr, zero_arr)).reshape(3,-1) 27 | coords = np.asarray(([0.0], [1.0], [0.0])).reshape(3,-1) 28 | target_field = TargetField(coords=coords) 29 | 30 | ################################################################################### 31 | # Function under test 32 | result = biot_savart_calc_b(wire_path, target_field) 33 | ################################################################################### 34 | expected = np.asarray(([0.000000e+00, 0.000000e+00, 8.9442747608e-08])) 35 | assert np.allclose(expected, result.T) 36 | 37 | # More than 1000 elements, wire_path is split into portions 38 | elements = 1501 # 1500 segments 39 | span = np.arange(0.0, 1.0, 1.0/elements, dtype=np.float64) 40 | span /= np.max(span) # 0 -> 1.0 41 | span -= 0.5 # -0.5 -> 0.5 42 | zero_arr = np.zeros_like(span) 43 | wire_path = np.asarray((span, zero_arr, zero_arr)).reshape(3,-1) 44 | coords = np.asarray(([0.0], [1.0], [0.0])).reshape(3,-1) 45 | target_field = TargetField(coords=coords) 46 | 47 | ################################################################################### 48 | # Function under test 49 | result = biot_savart_calc_b(wire_path, target_field) 50 | ################################################################################### 51 | assert np.allclose(expected, result.T) 52 | 53 | 54 | def test_biot_savart_calc_b_arrays2(): 55 | # Split target field into 100 elements 56 | elements = 100 # 1500 segments 57 | span = np.arange(0.0, 1.0, 1.0/elements, dtype=np.float64) 58 | span /= np.max(span) # 0 -> 1.0 59 | result_span = span.copy() 60 | span -= 0.5 # -0.5 -> 0.5 61 | zero_arr = np.zeros_like(span) 62 | wire_path = np.asarray((span, zero_arr, zero_arr)).reshape(3,-1) 63 | coords = np.asarray((zero_arr, span, zero_arr)).reshape(3,-1) 64 | target_field = TargetField(coords=coords) 65 | 66 | ################################################################################### 67 | # Function under test 68 | result = biot_savart_calc_b(wire_path, target_field) 69 | ################################################################################### 70 | 71 | assert np.allclose(-2.8284631974e-07, result[2,0]) # First 72 | assert np.allclose(4.8554163453e-05, result[2,50]) # Middle 73 | assert np.allclose(2.9160871578e-07, result[2,elements-1]) # Last 74 | 75 | 76 | 77 | 78 | if __name__ == "__main__": 79 | import logging 80 | # Set up logging 81 | log = logging.getLogger(__name__) 82 | logging.basicConfig(level=logging.DEBUG) 83 | 84 | 85 | # Input: 86 | # wire_path: -0.5, 0.5 87 | # field: 0.0, 1.0, 0.0 88 | # current: 1000.0 89 | # Result: 0.000000e+00, 0.000000e+00, 1.000000e-04 -------------------------------------------------------------------------------- /tests/test_calc_3d_rotation_matrix_by_vector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Code under test 4 | from pyCoilGen.sub_functions.calc_3d_rotation_matrix_by_vector import calc_3d_rotation_matrix_by_vector 5 | 6 | def test_calc_3d_rotation_matrix_by_vector_basic(): 7 | vec = [0,0,1] 8 | angle = 0 9 | rot_mat = calc_3d_rotation_matrix_by_vector(vec, angle) 10 | assert np.max(rot_mat) == 1.0 11 | assert np.min(rot_mat) == 0.0 12 | 13 | # No effect 14 | input = [1,1,1] 15 | rot = np.dot([input], rot_mat) 16 | assert rot[0].tolist() == input 17 | 18 | # Rotate 90 degrees about Z changes [1,0,0] to [0,1,0] 19 | def test_calc_3d_rotation_matrix_by_vector_about_z(): 20 | vec = [0,0,1] 21 | angle = np.pi/2.0 22 | rot_mat = calc_3d_rotation_matrix_by_vector(vec, angle) 23 | assert np.max(rot_mat) == 1.0 24 | assert np.min(rot_mat) == -1.0 25 | 26 | input = [1,0,0] 27 | rot = np.dot([input, input, input], rot_mat) 28 | assert np.allclose(rot[0], [0,1,0]) 29 | assert np.allclose(rot[1], [0,1,0]) 30 | 31 | # No change 32 | input = vec 33 | rot = np.dot([input, input, input], rot_mat) 34 | assert np.allclose(rot[0], input) 35 | 36 | # Rotate 90 degrees about X changes [0,0,1] to [0,-1,0] 37 | def test_calc_3d_rotation_matrix_by_vector_about_x(): 38 | vec = [1,0,0] 39 | angle = np.pi/2.0 40 | rot_mat = calc_3d_rotation_matrix_by_vector(vec, angle) 41 | assert np.max(rot_mat) == 1.0 42 | assert np.min(rot_mat) == -1.0 43 | 44 | input = [0,0,1] 45 | rot = np.dot([input, input, input], rot_mat) 46 | assert np.allclose(rot[0], [0,-1,0]) 47 | assert np.allclose(rot[1], [0,-1,0]) 48 | 49 | # No change 50 | input = vec 51 | rot = np.dot([input, input, input], rot_mat) 52 | assert np.allclose(rot[0], input) 53 | 54 | # Rotate 90 degrees about Y changes [1,0,0] to [0,0,-1] 55 | def test_calc_3d_rotation_matrix_by_vector_about_y(): 56 | vec = [0,1,0] 57 | angle = np.pi/2.0 58 | rot_mat = calc_3d_rotation_matrix_by_vector(vec, angle) 59 | assert np.max(rot_mat) == 1.0 60 | assert np.min(rot_mat) == -1.0 61 | 62 | input = [1,0,0] 63 | rot = np.dot([input, input, input], rot_mat) 64 | assert np.allclose(rot[0], [0,0,-1]) 65 | assert np.allclose(rot[1], [0,0,-1]) 66 | 67 | # No change 68 | input = vec 69 | rot = np.dot([input, input, input], rot_mat) 70 | assert np.allclose(rot[0], input) -------------------------------------------------------------------------------- /tests/test_data/biplanar_mesh.json: -------------------------------------------------------------------------------- 1 | {"vertices": [[-1.5, -1.0, 0.25], [-1.5, -0.6, 0.25], [-1.5, -0.19999999999999996, 0.25], [-1.5, 0.20000000000000018, 0.25], [-1.5, 0.6000000000000001, 0.25], [-1.5, 1.0, 0.25], [-0.75, -1.0, 0.25], [-0.75, -0.6, 0.25], [-0.75, -0.19999999999999996, 0.25], [-0.75, 0.20000000000000018, 0.25], [-0.75, 0.6000000000000001, 0.25], [-0.75, 1.0, 0.25], [0.0, -1.0, 0.25], [0.0, -0.6, 0.25], [0.0, -0.19999999999999996, 0.25], [0.0, 0.20000000000000018, 0.25], [0.0, 0.6000000000000001, 0.25], [0.0, 1.0, 0.25], [0.75, -1.0, 0.25], [0.75, -0.6, 0.25], [0.75, -0.19999999999999996, 0.25], [0.75, 0.20000000000000018, 0.25], [0.75, 0.6000000000000001, 0.25], [0.75, 1.0, 0.25], [1.5, -1.0, 0.25], [1.5, -0.6, 0.25], [1.5, -0.19999999999999996, 0.25], [1.5, 0.20000000000000018, 0.25], [1.5, 0.6000000000000001, 0.25], [1.5, 1.0, 0.25], [-1.5, -1.0, -0.25], [-1.5, -0.6, -0.25], [-1.5, -0.19999999999999996, -0.25], [-1.5, 0.20000000000000018, -0.25], [-1.5, 0.6000000000000001, -0.25], [-1.5, 1.0, -0.25], [-0.75, -1.0, -0.25], [-0.75, -0.6, -0.25], [-0.75, -0.19999999999999996, -0.25], [-0.75, 0.20000000000000018, -0.25], [-0.75, 0.6000000000000001, -0.25], [-0.75, 1.0, -0.25], [0.0, -1.0, -0.25], [0.0, -0.6, -0.25], [0.0, -0.19999999999999996, -0.25], [0.0, 0.20000000000000018, -0.25], [0.0, 0.6000000000000001, -0.25], [0.0, 1.0, -0.25], [0.75, -1.0, -0.25], [0.75, -0.6, -0.25], [0.75, -0.19999999999999996, -0.25], [0.75, 0.20000000000000018, -0.25], [0.75, 0.6000000000000001, -0.25], [0.75, 1.0, -0.25], [1.5, -1.0, -0.25], [1.5, -0.6, -0.25], [1.5, -0.19999999999999996, -0.25], [1.5, 0.20000000000000018, -0.25], [1.5, 0.6000000000000001, -0.25], [1.5, 1.0, -0.25]], "faces": [[0, 6, 7], [0, 7, 1], [1, 7, 8], [1, 8, 2], [2, 8, 9], [2, 9, 3], [3, 9, 10], [3, 10, 4], [4, 10, 11], [4, 11, 5], [6, 12, 13], [6, 13, 7], [7, 13, 14], [7, 14, 8], [8, 14, 15], [8, 15, 9], [9, 15, 16], [9, 16, 10], [10, 16, 17], [10, 17, 11], [12, 18, 19], [12, 19, 13], [13, 19, 20], [13, 20, 14], [14, 20, 21], [14, 21, 15], [15, 21, 22], [15, 22, 16], [16, 22, 23], [16, 23, 17], [18, 24, 25], [18, 25, 19], [19, 25, 26], [19, 26, 20], [20, 26, 27], [20, 27, 21], [21, 27, 28], [21, 28, 22], [22, 28, 29], [22, 29, 23], [30, 36, 37], [30, 37, 31], [31, 37, 38], [31, 38, 32], [32, 38, 39], [32, 39, 33], [33, 39, 40], [33, 40, 34], [34, 40, 41], [34, 41, 35], [36, 42, 43], [36, 43, 37], [37, 43, 44], [37, 44, 38], [38, 44, 45], [38, 45, 39], [39, 45, 46], [39, 46, 40], [40, 46, 47], [40, 47, 41], [42, 48, 49], [42, 49, 43], [43, 49, 50], [43, 50, 44], [44, 50, 51], [44, 51, 45], [45, 51, 52], [45, 52, 46], [46, 52, 53], [46, 53, 47], [48, 54, 55], [48, 55, 49], [49, 55, 56], [49, 56, 50], [50, 56, 57], [50, 57, 51], [51, 57, 58], [51, 58, 52], [52, 58, 59], [52, 59, 53]], "normal": [0.0, 0.0, 1.0]} -------------------------------------------------------------------------------- /tests/test_data/planar_mesh.json: -------------------------------------------------------------------------------- 1 | {"vertices": [[-1.5, -1.0, 0.0], [-1.5, -0.6, 0.0], [-1.5, -0.19999999999999996, 0.0], [-1.5, 0.20000000000000018, 0.0], [-1.5, 0.6000000000000001, 0.0], [-1.5, 1.0, 0.0], [-0.75, -1.0, 0.0], [-0.75, -0.6, 0.0], [-0.75, -0.19999999999999996, 0.0], [-0.75, 0.20000000000000018, 0.0], [-0.75, 0.6000000000000001, 0.0], [-0.75, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, -0.6, 0.0], [0.0, -0.19999999999999996, 0.0], [0.0, 0.20000000000000018, 0.0], [0.0, 0.6000000000000001, 0.0], [0.0, 1.0, 0.0], [0.75, -1.0, 0.0], [0.75, -0.6, 0.0], [0.75, -0.19999999999999996, 0.0], [0.75, 0.20000000000000018, 0.0], [0.75, 0.6000000000000001, 0.0], [0.75, 1.0, 0.0], [1.5, -1.0, 0.0], [1.5, -0.6, 0.0], [1.5, -0.19999999999999996, 0.0], [1.5, 0.20000000000000018, 0.0], [1.5, 0.6000000000000001, 0.0], [1.5, 1.0, 0.0]], "faces": [[0, 6, 7], [0, 7, 1], [1, 7, 8], [1, 8, 2], [2, 8, 9], [2, 9, 3], [3, 9, 10], [3, 10, 4], [4, 10, 11], [4, 11, 5], [6, 12, 13], [6, 13, 7], [7, 13, 14], [7, 14, 8], [8, 14, 15], [8, 15, 9], [9, 15, 16], [9, 16, 10], [10, 16, 17], [10, 17, 11], [12, 18, 19], [12, 19, 13], [13, 19, 20], [13, 20, 14], [14, 20, 21], [14, 21, 15], [15, 21, 22], [15, 22, 16], [16, 22, 23], [16, 23, 17], [18, 24, 25], [18, 25, 19], [19, 25, 26], [19, 26, 20], [20, 26, 27], [20, 27, 21], [21, 27, 28], [21, 28, 22], [22, 28, 29], [22, 29, 23]], "normal": [0.0, 0.0, 1.0]} -------------------------------------------------------------------------------- /tests/test_data/point_locations3.json: -------------------------------------------------------------------------------- 1 | {"point_ind": 3, "point": [-0.5149712016256258, 0.13824082824975878], "fail": 236, "pass": 237, "face_indices": [236, 237, 238, 239], "face_vertices": [[139, 141, 135], [138, 135, 141], [138, 141, 140], [142, 140, 141]], "vertices": [[[-0.4570580318325984, 0.24114001871774302], [-0.506525766592459, 0.11753700144732003], [-0.587305484601808, 0.3155669767235923]], [[-0.6515657267318129, 0.1557855575848038], [-0.587305484601808, 0.3155669767235923], [-0.506525766592459, 0.11753700144732003]], [[-0.6515657267318129, 0.1557855575848038], [-0.506525766592459, 0.11753700144732003], [-0.6725290826494086, -0.014761420261274348]], [[-0.5225492934340364, -0.01428059016381603], [-0.6725290826494086, -0.014761420261274348], [-0.506525766592459, 0.11753700144732003]]]} -------------------------------------------------------------------------------- /tests/test_data/point_locations4.json: -------------------------------------------------------------------------------- 1 | {"point_ind": 4, "point": [-0.47067526679831745, 0.2489212793366758], "fail": 233, "pass": 236, "face_indices": [232, 233, 236], "face_vertices": [[136, 139, 131], [135, 131, 139], [139, 141, 135]], "vertices": [[[-0.3771841072673602, 0.3481798593894972], [-0.4570580318325984, 0.24114001871774302], [-0.4837553424770434, 0.4537653406415379]], [[-0.587305484601808, 0.3155669767235923], [-0.4837553424770434, 0.4537653406415379], [-0.4570580318325984, 0.24114001871774302]], [[-0.4570580318325984, 0.24114001871774302], [-0.506525766592459, 0.11753700144732003], [-0.587305484601808, 0.3155669767235923]]]} -------------------------------------------------------------------------------- /tests/test_data/point_locations5.json: -------------------------------------------------------------------------------- 1 | {"point_ind": 5, "point": [-0.4604714624786703, 0.2683255955592293], "fail": 232, "pass": 233, "face_indices": [232, 233, 236], "face_vertices": [[136, 139, 131], [135, 131, 139], [139, 141, 135]], "vertices": [[[-0.3771841072673602, 0.3481798593894972], [-0.4570580318325984, 0.24114001871774302], [-0.4837553424770434, 0.4537653406415379]], [[-0.587305484601808, 0.3155669767235923], [-0.4837553424770434, 0.4537653406415379], [-0.4570580318325984, 0.24114001871774302]], [[-0.4570580318325984, 0.24114001871774302], [-0.506525766592459, 0.11753700144732003], [-0.587305484601808, 0.3155669767235923]]]} -------------------------------------------------------------------------------- /tests/test_data/test_add_nearest_ref_point_to_curve1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/tests/test_data/test_add_nearest_ref_point_to_curve1.npy -------------------------------------------------------------------------------- /tests/test_data/test_open_loop_with_3d_sphere1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/tests/test_data/test_open_loop_with_3d_sphere1.npy -------------------------------------------------------------------------------- /tests/test_data/test_remove_points_from_loop1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kev-m/pyCoilGen/7f87054b5f2bfff274aabddce91b59a15ee1457c/tests/test_data/test_remove_points_from_loop1.npy -------------------------------------------------------------------------------- /tests/test_gauss_legendre_integration_points_triangle.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | # Test support 4 | from pytest import approx 5 | # Code under test 6 | from pyCoilGen.sub_functions.gauss_legendre_integration_points_triangle import gauss_legendre_integration_points_triangle, calc_weights_gauss 7 | 8 | 9 | def test_calc_weights_gauss(): 10 | n = 2 11 | eta, w = calc_weights_gauss(n) 12 | assert np.isnan(w).any() == False 13 | assert np.isnan(eta).any() == False 14 | assert w.shape == (2,) 15 | assert w[0] == approx(1.00) 16 | assert w[1] == approx(1.00) 17 | assert eta.shape == (2,) 18 | assert eta[0] == approx(-0.57735) 19 | assert eta[1] == approx(0.57735) 20 | 21 | 22 | def test_gauss_legendre_integration_points_triangle(): 23 | n = 2 24 | u, v, ck = gauss_legendre_integration_points_triangle(n) 25 | assert np.isnan(u).any() == False 26 | assert np.isnan(v).any() == False 27 | assert np.isnan(ck).any() == False 28 | 29 | assert u.shape == (4, 1) 30 | assert np.allclose(u, [[0.2113], [0.2113], [0.7887], [0.7887]], atol=0.0001) 31 | 32 | assert v.shape == (4, 1) 33 | assert np.allclose(v, [[0.1667], [0.622], [0.0447], [0.1667]], atol=0.0001) 34 | 35 | assert ck.shape == (4, 1) 36 | assert np.allclose(ck, [[0.1972], [0.1972], [0.0528], [0.0528]], atol=0.0001) 37 | -------------------------------------------------------------------------------- /tests/test_mesh_factory.py: -------------------------------------------------------------------------------- 1 | # Local import 2 | from pyCoilGen.sub_functions.data_structures import DataStructure 3 | 4 | # Function under test 5 | from pyCoilGen.mesh_factory import load_mesh_factory_plugins 6 | 7 | 8 | def test_build_planar_mesh(): 9 | input_args = DataStructure(planar_mesh_parameter_list=[0.25, 0.25, 20, 20, 1, 0, 0, 0, 0, 0, 0]) 10 | plugin_name = 'create planar mesh'.replace(' ', '_').replace('-', '_') 11 | plugins = load_mesh_factory_plugins() 12 | found = False 13 | for plugin in plugins: 14 | mesh_creation_function = getattr(plugin, plugin_name, None) 15 | if mesh_creation_function: 16 | coil_mesh = mesh_creation_function(input_args) 17 | found = True 18 | break 19 | 20 | assert found 21 | -------------------------------------------------------------------------------- /tests/test_process_raw_loops.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy as np 3 | 4 | # Hack code 5 | # Set up paths: Add the project root directory to the Python path 6 | import sys 7 | import os 8 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 9 | 10 | 11 | # Test support 12 | from pyCoilGen.helpers.extraction import load_matlab 13 | from pyCoilGen.sub_functions.data_structures import Shape3D, DataStructure 14 | from pyCoilGen.helpers.visualisation import compare 15 | 16 | 17 | # Code under test 18 | from pyCoilGen.sub_functions.process_raw_loops import process_raw_loops 19 | 20 | 21 | if __name__ == "__main__": 22 | import logging 23 | # Set up logging 24 | log = logging.getLogger(__name__) 25 | logging.basicConfig(level=logging.DEBUG) 26 | 27 | #make_data('debug/ygradient_coil') 28 | #test_add_nearest_ref_point_to_curve() 29 | #test_open_loop_with_3d_sphere() 30 | #brute_test_process_raw_loops_brute() 31 | -------------------------------------------------------------------------------- /tests/test_read_mesh.py: -------------------------------------------------------------------------------- 1 | from pyCoilGen.sub_functions.read_mesh import read_mesh 2 | from pyCoilGen.sub_functions.data_structures import DataStructure 3 | 4 | 5 | def test_help(): 6 | input_args = DataStructure(coil_mesh='help') 7 | coil, target, shield = read_mesh(input_args) 8 | 9 | assert coil is None 10 | 11 | 12 | def test_coil_planar(): 13 | input_args = DataStructure(coil_mesh='create planar mesh', 14 | planar_mesh_parameter_list=[0.25, 0.25, 20, 20, 1, 0, 0, 0, 0, 0, 0.2], 15 | target_mesh='none', target_mesh_file='none', 16 | shield_mesh='none', secondary_target_mesh_file='none') 17 | coil, target, shield = read_mesh(input_args) 18 | 19 | assert coil is not None 20 | assert target is None 21 | assert shield is None 22 | 23 | def test_target_planar(): 24 | input_args = DataStructure(coil_mesh='create cylinder mesh', 25 | cylinder_mesh_parameter_list=[0.8, 0.3, 20, 20, 1, 0, 0, 0], 26 | target_mesh='create planar mesh', target_mesh_file='none', 27 | planar_mesh_parameter_list=[0.25, 0.25, 20, 20, 1, 0, 0, 0, 0, 0, 0.2], 28 | shield_mesh='none', secondary_target_mesh_file='none') 29 | coil, target, shield = read_mesh(input_args) 30 | 31 | assert coil is not None 32 | assert target is not None 33 | assert shield is None 34 | 35 | def test_target_circular(): 36 | input_args = DataStructure(coil_mesh='create bi-planar mesh', 37 | biplanar_mesh_parameter_list=[0.25, 0.25, 20, 20, 1, 0, 0, 0, 0, 0, 0.2], 38 | target_mesh='create circular mesh', target_mesh_file='none', 39 | circular_mesh_parameter_list=[0.25, 20, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,], 40 | shield_mesh='create planar mesh', secondary_target_mesh_file='none', 41 | planar_mesh_parameter_list=[0.25, 0.25, 20, 20, 1, 0, 0, 0, 0, 0, 0.2], 42 | ) 43 | coil, target, shield = read_mesh(input_args) 44 | 45 | assert coil is not None 46 | assert target is not None 47 | assert shield is not None 48 | 49 | 50 | if __name__ == '__main__': 51 | test_target_circular() 52 | -------------------------------------------------------------------------------- /tests/test_remove_points_from_loop.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy as np 3 | 4 | # Test support 5 | from pyCoilGen.helpers.extraction import load_matlab 6 | from pyCoilGen.sub_functions.data_structures import Shape3D, DataStructure 7 | from pyCoilGen.helpers.visualisation import compare 8 | 9 | # Code under test 10 | from pyCoilGen.sub_functions.remove_points_from_loop import remove_points_from_loop 11 | 12 | def test_remove_points_from_loop(): 13 | result = np.load('tests/test_data/test_remove_points_from_loop1.npy', allow_pickle=True)[0] 14 | 15 | for index1, level in enumerate(result['levels']): 16 | for index2, connection in enumerate(level['connections']): 17 | #log.debug(" Level: %d, connection: %d", index1, index2) 18 | inputs = connection['inputs'] 19 | outputs = connection['outputs'] 20 | 21 | loop = Shape3D(inputs.loop.uv, inputs.loop.v) 22 | points_to_remove = inputs.points_to_remove 23 | boundary_threshold = inputs.boundary_threshold 24 | 25 | # Function under test 26 | loop_out_uv, loop_out_v = remove_points_from_loop(loop, points_to_remove, boundary_threshold) 27 | 28 | assert compare(np.array(loop_out_uv), outputs.loop_out_uv) 29 | assert compare(np.array(loop_out_v), outputs.loop_out_v) 30 | #log.debug(" Result: %s", compare(np.array(loop_out_uv), outputs.loop_out_uv)) 31 | 32 | 33 | def make_data(filename): 34 | mat_data = load_matlab(filename) 35 | coil_parts = mat_data['coil_layouts'].out.coil_parts 36 | top_debug = coil_parts.interconnect_among_groups.level_debug 37 | 38 | # Test data for remove_points_from_loop 39 | result = {'levels' : []} 40 | for index1, level_debug in enumerate(top_debug.connections): 41 | level_entry = {'connections' : []} 42 | for index2, remove_debug in enumerate(level_debug.remove_points_debug): 43 | connection_entry = {} 44 | connection_entry['inputs'] = remove_debug.inputs 45 | connection_entry['outputs'] = remove_debug.outputs 46 | level_entry['connections'].append(connection_entry) 47 | result['levels'].append(level_entry) 48 | np.save('tests/test_data/test_remove_points_from_loop1.npy', [result]) 49 | 50 | if __name__ == "__main__": 51 | import logging 52 | # Set up logging 53 | log = logging.getLogger(__name__) 54 | logging.basicConfig(level=logging.DEBUG) 55 | 56 | # make_data('debug/ygradient_coil') 57 | test_remove_points_from_loop() 58 | -------------------------------------------------------------------------------- /tests/test_save_preoptimised_data.py: -------------------------------------------------------------------------------- 1 | #system imports 2 | import numpy as np 3 | from os import makedirs, path 4 | 5 | # Test support 6 | from pyCoilGen.sub_functions.data_structures import DataStructure, Mesh, CoilPart 7 | from pyCoilGen.mesh_factory.build_biplanar_mesh import build_biplanar_mesh 8 | from pyCoilGen.sub_functions.parameterize_mesh import parameterize_mesh 9 | from pyCoilGen.sub_functions.split_disconnected_mesh import split_disconnected_mesh 10 | from pyCoilGen.sub_functions.load_preoptimized_data import load_preoptimized_data 11 | from pyCoilGen.helpers.visualisation import compare 12 | 13 | # Code under test 14 | from pyCoilGen.helpers.persistence import save_preoptimised_data 15 | 16 | 17 | def test_save_preoptimised_data(): 18 | combined_mesh = build_biplanar_mesh(0.5, 0.5, 3, 3, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.2) 19 | parts = split_disconnected_mesh(Mesh(vertices=combined_mesh.vertices, faces=combined_mesh.faces)) 20 | 21 | # Depends on the following properties of the CoilSolution: 22 | # - target_field.coords, target_field.b 23 | # - coil_parts[n].stream_function 24 | # - combined_mesh.vertices, combined_mesh.faces 25 | # - input_args.sf_dest_file 26 | 27 | fake_target_field = DataStructure(coords=np.ones((10, 3)), b=np.ones((10, 3))) 28 | fake_input_args = DataStructure(sf_dest_file='test_save_preoptimised_data') 29 | fake_solution = DataStructure(combined_mesh=combined_mesh, target_field=fake_target_field, 30 | coil_parts=parts, input_args=fake_input_args) 31 | 32 | # Fake up a stream function 33 | for coil_part in parts: 34 | coil_part.stream_function = np.ones((len(coil_part.coil_mesh.get_vertices()))) 35 | 36 | ################################################## 37 | # Function under test 38 | filename1 = save_preoptimised_data(fake_solution, 'debug') # Save to default directory 39 | ################################################## 40 | 41 | assert 'debug' in filename1 42 | 43 | # Simplify test, use load_preoptimized_data to cross check 44 | crosscheck_input_args = DataStructure(sf_source_file=fake_input_args.sf_dest_file, 45 | surface_is_cylinder_flag=True, circular_diameter_factor=1.0, debug=0) 46 | solution = load_preoptimized_data(crosscheck_input_args, 'debug') 47 | 48 | assert len(solution.coil_parts) == len(parts) 49 | assert compare(solution.combined_mesh.vertices, combined_mesh.vertices) 50 | # assert compare(solution.combined_mesh.faces, combined_mesh.faces) # Faces are in a different order 51 | for index, coil_part in enumerate(solution.coil_parts): 52 | t_part : CoilPart = parts[index] 53 | t_mesh : Mesh = t_part.coil_mesh 54 | 55 | # Verify the Mesh 56 | assert compare(t_mesh.get_vertices(), coil_part.coil_mesh.get_vertices()) 57 | assert compare(t_mesh.get_faces(), coil_part.coil_mesh.get_faces()) 58 | 59 | # Verify the stream_function 60 | assert compare(t_part.stream_function, coil_part.stream_function) 61 | 62 | # Verify the target_field (coords, b) 63 | target_field = solution.target_field 64 | assert compare(fake_target_field.coords, target_field.coords) 65 | assert compare(fake_target_field.b, target_field.b) 66 | 67 | # Test case 2: Writing to user-specified directory 68 | save_dir = path.join('debug', 'test') 69 | makedirs(save_dir, exist_ok=True) 70 | fake_solution.input_args.sf_dest_file=path.join(save_dir, 'test_save_preoptimised_data') 71 | 72 | ################################################## 73 | # Function under test 74 | filename2 = save_preoptimised_data(fake_solution) # Override default directory 75 | ################################################## 76 | assert 'Pre_Optimized_Solutions' not in filename2 77 | assert path.exists(filename2) 78 | assert filename2.startswith(save_dir) 79 | 80 | crosscheck_input_args.sf_source_file = fake_solution.input_args.sf_dest_file 81 | ################################################## 82 | # Function under test 83 | solution2 = load_preoptimized_data(crosscheck_input_args) # Override default directory 84 | ################################################## 85 | 86 | 87 | if __name__ == "__main__": 88 | import logging 89 | # Set up logging 90 | log = logging.getLogger(__name__) 91 | logging.basicConfig(level=logging.DEBUG) 92 | 93 | test_save_preoptimised_data() 94 | -------------------------------------------------------------------------------- /tests/test_smooth_track_by_folding.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Code under test 4 | from pyCoilGen.sub_functions.smooth_track_by_folding import smooth_track_by_folding 5 | 6 | 7 | def generate_triangular_waveform(cycles, magnitude, wavelength): 8 | """ 9 | Generates a 2xM triangular waveform. 10 | 11 | Args: 12 | cycles (int): The number of repeats. 13 | magnitude (float): The magnitude of the waveform. 14 | wavelength (int): The wavelength of the waveform. 15 | 16 | Returns: 17 | numpy.ndarray: A 2xM array where index 0 represents X-values and index 1 represents Y-values. 18 | """ 19 | 20 | length = cycles*wavelength 21 | 22 | # Generate X-values 23 | x_values = np.linspace(0, length, length) 24 | 25 | # Generate Y-values (triangular waveform) 26 | y_values = np.concatenate((np.linspace(0, magnitude, wavelength // 2), 27 | np.linspace(magnitude, 0, wavelength // 2))) 28 | 29 | # Repeat the waveform to match the desired length 30 | y_values = np.tile(y_values, cycles) 31 | 32 | # Trim excess points if necessary 33 | x_values = x_values[:length] 34 | y_values = y_values[:length] 35 | 36 | return np.array([x_values, y_values]) 37 | 38 | 39 | def test_smooth_track_by_folding(): 40 | input_data = generate_triangular_waveform(2, 1.0, 10) 41 | smoothing_length = 2 42 | ########################################################## 43 | # Function under test 44 | output_data = smooth_track_by_folding(input_data, smoothing_length=smoothing_length) 45 | ########################################################## 46 | 47 | assert input_data.shape == output_data.shape 48 | assert output_data[0, 0] == input_data[0, 1]/(2*smoothing_length-1) 49 | assert output_data[1, 0] == input_data[1, 1]/(2*smoothing_length-1) 50 | assert output_data[0, 1] == input_data[0, 1] 51 | assert output_data[1, 1] == input_data[1, 1] 52 | 53 | test_smooth_track_by_folding() -------------------------------------------------------------------------------- /tests/test_symbolic_calculation_of_gradient.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pyCoilGen.sub_functions.data_structures import DataStructure, TargetField 4 | # Code under test 5 | from pyCoilGen.sub_functions.define_target_field import symbolic_calculation_of_gradient 6 | 7 | 8 | def test_symbolic_calculation_of_gradient(): 9 | input_args = DataStructure(debug=1, field_shape_function='x + y**2') 10 | target_field = np.full((3, 10), 3) 11 | result = symbolic_calculation_of_gradient(input_args=input_args, target_field=target_field) 12 | 13 | # TODO: test the result 14 | 15 | # Define the target field shape 16 | target_points = target_field 17 | def field_func(x, y, z): return eval(input_args.field_shape_function) 18 | target_field3 = np.zeros_like(target_points) 19 | target_field3[2, :] = field_func(target_points[0, :], target_points[1, :], target_points[2, :]) 20 | 21 | # TODO: test the result 22 | -------------------------------------------------------------------------------- /tests/test_visualisation.py: -------------------------------------------------------------------------------- 1 | ############################ 2 | # TEST DEBUG: Remove when test works 3 | import logging 4 | 5 | # Local imports 6 | import sys 7 | from pathlib import Path 8 | # Add the sub_functions directory to the Python module search path 9 | sub_functions_path = Path(__file__).resolve().parent / '..' 10 | sys.path.append(str(sub_functions_path)) 11 | # 12 | ############################ 13 | 14 | import numpy as np 15 | 16 | # Code under test 17 | import pyCoilGen.helpers.visualisation as vs 18 | 19 | def test_compare_1d(): 20 | val1 = np.array([0.1]) 21 | val2 = np.array([0.1]) 22 | assert vs.compare(val1, val2) 23 | 24 | val2 = np.array([0.11]) 25 | assert vs.compare(val1, val2) == False 26 | assert vs.compare(val1, val2, 0.01) 27 | 28 | def test_compare_2d(): 29 | val1 = np.array([0.1, 0.2]) 30 | val2 = np.array([0.1, 0.2]) 31 | assert vs.compare(val1, val2) 32 | 33 | val2 = np.array([0.11, 0.2]) 34 | assert vs.compare(val1, val2) == False 35 | assert vs.compare(val1, val2, 0.01) 36 | 37 | val2 = np.array([0.11, 0.21]) 38 | assert vs.compare(val1, val2) == False 39 | assert vs.compare(val1, val2, 0.01) 40 | 41 | 42 | def test_compare_contains_1d(): 43 | # Trivial 44 | val1 = np.array([0.1, 0.2, 0.3]) 45 | val2 = np.array([0.1, 0.2, 0.3]) 46 | assert vs.compare_contains(val1, val2) 47 | 48 | val2 = np.array([0.11, 0.2, 0.3]) 49 | assert vs.compare_contains(val1, val2) == False 50 | #assert vs.compare_contains(val1, val2, 0.01) 51 | 52 | # Order does not matter when both are 1D 53 | val1 = np.array([0.1, 0.2, 0.3]) 54 | val2 = np.array([0.3, 0.2, 0.1]) 55 | assert vs.compare_contains(val1, val2) 56 | 57 | 58 | def test_compare_contains_2d(): 59 | # Trivial 60 | val1 = np.array([[0.1, 0.2], [0.2, 0.3]]) 61 | val2 = np.array([[0.1, 0.2], [0.2, 0.3]]) 62 | assert vs.compare_contains(val1, val2) 63 | 64 | # Order matters when both are 2D 65 | val1 = np.array([[0.2, 0.3], [0.1, 0.2]]) 66 | val2 = np.array([[0.2, 0.1], [0.2, 0.3]]) 67 | assert vs.compare_contains(val1, val2) == False 68 | val2 = np.array([[0.1, 0.2], [0.3, 0.2]]) 69 | assert vs.compare_contains(val1, val2) == False 70 | 71 | 72 | # Order reversed 73 | val1 = np.array([[0.2, 0.3], [0.1, 0.2]]) 74 | val2 = np.array([[0.1, 0.2], [0.2, 0.3]]) 75 | assert vs.compare_contains(val1, val2) 76 | 77 | val2 = np.array([[0.11, 0.21], [0.21, 0.31]]) 78 | assert vs.compare_contains(val1, val2) == False 79 | assert vs.compare_contains(val1, val2, 0.01) 80 | 81 | 82 | if __name__ == "__main__": 83 | # Set up logging 84 | log = logging.getLogger(__name__) 85 | logging.basicConfig(level=logging.DEBUG) 86 | # logging.basicConfig(level=logging.INFO) 87 | 88 | test_compare_1d() 89 | test_compare_contains_1d() 90 | test_compare_contains_2d() 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /utilities/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.1 (2023-09-28) 4 | 5 | #### New Features 6 | 7 | * First release. 8 | -------------------------------------------------------------------------------- /utilities/README.md: -------------------------------------------------------------------------------- 1 | # pyCoilGen Utilities 2 | 3 | [pyCoilGen](https://github.com/kev-m/pyCoilGen) is an application for generating coil layouts within the MRI/NMR environment. 4 | 5 | This package provides optional extra utilities that users may find useful: 6 | - stl_asc2bin: Convert [ASCII STL](https://en.wikipedia.org/wiki/STL_(file_format)#ASCII) files to binary. 7 | 8 | ## Installation 9 | 10 | Install **pyCoilGen Utilities** using pip: 11 | ```bash 12 | $ pip install pycoilgen_utils 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### stl_asc2bin 18 | ```bash 19 | usage: stl_asc2bin [-h] input_file output_file 20 | 21 | Convert ASCII STL to Binary STL 22 | 23 | positional arguments: 24 | input_file Path to the input ASCII STL file 25 | output_file Path for the output binary STL file 26 | 27 | optional arguments: 28 | -h, --help show this help message and exit 29 | ``` 30 | 31 | ## License 32 | 33 | See [`LICENSE.txt`](https://github.com/kev-m/pyCoilGen/blob/master/LICENSE.txt) for more information. 34 | 35 | -------------------------------------------------------------------------------- /utilities/pyCoilGenUtils/__init__.py: -------------------------------------------------------------------------------- 1 | """Extra utilities for pyCoilGen, the Open Source Magnetic Resonance Coil Generator.""" 2 | __version__ = "0.0.2" 3 | -------------------------------------------------------------------------------- /utilities/pyCoilGenUtils/stl_asc2bin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script converts an ASCII format STL file to Binary format. 3 | 4 | An ASCII STL file begins with the line: 5 | solid name 6 | 7 | where name is an optional string. The remainder of the line is ignored and may store metadata. 8 | 9 | The file continues with any number of triangles, each represented as follows: 10 | 11 | facet normal ni nj nk 12 | outer loop 13 | vertex v1x v1y v1z 14 | vertex v2x v2y v2z 15 | vertex v3x v3y v3z 16 | endloop 17 | endfacet 18 | 19 | Whitespace (spaces, tabs, newlines) may be used anywhere in the file except within numbers or words. 20 | The spaces between facet and normal and between outer and loop are required. 21 | 22 | The script outputs a binary format STL file. 23 | """ 24 | import argparse 25 | import struct 26 | 27 | 28 | def read_ascii_stl(file_path): 29 | """ 30 | Reads an ASCII STL file and extracts the normals and vertices. 31 | 32 | Args: 33 | file_path (str): Path to the ASCII STL file. 34 | 35 | Returns: 36 | tuple: Tuple containing a list of normals and a list of vertices. 37 | """ 38 | with open(file_path, 'r') as f: 39 | lines = [line.strip() for line in f.readlines()] # Remove leading/trailing whitespace 40 | lines = [line for line in lines if line] # Remove empty lines 41 | 42 | assert "solid" in lines[0], "Invalid header: 'solid ....' not found!" # Check for valid format 43 | 44 | offset = 1 45 | num_entries = (len(lines) - 2) / 7 46 | print("Reading ", num_entries, "faces from ASCII file.") 47 | assert num_entries == int(num_entries), "Invalid number of lines in the file." # Check for valid structure 48 | num_entries = int(num_entries) 49 | vertices = [None] * num_entries 50 | normals = [None] * num_entries 51 | 52 | for i in range(num_entries): 53 | normal = list(map(float, lines[offset + i * 7 + 0].split()[2:])) 54 | normals[i] = normal 55 | vertex1 = list(map(float, lines[offset + i * 7 + 2].split()[1:])) 56 | vertex2 = list(map(float, lines[offset + i * 7 + 3].split()[1:])) 57 | vertex3 = list(map(float, lines[offset + i * 7 + 4].split()[1:])) 58 | vertices[i] = [vertex1, vertex2, vertex3] 59 | 60 | return normals, vertices 61 | 62 | 63 | def write_binary_stl(file_path, normals, vertices): 64 | print("Writing binary file.") 65 | with open(file_path, 'wb') as f: 66 | f.write(b'\x00' * 80) # Write 80 bytes header 67 | f.write(struct.pack('=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pycoilgen_utils" 7 | authors = [ 8 | {name = "Kevin Meyer", email = "kevin@kmz.co.za"}, 9 | ] 10 | readme = "README.md" 11 | license = {file = "LICENSE.txt"} 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Console", 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: Healthcare Industry", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Programming Language :: Python :: 3.6", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Topic :: Scientific/Engineering", 29 | ] 30 | 31 | requires-python = ">=3.6" 32 | keywords = [ 33 | "Utility", 34 | "STL", 35 | ] 36 | dependencies = [ ] 37 | dynamic = ["version", "description"] 38 | 39 | [project.urls] 40 | Home = "https://github.com/kev-m/pyCoilGen" 41 | Documentation = "https://pycoilgen.readthedocs.io/" 42 | Source = "https://github.com/kev-m/pyCoilGen/tree/master/utilities" 43 | "Code of Conduct" = "https://github.com/kev-m/pyCoilGen/blob/master/CODE_OF_CONDUCT.md" 44 | "Bug tracker" = "https://github.com/kev-m/pyCoilGen/issues" 45 | Changelog = "https://github.com/kev-m/pyCoilGen/blob/master/utilities/CHANGELOG.md" 46 | Contributing = "https://github.com/kev-m/pyCoilGen/blob/master/CONTRIBUTING.md" 47 | 48 | [project.scripts] 49 | stl_asc2bin = "pyCoilGenUtils:stl_asc2bin.main" 50 | 51 | [tool.flit.module] 52 | name = "pyCoilGenUtils" 53 | 54 | [tool.autopep8] 55 | max_line_length = 120 56 | aggressive = 3 --------------------------------------------------------------------------------