Source code for pmrf.models.composite.interconnected

"""
Composite models that physically connect ports of other models.
"""

import jax.numpy as jnp

from pmrf.frequency import Frequency
from pmrf.models.model import Model
from pmrf.models.components.ideal import Port
from pmrf.rf_functions.connections import connect_s_arbitrary, terminate_s_in_s, cascade_a, cascade_s
from pmrf.field import field

[docs] class Circuit(Model, transparent=True): """ Represents an arbitrary circuit defined by component connections. This model allows for the definition of a circuit by specifying how the ports of various sub-models are connected together. NB: The ports numbers are exposed in the order they appear in the connections list. Attributes ---------- models : list[Model] The list of unique models involved in the circuit. indexed_connections : list[list[tuple[int, int]]] Internal representation of connections using model indices instead of objects. Ports are exposed in the order they appear in the list. port_idxs : list[int] Indices of the models that act as external ports for the circuit. """ models: list[Model] indexed_connections: list[list[tuple[int, int]]] = field(static=True) port_idxs: list[int] = field(static=True) def __init__(self, connections: list[list[tuple[Model, int]]] = None, **kwargs): super().__init__() if 'models' in kwargs and 'indexed_connections' in kwargs and 'port_idxs' in kwargs: self.models = kwargs['models'] self.indexed_connections = kwargs['indexed_connections'] self.port_idxs = kwargs['port_idxs'] return self.models = [] self.indexed_connections = [] self.port_idxs = [] id_to_index: dict[int, int] = {} for connection in connections: indexed_connection = [] for model, value in connection: model_id = id(model) if model_id not in id_to_index: id_to_index[model_id] = len(self.models) self.models.append(model) model_idx = id_to_index[model_id] indexed_connection.append((model_idx, value)) if value > model.nports - 1: raise ValueError(f"Port index out of bounds for model {model.name or model}") self.indexed_connections.append(indexed_connection) for model in self.models: if type(model) == Port: self.port_idxs.append(id_to_index[id(model)])
[docs] def s(self, freq: Frequency) -> jnp.array: Smats = [model.s(freq) for model in self.models] z0s = [model.z0 for model in self.models] Scon, _z0con = connect_s_arbitrary(Smats, z0s, self.indexed_connections, self.port_idxs) return Scon
[docs] class Cascade(Model, transparent=True): """ Represents a cascade, or series connection, of two or more `Model` objects. This container connects multiple models end-to-end. The output port of one model is connected to the input port of the next. This is mathematically equivalent to chain-multiplying the ABCD-parameter matrices of the constituent models. The `Cascade` model automatically flattens any nested `Cascade` instances to maintain a simple, linear chain of models. The number of ports of the resulting `Cascade` network depends on the port count of the final model in the chain. Attributes ---------- models : tuple[Model] The sequence of models in the cascade. Examples -------- Cascading models is most easily done using the `**` operator, which is an alias for creating a `Cascade` model. >>> import pmrf as prf >>> from pmrf.models import Resistor, Capacitor, Inductor # Create individual component models >>> res = Resistor(50) >>> cap = Capacitor(1e-12) >>> ind = Inductor(1e-9) # Cascade them together in a series R-L-C configuration # This is equivalent to Cascade(models=(res, ind, cap)) >>> rlc_series = res ** ind ** cap # Define a frequency axis >>> freq = prf.Frequency(start=1, stop=10, npoints=101, unit='ghz') # Calculate the S-parameters of the cascaded network >>> s_params = rlc_series.s(freq) >>> print(f"Cascaded model has {rlc_series.nports} ports.") >>> print(f"S11 at first frequency point: {s_params[0,0,0]:.2f}") """ models: tuple[Model] def __post_init__(self): model_reduced = [] for model in self.models: if model.nports % 2 != 0: raise ValueError('All networks must be 2N-ports for Cascade') if isinstance(model, Cascade): model_reduced.extend(model.models) else: model_reduced.append(model) # Generate numerically sequenced defaults (model_1, model_2, etc.) self.models = model_reduced
[docs] def a(self, freq: Frequency) -> jnp.ndarray: return cascade_a([model.a(freq) for model in self.models])
[docs] def s(self, freq: Frequency) -> jnp.ndarray: Smats = jnp.array([model.s(freq) for model in self.models]) z0s = jnp.array([model.z0 for model in self.models]) Scas, z0cas = cascade_s(Smats, z0s) return Scas
[docs] class Terminated(Model, transparent=True): """ Represents one network terminated in another. """ from_model: Model into_model: Model def __post_init__(self): if self.from_model.nports != 2*self.into_model.nports: raise ValueError("Currently, Terminated only supports 2-port networks terminated in a 1-port")
[docs] def s(self, freq: Frequency) -> jnp.ndarray: Smat_from = self.from_model.s(freq) z0_from = self.from_model.z0 Smat_into = self.into_model.s(freq) z0_into = self.into_model.z0 S_term, z0_term = terminate_s_in_s(Smat_from, z0_from, Smat_into, z0_into) return S_term
[docs] class Shunt(Model, transparent=True): r""" Represents a 1-port network connected in parallel (shunt) across a 2-port line. This maps the reflection coefficient ($\Gamma$ or $S_{11}$) of a 1-port model into a 2-port transmission matrix. Attributes ---------- model : Model The 1-port model to be connected in shunt. """ model: Model def __post_init__(self): if self.model.nports != 1: raise ValueError(f"Shunt requires a 1-port model. Received a {self.model.nports}-port model.")
[docs] def s(self, freq: Frequency) -> jnp.ndarray: # Get the 1-port S-parameters. Shape: (npoints, 1, 1) # Note: This assumes self.model.z0 == self.z0. If your library allows # mixed reference impedances, you will need to renormalize s_1p first. s_1p = self.model.s(freq) # Extract the reflection coefficient array gamma = s_1p[:, 0, 0] # Calculate 2-port S-parameters directly from 1-port Gamma # This avoids divide-by-zero errors for ideal opens/shorts denom = gamma + 3.0 s11 = (gamma - 1.0) / denom s21 = 2.0 * (1.0 + gamma) / denom # Construct the (npoints, 2, 2) S-parameter array S_shunt = jnp.array([ [s11, s21], [s21, s11], ]).transpose(2, 0, 1) return S_shunt