paramrf 0.28.2__tar.gz → 0.28.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 (166) hide show
  1. {paramrf-0.28.2/paramrf.egg-info → paramrf-0.28.3}/PKG-INFO +1 -1
  2. {paramrf-0.28.2 → paramrf-0.28.3}/docs/skrf_comparison/index.rst +1 -1
  3. {paramrf-0.28.2 → paramrf-0.28.3}/docs/skrf_comparison/overview.rst +1 -1
  4. {paramrf-0.28.2 → paramrf-0.28.3}/docs/skrf_comparison/performance.rst +7 -7
  5. {paramrf-0.28.2 → paramrf-0.28.3/paramrf.egg-info}/PKG-INFO +1 -1
  6. {paramrf-0.28.2 → paramrf-0.28.3}/paramrf.egg-info/SOURCES.txt +0 -1
  7. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/ideal.py +4 -1
  8. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/base.py +7 -13
  9. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/circuit.py +75 -64
  10. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/solvers/scattering.py +51 -69
  11. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/rf/conversions.py +23 -12
  12. {paramrf-0.28.2 → paramrf-0.28.3}/pyproject.toml +1 -1
  13. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_circuit_scattering.py +30 -30
  14. paramrf-0.28.2/docs/skrf_comparison.rst +0 -0
  15. {paramrf-0.28.2 → paramrf-0.28.3}/.github/workflows/docs.yml +0 -0
  16. {paramrf-0.28.2 → paramrf-0.28.3}/.github/workflows/draft-pdf.yml +0 -0
  17. {paramrf-0.28.2 → paramrf-0.28.3}/.github/workflows/publish.yml +0 -0
  18. {paramrf-0.28.2 → paramrf-0.28.3}/.github/workflows/tests.yml +0 -0
  19. {paramrf-0.28.2 → paramrf-0.28.3}/.gitignore +0 -0
  20. {paramrf-0.28.2 → paramrf-0.28.3}/CONTRIBUTING.md +0 -0
  21. {paramrf-0.28.2 → paramrf-0.28.3}/LICENSE +0 -0
  22. {paramrf-0.28.2 → paramrf-0.28.3}/README.rst +0 -0
  23. {paramrf-0.28.2 → paramrf-0.28.3}/assets/logo.png +0 -0
  24. {paramrf-0.28.2 → paramrf-0.28.3}/docs/Makefile +0 -0
  25. {paramrf-0.28.2 → paramrf-0.28.3}/docs/_static/custom.css +0 -0
  26. {paramrf-0.28.2 → paramrf-0.28.3}/docs/_templates/autosummary/class.rst +0 -0
  27. {paramrf-0.28.2 → paramrf-0.28.3}/docs/_templates/autosummary/function.rst +0 -0
  28. {paramrf-0.28.2 → paramrf-0.28.3}/docs/_templates/autosummary/module.rst +0 -0
  29. {paramrf-0.28.2 → paramrf-0.28.3}/docs/api/index.rst +0 -0
  30. {paramrf-0.28.2 → paramrf-0.28.3}/docs/conf.py +0 -0
  31. {paramrf-0.28.2 → paramrf-0.28.3}/docs/core_concepts/core_primitives.rst +0 -0
  32. {paramrf-0.28.2 → paramrf-0.28.3}/docs/core_concepts/index.rst +0 -0
  33. {paramrf-0.28.2 → paramrf-0.28.3}/docs/core_concepts/jax_overview.rst +0 -0
  34. {paramrf-0.28.2 → paramrf-0.28.3}/docs/core_concepts/optimization_and_inference.rst +0 -0
  35. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/cascading_and_terminating.rst +0 -0
  36. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/circuit_clc.png +0 -0
  37. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/circuit_models.rst +0 -0
  38. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/custom_composite_models.rst +0 -0
  39. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/custom_parametric_models.rst +0 -0
  40. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/derivatives_and_sweeps.rst +0 -0
  41. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/index.rst +0 -0
  42. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/model_optimization.rst +0 -0
  43. {paramrf-0.28.2 → paramrf-0.28.3}/docs/examples/parameter_naming_and_model_manipulation.rst +0 -0
  44. {paramrf-0.28.2 → paramrf-0.28.3}/docs/index.rst +0 -0
  45. {paramrf-0.28.2 → paramrf-0.28.3}/docs/license.rst +0 -0
  46. {paramrf-0.28.2 → paramrf-0.28.3}/docs/make.bat +0 -0
  47. {paramrf-0.28.2 → paramrf-0.28.3}/docs/tutorials/1_cable_fitting.ipynb +0 -0
  48. {paramrf-0.28.2 → paramrf-0.28.3}/docs/tutorials/2_chip_inductor_fitting.ipynb +0 -0
  49. {paramrf-0.28.2 → paramrf-0.28.3}/docs/tutorials/data/CBN-1.5FT-SMSM.s2p +0 -0
  50. {paramrf-0.28.2 → paramrf-0.28.3}/docs/tutorials/data/on-chip-inductor.s2p +0 -0
  51. {paramrf-0.28.2 → paramrf-0.28.3}/docs/tutorials/index.rst +0 -0
  52. {paramrf-0.28.2 → paramrf-0.28.3}/paper/paper.bib +0 -0
  53. {paramrf-0.28.2 → paramrf-0.28.3}/paper/paper.md +0 -0
  54. {paramrf-0.28.2 → paramrf-0.28.3}/paper/rlc.png +0 -0
  55. {paramrf-0.28.2 → paramrf-0.28.3}/paramrf.egg-info/dependency_links.txt +0 -0
  56. {paramrf-0.28.2 → paramrf-0.28.3}/paramrf.egg-info/requires.txt +0 -0
  57. {paramrf-0.28.2 → paramrf-0.28.3}/paramrf.egg-info/top_level.txt +0 -0
  58. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/__init__.py +0 -0
  59. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/constraints.py +0 -0
  60. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/covariance_kernels.py +0 -0
  61. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/discrepancy_models.py +0 -0
  62. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/distributions.py +0 -0
  63. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/evaluators.py +0 -0
  64. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/fitting/__init__.py +0 -0
  65. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/fitting/minimize.py +0 -0
  66. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/fitting/result.py +0 -0
  67. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/fitting/routers.py +0 -0
  68. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/fitting/sample.py +0 -0
  69. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/frequency.py +0 -0
  70. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/__init__.py +0 -0
  71. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/base.py +0 -0
  72. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/result.py +0 -0
  73. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/sample.py +0 -0
  74. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/solvers/__init__.py +0 -0
  75. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/solvers/blackjax.py +0 -0
  76. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/infer/solvers/polychord.py +0 -0
  77. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/likelihoods.py +0 -0
  78. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/losses.py +0 -0
  79. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/math/__init__.py +0 -0
  80. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/math/aggregations.py +0 -0
  81. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/math/conversions.py +0 -0
  82. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/math/losses.py +0 -0
  83. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/math/misc.py +0 -0
  84. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/__init__.py +0 -0
  85. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/adapters/__init__.py +0 -0
  86. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/adapters/base.py +0 -0
  87. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/adapters/bridge.py +0 -0
  88. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/adapters/callable.py +0 -0
  89. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/adapters/static.py +0 -0
  90. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/base.py +0 -0
  91. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/__init__.py +0 -0
  92. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/__init__.py +0 -0
  93. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/base.py +0 -0
  94. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/ideal.py +0 -0
  95. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/nodal.py +0 -0
  96. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/nonuniform.py +0 -0
  97. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lines/physical.py +0 -0
  98. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/lumped.py +0 -0
  99. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/components/sections.py +0 -0
  100. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/__init__.py +0 -0
  101. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/__init__.py +0 -0
  102. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/cascade.py +0 -0
  103. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/__init__.py +0 -0
  104. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/solvers/__init__.py +0 -0
  105. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/circuit/solvers/nodal.py +0 -0
  106. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/interconnected/terminated.py +0 -0
  107. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/nodal.py +0 -0
  108. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/topological.py +0 -0
  109. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/transformed.py +0 -0
  110. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/composite/wrapped.py +0 -0
  111. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/surrogates/__init__.py +0 -0
  112. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/surrogates/expansion.py +0 -0
  113. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/models/surrogates/rational.py +0 -0
  114. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/network_collection.py +0 -0
  115. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/noise_models.py +0 -0
  116. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/__init__.py +0 -0
  117. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/base.py +0 -0
  118. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/minimize.py +0 -0
  119. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/result.py +0 -0
  120. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/solvers/__init__.py +0 -0
  121. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/solvers/jaxopt.py +0 -0
  122. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/solvers/optimistix.py +0 -0
  123. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/optimize/solvers/scipy.py +0 -0
  124. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/parameters.py +0 -0
  125. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/problem.py +0 -0
  126. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/rf/__init__.py +0 -0
  127. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/rf/mna.py +0 -0
  128. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/serialization.py +0 -0
  129. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/types.py +0 -0
  130. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/__init__.py +0 -0
  131. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/array.py +0 -0
  132. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/debug.py +0 -0
  133. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/network.py +0 -0
  134. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/optix.py +0 -0
  135. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/random.py +0 -0
  136. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/rf.py +0 -0
  137. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/transforms.py +0 -0
  138. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/tree.py +0 -0
  139. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/utils/type.py +0 -0
  140. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/viz/__init__.py +0 -0
  141. {paramrf-0.28.2 → paramrf-0.28.3}/pmrf/viz/plots.py +0 -0
  142. {paramrf-0.28.2 → paramrf-0.28.3}/setup.cfg +0 -0
  143. {paramrf-0.28.2 → paramrf-0.28.3}/tests/__init__.py +0 -0
  144. {paramrf-0.28.2 → paramrf-0.28.3}/tests/data/10m_cable.s2p +0 -0
  145. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_autodiff.py +0 -0
  146. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_conversions.py +0 -0
  147. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_evaluators.py +0 -0
  148. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_fitting_minimize.py +0 -0
  149. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_fitting_routers.py +0 -0
  150. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_fitting_sample.py +0 -0
  151. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_frequency.py +0 -0
  152. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_infer_base.py +0 -0
  153. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_infer_sample.py +0 -0
  154. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_model.py +0 -0
  155. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_adapters.py +0 -0
  156. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_circuit_nodal.py +0 -0
  157. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_interconnected.py +0 -0
  158. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_lines.py +0 -0
  159. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_lumped.py +0 -0
  160. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_nodal.py +0 -0
  161. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_sections.py +0 -0
  162. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_models/test_transformed.py +0 -0
  163. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_naming.py +0 -0
  164. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_optimize_base.py +0 -0
  165. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_optimize_minimize.py +0 -0
  166. {paramrf-0.28.2 → paramrf-0.28.3}/tests/test_transforms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.28.2
