paramrf 0.36.0__tar.gz → 0.36.2__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 (253) hide show
  1. {paramrf-0.36.0 → paramrf-0.36.2}/CONTEXT.md +17 -2
  2. {paramrf-0.36.0/paramrf.egg-info → paramrf-0.36.2}/PKG-INFO +3 -2
  3. paramrf-0.36.2/docs/adr/0007-edge-values.md +130 -0
  4. {paramrf-0.36.0 → paramrf-0.36.2/paramrf.egg-info}/PKG-INFO +3 -2
  5. {paramrf-0.36.0 → paramrf-0.36.2}/paramrf.egg-info/SOURCES.txt +5 -0
  6. {paramrf-0.36.0 → paramrf-0.36.2}/paramrf.egg-info/requires.txt +2 -1
  7. paramrf-0.36.2/pmrf/_solver_view.py +284 -0
  8. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/constraints.py +4 -0
  9. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/base.py +5 -4
  10. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/dielectric.py +10 -10
  11. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/roughness.py +2 -2
  12. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/surface_impedance.py +22 -7
  13. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/base.py +5 -3
  14. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/ideal.py +11 -13
  15. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/coaxial.py +4 -1
  16. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/empirical.py +5 -5
  17. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/ideal.py +3 -3
  18. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/microstrip.py +12 -1
  19. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lumped.py +133 -33
  20. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/sections.py +25 -12
  21. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/base.py +9 -3
  22. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/circuit.py +28 -10
  23. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/solvers/nodal.py +56 -40
  24. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/surrogates/rational.py +4 -4
  25. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/base.py +50 -24
  26. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/minimize.py +3 -3
  27. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/solvers/scipy.py +14 -0
  28. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/rf/__init__.py +2 -1
  29. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/rf/conversions.py +80 -0
  30. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/transforms.py +28 -12
  31. {paramrf-0.36.0 → paramrf-0.36.2}/pyproject.toml +3 -2
  32. paramrf-0.36.2/tests/test_box_space_solving.py +197 -0
  33. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_conversions.py +47 -2
  34. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_fitting_targets.py +4 -3
  35. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_materials/test_conductor.py +27 -0
  36. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_materials/test_dielectric.py +79 -0
  37. paramrf-0.36.2/tests/test_models/test_cascade.py +337 -0
  38. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_nodal.py +12 -8
  39. paramrf-0.36.2/tests/test_models/test_circuit_shorted_ports.py +138 -0
  40. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_derived.py +4 -1
  41. paramrf-0.36.2/tests/test_models/test_edge_values.py +256 -0
  42. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_lines.py +91 -0
  43. paramrf-0.36.2/tests/test_models/test_lumped.py +245 -0
  44. paramrf-0.36.2/tests/test_models/test_rational.py +112 -0
  45. paramrf-0.36.2/tests/test_models/test_sections.py +215 -0
  46. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_transformers.py +21 -0
  47. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_optimize_base.py +4 -3
  48. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_raw_space_solving.py +97 -14
  49. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_transforms.py +99 -0
  50. paramrf-0.36.0/pmrf/_solver_view.py +0 -125
  51. paramrf-0.36.0/tests/test_models/test_cascade.py +0 -190
  52. paramrf-0.36.0/tests/test_models/test_lumped.py +0 -82
  53. paramrf-0.36.0/tests/test_models/test_sections.py +0 -107
  54. {paramrf-0.36.0 → paramrf-0.36.2}/.github/workflows/docs.yml +0 -0
  55. {paramrf-0.36.0 → paramrf-0.36.2}/.github/workflows/draft-pdf.yml +0 -0
  56. {paramrf-0.36.0 → paramrf-0.36.2}/.github/workflows/publish.yml +0 -0
  57. {paramrf-0.36.0 → paramrf-0.36.2}/.github/workflows/tests.yml +0 -0
  58. {paramrf-0.36.0 → paramrf-0.36.2}/.gitignore +0 -0
  59. {paramrf-0.36.0 → paramrf-0.36.2}/AGENTS.md +0 -0
  60. {paramrf-0.36.0 → paramrf-0.36.2}/CHANGELOG.md +0 -0
  61. {paramrf-0.36.0 → paramrf-0.36.2}/CITATION.cff +0 -0
  62. {paramrf-0.36.0 → paramrf-0.36.2}/CLAUDE.md +0 -0
  63. {paramrf-0.36.0 → paramrf-0.36.2}/CONTRIBUTING.md +0 -0
  64. {paramrf-0.36.0 → paramrf-0.36.2}/LICENSE +0 -0
  65. {paramrf-0.36.0 → paramrf-0.36.2}/NOTICE +0 -0
  66. {paramrf-0.36.0 → paramrf-0.36.2}/README.rst +0 -0
  67. {paramrf-0.36.0 → paramrf-0.36.2}/assets/logo.png +0 -0
  68. {paramrf-0.36.0 → paramrf-0.36.2}/docs/Makefile +0 -0
  69. {paramrf-0.36.0 → paramrf-0.36.2}/docs/_static/custom.css +0 -0
  70. {paramrf-0.36.0 → paramrf-0.36.2}/docs/_templates/autosummary/class.rst +0 -0
  71. {paramrf-0.36.0 → paramrf-0.36.2}/docs/_templates/autosummary/function.rst +0 -0
  72. {paramrf-0.36.0 → paramrf-0.36.2}/docs/_templates/autosummary/module.rst +0 -0
  73. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0001-line-modelling-architecture.md +0 -0
  74. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0002-parameter-api.md +0 -0
  75. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0003-derived-models.md +0 -0
  76. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0004-profiled-lines.md +0 -0
  77. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0005-priors-by-name.md +0 -0
  78. {paramrf-0.36.0 → paramrf-0.36.2}/docs/adr/0006-reference-impedance.md +0 -0
  79. {paramrf-0.36.0 → paramrf-0.36.2}/docs/agents/domain.md +0 -0
  80. {paramrf-0.36.0 → paramrf-0.36.2}/docs/agents/issue-tracker.md +0 -0
  81. {paramrf-0.36.0 → paramrf-0.36.2}/docs/agents/triage-labels.md +0 -0
  82. {paramrf-0.36.0 → paramrf-0.36.2}/docs/api/index.rst +0 -0
  83. {paramrf-0.36.0 → paramrf-0.36.2}/docs/conf.py +0 -0
  84. {paramrf-0.36.0 → paramrf-0.36.2}/docs/core_concepts/core_primitives.rst +0 -0
  85. {paramrf-0.36.0 → paramrf-0.36.2}/docs/core_concepts/index.rst +0 -0
  86. {paramrf-0.36.0 → paramrf-0.36.2}/docs/core_concepts/jax_overview.rst +0 -0
  87. {paramrf-0.36.0 → paramrf-0.36.2}/docs/core_concepts/optimization_and_inference.rst +0 -0
  88. {paramrf-0.36.0 → paramrf-0.36.2}/docs/core_concepts/parameter_names.rst +0 -0
  89. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/cascading_and_terminating.rst +0 -0
  90. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/circuit_clc.png +0 -0
  91. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/circuit_models.rst +0 -0
  92. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/custom_models.rst +0 -0
  93. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/derivatives_and_sweeps.rst +0 -0
  94. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/index.rst +0 -0
  95. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/model_optimization.rst +0 -0
  96. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/multiple_models_one_parameter_set.rst +0 -0
  97. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/parameter_naming_and_model_manipulation.rst +0 -0
  98. {paramrf-0.36.0 → paramrf-0.36.2}/docs/examples/shared_substrates.rst +0 -0
  99. {paramrf-0.36.0 → paramrf-0.36.2}/docs/index.rst +0 -0
  100. {paramrf-0.36.0 → paramrf-0.36.2}/docs/license.rst +0 -0
  101. {paramrf-0.36.0 → paramrf-0.36.2}/docs/make.bat +0 -0
  102. {paramrf-0.36.0 → paramrf-0.36.2}/docs/research/conductor-loss-alternatives.md +0 -0
  103. {paramrf-0.36.0 → paramrf-0.36.2}/docs/research/holloway1994.pdf +0 -0
  104. {paramrf-0.36.0 → paramrf-0.36.2}/docs/research/microstrip-loss-conventions.md +0 -0
  105. {paramrf-0.36.0 → paramrf-0.36.2}/docs/research/precision-coax-microstrip-10-500mhz.md +0 -0
  106. {paramrf-0.36.0 → paramrf-0.36.2}/docs/skrf_comparison/index.rst +0 -0
  107. {paramrf-0.36.0 → paramrf-0.36.2}/docs/skrf_comparison/overview.rst +0 -0
  108. {paramrf-0.36.0 → paramrf-0.36.2}/docs/skrf_comparison/performance.rst +0 -0
  109. {paramrf-0.36.0 → paramrf-0.36.2}/docs/tutorials/1_cable_fitting.ipynb +0 -0
  110. {paramrf-0.36.0 → paramrf-0.36.2}/docs/tutorials/2_chip_inductor_fitting.ipynb +0 -0
  111. {paramrf-0.36.0 → paramrf-0.36.2}/docs/tutorials/data/CBN-1.5FT-SMSM.s2p +0 -0
  112. {paramrf-0.36.0 → paramrf-0.36.2}/docs/tutorials/data/on-chip-inductor.s2p +0 -0
  113. {paramrf-0.36.0 → paramrf-0.36.2}/docs/tutorials/index.rst +0 -0
  114. {paramrf-0.36.0 → paramrf-0.36.2}/paper/paper.bib +0 -0
  115. {paramrf-0.36.0 → paramrf-0.36.2}/paper/paper.md +0 -0
  116. {paramrf-0.36.0 → paramrf-0.36.2}/paper/rlc.png +0 -0
  117. {paramrf-0.36.0 → paramrf-0.36.2}/paramrf.egg-info/dependency_links.txt +0 -0
  118. {paramrf-0.36.0 → paramrf-0.36.2}/paramrf.egg-info/top_level.txt +0 -0
  119. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/__init__.py +0 -0
  120. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/bijectors.py +0 -0
  121. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/covariance_kernels.py +0 -0
  122. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/discrepancy_models.py +0 -0
  123. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/distributions.py +0 -0
  124. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/evaluators.py +0 -0
  125. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/__init__.py +0 -0
  126. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/minimize.py +0 -0
  127. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/result.py +0 -0
  128. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/routers.py +0 -0
  129. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/sample.py +0 -0
  130. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/fitting/targets.py +0 -0
  131. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/frequency.py +0 -0
  132. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/__init__.py +0 -0
  133. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/result.py +0 -0
  134. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/sample.py +0 -0
  135. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/solvers/__init__.py +0 -0
  136. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/solvers/blackjax.py +0 -0
  137. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/infer/solvers/polychord.py +0 -0
  138. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/likelihoods.py +0 -0
  139. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/losses.py +0 -0
  140. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/__init__.py +0 -0
  141. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/conductor.py +0 -0
  142. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/properties.py +0 -0
  143. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/materials/substrate.py +0 -0
  144. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/__init__.py +0 -0
  145. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/aggregations.py +0 -0
  146. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/bessel.py +0 -0
  147. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/conversions.py +0 -0
  148. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/losses.py +0 -0
  149. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/math/misc.py +0 -0
  150. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/__init__.py +0 -0
  151. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/__init__.py +0 -0
  152. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/base.py +0 -0
  153. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/bridge.py +0 -0
  154. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/callable.py +0 -0
  155. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/delegated.py +0 -0
  156. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/derived.py +0 -0
  157. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/static.py +0 -0
  158. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/adapters/wrapped.py +0 -0
  159. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/__init__.py +0 -0
  160. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/__init__.py +0 -0
  161. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/base.py +0 -0
  162. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/nodal.py +0 -0
  163. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/nonuniform.py +0 -0
  164. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/planar.py +0 -0
  165. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/profiles.py +0 -0
  166. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/components/lines/stripline.py +0 -0
  167. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/__init__.py +0 -0
  168. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/__init__.py +0 -0
  169. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/cascade.py +0 -0
  170. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/__init__.py +0 -0
  171. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/solvers/__init__.py +0 -0
  172. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/circuit/solvers/scattering.py +0 -0
  173. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/interconnected/terminated.py +0 -0
  174. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/nodal.py +0 -0
  175. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/topological.py +0 -0
  176. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/composite/transformed.py +0 -0
  177. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/surrogates/__init__.py +0 -0
  178. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/models/surrogates/expansion.py +0 -0
  179. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/modules/__init__.py +0 -0
  180. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/modules/base.py +0 -0
  181. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/modules/wrapped.py +0 -0
  182. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/network_collection.py +0 -0
  183. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/noise_models.py +0 -0
  184. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/__init__.py +0 -0
  185. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/result.py +0 -0
  186. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/solvers/__init__.py +0 -0
  187. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/solvers/jaxopt.py +0 -0
  188. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/optimize/solvers/optimistix.py +0 -0
  189. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/parameters.py +0 -0
  190. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/problems.py +0 -0
  191. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/rf/cascade.py +0 -0
  192. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/rf/mna.py +0 -0
  193. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/serialization.py +0 -0
  194. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/terms.py +0 -0
  195. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/types.py +0 -0
  196. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/__init__.py +0 -0
  197. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/array.py +0 -0
  198. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/debug.py +0 -0
  199. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/network.py +0 -0
  200. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/random.py +0 -0
  201. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/rf.py +0 -0
  202. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/tree.py +0 -0
  203. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/utils/type.py +0 -0
  204. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/viz/__init__.py +0 -0
  205. {paramrf-0.36.0 → paramrf-0.36.2}/pmrf/viz/plots.py +0 -0
  206. {paramrf-0.36.0 → paramrf-0.36.2}/scripts/install-test-deps.sh +0 -0
  207. {paramrf-0.36.0 → paramrf-0.36.2}/setup.cfg +0 -0
  208. {paramrf-0.36.0 → paramrf-0.36.2}/tests/__init__.py +0 -0
  209. {paramrf-0.36.0 → paramrf-0.36.2}/tests/_jit.py +0 -0
  210. {paramrf-0.36.0 → paramrf-0.36.2}/tests/conftest.py +0 -0
  211. {paramrf-0.36.0 → paramrf-0.36.2}/tests/data/10m_cable.s2p +0 -0
  212. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_autodiff.py +0 -0
  213. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_covariance_kernels.py +0 -0
  214. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_dc_limits.py +0 -0
  215. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_evaluators.py +0 -0
  216. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_fitting_minimize.py +0 -0
  217. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_fitting_routers.py +0 -0
  218. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_fitting_sample.py +0 -0
  219. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_frequency.py +0 -0
  220. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_infer_base.py +0 -0
  221. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_infer_sample.py +0 -0
  222. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_map_priors.py +0 -0
  223. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_materials/test_serialization.py +0 -0
  224. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_materials/test_substrate.py +0 -0
  225. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_materials/test_surface_impedance.py +0 -0
  226. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_math/test_bessel.py +0 -0
  227. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_model.py +0 -0
  228. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_adapters.py +0 -0
  229. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_native_z0.py +0 -0
  230. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_port_order.py +0 -0
  231. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_scattering.py +0 -0
  232. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_skrf_port_names.py +0 -0
  233. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_circuit_z0.py +0 -0
  234. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_current_distribution.py +0 -0
  235. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_interconnected.py +0 -0
  236. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_lines_skrf_matrix.py +0 -0
  237. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_nodal.py +0 -0
  238. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_profiles.py +0 -0
  239. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_repeated_cascade.py +0 -0
  240. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_models/test_transformed.py +0 -0
  241. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_module.py +0 -0
  242. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_naming.py +0 -0
  243. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_optimize_minimize.py +0 -0
  244. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_parameters.py +0 -0
  245. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_parameters_by_name.py +0 -0
  246. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_parameters_joint_prior.py +0 -0
  247. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_parameters_prior.py +0 -0
  248. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_parameters_validity.py +0 -0
  249. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_serialization.py +0 -0
  250. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_structural_updates.py +0 -0
  251. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_terms.py +0 -0
  252. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_utils/test_compress.py +0 -0
  253. {paramrf-0.36.0 → paramrf-0.36.2}/tests/test_utils/test_tree.py +0 -0
