PyFiberModes 0.13.0__tar.gz → 0.13.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.gitignore +2 -0
  2. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.zenodo.json +1 -1
  3. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/CHANGELOG.md +21 -1
  4. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PKG-INFO +6 -1
  5. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/__init__.py +11 -2
  6. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/_version.py +2 -2
  7. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/coordinates.py +7 -5
  8. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/directories.py +0 -12
  9. pyfibermodes-0.13.1/PyFiberModes/exceptions.py +17 -0
  10. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber.py +154 -72
  11. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/field.py +7 -58
  12. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/mode.py +0 -30
  13. pyfibermodes-0.13.1/PyFiberModes/models.py +63 -0
  14. pyfibermodes-0.13.1/PyFiberModes/services.py +162 -0
  15. pyfibermodes-0.13.1/PyFiberModes/solver/__init__.py +18 -0
  16. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/base_solver.py +88 -8
  17. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/mlsif/neff.py +0 -97
  18. pyfibermodes-0.13.1/PyFiberModes/solver/protocols.py +32 -0
  19. pyfibermodes-0.13.1/PyFiberModes/solver/results.py +56 -0
  20. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/stepindex.py +23 -9
  21. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes.egg-info/PKG-INFO +6 -1
  22. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes.egg-info/SOURCES.txt +10 -1
  23. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes.egg-info/requires.txt +3 -0
  24. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/README.rst +3 -0
  25. pyfibermodes-0.13.1/benchmarks/README.md +11 -0
  26. pyfibermodes-0.13.1/benchmarks/test_field_performance.py +32 -0
  27. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/conda.recipe/meta.yaml +1 -1
  28. pyfibermodes-0.13.1/docs/source/architecture.rst +37 -0
  29. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/code/analysis.rst +12 -0
  30. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/code/fundamentals.rst +8 -0
  31. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/conf.py +2 -2
  32. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/index.rst +1 -0
  33. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/pyproject.toml +5 -1
  34. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_api.py +2 -2
  35. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_architecture.py +0 -8
  36. pyfibermodes-0.13.1/tests/test_architecture_enhancements.py +86 -0
  37. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_field.py +21 -10
  38. pyfibermodes-0.13.1/tests/test_physics_regression.py +41 -0
  39. pyfibermodes-0.13.0/PyFiberModes/__future__.py +0 -177
  40. pyfibermodes-0.13.0/PyFiberModes/solver/__init__.py +0 -5
  41. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.coveragerc +0 -0
  42. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/dependabot.yml +0 -0
  43. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/deploy_PyPi.yml +0 -0
  44. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/deploy_anaconda.yml +0 -0
  45. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/deploy_coverage.yml +0 -0
  46. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/deploy_documentation.yml +0 -0
  47. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/quality.yml +0 -0
  48. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/.github/workflows/tests.yml +0 -0
  49. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/CITATION.cff +0 -0
  50. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/CONTRIBUTING.md +0 -0
  51. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/LICENSE +0 -0
  52. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/MANIFEST.in +0 -0
  53. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/Makefile +0 -0
  54. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/analysis.py +0 -0
  55. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/factory.py +0 -0
  56. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/1550BHP.yaml +0 -0
  57. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/DCF13.yaml +0 -0
  58. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/DCF1300S_20.yaml +0 -0
  59. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/DCF1300S_26.yaml +0 -0
  60. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/DCF1300S_33.yaml +0 -0
  61. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/DCF1300S_42.yaml +0 -0
  62. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/F2028M12.yaml +0 -0
  63. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/F2028M21.yaml +0 -0
  64. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/F2028M24.yaml +0 -0
  65. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/F2058G1.yaml +0 -0
  66. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/F2058L1.yaml +0 -0
  67. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/HI1060.yaml +0 -0
  68. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/HP630.yaml +0 -0
  69. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/SM1950.yaml +0 -0
  70. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/SMF28.yaml +0 -0
  71. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/fluorine_doped_1%_capillary.yaml +0 -0
  72. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/fluorine_doped_2%_capillary.yaml +0 -0
  73. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/test_fiber.yaml +0 -0
  74. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fiber_files/test_multimode_fiber.yaml +0 -0
  75. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/fundamentals.py +0 -0
  76. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/loader.py +0 -0
  77. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/mode_instances.py +0 -0
  78. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/optimization.py +0 -0
  79. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/propagation.py +0 -0
  80. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/mlsif/__init__.py +0 -0
  81. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/ssif/__init__.py +0 -0
  82. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/ssif/cutoff.py +0 -0
  83. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/ssif/neff.py +0 -0
  84. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/tlsif/__init__.py +0 -0
  85. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/solver/tlsif/cutoff.py +0 -0
  86. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes/source.py +0 -0
  87. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes.egg-info/dependency_links.txt +0 -0
  88. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/PyFiberModes.egg-info/top_level.txt +0 -0
  89. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/Makefile +0 -0
  90. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/DCF/README.rst +0 -0
  91. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/DCF/plot_DCF_fields.py +0 -0
  92. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/README.rst +0 -0
  93. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/SMF28/README.rst +0 -0
  94. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/SMF28/plot_smf28.py +0 -0
  95. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/SMF28/plot_smf28_dispersion_vs_wavelength.py +0 -0
  96. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/SMF28/plot_smf28_group_index_vs_wavelength.py +0 -0
  97. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/analysis/README.rst +0 -0
  98. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/analysis/plot_coupled_mode_exchange.py +0 -0
  99. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/analysis/plot_structured_wavelength_sweep.py +0 -0
  100. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/README.rst +0 -0
  101. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_U_vs_V.py +0 -0
  102. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_comparison_solvers.py +0 -0
  103. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_dispersion.py +0 -0
  104. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_effective_index.py +0 -0
  105. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_fiber_LP_modes.py +0 -0
  106. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_group_index.py +0 -0
  107. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_mode_field.py +0 -0
  108. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_mode_field_1.py +0 -0
  109. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_mode_field_2.py +0 -0
  110. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/basic/plot_neff_tapered_fiber.py +0 -0
  111. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/benchmarks/README.rst +0 -0
  112. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/benchmarks/plot_field_grid_scaling.py +0 -0
  113. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/fields/README.rst +0 -0
  114. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/fields/plot_vector_components.py +0 -0
  115. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/validation/README.rst +0 -0
  116. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/examples/validation/plot_effective_index_bounds.py +0 -0
  117. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/make.bat +0 -0
  118. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/_static/default.css +0 -0
  119. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/_static/favicon.png +0 -0
  120. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/_static/logo.png +0 -0
  121. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/_static/thumbnail.png +0 -0
  122. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/code/fibers.rst +0 -0
  123. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/code/fields.rst +0 -0
  124. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/code.rst +0 -0
  125. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/examples.rst +0 -0
  126. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/getting_started.rst +0 -0
  127. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/performance.rst +0 -0
  128. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/references.rst +0 -0
  129. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/theory.rst +0 -0
  130. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/troubleshooting.rst +0 -0
  131. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/docs/source/workflows.rst +0 -0
  132. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/pytest.ini +0 -0
  133. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/setup.cfg +0 -0
  134. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/__init__.py +0 -0
  135. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/helpers.py +0 -0
  136. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_advanced_features.py +0 -0
  137. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_coordinates.py +0 -0
  138. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_docstrings.py +0 -0
  139. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_fiber.py +0 -0
  140. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_file_loading.py +0 -0
  141. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_release_tools.py +0 -0
  142. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_ssif.py +0 -0
  143. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_tlsif.py +0 -0
  144. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tests/test_validation.py +0 -0
  145. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tools/check_release.py +0 -0
  146. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tools/next_release_version.py +0 -0
  147. {pyfibermodes-0.13.0 → pyfibermodes-0.13.1}/tools/release_tag.py +0 -0
