paramrf 0.35.2__tar.gz → 0.35.3__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 (231) hide show
  1. {paramrf-0.35.2 → paramrf-0.35.3}/CONTEXT.md +15 -3
  2. {paramrf-0.35.2 → paramrf-0.35.3}/PKG-INFO +1 -1
  3. {paramrf-0.35.2 → paramrf-0.35.3}/docs/adr/0002-parameter-api.md +16 -7
  4. paramrf-0.35.3/docs/adr/0003-derived-models.md +80 -0
  5. {paramrf-0.35.2 → paramrf-0.35.3}/docs/api/index.rst +1 -0
  6. {paramrf-0.35.2 → paramrf-0.35.3}/docs/core_concepts/parameter_names.rst +40 -0
  7. {paramrf-0.35.2 → paramrf-0.35.3}/paramrf.egg-info/PKG-INFO +1 -1
  8. {paramrf-0.35.2 → paramrf-0.35.3}/paramrf.egg-info/SOURCES.txt +3 -0
  9. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/__init__.py +2 -0
  10. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/__init__.py +5 -0
  11. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/__init__.py +2 -1
  12. paramrf-0.35.3/pmrf/models/adapters/derived.py +144 -0
  13. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/parameters.py +54 -10
  14. {paramrf-0.35.2 → paramrf-0.35.3}/pyproject.toml +1 -1
  15. paramrf-0.35.3/tests/test_models/test_derived.py +234 -0
  16. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_structural_updates.py +71 -0
  17. {paramrf-0.35.2 → paramrf-0.35.3}/.github/workflows/docs.yml +0 -0
  18. {paramrf-0.35.2 → paramrf-0.35.3}/.github/workflows/draft-pdf.yml +0 -0
  19. {paramrf-0.35.2 → paramrf-0.35.3}/.github/workflows/publish.yml +0 -0
  20. {paramrf-0.35.2 → paramrf-0.35.3}/.github/workflows/tests.yml +0 -0
  21. {paramrf-0.35.2 → paramrf-0.35.3}/.gitignore +0 -0
  22. {paramrf-0.35.2 → paramrf-0.35.3}/AGENTS.md +0 -0
  23. {paramrf-0.35.2 → paramrf-0.35.3}/CHANGELOG.md +0 -0
  24. {paramrf-0.35.2 → paramrf-0.35.3}/CITATION.cff +0 -0
  25. {paramrf-0.35.2 → paramrf-0.35.3}/CLAUDE.md +0 -0
  26. {paramrf-0.35.2 → paramrf-0.35.3}/CONTRIBUTING.md +0 -0
  27. {paramrf-0.35.2 → paramrf-0.35.3}/LICENSE +0 -0
  28. {paramrf-0.35.2 → paramrf-0.35.3}/NOTICE +0 -0
  29. {paramrf-0.35.2 → paramrf-0.35.3}/README.rst +0 -0
  30. {paramrf-0.35.2 → paramrf-0.35.3}/assets/logo.png +0 -0
  31. {paramrf-0.35.2 → paramrf-0.35.3}/docs/Makefile +0 -0
  32. {paramrf-0.35.2 → paramrf-0.35.3}/docs/_static/custom.css +0 -0
  33. {paramrf-0.35.2 → paramrf-0.35.3}/docs/_templates/autosummary/class.rst +0 -0
  34. {paramrf-0.35.2 → paramrf-0.35.3}/docs/_templates/autosummary/function.rst +0 -0
  35. {paramrf-0.35.2 → paramrf-0.35.3}/docs/_templates/autosummary/module.rst +0 -0
  36. {paramrf-0.35.2 → paramrf-0.35.3}/docs/adr/0001-line-modelling-architecture.md +0 -0
  37. {paramrf-0.35.2 → paramrf-0.35.3}/docs/agents/domain.md +0 -0
  38. {paramrf-0.35.2 → paramrf-0.35.3}/docs/agents/issue-tracker.md +0 -0
  39. {paramrf-0.35.2 → paramrf-0.35.3}/docs/agents/triage-labels.md +0 -0
  40. {paramrf-0.35.2 → paramrf-0.35.3}/docs/conf.py +0 -0
  41. {paramrf-0.35.2 → paramrf-0.35.3}/docs/core_concepts/core_primitives.rst +0 -0
  42. {paramrf-0.35.2 → paramrf-0.35.3}/docs/core_concepts/index.rst +0 -0
  43. {paramrf-0.35.2 → paramrf-0.35.3}/docs/core_concepts/jax_overview.rst +0 -0
  44. {paramrf-0.35.2 → paramrf-0.35.3}/docs/core_concepts/optimization_and_inference.rst +0 -0
  45. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/cascading_and_terminating.rst +0 -0
  46. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/circuit_clc.png +0 -0
  47. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/circuit_models.rst +0 -0
  48. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/custom_models.rst +0 -0
  49. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/derivatives_and_sweeps.rst +0 -0
  50. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/index.rst +0 -0
  51. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/model_optimization.rst +0 -0
  52. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/multiple_models_one_parameter_set.rst +0 -0
  53. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/parameter_naming_and_model_manipulation.rst +0 -0
  54. {paramrf-0.35.2 → paramrf-0.35.3}/docs/examples/shared_substrates.rst +0 -0
  55. {paramrf-0.35.2 → paramrf-0.35.3}/docs/index.rst +0 -0
  56. {paramrf-0.35.2 → paramrf-0.35.3}/docs/license.rst +0 -0
  57. {paramrf-0.35.2 → paramrf-0.35.3}/docs/make.bat +0 -0
  58. {paramrf-0.35.2 → paramrf-0.35.3}/docs/research/conductor-loss-alternatives.md +0 -0
  59. {paramrf-0.35.2 → paramrf-0.35.3}/docs/research/holloway1994.pdf +0 -0
  60. {paramrf-0.35.2 → paramrf-0.35.3}/docs/research/microstrip-loss-conventions.md +0 -0
  61. {paramrf-0.35.2 → paramrf-0.35.3}/docs/research/precision-coax-microstrip-10-500mhz.md +0 -0
  62. {paramrf-0.35.2 → paramrf-0.35.3}/docs/skrf_comparison/index.rst +0 -0
  63. {paramrf-0.35.2 → paramrf-0.35.3}/docs/skrf_comparison/overview.rst +0 -0
  64. {paramrf-0.35.2 → paramrf-0.35.3}/docs/skrf_comparison/performance.rst +0 -0
  65. {paramrf-0.35.2 → paramrf-0.35.3}/docs/tutorials/1_cable_fitting.ipynb +0 -0
  66. {paramrf-0.35.2 → paramrf-0.35.3}/docs/tutorials/2_chip_inductor_fitting.ipynb +0 -0
  67. {paramrf-0.35.2 → paramrf-0.35.3}/docs/tutorials/data/CBN-1.5FT-SMSM.s2p +0 -0
  68. {paramrf-0.35.2 → paramrf-0.35.3}/docs/tutorials/data/on-chip-inductor.s2p +0 -0
  69. {paramrf-0.35.2 → paramrf-0.35.3}/docs/tutorials/index.rst +0 -0
  70. {paramrf-0.35.2 → paramrf-0.35.3}/paper/paper.bib +0 -0
  71. {paramrf-0.35.2 → paramrf-0.35.3}/paper/paper.md +0 -0
  72. {paramrf-0.35.2 → paramrf-0.35.3}/paper/rlc.png +0 -0
  73. {paramrf-0.35.2 → paramrf-0.35.3}/paramrf.egg-info/dependency_links.txt +0 -0
  74. {paramrf-0.35.2 → paramrf-0.35.3}/paramrf.egg-info/requires.txt +0 -0
  75. {paramrf-0.35.2 → paramrf-0.35.3}/paramrf.egg-info/top_level.txt +0 -0
  76. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/_solver_view.py +0 -0
  77. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/bijectors.py +0 -0
  78. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/constraints.py +0 -0
  79. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/covariance_kernels.py +0 -0
  80. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/discrepancy_models.py +0 -0
  81. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/distributions.py +0 -0
  82. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/evaluators.py +0 -0
  83. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/__init__.py +0 -0
  84. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/minimize.py +0 -0
  85. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/result.py +0 -0
  86. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/routers.py +0 -0
  87. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/sample.py +0 -0
  88. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/fitting/targets.py +0 -0
  89. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/frequency.py +0 -0
  90. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/__init__.py +0 -0
  91. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/base.py +0 -0
  92. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/result.py +0 -0
  93. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/sample.py +0 -0
  94. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/solvers/__init__.py +0 -0
  95. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/solvers/blackjax.py +0 -0
  96. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/infer/solvers/polychord.py +0 -0
  97. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/likelihoods.py +0 -0
  98. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/losses.py +0 -0
  99. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/__init__.py +0 -0
  100. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/conductor.py +0 -0
  101. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/dielectric.py +0 -0
  102. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/properties.py +0 -0
  103. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/roughness.py +0 -0
  104. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/substrate.py +0 -0
  105. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/materials/surface_impedance.py +0 -0
  106. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/__init__.py +0 -0
  107. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/aggregations.py +0 -0
  108. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/bessel.py +0 -0
  109. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/conversions.py +0 -0
  110. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/losses.py +0 -0
  111. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/math/misc.py +0 -0
  112. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/base.py +0 -0
  113. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/bridge.py +0 -0
  114. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/callable.py +0 -0
  115. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/delegated.py +0 -0
  116. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/static.py +0 -0
  117. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/adapters/wrapped.py +0 -0
  118. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/base.py +0 -0
  119. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/__init__.py +0 -0
  120. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/ideal.py +0 -0
  121. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/__init__.py +0 -0
  122. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/base.py +0 -0
  123. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/coaxial.py +0 -0
  124. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/empirical.py +0 -0
  125. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/ideal.py +0 -0
  126. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/microstrip.py +0 -0
  127. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/nodal.py +0 -0
  128. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/nonuniform.py +0 -0
  129. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/planar.py +0 -0
  130. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lines/stripline.py +0 -0
  131. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/lumped.py +0 -0
  132. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/components/sections.py +0 -0
  133. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/__init__.py +0 -0
  134. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/__init__.py +0 -0
  135. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/cascade.py +0 -0
  136. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/__init__.py +0 -0
  137. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/base.py +0 -0
  138. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/circuit.py +0 -0
  139. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/solvers/__init__.py +0 -0
  140. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/solvers/nodal.py +0 -0
  141. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/circuit/solvers/scattering.py +0 -0
  142. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/interconnected/terminated.py +0 -0
  143. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/nodal.py +0 -0
  144. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/topological.py +0 -0
  145. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/composite/transformed.py +0 -0
  146. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/surrogates/__init__.py +0 -0
  147. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/surrogates/expansion.py +0 -0
  148. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/models/surrogates/rational.py +0 -0
  149. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/modules/__init__.py +0 -0
  150. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/modules/base.py +0 -0
  151. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/modules/wrapped.py +0 -0
  152. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/network_collection.py +0 -0
  153. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/noise_models.py +0 -0
  154. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/__init__.py +0 -0
  155. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/base.py +0 -0
  156. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/minimize.py +0 -0
  157. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/result.py +0 -0
  158. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/solvers/__init__.py +0 -0
  159. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/solvers/jaxopt.py +0 -0
  160. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/solvers/optimistix.py +0 -0
  161. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/optimize/solvers/scipy.py +0 -0
  162. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/problems.py +0 -0
  163. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/rf/__init__.py +0 -0
  164. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/rf/conversions.py +0 -0
  165. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/rf/mna.py +0 -0
  166. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/serialization.py +0 -0
  167. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/terms.py +0 -0
  168. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/types.py +0 -0
  169. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/__init__.py +0 -0
  170. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/array.py +0 -0
  171. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/debug.py +0 -0
  172. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/network.py +0 -0
  173. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/random.py +0 -0
  174. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/rf.py +0 -0
  175. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/transforms.py +0 -0
  176. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/tree.py +0 -0
  177. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/utils/type.py +0 -0
  178. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/viz/__init__.py +0 -0
  179. {paramrf-0.35.2 → paramrf-0.35.3}/pmrf/viz/plots.py +0 -0
  180. {paramrf-0.35.2 → paramrf-0.35.3}/scripts/install-test-deps.sh +0 -0
  181. {paramrf-0.35.2 → paramrf-0.35.3}/setup.cfg +0 -0
  182. {paramrf-0.35.2 → paramrf-0.35.3}/tests/__init__.py +0 -0
  183. {paramrf-0.35.2 → paramrf-0.35.3}/tests/_dependency_checks.py +0 -0
  184. {paramrf-0.35.2 → paramrf-0.35.3}/tests/_jit.py +0 -0
  185. {paramrf-0.35.2 → paramrf-0.35.3}/tests/conftest.py +0 -0
  186. {paramrf-0.35.2 → paramrf-0.35.3}/tests/data/10m_cable.s2p +0 -0
  187. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_autodiff.py +0 -0
  188. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_conversions.py +0 -0
  189. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_covariance_kernels.py +0 -0
  190. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_dc_limits.py +0 -0
  191. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_evaluators.py +0 -0
  192. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_fitting_minimize.py +0 -0
  193. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_fitting_routers.py +0 -0
  194. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_fitting_sample.py +0 -0
  195. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_fitting_targets.py +0 -0
  196. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_frequency.py +0 -0
  197. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_infer_base.py +0 -0
  198. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_infer_sample.py +0 -0
  199. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_map_priors.py +0 -0
  200. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_materials/test_conductor.py +0 -0
  201. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_materials/test_dielectric.py +0 -0
  202. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_materials/test_serialization.py +0 -0
  203. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_materials/test_substrate.py +0 -0
  204. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_materials/test_surface_impedance.py +0 -0
  205. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_math/test_bessel.py +0 -0
  206. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_model.py +0 -0
  207. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_adapters.py +0 -0
  208. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_circuit_nodal.py +0 -0
  209. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_circuit_port_order.py +0 -0
  210. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_circuit_scattering.py +0 -0
  211. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_current_distribution.py +0 -0
  212. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_interconnected.py +0 -0
  213. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_lines.py +0 -0
  214. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_lines_skrf_matrix.py +0 -0
  215. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_lumped.py +0 -0
  216. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_nodal.py +0 -0
  217. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_sections.py +0 -0
  218. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_transformed.py +0 -0
  219. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_models/test_transformers.py +0 -0
  220. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_module.py +0 -0
  221. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_naming.py +0 -0
  222. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_optimize_base.py +0 -0
  223. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_optimize_minimize.py +0 -0
  224. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_parameters.py +0 -0
  225. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_parameters_by_name.py +0 -0
  226. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_raw_space_solving.py +0 -0
  227. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_serialization.py +0 -0
  228. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_terms.py +0 -0
  229. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_transforms.py +0 -0
  230. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_utils/test_compress.py +0 -0
  231. {paramrf-0.35.2 → paramrf-0.35.3}/tests/test_utils/test_tree.py +0 -0