3
+ Version: 0.28.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
@@ -3,7 +3,7 @@ scikit-rf Comparison
3
3
 
4
4
  ParamRF aims to complement and not replace scikit-rf. While scikit-rf excels at static network manipulation, it does not take a parameter-first modeling approach, and also suffers from Python interpreter overhead and finite difference approximations which JAX eleviates.
5
5
 
6
- This section provides a brief comparison between the core philosophies of scikit-rf and ParamRF, and also demonstrates a short benchmarking scripting showing the performance benefits when using JAX.
6
+ This section provides a brief comparison between the core philosophies of scikit-rf and ParamRF, and also demonstrates a short benchmarking script showing the performance benefits when using JAX.
7
7
 
8
8
  .. toctree::
9
9
  :maxdepth: 2
@@ -1,4 +1,4 @@
1
- scikit-rf Comparison
1
+ Overview
2
2
  =====================
3
3
  **ParamRF** is designed to complement **scikit-rf** and not replace it, offering a fundamentally different approach to RF modeling. A very brief summary of the differences, strengths and weakness of the libraries is presented below.
4
4
 
@@ -181,18 +181,18 @@ Running this script on a standard modern CPU yields results similar to the follo
181
181
  =======================================================
182
182
  FORWARD PASS EXECUTION TIMES (2001 Points)
