paramrf 0.36.2__tar.gz → 0.36.4__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 (254) hide show
  1. {paramrf-0.36.2 → paramrf-0.36.4}/CHANGELOG.md +20 -0
  2. {paramrf-0.36.2/paramrf.egg-info → paramrf-0.36.4}/PKG-INFO +1 -1
  3. {paramrf-0.36.2 → paramrf-0.36.4/paramrf.egg-info}/PKG-INFO +1 -1
  4. {paramrf-0.36.2 → paramrf-0.36.4}/paramrf.egg-info/SOURCES.txt +2 -0
  5. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/covariance_kernels.py +58 -17
  6. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/discrepancy_models.py +100 -0
  7. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/evaluators.py +49 -14
  8. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/minimize.py +8 -4
  9. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/likelihoods.py +38 -9
  10. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/parameters.py +4 -1
  11. paramrf-0.36.4/pmrf/utils/debug.py +83 -0
  12. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/transforms.py +23 -4
  13. {paramrf-0.36.2 → paramrf-0.36.4}/pyproject.toml +1 -1
  14. paramrf-0.36.4/scripts/benchmark_gp_marginal_likelihood.py +121 -0
  15. paramrf-0.36.4/tests/test_covariance_kernels.py +320 -0
  16. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_evaluators.py +222 -1
  17. paramrf-0.36.4/tests/test_fitting_minimize.py +148 -0
  18. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_edge_values.py +65 -3
  19. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_parameters_validity.py +78 -1
  20. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_transforms.py +98 -0
  21. paramrf-0.36.4/tests/test_utils/test_debug.py +61 -0
  22. paramrf-0.36.2/pmrf/utils/debug.py +0 -72
  23. paramrf-0.36.2/tests/test_covariance_kernels.py +0 -132
  24. paramrf-0.36.2/tests/test_fitting_minimize.py +0 -84
  25. {paramrf-0.36.2 → paramrf-0.36.4}/.github/workflows/docs.yml +0 -0
  26. {paramrf-0.36.2 → paramrf-0.36.4}/.github/workflows/draft-pdf.yml +0 -0
  27. {paramrf-0.36.2 → paramrf-0.36.4}/.github/workflows/publish.yml +0 -0
  28. {paramrf-0.36.2 → paramrf-0.36.4}/.github/workflows/tests.yml +0 -0
  29. {paramrf-0.36.2 → paramrf-0.36.4}/.gitignore +0 -0
  30. {paramrf-0.36.2 → paramrf-0.36.4}/AGENTS.md +0 -0
  31. {paramrf-0.36.2 → paramrf-0.36.4}/CITATION.cff +0 -0
  32. {paramrf-0.36.2 → paramrf-0.36.4}/CLAUDE.md +0 -0
  33. {paramrf-0.36.2 → paramrf-0.36.4}/CONTEXT.md +0 -0
  34. {paramrf-0.36.2 → paramrf-0.36.4}/CONTRIBUTING.md +0 -0
  35. {paramrf-0.36.2 → paramrf-0.36.4}/LICENSE +0 -0
  36. {paramrf-0.36.2 → paramrf-0.36.4}/NOTICE +0 -0
  37. {paramrf-0.36.2 → paramrf-0.36.4}/README.rst +0 -0
  38. {paramrf-0.36.2 → paramrf-0.36.4}/assets/logo.png +0 -0
  39. {paramrf-0.36.2 → paramrf-0.36.4}/docs/Makefile +0 -0
  40. {paramrf-0.36.2 → paramrf-0.36.4}/docs/_static/custom.css +0 -0
  41. {paramrf-0.36.2 → paramrf-0.36.4}/docs/_templates/autosummary/class.rst +0 -0
  42. {paramrf-0.36.2 → paramrf-0.36.4}/docs/_templates/autosummary/function.rst +0 -0
  43. {paramrf-0.36.2 → paramrf-0.36.4}/docs/_templates/autosummary/module.rst +0 -0
  44. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0001-line-modelling-architecture.md +0 -0
  45. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0002-parameter-api.md +0 -0
  46. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0003-derived-models.md +0 -0
  47. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0004-profiled-lines.md +0 -0
  48. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0005-priors-by-name.md +0 -0
  49. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0006-reference-impedance.md +0 -0
  50. {paramrf-0.36.2 → paramrf-0.36.4}/docs/adr/0007-edge-values.md +0 -0
  51. {paramrf-0.36.2 → paramrf-0.36.4}/docs/agents/domain.md +0 -0
  52. {paramrf-0.36.2 → paramrf-0.36.4}/docs/agents/issue-tracker.md +0 -0
  53. {paramrf-0.36.2 → paramrf-0.36.4}/docs/agents/triage-labels.md +0 -0
  54. {paramrf-0.36.2 → paramrf-0.36.4}/docs/api/index.rst +0 -0
  55. {paramrf-0.36.2 → paramrf-0.36.4}/docs/conf.py +0 -0
  56. {paramrf-0.36.2 → paramrf-0.36.4}/docs/core_concepts/core_primitives.rst +0 -0
  57. {paramrf-0.36.2 → paramrf-0.36.4}/docs/core_concepts/index.rst +0 -0
  58. {paramrf-0.36.2 → paramrf-0.36.4}/docs/core_concepts/jax_overview.rst +0 -0
  59. {paramrf-0.36.2 → paramrf-0.36.4}/docs/core_concepts/optimization_and_inference.rst +0 -0
  60. {paramrf-0.36.2 → paramrf-0.36.4}/docs/core_concepts/parameter_names.rst +0 -0
  61. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/cascading_and_terminating.rst +0 -0
  62. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/circuit_clc.png +0 -0
  63. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/circuit_models.rst +0 -0
  64. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/custom_models.rst +0 -0
  65. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/derivatives_and_sweeps.rst +0 -0
  66. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/index.rst +0 -0
  67. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/model_optimization.rst +0 -0
  68. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/multiple_models_one_parameter_set.rst +0 -0
  69. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/parameter_naming_and_model_manipulation.rst +0 -0
  70. {paramrf-0.36.2 → paramrf-0.36.4}/docs/examples/shared_substrates.rst +0 -0
  71. {paramrf-0.36.2 → paramrf-0.36.4}/docs/index.rst +0 -0
  72. {paramrf-0.36.2 → paramrf-0.36.4}/docs/license.rst +0 -0
  73. {paramrf-0.36.2 → paramrf-0.36.4}/docs/make.bat +0 -0
  74. {paramrf-0.36.2 → paramrf-0.36.4}/docs/research/conductor-loss-alternatives.md +0 -0
  75. {paramrf-0.36.2 → paramrf-0.36.4}/docs/research/holloway1994.pdf +0 -0
  76. {paramrf-0.36.2 → paramrf-0.36.4}/docs/research/microstrip-loss-conventions.md +0 -0
  77. {paramrf-0.36.2 → paramrf-0.36.4}/docs/research/precision-coax-microstrip-10-500mhz.md +0 -0
  78. {paramrf-0.36.2 → paramrf-0.36.4}/docs/skrf_comparison/index.rst +0 -0
  79. {paramrf-0.36.2 → paramrf-0.36.4}/docs/skrf_comparison/overview.rst +0 -0
  80. {paramrf-0.36.2 → paramrf-0.36.4}/docs/skrf_comparison/performance.rst +0 -0
  81. {paramrf-0.36.2 → paramrf-0.36.4}/docs/tutorials/1_cable_fitting.ipynb +0 -0
  82. {paramrf-0.36.2 → paramrf-0.36.4}/docs/tutorials/2_chip_inductor_fitting.ipynb +0 -0
  83. {paramrf-0.36.2 → paramrf-0.36.4}/docs/tutorials/data/CBN-1.5FT-SMSM.s2p +0 -0
  84. {paramrf-0.36.2 → paramrf-0.36.4}/docs/tutorials/data/on-chip-inductor.s2p +0 -0
  85. {paramrf-0.36.2 → paramrf-0.36.4}/docs/tutorials/index.rst +0 -0
  86. {paramrf-0.36.2 → paramrf-0.36.4}/paper/paper.bib +0 -0
  87. {paramrf-0.36.2 → paramrf-0.36.4}/paper/paper.md +0 -0
  88. {paramrf-0.36.2 → paramrf-0.36.4}/paper/rlc.png +0 -0
  89. {paramrf-0.36.2 → paramrf-0.36.4}/paramrf.egg-info/dependency_links.txt +0 -0
  90. {paramrf-0.36.2 → paramrf-0.36.4}/paramrf.egg-info/requires.txt +0 -0
  91. {paramrf-0.36.2 → paramrf-0.36.4}/paramrf.egg-info/top_level.txt +0 -0
  92. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/__init__.py +0 -0
  93. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/_solver_view.py +0 -0
  94. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/bijectors.py +0 -0
  95. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/constraints.py +0 -0
  96. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/distributions.py +0 -0
  97. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/__init__.py +0 -0
  98. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/result.py +0 -0
  99. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/routers.py +0 -0
  100. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/sample.py +0 -0
  101. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/fitting/targets.py +0 -0
  102. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/frequency.py +0 -0
  103. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/__init__.py +0 -0
  104. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/base.py +0 -0
  105. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/result.py +0 -0
  106. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/sample.py +0 -0
  107. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/solvers/__init__.py +0 -0
  108. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/solvers/blackjax.py +0 -0
  109. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/infer/solvers/polychord.py +0 -0
  110. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/losses.py +0 -0
  111. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/__init__.py +0 -0
  112. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/conductor.py +0 -0
  113. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/dielectric.py +0 -0
  114. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/properties.py +0 -0
  115. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/roughness.py +0 -0
  116. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/substrate.py +0 -0
  117. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/materials/surface_impedance.py +0 -0
  118. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/__init__.py +0 -0
  119. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/aggregations.py +0 -0
  120. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/bessel.py +0 -0
  121. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/conversions.py +0 -0
  122. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/losses.py +0 -0
  123. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/math/misc.py +0 -0
  124. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/__init__.py +0 -0
  125. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/__init__.py +0 -0
  126. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/base.py +0 -0
  127. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/bridge.py +0 -0
  128. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/callable.py +0 -0
  129. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/delegated.py +0 -0
  130. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/derived.py +0 -0
  131. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/static.py +0 -0
  132. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/adapters/wrapped.py +0 -0
  133. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/base.py +0 -0
  134. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/__init__.py +0 -0
  135. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/ideal.py +0 -0
  136. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/__init__.py +0 -0
  137. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/base.py +0 -0
  138. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/coaxial.py +0 -0
  139. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/empirical.py +0 -0
  140. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/ideal.py +0 -0
  141. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/microstrip.py +0 -0
  142. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/nodal.py +0 -0
  143. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/nonuniform.py +0 -0
  144. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/planar.py +0 -0
  145. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/profiles.py +0 -0
  146. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lines/stripline.py +0 -0
  147. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/lumped.py +0 -0
  148. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/components/sections.py +0 -0
  149. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/__init__.py +0 -0
  150. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/__init__.py +0 -0
  151. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/cascade.py +0 -0
  152. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/__init__.py +0 -0
  153. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/base.py +0 -0
  154. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/circuit.py +0 -0
  155. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/solvers/__init__.py +0 -0
  156. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/solvers/nodal.py +0 -0
  157. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/circuit/solvers/scattering.py +0 -0
  158. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/interconnected/terminated.py +0 -0
  159. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/nodal.py +0 -0
  160. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/topological.py +0 -0
  161. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/composite/transformed.py +0 -0
  162. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/surrogates/__init__.py +0 -0
  163. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/surrogates/expansion.py +0 -0
  164. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/models/surrogates/rational.py +0 -0
  165. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/modules/__init__.py +0 -0
  166. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/modules/base.py +0 -0
  167. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/modules/wrapped.py +0 -0
  168. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/network_collection.py +0 -0
  169. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/noise_models.py +0 -0
  170. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/__init__.py +0 -0
  171. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/base.py +0 -0
  172. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/minimize.py +0 -0
  173. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/result.py +0 -0
  174. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/solvers/__init__.py +0 -0
  175. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/solvers/jaxopt.py +0 -0
  176. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/solvers/optimistix.py +0 -0
  177. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/optimize/solvers/scipy.py +0 -0
  178. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/problems.py +0 -0
  179. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/rf/__init__.py +0 -0
  180. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/rf/cascade.py +0 -0
  181. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/rf/conversions.py +0 -0
  182. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/rf/mna.py +0 -0
  183. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/serialization.py +0 -0
  184. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/terms.py +0 -0
  185. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/types.py +0 -0
  186. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/__init__.py +0 -0
  187. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/array.py +0 -0
  188. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/network.py +0 -0
  189. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/random.py +0 -0
  190. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/rf.py +0 -0
  191. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/tree.py +0 -0
  192. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/utils/type.py +0 -0
  193. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/viz/__init__.py +0 -0
  194. {paramrf-0.36.2 → paramrf-0.36.4}/pmrf/viz/plots.py +0 -0
  195. {paramrf-0.36.2 → paramrf-0.36.4}/scripts/install-test-deps.sh +0 -0
  196. {paramrf-0.36.2 → paramrf-0.36.4}/setup.cfg +0 -0
  197. {paramrf-0.36.2 → paramrf-0.36.4}/tests/__init__.py +0 -0
  198. {paramrf-0.36.2 → paramrf-0.36.4}/tests/_jit.py +0 -0
  199. {paramrf-0.36.2 → paramrf-0.36.4}/tests/conftest.py +0 -0
  200. {paramrf-0.36.2 → paramrf-0.36.4}/tests/data/10m_cable.s2p +0 -0
  201. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_autodiff.py +0 -0
  202. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_box_space_solving.py +0 -0
  203. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_conversions.py +0 -0
  204. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_dc_limits.py +0 -0
  205. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_fitting_routers.py +0 -0
  206. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_fitting_sample.py +0 -0
  207. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_fitting_targets.py +0 -0
  208. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_frequency.py +0 -0
  209. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_infer_base.py +0 -0
  210. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_infer_sample.py +0 -0
  211. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_map_priors.py +0 -0
  212. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_materials/test_conductor.py +0 -0
  213. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_materials/test_dielectric.py +0 -0
  214. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_materials/test_serialization.py +0 -0
  215. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_materials/test_substrate.py +0 -0
  216. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_materials/test_surface_impedance.py +0 -0
  217. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_math/test_bessel.py +0 -0
  218. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_model.py +0 -0
  219. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_adapters.py +0 -0
  220. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_cascade.py +0 -0
  221. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_native_z0.py +0 -0
  222. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_nodal.py +0 -0
  223. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_port_order.py +0 -0
  224. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_scattering.py +0 -0
  225. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_shorted_ports.py +0 -0
  226. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_skrf_port_names.py +0 -0
  227. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_circuit_z0.py +0 -0
  228. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_current_distribution.py +0 -0
  229. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_derived.py +0 -0
  230. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_interconnected.py +0 -0
  231. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_lines.py +0 -0
  232. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_lines_skrf_matrix.py +0 -0
  233. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_lumped.py +0 -0
  234. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_nodal.py +0 -0
  235. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_profiles.py +0 -0
  236. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_rational.py +0 -0
  237. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_repeated_cascade.py +0 -0
  238. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_sections.py +0 -0
  239. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_transformed.py +0 -0
  240. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_models/test_transformers.py +0 -0
  241. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_module.py +0 -0
  242. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_naming.py +0 -0
  243. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_optimize_base.py +0 -0
  244. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_optimize_minimize.py +0 -0
  245. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_parameters.py +0 -0
  246. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_parameters_by_name.py +0 -0
  247. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_parameters_joint_prior.py +0 -0
  248. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_parameters_prior.py +0 -0
  249. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_raw_space_solving.py +0 -0
  250. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_serialization.py +0 -0
  251. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_structural_updates.py +0 -0
  252. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_terms.py +0 -0
  253. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_utils/test_compress.py +0 -0
  254. {paramrf-0.36.2 → paramrf-0.36.4}/tests/test_utils/test_tree.py +0 -0
