import jax
import jax.numpy as jnp
from pmrf.parameters import Parameter
from pmrf.frequency import Frequency
from pmrf.models.model import Model
from pmrf.functions import y2s
[docs]
class PiCLC(Model):
"""
A 2-port or 3-port model of a Pi-network with a Capacitor-Inductor-Capacitor topology.
This model consists of a shunt capacitor (`C1`), a series inductor (`L`),
and a second shunt capacitor (`C2`). It is a fundamental building block
for various filters and matching networks, and is also commonly used to
model the parasitic effects of physical components like SMD resistors.
The parameter `three_port` determines whether all four ports are exposed or not.
To ensure numerical stability when using with JAX, this model provides a
special case for when the series inductance `L` is zero, where the network
behaves as a single shunt capacitor.
Attributes
----------
C1 : Parameter, default=1.0e-12
The value of the first shunt capacitor in Farads.
L : Parameter, default=1.0e-9
The value of the series inductor in Henrys.
C2 : Parameter, default=1.0e-12
The value of the second shunt capacitor in Farads.
three_port : bool, default=False
If True, treats the network as a 3-port device (where the ground reference is implicit or shared).
If False, treats it as a standard 2-port network.
"""
C1: Parameter = 1.0e-12
L: Parameter = 1.0e-9
C2: Parameter = 1.0e-12
three_port: bool = False
[docs]
def y(self, freq: Frequency) -> jnp.ndarray:
if not self.three_port:
raise Exception('y only available for pi-CLC for three_port == True')
Y1 = 1j * freq.w * self.C1
Y2 = 1j * freq.w * self.C2
Y3 = 1 / (1j * freq.w * self.L)
return jnp.array([
[Y1 + Y3, -Y3, -Y1],
[-Y3, Y2 + Y3, -Y2],
[-Y1, -Y2, Y1 + Y2],
]).transpose(2, 0, 1)
[docs]
def a(self, freq: Frequency) -> jnp.ndarray:
if self.three_port:
raise Exception('Cannot calculate ABCD matrix of pi network when three_port == True')
# This conditional dispatch is used to avoid division-by-zero errors
# during JAX transformations if L becomes zero.
return jax.lax.cond(
self.L == 0.0,
lambda: self.a_zero_inductance(freq),
lambda: self.a_general(freq),
)
[docs]
def a_general(self, freq: Frequency):
"""
Internal calculation for the general case (L != 0).
Parameters
----------
freq : Frequency
The frequency points.
Returns
-------
jnp.ndarray
The ABCD matrix.
"""
# Internal method for the general case where L is non-zero.
C1, C2, L = self.C1, self.C2, self.L
w = freq.w
Y1 = 1j * w * C1
Y2 = 1j * w * C2
Y3 = 1 / (1j * w * L)
return jnp.array([
[1 + Y2 / Y3, 1 / Y3 ],
[Y1 + Y2 + Y1*Y2/Y3, 1 + Y1 / Y3 ],
]).transpose(2, 0, 1)
[docs]
def a_zero_inductance(self, freq: Frequency):
"""
Internal calculation for the zero inductance case (L == 0).
The network simplifies to a single shunt capacitor C = C1 + C2.
Parameters
----------
freq : Frequency
The frequency points.
Returns
-------
jnp.ndarray
The ABCD matrix.
"""
# Internal method for the special case where L is zero.
# The network simplifies to a single shunt capacitor C = C1 + C2.
C1, C2 = self.C1, self.C2
w = freq.w
C = C1 + C2
Y = 1j * w * C
ones = jnp.ones_like(Y)
zeros = jnp.zeros_like(Y)
return jnp.array([
[ones, zeros],
[Y, ones]
]).transpose(2, 0, 1)
[docs]
def s(self, freq: Frequency) -> jnp.ndarray:
if not self.three_port:
return super().s(freq)
return y2s(self.y(freq), self.z0)
[docs]
class BoxCLCC(Model):
"""
A 3-port or 4-port model of a Box-network with a Capacitor-Inductor-Capacitor-Capacitor topology.
This model consists of a shunt capacitor (`C1`), a series inductor (`L`),
and a second shunt capacitor (`C2`), and a bridging capacitor (`C3`).
The parameter `four_port` determines whether all four ports are exposed or not.
Attributes
----------
C1 : Parameter, default=1.0e-12
First shunt capacitor.
L : Parameter, default=1.0e-9
Series inductor.
C2 : Parameter, default=1.0e-12
Second shunt capacitor.
C3 : Parameter, default=1.0e-12
Bridging capacitor.
four_port : bool, default=False
If True, exposes the network as a 4-port model.
"""
C1: Parameter = 1.0e-12
L: Parameter = 1.0e-9
C2: Parameter = 1.0e-12
C3: Parameter = 1.0e-12
four_port: bool = False
[docs]
def y(self, freq: Frequency) -> jnp.ndarray:
if not self.four_port:
raise NotImplementedError('y only available for pi-CLC for four_port == True')
return jax.lax.cond(
jnp.array(self.L) <= 1e-18,
lambda: self.y_zero_inductance(freq),
lambda: self.y_general(freq),
)
[docs]
def y_general(self, freq: Frequency) -> jnp.ndarray:
"""
Internal calculation for the general case (L > 1e-18).
Parameters
----------
freq : Frequency
The frequency points.
Returns
-------
jnp.ndarray
The Y matrix.
"""
Y1 = 1j * freq.w * self.C1
Y2 = 1j * freq.w * self.C2
Y3 = 1 / (1j * freq.w * self.L)
Y4 = 1j * freq.w * self.C3
zero = jnp.zeros(freq.npoints)
zero = zero.astype(dtype=complex)
return jnp.array([
[Y1 + Y3, -Y3, -Y1, zero],
[-Y3, Y2 + Y3, zero, -Y2],
[-Y1, zero, Y1 + Y4, -Y4],
[zero, -Y2, -Y4, Y2 + Y4]
]).transpose(2, 0, 1)
[docs]
def y_zero_inductance(self, freq: Frequency) -> jnp.ndarray:
"""
Internal calculation for the zero inductance case.
Uses a small epsilon for L to avoid division by zero while approximating
the behavior.
Parameters
----------
freq : Frequency
The frequency points.
Returns
-------
jnp.ndarray
The Y matrix.
"""
Y1 = 1j * freq.w * self.C1
Y2 = 1j * freq.w * self.C2
# Hack for now
L = 1e-18
Y3 = 1 / (1j * freq.w * L)
Y4 = 1j * freq.w * self.C3
zero = jnp.zeros(freq.npoints)
zero = zero.astype(dtype=complex)
return jnp.array([
[Y1 + Y3, -Y3, -Y1, zero],
[-Y3, Y2 + Y3, zero, -Y2],
[-Y1, zero, Y1 + Y4, -Y4],
[zero, -Y2, -Y4, Y2 + Y4]
]).transpose(2, 0, 1)
[docs]
def s(self, freq: Frequency) -> jnp.ndarray:
if not self.four_port:
raise NotImplementedError('Box-CLCC network not yet available for four_port == False')
return y2s(self.y(freq), self.z0)