@@ -8,6 +8,8 @@
8
8
  !development
9
9
  !docs
10
10
  !tests
11
+ !benchmarks/
12
+ !benchmarks/**
11
13
  !.flake8
12
14
  !.gitignore
13
15
  !.pre-commit-config.yaml
@@ -9,7 +9,7 @@
9
9
  "license": "MIT",
10
10
  "upload_type": "software",
11
11
  "access_right": "open",
12
- "version": "0.13.0",
12
+ "version": "0.13.1",
13
13
  "publication_date": "2026-09-22",
14
14
  "keywords": [
15
15
  "optical fibers",
@@ -6,6 +6,25 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.13.1] - 2026-09-22
10
+
11
+ ### Added
12
+
13
+ - Added validated immutable layer and solver settings, structural solver
14
+ protocols, domain-specific exceptions, and structured solver diagnostics.
15
+ - Added deterministic physical-reference, convergence, caching, architecture,
16
+ and opt-in `pytest-benchmark` performance suites.
17
+
18
+ ### Changed
19
+
20
+ - Unified cylindrical coordinates under one canonical model, split modal and
21
+ field orchestration into focused services, and added geometry-aware caching.
22
+ - Vectorized radial index lookup and routed field construction through a
23
+ single batched radial-field interface.
24
+ - Standardized the group-velocity API on `get_group_velocity` and removed
25
+ unreferenced experimental modules, unfinished solver branches, misspelled
26
+ methods, lowercase directory aliases, and obsolete compatibility wrappers.
27
+
9
28
  ## [0.13.0] - 2026-09-22
10
29
 
11
30
  ### Changed
@@ -55,7 +74,8 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55
74
 
56
75
  - Established the current tagged baseline for subsequent documented releases.
57
76
 
58
- [Unreleased]: https://github.com/MartinPdeS/PyFiberModes/compare/v0.13.0...HEAD
77
+ [Unreleased]: https://github.com/MartinPdeS/PyFiberModes/compare/v0.13.1...HEAD
78
+ [0.13.1]: https://github.com/MartinPdeS/PyFiberModes/releases/tag/v0.13.1
59
79
  [0.13.0]: https://github.com/MartinPdeS/PyFiberModes/releases/tag/v0.13.0
60
80
  [0.12.0]: https://github.com/MartinPdeS/PyFiberModes/releases/tag/v0.12.0
61
81
  [0.11.0]: https://github.com/MartinPdeS/PyFiberModes/releases/tag/v0.11.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFiberModes
3
- Version: 0.13.0
3
+ Version: 0.13.1
4
4
  Summary: A package for light propagation in fiber optics.
5
5
  Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -709,6 +709,8 @@ Requires-Dist: pytest-json-report~=1.5; extra == "testing"
709
709
  Requires-Dist: coverage~=7.6; extra == "testing"
710
710
  Requires-Dist: ruff<1,>=0.8; extra == "testing"
711
711
  Requires-Dist: tomli<3,>=2; python_version < "3.11" and extra == "testing"
712
+ Provides-Extra: benchmarking
713
+ Requires-Dist: pytest-benchmark<6,>=5; extra == "benchmarking"
712
714
  Provides-Extra: documentation
713
715
  Requires-Dist: numpydoc==1.10.0; extra == "documentation"
714
716
  Requires-Dist: sphinx>=5.1.1; extra == "documentation"
@@ -775,6 +777,9 @@ Features
775
777
  * Constrained optimization of layer radii, refractive indices, and custom parameters.
776
778
  * Constant or longitudinally varying coupled-mode propagation.
777
779
  * A catalog of common and example fiber definitions stored as readable YAML files.
780
+ * Structured solver diagnostics, validated configuration models, and explicit
781
+ solver protocols for extending numerical backends.
782
+ * Geometry-aware modal caching and vectorized radial index and field workflows.
778
783
 
779
784
  Installation
780
785
  ------------
@@ -3,7 +3,12 @@
3
3
  from PyFiberModes.mode import Mode, Family # noqa: F401
4
4
  from PyFiberModes.mode_instances import * # noqa: F403
5
5
  from PyFiberModes.factory import FiberFactory
6
+ from PyFiberModes.fiber import Fiber, load_fiber
6
7
  from PyFiberModes.field import Field
8
+ from PyFiberModes.coordinates import CartesianCoordinates, CylindricalCoordinates
9
+ from PyFiberModes.exceptions import ConvergenceError, SolverError, ValidationError
10
+ from PyFiberModes.models import LayerSpec, SolverSettings
11
+ from PyFiberModes.solver.results import SolverResult
7
12
  from PyFiberModes.analysis import ModeSweepResult, find_modes, sweep_modes
8
13
  from PyFiberModes.optimization import DesignParameter, DesignResult, optimize_fiber
9
14
  from PyFiberModes.propagation import CoupledModeSystem, PropagationResult, coupling_matrix, overlap
@@ -17,11 +22,15 @@ except ImportError:
17
22
 
18
23
 
19
24
  __all__ = [ # noqa: F405
20
- 'Wavelength',
21
25
  'Mode',
22
- 'ModeFamily',
26
+ 'Family',
27
+ 'Fiber',
28
+ 'load_fiber',
23
29
  'FiberFactory',
24
30
  'Field',
31
+ 'CartesianCoordinates', 'CylindricalCoordinates',
32
+ 'LayerSpec', 'SolverSettings', 'SolverResult',
33
+ 'ValidationError', 'SolverError', 'ConvergenceError',
25
34
  'ModeSweepResult', 'find_modes', 'sweep_modes',
26
35
  'DesignParameter', 'DesignResult', 'optimize_fiber',
27
36
  'CoupledModeSystem', 'PropagationResult', 'coupling_matrix', 'overlap',
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.13.0'
22
- __version_tuple__ = version_tuple = (0, 13, 0)
21
+ __version__ = version = '0.13.1'
22
+ __version_tuple__ = version_tuple = (0, 13, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -74,9 +74,9 @@ class CartesianCoordinates:
74
74
 
75
75
  def __post_init__(self) -> None:
76
76
  """Convert all coordinate arrays to floating-point arrays."""
77
- self.x = self.x.astype(float)
78
- self.y = self.y.astype(float)
79
- self.z = self.z.astype(float)
77
+ self.x = np.asarray(self.x, dtype=float)
78
+ self.y = np.asarray(self.y, dtype=float)
79
+ self.z = np.asarray(self.z, dtype=float)
80
80
 
81
81
  @property
82
82
  def x_boundaries(self) -> Tuple[float, float]:
@@ -119,7 +119,8 @@ class CartesianCoordinates:
119
119
  """
120
120
  if self.is_structured:
121
121
  x_min, x_max = self.x_boundaries
122
- return (x_max - x_min) / len(self.x)
122
+ unique_x = np.unique(self.x)
123
+ return float(unique_x[1] - unique_x[0])
123
124
  raise ValueError("dx value cannot be inferred from an unstructured mesh.")
124
125
 
125
126
  @property
@@ -139,7 +140,8 @@ class CartesianCoordinates:
139
140
  """
140
141
  if self.is_structured:
141
142
  y_min, y_max = self.y_boundaries
142
- return (y_max - y_min) / len(self.y)
143
+ unique_y = np.unique(self.y)
144
+ return float(unique_y[1] - unique_y[0])
143
145
  raise ValueError("dx value cannot be inferred from an unstructured mesh.")
144
146
 
145
147
  @classmethod
@@ -9,11 +9,6 @@ __all__ = [
9
9
  "DOCS_PATH",
10
10
  "EXAMPLES_PATH",
11
11
  "DOC_CSS_PATH",
12
- "root_path",
13
- "project_path",
14
- "examples_path",
15
- "doc_path",
16
- "doc_css_path",
17
12
  ]
18
13
 
19
14
  PACKAGE_PATH = Path(__file__).resolve().parent
@@ -21,10 +16,3 @@ PROJECT_PATH = PACKAGE_PATH.parent
21
16
  DOCS_PATH = PROJECT_PATH / "docs"
22
17
  EXAMPLES_PATH = DOCS_PATH / "examples"
23
18
  DOC_CSS_PATH = DOCS_PATH / "source" / "_static" / "default.css"
24
-
25
- # Backwards-compatible aliases for the original public names.
26
- root_path = PACKAGE_PATH
27
- project_path = PROJECT_PATH
28
- doc_path = DOCS_PATH
29
- examples_path = EXAMPLES_PATH
30
- doc_css_path = DOC_CSS_PATH
@@ -0,0 +1,17 @@
1
+ """Domain-specific exceptions raised by PyFiberModes."""
2
+
3
+
4
+ class PyFiberModesError(Exception):
5
+ """Base class for package-specific errors."""
6
+
7
+
8
+ class ValidationError(PyFiberModesError, ValueError):
9
+ """Indicate invalid physical geometry or solver configuration."""
10
+
11
+
12
+ class SolverError(PyFiberModesError, RuntimeError):
13
+ """Indicate that a modal solver could not complete normally."""
14
+
15
+
16
+ class ConvergenceError(SolverError):
17
+ """Indicate that a numerical solver did not converge to a valid root."""
@@ -15,14 +15,14 @@ from PyFinitDiff.finite_difference_1D import get_function_derivative
15
15
  from PyFiberModes.field import Field
16
16
 
17
17
  from PyFiberModes.fundamentals import (
18
- get_effective_index,
19
- get_mode_cutoff_v0,
20
18
  get_radial_field,
21
19
  get_propagation_constant_from_omega
22
20
  )
23
21
 
24
22
  from PyFiberModes import loader
25
23
  from PyFiberModes.coordinates import CylindricalCoordinates
24
+ from PyFiberModes.exceptions import ValidationError
25
+ from PyFiberModes.services import FieldAnalysis, ModalAnalysis
26
26
 
27
27
 
28
28
  @dataclass
@@ -55,9 +55,62 @@ class Fiber(object):
55
55
 
56
56
  def __post_init__(self):
57
57
  """Initialize mutable layer and solver state after construction."""
58
+ if self.wavelength is not None and (
59
+ not numpy.isfinite(self.wavelength) or self.wavelength <= 0
60
+ ):
61
+ raise ValidationError("wavelength must be positive and finite")
58
62
  self.layers_parameters = []
59
63
  self.radius_in = 0
60
64
  self.layers = []
65
+ self._analysis_service = None
66
+ self._field_service = None
67
+
68
+ @property
69
+ def geometry_signature(self) -> tuple:
70
+ """Return an immutable signature of all solver-relevant fiber state.
71
+
72
+ Returns
73
+ -------
74
+ tuple
75
+ Wavelength and ordered layer geometry/index values.
76
+ """
77
+ layers = tuple(
78
+ (layer.radius_in, layer.radius_out, layer.refractive_index)
79
+ for layer in self.layers
80
+ )
81
+ return self.wavelength, layers
82
+
83
+ @property
84
+ def analysis(self) -> ModalAnalysis:
85
+ """Return the focused modal-analysis service.
86
+
87
+ Returns
88
+ -------
89
+ ModalAnalysis
90
+ Geometry-aware cached analysis service.
91
+ """
92
+ if self._analysis_service is None:
93
+ self._analysis_service = ModalAnalysis(self)
94
+ return self._analysis_service
95
+
96
+ @property
97
+ def fields(self) -> FieldAnalysis:
98
+ """Return the focused field-construction service.
99
+
100
+ Returns
101
+ -------
102
+ FieldAnalysis
103
+ Field orchestration service.
104
+ """
105
+ if self._field_service is None:
106
+ self._field_service = FieldAnalysis(self)
107
+ return self._field_service
108
+
109
+ def clear_caches(self) -> None:
110
+ """Clear modal and radial-field caches after a physical mutation."""
111
+ if self._analysis_service is not None:
112
+ self._analysis_service.clear()
113
+ self.get_radial_field.cache_clear()
61
114
 
62
115
  def scale(self, factor: float) -> None:
63
116
  """
@@ -195,7 +248,7 @@ class Fiber(object):
195
248
  self.wavelength = wavelength
196
249
  for layer in self.layers:
197
250
  layer.wavelength = wavelength
198
- self.get_radial_field.cache_clear()
251
+ self.clear_caches()
199
252
 
200
253
  def add_layer(self, name: str, radius: float, index: float) -> None:
201
254
  """
@@ -214,6 +267,16 @@ class Fiber(object):
214
267
  -----
215
268
  Layers should be added in order, starting with the innermost layer (core).
216
269
  """
270
+ if not name:
271
+ raise ValidationError("layer name must not be empty")
272
+ if not numpy.isfinite(index) or index <= 0:
273
+ raise ValidationError("refractive index must be positive and finite")
274
+ radius = float(radius)
275
+ if radius <= 0:
276
+ radius = numpy.inf
277
+ if radius <= self.radius_in:
278
+ raise ValidationError("layer radii must be strictly increasing")
279
+
217
280
  self.layer_names.append(name)
218
281
  self.index_list.append(index)
219
282
 
@@ -231,6 +294,7 @@ class Fiber(object):
231
294
  self.layers.append(layer)
232
295
 
233
296
  self.radius_in = radius
297
+ self.clear_caches()
234
298
 
235
299
  def initialize_layers(self) -> None:
236
300
  """Initializes the layers.
@@ -240,6 +304,8 @@ class Fiber(object):
240
304
  None
241
305
  No returns
242
306
  """
307
+ if not self.layers:
308
+ raise ValidationError("a fiber must contain at least one layer")
243
309
  self.layers[-1].is_last_layer = True
244
310
  self.layers[0].is_first_layer = True
245
311
 
@@ -247,6 +313,7 @@ class Fiber(object):
247
313
 
248
314
  for position, layer in enumerate(self.layers):
249
315
  layer.position = position
316
+ self.clear_caches()
250
317
 
251
318
  def get_layer_at_radius(self, radius: float) -> StepIndex:
252
319
  """Gets the layer that is associated to a given radius.
@@ -263,8 +330,29 @@ class Fiber(object):
263
330
  """
264
331
  radius = abs(radius)
265
332
  for layer in self.layers:
266
- if (radius > layer.radius_in) and (radius < layer.radius_out):
333
+ if layer.radius_in <= radius <= layer.radius_out:
267
334
  return layer
335
+ raise ValidationError(f"radius {radius} lies outside the fiber geometry")
336
+
337
+ def get_index_profile(self, radius) -> numpy.ndarray:
338
+ """Evaluate the refractive-index profile for many radii at once.
339
+
340
+ Parameters
341
+ ----------
342
+ radius : array-like
343
+ Scalar or array of radial positions in meters.
344
+
345
+ Returns
346
+ -------
347
+ numpy.ndarray
348
+ Refractive indices with the same shape as ``radius``.
349
+ """
350
+ radii = numpy.abs(numpy.asarray(radius, dtype=float))
351
+ boundaries = numpy.asarray([layer.radius_out for layer in self.layers])
352
+ indices = numpy.asarray([layer.refractive_index for layer in self.layers])
353
+ positions = numpy.searchsorted(boundaries, radii, side="left")
354
+ positions = numpy.clip(positions, 0, len(indices) - 1)
355
+ return indices[positions]
268
356
 
269
357
  @property
270
358
  def radius(self) -> float:
@@ -417,13 +505,8 @@ class Fiber(object):
417
505
  float
418
506
  The cutoff wavelength.
419
507
  """
420
- cutoff_V0 = get_mode_cutoff_v0(
421
- mode=mode,
422
- fiber=self,
423
- wavelength=self.wavelength
424
- )
425
-
426
- return cutoff_V0
508
+ result = self.analysis.cutoff_result(mode)
509
+ return result.value if result.converged else numpy.nan
427
510
 
428
511
  def get_mode_cutoff_wavelength(self, mode: Mode) -> float:
429
512
  """
@@ -472,13 +555,22 @@ class Fiber(object):
472
555
  float
473
556
  The effective index.
474
557
  """
475
- neff = get_effective_index(
476
- fiber=self,
477
- wavelength=self.wavelength,
478
- mode=mode
479
- )
558
+ return self.analysis.effective_index(mode)
559
+
560
+ def solve_effective_index(self, mode: Mode):
561
+ """Return a structured effective-index solver result.
480
562
 
481
- return neff
563
+ Parameters
564
+ ----------
565
+ mode : Mode
566
+ Mode to solve.
567
+
568
+ Returns
569
+ -------
570
+ SolverResult[float]
571
+ Value and explicit convergence diagnostics.
572
+ """
573
+ return self.analysis.effective_index_result(mode)
482
574
 
483
575
  def get_normalized_beta(self, mode: Mode) -> float:
484
576
  """Gets the normalized propagation constant [beta].
@@ -493,11 +585,7 @@ class Fiber(object):
493
585
  float
494
586
  The normalized propagation constant.
495
587
  """
496
- neff = get_effective_index(
497
- fiber=self,
498
- wavelength=self.wavelength,
499
- mode=mode,
500
- )
588
+ neff = self.get_effective_index(mode)
501
589
 
502
590
  n_max = self.maximum_index
503
591
 
@@ -522,11 +610,7 @@ class Fiber(object):
522
610
  float
523
611
  The propagation constant [:math:`beta`].
524
612
  """
525
- neff = get_effective_index(
526
- fiber=self,
527
- wavelength=self.wavelength,
528
- mode=mode,
529
- )
613
+ neff = self.get_effective_index(mode)
530
614
 
531
615
  beta = neff * (2 * numpy.pi / self.wavelength)
532
616
 
@@ -555,11 +639,7 @@ class Fiber(object):
555
639
 
556
640
  where :math:`n_{eff}` is the effective refractive index.
557
641
  """
558
- n_eff = get_effective_index(
559
- fiber=self,
560
- wavelength=self.wavelength,
561
- mode=mode,
562
- )
642
+ n_eff = self.get_effective_index(mode)
563
643
 
564
644
  return constants.c / n_eff
565
645
 
@@ -589,34 +669,20 @@ class Fiber(object):
589
669
 
590
670
  return derivative * constants.c
591
671
 
592
- def get_groupe_velocity(self, mode: Mode) -> float:
593
- r"""Gets the groupe velocity defined as:
594
-
595
- .. math::
596
- \left( \frac{\partial \beta}{\partial \omega} \right)^{-1}
672
+ def get_group_velocity(self, mode: Mode) -> float:
673
+ """Compute the correctly named modal group velocity.
597
674
 
598
675
  Parameters
599
676
  ----------
600
677
  mode : Mode
601
- The mode to consider
678
+ Mode to evaluate.
602
679
 
603
680
  Returns
604
681
  -------
605
682
  float
606
- The groupe velocity.
683
+ Group velocity in meters per second.
607
684
  """
608
- omega = c * 2 * numpy.pi / self.wavelength
609
-
610
- derivative = get_function_derivative(
611
- function=get_propagation_constant_from_omega,
612
- x_eval=omega,
613
- derivative=1,
614
- accuracy=4,
615
- delta=1e12, # This value is critical for accurate computation
616
- function_kwargs=dict(fiber=self, mode=mode)
617
- )
618
-
619
- return 1 / derivative
685
+ return constants.c / self.get_group_index(mode)
620
686
 
621
687
  def get_group_velocity_dispersion(self, mode: Mode) -> float:
622
688
  r"""Gets the fiber group velocity dispersion defined as:
@@ -736,20 +802,12 @@ class Fiber(object):
736
802
  Field
737
803
  The field instance of the mode.
738
804
  """
739
- if limit is None:
740
- limit = self.radius * 5.5
741
-
742
- field = Field(
743
- fiber=self,
744
- mode=mode,
745
- limit=limit,
746
- n_point=n_point
747
- )
748
-
749
- return field
805
+ return self.fields.mode_field(mode=mode, limit=limit, n_point=n_point)
750
806
 
751
807
  @cache
752
- def get_radial_field(self, mode: Mode, radius: float) -> CylindricalCoordinates:
808
+ def get_radial_field(
809
+ self, mode: Mode, radius: float
810
+ ) -> tuple[CylindricalCoordinates, CylindricalCoordinates]:
753
811
  r"""Gets the mode field without the azimuthal component.
754
812
  Tuple structure is [:math:`E_{r}`, :math:`E_{\phi}`, :math:`E_{z}`], [:math:`H_{r}`, :math:`H_{\phi}`, :math:`H_{z}`]
755
813
 
@@ -762,8 +820,8 @@ class Fiber(object):
762
820
 
763
821
  Returns
764
822
  -------
765
- PyFiberModes.coordinates.CylindricalCoordinates
766
- The radial field.
823
+ tuple[CylindricalCoordinates, CylindricalCoordinates]
824
+ Electric and magnetic radial fields.
767
825
  """
768
826
  radial_field = get_radial_field(
769
827
  fiber=self,
@@ -774,6 +832,35 @@ class Fiber(object):
774
832
 
775
833
  return radial_field
776
834
 
835
+ def get_radial_fields(self, mode: Mode, radius) -> tuple[CylindricalCoordinates, CylindricalCoordinates]:
836
+ """Evaluate radial electric and magnetic fields over an array.
837
+
838
+ Parameters
839
+ ----------
840
+ mode : Mode
841
+ Mode to evaluate.
842
+ radius : array-like
843
+ Radial sampling positions.
844
+
845
+ Returns
846
+ -------
847
+ tuple[CylindricalCoordinates, CylindricalCoordinates]
848
+ Electric and magnetic cylindrical components as arrays.
849
+ """
850
+ radii = numpy.asarray(radius, dtype=float)
851
+ flat = radii.ravel()
852
+ solved = [self.get_radial_field(mode, float(value)) for value in flat]
853
+
854
+ def assemble(position):
855
+ """Assemble one electric or magnetic coordinate container."""
856
+ values = [item[position] for item in solved]
857
+ return CylindricalCoordinates(*(
858
+ numpy.asarray([getattr(value, name) for value in values]).reshape(radii.shape)
859
+ for name in ("rho", "phi", "z")
860
+ ))
861
+
862
+ return assemble(0), assemble(1)
863
+
777
864
  def get_radial_field_norm(self, mode: Mode, radius: float) -> float:
778
865
  r"""Gets the norm of the mode field without the azimuthal component.
779
866
  Tuple structure is [:math:`E_{r}`, :math:`E_{\phi}`, :math:`E_{z}`], [:math:`H_{r}`, :math:`H_{\phi}`, :math:`H_{z}`]
@@ -790,12 +877,7 @@ class Fiber(object):
790
877
  float
791
878
  The radial field.
792
879
  """
793
- e_field, h_field = get_radial_field(
794
- fiber=self,
795
- mode=mode,
796
- wavelength=self.wavelength,
797
- radius=radius
798
- )
880
+ e_field, h_field = self.get_radial_field(mode=mode, radius=radius)
799
881
 
800
882
  norm = numpy.sqrt(e_field.rho**2 + e_field.phi**2 + e_field.z**2)
801
883
 
@@ -817,7 +899,7 @@ class Fiber(object):
817
899
  mode_exist = []
818
900
  for mode in mode_list:
819
901
  neff = self.get_effective_index(mode=mode)
820
- if neff is numpy.nan:
902
+ if not numpy.isfinite(neff):
821
903
  mode_exist.append(False)
822
904
  else:
823
905
  mode_exist.append(True)
@@ -10,6 +10,7 @@ from dataclasses import dataclass
10
10
  import matplotlib.pyplot as plt
11
11
  from matplotlib.colors import LinearSegmentedColormap
12
12
  from PyFiberModes.coordinates import CartesianCoordinates
13
+ from PyFiberModes.coordinates import CylindricalCoordinates as CylindricalCoordinates # noqa: F401
13
14
 
14
15
 
15
16
  BLUE_BLACK_RED = LinearSegmentedColormap.from_list(
@@ -18,50 +19,6 @@ BLUE_BLACK_RED = LinearSegmentedColormap.from_list(
18
19
  )
19
20
 
20
21
 
21
- @dataclass
22
- class CylindricalCoordinates:
23
- """Store cylindrical coordinate arrays used by a field grid.
24
-
25
- Parameters
26
- ----------
27
- rho : numpy.ndarray
28
- Radial distances.
29
- phi : numpy.ndarray
30
- Azimuthal angles in radians.
31
- z : numpy.ndarray
32
- Longitudinal positions.
33
- """
34
- rho: numpy.ndarray
35
- phi: numpy.ndarray
36
- z: numpy.ndarray
37
-
38
- def to_cartesian(self) -> object:
39
- """Convert the coordinate arrays to Cartesian form.
40
-
41
- Returns
42
- -------
43
- CartesianCoordinates
44
- Equivalent Cartesian coordinate arrays.
45
- """
46
- x = self.rho * numpy.cos(self.phi)
47
- y = self.rho * numpy.sin(self.phi)
48
- z = self.z
49
-
50
- cartesian_coordinate = CartesianCoordinates(x=x, y=y, z=z)
51
-
52
- return cartesian_coordinate
53
-
54
- def to_cylindrical(self):
55
- """Return this already-cylindrical coordinate object.
56
-
57
- Returns
58
- -------
59
- CylindricalCoordinates
60
- The current object.
61
- """
62
- return self
63
-
64
-
65
22
  @dataclass
66
23
  class Field:
67
24
  """
@@ -123,23 +80,15 @@ class Field:
123
80
 
124
81
  count = max(257, 2 * self.n_point)
125
82
  radii = np.linspace(0.0, np.sqrt(2.0) * self.limit, count)
126
- component_names = ("rho", "phi", "z")
83
+ electric, magnetic = self.fiber.get_radial_fields(
84
+ mode=self.mode, radius=radii
85
+ )
127
86
  values = {
128
- (field_name, component): np.empty(count, dtype=complex)
129
- for field_name in ("E", "H")
130
- for component in component_names
87
+ (field_name, component): np.asarray(getattr(vector, component), dtype=complex)
88
+ for field_name, vector in (("E", electric), ("H", magnetic))
89
+ for component in ("rho", "phi", "z")
131
90
  }
132
91
 
133
- for index, radius in enumerate(radii):
134
- electric, magnetic = self.fiber.get_radial_field(
135
- mode=self.mode, radius=float(radius)
136
- )
137
- for field_name, vector in (("E", electric), ("H", magnetic)):
138
- for component in component_names:
139
- values[field_name, component][index] = complex(
140
- getattr(vector, component)
141
- )
142
-
143
92
  self._radial_cache = {
144
93
  key: (radii, component_values)
145
94
  for key, component_values in values.items()