183
183
  =======================================================
184
- scikit-rf (Baseline) | 20.799 ms | 1.00x
184
+ scikit-rf (Baseline) | 25.343 ms | 1.00x
185
185
  -------------------------------------------------------
186
- ParamRF Scattering Solver | 6.458 ms | 3.22x
187
- ParamRF MNA Solver | 3.139 ms | 6.63x
186
+ ParamRF Scattering Solver | 4.823 ms | 5.25x
187
+ ParamRF MNA Solver | 2.979 ms | 8.51x
188
188
  =======================================================
189
189
 
190
- By vectorizing the linear matrix solutions and bypassing the Python interpreter via JIT compilation, ParamRF's MNA solver evaluates nearly 7x faster
190
+ By vectorizing the linear matrix solutions and bypassing the Python interpreter via JIT compilation, ParamRF's MNA solver evaluates nearly 9x faster, while the scattering solver evaluates 5x faster (which is most similar to the scattering algorithm in scikit-rf's Circuit).
191
191
 
192
192
  The Autodiff Advantage
193
193
  ----------------------
194
- While a 6x speedup on the forward pass is significant, the true performance gain of ParamRF is realized during gradient-based optimization (the *backward pass*).
194
+ While a 5-10x speedup on the forward pass is significant, the true performance gain of ParamRF is realized during gradient-based optimization (the *backward pass*).
195
195
 
196
- In traditional libraries, calculating the gradient for an $N$-parameter circuit requires Finite Differences (FD), meaning the circuit must be simulated $N+1$ times per iteration. For a 50-parameter model, this means a single gradient step in ``scikit-rf`` would take over **1.3 seconds** (``51 * 26.7 ms``).
196
+ In traditional libraries, calculating the gradient for an $N$-parameter circuit requires Finite Differences (FD), meaning the circuit must be simulated $N+1$ times per iteration. For a 50-parameter model, this means a single gradient step in ``scikit-rf`` would take **1.3 seconds** (``51 * 25.3 ms``).
197
197
 
198
- In ParamRF, because the entire circuit math is traced, calling ``jax.value_and_grad`` yields the exact analytic gradients for all 50 parameters simultaneously in a single, compiled backward pass—often executing in **less than 15 ms**. This results in a massive speedup (often 40x to 100x) when optimizing complex circuits.
198
+ In ParamRF, because the entire circuit math is traced, calling ``jax.value_and_grad`` yields the exact analytic gradients for all 50 parameters simultaneously in a single, compiled backward pass—often executing in **less than 10 ms**. This results in a massive speedup (often 40x to 100x) when optimizing complex circuits.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramrf
3
- Version: 0.28.2
3
+ Version: 0.28.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
@@ -13,7 +13,6 @@ docs/conf.py
13
13
  docs/index.rst
14
14
  docs/license.rst
15
15
  docs/make.bat
16
- docs/skrf_comparison.rst
17
16
  docs/_static/custom.css
18
17
  docs/_templates/autosummary/class.rst
19
18
  docs/_templates/autosummary/function.rst
@@ -104,7 +104,10 @@ class Port(Model):
104
104
  """
105
105
  Represents a circuit port with a specific characteristic impedance.
106
106
 
107
- This class serves as a placeholder or marker for external connections in a :class:`pmrf.models.Circuit` definition.
107
+ This class serves as a placeholder or marker for external connections
108
+ in a :class:`pmrf.models.Circuit` definition, though can also be used
109
+ as a simple tagged load in other types of models.
110
+
108
111
  Calling `build` returns a matched load model.
109
112
  """
110
113
  #: Port characteristic impedance
@@ -69,25 +69,19 @@ class MNARepresentation(eqx.Module):
69
69
  def num_aux(self) -> int:
70
70
  """The total number of global auxiliary variables."""
71
71
  return self.aux_idx.shape[0]
72
-
73
-
72
+
73
+
74
74
  class PortRepresentation(eqx.Module):