@@ -45,13 +45,25 @@ on a value overrides a field's default; scales never multiply.
45
45
 
46
46
  `prf.update` returns a copy of a model with the parts a **selector** picks
47
47
  replaced. A selector is a parameter name, a glob over names, a sequence of
48
- names, or a callable. A **validated update** (a name → value mapping,
49
- `value=`, `fixed=`) goes through each parameter's constructor; a **structural
50
- update** (a new node, `fn=`) bypasses validation. `prf.replace` is the plain
48
+ names, or a callable. A **validated update** (a name → value mapping
49
+ entry, `value=`, `fixed=`) goes through each parameter's constructor; a
50
+ **structural update** (a new node, `fn=`, or a mapping entry whose value is a
51
+ `Model`, keyed by sub-model name) bypasses validation. In a mapping the tier is
52
+ decided per entry by the value's type. `prf.replace` is the plain
51
53
  dataclass field replace, not an update.
52
54
 
53
55
  *Avoid:* "set values", "with values"; "update" for an optimiser step.
54
56
 
57
+ ### Derived model
58
+
59
+ A model computed from a **base** model and **new parameters** by a function,
60
+ `f(base, **new)`, built with `prf.derived` (ADR-0003). The base and the new
61
+ parameters are held once; the base keeps its names and each new parameter is
62
+ named by its keyword. Used to derive a more complete model from a nominal one
63
+ (a wet section, a cut) and, by nesting, to share a parameter across parts.
64
+
65
+ *Avoid:* "tie with new parameters", "shared parameter" as a separate concept.
66
+
55
67
  ### Parameter values