@@ -26,10 +26,23 @@ Where a parameter's number lives. Every surface defaults to **declared**.
26
26
  declares (2.0 for 2 pF). Construction, bounds, priors and `Param.value` are
27
27
  in declared space.
28
28
  - **physical**: the scaled, SI value (2e-12).
29
+ - **box**: the box a bounded minimiser searches, built from a parameter's
30
+ bounds, not its prior: the unit box when both bounds are finite, declared
31
+ space otherwise (ADR-0007).
29
32
 
30
33
  *Avoid:* "unconstrained space" (reads as a parameter without bounds; that is
31
34
  `prf.Unconstrained`), "unscaled" and "constrained" for declared space, "unit
32
- space" (the unit hypercube).
35
+ space" (the unit hypercube; box space is the unit box only when both bounds are
36
+ finite), "base space" for box space (Parax's name for it, but ADR-0005's "base"
37
+ is a flow's latent, which is raw).
38
+
39
+ ### Open and closed bounds
40
+
41
+ Each bound of a parameter is **closed** (the bound is a valid value, and a
42
+ model must evaluate there) or **open** (the bound is excluded, and a value on
43
+ it is rejected at construction). Closedness belongs to the constraint:
44
+ `pmrf.Bounded` and a `Uniform` support are closed, `Positive()` is open, and
45
+ where two constraints meet at one bound, open wins (ADR-0007).
33
46
 
34
47
  ### Scale
35
48
 
@@ -184,4 +197,6 @@ paper with no ParamRF objects in sight.
184
197
  interface or a default, and `docs/adr/0002-parameter-api.md` before adding a
185
198
  method, a public function or a value space. Read
186
199
  `docs/adr/0005-priors-by-name.md` before changing how priors are attached or
187
- scored.
200
+ scored, and `docs/adr/0007-edge-values.md` before changing how a
201
+ circuit solver produces S, how bounds are validated, or which space a solver
202
+ moves through.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.36.0
3
+ Version: 0.36.2
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
7
7
  Description-Content-Type: text/x-rst
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: parax>=0.11.1
10
+ Requires-Dist: parax>=0.11.5
11
11
  Requires-Dist: threadpoolctl
12
12
  Requires-Dist: eqxpress
13
13
  Requires-Dist: jax
@@ -38,6 +38,7 @@ Requires-Dist: pytest-xdist; extra == "tests"
38
38
  Requires-Dist: filelock; extra == "tests"
39
39
  Requires-Dist: ipython; extra == "tests"
40
40
  Requires-Dist: scikit-rf; extra == "tests"
41
+ Requires-Dist: mpmath; extra == "tests"
41
42
  Requires-Dist: anesthetic; extra == "tests"
42
43
  Requires-Dist: mpi4py; extra == "tests"
43
44
  Requires-Dist: blackjax; extra == "tests"
@@ -0,0 +1,130 @@
1
+ # ADR-0007: Values at the edge of a domain: zero impedance and parameters on bounds
2
+
3
+ Status: accepted (2026-09)
4
+
5
+ ## Context
6
+
7
+ #215 reported zero inductance breaking the default `GlobalMNACircuitSolver`, and
8
+ parameters that start exactly on a bound being unable to move. Both are values
9
+ at the edge of a domain, and both failed quietly:
10
+
11
+ - `Inductor.y()` is `1/(jωL)`, which is not finite at L = 0. `Model.mna()`
12
+ stamps from `y()`, so the MNA solve returns NaN.
13
+ - With a finite stamp (`PiSectionCLC` goes through ABCD), the MNA solver still
14
+ returns Y, and two ports joined by a short have no finite Y. The solver
15
+ regularises it to about 1e12, and `y2s` of that Y has a condition number
16
+ around 1e14: |S21|² came out 0.7% wrong and dL wrong by ten orders of
17
+ magnitude, without a warning. Removing the regularisation from the auxiliary
18
+ rows made it worse: the singular block went to a least-squares solve, and
19
+ the short came out as an open.
20
+ - `BoxSectionCLCC` substitutes eps for L = 0, which fixes the value and zeroes
21
+ the gradient.
22
+ - Parax documents `Interval` and `GreaterThan` as exclusive, but validates them
23
+ as inclusive, and maps them onto open raw spaces. So `Bounded(0, 10, value=0)`
24
+ is accepted, has raw value −∞, and cannot move. `minimize` rejects it.
25
+ `Random(Uniform(0, 10), value=0)` is silently moved to 2.2e-15 by a clip in
26
+ parax. `minimize` gives even bounded solvers raw space with infinite bounds,
27
+ so no solver ever sees the box the user wrote.
28
+
29
+ Engineers expect both to work: an inductor fitted down to 0, and a parameter
30
+ started on, or converging to, the bound they gave it.
31
+
32
+ ## Decision
33
+
34
+ ### Circuit solvers return S at the probe reference
35
+
36
+ A circuit solver that assembles a nodal system loads each external port with
37
+ the probe reference impedance $Z_r$ (ADR-0006) and drives it through $Z_r$. With
38
+ $H$ the port-voltage response to the drive, it returns power-wave S directly:
39
+
40
+ $$H = (Y + Z_r^{-1})^{-1} Z_r^{-1}, \qquad
41
+ S = F\,\big(2\,\mathrm{Re}(Z_r)\,Z_r^{-1} H - Z_r^{*} Z_r^{-1}\big)\,F^{-1}, \qquad
42
+ F = \mathrm{diag}\big(1 / (2\sqrt{\mathrm{Re}\,Z_r})\big)$$
43
+
44
+ where $Y + Z_r^{-1}$ is eliminated inside the full MNA system, never formed.
45
+ Loaded ports keep the system non-singular when ports are shorted together, so
46
+ the result is exact there. $Z_r$ may be complex and per-port. For real $Z_r$
47
+ this reduces to $S = 2\sqrt{G}\,(Y+G)^{-1}\sqrt{G} - I$; that short form is
48
+ wrong for complex $Z_r$.
49
+
50
+ - **One implementation.** `pmrf.rf.mna2s(stamp, z0)` holds the formula. The
51
+ MNA solver uses it, and so does `Model.s()` for a model whose primary domain
52
+ is MNA.
53
+ - **The solve depends on the probe.** `Circuit.s(z0=...)` passes its probe into
54
+ the solve rather than renormalising a probe-free Y afterwards.
55
+ - **Regularisation is physical.** GMIN is a conductance from every node to
56
+ ground. Each auxiliary branch gets a +eps Ω series resistance, which resolves
57
+ loops of zero-impedance branches. The system is therefore never singular,
58
+ and the default linear solver is LU (`well_posed=True`): a singular system is
59
+ a bug and shows up as NaN, not as a least-squares answer.
60
+ - **Y stays honest.** `Circuit.y()` is `s2y` of S at the native reference, and
61
+ is non-finite for shorted ports. `Circuit.mna()` is `s2mna` of S at the hub
62
+ reference, which stays finite, so nested circuits are exact.
63
+ - **Series elements that can short stamp as branches.** An element whose
64
+ impedance can be zero (`Inductor`, `InductorQ`, the inductor in
65
+ `BoxSectionCLCC`) stamps with an auxiliary branch current,
66
+ $V_1 - V_2 - Z I = 0$, not from `y()`. Its `y()` is undefined at zero and may
67
+ be non-finite there. No model substitutes eps for a zero value.
68
+ - **The nodal solver stays Y-only.** `GlobalNodalCircuitSolver` cannot
69
+ represent a zero impedance and documents it.
70
+
71
+ ### Bounds are open or closed, and solvers see the box
72
+
73
+ - **Each bound is open or closed**, declared by its Parax constraint.
74
+ `Positive` and `Negative` are open; `NonNegative` and `NonPositive` are
75
+ closed; `Interval`, `GreaterThan` and `LessThan` take `closed` and default to
76
+ closed. `pmrf.Bounded` and a `Uniform` support are closed. Where two
77
+ constraints meet at one bound, open wins. Validation respects closedness, so
78
+ a value on an open bound is rejected at construction.
79
+ - **A closed bound is part of the model's domain.** A model evaluates, with a
80
+ finite value and gradient, at every closed bound of its parameters and at
81
+ zero where zero is valid. A value it cannot evaluate gets an open validity
82
+ constraint instead: a line width is `Positive()`, a quality factor is
83
+ `Positive()`. Lumped L, C and R stay unconstrained, because fitted
84
+ equivalent circuits use negative values.
85
+ - **Box space** is a fourth space: the box a bounded minimiser searches. It is
86
+ built from a parameter's bounds, not its prior: the unit box when both bounds
87
+ are finite, declared space otherwise. A closed edge of the box is the bound
88
+ itself; an open edge is inset by δ = 1e-6.
89
+ - **Minimisers say whether they honour bounds**, through the minimiser
90
+ interface. `SolverView` hands box space to one that does and raw space to one
91
+ that does not. Nothing in `SolverView` knows about a specific backend.
92
+ - **A start on a closed bound is nudged for raw space.** For raw-space
93
+ consumers (unbounded minimisers and samplers), `SolverView` moves a start on a
94
+ closed bound δ = 1e-6 inward in box space before mapping it to raw. The nudge
95
+ is silent: it is the kind of difference engineers expect between optimisers.
96
+ - **Raw space is unchanged.** It stays whitened by the prior (ADR-0005).
97
+
98
+ ## Rejected options
99
+
100
+ - **Keep Y from the MNA solver and detect ill-conditioning.** Wrong answers get
101
+ louder but stay wrong.
102
+ - **Substitute eps for zero in models.** It is what `BoxSectionCLCC` did, and
103
+ it zeroes the gradient at exactly the value being fitted.
104
+ - **A tanh map for `Interval`.** Any map ending in `lower + width·u` has an
105
+ absolute precision of about eps·width. tanh only moves the exact point from
106
+ the lower bound to the midpoint, and loses it at a lower bound of 0, the
107
+ common case.
108
+ - **Reject every start on a bound.** Correct, but engineers write bounds they
109
+ mean to include.
110
+ - **Nudge by a fixed raw value.** A raw value of −14 is 1e-6 of a sigmoid's
111
+ width, but 1e-43 of a prior-whitened parameter's slope. The nudge has to be
112
+ in box space to mean the same thing for every parameter.
113
+ - **Track whether a bound came from a range or a validity constraint.**
114
+ Closedness is a property of the constraint; recording it there needs no
115
+ bookkeeping when constraints are intersected.
116
+ - **Box space whitened by the prior**, as Parax does for `Random`. A prior is a
117
+ belief, not the shape of the search; a `Normal` prior made the box edges ±∞,
118
+ kept finite only by a clip.
119
+
120
+ ## Consequences
121
+
122
+ - Circuits with shorted ports, and every component at L, C or R = 0, give the
123
+ same S and gradient under MNA and scattering solvers.
124
+ - `value=0` on a `Positive` field now raises at construction.
125
+ - A `Random` parameter on a bound has raw value ±∞ once Parax drops its clip,
126
+ and is nudged like any other.
127
+ - Fits run through a minimiser that honours bounds now evaluate the objective
128
+ exactly on closed bounds. A test sweep evaluates every component at its
129
+ closed bounds and at zero so this is safe.
130
+ - A fit that started on a bound gives slightly different results than before.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.36.0
3
+ Version: 0.36.2
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
7
7
  Description-Content-Type: text/x-rst
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: parax>=0.11.1
10
+ Requires-Dist: parax>=0.11.5
11
11
  Requires-Dist: threadpoolctl
12
12
  Requires-Dist: eqxpress
13
13
  Requires-Dist: jax
@@ -38,6 +38,7 @@ Requires-Dist: pytest-xdist; extra == "tests"
38
38
  Requires-Dist: filelock; extra == "tests"
39
39
  Requires-Dist: ipython; extra == "tests"
40
40
  Requires-Dist: scikit-rf; extra == "tests"
41
+ Requires-Dist: mpmath; extra == "tests"
41
42
  Requires-Dist: anesthetic; extra == "tests"
42
43
  Requires-Dist: mpi4py; extra == "tests"
43
44
  Requires-Dist: blackjax; extra == "tests"
@@ -29,6 +29,7 @@ docs/adr/0003-derived-models.md
29
29
  docs/adr/0004-profiled-lines.md
30
30
  docs/adr/0005-priors-by-name.md
31
31
  docs/adr/0006-reference-impedance.md
32
+ docs/adr/0007-edge-values.md
32
33
  docs/agents/domain.md
33
34
  docs/agents/issue-tracker.md
34
35
  docs/agents/triage-labels.md
@@ -184,6 +185,7 @@ tests/__init__.py
184
185
  tests/_jit.py
185
186
  tests/conftest.py
186
187
  tests/test_autodiff.py
188
+ tests/test_box_space_solving.py
187
189
  tests/test_conversions.py
188
190
  tests/test_covariance_kernels.py
189
191
  tests/test_dc_limits.py
@@ -224,16 +226,19 @@ tests/test_models/test_circuit_native_z0.py
224
226
  tests/test_models/test_circuit_nodal.py
225
227
  tests/test_models/test_circuit_port_order.py
226
228
  tests/test_models/test_circuit_scattering.py
229
+ tests/test_models/test_circuit_shorted_ports.py
227
230
  tests/test_models/test_circuit_skrf_port_names.py
228
231
  tests/test_models/test_circuit_z0.py
229
232
  tests/test_models/test_current_distribution.py
230
233
  tests/test_models/test_derived.py
234
+ tests/test_models/test_edge_values.py
231
235
  tests/test_models/test_interconnected.py
232
236
  tests/test_models/test_lines.py
233
237
  tests/test_models/test_lines_skrf_matrix.py
234
238
  tests/test_models/test_lumped.py
235
239
  tests/test_models/test_nodal.py
236
240
  tests/test_models/test_profiles.py
241
+ tests/test_models/test_rational.py
237
242
  tests/test_models/test_repeated_cascade.py
238
243
  tests/test_models/test_sections.py
239
244
  tests/test_models/test_transformed.py
@@ -1,4 +1,4 @@
1
- parax>=0.11.1
1
+ parax>=0.11.5
2
2
  threadpoolctl
3
3
  eqxpress
4
4
  jax
@@ -31,6 +31,7 @@ pytest-xdist
31
31
  filelock
32
32
  ipython
33
33
  scikit-rf
34
+ mpmath
34
35
  anesthetic
35
36
  mpi4py
36
37
  blackjax
@@ -0,0 +1,284 @@
1
+ """
2
+ The view of a model a solver moves through, shared by the minimiser and every sampler.
3
+
4
+ A solver never sees the model itself: it sees the free parameters as a name-keyed dict
5
+ of values: in box space for a minimiser that honours bounds, in raw space for one that
6
+ does not and for the joint and split samplers, and in declared space for the hypercube
7
+ samplers. Values are read and written through the public :func:`pmrf.values`,
8
+ :func:`pmrf.update` and :func:`pmrf.log_prior`, so a solver sees exactly what a user
9
+ would with those functions.
10
+
11
+ Box space is the box a bounded minimiser searches (ADR-0007). Along each parameter it is
12
+ built from the constraint's bounds, not its prior (Parax's ``base_bounds`` and
13
+ ``base_bijector``): the unit box between two finite bounds, declared space otherwise. A closed edge of the box is the
14
+ bound itself; an open edge is inset by 1e-6.
15
+ """
16
+
17
+ import dataclasses
18
+ from typing import Any, Callable
19
+
20
+ from jaxtyping import Array, PyTree, Scalar
21
+
22
+ import equinox as eqx
23
+ import jax
24
+ import jax.numpy as jnp
25
+ import parax as prx
26
+
27
+ from pmrf.parameters import (
28
+ Param, is_param, log_prior as _log_prior, params, tree_param_paths, values as _values, update,
29
+ )
30
+ from pmrf.utils import unwrap
31
+ from pmrf.utils.tree import Pathgetter
32
+
33
+
34
+ # How far a start on a closed bound is moved inward, and an open edge of the box is
35
+ # inset, in box space (ADR-0007).
36
+ _NUDGE = 1e-6
37
+
38
+
39
+ def _box_constraint(node: Param | Array) -> prx.constraints.AbstractConstraint | None:
40
+ """Returns the constraint whose bounds give the box of `node`, or None if `node` is
41
+ a raw array or an unconstrained parameter, whose box is its declared space."""
42
+ return prx.unwrap(node.constraint) if is_param(node) else None
43
+
44
+
45
+ def _box_edges(node: Param | Array) -> tuple[Array, Array]:
46
+ """Returns the lower and upper edges of the box of `node`, shaped like its value."""
47
+ value = jnp.asarray(node.value if is_param(node) else node)
48
+ constraint = _box_constraint(node)
49
+ if constraint is None:
50
+ return jnp.full_like(value, -jnp.inf), jnp.full_like(value, jnp.inf)
51
+
52
+ def _like_value(x):
53
+ return jnp.broadcast_to(jnp.asarray(x), value.shape).astype(value.dtype)
54
+
55
+ lower, upper = (_like_value(b) for b in constraint.base_bounds)
56
+ lower_closed, upper_closed = (jnp.broadcast_to(jnp.asarray(c), value.shape) for c in constraint.closed)
57
+ return jnp.where(lower_closed, lower, lower + _NUDGE), jnp.where(upper_closed, upper, upper - _NUDGE)
58
+
59
+
60
+ def _to_box(node: Param | Array) -> Array:
61
+ """Returns the declared value of `node` in its box space."""
62
+ value = jnp.asarray(node.value if is_param(node) else node)
63
+ constraint = _box_constraint(node)
64
+ if constraint is None:
65
+ return value
66
+ return jnp.broadcast_to(constraint.base_bijector.inverse(value), value.shape).astype(value.dtype)
67
+
68
+
69
+ def _from_box(node: Param | Array, box: Array) -> Array:
70
+ """Returns the box-space value `box` of `node` in declared space.
71
+
72
+ The result is kept inside the bounds, which rounding at a closed edge could
73
+ otherwise step past. The comparison leaves the gradient at the edge intact.
74
+ """
75
+ constraint = _box_constraint(node)
76
+ if constraint is None:
77
+ return box
78
+ declared = constraint.base_bijector.forward(box)
79
+ lower, upper = constraint.bounds
80
+ return jnp.where(declared < lower, lower, jnp.where(declared > upper, upper, declared))
81
+
82
+
83
+ def _node_from_box(node: Param | Array, box: Array) -> Param | Array:
84
+ """Returns `node` holding the box-space value `box`, for evaluating the objective.
85
+
86
+ A parameter stays a parameter, with its scale, name and validity, so a derived
87
+ model's builder sees what it would otherwise. It holds its declared value directly,
88
+ not through its raw value, which is infinite on a closed bound, so the value and
89
+ its gradient are finite there.
90
+ """
91
+ declared = _from_box(node, box)
92
+ if not is_param(node):
93
+ return declared
94
+ return dataclasses.replace(node, variable=prx.Real(declared))
95
+
96
+
97
+ def _nudged(param: Param) -> Array | None:
98
+ """Returns the declared value of `param` with each element on a closed bound moved
99
+ 1e-6 inward in box space, or None if no element is on one.
100
+
101
+ Box space is built from the constraint's bounds: the unit box between two finite
102
+ bounds, declared space otherwise. An element also counts as on a bound when it is
103
+ no further from it, in box space, than twice the bound's image through the
104
+ raw-to-declared bijector. A Parax prior whitening clips on the way out of raw
105
+ space, so a prior's value set on its bound reads back slightly inside it, with a
106
+ raw value too far out to move.
107
+ """
108
+ constraint = prx.unwrap(param.constraint)
109
+ if constraint is None:
110
+ return None
111
+ value = param.value
112
+ to_declared = constraint.base_bijector
113
+
114
+ def _like_value(x):
115
+ return jnp.broadcast_to(jnp.asarray(x), value.shape)
116
+
117
+ box = _like_value(to_declared.inverse(value))
118
+ lower, upper = (_like_value(b).astype(value.dtype) for b in constraint.base_bounds)
119
+ lower_closed, upper_closed = (_like_value(c) for c in constraint.closed)
120
+ ends = [
121
+ _like_value(to_declared.inverse(constraint.bijector.forward(jnp.full(value.shape, x, value.dtype))))
122
+ for x in (-jnp.inf, jnp.inf)
123
+ ]
124
+ unknown = jnp.isnan(ends[0]) | jnp.isnan(ends[1])
125
+ lower_image = jnp.where(unknown, lower, jnp.minimum(*ends))
126
+ upper_image = jnp.where(unknown, upper, jnp.maximum(*ends))
127
+
128
+ at_lower = lower_closed & jnp.isfinite(lower) & (box - lower <= 2 * (lower_image - lower))
129
+ at_upper = upper_closed & jnp.isfinite(upper) & ~at_lower & (upper - box <= 2 * (upper - upper_image))
130
+ if not bool(jnp.any(at_lower | at_upper)):
131
+ return None
132
+
133
+ nudged = to_declared.forward(jnp.where(at_lower, lower + _NUDGE, upper - _NUDGE))
134
+ return jnp.where(at_lower | at_upper, nudged, value).astype(value.dtype)
135
+
136
+
137
+ class SolverView:
138
+ """
139
+ The free parameters of a model, as the dict of values a solver moves through.
140
+
141
+ The starting values :attr:`y0` are raw, and :meth:`updated` and :meth:`objective`
142
+ take a ``space`` so a solver working in declared space, such as a hypercube
143
+ sampler, or in box space, such as a minimiser that honours bounds, can use the
144
+ same view. :meth:`box` gives the start and edges in box space.
145
+
146
+ A parameter starting on a closed bound has an infinite raw value, so :attr:`y0`
147
+ starts it 1e-6 inward in box space instead (ADR-0007). The model keeps
148
+ the value it was given.
149
+
150
+ Parameters
151
+ ----------
152
+ model : PyTree
153
+ A model, or any collection of models and parameters.
154
+ action : str
155
+ What the solver does, such as ``'optimize'``, for the error raised when there
156
+ is nothing to move.
157
+
158
+ Raises
159
+ ------
160
+ ValueError
161
+ If `model` has no free parameters.
162
+ """
163
+
164
+ def __init__(self, model: PyTree, action: str):
165
+ #: The model the values are written into.
166
+ self.model = model
167
+ #: What the solver does, used in error messages.
168
+ self.action = action
169
+ raw = _values(model, free_only=True, space='raw')
170
+ if not raw:
171
+ raise ValueError(
172
+ f"Nothing to {action}: the tree has no free parameters. Every parameter is "
173
+ "either fixed or a plain value."
174
+ )
175
+ nudged = {}
176
+ for name, param in params(model, free_only=True).items():
177
+ if is_param(param) and (value := _nudged(param)) is not None:
178
+ nudged[name] = value
179
+ if nudged:
180
+ raw = _values(update(model, nudged), free_only=True, space='raw')
181
+ #: The starting raw values of the free parameters, by name, with each start on
182
+ #: a closed bound nudged inward.
183
+ self.y0 = raw
184
+ # The free parameters' paths and nodes, by name, for box space.
185
+ self._free = tree_param_paths(model, free_only=True)
186
+
187
+ def box(self) -> tuple[dict, tuple[dict, dict]]:
188
+ """Returns the start and the box a bounded minimiser searches, in box space.
189
+
190
+ A start on a closed bound stays on it. A start outside the box, which can only
191
+ be within 1e-6 of an open bound, is moved onto its edge.
192
+
193
+ Returns
194
+ -------
195
+ tuple
196
+ ``(y0, (lower, upper))``: the starting box values of the free parameters
197
+ and the lower and upper edges of the box, each a dict by name.
198
+ """
199
+ lower, upper, y0 = {}, {}, {}
200
+ for name, (_, node) in self._free.items():
201
+ lower[name], upper[name] = _box_edges(node)
202
+ y0[name] = jnp.clip(_to_box(node), lower[name], upper[name])
203
+ return y0, (lower, upper)
204
+
205
+ def check_finite(self) -> None:
206
+ """Checks that no starting raw value is NaN, so a solver can move it.
207
+
208
+ Only for solvers that move raw values.
209
+
210
+ Raises
211
+ ------
212
+ ValueError
213
+ If a starting raw value is NaN.
214
+ """
215
+ def _leaves(v):
216
+ return [jnp.asarray(x) for x in jax.tree.leaves(v)]
217
+
218
+ nan = [name for name, v in self.y0.items() if any(bool(jnp.any(jnp.isnan(x))) for x in _leaves(v))]
219
+ if nan:
220
+ raise ValueError(
221
+ f"Cannot {self.action}: {', '.join(repr(name) for name in nan)} start at NaN, "
222
+ "which no solver can move. Give them a finite starting value."
223
+ )
224
+
225
+ def updated(self, values: dict, space: str = 'raw') -> PyTree:
226
+ """Returns the model with `values`, by name and in `space`, written into it.
227
+
228
+ `space` is ``'box'`` or any space :func:`pmrf.update` takes. Batched values give
229
+ a batched model; fixed parameters stay unbatched.
230
+ """
231
+ if space == 'box':
232
+ declared = {name: _from_box(self._free[name][1], v) for name, v in values.items()}
233
+ return update(self.model, declared, space='declared')
234
+ return update(self.model, values, space=space)
235
+
236
+ def read(self, batch: PyTree) -> dict:
237
+ """Returns the raw values of the free parameters of `batch`, a model like this one.
238
+
239
+ Raises
240
+ ------
241
+ ValueError
242
+ If a free parameter of this model is missing from `batch`, or is fixed there.
243
+ """
244
+ values = _values(batch, free_only=True, space='raw')
245
+ missing = [name for name in self.y0 if name not in values]
246
+ if missing:
247
+ raise ValueError(
248
+ f"Cannot {self.action}: {', '.join(repr(name) for name in missing)} "
249
+ f"{'is' if len(missing) == 1 else 'are'} free in the model but missing or "
250
+ "fixed in the given tree. It must have the same free parameters as the model."
251
+ )
252
+ return {name: values[name] for name in self.y0}
253
+
254
+ def objective(self, fn: Callable[[PyTree, Any], Scalar], space: str = 'raw') -> Callable[[dict, Any], Scalar]:
255
+ """Returns `fn`, which takes the unwrapped model, as a function of values in `space`.
256
+
257
+ In box space each free parameter holds its declared value directly, rather than
258
+ through its raw value, which is infinite on a closed bound. So the objective and
259
+ its gradient are finite there.
260
+ """
261
+ if space == 'box':
262
+ names = list(self._free)
263
+ getter = Pathgetter(*(path for path, _ in self._free.values()))
264
+
265
+ def box_value_fn(values: dict, args: Any) -> Scalar:
266
+ nodes = [_node_from_box(self._free[name][1], values[name]) for name in names]
267
+ model = eqx.tree_at(getter, self.model, nodes[0] if len(nodes) == 1 else tuple(nodes))
268
+ return fn(unwrap(model), args)
269
+ return box_value_fn
270
+
271
+ def value_fn(values: dict, args: Any) -> Scalar:
272
+ return fn(unwrap(self.updated(values, space)), args)
273
+ return value_fn
274
+
275
+ def log_prior(self, values: dict, _args: Any = None) -> Scalar:
276
+ """Returns the raw-space log prior of the model at `values`."""
277
+ return _log_prior(self.updated(values), space='raw')
278
+
279
+ def log_posterior(self, loglikelihood_fn: Callable[[PyTree, Any], Scalar]) -> Callable[[dict, Any], Scalar]:
280
+ """Returns the raw-space log posterior: `loglikelihood_fn` plus the raw log prior."""
281
+ def value_fn(values: dict, args: Any) -> Scalar:
282
+ model = self.updated(values)
283
+ return loglikelihood_fn(unwrap(model), args) + _log_prior(model, space='raw')
284
+ return value_fn
@@ -12,6 +12,8 @@ from parax.constraints import (
12
12
  RealLine as RealLine,
13
13
  Positive as Positive,
14
14
  Negative as Negative,
15
+ NonNegative as NonNegative,
16
+ NonPositive as NonPositive,
15
17
  Interval as Interval,
16
18
  GreaterThan as GreaterThan,
17
19
  LessThan as LessThan,
@@ -25,6 +27,8 @@ __all__ = [
25
27
  'RealLine',
26
28
  'Positive',
27
29
  'Negative',
30
+ 'NonNegative',
31
+ 'NonPositive',
28
32
  'Interval',
29
33
  'GreaterThan',
30
34
  'LessThan',
@@ -237,9 +237,10 @@ def run_sampler(
237
237
  :func:`pmrf.update`, so fixed parameters, names, scales and priors are unchanged,
238
238
  and only free parameters are batched.
239
239
 
240
- A parameter starting exactly on one of its bounds has an infinite raw value and
241
- could not move, so a joint or split sampler raises; start it inside its bounds.
242
- A hypercube sampler works in declared space and accepts it.
240
+ A parameter starting exactly on a closed bound has an infinite raw value, so a
241
+ joint or split sampler starts it just inside: by 1e-6 of the width between two
242
+ finite bounds, or 1e-6 in declared units otherwise. A hypercube sampler works in
243
+ declared space and starts it on the bound.
243
244
 
244
245
  Parameters
245
246
  ----------
@@ -271,7 +272,7 @@ def run_sampler(
271
272
  ------
272
273
  ValueError
273
274
  If `model` has no free parameters, a joint or split sampler is given a
274
- parameter starting on a bound, `init_samples` is missing a free parameter, or
275
+ parameter starting at NaN, `init_samples` is missing a free parameter, or
275
276
  a hypercube sampler is given a free parameter without a prior, a prior with no
276
277
  inverse CDF, or a joint prior with no independent normal base.
277
278
  """
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from abc import abstractmethod
11
11
 
12
12
  import jax.numpy as jnp
13
- from pmrf.constraints import Positive, GreaterThan, Interval
13
+ from pmrf.constraints import Positive, NonNegative, GreaterThan, Interval
14
14
  from pmrf.frequency import Frequency
15
15
  from pmrf.modules.base import Module
16
16
  from pmrf.parameters import Param, param
@@ -89,10 +89,10 @@ class ConstantDielectric(AbstractDielectric):
89
89
  ep_r: Param = param(default=1.0, constraint=GreaterThan(1.0))
90
90
 
91
91
  #: Dielectric loss tangent
92
- tand: Param = param(default=0.0, constraint=Positive())
92
+ tand: Param = param(default=0.0, constraint=NonNegative())
93
93
 
94
94
  #: Static bulk conductivity in S/m
95
- sigma: Param = param(default=0.0, constraint=Positive())
95
+ sigma: Param = param(default=0.0, constraint=NonNegative())
96
96
 
97
97
  #: Relative permeability of the medium
98
98
  mu_r: Param = param(default=1.0, constraint=Positive())
@@ -155,7 +155,7 @@ class DjordjevicSarkarDielectric(AbstractDielectric):
155
155
  ep_r: Param = param(default=1.0, constraint=GreaterThan(1.0))
156
156
 
157
157
  #: Dielectric loss tangent at `f_ref`
158
- tand: Param = param(default=0.0, constraint=Positive())
158
+ tand: Param = param(default=0.0, constraint=NonNegative())
159
159
 
160
160
  #: Lower bound of the relaxation-time distribution
161
161
  f_low: Param = param(default=1e3, constraint=Positive())
@@ -167,7 +167,7 @@ class DjordjevicSarkarDielectric(AbstractDielectric):
167
167
  f_ref: Param = param(default=1e9, constraint=Positive())
168
168
 
169
169
  #: Static bulk conductivity in S/m
170
- sigma: Param = param(default=0.0, constraint=Positive())
170
+ sigma: Param = param(default=0.0, constraint=NonNegative())
171
171
 
172
172
  def properties(self, freq: Frequency) -> DielectricProperties:
173
173
  f = freq.f
@@ -201,7 +201,7 @@ class DebyePole(Module):
201
201
  Relaxation frequency of the pole in Hz.
202
202
  """
203
203
  #: Permittivity increment of the pole
204
- dep_r: Param = param(default=0.0, constraint=Positive())
204
+ dep_r: Param = param(default=0.0, constraint=NonNegative())
205
205
 
206
206
  #: Relaxation frequency of the pole
207
207
  f_relax: Param = param(default=1e9, constraint=Positive())
@@ -256,7 +256,7 @@ class MultipoleDebyeDielectric(AbstractDielectric):
256
256
  poles: tuple[DebyePole, ...] = field(default=(), converter=_as_poles)
257
257
 
258
258
  #: Static bulk conductivity in S/m
259
- sigma: Param = param(default=0.0, constraint=Positive())
259
+ sigma: Param = param(default=0.0, constraint=NonNegative())
260
260
 
261
261
  def properties(self, freq: Frequency) -> DielectricProperties:
262
262
  eps = self.ep_inf * jnp.ones(freq.npoints, dtype=complex)
@@ -300,7 +300,7 @@ class ColeColeDielectric(AbstractDielectric):
300
300
  ep_inf: Param = param(default=1.0, constraint=GreaterThan(1.0))
301
301
 
302
302
  #: Permittivity increment
303
- dep_r: Param = param(default=0.0, constraint=Positive())
303
+ dep_r: Param = param(default=0.0, constraint=NonNegative())
304
304
 
305
305
  #: Relaxation frequency
306
306
  f_relax: Param = param(default=1e9, constraint=Positive())
@@ -309,7 +309,7 @@ class ColeColeDielectric(AbstractDielectric):
309
309
  alpha: Param = param(default=0.0, constraint=Interval(0.0, 1.0))
310
310
 
311
311
  #: Static bulk conductivity in S/m
312
- sigma: Param = param(default=0.0, constraint=Positive())
312
+ sigma: Param = param(default=0.0, constraint=NonNegative())
313
313
 
314
314
  def properties(self, freq: Frequency) -> DielectricProperties:
315
315
  # Guard f = 0, where (jf/f_r)**(1-alpha) is a branch point.
@@ -355,7 +355,7 @@ class TabulatedDielectric(AbstractDielectric):
355
355
  ep_r: jnp.ndarray = field(converter=lambda x: jnp.asarray(x, dtype=complex))
356
356
 
357
357
  #: Static bulk conductivity in S/m
358
- sigma: Param = param(default=0.0, constraint=Positive())
358
+ sigma: Param = param(default=0.0, constraint=NonNegative())
359
359
 
360
360
  def __check_init__(self):
361
361
  if self.f.ndim != 1 or self.ep_r.ndim != 1: