Custom Models

Sometimes, it is more convenient or elegant to define a custom model that is not available in ParamRF. For example, you may want to:

  • Define a model using a custom S-parameter equation

  • Implement a specialized version of an existing model

  • Add methods on top of your model e.g. for conversions/analysis

In ParamRF, this is done by inheriting directly from Model and overriding at least one of its RF response methods, namely s(), a(), y(), z(), or primary_matrix().

Defining a Capacitor

Let’s define a capacitor from first principles using its ABCD parameters:

import pmrf as prf
import jax.numpy as jnp

class Capacitor(prf.Model):
    C: prf.Param

    def a(self, freq: prf.Frequency) -> jnp.ndarray:
        w, C = freq.w, self.C
        ones, zeros = jnp.ones_like(w), jnp.zeros_like(w)

        return jnp.array([
            [ones,  1.0 / (1j * w * C)],
            [zeros, ones]
        ]).transpose(2, 0, 1)

By inheriting from Model, Capacitor becomes both a Python dataclass and a JAX PyTree! For those familiar with dataclasses, this means that any standard dataclass syntax applies.

Note that Param is merely a field type-hint, and does not enforce that the resulting field is registered as a parameter. To convert caller values into free or fixed parameters, which pmrf.params() then returns by name, and to specify constraints and related metadata, use a field specifier.

Adding a Field Specifier

ParamRF provides two field specifiers: field and param. For parameters, param allows the following:

  • Setting a default value

  • Specifying parameter constraints that are inherent to the model

  • Attaching additional metadata and scaling at the model level

  • Auto-converting floats and other array-like values into variable/fixed parameters

The code below demonstrates this by extending the previous class, while constraining C to be only positive and defining the capacitance in terms of picofarads (pF) instead of farads (F):

# <previous imports>
from pmrf.parameters.constraints import Positive

class Capacitor(Capacitor):
    C: prf.Param = prf.param(constraint=Positive(), as_free=True, scale=1e-12)

    # def a(self, freq: prf.Frequency) -> jnp.ndarray:
        # <same as before>

Passing as_free=True makes the incoming value a free parameter even if a float is passed, and as_fixed=True fixes it. Both are optional: by default a parameter keeps the free or fixed state it arrived with, which is the common case, and the field specifier only registers it under its name.

Note that constraints will also always be enforced (even for unconstrained optimizers!), and will also automatically be intersected with any new constraints provided by the caller.

Evaluating the S-parameters

We can now create a capacitor and plot its S-parameters:

cap = Capacitor(1.0)
cap.plot_s_db(prf.Frequency(10, 100, 101, 'MHz'), m=1, n=0)
../_images/custom_models-3.png

ParamRF will automatically perform the conversion between ABCD and S-parameters internally.