56
68
 
57
69
  `prf.param_values`: a name-keyed dict of arrays in one space, the form values
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.35.2
3
+ Version: 0.35.3
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
@@ -85,6 +85,7 @@ replaced. Exactly one form says what with:
85
85
 
86
86
  ```python
87
87
  prf.update(model, {'L1.L': 3.0, 'C1.C': 2.0}) # parameter values by name
88
+ prf.update(model, {'east': wet(model.east), 'west': wet(model.west)}) # sub-models by name
88
89
  prf.update(model, 'L1.L', value=3.0) # parameter fields on a selection
89
90
  prf.update(model, 'cable.*', fixed=True) # fixed state
90
91
  prf.update(model, 'cascade[1]', Short()) # a new sub-model or node
@@ -98,19 +99,26 @@ It replaces `with_values`, `with_free`, `with_fixed`, `Module.map`,
98
99
 
99
100
  - **Selectors** are parameter names, `fnmatch` globs over them, sequences of
100
101
  names, or callables, resolved by the #133 resolver.
101
- - **Two tiers, set by the form.** The mapping, `value=` and `fixed=` forms go
102
+ - **Two tiers, set by the form, or per entry in a mapping.** The `value=` and
103
+ `fixed=` forms, and mapping entries whose value is an array or `Param`, go
102
104
  through each parameter's constructor: they validate bounds and keep the prior,
103
- constraint, scale, name and metadata. The node and `fn=` forms are
104
- structural: they bypass converters and validation, and the docstring says so.
105
+ constraint, scale, name and metadata. The node and `fn=` forms, and mapping
106
+ entries whose value is a `Model`, are structural: they bypass converters and
107
+ validation, and the docstring says so.
105
108
  - **The mapping form** is recognised only as the second positional argument
106
- with every key a string. Its values may be arrays or `Param` objects. Any
109
+ with every key a string. A value that is an array or `Param` is keyed by a
110
+ parameter name, and `space=` applies to it. A value that is a `Model` is keyed
111
+ by a sub-model name (`'cascade[1]'`, a named module's name) and replaces that
112
+ sub-model as the node form does (amended by #168, so several sub-models can be
113
+ replaced in one call). One mapping may mix both; a model for a parameter name,
114
+ or a value for a sub-model name, raises. Any
107
115
  other second argument is a selector, and a form mismatch raises an error that
108
116
  lists the forms.
109
117
  - **`fixed=`** is additive. `fixed=False` frees a parameter even if it was
110
118
  created fixed, and parameters the selector does not match are untouched.
111
119
  "Only these free" is `update(update(m, '*', fixed=True), names, fixed=False)`.
112
- - **Value forms keep the jit cache key.** The mapping, `value=` and `space=`
113
- forms never change the treedef, or any leaf's dtype, shape or `weak_type`
120
+ - **Value forms keep the jit cache key.** Mappings of values only, and the
121
+ `value=` and `space=` forms, never change the treedef, or any leaf's dtype, shape or `weak_type`
114
122
  (decision 10). Changing `fixed=`, or any structural form, changes the model's
115
123
  structure, and recompiling is expected.
116
124
  - **Not an optimiser step.** In fitting, "updates" also means Optax gradient
@@ -122,7 +130,8 @@ anything name-based.
122
130
 
123
131
  `prf.tie(model, target, source, fn=identity)` stays a separate verb. A tie is
124
132
  not a replacement: its target is recomputed from its source every time the
125
- model is unwrapped.
133
+ model is unwrapped. A relation that needs a quantity the model does not yet hold
134
+ is a derived model (`prf.derived`, ADR-0003), not a tie.
126
135
 
127
136
  ### 4. Reading: `prf.params` and `prf.param_values`
128
137
 
@@ -0,0 +1,80 @@
1
+ # ADR-0003: Derived models add parameters by deriving from a whole base
2
+
3
+ Status: accepted (2026-09)
4
+
5
+ ## Context
6
+
7
+ Users constrain parameters by relations such as "this part is computed from
8
+ those". `prf.tie` (ADR-0002) covers this only when every source already exists
9
+ in the model. It fails when the relation needs a quantity that has no home.
10
+
11
+ The driving case (#167): a coaxial cable of total length L is wet for its first
12
+ w and dry for the rest. The model is `wet ** dry`, but neither section can hold
13
+ L. L must stay a parameter with its own prior, possibly a joint lab prior with
14
+ the cable's geometry and material, and must not drift as w changes. w is a new
15
+ parameter with its own prior. Several parts may share one such parameter (one
16
+ water level on both arms of a balun).
17
+
18
+ ## Decision
19
+
20
+ `prf.derived` turns a function `f(base, **new)` returning a model into a
21
+ constructor of a **derived model**: a `pmrf.Model` (`pmrf.models.Derived`, an
22
+ `AbstractBuilder`) that holds the base and the new parameters once and calls
23
+ `f` on their unwrapped values whenever the model is used. `f` is a static field,
24
+ so models derived with one function share a jit cache entry.
25
+
26
+ ### The base stays whole
27
+
28
+ The derived model holds the base model itself, not parameters extracted from
29
+ it. This is what makes two things work with no extra machinery:
30
+
31
+ - **Joint priors.** A `Probabilistic` prior over the base (length with
32
+ geometry) is still in the tree, over the same parameters, under the same
33
+ names, and scores unchanged.
34
+ - **Sharing with no ties.** `f` uses the base's geometry in both sections; it
35
+ is one parameter because it is stored once. No tie is needed to keep the wet
36
+ and dry sections consistent.
37
+
38
+ ### Naming rule
39
+
40
+ - The base's parameters keep exactly the names they have on the base. The
41
+ wrapper is transparent to the name resolver, and the base's own name moves to
42
+ the derived model (unless `name=` is given), so a container prefixes as usual.
43
+ - Each new parameter is named by its keyword; a keyword that clashes with a
44
+ base name raises.
45
+ - Nothing produced inside `f` is named: it is not in the tree.
46
+
47
+ A values dict saved from a fit of the base therefore applies unchanged to the
48
+ derived model.
49
+
50
+ ### Sharing by nesting
51
+
52
+ A parameter shared by several parts is a new parameter of a derived model at
53
+ the level that owns them. Its `f` derives each part, passing the same value,
54
+ and puts them back with the multi-model `prf.update` mapping form (#168). A
55
+ derived model can be the base of another; names accumulate flat. There is no
56
+ separate "shared parameter" concept.
57
+
58
+ ## Rejected options
59
+
60
+ - **A tie with new parameters** (`tie(..., new={...})` and other tie-centric
61
+ designs). L ends up separate from the geometry and material, so no joint prior
62
+ covers them, and the dry section needs extra ties to share the geometry.
63
+ - **A separate "add parameters" function** followed by a tie. Two steps for one
64
+ idea, and the added parameters have no relation to the model until tied, with
65
+ the same joint-prior problem.
66
+ - **Reparametrising a parameter in place** (replace L by a function of new
67
+ parameters). The relation is not a function of one parameter: it replaces
68
+ part of the model's structure (one section becomes two), and L loses its prior.
69
+ - **Builder classes.** They work, but an engineer should not need a class for
70
+ a one-off constraint, and topology code had to reach into the class.
71
+ `prf.derived` is a builder whose class is generated from a function.
72
+
73
+ ## Consequences
74
+
75
+ - `tie` is unchanged.
76
+ - `f` must be pure and its output's structure must not depend on parameter
77
+ values, as for `AbstractBuilder.build`. It should be defined once at module
78
+ level; a lambda made on every call recompiles.
79
+ - A non-model return is only detected when the model is used, since calling
80
+ `f` eagerly at construction would repeat the work at every nesting level.
@@ -31,6 +31,7 @@ Working with Parameters
31
31
  pmrf.log_prior
32
32
  pmrf.update
33
33
  pmrf.tie
34
+ pmrf.derived
34
35
 
35
36
 
36
37
  Main Modules
@@ -40,9 +40,49 @@ Models are immutable, so :func:`pmrf.update` returns a changed copy rather than
40
40
 
41
41
  **Structural changes** replace a part of the model outright, either with a new sub-model or parameter, or with the result of a function applied to the old part. These put exactly what they are given in place, without validation. An exact name can select a whole sub-model here, whereas a glob only ever matches parameters.
42
42
 
43
+ A dictionary passed to :func:`pmrf.update` can hold both kinds. Each entry is decided by its value: a :class:`pmrf.Model` keyed by a sub-model name is a structural change, unvalidated, while an array or parameter keyed by a parameter name is a value change. For example, ``prf.update(system, {'east_coax': new_east, 'west_coax': new_west})`` replaces two sub-models in one call. A model given for a parameter name, or a value given for a sub-model name, raises an error.
44
+
43
45
  The distinction matters for performance. RF methods such as :meth:`pmrf.Model.s` are compiled just-in-time, and the compiled code is only reused while the model's structure is unchanged. Changing a parameter's value keeps that structure, so it never triggers a recompile. Fixing or freeing a parameter, making a structural change, or swapping in a parameter with a different constraint or scale all change the structure, and so recompile. On a large circuit this can take noticeably longer than an evaluation, so value changes should be preferred inside loops.
44
46
 
45
47
  Tied Parameters
46
48
  ~~~~~~~~~~~~~~~
47
49
 
48
50
  Rather than setting a parameter once, :func:`pmrf.tie` derives it from another parameter. The target is removed from the model's parameters and is recomputed from its source every time the model is evaluated, so it follows the source through updates, optimization and sampling. Because it is no longer a parameter, it also no longer has a name. The tie function receives and returns physical values, and derivatives with respect to the source include the path through the tie.
51
+
52
+ Derived Models
53
+ ~~~~~~~~~~~~~~
54
+
55
+ A tie can only relate parameters that already exist. Sometimes a relation needs a quantity the model has no place for. Take a coaxial cable of total length ``L`` that is wet for its first ``w``: the model is a wet section cascaded with a dry one, but neither section can hold ``L``, and ``L`` should keep its own prior (perhaps a joint lab prior with the cable's geometry) rather than drift as ``w`` changes.
56
+
57
+ :func:`pmrf.derived` handles this by starting from the nominal model and deriving a more complete one. It turns a function ``f(base, **new)`` into a constructor: the base model and the new parameters are held once, and ``f`` is called on them whenever the model is used.
58
+
59
+ .. code-block:: python
60
+
61
+ @prf.derived
62
+ def wet(cable, wet_length, wet_ep_r):
63
+ wet = prf.replace(cable, length=wet_length,
64
+ dielectric=prf.replace(cable.dielectric, ep_r=wet_ep_r))
65
+ dry = prf.replace(cable, length=cable.length - wet_length)
66
+ return wet ** dry
67
+
68
+ coax = wet(coax, wet_length=prf.Random(Uniform(0, 20), scale=1e-3),
69
+ wet_ep_r=prf.Random(Uniform(1, 80)))
70
+
71
+ The result is an ordinary :class:`pmrf.Model` with the same port count, so it can be cascaded, wrapped, tied and fitted like any other. Its parameters are the base's, under exactly the names they had on the base, plus one per keyword (``wet_length`` and ``wet_ep_r``). The cable's geometry is used by both sections but is still one parameter, and a values dictionary saved from a fit of the dry cable applies unchanged. Nothing built inside ``f`` is named, and the derived model takes the base's name, so inside a named container everything is prefixed as usual.
72
+
73
+ Like a tie function, ``f`` receives physical values. Inside it, use :func:`pmrf.replace` to change fields of the object in hand, and :func:`pmrf.update` to change parts reached by name. ``f`` must be pure and must return a model whose structure does not depend on parameter values. It is part of the model's static structure, so define it once at module level: a lambda created anew on every call recompiles.
74
+
75
+ A parameter shared by several parts is expressed by deriving at the level that owns it. A derived model can be the base of another, and names accumulate flat, so one water level for both arms of a balun is:
76
+
77
+ .. code-block:: python
78
+
79
+ @prf.derived
80
+ def wet_balun(system, wet_length):
81
+ return prf.update(system, {
82
+ 'east_coax': wet(system.east_coax, wet_length=wet_length, wet_ep_r=80.0),
83
+ 'west_coax': wet(system.west_coax, wet_length=wet_length, wet_ep_r=80.0),
84
+ })
85
+
86
+ balun = wet_balun(balun, wet_length=prf.Random(Uniform(0, 20), scale=1e-3))
87
+
88
+ Here ``prf.params(balun)`` holds a single ``wet_length``, and changing it changes both cables. The reasoning behind this design is recorded in ADR-0003.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.35.2
3
+ Version: 0.35.3
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
@@ -25,6 +25,7 @@ docs/_templates/autosummary/function.rst
25
25
  docs/_templates/autosummary/module.rst
26
26
  docs/adr/0001-line-modelling-architecture.md
27
27
  docs/adr/0002-parameter-api.md
28
+ docs/adr/0003-derived-models.md
28
29
  docs/agents/domain.md
29
30
  docs/agents/issue-tracker.md
30
31
  docs/agents/triage-labels.md
@@ -115,6 +116,7 @@ pmrf/models/adapters/base.py
115
116
  pmrf/models/adapters/bridge.py
116
117
  pmrf/models/adapters/callable.py
117
118
  pmrf/models/adapters/delegated.py
119
+ pmrf/models/adapters/derived.py
118
120
  pmrf/models/adapters/static.py
119
121
  pmrf/models/adapters/wrapped.py
120
122
  pmrf/models/components/__init__.py
@@ -214,6 +216,7 @@ tests/test_models/test_circuit_nodal.py
214
216
  tests/test_models/test_circuit_port_order.py
215
217
  tests/test_models/test_circuit_scattering.py
216
218
  tests/test_models/test_current_distribution.py
219
+ tests/test_models/test_derived.py
217
220
  tests/test_models/test_interconnected.py
218
221
  tests/test_models/test_lines.py
219
222
  tests/test_models/test_lines_skrf_matrix.py
@@ -46,6 +46,7 @@ except PackageNotFoundError:
46
46
  from pmrf.models import (
47
47
  Model as Model,
48
48
  is_model as is_model,
49
+ derived as derived,
49
50
  )
50
51
  from pmrf.modules.base import Module as Module, is_module as is_module
51
52
  from pmrf.frequency import Frequency as Frequency
@@ -125,6 +126,7 @@ __all__ = [
125
126
  # Base/Core
126
127
  "Model",
127
128
  "is_model",
129
+ "derived",
128
130
  "Module",
129
131
  "is_module",
130
132
  "Frequency",
@@ -198,6 +198,11 @@ from pmrf.models.adapters.wrapped import (
198
198
  Wrapped as Wrapped,
199
199
  )
200
200
 
201
+ from pmrf.models.adapters.derived import (
202
+ Derived as Derived,
203
+ derived as derived,
204
+ )
205
+
201
206
  # Compatibility re-exports. Parameter-aware wrappers live under ``pmrf.modules``.
202
207
  from pmrf.modules import Tied as Tied, Probabilistic as Probabilistic
203
208
 
@@ -7,7 +7,7 @@ This includes scikit-rf Networks, EM simulation software, and generic Equinox mo
7
7
  """
8
8
 
9
9
  from pmrf.models.adapters import base
10
- from pmrf.models.adapters import bridge, static, callable, delegated, wrapped
10
+ from pmrf.models.adapters import bridge, static, callable, delegated, derived, wrapped
11
11
 
12
12
  __all__ = [
13
13
  "base",
@@ -15,6 +15,7 @@ __all__ = [
15
15
  "static",
16
16
  "callable",
17
17
  "delegated",
18
+ "derived",
18
19
  "wrapped",
19
20
  ]
20
21
 
@@ -0,0 +1,144 @@
1
+ """Models derived from an existing model with new parameters."""
2
+
3
+ import copy
4
+ import functools
5
+ from typing import Any, Callable
6
+
7
+ from pmrf.models.base import Model
8
+ from pmrf.models.adapters.delegated import AbstractBuilder
9
+ from pmrf.utils import field
10
+
11
+
12
+ class Derived(AbstractBuilder):
13
+ """A model computed by a function from a base and new named parameters.
14
+
15
+ Built by :func:`derived`, which documents the contract. The base and the new
16
+ parameters are held once, as ``operands = (base, new)``, and ``fn(base, **new)``
17
+ is called on their unwrapped values whenever the model is used. Parameter names
18
+ see through this wrapper: the base's names are unchanged and each new parameter
19
+ is named by its keyword.
20
+ """
21
+
22
+ #: The base, and the new parameters keyed by keyword.
23
+ operands: tuple[Any, dict[str, Any]]
24
+
25
+ #: The function building the model from the base and the new parameters.
26
+ fn: Callable[..., Model] = field(static=True, kw_only=True)
27
+
28
+ def build(self) -> Model:
29
+ base, new = self.operands
30
+ model = self.fn(base, **new)
31
+ if not isinstance(model, Model):
32
+ raise TypeError(
33
+ f"The derived function {getattr(self.fn, '__qualname__', self.fn)!r} must "
34
+ f"return a pmrf.Model; got {type(model).__name__}."
35
+ )
36
+ return model
37
+
38
+
39
+ def _unnamed(node: Any) -> Any:
40
+ """Returns `node` without its own name, so its parameters keep their names below it."""
41
+ if getattr(node, 'name', None) is None:
42
+ return node
43
+ node = copy.copy(node)
44
+ object.__setattr__(node, 'name', None)
45
+ return node
46
+
47
+
48
+ def derived(fn: Callable[..., Model]) -> Callable[..., Derived]:
49
+ """
50
+ Turns a function of a base model and new parameters into a derived model constructor.
51
+
52
+ A derived model starts from a nominal model and derives a more complete one that
53
+ captures some artifact (a wet section, degraded copper, a cut). The decorated
54
+ function is called as ``f(base, **new)``: exactly one positional base (a model,
55
+ or any collection of models and parameters) and keyword arguments that are the
56
+ new parameters (a :class:`pmrf.Param`, or anything :func:`pmrf.as_param`
57
+ accepts). It returns a :class:`pmrf.Model` that holds the base and the new
58
+ parameters once, and calls `fn` on them whenever the model is used.
59
+
60
+ `fn` receives the base and the new parameters as the model is unwrapped, so
61
+ parameters arrive as physical values, as for :func:`pmrf.tie`. It must return a
62
+ :class:`pmrf.Model`, must be pure, and its output's structure and port count must
63
+ not depend on parameter values. Inside `fn`, use :func:`pmrf.replace` for fields
64
+ of the object in hand and :func:`pmrf.update` for parts reached by name.
65
+
66
+ **Names.** The base's parameters keep the names they have on the base, and each
67
+ new parameter is named by its keyword. Nothing produced inside `fn` is named. The
68
+ derived model takes the base's name unless ``name=`` is passed, so a container
69
+ prefixes both as usual. A derived model can be the base of another, and names
70
+ accumulate flat; a parameter shared by several parts is expressed by deriving at
71
+ the level that owns it.
72
+
73
+ `fn` is a static part of the model: two models derived with the same function
74
+ share a jit cache entry. Define it once at module level; a lambda created on every
75
+ call recompiles.
76
+
77
+ Parameters
78
+ ----------
79
+ fn : Callable
80
+ ``fn(base, **new) -> Model``.
81
+
82
+ Returns
83
+ -------
84
+ Callable
85
+ ``constructor(base, *, name=None, **new) -> Derived``.
86
+
87
+ Raises
88
+ ------
89
+ TypeError
90
+ On a call with other than one positional base or with no new parameters, or
91
+ (when the model is used) if `fn` does not return a model.
92
+ ValueError
93
+ If a keyword clashes with a parameter name of the base.
94
+
95
+ Examples
96
+ --------
97
+ A coaxial cable of total length ``L`` that is wet for its first ``wet_length``:
98
+
99
+ .. code-block:: python
100
+
101
+ @prf.derived
102
+ def wet(cable, wet_length, wet_ep_r):
103
+ wet = prf.replace(cable, length=wet_length,
104
+ dielectric=prf.replace(cable.dielectric, ep_r=wet_ep_r))
105
+ dry = prf.replace(cable, length=cable.length - wet_length)
106
+ return wet ** dry
107
+
108
+ coax = wet(coax, wet_length=prf.Random(Uniform(0, 20), scale=1e-3),
109
+ wet_ep_r=prf.Random(Uniform(1, 80)))
110
+ prf.params(coax) # the cable's names, plus 'wet_length' and 'wet_ep_r'
111
+ """
112
+ from pmrf.parameters import as_param, params
113
+
114
+ @functools.wraps(fn)
115
+ def constructor(*args, name: str | None = None, **new) -> Derived:
116
+ if len(args) != 1:
117
+ raise TypeError(
118
+ f"{fn.__name__}() takes exactly one positional base and new parameters as "
119
+ f"keywords, e.g. {fn.__name__}(model, wet_length=...); got {len(args)} "
120
+ "positional arguments."
121
+ )
122
+ if not new:
123
+ raise TypeError(f"{fn.__name__}() needs at least one new parameter as a keyword.")
124
+ base = args[0]
125
+ clashes = sorted(set(new) & set(params(base)))
126
+ if clashes:
127
+ raise ValueError(
128
+ f"{fn.__name__}(): new parameter names {clashes} clash with parameters of "
129
+ "the base. Choose different keywords."
130
+ )
131
+ coerced = {}
132
+ for key, value in new.items():
133
+ value = as_param(value)
134
+ if value.name is not None:
135
+ value = _unnamed(value)
136
+ coerced[key] = value
137
+ if name is None:
138
+ name = getattr(base, 'name', None)
139
+ return Derived((_unnamed(base), coerced), fn=fn, name=name)
140
+
141
+ return constructor
142
+
143
+
144
+ __all__ = ["Derived", "derived"]
@@ -955,11 +955,12 @@ def _is_name_leaf(x: Any) -> bool:
955
955
 
956
956
  def _is_name_transparent(x: Any) -> bool:
957
957
  """Wrappers whose own path parts are omitted from parameter names."""
958
+ from pmrf.models.adapters.derived import Derived
958
959
  from pmrf.models.adapters.wrapped import Wrapped
959
960
  from pmrf.modules.base import Module
960
961
  from pmrf.modules.wrapped import Probabilistic, Tied
961
962
 
962
- if isinstance(x, (Tied, Probabilistic, Wrapped)):
963
+ if isinstance(x, (Tied, Probabilistic, Wrapped, Derived)):
963
964
  return True
964
965
  return isinstance(x, prx.AbstractUnwrappable) and not isinstance(x, Module) and not is_param(x)
965
966
 
@@ -1378,15 +1379,20 @@ def _select_parts(tree, where: Selector) -> list[tuple[Any, ...]]:
1378
1379
  paths.extend(resolved[name][0] for name in hits)
1379
1380
 
1380
1381
  unique = list(dict.fromkeys(paths))
1381
- for a in unique:
1382
- for b in unique:
1382
+ _check_no_overlap(unique)
1383
+ return unique
1384
+
1385
+
1386
+ def _check_no_overlap(paths: list[tuple[Any, ...]]):
1387
+ """Raises if one of the selected paths contains another."""
1388
+ for a in paths:
1389
+ for b in paths:
1383
1390
  if a != b and b[:len(a)] == a:
1384
1391
  raise ValueError("The selected parts overlap: one contains another.")
1385
- return unique
1386
1392
 
1387
1393
 
1388
1394
  _UPDATE_FORMS = """prf.update takes one of these forms:
1389
- update(model, {'name': value, ...}, space=...) values by name
1395
+ update(model, {'name': value, ...}, space=...) values by name, or sub-models by name
1390
1396
  update(model, where, value=..., space=...) one value for the selected parameters
1391
1397
  update(model, where, fixed=True or False) fixed state of the selected parameters
1392
1398
  update(model, where, node) replace the selected parts with `node`
@@ -1417,6 +1423,7 @@ def update(
1417
1423
  .. code-block:: python
1418
1424
 
1419
1425
  prf.update(model, {'L1.L': 3.0, 'C1.C': 2.0}) # values by name
1426
+ prf.update(model, {'load': Short(), 'L1.L': 3.0}) # sub-models and values by name
1420
1427
  prf.update(model, 'L1.*', value=3.0) # one value for a selection
1421
1428
  prf.update(model, 'cable.*', fixed=True) # fixed state
1422
1429
  prf.update(model, 'cascade[1]', Short()) # a new sub-model or node
@@ -1434,6 +1441,13 @@ def update(
1434
1441
  and put exactly what they are given in place of each selected part, so the caller
1435
1442
  keeps field invariants. They usually change the structure, and recompile.
1436
1443
 
1444
+ In the mapping form the tier is decided per entry by the value's type. A
1445
+ :class:`pmrf.Model` value is a structural replacement of the sub-model its key
1446
+ names, as in ``update(model, key, node)``: it is unvalidated and usually
1447
+ recompiles, and `space` does not apply to it. Any other value is a validated
1448
+ value update of the parameter its key names. A mapping of values only keeps the
1449
+ jit cache key.
1450
+
1437
1451
  ``fixed=`` is additive: parameters the selector does not match are untouched,
1438
1452
  and ``fixed=False`` frees a parameter even if it was created fixed. It does not
1439
1453
  unfreeze: a parameter inside a :func:`pmrf.freeze` sub-tree stays frozen. To
@@ -1452,7 +1466,9 @@ def update(
1452
1466
  Either a mapping from names to values, or a selector: a name, an `fnmatch`
1453
1467
  glob over names, a sequence of them, or a callable returning nodes of
1454
1468
  `tree`. A mapping is recognised only when every key is a string. Its values
1455
- may be arrays, or parameters, whose value in `space` is used. Omit it to
1469
+ may be arrays, or parameters, whose value in `space` is used, keyed by
1470
+ parameter name; or models, keyed by sub-model name, which replace that
1471
+ sub-model structurally. Omit it to
1456
1472
  update `tree` itself, which must then be a parameter. In the structural
1457
1473
  forms, an exact name may also name a sub-model (``'cascade[1]'``, or a named
1458
1474
  module's name), a glob matches parameter names only, and a callable selects
@@ -1480,7 +1496,8 @@ def update(
1480
1496
  structurally selected parts overlap. Under `jax.jit` the bounds check raises
1481
1497
  at runtime.
1482
1498
  TypeError
1483
- If the arguments match none of the forms.
1499
+ If the arguments match none of the forms, or a mapping gives a model for a
1500
+ parameter name or a non-model for a sub-model name.
1484
1501
  """
1485
1502
  has_value = value is not _MISSING
1486
1503
  has_fixed = fixed is not None
@@ -1517,12 +1534,39 @@ def update(
1517
1534
  if isinstance(selection, Mapping):
1518
1535
  if has_value or has_fixed or not all(isinstance(k, str) for k in selection):
1519
1536
  raise form_error()
1537
+ from pmrf.models.base import Model
1538
+
1520
1539
  resolved = tree_param_paths(tree)
1521
- unknown = [name for name in selection if name not in resolved]
1540
+ submodels = None
1541
+ paths, nodes, unknown = [], [], []
1542
+ for name, v in selection.items():
1543
+ # A joint target is named like a parameter, and its value may itself be a model.
1544
+ if isinstance(v, Model) and not (name in resolved and _is_joint_target(resolved[name][1])):
1545
+ if name in resolved:
1546
+ raise TypeError(f"'{name}' is a parameter name, but its value is a model; "
1547
+ "a model can only replace a sub-model.")
1548
+ if submodels is None:
1549
+ submodels = _tree_submodel_paths(tree)
1550
+ if name not in submodels:
1551
+ raise ValueError(f"Unknown sub-model name: '{name}'")
1552
+ if len(submodels[name]) > 1:
1553
+ raise ValueError(f"Sub-model name '{name}' is ambiguous: several sub-models have it.")
1554
+ paths.append(submodels[name][0])
1555
+ nodes.append(v)
1556
+ elif name in resolved:
1557
+ paths.append(resolved[name][0])
1558
+ nodes.append(_write(resolved[name][1], v, space))
1559
+ else:
1560
+ if submodels is None:
1561
+ submodels = _tree_submodel_paths(tree)
1562
+ if name in submodels:
1563
+ raise TypeError(f"'{name}' names a sub-model, but its value is not a model; "
1564
+ "only a pmrf.Model can replace a sub-model.")
1565
+ unknown.append(name)
1522
1566
  if unknown:
1523
1567
  raise ValueError(f"Unknown parameter names: {unknown}")
1524
- paths = [resolved[name][0] for name in selection]
1525
- nodes = [_write(resolved[name][1], v, space) for name, v in selection.items()]
1568
+ if submodels is not None:
1569
+ _check_no_overlap(paths)
1526
1570
  return _set_paths(tree, paths, nodes)
1527
1571
 
1528
1572
  if not _is_selector(selection) or has_value == has_fixed:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "paramrf"
3
- version = "0.35.2"
3
+ version = "0.35.3"
4
4
  description = "Parametric radio frequency modeling"
5
5
  readme = "README.rst"
6
6
  authors = [