@@ -117,6 +117,24 @@ longer recompiles a model's RF methods.
117
117
 
118
118
  ## Unreleased
119
119
 
120
+ - **Breaking (#245):** a frequentist fit with neither `loss` nor `likelihood`
121
+ now minimises the mean squared error (`MSELoss`) instead of the RMSE. MSE is
122
+ smooth in the residuals, pools every output's residuals equally rather than
123
+ averaging per-output RMSEs, and matches the fixed-noise Gaussian likelihood
124
+ of the Bayesian path. Fits relying on the default stop at slightly different
125
+ points and report loss values on a squared scale; pass `loss=RMSELoss()` for
126
+ the old objective. SciPy's default `gtol`, and its `ftol` (absolute below an
127
+ objective of 1), can now stop a fit whose MSE is already tiny before it
128
+ moves: tighten them through `ScipyMinimize(options=...)`.
129
+ - **Breaking (#247):** `SharedIndependentKernel` appends its shared axes as
130
+ size-1 trailing axes instead of materializing them, which fixes its routing
131
+ over a batched base kernel. Wrapping an `AutoCrossKernel` previously
132
+ prepended the shared axes, so half the real/imaginary entries got the wrong
133
+ kernel (S11 Im got cross, S21 Re got auto). Log-likelihoods of every model
134
+ using this composition change. `output_shape` is replaced by
135
+ `num_shared_axes` (default 1), since only its length was ever meaningful;
136
+ `gram` now returns e.g. `(2, 2, 1, N, N)` rather than `(2, 2, 2, N, N)`, and
137
+ the orthogonal discrepancy path factorizes the smaller batch.
120
138
  - Complete Tesche coaxial conductor physics across the low-frequency regime,
121
139
  and pass evaluated material properties to pure coaxial formulations.
122
140
  - Correct microstrip results by enabling Kirschning--Jansen modal dispersion by
@@ -124,3 +142,5 @@ longer recompiles a model's RF methods.
124
142
  pipeline explicitly.
125
143
  - Add the Hammerstad--Jensen microstrip formulation and selectable complex
126
144
  (ADS-like) and real (QUCS-like) permittivity conventions.
145
+ - Add `CosineKernel` to `pmrf.covariance_kernels` for zero-mean oscillatory
146
+ (negative) correlation. Multiply it by a decaying kernel for a damped cosine.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.36.2
3
+ Version: 0.36.4
4
4
  Summary: Parametric radio frequency modeling
5
5
  Author-email: Gary Allen <gvcallen@gmail.com>
6
6
  Project-URL: homepage, https://github.com/gvcallen/paramrf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.36.2
3
+ Version: 0.36.4
4
4
  Summary: Parametric radio frequency modeling
5
5
  Author-email: Gary Allen <gvcallen@gmail.com>
6
6
  Project-URL: homepage, https://github.com/gvcallen/paramrf
@@ -180,6 +180,7 @@ pmrf/utils/tree.py
180
180
  pmrf/utils/type.py
181
181
  pmrf/viz/__init__.py
182
182
  pmrf/viz/plots.py
183
+ scripts/benchmark_gp_marginal_likelihood.py
183
184
  scripts/install-test-deps.sh
184
185
  tests/__init__.py
185
186
  tests/_jit.py
@@ -244,4 +245,5 @@ tests/test_models/test_sections.py
244
245
  tests/test_models/test_transformed.py
245
246
  tests/test_models/test_transformers.py
246
247
  tests/test_utils/test_compress.py
248
+ tests/test_utils/test_debug.py
247
249
  tests/test_utils/test_tree.py
@@ -7,10 +7,11 @@ for more details.
7
7
  from abc import abstractmethod
8
8
  from collections.abc import Callable
9
9
 
10
+ import equinox as eqx
10
11
  import jax
11
12
  import jax.numpy as jnp
12
13
 
13
- from pmrf.utils import field
14
+ from pmrf.utils import field, unwrap
14
15
  from pmrf.types import ArrayLike
15
16
  from pmrf.modules.base import Module
16
17
 
@@ -51,6 +52,12 @@ def gram(
51
52
  The Gram matrix, of shape ``(*batch_shape, N, N)``, where
52
53
  ``batch_shape`` is the shape returned by the kernel for a single pair.
53
54
  """
55
+ # Unwrap the hyperparameters to their values once, behind a barrier. Otherwise
56
+ # XLA can fuse each one's raw-to-value bijector chain into the elementwise loop
57
+ # that builds the matrix and recompute it per entry.
58
+ leaves, structure = eqx.partition(unwrap(kernel), eqx.is_array)
59
+ kernel = eqx.combine(jax.lax.optimization_barrier(leaves), structure)
60
+
54
61
  x = jnp.asarray(x)
55
62
  if x.ndim == 1:
56
63
  x_feat = x[:, None]
@@ -231,6 +238,40 @@ class PeriodicKernel(AbstractCovarianceKernel):
231
238
  return jnp.exp(-2.0 * (sin_term / self.lengthscale)**2)
232
239
 
233
240
 
241
+ class CosineKernel(AbstractCovarianceKernel):
242
+ r"""
243
+ Cosine kernel.
244
+
245
+ Models zero-mean oscillatory correlation with a single period, which can be
246
+ negative. Unlike :class:`PeriodicKernel`, the correlation goes to $-1$ at
247
+ half a period.
248
+
249
+ $$k(x_1, x_2) = \cos\left(\frac{2\pi\, \mathbf{1}^\top (x_1 - x_2)}{p}\right)$$
250
+
251
+ where $p$ is the ``period``.
252
+
253
+ For $d$-dimensional inputs the oscillation runs along the all-ones direction:
254
+ the differences are summed, not combined into a Euclidean distance, since
255
+ $\cos(2\pi \lVert x_1 - x_2 \rVert / p)$ is not positive semi-definite
256
+ for $d > 1$.
257
+
258
+ Its Gram matrix has rank two, so it is singular on its own. Multiply by a
259
+ decaying kernel (e.g. :class:`Matern52Kernel`) for a damped oscillation,
260
+ and by a float for its variance.
261
+
262
+ Parameters
263
+ ----------
264
+ period : ArrayLike
265
+ The period of the oscillation. A period of shape ``(D,)`` gives ``D``
266
+ batched kernels, as for :class:`PeriodicKernel`.
267
+ """
268
+ #: The period.
269
+ period: ArrayLike
270
+
271
+ def __call__(self, x1, x2, key=None):
272
+ return jnp.cos(2 * jnp.pi * jnp.sum(x1 - x2) / self.period)
273
+
274
+
234
275
  class WhiteNoiseKernel(AbstractCovarianceKernel):
235
276
  """
236
277
  Kernel representing independent Gaussian noise.
@@ -353,32 +394,32 @@ class AutoCrossKernel(AbstractCovarianceKernel):
353
394
 
354
395
  class SharedIndependentKernel(AbstractCovarianceKernel):
355
396
  """
356
- Evaluates a base kernel and broadcasts its output to represent
357
- multiple independent dimensions (e.g., real and imaginary parts)
358
- withed share hyperparameters.
397
+ Shares a base kernel across trailing independent batch axes (e.g. the real
398
+ and imaginary parts), so that every element on those axes uses the same
399
+ hyperparameters.
400
+
401
+ The shared axes are appended to the base kernel's output as size-1 axes
402
+ rather than materialized. A base output of shape ``batch_shape`` becomes
403
+ ``(*batch_shape, 1, ..., 1)``, which broadcasts to any size along the
404
+ shared axes, so a batched base kernel (e.g. :class:`AutoCrossKernel`)
405
+ keeps routing along its own leading axes.
359
406
 
360
407
  Parameters
361
408
  ----------
362
- base_kernel : CovarianceKernel
409
+ base_kernel : AbstractCovarianceKernel
363
410
  The underlying kernel whose parameters are shared.
364
- output_shape : tuple
365
- The shape of the independent outputs to broadcast to.
411
+ num_shared_axes : int, default=1
412
+ The number of trailing batch axes the base kernel is shared across.
366
413
  """
367
414
  #: The base kernel.
368
415
  base_kernel: AbstractCovarianceKernel
369
416
 
370
- #: The output shape.
371
- output_shape: tuple = field(static=True)
417
+ #: The number of trailing shared axes.
418
+ num_shared_axes: int = field(default=1, static=True)
372
419
 
373
420
  def __call__(self, x1, x2, key=None):
374
- # Evaluate the underlying shared kernel
375
- val = self.base_kernel(x1, x2, key=key)
376
-
377
- # Broadcast the evaluation to the target shape.
378
- # By appending the output_shape to the end of val.shape,
379
- # this safely handles both scalar evaluations and already-batched evaluations.
380
- target_shape = val.shape + self.output_shape
381
- return jnp.broadcast_to(val, target_shape)
421
+ val = jnp.asarray(self.base_kernel(x1, x2, key=key))
422
+ return val.reshape(val.shape + (1,) * self.num_shared_axes)
382
423
 
383
424
 
384
425
  class ZeroKernel(AbstractCovarianceKernel):
@@ -6,7 +6,10 @@ Useful for modeling discrepancy of RF models during fitting.
6
6
  from collections.abc import Callable
7
7
  from abc import abstractmethod
8
8
 
9
+ import math
10
+
9
11
  import equinox as eqx
12
+ import jax
10
13
  import jax.scipy as jsp
11
14
  import jax.numpy as jnp
12
15
  import parax.distributions as dist
@@ -49,6 +52,44 @@ class AbstractDiscrepancyModel(Module):
49
52
  """
50
53
  raise NotImplementedError
51
54
 
55
+
56
+ @jax.custom_vjp
57
+ def _gaussian_log_prob(M: jnp.ndarray, R: jnp.ndarray) -> jnp.ndarray:
58
+ """Sum the zero-mean Gaussian log densities of the columns of ``R`` under ``M``.
59
+
60
+ ``M`` has shape ``(*batch, N, N)`` and ``R`` has shape ``(*batch, N, k)``: the
61
+ ``k`` residuals in ``R[b]`` share the covariance ``M[b]`` and are solved together.
62
+ """
63
+ return _gaussian_log_prob_fwd(M, R)[0]
64
+
65
+
66
+ def _gaussian_log_prob_fwd(M, R):
67
+ L = jnp.linalg.cholesky(M)
68
+ A = jsp.linalg.cho_solve((L, True), R)
69
+ n, k = R.shape[-2:]
70
+ num_matrices = math.prod(M.shape[:-2])
71
+ logdet = 2 * jnp.sum(jnp.log(jnp.diagonal(L, axis1=-2, axis2=-1)))
72
+ value = -0.5 * (
73
+ jnp.sum(R * A) + k * logdet + k * num_matrices * n * jnp.log(2 * jnp.pi)
74
+ )
75
+ return value, (L, A)
76
+
77
+
78
+ def _gaussian_log_prob_bwd(residuals, g):
79
+ # With alpha = M^-1 r, d/dM = (sum alpha alpha^T - k M^-1) / 2 and d/dr = -alpha.
80
+ # JAX has no potri, so M^-1 = L^-T L^-1 from one triangular solve and a matmul.
81
+ L, A = residuals
82
+ n, k = A.shape[-2:]
83
+ identity = jnp.broadcast_to(jnp.eye(n, dtype=L.dtype), L.shape)
84
+ L_inv = jsp.linalg.solve_triangular(L, identity, lower=True)
85
+ M_inv = jnp.swapaxes(L_inv, -1, -2) @ L_inv
86
+ dM = 0.5 * g * (A @ jnp.swapaxes(A, -1, -2) - k * M_inv)
87
+ return dM, -g * A
88
+
89
+
90
+ _gaussian_log_prob.defvjp(_gaussian_log_prob_fwd, _gaussian_log_prob_bwd)
91
+
92
+
52
93
  class GaussianProcess(AbstractDiscrepancyModel):
53
94
  """
54
95
  Gaussian process discrepancy model with a covariance kernel.
@@ -82,6 +123,65 @@ class GaussianProcess(AbstractDiscrepancyModel):
82
123
  #: The added jitter.
83
124
  jitter: float = field(default=1e-10, static=True)
84
125
 
126
+ def log_prob(
127
+ self,
128
+ y_event: jnp.ndarray,
129
+ observed: jnp.ndarray,
130
+ x: jnp.ndarray,
131
+ noise_variance: jnp.ndarray,
132
+ ) -> jnp.ndarray:
133
+ r"""Evaluate the summed log density of ``observed`` under the GP plus Gaussian noise.
134
+
135
+ Each batch entry of ``observed`` is distributed as
136
+ $\mathcal{N}(y, K + \sigma^2 I)$. Equal to the log probability of the
137
+ distribution built by :meth:`__call__` and
138
+ :class:`pmrf.likelihoods.GaussianLikelihood`, summed over the batch.
139
+
140
+ ``M = K + sigma^2 I`` is formed and factorized at the broadcast shape of the
141
+ kernel's Gram batch and ``noise_variance``, rather than the full batch shape,
142
+ and the residuals sharing each ``M`` are solved together.
143
+
144
+ Parameters
145
+ ----------
146
+ y_event : jnp.ndarray
147
+ The model prediction in event space, with shape ``(*batch_shape, N)``.
148
+ observed : jnp.ndarray
149
+ The observation in event space, broadcastable to ``y_event``.
150
+ x : jnp.ndarray
151
+ The frequency points, with shape ``(N,)``.
152
+ noise_variance : jnp.ndarray
153
+ The noise variance, constant along the event axis and broadcastable to
154
+ ``batch_shape``.
155
+
156
+ Returns
157
+ -------
158
+ jnp.ndarray
159
+ The scalar log density, summed over the batch.
160
+ """
161
+ # Materialize K before forming M. Otherwise XLA on CPU fuses the Gram
162
+ # construction into the transpose to LAPACK's column-major layout, and that
163
+ # strided build nearly doubles the value's cost at N = 1000.
164
+ K = jax.lax.optimization_barrier(gram(self.kernel, x, jitter=self.jitter))
165
+ variance = jnp.asarray(noise_variance)
166
+ batch_shape = y_event.shape[:-1]
167
+ n = y_event.shape[-1]
168
+ M = K + variance[..., None, None] * jnp.eye(n, dtype=K.dtype)
169
+ if jnp.broadcast_shapes(M.shape[:-2], batch_shape) != batch_shape:
170
+ raise ValueError(
171
+ f"The kernel's batch shape {K.shape[:-2]} and noise variance shape "
172
+ f"{variance.shape} do not broadcast to the event batch shape {batch_shape}."
173
+ )
174
+ matrix_batch = (1,) * (len(batch_shape) - (M.ndim - 2)) + M.shape[:-2]
175
+ # Batch axes along which M is shared become extra right-hand sides.
176
+ matrix_axes = [i for i, size in enumerate(matrix_batch) if size == batch_shape[i]]
177
+ shared_axes = [i for i in range(len(batch_shape)) if i not in matrix_axes]
178
+ residual = jnp.broadcast_to(observed - y_event, y_event.shape)
179
+ R = jnp.transpose(residual, matrix_axes + [len(batch_shape)] + shared_axes)
180
+ matrix_shape = tuple(batch_shape[i] for i in matrix_axes)
181
+ R = R.reshape(matrix_shape + (n, -1))
182
+ M = M.reshape(matrix_shape + (n, n))
183
+ return _gaussian_log_prob(M, R)
184
+
85
185
  def orthogonal_log_prob(
86
186
  self,
87
187
  y_event: jnp.ndarray,
@@ -255,6 +255,11 @@ class MarginalLogLikelihood(AbstractEvaluator):
255
255
  An optional discrepancy model to cater for model misspecification.
256
256
  Can be a function or a PyTree with optional parameters.
257
257
  See :class:`pmrf.discrepancy_models` for common discrepancy models.
258
+ A :class:`~pmrf.discrepancy_models.GaussianProcess` with a
259
+ :class:`~pmrf.likelihoods.GaussianLikelihood` whose noise is constant along
260
+ the event axis is evaluated in closed form
261
+ (:meth:`~pmrf.discrepancy_models.GaussianProcess.log_prob`), which factorizes
262
+ each distinct covariance matrix once.
258
263
  use_orthogonal_discrepancy
259
264
  Constrain a Gaussian-process discrepancy to the complement of the free-parameter
260
265
  tangent space. This retains the full-data likelihood; it is not REML.
@@ -403,17 +408,55 @@ class MarginalLogLikelihood(AbstractEvaluator):
403
408
  log_prob, event_transform = self._orthogonal_log_prob(
404
409
  model, frequency, **kwargs
405
410
  )
406
- if self.has_conditional_event_transform:
407
- log_prob = log_prob + jnp.sum(
408
- event_transform.forward_log_det_jacobian(observed)
411
+ else:
412
+ closed_form = self._closed_form_log_prob(model, frequency, **kwargs)
413
+ if closed_form is not None:
414
+ log_prob, event_transform = closed_form
415
+ else:
416
+ log_prob, event_transform = self._distribution_log_prob(
417
+ model, frequency, **kwargs
409
418
  )
410
- return log_prob
411
419
 
420
+ # A prediction-dependent change of variables carries a Jacobian determinant that
421
+ # varies with the model, so it must be included for the density to be normalized.
422
+ # For a static transform the term is a constant offset, so it is omitted.
423
+ if self.has_conditional_event_transform:
424
+ log_prob = log_prob + jnp.sum(event_transform.forward_log_det_jacobian(observed))
425
+
426
+ return log_prob
427
+
428
+ def _closed_form_log_prob(
429
+ self, model: PyTree, frequency: Frequency, **kwargs
430
+ ) -> tuple[jnp.ndarray, bij.AbstractBijector] | None:
431
+ """The log-likelihood of a GP with Gaussian noise constant over the event axis.
432
+
433
+ Returns ``None`` when it does not apply, in which case the distribution path
434
+ gives the same value.
435
+ """
436
+ if not isinstance(self.discrepancy, GaussianProcess) or not isinstance(
437
+ self.likelihood, GaussianLikelihood
438
+ ):
439
+ return None
440
+ pred_event, event_transform = self._event(model, frequency, **kwargs)
441
+ variance = self.likelihood._constant_variance(pred_event)
442
+ if variance is None:
443
+ return None
444
+ log_prob = self.discrepancy.log_prob(
445
+ pred_event,
446
+ event_transform.forward(self.observed),
447
+ frequency.f_scaled,
448
+ variance,
449
+ )
450
+ return log_prob, event_transform
451
+
452
+ def _distribution_log_prob(
453
+ self, model: PyTree, frequency: Frequency, **kwargs
454
+ ) -> tuple[jnp.ndarray, bij.AbstractBijector]:
412
455
  # Get the distribution over obs_event and the actual observed event.
413
456
  # The observation is mapped by the *same* resolved transform as the prediction,
414
457
  # which for a conditional transform depends on the model.
415
458
  obs_dist, event_transform = self._predictive(model, frequency, **kwargs)
416
- obs_event = event_transform.forward(observed)
459
+ obs_event = event_transform.forward(self.observed)
417
460
  batch_ndims = obs_event.ndim - self.event_ndims
418
461
 
419
462
  # We evaluate the log prob `batch_ndims` many times
@@ -424,15 +467,7 @@ class MarginalLogLikelihood(AbstractEvaluator):
424
467
  mapped_log_prob = eqx.filter_vmap(mapped_log_prob)
425
468
 
426
469
  log_probs = mapped_log_prob(obs_dist, obs_event)
427
- log_prob = jnp.sum(log_probs)
428
-
429
- # A prediction-dependent change of variables carries a Jacobian determinant that
430
- # varies with the model, so it must be included for the density to be normalized.
431
- # For a static transform the term is a constant offset, so it is omitted.
432
- if self.has_conditional_event_transform:
433
- log_prob = log_prob + jnp.sum(event_transform.forward_log_det_jacobian(observed))
434
-
435
- return log_prob
470
+ return jnp.sum(log_probs), event_transform
436
471
 
437
472
  def with_orthogonal_reference(
438
473
  self, model: PyTree, frequency: Frequency, **kwargs
@@ -17,7 +17,7 @@ from pmrf.problems import SummedTerms, PriorPenalized
17
17
  from pmrf.terms import as_terms
18
18
  from pmrf.fitting.targets import resolve_datasets, union_frequency
19
19
  from pmrf.likelihoods import GaussianLikelihood
20
- from pmrf.losses import RMSELoss
20
+ from pmrf.losses import MSELoss
21
21
  from pmrf.parameters import Random
22
22
  from pmrf.distributions import Uniform
23
23
 
@@ -74,16 +74,20 @@ def fit_minimize(
74
74
  Can be a function or a callable PyTree with optional parameters.
75
75
  Used to internally create a :class:`pmrf.evaluators.TargetLoss` evaluator.
76
76
  Mutually exclusive with `likelihood`. If neither `loss` nor `likelihood` is passed,
77
- :class:`pmrf.losses.RMSELoss` is used for `loss` if `inference` is 'frequentist',
77
+ :class:`pmrf.losses.MSELoss` is used for `loss` if `inference` is 'frequentist',
78
78
  otherwise :class:`pmrf.likelihoods.GaussianLikelihood` is used for `likelihood`.
79
79
  See :mod:`pmrf.losses` for common losses.
80
+
81
+ An MSE that starts very small (e.g. fitting a feature that barely moves) can
82
+ fall below SciPy's absolute `gtol` and `ftol` before the fit moves; tighten them
83
+ via ``ScipyMinimize(options=...)``.
80
84
  likelihood : str | Callable, optional
81
85
  A likelihood model representing the probability of observing the data.
82
86
  Can be a function or a callable PyTree with optional parameters.
83
87
  Used to internally create a :class:`pmrf.evaluators.MarginalLogLikelihood`,
84
88
  wrapped in a :class:`pmrf.evaluators.Negated` evaluator.
85
89
  Mutually exclusive with `loss`. If neither `loss` nor `likelihood` is passed,
86
- :class:`pmrf.losses.RMSELoss` is used for `loss` if `inference` is 'frequentist',
90
+ :class:`pmrf.losses.MSELoss` is used for `loss` if `inference` is 'frequentist',
87
91
  otherwise :class:`pmrf.likelihoods.GaussianLikelihood` is used for `likelihood`.
88
92
  See :mod:`pmrf.losses` for common losses.
89
93
  noise : prf.Param | Callable[[jnp.ndarray], jnp.ndarray], optional
@@ -135,7 +139,7 @@ def fit_minimize(
135
139
  # Resolve defaults e.g. loss vs MLE vs MAP optimization
136
140
  if loss is None and likelihood is None:
137
141
  if inference == 'frequentist':
138
- loss = RMSELoss()
142
+ loss = MSELoss()
139
143
  else:
140
144
  if noise is None:
141
145
  # Lower bound kept strictly positive: at noise=0 the Gaussian likelihood
@@ -92,6 +92,43 @@ class GaussianLikelihood(AbstractLikelihood):
92
92
  )
93
93
  return var
94
94
 
95
+ def _constant_variance(self, y_event: jnp.ndarray) -> jnp.ndarray | None:
96
+ """Return the noise variance over the batch shape if it is constant along the event axis.
97
+
98
+ The variance is interpreted exactly as in :meth:`__call__`, and kept at its own
99
+ shape, which broadcasts to the non-event batch shape of ``y_event``.
100
+
101
+ Returns
102
+ -------
103
+ jnp.ndarray | None
104
+ The variance, or ``None`` if it varies along the event axis or does not
105
+ broadcast to ``y_event``.
106
+ """
107
+ var = self._event_variance(y_event)
108
+ try:
109
+ broadcast_shape = jnp.broadcast_shapes(var.shape, y_event.shape)
110
+ except ValueError:
111
+ return None
112
+ if broadcast_shape != y_event.shape:
113
+ return None
114
+ if var.ndim == 0:
115
+ return var
116
+ if var.shape[-1] != 1:
117
+ return None
118
+ return var[..., 0]
119
+
120
+ def _event_variance(self, y_event: jnp.ndarray) -> jnp.ndarray:
121
+ """The noise variance with the event as its last axis, broadcastable to ``y_event``.
122
+
123
+ A variance of exactly the batch shape is per batch entry; any other shape
124
+ aligns its trailing axis with the event axis.
125
+ """
126
+ var = self.noise(y_event) if callable(self.noise) else self.noise
127
+ var = jnp.asarray(var)
128
+ if var.shape == y_event.shape[:-1]:
129
+ var = var[..., None]
130
+ return var
131
+
95
132
  def __call__(self, y_event: jnp.ndarray | AbstractDistribution) -> AbstractDistribution:
96
133
  # If y_event is an array, the prediction is deterministic
97
134
  # and we can simply use a regular gaussian likelihood.
@@ -101,15 +138,7 @@ class GaussianLikelihood(AbstractLikelihood):
101
138
  is_dist = isinstance(y_event, AbstractDistribution)
102
139
  y_mean = y_event.mean() if is_dist else y_event
103
140
 
104
- # Evaluate noise
105
- var = self.noise(y_mean) if callable(self.noise) else self.noise
106
- var = jnp.asarray(var)
107
-
108
- # Broadcast noise onto y_mean
109
- batch_shape = y_mean.shape[:-1]
110
- if var.shape == batch_shape:
111
- var = var[..., None]
112
- mapped_var = jnp.broadcast_to(var, y_mean.shape)
141
+ mapped_var = jnp.broadcast_to(self._event_variance(y_mean), y_mean.shape)
113
142
  num_batch_dims = y_mean.ndim - 1
114
143
 
115
144
  if not is_dist:
@@ -450,11 +450,14 @@ def _check_in_constraint(value: ArrayLike, constraint: AbstractConstraint) -> Ar
450
450
  The returned array must be used: under `jax.jit` the check lives only in its graph.
451
451
  """
452
452
  value_array = jnp.asarray(value)
453
+ # Escaped so that braces in the constraint's repr are not read as format fields.
454
+ constraint_text = str(constraint).replace("{", "{{").replace("}", "}}")
453
455
  return error_if(
454
456
  value_array,
455
457
  constraint.is_outside(value_array),
456
- f"\n\nA parameter value falls outside the constraint ({value} is not in {constraint}). "
458
+ f"\n\nA parameter value falls outside the constraint ({{}} is not in {constraint_text}). "
457
459
  f"\nMake sure the values match the parameter and model constraints.",
460
+ value_array,
458
461
  )
459
462
 
460
463
 
@@ -0,0 +1,83 @@
1
+ import equinox as eqx
2
+ import jax
3
+ import jax.numpy as jnp
4
+ import numpy as np
5
+
6
+
7
+ def error_if(x, pred, msg, *print_args, on_error="default", **print_kwargs):
8
+ """
9
+ Conditionally halts JAX execution and prints formatted debug values at runtime.
10
+
11
+ This function evaluates a boolean condition inside JIT-compiled code. If the
12
+ condition is met (e.g., an unphysical parameter like negative impedance is
13
+ detected), it outputs the formatted runtime arrays to standard output before
14
+ raising. Whether to print is decided on the host, so nothing is printed when
15
+ the check passes, under any transform. Under `jax.vmap`, the message is printed
16
+ once for each failing batch element, with that element's values. Without print
17
+ arguments nothing is printed: the raised error carries `msg`.
18
+
19
+ Parameters
20
+ ----------
21
+ x : Any
22
+ The input data (JAX array or PyTree) to pass through. The check runs only
23
+ if the returned value is used.
24
+ pred : bool or jax.Array
25
+ A boolean condition or boolean array. If any element evaluates to `True`,
26
+ execution halts, printing the message if print arguments are given.
27
+ msg : str
28
+ The format string for the error message and console output. Uses standard
29
+ Python `{}` formatting to dynamically inject JAX arrays.
30
+ *print_args : Any
31
+ Dynamic tensors or values to sequentially format into the `msg` string.
32
+ on_error : str, optional
33
+ The internal error handling mode. Default is "default".
34
+ **print_kwargs : Any
35
+ Dynamic tensors or values to format into the `msg` string via keywords.
36
+
37
+ Returns
38
+ -------
39
+ Any
40
+ The unmodified input `x`.
41
+
42
+ Raises
43
+ ------
44
+ equinox.EquinoxRuntimeError
45
+ At runtime, if `pred` contains any `True` elements.
46
+
47
+ Examples
48
+ --------
49
+ Catching an invalid characteristic impedance inside a JIT-compiled simulation:
50
+
51
+ >>> z0 = jnp.array(-50.0)
52
+ >>> is_invalid = z0 < 0
53
+ >>> z0 = error_if(
54
+ ... z0,
55
+ ... is_invalid,
56
+ ... "Characteristic impedance must be positive, got Z0 = {} Ohms",
57
+ ... z0
58
+ ... )
59
+ """
60
+ error_msg = msg
61
+ if print_args or print_kwargs:
62
+ # Decided on the host: a `lax.cond` on a batched predicate lowers to `select`
63
+ # under `vmap`, which runs the print whether or not the check fails. A
64
+ # `pure_callback` rather than `jax.debug.callback`, so that `eqx.error_if` can
65
+ # consume its output: that data dependency makes the print precede the raise.
66
+ def print_if(host_pred, args, kwargs):
67
+ if np.any(host_pred):
68
+ print(msg.format(*args, **kwargs))
69
+ return host_pred
70
+
71
+ pred = jnp.asarray(pred)
72
+ args, kwargs = jax.lax.stop_gradient((print_args, print_kwargs))
73
+ pred = jax.pure_callback(
74
+ print_if,
75
+ jax.ShapeDtypeStruct(pred.shape, pred.dtype),
76
+ pred,
77
+ args,
78
+ kwargs,
79
+ vmap_method="sequential",
80
+ )
81
+ error_msg = f"{msg} (Check standard output for runtime values)"
82
+
83
+ return eqx.error_if(x, pred, error_msg, on_error=on_error)
@@ -1,6 +1,9 @@
1
+ import dataclasses
2
+
1
3
  import jax
2
4
  import jax.numpy as jnp
3
5
  import equinox as eqx
6
+ import parax as prx
4
7
  from typing import Callable, Any, Tuple, TypeVarTuple
5
8
 
6
9
  from pmrf.utils.tree import unwrap, batch_axes
@@ -35,6 +38,11 @@ def derivative(eval_fn: Callable[..., Any], *args: *Ts, space: str = 'declared')
35
38
  inside the differentiated function, so the source's derivative includes the
36
39
  path through the tie. Raw JAX arrays are differentiated as they are.
37
40
 
41
+ In declared and physical space a parameter on a closed bound, such as a
42
+ ``NonNegative`` loss at 0, has a finite derivative. In raw space it has none:
43
+ its raw value there is infinite, so a derivative with respect to it has no
44
+ finite meaning.
45
+
38
46
  Safely handles models as inputs by filtering out non-differentiable
39
47
  static fields (like strings, booleans, integers, AND NumPy arrays).
40
48
 
@@ -82,15 +90,15 @@ def derivative(eval_fn: Callable[..., Any], *args: *Ts, space: str = 'declared')
82
90
  -1.164e-01
83
91
  """
84
92
  # Imported here: pmrf.parameters imports pmrf.utils.
85
- from pmrf.parameters import _check_space, _read, _set_paths, _with_fixed, _write, tree_param_paths
93
+ from pmrf.parameters import _check_space, _read, _set_paths, _with_fixed, _write, is_param, tree_param_paths
86
94
  from pmrf.utils.tree import Pathgetter
87
95
 
88
96
  _check_space(space)
89
97
 
90
98
  # Each parameter is replaced by its value in `space`, and rebuilt from it inside
91
99
  # the differentiated function, so the derivative is taken with respect to that
92
- # value and the chain rule carries it through scale, constraint and ties. Fixed
93
- # is an optimisation state, not a zero sensitivity, so parameters are rebuilt free.
100
+ # value and the chain rule carries it through scale and ties. Fixed is an
101
+ # optimisation state, not a zero sensitivity, so parameters are rebuilt free.
94
102
  rebuilds = []
95
103
  surrogates = []
96
104
  for arg in args:
@@ -103,12 +111,23 @@ def derivative(eval_fn: Callable[..., Any], *args: *Ts, space: str = 'declared')
103
111
  rebuilds.append((paths, nodes))
104
112
  surrogates.append(_set_paths(arg, paths, [_read(node, space) for node in nodes]))
105
113
 
114
+ def _rebuild_node(node, value):
115
+ # In declared and physical space a parameter holds its declared value directly,
116
+ # as in box space (#226), not through its raw value: on a closed bound that is
117
+ # infinite, the constraint's bijector gives 0 * inf = NaN, and forward mode
118
+ # spreads the NaN to every parameter. The value is the one the parameter was
119
+ # built with, so its bounds need no checking again. A `Real` is free.
120
+ if not is_param(node) or space == 'raw':
121
+ return _with_fixed(_write(node, value, space), False)
122
+ declared = value / node._scale if space == 'physical' and node._scale != 1.0 else value
123
+ return dataclasses.replace(node, variable=prx.Real(declared))
124
+
106
125
  def _rebuild(surrogate_args):
107
126
  rebuilt = []
108
127
  for surrogate, (paths, nodes) in zip(surrogate_args, rebuilds):
109
128
  values = [Pathgetter(path)(surrogate) for path in paths]
110
129
  rebuilt.append(_set_paths(
111
- surrogate, paths, [_with_fixed(_write(node, value, space), False) for node, value in zip(nodes, values)]
130
+ surrogate, paths, [_rebuild_node(node, value) for node, value in zip(nodes, values)]
112
131
  ))
113
132
  return unwrap(tuple(rebuilt))
114
133
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "paramrf"
3
- version = "0.36.2"
3
+ version = "0.36.4"
4
4
  description = "Parametric radio frequency modeling"
5
5
  readme = "README.rst"
6
6
  authors = [