75
- """
76
- Static topological representation for scattering parameter connection and reduction.
77
- """
78
- #: A 1D array of port indices designated as external (unconnected or specifically retained).
79
- ext_idx: np.ndarray
80
-
81
- #: A 1D array of port indices designated as internal (connected to other ports).
82
- int_idx: np.ndarray
83
-
84
- #: A 1D array of length `num_ports` mapping each port to the integer ID of its connected net.
75
+ #: Array mapping each active device port to a net ID
85
76
  port_to_net_map: np.ndarray
86
77
 
78
+ #: Ordered array of net IDs that act as external ports
79
+ ext_net_ids: np.ndarray
80
+
87
81
  @property
88
82
  def num_ports(self) -> int:
89
83
  """The total number of ports across all components in the topology."""
90
- return self.port_to_net_map.shape[0]
84
+ return self.port_to_net_map.shape[0]
91
85
 
92
86
 
93
87
  class ScatteringResult(eqx.Module):
@@ -14,7 +14,7 @@ from pmrf.frequency import Frequency
14
14
  from collections import defaultdict
15
15
  from pmrf.utils import field
16
16
  from pmrf.types import ArrayLike
17
- from pmrf.rf import y2s, s2y
17
+ from pmrf.rf import y2s, s2y, renormalize_s
18
18
 
19
19
  from pmrf.models.composite.interconnected.circuit.base import (
20
20
  AbstractCircuitSolver,
@@ -175,72 +175,77 @@ class Circuit(Model):
175
175
  @cached_property
176
176
  def port_representation(self) -> PortRepresentation:
177
177
  """Generates the static topological map for scattering connection and reduction."""
178
- port_to_net_map = compute_unique_nets(self.circuit, self.indexed_connections)
178
+ global_net_map = compute_unique_nets(self.circuit, self.indexed_connections)
179
179
 
180
- ext_idx, int_idx = [], []
180
+ ext_net_ids = []
181
+
181
182
  offset = 0
182
183
  for model in self.circuit:
183
- for p in range(model.nports):
184
- if isinstance(model, Port):
185
- ext_idx.append(offset + p)
186
- else:
187
- int_idx.append(offset + p)
184
+ if isinstance(model, Port):
185
+ ext_net_ids.append(global_net_map[offset])
188
186
  offset += model.nports
189
187
 
188
+ active_net_map = []
189
+ offset = 0
190
+ for model in self.circuit:
191
+ n = model.nports
192
+ if not isinstance(model, Port):
193
+ active_net_map.extend(global_net_map[offset : offset + n])
194
+ offset += n
195
+
190
196
  return PortRepresentation(
191
- ext_idx=np.array(ext_idx, dtype=int),
192
- int_idx=np.array(int_idx, dtype=int),
193
- port_to_net_map=port_to_net_map
197
+ ext_net_ids=np.array(ext_net_ids, dtype=int),
198
+ port_to_net_map=np.array(active_net_map, dtype=int)
194
199
  )
195
200
 
196
201
  @cached_property
197
202
  def nodal_representation(self) -> NodalRepresentation:
198
203
  """Generates the static topological map for nodal admittance assembly and reduction."""
199
- port_rep = self.port_representation
200
- port_to_net_map = port_rep.port_to_net_map
204
+ global_net_map = compute_unique_nets(self.circuit, self.indexed_connections)
201
205
 
202
- r_idx, c_idx = [], []
203
- offset = 0
204
- for model in self.circuit:
205
- n = model.nports
206
- nets = port_to_net_map[offset:offset+n]
207
- R, C = np.meshgrid(nets, nets, indexing='ij')
208
- r_idx.append(R.flatten())
209
- c_idx.append(C.flatten())
210
- offset += n
211
-
212
- unique_ext_nets = np.unique(port_to_net_map[port_rep.ext_idx])
213
- unique_int_nets = np.setdiff1d(np.unique(port_to_net_map), unique_ext_nets)
214
-
215
- # Drop ground nodes
216
206
  ground_nets = set()
207
+ ext_nets = set()
217
208
  offset = 0
218
209
  for model in self.circuit:
210
+ nets = global_net_map[offset : offset + model.nports]
219
211
  if isinstance(model, Ground):
220
- ground_nets.update(port_to_net_map[offset:offset+model.nports])
212
+ ground_nets.update(nets)
213
+ elif isinstance(model, Port):
214
+ ext_nets.update(nets)
221
215
  offset += model.nports
222
216
 
223
- unique_all_nets = np.unique(port_to_net_map)
217
+ unique_all_nets = np.unique(global_net_map)
224
218
  active_nets = np.array([n for n in unique_all_nets if n not in ground_nets], dtype=int)
225
219
 
226
- # Create mapping: active nodes -> 0..N-1. Ground nodes -> N (to be safely dropped)
227
220
  N_active = len(active_nets)
228
221
  mapping = np.full(np.max(unique_all_nets) + 1, N_active, dtype=int)
229
222
  mapping[active_nets] = np.arange(N_active)
230
223
 
231
- # Remap local rows/cols
232
- r_idx_mapped = mapping[np.concatenate(r_idx).astype(int)]
233
- c_idx_mapped = mapping[np.concatenate(c_idx).astype(int)]
224
+ r_idx, c_idx = [], []
225
+ offset = 0
226
+ for model in self.circuit:
227
+ n = model.nports
228
+ if not isinstance(model, (Port, Ground)):
229
+ nets = global_net_map[offset : offset + n]
230
+ R, C = np.meshgrid(nets, nets, indexing='ij')
231
+ r_idx.append(R.flatten())
232
+ c_idx.append(C.flatten())
233
+ offset += n
234
+
235
+ def _safe_cat(lst):
236
+ return np.concatenate(lst).astype(int) if lst else np.array([], dtype=int)
237
+
238
+ r_idx_mapped = mapping[_safe_cat(r_idx)]
239
+ c_idx_mapped = mapping[_safe_cat(c_idx)]
234
240
 
235
- unique_ext_nets = np.unique(port_to_net_map[port_rep.ext_idx])
236
- ext_active = np.array([n for n in unique_ext_nets if n not in ground_nets], dtype=int)
241
+ ext_active = np.array([n for n in ext_nets if n not in ground_nets], dtype=int)
237
242
  int_active = np.setdiff1d(active_nets, ext_active)
238
243
 
239
244
  return NodalRepresentation(
240
245
  r_idx=r_idx_mapped,
241
246
  c_idx=c_idx_mapped,
242
- ext_idx=mapping[ext_active], # mapped external indices
243
- int_idx=mapping[int_active] # mapped internal indices
247
+ ext_idx=mapping[ext_active],
248
+ int_idx=mapping[int_active]
244
249
  )
245
250
 
246
251
  @property
@@ -257,7 +262,8 @@ class Circuit(Model):
257
262
  offset_y = 0 # Tracks the index in the flattened n*n nodal arrays
258
263
  freq_dummy = Frequency(1, 2, 2)
259
264
 
260
- for model in self.circuit:
265
+ nodal_components = [m for m in self.circuit if not isinstance(m, (Port, Ground))]
266
+ for model in nodal_components:
261
267
  stamp_shape = jax.eval_shape(lambda m=model: m.mna(freq_dummy))
262
268
  k = stamp_shape.D.shape[1]
263
269
  n = model.nports
@@ -301,47 +307,50 @@ class Circuit(Model):
301
307
 
302
308
  # --- DATA EVALUATION ---
303
309
 
304
- def _evaluate_scattering(self, freq: Frequency, z0: ArrayLike = 50.0) -> tuple[jnp.ndarray, jnp.ndarray]:
310
+ def _evaluate_scattering(self, freq: Frequency, z0: ArrayLike = 50.0) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
305
311
  """Evaluates and block-diagonalizes the scattering matrices of all contained components."""
306
- S_blocks = [c.s(freq, z0=z0) for c in self.circuit]
312
+ scattering_components = [c for c in self.circuit if not isinstance(c, Port)]
313
+ S_blocks = [c.s(freq, z0=z0) for c in scattering_components]
307
314
 
308
- batched_S = jax.vmap(block_diag)(*S_blocks)
315
+ if S_blocks:
316
+ batched_S = jax.vmap(block_diag)(*S_blocks)
317
+ else:
318
+ batched_S = jnp.zeros((freq.npoints, 0, 0), dtype=jnp.complex128)
319
+
309
320
  num_ports = batched_S.shape[-1]
310
321
  dtype = batched_S.dtype
311
322
  z0_ports = jnp.broadcast_to(jnp.asarray(z0, dtype=dtype), (num_ports,))
312
323
 
313
- return batched_S, z0_ports
324
+ # Dynamically extract the specific z0 constraints of the external ports
325
+ ext_z0_list = [jnp.asarray(getattr(c, 'z0', EVAL_Z0), dtype=dtype) for c in self.circuit if isinstance(c, Port)]
326
+ z0_ext = jnp.stack(ext_z0_list) if ext_z0_list else jnp.zeros((0,), dtype=dtype)
327
+
328
+ return batched_S, z0_ports, z0_ext
314
329
 
315
330
  def _evaluate_admittance(self, freq: Frequency) -> jnp.ndarray:
316
331
  """Evaluates and flattens the admittance matrices of all contained components."""
317
- Y_blocks = []
318
- for c in self.circuit:
319
- if isinstance(c, Port):
320
- Y_blocks.append(jnp.zeros((freq.npoints, c.nports, c.nports), dtype=jnp.complex128))
321
- else:
322
- Y_blocks.append(c.y(freq))
323
-
332
+ nodal_components = [c for c in self.circuit if not isinstance(c, (Port, Ground))]
333
+ Y_blocks = [c.y(freq) for c in nodal_components]
334
+
324
335
  flat_Y_list = []
325
336
  for Y in Y_blocks:
326
337
  Nf, n, _ = Y.shape
327
338
  flat_Y_list.append(Y.reshape(Nf, n * n))
328
- return jnp.concatenate(flat_Y_list, axis=1)
339
+
340
+ if flat_Y_list:
341
+ return jnp.concatenate(flat_Y_list, axis=1)
342
+ else:
343
+ return jnp.zeros((freq.npoints, 0), dtype=jnp.complex128)
329
344
 
330
345
  def _evaluate_mna(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
331
346
  """Evaluates and flattens the MNA stamps of all contained components."""
332
347
  flat_Y, flat_B, flat_C, flat_D = [], [], [], []
333
348
 
334
- for c in self.circuit:
349
+ nodal_components = [c for c in self.circuit if not isinstance(c, (Port, Ground))]
350
+ for c in nodal_components:
335
351
  stamp = c.mna(freq)
336
- # FIX: Zero out virtual topological markers (Ports)
337
- if isinstance(c, Port):
338
- Y = jnp.zeros_like(stamp.Y)
339
- B = jnp.zeros_like(stamp.B)
340
- C = jnp.zeros_like(stamp.C)
341
- D = jnp.zeros_like(stamp.D)
342
- else:
343
- Y, B, C, D = stamp.Y, stamp.B, stamp.C, stamp.D
344
-
352
+ Y, B, C, D = stamp.Y, stamp.B, stamp.C, stamp.D
353
+
345
354
  Nf, n, _ = Y.shape
346
355
  k = D.shape[1]
347
356
 
@@ -366,9 +375,9 @@ class Circuit(Model):
366
375
  flat = self
367
376
 
368
377
  if isinstance(flat.solver, AbstractScatteringCircuitSolver):
369
- s_bdiag, z0_ports = flat._evaluate_scattering(freq, z0)
370
- run_vmap = jax.vmap(flat.solver.run, in_axes=(0, None, None))
371
- return run_vmap(s_bdiag, z0_ports, flat.port_representation)
378
+ s_bdiag, z0_ports, z0_ext = flat._evaluate_scattering(freq, z0)
379
+ run_vmap = jax.vmap(flat.solver.run, in_axes=(0, None, None, None))
380
+ return run_vmap(s_bdiag, z0_ports, z0_ext, flat.port_representation)
372
381
 
373
382
  elif isinstance(flat.solver, AbstractAdmittanceCircuitSolver):
374
383
  y_flat = flat._evaluate_admittance(freq)
@@ -386,8 +395,10 @@ class Circuit(Model):
386
395
  def s(self, freq: Frequency, z0: ArrayLike = 50.0) -> jnp.ndarray:
387
396
  """Evaluates the composite scattering parameters of the circuit."""
388
397
  result = self._solve(freq, z0)
398
+
389
399
  if isinstance(result, ScatteringResult):
390
- return result.s
400
+ # Renormalize from native port z0 to the requested measurement z0
401
+ return renormalize_s(result.s, z_old=result.z0, z_new=z0)
391
402
  elif isinstance(result, AdmittanceResult):
392
403
  return y2s(result.y, z0=z0)
393
404
  else:
@@ -15,7 +15,6 @@ from pmrf.models.composite.interconnected.circuit.base import (
15
15
 
16
16
  from pmrf.rf.conversions import s2s
17
17
 
18
-
19
18
  class GlobalScatteringCircuitSolver(AbstractScatteringCircuitSolver):
20
19
  """
21
20
  Global S-parameter reduction solver.
@@ -32,30 +31,32 @@ class GlobalScatteringCircuitSolver(AbstractScatteringCircuitSolver):
32
31
  linear_solver: lx.AbstractLinearSolver = eqx.field(
33
32
  default=lx.AutoLinearSolver(well_posed=None), static=True
34
33
  )
35
-
36
- def run(
37
- self,
38
- s_block_diagonal: jax.Array,
39
- z0_ports: jax.Array,
40
- topology: PortRepresentation,
41
- ) -> ScatteringResult:
34
+
35
+ def run(self, s_block_diagonal: jax.Array, z0_ports: jax.Array, z0_ext: jax.Array, topology: PortRepresentation) -> ScatteringResult:
36
+ N_act = s_block_diagonal.shape[-1]
37
+ N_ext = len(topology.ext_net_ids)
42
38
 
43
- s_bd, z0, ext_idx_np, int_idx_np, net_map_np = inject_virtual_probes(
44
- s_block_diagonal, z0_ports,
45
- np.array(topology.ext_idx),
46
- np.array(topology.int_idx),
47
- np.array(topology.port_to_net_map)
48
- )
49
-
50
- S_trav = s2s(s_bd, z0, 'traveling', 'power')
51
- X = build_connection_matrix(net_map_np, z0, S_trav.dtype)
39
+ # Statically inject the VNA probes (S=0)
40
+ pad_width = ((0, N_ext), (0, N_ext))
41
+ s_bd = jnp.pad(s_block_diagonal, pad_width, mode='constant')
52
42
 
53
- T = jnp.eye(net_map_np.shape[0], dtype=S_trav.dtype) - S_trav @ X
43
+ z0 = jnp.concatenate([z0_ports, z0_ext])
54
44
 
55
- S_ext_trav = reduce_to_external(T, X, jnp.array(ext_idx_np), self.eps, self.linear_solver)
45
+ # Wire the active topology and the VNA probes together
46
+ # The active ports use port_to_net_map. The VNA probes connect to ext_net_ids.
47
+ net_map = jnp.concatenate([topology.port_to_net_map, topology.ext_net_ids])
56
48
 
57
- z0_ext = z0[ext_idx_np]
58
- return ScatteringResult(s=s2s(S_ext_trav, z0_ext, 'power', 'traveling'), z0=z0_ext)
49
+ # The external indices are simply the newly added probes at the end of the array
50
+ ext_idx = jnp.arange(N_act, N_act + N_ext)
51
+
52
+ # Standard Hallbjörner generalized wave reduction
53
+ S_trav = s2s(s_bd, z0, 'traveling', 'power')
54
+ X = build_connection_matrix(net_map, z0, S_trav.dtype)
55
+ T = jnp.eye(net_map.shape[0], dtype=S_trav.dtype) - S_trav @ X
56
+
57
+ S_ext_trav = reduce_to_external(T, X, ext_idx, self.eps, self.linear_solver)
58
+
59
+ return ScatteringResult(s=s2s(S_ext_trav, z0_ext, 'power', 'traveling'), z0=z0_ext)
59
60
 
60
61
 
61
62
  class HierarchicalScatteringCircuitSolver(AbstractScatteringCircuitSolver):
@@ -79,18 +80,26 @@ class HierarchicalScatteringCircuitSolver(AbstractScatteringCircuitSolver):
79
80
  self,
80
81
  s_block_diagonal: jax.Array,
81
82
  z0_ports: jax.Array,
83
+ z0_ext: jax.Array,
82
84
  topology: PortRepresentation,
83
85
  ) -> ScatteringResult:
84
86
 
85
- s_bd, z0, ext_idx_np, _, net_map_np = inject_virtual_probes(
86
- s_block_diagonal, z0_ports,
87
- np.array(topology.ext_idx),
88
- np.array(topology.int_idx),
89
- np.array(topology.port_to_net_map)
90
- )
87
+ N_act = s_block_diagonal.shape[-1]
88
+ N_ext = len(topology.ext_net_ids)
89
+
90
+ # Statically inject the VNA probes (S=0)
91
+ pad_width = ((0, N_ext), (0, N_ext))
92
+ s_bd = jnp.pad(s_block_diagonal, pad_width, mode='constant')
93
+
94
+ z0 = jnp.concatenate([z0_ports, z0_ext])
95
+
96
+ # Wire the active topology and the VNA probes together
97
+ net_map_np = np.concatenate([topology.port_to_net_map, topology.ext_net_ids])
98
+ ext_idx_np = np.arange(N_act, N_act + N_ext)
91
99
 
100
+ # Map the nets
92
101
  all_net_ids = np.unique(net_map_np)
93
- ext_net_ids = np.unique(net_map_np[ext_idx_np])
102
+ ext_net_ids = np.unique(topology.ext_net_ids)
94
103
  pure_int_net_ids = np.setdiff1d(all_net_ids, ext_net_ids)
95
104
 
96
105
  S = s2s(s_bd, z0, 'traveling', 'power')
@@ -129,7 +138,6 @@ class HierarchicalScatteringCircuitSolver(AbstractScatteringCircuitSolver):
129
138
 
130
139
  S_ext_trav = reduce_to_external(T, X, jnp.array(ext_idx_np), self.eps, self.linear_solver)
131
140
 
132
- z0_ext = z0[ext_idx_np]
133
141
  return ScatteringResult(s=s2s(S_ext_trav, z0_ext, 'power', 'traveling'), z0=z0_ext)
134
142
 
135
143
 
@@ -145,7 +153,6 @@ class SequentialScatteringCircuitSolver(AbstractScatteringCircuitSolver):
145
153
 
146
154
  Best suited for long chain-like/separated networks,
147
155
  though consider using an explicit Cascade if possible.
148
-
149
156
  """
150
157
  eps: float = eqx.field(default=1e-12, static=True)
151
158
  linear_solver: lx.AbstractLinearSolver = eqx.field(
@@ -156,21 +163,28 @@ class SequentialScatteringCircuitSolver(AbstractScatteringCircuitSolver):
156
163
  self,
157
164
  s_block_diagonal: jax.Array,
158
165
  z0_ports: jax.Array,
166
+ z0_ext: jax.Array,
159
167
  topology: PortRepresentation,
160
168
  ) -> ScatteringResult:
161
169
 
162
- s_bd, z0, ext_idx_np, _, net_map_np = inject_virtual_probes(
163
- s_block_diagonal, z0_ports,
164
- np.array(topology.ext_idx),
165
- np.array(topology.int_idx),
166
- np.array(topology.port_to_net_map)
167
- )
170
+ N_act = s_block_diagonal.shape[-1]
171
+ N_ext = len(topology.ext_net_ids)
172
+
173
+ # Statically inject the VNA probes (S=0)
174
+ pad_width = ((0, N_ext), (0, N_ext))
175
+ s_bd = jnp.pad(s_block_diagonal, pad_width, mode='constant')
176
+
177
+ z0 = jnp.concatenate([z0_ports, z0_ext])
178
+
179
+ # Wire the active topology and the VNA probes together
180
+ net_map_np = np.concatenate([topology.port_to_net_map, topology.ext_net_ids])
181
+ ext_idx_np = np.arange(N_act, N_act + N_ext)
168
182
 
169
183
  S = s2s(s_bd, z0, 'traveling', 'power')
170
184
  active_ports = list(range(S.shape[-1]))
171
185
 
172
186
  all_net_ids = np.unique(net_map_np)
173
- ext_net_ids = np.unique(net_map_np[ext_idx_np])
187
+ ext_net_ids = np.unique(topology.ext_net_ids)
174
188
  pure_int_net_ids = np.setdiff1d(all_net_ids, ext_net_ids)
175
189
 
176
190
  # Sequentially contract purely internal nets
@@ -222,39 +236,7 @@ class SequentialScatteringCircuitSolver(AbstractScatteringCircuitSolver):
222
236
 
223
237
  S_ext_trav = reduce_to_external(T, X, local_ext_idx, self.eps, self.linear_solver)
224
238
 
225
- z0_ext = z0[ext_idx_np]
226
239
  return ScatteringResult(s=s2s(S_ext_trav, z0_ext, 'power', 'traveling'), z0=z0_ext)
227
-
228
-
229
- def inject_virtual_probes(
230
- s_block_diagonal: jax.Array,
231
- z0_ports: jax.Array,
232
- ext_idx: np.ndarray,
233
- int_idx: np.ndarray,
234
- net_map: np.ndarray
235
- ) -> Tuple[jax.Array, jax.Array, np.ndarray, np.ndarray, np.ndarray]:
236
- """Handles dangling external ports by injecting virtual matched probes."""
237
- net_counts = np.bincount(net_map)
238
- ext_net_counts = net_counts[net_map[ext_idx]]
239
-
240
- dangling_mask = ext_net_counts == 1
241
- num_dangling = np.sum(dangling_mask)
242
-
243
- if num_dangling > 0:
244
- N_orig = s_block_diagonal.shape[-1]
245
- pad_width = ((0, num_dangling), (0, num_dangling))
246
- s_block_diagonal = jnp.pad(s_block_diagonal, pad_width, mode='constant')
247
-
248
- dangling_ext_indices = ext_idx[dangling_mask]
249
- z0_new = z0_ports[dangling_ext_indices]
250
- z0_ports = jnp.concatenate([z0_ports, z0_new])
251
-
252
- new_port_indices = np.arange(N_orig, N_orig + num_dangling)
253
- net_map = np.concatenate([net_map, net_map[dangling_ext_indices]])
254
- int_idx = np.concatenate([int_idx, dangling_ext_indices])
255
- ext_idx[dangling_mask] = new_port_indices
256
-
257
- return s_block_diagonal, z0_ports, ext_idx, int_idx, net_map
258
240
 
259
241
 
260
242
  def build_connection_matrix(
@@ -722,13 +722,14 @@ def a2mna(a: ArrayLike) -> MNAStamp:
722
722
  else:
723
723
  raise ValueError(f"ABCD parameters must be 2D or 3D. Got {a_arr.ndim}D.")
724
724
 
725
- def s2mna(s: ArrayLike, z0: ArrayLike) -> MNAStamp:
725
+ def s2mna(s: ArrayLike, z0: ArrayLike, s_def: str = 'power') -> MNAStamp:
726
726
  """
727
727
  Convert S-parameters to a Modified Nodal Analysis (MNA) stamp.
728
728
 
729
- This formulation maps the incident voltage waves as the auxiliary
729
+ This formulation maps the incident waves as the auxiliary
730
730
  variables, allowing raw S-parameters to be stamped directly into
731
- the MNA system without any matrix inversions.
731
+ the MNA system without any matrix inversions. Natively supports both
732
+ traveling and power wave definitions.
732
733
 
733
734
  Parameters
734
735
  ----------
@@ -737,6 +738,9 @@ def s2mna(s: ArrayLike, z0: ArrayLike) -> MNAStamp:
737
738
  z0 : ArrayLike
738
739
  The characteristic impedance. Can be a scalar or an array broadcastable
739
740
  to `(nports,)` or `(nfreqs, nports)`.
741
+ s_def : str, optional
742
+ The wave definition used for the S-parameters. Options are 'traveling'
743
+ (pseudo-voltage) or 'power' (Kurokawa). Default is 'traveling'.
740
744
 
741
745
  Returns
742
746
  -------
@@ -748,22 +752,29 @@ def s2mna(s: ArrayLike, z0: ArrayLike) -> MNAStamp:
748
752
  if s_arr.ndim == 3:
749
753
  nfreqs, nports, _ = s_arr.shape
750
754
  z0_fixed = fix_z0_shape(z0, nfreqs, nports)
751
- return jax.vmap(s2mna, in_axes=(0, 0))(s_arr, z0_fixed)
755
+ return jax.vmap(s2mna, in_axes=(0, 0, None))(s_arr, z0_fixed, s_def)
756
+
752
757
  elif s_arr.ndim == 2:
753
758
  nports = s_arr.shape[0]
754
759
  z0_arr = fix_z0_shape(z0, 1, nports)[0]
755
760
 
756
- # Calculate Y0 and expand dims to broadcast across the columns of (I - S)
757
- y0 = 1.0 / z0_arr
758
- y0 = jnp.expand_dims(y0, axis=-1)
759
-
760
761
  I = jnp.eye(nports, dtype=s_arr.dtype)
761
-
762
- # Universal S-parameter MNA derivation
763
762
  Y = jnp.zeros_like(s_arr)
764
- B = y0 * (I - s_arr)
765
763
  C = I
766
- D = -(I + s_arr)
764
+
765
+ if s_def.lower() == 'traveling':
766
+ pre_B = jnp.expand_dims(1.0 / z0_arr, axis=-1)
767
+ pre_D = 1.0
768
+
769
+ elif s_def.lower() == 'power':
770
+ pre_B = jnp.expand_dims(1.0 / jnp.sqrt(z0_arr), axis=-1)
771
+ pre_D = jnp.expand_dims(jnp.sqrt(z0_arr), axis=-1)
772
+
773
+ else:
774
+ raise ValueError(f"s_def must be 'traveling' or 'power'. Got: '{s_def}'")
775
+
776
+ B = pre_B * (I - s_arr)
777
+ D = -(pre_D * (I + s_arr))
767
778
 
768
779
  return MNAStamp(Y=Y, B=B, C=C, D=D)
769
780
  else:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "paramrf"
3
- version = "0.28.2"
3
+ version = "0.28.3"
4
4
  description = "Parametric radio frequency modeling"
5
5
  readme = "README.rst"
6
6
  authors = [