diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..02b4b4591 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,35 @@ +# This workflows will upload a Python Package using uv when a release is created +# reference: +# https://github.com/astral-sh/trusted-publishing-examples/blob/main/.github/workflows/release.yml + +name: Publish on PyPI + +on: + release: + types: + - created + +jobs: + build_and_deploy: + + runs-on: ubuntu-latest + + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Set Up Python + run: uv python install + + - name: Build Package + run: uv build + + - name: Publish to PyPi + run: uv publish --trusted-publishing always diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index b645ced50..a6b6a3d5f 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "scikit-rf" %} -{% set version = "1.3.0" %} +{% set version = "1.5.0" %} {% set hash_val = "8223204281599ba14b685d7e28b7e361" %} package: diff --git a/doc/source/contributing/index.rst b/doc/source/contributing/index.rst index 0f186f39d..edfe583c9 100644 --- a/doc/source/contributing/index.rst +++ b/doc/source/contributing/index.rst @@ -126,7 +126,7 @@ To run all the tests (except the virtual instruments) cd scikit-rf # Create environment and install dependencies, needed only once. python -m venv .venv - python -e[test,visa,netw,xlsx,plot,docs,testspice] + pip install -e .[test,visa,netw,xlsx,plot,docs,testspice] --compile # Activate Environment, needed for all following steps. # on Linux Systems diff --git a/pyproject.toml b/pyproject.toml index 77b5f4563..c38163c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,7 +133,8 @@ select = ["B", "E", "F", "FA", "I", "NPY", "UP", "W"] # Ignore some rules for all files # E741: ambiguous-variable-name -ignore = ["E741"] +# UP031: Use format specifiers instead of percent format +ignore = ["E741", "UP031"] [tool.ruff.lint.per-file-ignores] # Ignore some rules for some specific files diff --git a/skrf/__init__.py b/skrf/__init__.py index 82d91832b..2ede8dca7 100644 --- a/skrf/__init__.py +++ b/skrf/__init__.py @@ -3,7 +3,7 @@ implemented in Python. """ -__version__ = '1.3.0' +__version__ = '1.5.0' ## Import all module names for coherent reference of name-space #import io diff --git a/skrf/constants.py b/skrf/constants.py index 497261ab5..0d0480f57 100644 --- a/skrf/constants.py +++ b/skrf/constants.py @@ -111,7 +111,7 @@ S_DEF_HFSS_DEFAULT = 'traveling' FrequencyUnitT = Literal["Hz", "kHz", "MHz", "GHz", "THz"] -FREQ_UNITS = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9, "THz": 1e12} +FREQ_UNITS: dict[FrequencyUnitT, float] = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9, "THz": 1e12} SweepTypeT = Literal["lin", "log"] CoordT = Literal["cart", "polar"] diff --git a/skrf/io/csv.py b/skrf/io/csv.py index 65042d2bf..9056ce7a2 100644 --- a/skrf/io/csv.py +++ b/skrf/io/csv.py @@ -47,6 +47,8 @@ """ +from __future__ import annotations + import os from warnings import warn @@ -54,13 +56,14 @@ from .. import mathFunctions as mf from .. import util +from ..constants import FREQ_UNITS, FrequencyUnitT from ..frequency import Frequency from ..network import Network # delayed imports # from pandas import Series, Index, DataFrame -def read_pna_csv(filename, *args, **kwargs): +def read_pna_csv(filename, *args, **kwargs) -> tuple[str, str, np.ndarray]: r""" Reads data from a csv file written by an Agilient PNA. @@ -134,6 +137,17 @@ def read_pna_csv(filename, *args, **kwargs): # pna uses unicode coding for degree symbol, but we dont need that header = header.replace('\xb0','deg').rstrip('\n').rstrip('\r') + units_dict: dict[str, FrequencyUnitT] = {k.lower(): k for k in FREQ_UNITS.keys()} + + # Get the frequency unit from the header and convert to Hz + unit_raw = header.split(',')[0].strip('Freq')[1:-1] + try: + unit_tmp = unit_raw.lower() + if unit_tmp in units_dict: + data[:, 0] *= FREQ_UNITS[units_dict[unit_tmp]] + except Exception as exc: + raise ValueError(f"Could not parse frequency unit '{unit_raw}'") from exc + return header, comments, data def pna_csv_2_df(filename): @@ -166,7 +180,7 @@ def pna_csv_2_ntwks2(filename, *args, **kwargs): header, comments, d = read_pna_csv(filename) ntwk_dict = {} param_set=set([k[:3] for k in df.columns]) - f = df.index.values*1e-9 + f = df.index.values for param in param_set: try: s = mf.dbdeg_2_reim( @@ -179,7 +193,7 @@ def pna_csv_2_ntwks2(filename, *args, **kwargs): df[f'{param} (IMAG)'].values, ) - ntwk_dict[param] = Network(f=f, s=s, name=param, comments=comments) + ntwk_dict[param] = Network(f=f, s=s, name=param, comments=comments, f_unit='Hz') try: @@ -216,8 +230,8 @@ def pna_csv_2_ntwks3(filename): # set impedance to 50 Ohm (doesn't matter for now) z0 = np.ones(np.shape(d)[0])*50 - # read f values, convert to GHz - f = d[:,0]/1e9 + # read f values + f = d[:,0] name = os.path.splitext(os.path.basename(filename))[0] @@ -236,7 +250,7 @@ def pna_csv_2_ntwks3(filename): elif 's22' in h.lower() and 'db' in h.lower(): s[:,1,1] = mf.dbdeg_2_reim(d[:,k+1], d[:,k+2]) - n = Network(f=f,s=s,z0=z0, name = name) + n = Network(f=f,s=s,z0=z0, name = name, f_unit="Hz") return n else: @@ -619,16 +633,16 @@ def pna_csv_2_ntwks(filename): if (d.shape[1]-1)/2 == 0 : # this isnt complex data - f = d[:,0]*1e-9 + f = d[:,0] if 'db' in header.lower(): s = mf.db_2_mag(d[:,1]) else: raise (NotImplementedError) name = os.path.splitext(os.path.basename(filename))[0] - return Network(f=f, s=s, name=name, comments=comments) + return Network(f=f, s=s, name=name, comments=comments, f_unit='Hz') else: for k in range(int((d.shape[1]-1)/2)): - f = d[:,0]*1e-9 + f = d[:,0] name = names[k] print((names[k], names[k+1])) if 'db' in names[k].lower() and 'deg' in names[k+1].lower(): @@ -640,7 +654,7 @@ def pna_csv_2_ntwks(filename): s = d[:,k*2+1]+1j*d[:,k*2+2] ntwk_list.append( - Network(f=f, s=s, name=name, comments=comments) + Network(f=f, s=s, name=name, comments=comments, f_unit='Hz') ) return ntwk_list @@ -648,15 +662,9 @@ def pna_csv_2_ntwks(filename): def pna_csv_2_freq(filename): warn("deprecated", DeprecationWarning, stacklevel=2) header, comments, d = read_pna_csv(filename) - #try to pull out frequency unit - cols = pna_csv_header_split(filename) - try: - f_unit = cols[0].split('(')[1].split(')')[0] - except Exception: - f_unit = 'hz' f = d[:,0] - return Frequency.from_f(f, unit = f_unit) + return Frequency.from_f(f, unit = "Hz") def pna_csv_2_scalar_ntwks(filename, *args, **kwargs): @@ -683,15 +691,8 @@ def pna_csv_2_scalar_ntwks(filename, *args, **kwargs): cols = pna_csv_header_split(filename) - - #try to pull out frequency unit - try: - f_unit = cols[0].split('(')[1].split(')')[0] - except Exception: - f_unit = 'hz' - f = d[:,0] - freq = Frequency.from_f(f, unit = f_unit) + freq = Frequency.from_f(f, unit = 'Hz') # loop through columns and create a single network for each column ntwk_list = [] diff --git a/skrf/io/tests/test_csv.py b/skrf/io/tests/test_csv.py index 64298bcd3..341cd0497 100644 --- a/skrf/io/tests/test_csv.py +++ b/skrf/io/tests/test_csv.py @@ -1,5 +1,6 @@ import os import unittest +import warnings import numpy as np import pytest @@ -19,8 +20,8 @@ def setUp(self): name for pna_csv_reim.csv file, then reads it in. """ self.test_dir = os.path.dirname(os.path.abspath(__file__))+'/' - filename = os.path.join(self.test_dir, 'pna_csv_reim.csv') - self.acsv = rf.AgilentCSV(filename) + self.filename = os.path.join(self.test_dir, 'pna_csv_reim.csv') + self.acsv = rf.AgilentCSV(self.filename) def test_columns(self): """ @@ -64,3 +65,17 @@ def test_scalar_networks(self): This only tests for execution, not accuracy """ a = self.acsv.scalar_networks + + def test_read_pna_csv(self): + """ + This tests reading of a csv file written by an Agilient PNA. + """ + with warnings.catch_warnings(record=True) as w: + with self.assertWarns(DeprecationWarning): + header, comment, data = rf.read_pna_csv(self.filename) + + self.assertEqual(header, 'Freq(Hz),"A,1"(REAL),"A,1"(IMAG),"R1,1"(REAL),"R1,1"(IMAG)') + self.assertEqual(comment, 'this is a comment\nline\n') + self.assertTrue((data == np.array([[750000000000, 1, 2, 3, 4], + [1100000000000, 5, 6, 7,8], + ])).all()) diff --git a/skrf/io/touchstone.py b/skrf/io/touchstone.py index 3c8fc9ee7..d8dfeef9a 100644 --- a/skrf/io/touchstone.py +++ b/skrf/io/touchstone.py @@ -537,7 +537,7 @@ def load_file(self, fid: typing.TextIO): if state.matrix_format == "full": self.s[:] = s_flat else: - index = np.tril_indices(state.rank) if state.matrix_format == "lower" else np.triu_indices(self.rank) + index = np.tril_indices(state.rank) if state.matrix_format == "lower" else np.triu_indices(state.rank) index_flat = np.ravel_multi_index(index, (state.rank, state.rank)) self.s[:, index_flat] = s_flat @@ -546,8 +546,12 @@ def load_file(self, fid: typing.TextIO): else: self.s = self.s.reshape((-1, state.rank, state.rank)) - if state.matrix_format != "full": - self.s = np.nanmax((self.s, self.s.transpose(0, 2, 1)), axis=0) + if state.matrix_format == "upper": + index_lower = np.tril_indices(state.rank) + self.s[(...,*index_lower)] = self.s.transpose(0, 2, 1)[(...,*index_lower)] + elif state.matrix_format == "lower": + index_upper = np.triu_indices(state.rank) + self.s[(...,*index_upper)] = self.s.transpose(0, 2, 1)[(...,*index_upper)] self.port_modes = np.array(["S"] * state.rank) if state.mixed_mode_order: diff --git a/skrf/media/definedAEpTandZ0.py b/skrf/media/definedAEpTandZ0.py index 4976bdd7c..6e04f2b00 100644 --- a/skrf/media/definedAEpTandZ0.py +++ b/skrf/media/definedAEpTandZ0.py @@ -18,8 +18,9 @@ from __future__ import annotations import warnings +from typing import Sequence -from numpy import imag, log, ones, pi, real, sqrt +from numpy import imag, log, ndarray, ones, pi, real, sqrt from scipy.constants import c from ..constants import NumberLike @@ -32,7 +33,7 @@ class DefinedAEpTandZ0(Media): Transmission line medium defined by A, Ep, Tand and Z0. This medium is defined by attenuation `A`, relative permittivity `Ep_r`, - loss angle `tand` and characteristic impedance `Z0`. + loss angle `tand` and nominal impedance `Z0`. Djirdjevic [#Djordjevic]_ / Svennson [#Svensson]_ dispersion model is provided for dielectric. Default behaviour is frequency invariant. @@ -51,7 +52,7 @@ class DefinedAEpTandZ0(Media): (Default is None) A : number, array-like, default 0.0 Attenuation due to conductor loss in dB/m/sqrt(Hz) - The attenuation :math:`A(f)`at frequency :math:`f` is: + The attenuation :math:`A(f)` at frequency :math:`f` is: .. math:: @@ -86,14 +87,43 @@ class DefinedAEpTandZ0(Media): tanD : number, array-like, default 0.0 Dielectric relative permittivity loss tangent :math:`\tan\delta`. See `ep_r`. z0 : number, array-like, default 50.0 - Quasi-static characteristic impedance of the medium. + Quasi-static nominal impedance of the medium. + Because of the dispersion introduced by the conductor and dielectric + losses, the characteristic impedance can be frequency-dependent with an + imaginary part. The characteristic impedance is computed with an RLGC + model. + If the impedance parameter is array-like, the characteristic impedance + is assigned to this value without modification. + + .. math:: + + R = 2 Z_n \alpha_{conductor} + + .. math:: + + L = \frac{Z_n \sqrt{\epsilon_r}}{C_0} + + .. math:: + + G = \frac{2}{Z_n} \alpha_{dielectric} + + .. math:: + + C = \frac{\sqrt{\epsilon_r}}{C_0 Z_n} + + .. math:: + + Z_0 = \sqrt{\frac{R + j\omega L}{G + j\omega C}} + + where :math:`Z_n` is the nominal impedance and :math:`Z_0` is the + characteristic impedance. Z0 : number, array-like, or None deprecated parameter, only emmit a deprecation warning. f_low : number, default 1e3, optional - Low frequency in Hz for for Djirdjevic/Svennson dispersion model. + Low frequency in Hz for for Djordjevic/Svennson dispersion model. See `ep_r`. f_high : number, default 1e12, optional - High frequency in Hz for for Djirdjevic/Svennson dispersion model. + High frequency in Hz for for Djordjevic/Svennson dispersion model. See `ep_r`. f_ep : number, default 1e9, , optional Specification frequency in Hz for for Djirdjevic/Svennson dispersion model. @@ -139,10 +169,25 @@ def __init__(self, frequency: Frequency| None = None, Media.__init__(self, frequency=frequency, z0_port=z0_port) self.A, self.f_A = A, f_A self.ep_r, self.tanD = ep_r, tanD - self.z0_characteristic = z0 + self.f_low, self.f_high, self.f_ep = f_low, f_high, f_ep self.model = model + if isinstance(z0, (Sequence, ndarray)): + # keep raw impedance for characteristic impedance + self.z0_characteristic = z0 + else: + # z0 is the nominal impedance. Compute characteristic impedance with + # an RLGC model based on alpha conductor and alpha dielectric. + self.Zn = z0 + self.R = 2 * self.Zn * self.alpha_conductor + self.L = self.Zn * sqrt(self.ep_r) / c + self.G = 2 / self.Zn * self.alpha_dielectric + self.C = sqrt(ep_r) / c / self.Zn + self.z0_characteristic = sqrt( + (self.R + 1j * self.frequency.w * self.L) / + (self.G + 1j * self.frequency.w * self.C)) + if Z0 is not None: # warns of deprecation warnings.warn( diff --git a/skrf/media/media.py b/skrf/media/media.py index 5559e04c9..222e7cd1a 100644 --- a/skrf/media/media.py +++ b/skrf/media/media.py @@ -643,14 +643,27 @@ def resistor(self, R: NumberLike, *args, **kwargs) -> Network: capacitor inductor """ - result = self.match(nports=2, **kwargs) - y = np.zeros(shape=result.s.shape, dtype=complex) + s_def = kwargs.pop('s_def', S_DEF_DEFAULT) + result = self.match(nports=2, s_def='power', **kwargs) + s = np.zeros(shape=result.s.shape, dtype=complex) R = np.array(R) - y[:, 0, 0] = 1.0 / R - y[:, 1, 1] = 1.0 / R - y[:, 0, 1] = -1.0 / R - y[:, 1, 0] = -1.0 / R - result.y = y + # Convert Y-parameters resistor to S-parameters in power wave to accommodate any R value. + # y[:, 0, 0] = 1.0 / R + # y[:, 1, 1] = 1.0 / R + # y[:, 0, 1] = -1.0 / R + # y[:, 1, 0] = -1.0 / R + z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1] + denom = R + (z0_0 + z0_1) + s[:, 0, 0] = (R - z0_0.conj() + z0_1) / denom + s[:, 1, 1] = (R + z0_0 - z0_1.conj()) / denom + s[:, 0, 1] = 2 * (z0_0.real * z0_1.real)**0.5 / denom + s[:, 1, 0] = 2 * (z0_0.real * z0_1.real)**0.5 / denom + result.s = s + + # Renormalize into s_def if required + if s_def != 'power': + result.renormalize(z_new=result.z0, s_def=s_def) + return result def capacitor(self, C: NumberLike, **kwargs) -> Network: @@ -679,15 +692,28 @@ def capacitor(self, C: NumberLike, **kwargs) -> Network: resistor inductor """ - result = self.match(nports=2, **kwargs) + s_def = kwargs.pop('s_def', S_DEF_DEFAULT) + result = self.match(nports=2, s_def='power', **kwargs) w = self.frequency.w - y = np.zeros(shape=result.s.shape, dtype=complex) + s = np.zeros(shape=result.s.shape, dtype=complex) C = np.array(C) - y[:, 0, 0] = 1j * w * C - y[:, 1, 1] = 1j * w * C - y[:, 0, 1] = -1j * w * C - y[:, 1, 0] = -1j * w * C - result.y = y + # Convert Y-parameters capacitor to S-parameters in power wave to accommodate any C value. + # y[:, 0, 0] = 1j * w * C + # y[:, 1, 1] = 1j * w * C + # y[:, 0, 1] = -1j * w * C + # y[:, 1, 0] = -1j * w * C + z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1] + denom = 1.0 + 1j * w * C * (z0_0 + z0_1) + s[:, 0, 0] = (1.0 - 1j * w * C * (z0_0.conj() - z0_1) ) / denom + s[:, 1, 1] = (1.0 - 1j * w * C * (z0_1.conj() - z0_0) ) / denom + s[:, 0, 1] = (2j * w * C * (z0_0.real * z0_1.real)**0.5) / denom + s[:, 1, 0] = (2j * w * C * (z0_0.real * z0_1.real)**0.5) / denom + result.s = s + + # Renormalize into s_def if required + if s_def != 'power': + result.renormalize(z_new=result.z0, s_def=s_def) + return result def inductor(self, L: NumberLike, **kwargs) -> Network: @@ -716,15 +742,28 @@ def inductor(self, L: NumberLike, **kwargs) -> Network: capacitor resistor """ - result = self.match(nports=2, **kwargs) + s_def = kwargs.pop('s_def', S_DEF_DEFAULT) + result = self.match(nports=2, s_def='power', **kwargs) w = self.frequency.w - y = np.zeros(shape=result.s.shape, dtype=complex) + s = np.zeros(shape=result.s.shape, dtype=complex) L = np.array(L) - y[:, 0, 0] = 1.0 / (1j * w * L) - y[:, 1, 1] = 1.0 / (1j * w * L) - y[:, 0, 1] = -1.0 / (1j * w * L) - y[:, 1, 0] = -1.0 / (1j * w * L) - result.y = y + # Convert Y-parameters inductor to S-parameters in power wave to accommodate any L value. + # y[:, 0, 0] = 1.0 / (1j * w * L) + # y[:, 1, 1] = 1.0 / (1j * w * L) + # y[:, 0, 1] = -1.0 / (1j * w * L) + # y[:, 1, 0] = -1.0 / (1j * w * L) + z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1] + denom = (1j * w * L) + (z0_0 + z0_1) + s[:, 0, 0] = (1j * w * L - z0_0.conj() + z0_1) / denom + s[:, 1, 1] = (1j * w * L + z0_0 - z0_1.conj()) / denom + s[:, 0, 1] = 2 * (z0_0.real * z0_1.real)**0.5 / denom + s[:, 1, 0] = 2 * (z0_0.real * z0_1.real)**0.5 / denom + result.s = s + + # Renormalize into s_def if required + if s_def != 'power': + result.renormalize(z_new=result.z0, s_def=s_def) + return result def impedance_mismatch(self, z1: NumberLike, z2: NumberLike, **kwargs) -> Network: @@ -974,8 +1013,8 @@ def line(self, d: NumberLike, unit: str = 'deg', '`embed` will be removed in version 1.0', DeprecationWarning, stacklevel = 2) - if isinstance(z0,str): - z0 = parse_z0(z0)* self.z0 + if isinstance(z0, str): + z0 = parse_z0(z0) * self.z0 if z0 is None: z0 = self.z0 @@ -1002,6 +1041,79 @@ def line(self, d: NumberLike, unit: str = 'deg', return result + def line_floating(self, d: NumberLike, unit: str = 'deg', + z0: NumberLike | str | None = None, **kwargs) -> Network: + r""" + Floating transmission line of a given length and impedance. + + This method returns a transmission line with floating shields. + This is a four-port network, as opposed to the two-port _line_ method. + Ports 1 and 3 are one side of the line, and electrically connect to + ports 2 and 4 on the other side, respectively. + + The units of `length` are interpreted according to the value + of `unit`. If `z0` is not None, then a line specified impedance + is produced. + + Parameters + ---------- + d : number + the length of transmission line (see unit argument) + unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps'] + the units of d. See :func:`to_meters`, for details + z0 : number, string, or array-like or None + the characteristic impedance of the line, if different + from `media.z0`. To set z0 in terms of normalized impedance, + pass a string, like `z0='1+.2j'` + \*\*kwargs : key word arguments + passed to :func:`match`, which is called initially to create a + 'blank' network. + + Returns + ------- + line_floating : :class:`~skrf.network.Network` object + matched, floating transmission line of given length + + Examples + -------- + >>> my_media.line_floating(1, 'mm', z0=100) + >>> my_media.line_floating(90, 'deg', z0='2') # set z0 as normalized impedance + + """ + if isinstance(z0, str): + z0 = self.parse_z0(z0) * self.z0 + + if z0 is None: + z0 = self.z0 + + s_def = kwargs.pop('s_def', S_DEF_DEFAULT) + + # The use of either traveling or pseudo waves s-parameters definition + # is required here. + # The definition of the reflection coefficient for power waves has + # conjugation. + result = self.match(nports=4, z0=z0, s_def='traveling', **kwargs) + + t = self.electrical_length(self.to_meters(d=d, unit=unit)) + + denom = -1 + 9*np.exp(2*t) + s11 = (1 + 3*np.exp(2*t)) / denom + s12 = 4*np.exp(t) / denom + s13 = (-2 + 6*np.exp(2*t)) / denom + s14 = -s12 + + result.s = np.array([[s11, s12, s13, s14], + [s12, s11, s14, s13], + [s13, s14, s11, s12], + [s14, s13, s12, s11]]).transpose(2, 0, 1) + + # renormalize (or embed) into z0_port if required + if self.z0_port is not None: + result.renormalize(self.z0_port) + result.renormalize(result.z0, s_def=s_def) + + return result + def delay_load(self, Gamma0: NumberLike, d: Number, unit: str = 'deg', **kwargs) -> Network: r""" diff --git a/skrf/media/tests/awr/tlinp.s2p b/skrf/media/tests/awr/tlinp.s2p new file mode 100644 index 000000000..f304ef8ab --- /dev/null +++ b/skrf/media/tests/awr/tlinp.s2p @@ -0,0 +1,207 @@ +! AWR Design Environment (17415) Thu Oct 17 09:15:28 2024 +! Document = 'TLine' +! nPorts: 2, nXvals: 201 + +# MHz S DB R 50 +!Freq DBS11 AngS11 DBS21 AngS21 DBS12 AngS12 DBS22 AngS22 +10 -45.0952624156537 -89.1519419491076 -0.00620899528653234 -1.63925681388553 -0.00620899528653137 -1.6392568138855 -45.0952624156528 -89.1519419491102 +14.95 -41.6102998374044 -90.8444540909481 -0.00826868959441112 -2.45076154045696 -0.00826868959441063 -2.45076154045707 -41.6102998374065 -90.8444540909475 +19.9 -39.1317154120894 -92.183211865309 -0.0102521133236231 -3.26220839095278 -0.0102521133236256 -3.26220839095272 -39.1317154120886 -92.1832118653082 +24.85 -37.207497576415 -93.3550441786291 -0.0122098778368871 -4.0735848759955 -0.0122098778368837 -4.07358487599545 -37.2074975764145 -93.3550441786306 +29.8 -35.6350998411762 -94.4325590395497 -0.0141678413536272 -4.88487823580607 -0.0141678413536272 -4.88487823580613 -35.6350998411767 -94.43255903955 +34.75 -34.3061016449838 -95.4506319879408 -0.016141200233436 -5.69607576667909 -0.016141200233436 -5.69607576667908 -34.3061016449837 -95.450631987941 +39.7 -33.1556371777342 -96.4283681724419 -0.0181396722271922 -6.50716490484516 -0.0181396722271922 -6.50716490484516 -33.1556371777342 -96.4283681724419 +44.65 -32.1417984679386 -97.3772235663188 -0.0201698176819533 -7.31813325088696 -0.0201698176819533 -7.31813325088695 -32.1417984679385 -97.377223566319 +49.6 -31.2359361446189 -98.3045377929358 -0.0222362218189262 -8.12896857765049 -0.0222362218189233 -8.12896857765057 -31.2359361446194 -98.3045377929365 +54.55 -30.4175964105473 -99.2152577780477 -0.0243421548613419 -8.9396588337971 -0.0243421548613448 -8.93965883379706 -30.417596410547 -99.215257778047 +59.5 -29.6716654904551 -100.112854170845 -0.0264899670923303 -9.75019214670331 -0.0264899670923313 -9.7501921467033 -29.6716654904551 -100.112854170844 +64.45 -28.9866586957235 -100.999842354229 -0.0286813385214369 -10.5605568258281 -0.0286813385214379 -10.5605568258282 -28.9866586957237 -100.999842354228 +69.4 -28.3536441757883 -101.878095044142 -0.0309174437891482 -11.3707413667972 -0.0309174437891478 -11.3707413667972 -28.3536441757882 -101.878095044142 +74.35 -27.7655382390571 -102.749038339782 -0.0331990651537033 -12.1807344561663 -0.0331990651537028 -12.1807344561663 -27.7655382390572 -102.749038339782 +79.3 -27.2166281994987 -103.613779327469 -0.0355266723514358 -12.9905249767521 -0.0355266723514358 -12.9905249767521 -27.2166281994987 -103.613779327469 +84.25 -26.7022399165705 -104.473191798227 -0.0379004805812124 -13.8001020133952 -0.0379004805812124 -13.8001020133952 -26.7022399165705 -104.473191798228 +89.2 -26.2185003738652 -105.327975415714 -0.0403204936126105 -14.6094548590379 -0.0403204936126095 -14.6094548590379 -26.2185003738653 -105.327975415714 +94.15 -25.7621644507583 -106.178697536582 -0.0427865365185193 -15.4185730210173 -0.0427865365185222 -15.4185730210173 -25.7621644507583 -106.178697536582 +99.1 -25.3304861241117 -107.025823391182 -0.0452982810099401 -16.2274462274848 -0.0452982810099425 -16.2274462274848 -25.3304861241117 -107.025823391182 +104.05 -24.9211210903222 -107.869738269704 -0.0478552653930253 -17.0360644338866 -0.0478552653930258 -17.0360644338866 -24.9211210903221 -107.869738269704 +109 -24.5320520362659 -108.710764102136 -0.0504569105492697 -17.8444178294476 -0.0504569105492682 -17.8444178294476 -24.5320520362659 -108.710764102136 +113.95 -24.1615305178926 -109.549172033239 -0.0531025329296984 -18.6524968436092 -0.0531025329296989 -18.6524968436092 -24.1615305178926 -109.549172033239 +118.9 -23.8080312054371 -110.385192088175 -0.0557913552762182 -19.4602921523853 -0.0557913552762196 -19.4602921523853 -23.8080312054372 -110.385192088175 +123.85 -23.4702154663214 -111.219020692537 -0.0585225155915977 -20.2677946846032 -0.0585225155915992 -20.2677946846032 -23.4702154663214 -111.219020692537 +128.8 -23.146902088481 -112.050826588113 -0.0612950747448859 -21.074995628001 -0.0612950747448873 -21.074995628001 -23.146902088481 -112.050826588113 +133.75 -22.8370435273739 -112.880755533997 -0.064108023002849 -21.8818864351613 -0.0641080230028485 -21.8818864351613 -22.8370435273739 -112.880755533998 +138.7 -22.5397064715107 -113.708934077388 -0.0669602857083159 -22.6884588292605 -0.066960285708315 -22.6884588292605 -22.5397064715107 -113.708934077388 +143.65 -22.2540558173856 -114.535472604256 -0.0698507282751327 -23.4947048096176 -0.0698507282751337 -23.4947048096176 -22.2540558173856 -114.535472604256 +148.6 -21.9793413604171 -115.360467827101 -0.0727781606313772 -24.3006166570282 -0.0727781606313762 -24.3006166570282 -21.9793413604171 -115.360467827101 +153.55 -21.7148866676494 -116.184004828677 -0.0757413412138497 -25.1061869388733 -0.0757413412138516 -25.1061869388733 -21.7148866676495 -116.184004828677 +158.5 -21.4600797166931 -117.006158752483 -0.0787389805951157 -25.9114085139917 -0.0787389805951172 -25.9114085139917 -21.4600797166931 -117.006158752483 +163.45 -21.2143649748695 -117.826996210058 -0.0817697448076968 -26.7162745373073 -0.0817697448076963 -26.7162745373073 -21.2143649748695 -117.826996210058 +168.4 -20.977236660656 -118.646576459527 -0.0848322584170559 -27.5207784642048 -0.0848322584170589 -27.5207784642048 -20.977236660656 -118.646576459527 +173.35 -20.7482329818466 -119.464952398114 -0.0879251073849553 -28.3249140546467 -0.0879251073849563 -28.3249140546467 -20.7482329818467 -119.464952398114 +178.3 -20.5269311853797 -120.282171402383 -0.0910468417568239 -29.1286753770274 -0.0910468417568244 -29.1286753770274 -20.5269311853797 -120.282171402383 +183.25 -20.3129432854359 -121.098276043067 -0.0941959782004642 -29.9320568117593 -0.0941959782004642 -29.9320568117593 -20.3129432854359 -121.098276043067 +188.2 -20.1059123613041 -121.913304696024 -0.0973710024184134 -30.7350530545883 -0.0973710024184134 -30.7350530545883 -20.1059123613041 -121.913304696024 +193.15 -19.9055093362435 -122.727292066687 -0.100570371452239 -31.5376591196351 -0.100570371452241 -31.5376591196351 -19.9055093362435 -122.727292066687 +198.1 -19.7114301642931 -123.540269642083 -0.103792515893782 -32.3398703421614 -0.103792515893782 -32.3398703421614 -19.7114301642931 -123.540269642083 +203.05 -19.5233933646096 -124.352266081949 -0.107035842015721 -33.141682381058 -0.107035842015721 -33.141682381058 -19.5233933646096 -124.352266081949 +208 -19.3411378531012 -125.163307558348 -0.110298733831677 -33.9430912210564 -0.110298733831677 -33.9430912210564 -19.3411378531011 -125.163307558348 +212.95 -19.1644210293981 -125.973418051586 -0.113579555094271 -34.7440931746604 -0.11357955509427 -34.7440931746604 -19.1644210293981 -125.973418051586 +217.9 -18.9930170839515 -126.782619608869 -0.116876651238153 -35.5446848838011 -0.116876651238153 -35.5446848838011 -18.9930170839516 -126.782619608869 +222.85 -18.8267154955898 -127.590932571069 -0.120188351273716 -36.3448633212137 -0.120188351273718 -36.3448633212137 -18.8267154955898 -127.590932571069 +227.8 -18.6653196944256 -128.398375772084 -0.123512969636322 -37.1446257915382 -0.123512969636322 -37.1446257915382 -18.6653196944256 -128.398375772084 +232.75 -18.5086458687854 -129.204966714567 -0.12684880799495 -37.9439699321457 -0.12684880799495 -37.9439699321457 -18.5086458687854 -129.204966714567 +237.7 -18.3565218979769 -130.010721725196 -0.130194157023491 -38.7428937136906 -0.130194157023492 -38.7428937136906 -18.3565218979769 -130.010721725196 +242.65 -18.2087863953322 -130.81565609218 -0.133547298137376 -39.541395440393 -0.133547298137375 -39.541395440393 -18.2087863953322 -130.81565609218 +247.6 -18.0652878481657 -131.619784187282 -0.136906505197712 -40.3394737500528 -0.136906505197711 -40.3394737500528 -18.0652878481657 -131.619784187282 +252.55 -17.9258838431396 -132.423119574305 -0.140270046184662 -41.1371276137976 -0.140270046184663 -41.1371276137976 -17.9258838431396 -132.423119574305 +257.5 -17.7904403670894 -133.225675105715 -0.143636184841511 -41.9343563355691 -0.14363618484151 -41.9343563355691 -17.7904403670894 -133.225675105715 +262.45 -17.6588311746913 -134.027463008822 -0.147003182290551 -42.7311595513499 -0.147003182290552 -42.7311595513499 -17.6588311746913 -134.027463008822 +267.4 -17.5309372154805 -134.828494962763 -0.15036929862167 -43.5275372281353 -0.150369298621671 -43.5275372281353 -17.5309372154805 -134.828494962763 +272.35 -17.4066461136901 -135.628782167326 -0.153732794454334 -44.3234896626534 -0.153732794454335 -44.3234896626535 -17.4066461136901 -135.628782167326 +277.3 -17.2858516952051 -136.428335404573 -0.157091932473484 -45.1190174798386 -0.157091932473485 -45.1190174798386 -17.2858516952051 -136.428335404573 +282.25 -17.1684535566328 -137.227165094022 -0.160444978939714 -45.9141216310596 -0.160444978939717 -45.9141216310596 -17.1684535566329 -137.227165094022 +287.2 -17.0543566720982 -138.025281342112 -0.163790205174001 -46.7088033921118 -0.163790205174002 -46.7088033921118 -17.0543566720982 -138.025281342112 +292.15 -16.9434710339007 -138.822693986552 -0.167125889017133 -47.5030643609726 -0.167125889017133 -47.5030643609726 -16.9434710339008 -138.822693986552 +297.1 -16.835711323621 -139.619412636082 -0.170450316263939 -48.2969064553288 -0.17045031626394 -48.2969064553288 -16.835711323621 -139.619412636082 +302.05 -16.7309966106632 -140.415446706116 -0.173761782072281 -49.0903319098789 -0.17376178207228 -49.0903319098789 -16.7309966106632 -140.415446706116 +307 -16.6292500755593 -141.210805450682 -0.177058592346843 -49.8833432734155 -0.177058592346841 -49.8833432734155 -16.6292500755593 -141.210805450682 +311.95 -16.5303987556645 -142.005497991016 -0.180339065097535 -50.6759434056933 -0.180339065097534 -50.6759434056933 -16.5303987556645 -142.005497991016 +316.9 -16.434373311131 -142.799533341136 -0.183601531772523 -51.4681354740872 -0.183601531772525 -51.4681354740872 -16.434373311131 -142.799533341136 +321.85 -16.3411078092796 -143.592920430669 -0.186844338565597 -52.2599229500465 -0.186844338565596 -52.2599229500465 -16.3411078092796 -143.592920430669 +326.8 -16.250539525688 -144.385668125208 -0.190065847697828 -53.0513096053499 -0.190065847697828 -53.0513096053499 -16.250539525688 -144.385668125207 +331.75 -16.1626087604933 -145.177785244382 -0.193264438673287 -53.8422995081667 -0.193264438673287 -53.8422995081666 -16.1626087604933 -145.177785244382 +336.7 -16.0772586685606 -145.969280577887 -0.196438509508642 -54.6328970189294 -0.196438509508642 -54.6328970189295 -16.0772586685606 -145.969280577887 +341.65 -15.9944351023096 -146.760162899619 -0.199586477936473 -55.4231067860245 -0.199586477936471 -55.4231067860245 -15.9944351023096 -146.760162899619 +346.6 -15.9140864661109 -147.550440980083 -0.202706782582069 -56.2129337413036 -0.202706782582069 -56.2129337413037 -15.9140864661109 -147.550440980083 +351.55 -15.8361635812743 -148.340123597223 -0.205797884113576 -57.0023830954249 -0.205797884113577 -57.0023830954249 -15.8361635812743 -148.340123597222 +356.5 -15.7606195607453 -149.129219545801 -0.208858266365261 -57.7914603330258 -0.208858266365262 -57.7914603330258 -15.7606195607453 -149.129219545801 +361.45 -15.6874096927133 -149.917737645435 -0.211886437433751 -58.5801712077357 -0.211886437433752 -58.5801712077357 -15.6874096927133 -149.917737645435 +366.4 -15.6164913324106 -150.705686747403 -0.214880930747062 -59.3685217370315 -0.214880930747062 -59.3685217370315 -15.6164913324106 -150.705686747403 +371.35 -15.547823801449 -151.493075740313 -0.217840306106265 -60.156518196944 -0.217840306106265 -60.156518196944 -15.547823801449 -151.493075740313 +376.3 -15.4813682941026 -152.279913554708 -0.220763150699639 -60.944167116618 -0.22076315069964 -60.944167116618 -15.4813682941026 -152.279913554708 +381.25 -15.417087789999 -153.066209166709 -0.223648080089165 -61.7314752727322 -0.223648080089166 -61.7314752727322 -15.417087789999 -153.066209166709 +386.2 -15.3549469727314 -153.851971600734 -0.226493739169254 -62.5184496837855 -0.226493739169255 -62.5184496837855 -15.3549469727314 -153.851971600734 +391.15 -15.2949121539462 -154.63720993138 -0.229298803097577 -63.3050976042526 -0.229298803097575 -63.3050976042526 -15.2949121539462 -154.63720993138 +396.1 -15.2369512025024 -155.421933284522 -0.232061978197891 -64.0914265186157 -0.232061978197892 -64.0914265186157 -15.2369512025024 -155.421933284522 +401.05 -15.1810334783329 -156.206150837661 -0.234782002834806 -64.8774441352765 -0.234782002834806 -64.8774441352765 -15.1810334783329 -156.206150837661 +406 -15.1271297706713 -156.989871819602 -0.237457648260363 -65.6631583803544 -0.237457648260364 -65.6631583803544 -15.1271297706713 -156.989871819602 +410.95 -15.0752122403371 -157.773105509472 -0.240087719432401 -66.4485773913743 -0.2400877194324 -66.4485773913743 -15.0752122403371 -157.773105509472 +415.9 -15.0252543657961 -158.555861235143 -0.242671055804625 -67.2337095108507 -0.242671055804625 -67.2337095108507 -15.0252543657961 -158.555861235143 +420.85 -14.9772308927412 -159.338148371074 -0.245206532088357 -68.0185632797715 -0.245206532088357 -68.0185632797715 -14.9772308927412 -159.338148371074 +425.8 -14.9311177869553 -160.119976335625 -0.247693058985931 -68.8031474309864 -0.247693058985931 -68.8031474309864 -14.9311177869553 -160.119976335625 +430.75 -14.8868921902422 -160.901354587854 -0.250129583895687 -69.5874708825059 -0.250129583895687 -69.5874708825059 -14.8868921902422 -160.901354587854 +435.7 -14.8445323792262 -161.682292623842 -0.252515091588574 -70.3715427307128 -0.252515091588573 -70.3715427307128 -14.8445323792262 -161.682292623842 +440.65 -14.8040177268399 -162.462799972552 -0.254848604856347 -71.1553722434934 -0.254848604856347 -71.1553722434934 -14.8040177268399 -162.462799972552 +445.6 -14.765328666332 -163.242886191254 -0.257129185131384 -71.9389688532914 -0.257129185131383 -71.9389688532914 -14.765328666332 -163.242886191254 +450.55 -14.7284466576437 -164.022560860544 -0.259355933078085 -72.7223421500883 -0.259355933078085 -72.7223421500883 -14.7284466576437 -164.022560860544 +455.5 -14.6933541560116 -164.801833578955 -0.261527989155913 -73.5055018743161 -0.261527989155914 -73.5055018743161 -14.6933541560116 -164.801833578955 +460.45 -14.6600345826697 -165.580713957194 -0.263644534154103 -74.288457909705 -0.263644534154104 -74.288457909705 -14.6600345826697 -165.580713957194 +465.4 -14.6284722975307 -166.35921161201 -0.265704789698028 -75.0712202760712 -0.265704789698029 -75.0712202760712 -14.6284722975307 -166.35921161201 +470.35 -14.5986525737382 -167.13733615971 -0.267708018727324 -75.8537991220478 -0.267708018727325 -75.8537991220478 -14.5986525737382 -167.13733615971 +475.3 -14.5705615739911 -167.915097209338 -0.269653525945734 -76.6362047177632 -0.269653525945735 -76.6362047177632 -14.5705615739911 -167.915097209338 +480.25 -14.5441863285467 -168.692504355509 -0.271540658242787 -77.4184474474719 -0.271540658242787 -77.4184474474719 -14.5441863285467 -168.692504355509 +485.2 -14.5195147148203 -169.469567170939 -0.273368805087321 -78.200537802139 -0.27336880508732 -78.200537802139 -14.5195147148203 -169.469567170939 +490.15 -14.4965354385047 -170.246295198642 -0.275137398892903 -78.9824863719844 -0.275137398892903 -78.9824863719844 -14.4965354385047 -170.246295198642 +495.1 -14.4752380161371 -171.022697943829 -0.276845915355217 -79.7643038389893 -0.276845915355215 -79.7643038389893 -14.4752380161371 -171.022697943829 +500.05 -14.4556127590533 -171.798784865498 -0.278493873761436 -80.5460009693691 -0.278493873761437 -80.5460009693691 -14.4556127590533 -171.798784865498 +505 -14.4376507586673 -172.574565367725 -0.280080837271683 -81.3275886060151 -0.280080837271684 -81.3275886060151 -14.4376507586673 -172.574565367725 +509.95 -14.4213438730255 -173.350048790652 -0.281606413172598 -82.1090776609095 -0.281606413172596 -82.1090776609095 -14.4213438730255 -173.350048790652 +514.9 -14.4066847145874 -174.125244401185 -0.283070253103084 -82.8904791075177 -0.283070253103082 -82.8904791075177 -14.4066847145874 -174.125244401185 +519.85 -14.3936666391901 -174.900161383391 -0.284472053252276 -83.6718039731591 -0.284472053252276 -83.6718039731591 -14.3936666391901 -174.900161383391 +524.8 -14.3822837361567 -175.674808828596 -0.285811554529806 -84.4530633313618 -0.285811554529805 -84.4530633313618 -14.3822837361567 -175.674808828596 +529.75 -14.3725308195165 -176.449195725189 -0.287088542708401 -85.2342682942037 -0.2870885427084 -85.2342682942037 -14.3725308195165 -176.449195725189 +534.7 -14.3644034203048 -177.223330948118 -0.288302848538879 -86.015430004643 -0.28830284853888 -86.015430004643 -14.3644034203048 -177.223330948118 +539.65 -14.3578977799186 -177.997223248075 -0.289454347837589 -86.7965596288415 -0.289454347837589 -86.7965596288415 -14.3578977799186 -177.997223248075 +544.6 -14.3530108445026 -178.770881240381 -0.290542961546342 -87.577668348485 -0.290542961546344 -87.577668348485 -14.3530108445026 -178.770881240381 +549.55 -14.3497402603494 -179.544313393535 -0.291568655764908 -88.3587673531009 -0.291568655764909 -88.3587673531009 -14.3497402603494 -179.544313393535 +554.5 -14.3480843702975 179.682471982552 -0.292531441756084 -89.1398678323803 -0.292531441756085 -89.1398678323803 -14.3480843702975 179.682471982552 +559.45 -14.3480422111148 178.909466748674 -0.293431375923405 -89.920980968505 -0.293431375923407 -89.920980968505 -14.3480422111148 178.909466748674 +564.4 -14.3496135118596 178.136662948781 -0.294268559761555 -90.7021179284821 -0.294268559761557 -90.7021179284821 -14.3496135118596 178.136662948781 +569.35 -14.3527986932143 177.364052822808 -0.295043139779505 -91.4832898564924 -0.295043139779505 -91.4832898564924 -14.3527986932143 177.364052822808 +574.3 -14.3575988677887 176.591628819864 -0.295755307396378 -92.264507866252 -0.295755307396379 -92.264507866252 -14.3575988677887 176.591628819864 +579.25 -14.364015841395 175.819383611825 -0.296405298810189 -93.0457830333932 -0.296405298810189 -93.0457830333932 -14.364015841395 175.819383611825 +584.2 -14.3720521152994 175.047310107331 -0.296993394839371 -93.8271263878663 -0.29699339483937 -93.8271263878663 -14.3720521152994 175.047310107331 +589.15 -14.3817108894573 174.275401466207 -0.297519920737233 -94.6085489063658 -0.297519920737233 -94.6085489063658 -14.3817108894573 174.275401466207 +594.1 -14.3929960667437 173.503651114344 -0.297985245979304 -95.3900615047844 -0.297985245979306 -95.3900615047844 -14.3929960667437 173.503651114344 +599.05 -14.4059122581944 172.732052759043 -0.298389784023667 -96.1716750306981 -0.298389784023668 -96.1716750306981 -14.4059122581944 172.732052759043 +604 -14.4204647892739 171.96060040487 -0.298733992044266 -96.953400255885 -0.298733992044266 -96.953400255885 -14.4204647892739 171.96060040487 +608.95 -14.4366597071949 171.189288370027 -0.299018370637264 -97.7352478688825 -0.299018370637263 -97.7352478688825 -14.4366597071949 171.189288370027 +613.9 -14.4545037893113 170.418111303293 -0.299243463500479 -98.5172284675839 -0.299243463500479 -98.5172284675839 -14.4545037893113 170.418111303293 +618.85 -14.4740045526174 169.647064201551 -0.299409857085916 -99.2993525518807 -0.299409857085917 -99.2993525518807 -14.4740045526175 169.647064201551 +623.8 -14.495170264384 168.876142427948 -0.299518180225474 -100.081630516351 -0.299518180225473 -100.081630516351 -14.495170264384 168.876142427948 +628.75 -14.5180099539703 168.105341730733 -0.299569103729866 -100.864072643002 -0.299569103729867 -100.864072643002 -14.5180099539703 168.105341730733 +633.7 -14.5425334258544 167.3346582628 -0.299563339960755 -101.646689094061 -0.299563339960754 -101.646689094061 -14.5425334258544 167.3346582628 +638.65 -14.5687512739267 166.564088602002 -0.299501642376249 -102.429489904831 -0.299501642376247 -102.429489904831 -14.5687512739267 166.564088602002 +643.6 -14.5966748971006 165.793629772287 -0.29938480504971 -103.212484976607 -0.299384805049711 -103.212484976607 -14.5966748971006 165.793629772287 +648.55 -14.6263165162957 165.023279265704 -0.299213662162036 -103.995684069653 -0.299213662162037 -103.995684069653 -14.6263165162957 165.023279265704 +653.5 -14.6576891928577 164.253035065346 -0.298989087467411 -104.779096796252 -0.298989087467411 -104.779096796252 -14.6576891928577 164.253035065346 +658.45 -14.6908068484823 163.482895669315 -0.298711993732648 -105.562732613833 -0.298711993732648 -105.562732613833 -14.6908068484823 163.482895669315 +663.4 -14.7256842867191 162.712860115755 -0.298383332150194 -106.346600818163 -0.298383332150194 -106.346600818163 -14.7256842867191 162.712860115755 +668.35 -14.7623372161383 161.942928009064 -0.298004091724872 -107.130710536636 -0.298004091724873 -107.130710536636 -14.7623372161383 161.942928009064 +673.3 -14.800782275249 161.173099547357 -0.297575298634526 -107.91507072163 -0.297575298634528 -107.91507072163 -14.800782275249 161.173099547357 +678.25 -14.8410370592669 160.403375551292 -0.297098015564591 -108.699690143969 -0.297098015564593 -108.699690143969 -14.8410370592669 160.403375551292 +683.2 -14.8831201488392 159.633757494353 -0.296573341016818 -109.484577386465 -0.296573341016817 -109.484577386465 -14.8831201488392 159.633757494353 +688.15 -14.9270511408421 158.864247534723 -0.296002408592193 -110.269740837565 -0.296002408592193 -110.269740837565 -14.9270511408421 158.864247534723 +693.1 -14.9728506813769 158.09484854887 -0.295386386248299 -111.055188685097 -0.295386386248299 -111.055188685097 -14.9728506813769 158.09484854887 +698.05 -15.0205405011035 157.325564166989 -0.294726475531207 -111.840928910123 -0.294726475531207 -111.840928910123 -15.0205405011035 157.325564166989 +703 -15.0701434530608 156.556398810455 -0.294023910782144 -112.626969280903 -0.294023910782145 -112.626969280903 -15.0701434530608 156.556398810455 +707.95 -15.1216835531372 155.787357731472 -0.293279958319086 -113.413317346967 -0.293279958319085 -113.413317346967 -15.1216835531372 155.787357731472 +712.9 -15.1751860233687 155.018447055084 -0.292495915593541 -114.19998043332 -0.292495915593543 -114.19998043332 -15.1751860233687 155.018447055084 +717.85 -15.2306773382605 154.249673823785 -0.29167311032272 -114.986965634759 -0.29167311032272 -114.986965634759 -15.2306773382605 154.249673823786 +722.8 -15.2881852743406 153.481046044935 -0.290812899597384 -115.77427981032 -0.290812899597386 -115.77427981032 -15.2881852743406 153.481046044934 +727.75 -15.3477389631779 152.712572741245 -0.28991666896563 -116.561929577868 -0.28991666896563 -116.561929577868 -15.3477389631779 152.712572741245 +732.7 -15.4093689481169 151.94426400462 -0.288985831492896 -117.349921308811 -0.288985831492895 -117.349921308811 -15.4093689481169 151.94426400462 +737.65 -15.473107245002 151.176131053652 -0.288021826798565 -118.138261122967 -0.288021826798565 -118.138261122967 -15.473107245002 151.176131053652 +742.6 -15.5389874071943 150.408186295121 -0.287026120069445 -118.926954883579 -0.287026120069447 -118.926954883579 -15.5389874071944 150.408186295121 +747.55 -15.6070445952081 149.640443389874 -0.286000201050545 -119.716008192471 -0.286000201050547 -119.716008192471 -15.6070445952081 149.640443389874 +752.5 -15.677315651328 148.872917323512 -0.284945583013515 -120.505426385376 -0.284945583013515 -120.505426385376 -15.677315651328 148.872917323512 +757.45 -15.7498391796016 148.105624482349 -0.283863801703196 -121.295214527418 -0.283863801703197 -121.295214527418 -15.7498391796016 148.105624482349 +762.4 -15.8246556316402 147.338582735158 -0.282756414262716 -122.085377408761 -0.282756414262717 -122.085377408761 -15.8246556316402 147.338582735158 +767.35 -15.9018073987045 146.571811521298 -0.281624998137592 -122.875919540436 -0.281624998137593 -122.875919540436 -15.9018073987045 146.571811521298 +772.3 -15.9813389105966 145.805331945855 -0.280471149959399 -123.666845150336 -0.2804711499594 -123.666845150335 -15.9813389105966 145.805331945855 +777.25 -16.0632967419361 145.039166882518 -0.279296484409475 -124.458158179399 -0.279296484409475 -124.458158179399 -16.0632967419361 145.039166882518 +782.2 -16.1477297264541 144.273341085012 -0.278102633063294 -125.249862277981 -0.278102633063296 -125.249862277981 -16.1477297264541 144.273341085012 +787.15 -16.2346890800053 143.507881307974 -0.276891243216042 -126.041960802411 -0.276891243216042 -126.041960802411 -16.2346890800053 143.507881307974 +792.1 -16.3242285330727 142.742816438295 -0.275663976690073 -126.834456811747 -0.275663976690073 -126.834456811747 -16.3242285330727 142.742816438295 +797.05 -16.416404473622 141.97817763806 -0.2744225086249 -127.62735306474 -0.274422508624901 -127.62735306474 -16.416404473622 141.97817763806 +802 -16.5112761012522 141.213998500375 -0.273168526250375 -128.420652016991 -0.273168526250375 -128.420652016991 -16.5112761012522 141.213998500375 +806.95 -16.608905593699 140.45031521951 -0.271903727643835 -129.214355818328 -0.271903727643834 -129.214355818328 -16.608905593699 140.45031521951 +811.9 -16.7093582868577 139.687166777005 -0.27062982047194 -130.008466310395 -0.270629820471942 -130.008466310395 -16.7093582868577 139.687166777005 +816.85 -16.8127028696308 138.924595145563 -0.269348520718017 -130.802985024459 -0.269348520718016 -130.802985024459 -16.8127028696308 138.924595145563 +821.8 -16.91901159505 138.16264551283 -0.268061551395706 -131.597913179448 -0.268061551395707 -131.597913179448 -16.91901159505 138.16264551283 +826.75 -17.028360509297 137.401366527428 -0.266770641249801 -132.393251680203 -0.266770641249801 -132.393251680203 -17.0283605092969 137.401366527428 +831.7 -17.1408297004332 136.640810569937 -0.265477523445156 -133.189001115981 -0.265477523445156 -133.189001115981 -17.1408297004332 136.640810569937 +836.65 -17.2565035688756 135.881034051898 -0.264183934244575 -133.985161759177 -0.264183934244572 -133.985161759177 -17.2565035688756 135.881034051898 +841.6 -17.375471121896 135.122097746353 -0.262891611676678 -134.781733564298 -0.262891611676677 -134.781733564298 -17.375471121896 135.122097746353 +846.55 -17.4978262947148 134.364067153934 -0.261602294194693 -135.578716167175 -0.261602294194692 -135.578716167175 -17.4978262947148 134.364067153934 +851.5 -17.6236683010791 133.607012909123 -0.260317719327228 -136.376108884424 -0.260317719327225 -136.376108884424 -17.623668301079 133.607012909123 +856.45 -17.7531020165949 132.851011231979 -0.259039622322037 -137.17391071316 -0.259039622322037 -137.17391071316 -17.7531020165949 132.851011231979 +861.4 -17.8862383985104 132.096144431458 -0.257769734783917 -137.97212033096 -0.257769734783916 -137.97212033096 -17.8862383985104 132.096144431458 +866.35 -18.0231949461439 131.342501467395 -0.256509783307828 -138.77073609609 -0.25650978330783 -138.77073609609 -18.0231949461439 131.342501467395 +871.3 -18.1640962067266 130.59017857935 -0.255261488108338 -139.569756047983 -0.25526148810834 -139.569756047983 -18.1640962067266 130.59017857935 +876.25 -18.3090743320928 129.839279991836 -0.254026561646641 -140.369177907987 -0.254026561646642 -140.369177907987 -18.3090743320928 129.839279991836 +881.2 -18.458269692424 129.089918707059 -0.252806707256306 -141.168999080373 -0.252806707256308 -141.168999080373 -18.458269692424 129.089918707059 +886.15 -18.6118315541585 128.342217398125 -0.251603617768955 -141.969216653612 -0.251603617768956 -141.969216653612 -18.6118315541585 128.342217398125 +891.1 -18.769918830225 127.596309417977 -0.250418974141193 -142.769827401916 -0.250418974141194 -142.769827401916 -18.769918830225 127.596309417977 +896.05 -18.9327009120032 126.852339941946 -0.249254444083988 -143.570827787056 -0.249254444083989 -143.570827787056 -18.9327009120032 126.852339941946 +901 -19.1003585938619 126.110467265097 -0.248111680695843 -144.372213960442 -0.248111680695846 -144.372213960442 -19.1003585938619 126.110467265097 +905.95 -19.2730851028455 125.370864279403 -0.24699232110108 -145.173981765485 -0.246992321101081 -145.173981765485 -19.2730851028455 125.370864279402 +910.9 -19.4510872481135 124.63372016055 -0.2458979850945 -145.976126740218 -0.245897985094502 -145.976126740218 -19.4510872481135 124.63372016055 +915.85 -19.6345867071513 123.89924229992 -0.244830273793877 -146.778644120205 -0.244830273793877 -146.778644120205 -19.6345867071513 123.89924229992 +920.8 -19.8238214686609 123.167658524357 -0.243790768301564 -147.581528841711 -0.243790768301565 -147.581528841711 -19.8238214686609 123.167658524357 +925.75 -20.0190474554901 122.43921965497 -0.242781028376624 -148.384775545148 -0.242781028376624 -148.384775545148 -20.0190474554901 122.43921965497 +930.7 -20.2205403551203 121.714202466927 -0.241802591118878 -149.188378578797 -0.241802591118877 -149.188378578797 -20.2205403551203 121.714202466927 +935.65 -20.4285976902505 120.992913125442 -0.240856969666242 -149.992332002788 -0.240856969666242 -149.992332002788 -20.4285976902505 120.992913125442 +940.6 -20.6435411681099 120.275691189693 -0.239945651906802 -150.796629593368 -0.239945651906801 -150.796629593368 -20.6435411681099 120.275691189693 +945.55 -20.8657193545612 119.562914297164 -0.239070099206971 -151.601264847415 -0.239070099206969 -151.601264847415 -20.8657193545612 119.562914297164 +950.5 -21.0955107281537 118.855003667024 -0.238231745157192 -152.406230987236 -0.238231745157191 -152.406230987236 -21.0955107281536 118.855003667024 +955.45 -21.3333271804946 118.152430594406 -0.237431994336578 -153.211520965614 -0.237431994336578 -153.211520965614 -21.3333271804946 118.152430594406 +960.4 -21.5796180431828 117.455724149801 -0.236672221097871 -154.017127471124 -0.236672221097872 -154.017127471124 -21.5796180431829 117.455724149801 +965.35 -21.8348747388229 116.765480352355 -0.235953768374142 -154.823042933695 -0.235953768374141 -154.823042933695 -21.8348747388229 116.765480352355 +970.3 -22.0996361752864 116.082373156485 -0.235277946508616 -155.629259530442 -0.235277946508614 -155.629259530442 -22.0996361752864 116.082373156485 +975.25 -22.3744950296561 115.40716768344 -0.234646032109009 -156.435769191724 -0.234646032109007 -156.435769191724 -22.374495029656 115.40716768344 +980.2 -22.660105102923 114.740736250761 -0.234059266927705 -157.242563607468 -0.234059266927704 -157.242563607468 -22.660105102923 114.740736250761 +985.15 -22.9571899707246 114.08407791363 -0.233518856769174 -158.049634233719 -0.233518856769173 -158.049634233719 -22.9571899707247 114.08407791363 +990.1 -23.2665532123679 113.438342447742 -0.233025970425903 -158.85697229943 -0.233025970425903 -158.85697229943 -23.2665532123679 113.438342447742 +995.05 -23.5890905742609 112.804859995029 -0.232581738644205 -159.664568813481 -0.232581738644202 -159.664568813481 -23.5890905742609 112.80485999503 +1000 -23.92580452052 112.185177992112 -0.23218725312111 -160.472414571918 -0.232187253121107 -160.472414571918 -23.92580452052 112.185177992112 diff --git a/skrf/media/tests/test_all_construction.py b/skrf/media/tests/test_all_construction.py index 482ba4150..40432b049 100644 --- a/skrf/media/tests/test_all_construction.py +++ b/skrf/media/tests/test_all_construction.py @@ -59,6 +59,9 @@ def test_thru(self): def test_line(self): self.media.line(1) + def test_line_floating(self): + self.media.line_floating(1) + def test_delay_load(self): self.media.delay_load(1,2) diff --git a/skrf/media/tests/test_definedaeptandz0.py b/skrf/media/tests/test_definedaeptandz0.py new file mode 100644 index 000000000..5a778606f --- /dev/null +++ b/skrf/media/tests/test_definedaeptandz0.py @@ -0,0 +1,177 @@ +import os +import unittest + +import numpy as np + +import skrf as rf +from skrf.media import DefinedAEpTandZ0 + +try: + from matplotlib import pyplot as plt + rf.stylely() +except ImportError: + pass + + + +class DefinedAEpTandZ0TestCase(unittest.TestCase): + """ + Testcase for the DefinedAEpTandZ0 Media + """ + def setUp(self): + """ + Read in all the network data required for tests + """ + self.data_dir_awr = os.path.dirname(os.path.abspath(__file__)) + \ + '/awr/' + + self.ref_awr = [ + {'color': 'k', + 'n': rf.Network(os.path.join(self.data_dir_awr, + 'tlinp.s2p'))}, + ] + + # default parameter set for tests + self.verbose = False # output comparison plots if True + self.Zn = 41.0635803351402 + self.A = 0.0670188722315605 + self.l = 74.2415154883262e-3 + self.ep_r = 3.25354286428265 + self.tand = 0.0133936758939493 + # frequency vector with only 3 points + self.freq = rf.Frequency(1, 3, 3, 'GHz') + + + def test_line_awr(self): + """ + Test against the AWR results + """ + if self.verbose: + fig, axs = plt.subplots(2, 2, figsize = (8,6)) + fig.suptitle('awr/skrf') + fig2, axs2 = plt.subplots(2, 2, figsize = (8,6)) + fig2.suptitle('awr/skrf residuals') + + limit_db = 1e-5 + limit_deg = 1e-4 + + for ref in self.ref_awr: + m = DefinedAEpTandZ0( + frequency = ref['n'].frequency, + z0 = self.Zn, + ep_r = self.ep_r, + A = self.A, + f_A = ref['n'].frequency.f[0], + tanD = self.tand, + z0_port = 50 + ) + line = m.line(d=self.l, unit='m') + line.name = 'skrf,awr' + + # residuals + res = line / ref['n'] + res.name = 'residuals ' + ref['n'].name + + # test if within limit lines + self.assertTrue( + np.all(np.abs(res.s_db[:, 0, 0]) < limit_db)) + self.assertTrue( + np.all(np.abs(res.s_deg[:, 0, 0]) < limit_deg)) + self.assertTrue(np.all(np.abs(res.s_db[:, 1, 0]) < limit_db)) + self.assertTrue(np.all(np.abs(res.s_deg[:, 1, 0]) < limit_deg)) + + if self.verbose: + line.plot_s_db(0, 0, ax = axs[0, 0], color = ref['color'], + linestyle = 'none', marker = 'x') + ref['n'].plot_s_db(0, 0, ax = axs[0, 0], color = ref['color']) + res.plot_s_db(0, 0, ax = axs2[0, 0], linestyle = 'dashed', + color = ref['color']) + + line.plot_s_deg(0, 0, ax = axs[0, 1], color = ref['color'], + linestyle = 'none', marker = 'x') + ref['n'].plot_s_deg(0, 0, ax = axs[0, 1], color = ref['color']) + res.plot_s_deg(0, 0, ax = axs2[0, 1], linestyle = 'dashed', + color = ref['color']) + + line.plot_s_db(1, 0, ax = axs[1, 0], color = ref['color'], + linestyle = 'none', marker = 'x') + ref['n'].plot_s_db(1, 0, ax = axs[1, 0], color = ref['color']) + res.plot_s_db(1, 0, ax = axs2[1, 0], linestyle = 'dashed', + color = ref['color']) + + line.plot_s_deg(1, 0, ax = axs[1, 1], color = ref['color'], + linestyle = 'none', marker = 'x') + ref['n'].plot_s_deg(1, 0, ax = axs[1, 1], color = ref['color']) + res.plot_s_deg(1, 0, ax = axs2[1, 1], linestyle = 'dashed', + color = ref['color']) + + + if self.verbose: + fig.tight_layout() + fig2.tight_layout() + + def test_nominal_impedance_dispersion(self): + """ + Test if passing scalar value to z0 results to this nominal impedance + being dispersed to a complex frequency-dependent characteristic + impedance. + """ + Zn = 75 + m = DefinedAEpTandZ0( + frequency = self.freq, + z0 = Zn, + ep_r = self.ep_r, + A = self.A, + f_A = self.freq.f[0], + tanD = self.tand, + z0_port = 50 + ) + # z0_characteristic complex and frequency dependant + self.assertTrue(np.all(np.abs(np.imag(m.z0_characteristic) > 1e-1))) + self.assertTrue(m.z0_characteristic[0] != m.z0_characteristic[-1]) + + def test_raw_z0_array(self): + """ + Test if passing array-like value to z0 results to this raw value being + assigned to characteristic impedance. + """ + + # Single-element array are kept as raw z0_characteristic + z1 = [75] + m1 = DefinedAEpTandZ0( + frequency = self.freq, + z0 = z1, + ep_r = self.ep_r, + A = self.A, + f_A = self.freq.f[0], + tanD = self.tand, + z0_port = 50 + ) + self.assertTrue(np.allclose(z1 * np.ones(3), m1.z0_characteristic)) + # Sequence are kept as raw z0_characteristic + z2 = [45, 50, 55] + m2 = DefinedAEpTandZ0( + frequency = self.freq, + z0 = z2, + ep_r = self.ep_r, + A = self.A, + f_A = self.freq.f[0], + tanD = self.tand, + z0_port = 50 + ) + self.assertTrue(np.allclose(z2, m2.z0_characteristic)) + # Numpy arrays are kept as raw z0_characteristic + z3 = np.array([48, 50, 52]) + m3 = DefinedAEpTandZ0( + frequency = self.freq, + z0 = z3, + ep_r = self.ep_r, + A = self.A, + f_A = self.freq.f[0], + tanD = self.tand, + z0_port = 50 + ) + self.assertTrue(np.allclose(z3, m3.z0_characteristic)) + +if __name__ == '__main__': + unittest.main() diff --git a/skrf/media/tests/test_media.py b/skrf/media/tests/test_media.py index 53d1ed01b..b7a5de332 100644 --- a/skrf/media/tests/test_media.py +++ b/skrf/media/tests/test_media.py @@ -4,6 +4,7 @@ import numpy as np from numpy.testing import assert_array_almost_equal +from skrf.constants import S_DEF_DEFAULT from skrf.frequency import Frequency from skrf.media import DefinedGammaZ0 from skrf.network import Network, concat_ports, connect @@ -182,6 +183,53 @@ def test_resistor(self): ABCD[:,0,1] = Z assert_array_almost_equal(ABCD, ntwk.a) + def test_resistor_mismatch(self): + """ + Compare the component values against s-parameters generated by QUCS. + The mismatch QUCS component is generated by renormalize() method. + """ + # Config the qucs_ntwk and media + name = 'resistor,1ohm' + qucs_ntwk = Network(os.path.join(self.files_dir, name + '.s2p')) + self.dummy_media.frequency = qucs_ntwk.frequency + mismatch_z0_tuple = ([25, 50], [25-5j, 50+10j], [25, 50-10j]) + + # Test against Y-parameter defination + def resistor_y_def(media: DefinedGammaZ0, R, **kwargs): + result = media.match(nports=2, **kwargs) + y = np.zeros(shape=result.s.shape, dtype=complex) + R = np.array(R) + y[:, 0, 0] = 1.0 / R + y[:, 1, 1] = 1.0 / R + y[:, 0, 1] = -1.0 / R + y[:, 1, 0] = -1.0 / R + result.y = y + return result + + # Test real and imag port impedance with Y-parameter defination and renormalize() + for mismatch_z0 in mismatch_z0_tuple: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0) + skrf_ntwk = self.dummy_media.resistor(1, name = name, z0=mismatch_z0) + skrf_ntwk_y = resistor_y_def(self.dummy_media, 1, z0=mismatch_z0) + self.assertEqual(qucs_ntwk_copy, skrf_ntwk) + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s) + + # Config the s_def parameters + mismatch_z0 = [25, 50-10j] + s_defs = ("power", "pseudo", "traveling") + + # Test different s_def configurations + for s_def in s_defs: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0, s_def=s_def) + skrf_ntwk = self.dummy_media.resistor(1, name=name, z0=mismatch_z0, s_def=s_def) + skrf_ntwk_y = resistor_y_def(self.dummy_media, 1, z0=mismatch_z0, s_def=s_def) + self.assertEqual(skrf_ntwk, qucs_ntwk_copy) + self.assertEqual(skrf_ntwk.s_def, qucs_ntwk_copy.s_def) + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s) + self.assertEqual(skrf_ntwk.s_def, skrf_ntwk_y.s_def) + def test_shunt_resistor(self): """ Test the naming of the network. When circuit is used to connect a @@ -218,6 +266,54 @@ def test_capacitor(self): ABCD[:,0,1] = Z assert_array_almost_equal(ABCD, ntwk.a) + def test_capacitor_mismatch(self): + """ + Compare the component values against s-parameters generated by QUCS. + The mismatch QUCS component is generated by renormalize() method. + """ + # Config the qucs_ntwk and media + name = 'capacitor,p01pF' + qucs_ntwk = Network(os.path.join(self.files_dir, name + '.s2p')) + self.dummy_media.frequency = qucs_ntwk.frequency + mismatch_z0_tuple = ([25, 50], [25-5j, 50+10j], [25, 50-10j]) + + # Test against Y-parameter defination + def capacitor_y_def(media: DefinedGammaZ0, C, **kwargs): + result = media.match(nports=2, **kwargs) + w = media.frequency.w + y = np.zeros(shape=result.s.shape, dtype=complex) + C = np.array(C) + y[:, 0, 0] = 1j * w * C + y[:, 1, 1] = 1j * w * C + y[:, 0, 1] = -1j * w * C + y[:, 1, 0] = -1j * w * C + result.y = y + return result + + # Test real and imag port impedance with Y-parameter defination and renormalize() + for mismatch_z0 in mismatch_z0_tuple: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0) + skrf_ntwk = self.dummy_media.capacitor(.01e-12, name = name, z0=mismatch_z0) + skrf_ntwk_y = capacitor_y_def(self.dummy_media, .01e-12, z0=mismatch_z0) + self.assertEqual(qucs_ntwk_copy, skrf_ntwk) + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s) + + # Config the s_def parameters + mismatch_z0 = [25, 50-10j] + s_defs = ("power", "pseudo", "traveling") + + # Test different s_def configurations + for s_def in s_defs: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0, s_def=s_def) + skrf_ntwk = self.dummy_media.capacitor(.01e-12, name=name, z0=mismatch_z0, s_def=s_def) + skrf_ntwk_y = capacitor_y_def(self.dummy_media, .01e-12, z0=mismatch_z0, s_def=s_def) + self.assertEqual(skrf_ntwk, qucs_ntwk_copy) + self.assertEqual(skrf_ntwk.s_def, qucs_ntwk_copy.s_def) + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s) + self.assertEqual(skrf_ntwk.s_def, skrf_ntwk_y.s_def) + def test_shunt_capacitor(self): """ Test the naming of the network. When circuit is used to connect a @@ -285,6 +381,55 @@ def test_inductor(self): ABCD[:,0,1] = Z assert_array_almost_equal(ABCD, ntwk.a) + def test_inductor_mismatch(self): + """ + Compare the component values against s-parameters generated by QUCS. + The mismatch QUCS component is generated by renormalize() method. + """ + # Config the qucs_ntwk and media + name = 'inductor,p1nH' + qucs_ntwk = Network(os.path.join(self.files_dir, name + '.s2p')) + self.dummy_media.frequency = qucs_ntwk.frequency + mismatch_z0_tuple = ([25, 50], [25-5j, 50+10j], [25, 50-10j]) + + # Test against Y-parameter defination + def inductor_y_def(media: DefinedGammaZ0, L, **kwargs): + result = media.match(nports=2, **kwargs) + w = media.frequency.w + y = np.zeros(shape=result.s.shape, dtype=complex) + L = np.array(L) + y[:, 0, 0] = 1.0 / (1j * w * L) + y[:, 1, 1] = 1.0 / (1j * w * L) + y[:, 0, 1] = -1.0 / (1j * w * L) + y[:, 1, 0] = -1.0 / (1j * w * L) + result.y = y + return result + + # Test real and imag port impedance with Y-parameter defination and renormalize() + for mismatch_z0 in mismatch_z0_tuple: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0) + skrf_ntwk = self.dummy_media.inductor(.1e-9, name = name, z0=mismatch_z0) + skrf_ntwk_y = inductor_y_def(self.dummy_media, .1e-9, z0=mismatch_z0) + self.assertEqual(qucs_ntwk_copy, skrf_ntwk) + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s) + + # Config the s_def parameters + mismatch_z0 = [25, 50-10j] + s_defs = ("power", "pseudo", "traveling") + + # Test different s_def configurations + for s_def in s_defs: + qucs_ntwk_copy = qucs_ntwk.copy() + qucs_ntwk_copy.renormalize(mismatch_z0, s_def=s_def) + skrf_ntwk = self.dummy_media.inductor(.1e-9, name=name, z0=mismatch_z0, s_def=s_def) + skrf_ntwk_y = inductor_y_def(self.dummy_media, .1e-9, z0=mismatch_z0, s_def=s_def) + self.assertEqual(skrf_ntwk, qucs_ntwk_copy) + self.assertEqual(skrf_ntwk.s_def, qucs_ntwk_copy.s_def) + # numerical precision is slightly reduced when using pseudo s_def + assert_array_almost_equal(skrf_ntwk.s, skrf_ntwk_y.s, decimal={'pseudo': 5}.get(s_def, 6)) + self.assertEqual(skrf_ntwk.s_def, skrf_ntwk_y.s_def) + def test_shunt_inductor(self): """ Test the naming of the network. When circuit is used to connect a @@ -368,6 +513,75 @@ def test_isolator(self): skrf_ntwk = self.dummy_media.isolator(name = name) self.assertEqual(name, skrf_ntwk.name) + def test_line_floating(self): + """ + Test the naming of the network. When circuit is used to connect a + topology of networks, they should have unique names. + """ + name = 'floating_line' + self.dummy_media.frequency = Frequency(1, 1, 1, unit='GHz') + skrf_ntwk = self.dummy_media.line_floating(90, 'deg', name=name) + self.assertEqual(name, skrf_ntwk.name) + + # Expected to be a 4-port network + self.assertEqual(skrf_ntwk.nports, 4) + + # zero length case + s_zero = 1/2 * np.array([[1, 1, 1, -1], + [1, 1, -1, 1], + [1, -1, 1, 1], + [-1, 1, 1, 1]] + ).transpose().reshape(-1,4,4) + for z0 in [50, 1]: + zero_length = self.dummy_media.line_floating(d=0, z0=z0) + assert_array_almost_equal(zero_length.s, s_zero) + + def test_line_floating2(self): + """ + Test against line + """ + d = 100 + unit = 'mm' + z0 = 50 + line = self.dummy_media.line(d, unit, z0) + line_floating = self.dummy_media.line_floating(d, unit, z0) + gnd = self.dummy_media.short(n_ports=1) + line_floating = connect(line_floating, 3, gnd, 0) + line_floating = connect(line_floating, 2, gnd, 0) + assert_array_almost_equal(line_floating.s, line.s) + + def test_line_floating3(self): + """ + Test against Y-parameter definition + """ + d = 100 + unit = 'mm' + + def yparam_ref(media, d, unit, z0, s_def=S_DEF_DEFAULT): + result = media.match(nports=4, z0=z0, s_def='traveling') + + theta = media.electrical_length(media.to_meters(d=d, unit=unit)) + + # From AWR docs on TLINP4. + y11 = 1 / (z0 * np.tanh(theta)) + y12 = -1 / (z0 * np.sinh(theta)) + y22 = y11 + y21 = y12 + + result.y = \ + np.array([[ y11, y12, -y11, -y12], + [ y21, y22, -y21, -y22], + [-y11, -y12, y11, y12], + [-y21, -y22, y21, y22]]).transpose(2,0,1) + if media.z0_port is not None: + result.renormalize(media.z0_port) + result.renormalize(result.z0, s_def=s_def) + return result + + for z0 in [50, 1, 100+10j]: + line = self.dummy_media.line_floating(d, unit, z0) + ref_line = yparam_ref(self.dummy_media, d, unit, z0) + assert_array_almost_equal(ref_line.s, line.s) def test_scalar_gamma_z0_media(self): """ diff --git a/skrf/network.py b/skrf/network.py index 505f9e93b..37e0fcc99 100644 --- a/skrf/network.py +++ b/skrf/network.py @@ -429,6 +429,8 @@ def __init__(self, file: str = None, name: str = None, params: dict = None, # allows user to pass StringIO, filename or file obj if isinstance(file, io.StringIO): + if not hasattr(file, "name") and name is not None: + file.name = name self.read_touchstone(file, self.encoding) else: @@ -2043,13 +2045,30 @@ def is_lossless(self, tol: float = mf.ALMOST_ZERO) -> bool: return True ## CLASS METHODS - def copy(self) -> Network: + def copy(self, *, shallow_copy: bool = False) -> Network: """ Return a copy of this Network. Needed to allow pass-by-value for a Network instead of pass-by-reference + Parameters + ---------- + shallow_copy : bool, optional + If True, the method creates a new Network object with empty s-parameters that share the same shape + as the original Network, but without copying the actual s-parameters data. This is useful when you + plan to immediately modify the s-parameters after creating the Network, as a deep copy would be + unnecessary and costly. Using `shallow_copy` improves performance by leveraging lazy initialization + through `numpy's np.empty()`, which allocates virtual memory without immediate physical memory + allocation, deferring actual memory initialization until first access. This approach can significantly + enhance `copy()` performance when dealing with large `Network` objects, when you are intended for + immediate modification after the Network's creation. + + Note + ---- + If you require a complete copy of the `Network` instance or need to perform operation on the s-parameters + of the copied Network, it is essential not to use the `shallow_copy` parameter! + Returns ------- ntwk : :class:`Network` @@ -2058,7 +2077,11 @@ def copy(self) -> Network: """ ntwk = Network(z0=self.z0, s_def=self.s_def, comments=self.comments) - ntwk._s = self.s.copy() + ntwk._s = ( + np.empty(shape=self.s.shape, dtype=self.s.dtype) + if shallow_copy + else self.s.copy() + ) ntwk.frequency._f = self.frequency._f.copy() ntwk.frequency.unit = self.frequency.unit ntwk.port_modes = self.port_modes.copy() @@ -2884,6 +2907,7 @@ def interpolate(self, freq_or_n: Frequency | NumberLike, basis: str = 's', #Not supported by rational_interp is_rational = True else: + kwargs["kind"] = kind if kind is not None else "linear" f_interp = interp1d # interpret input @@ -4903,7 +4927,7 @@ def plot_reciprocity2(self, db=False, *args, **kwargs): @copy_doc(rfplt.plot_s_db_time) def plot_s_db_time(self, center_to_dc=None, *args, **kwargs): - return rfplt.plot_s_db_time(self, center_to_dc, *args, **kwargs) + return rfplt.plot_s_db_time(self, *args, center_to_dc=center_to_dc, **kwargs) @copy_doc(rfplt.plot_s_smith) def plot_s_smith(self, m=None, n=None,r=1, ax=None, show_legend=True,\ @@ -5072,13 +5096,15 @@ def connect(ntwkA: Network, k: int, ntwkB: Network, l: int, num: int = 1) -> Net s_def = 'traveling' # create output Network, from copy of input - ntwkC = ntwkA.copy() + # Since ntwkC's s-parameters will change later, use shallow_copy for speedup + ntwkC = ntwkA.copy(shallow_copy=True) # if networks' z0's are not identical, then connect a impedance # mismatch, which takes into account the effect of differing port # impedances. # import pdb;pdb.set_trace() - if not assert_z0_at_ports_equal(ntwkA, k, ntwkB, l): + z0_equal = assert_z0_at_ports_equal(ntwkA, k, ntwkB, l) + if not z0_equal: # connect a impedance mismatch, which will takes into account the # effect of differing port impedances mismatch = impedance_mismatch(ntwkA.z0[:, k], ntwkB.z0[:, l], s_def) @@ -5090,7 +5116,7 @@ def connect(ntwkA: Network, k: int, ntwkB: Network, l: int, num: int = 1) -> Net ntwkC.renumber(from_ports=[ntwkC.nports - 1] + list(range(k, ntwkC.nports - 1)), to_ports=list(range(k, ntwkC.nports))) # call s-matrix connection function - ntwkC.s = connect_s(ntwkC.s, k, ntwkB.s, l, num) + ntwkC.s = connect_s(ntwkC.s if not z0_equal else ntwkA.s, k, ntwkB.s, l, num) # combine z0 arrays and remove ports which were `connected` ntwkC.z0 = np.hstack( @@ -5461,20 +5487,24 @@ def innerconnect(ntwkA: Network, k: int, l: int, num: int = 1) -> Network: if (l + num - 1 > ntwkA.nports - 1): raise IndexError('Port `l` out of range') + # 'power' is not supported, convert to supported definition and back afterwards + if ntwkA.s_def == 'power': + ntwkA = ntwkA.copy() + ntwkA.renormalize(ntwkA.z0, 'pseudo') + # create output Network, from copy of input - ntwkC = ntwkA.copy() + # Since ntwkC's s-parameters will change later, use shallow_copy for speedup + ntwkC = ntwkA.copy(shallow_copy=True) s_def_original = ntwkC.s_def - # 'power' is not supported, convert to supported definition and back afterwards - if ntwkC.s_def == 'power': - ntwkC.renormalize(ntwkC.z0, 'pseudo') + z0_equal = (ntwkC.z0[:, k] == ntwkC.z0[:, l]).all() - if not (ntwkC.z0[:, k] == ntwkC.z0[:, l]).all(): + if not z0_equal: # connect a impedance mismatch, which will takes into account the # effect of differing port impedances - mismatch = impedance_mismatch(ntwkC.z0[:, k], ntwkC.z0[:, l], ntwkC.s_def) - ntwkC.s = connect_s(ntwkC.s, k, mismatch, 0, num=-1) + mismatch = impedance_mismatch(ntwkA.z0[:, k], ntwkA.z0[:, l], ntwkA.s_def) + ntwkC.s = connect_s(ntwkA.s, k, mismatch, 0, num=-1) # the connect_s() put the mismatch's output port at the end of # ntwkC's ports. Fix the new port's impedance, then insert it # at position k where it belongs. @@ -5483,7 +5513,7 @@ def innerconnect(ntwkA: Network, k: int, l: int, num: int = 1) -> Network: to_ports=list(range(k, ntwkC.nports))) # call s-matrix connection function - ntwkC.s = innerconnect_s(ntwkC.s, k, l) + ntwkC.s = innerconnect_s(ntwkC.s if not z0_equal else ntwkA.s, k, l) # update the characteristic impedance matrix ntwkC.z0 = np.delete(ntwkC.z0, list(range(k, k + 1)) + list(range(l, l + 1)), 1) diff --git a/skrf/tests/test_network.py b/skrf/tests/test_network.py index 5b60f2c04..5aa589f8d 100644 --- a/skrf/tests/test_network.py +++ b/skrf/tests/test_network.py @@ -329,6 +329,7 @@ def test_constructor_from_stringio(self): sio.name = os.path.basename(filename) # hack a bug to touchstone reader rf.Network(sio) + def test_constructor_from_stringio_hfss(self): filename = os.path.join(self.test_dir, 'hfss_oneport.s1p') with open(filename) as fid: data = fid.read() @@ -336,6 +337,13 @@ def test_constructor_from_stringio(self): sio.name = os.path.basename(filename) # hack a bug to touchstone reader rf.Network(sio) + def test_constructor_from_stringio_name_kwawrg(self): + filename = os.path.join(self.test_dir, 'ntwk1.s2p') + with open(filename) as fid: + data = fid.read() + sio = io.StringIO(data) + rf.Network(sio, name=filename) + def test_different_ext(self): filename= os.path.join(self.test_dir, 'ntwk1.s2p') for par in ["g", "h", "s", "y", "z"]: @@ -1461,11 +1469,17 @@ def test_add(self): self.assertTrue( ((a+[1+1j,2+2j]).s == np.array([[[2+3j]],[[5+6j]]])).all()) - def test_interpolate(self): - a = rf.N(f=[1,2],s=[1+2j, 3+4j],z0=1, f_unit="ghz") - freq = rf.F.from_f(np.linspace(1,2,4), unit='ghz') - b = a.interpolate(freq) - # TODO: numerically test for correct interpolation + def test_interpolate_linear(self): + net = rf.Network(f=[0, 1, 3, 4], s=[0,1,9,16], f_unit="Hz") + + interp = net.interpolate(rf.Frequency(0, 4, 5, unit="Hz"), kind="linear") + assert np.allclose(interp.s[2], 5.0) + + def test_interpolate_cubic(self): + net = rf.Network(f=[0, 1, 3, 4], s=[0,1,9,16], f_unit="Hz") + + interp = net.interpolate(rf.Frequency(0, 4, 5, unit="Hz"), kind="cubic") + assert np.allclose(interp.s[2], 4.0) def test_interpolate_rational(self): a = rf.N(f=np.linspace(1,2,5),s=np.linspace(0,1,5)*(1+1j),z0=1, f_unit="ghz") @@ -1479,21 +1493,6 @@ def test_interpolate_rational(self): self.assertTrue(all(np.diff(np.abs(b.s.flatten())) > 0)) self.assertTrue(b.z0[0] == a.z0[0]) - def test_interpolate_linear(self): - a = rf.N(f=[1,2],s=[1+2j, 3+4j],z0=[1,2], f_unit="ghz") - freq = rf.F.from_f(np.linspace(1,2,3,endpoint=True), unit='GHz') - b = a.interpolate(freq, kind='linear') - self.assertFalse(any(np.isnan(b.s))) - # Test that the endpoints are the equal - # Middle point can also be calculated in this case - self.assertTrue(b.s[0] == a.s[0]) - self.assertTrue(b.s[1] == 0.5*(a.s[0] + a.s[1])) - self.assertTrue(b.s[-1] == a.s[-1]) - # Check Z0 interpolation - self.assertTrue(b.z0[0] == a.z0[0]) - self.assertTrue(b.z0[1] == 0.5*(a.z0[0] + a.z0[1])) - self.assertTrue(b.z0[-1] == a.z0[-1]) - def test_interpolate_freq_cropped(self): a = rf.N(f=np.arange(20), s=np.arange(20)*(1+1j),z0=1, f_unit="ghz") freq = rf.F.from_f(np.linspace(1,2,3,endpoint=True), unit='GHz') diff --git a/skrf/tests/test_plotting.py b/skrf/tests/test_plotting.py index 2b6b5b760..bd3cfeecd 100644 --- a/skrf/tests/test_plotting.py +++ b/skrf/tests/test_plotting.py @@ -45,6 +45,7 @@ def test_plot_reciprocity2(ntwk1_dc: rf.Network, ): def test_plot_s_db_time(ntwk1_dc: rf.Network, ): ntwk1_dc.plot_s_db_time() + ntwk1_dc.plot_s_db_time(m=0, n=0) def test_plot_s_smith(ntwk1_dc: rf.Network, ): ntwk1_dc.plot_s_smith() diff --git a/skrf/tests/test_vectorfitting.py b/skrf/tests/test_vectorfitting.py index 785654b13..8bbaea650 100644 --- a/skrf/tests/test_vectorfitting.py +++ b/skrf/tests/test_vectorfitting.py @@ -79,7 +79,7 @@ def test_dc(self): @pytest.mark.skipif( "PySpice" not in sys.modules, - reason="Spice subcircuit uses Engformatter which is not available without matplotlib.") + reason="test_spice_subcircuit uses PySpice parser but it is not available.") def test_spice_subcircuit(self): # fit ring slot example network nw = skrf.data.ring_slot @@ -92,15 +92,15 @@ def test_spice_subcircuit(self): tmp_file.close() vf.write_spice_subcircuit_s(name) - # written tmp file should contain 69 lines - with open(name) as f: - parser = SpiceParser(name) + parser = SpiceParser(name) - assert len(parser.subcircuits[0]._statements) == 52 + # Number of elements on global level + assert len(parser.subcircuits[0]._statements) == 38 + # Number of elements in RLCG subckt assert len(parser.subcircuits[1]._statements) == 4 + # Number of elements in RL subckt assert len(parser.subcircuits[2]._statements) == 2 - os.remove(name) def test_read_write_npz(self): @@ -153,6 +153,17 @@ def test_passivity_enforcement(self): # check if model is now passive self.assertTrue(vf.is_passive()) + def test_autofit(self): + vf = skrf.VectorFitting(skrf.data.ring_slot) + vf.auto_fit() + + assert vf.get_model_order(vf.poles) == 6 + assert np.sum(vf.poles.imag == 0.0) == 0 + assert np.sum(vf.poles.imag > 0.0) == 3 + + assert np.allclose(vf.get_rms_error(), 1.2748979815157275e-06) + + suite = unittest.TestLoader().loadTestsFromTestCase(VectorFittingTestCase) unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/skrf/vectorFitting.py b/skrf/vectorFitting.py index 2829e7a7c..41fc09b57 100644 --- a/skrf/vectorFitting.py +++ b/skrf/vectorFitting.py @@ -7,11 +7,7 @@ from typing import TYPE_CHECKING, Any import numpy as np - -try: - from matplotlib.ticker import EngFormatter -except ImportError: - pass +from scipy.integrate import trapezoid from .util import Axes, axes_kwarg @@ -140,7 +136,7 @@ def get_spurious(poles: np.ndarray, residues: np.ndarray, n_freqs: int = 101, ga omega_eval = np.linspace(np.min(poles.imag) / 3, np.max(poles.imag) * 3, n_freqs) h = (residues[:, None, :] / (1j * omega_eval[:, None] - poles) + np.conj(residues[:, None, :]) / (1j * omega_eval[:, None] - np.conj(poles))) - norm2 = np.sqrt(np.trapezoid(h.real ** 2 + h.imag ** 2, omega_eval, axis=1)) + norm2 = np.sqrt(trapezoid(h.real ** 2 + h.imag ** 2, omega_eval, axis=1)) spurious = np.all(norm2 / np.mean(norm2) < gamma, axis=0) return spurious @@ -2260,30 +2256,38 @@ def plot_passivation(self, ax: Axes = None) -> Axes: ax.set_ylabel('Max. singular value') return ax - def write_spice_subcircuit_s(self, file: str, fitted_model_name: str = "s_equivalent") -> None: + def write_spice_subcircuit_s(self, file: str, fitted_model_name: str = "s_equivalent", + create_reference_pins: bool=False) -> None: """ - Creates an equivalent N-port SPICE subcircuit based on its vector fitted S parameter responses. + Creates an equivalent N-port subcircuit based on its vector fitted S parameter responses + in spice simulator netlist syntax Parameters ---------- file : str - Path and filename including file extension (usually .sp) for the SPICE subcircuit file. + Path and filename including file extension (usually .sNp) for the subcircuit file. + fitted_model_name: str Name of the resulting model, default "s_equivalent" + create_reference_pins: bool + If set to True, the synthesized subcircuit will have N pin-pairs: + P0, P0_reference, ..., PN, PN_reference + + If set to False, the synthesized subcircuit will have N pins + P0, ..., PN + In this case, the reference nodes will be internally connected + to the global ground net 0. + + The default is False + Returns ------- None - Notes - ----- - In the SPICE subcircuit, all ports will share a common reference node (global SPICE ground on node 0). The - equivalent circuit uses linear dependent current sources on all ports, which are controlled by the currents - through equivalent admittances modelling the parameters from a vector fit. This approach is based on [#]_. - Examples -------- - Load and fit the `Network`, then export the equivalent SPICE subcircuit: + Load and fit the `Network`, then export the equivalent subcircuit: >>> nw_3port = skrf.Network('my3port.s3p') >>> vf = skrf.VectorFitting(nw_3port) @@ -2292,151 +2296,151 @@ def write_spice_subcircuit_s(self, file: str, fitted_model_name: str = "s_equiva References ---------- - .. [#] G. Antonini, "SPICE Equivalent Circuits of Frequency-Domain Responses", IEEE Transactions on + .. [1] G. Antonini, "SPICE Equivalent Circuits of Frequency-Domain Responses", IEEE Transactions on Electromagnetic Compatibility, vol. 45, no. 3, pp. 502-512, August 2003, - DOI: https://doi.org/10.1109/TEMC.2003.815528 + doi: https://doi.org/10.1109/TEMC.2003.815528 + + .. [2] C. -C. Chou and J. E. Schutt-Ainé, "Equivalent Circuit Synthesis of Multiport S Parameters in + Pole–Residue Form," in IEEE Transactions on Components, Packaging and Manufacturing Technology, + vol. 11, no. 11, pp. 1971-1979, Nov. 2021, doi: 10.1109/TCPMT.2021.3115113 + + .. [3] Romano D, Antonini G, Grossner U, Kovačević-Badstübner I. Circuit synthesis techniques of + rational models of electromagnetic systems: A tutorial paper. Int J Numer Model. 2019 + doi: https://doi.org/10.1002/jnm.2612 + """ - # list of subcircuits for the equivalent admittances + # List of subcircuits for the equivalent admittances subcircuits = [] - # provides a unique SPICE subcircuit identifier (X1, X2, X3, ...) + # Provides a unique subcircuit identifier (X1, X2, X3, ...) def get_new_subckt_identifier(): subcircuits.append(f'X{len(subcircuits) + 1}') return subcircuits[-1] - # use engineering notation for the numbers in the SPICE file (1000 --> 1k) - formatter = EngFormatter(sep="", places=3, usetex=False) - # replace "micron" sign by "u" and "mega" sign by "meg" - letters_dict = formatter.ENG_PREFIXES - letters_dict.update({-6: 'u', 6: 'meg'}) - formatter.ENG_PREFIXES = letters_dict - with open(file, 'w') as f: # write title line f.write('* EQUIVALENT CIRCUIT FOR VECTOR FITTED S-MATRIX\n') - f.write('* Created using scikit-rf vectorFitting.py\n') - f.write('*\n') + f.write('* Created using scikit-rf vectorFitting.py\n\n') - # define the complete equivalent circuit as a subcircuit with one input node per port - # those port nodes are labeled p1, p2, p3, ... - # all ports share a common node for ground reference (node 0) - str_input_nodes = '' - for n in range(self.network.nports): - str_input_nodes += f'p{n + 1} ' + # Create subcircuit pin string and reference nodes + if create_reference_pins: + str_input_nodes = " ".join(map(lambda x: f'p{x + 1} r{x + 1}', range(self.network.nports))) + ref_nodes = list(map(lambda x: f'r{x + 1}', range(self.network.nports))) + else: + str_input_nodes = " ".join(map(lambda x: f'p{x + 1}', range(self.network.nports))) + ref_nodes = list(map(lambda x: '0', range(self.network.nports))) f.write(f'.SUBCKT {fitted_model_name} {str_input_nodes}\n') for n in range(self.network.nports): - f.write('*\n') - f.write(f'* port {n + 1}\n') - # add port reference impedance z0 (has to be resistive, no imaginary part) - f.write(f'R{n + 1} a{n + 1} 0 {np.real(self.network.z0[0, n])}\n') - - # add dummy voltage sources (V=0) to measure the input current - f.write(f'V{n + 1} p{n + 1} a{n + 1} 0\n') - - # CCVS and VCVS driving the transfer admittances with a = V/2/sqrt(Z0) + I/2*sqrt(Z0) - # In - f.write(f'H{n + 1} nt{n + 1} nts{n + 1} V{n + 1} {np.real(self.network.z0[0, n])}\n') - # Vn - f.write(f'E{n + 1} nts{n + 1} 0 p{n + 1} 0 {1}\n') + f.write(f'\n* Port network for port {n + 1}\n') + + # Calculate sqrt of Z0 for port current port + sqrt_Z0_n=np.sqrt(np.real(self.network.z0[0, n])) + + # Port reference impedance Z0 + f.write(f'R_ref_{n + 1} p{n+1} a{n + 1} {np.real(self.network.z0[0, n])}\n') + + # CCVS implementing the reflected wave b. + # Also used as current sensor to measure the input current + # + # The type of the source (voltage source) and its gain 2*sqrt(Z0N) arise from the + # definition of the reflected wave b at port N: bN=(VN-Z0N*IN)/(2*sqrt(Z0N)) + # This equation represents the Kirchhoff voltage law of the port network: + # 2*sqrt(Z0N)*bN=VN-Z0N*IN + # The left hand side of the equation is realized with a (controlled) voltage + # source with a gain of 2*sqrt(Z0N). + f.write(f'H_b_{n + 1} a{n + 1} {ref_nodes[n]} V_c_{n + 1} {2.0*sqrt_Z0_n}\n') + + f.write(f'* Differential incident wave a sources for transfer from port {n + 1}\n') + + # CCVS and VCVS driving the transfer admittances with incident wave a = V/(2.0*sqrt(Z0)) + I*sqrt(Z0)/2 + # + # These voltage sources in series realize the incident wave a. The types of the sources + # and their gains arise from the definition of the incident wave a at port N: + # aN=VN/(2*sqrt(Z0N)) + IN*sqrt(Z0N)/2 + # So we need a VCVS with a gain 1/(2*sqrt(Z0N)) in series with a CCVS with a gain sqrt(Z0N)/2 + f.write(f'H_p_{n + 1} nt_p_{n + 1} nts_p_{n + 1} H_b_{n + 1} {0.5*sqrt_Z0_n}\n') + f.write(f'E_p_{n + 1} nts_p_{n + 1} {ref_nodes[n]} p{n + 1} {ref_nodes[n]} {1.0/(2.0*sqrt_Z0_n)}\n') + + # VCVS driving the transfer admittances with -a + # + # This source just copies the a wave and multiplies it by -1 to implement the negative side + # of the differential a wave. The inversion of the sign is done by the connecting the source + # in opposite direction to the reference node. Thus, the gain is 1. + f.write(f'E_n_{n + 1} {ref_nodes[n]} nt_n_{n + 1} nt_p_{n + 1} {ref_nodes[n]} 1\n') + + f.write(f'* Current sensor on center node for transfer to port {n + 1}\n') + + # Current sensor for the transfer to current port + f.write(f'V_c_{n + 1} nt_c_{n + 1} {ref_nodes[n]} 0\n') for j in range(self.network.nports): - f.write(f'* transfer network for s{n + 1}{j + 1}\n') + f.write(f'* Transfer network from port {j + 1} to port {n + 1}\n') - # stacking order in VectorFitting class variables: + # Stacking order in VectorFitting class variables: # s11, s12, s13, ..., s21, s22, s23, ... i_response = n * self.network.nports + j - # add CCCS to generate the scattered current I_nj at port n - # control current is measured by the dummy voltage source at the transfer network Y_nj - # the scattered current is injected into the port (source positive connected to ground) - f.write(f'F{n + 1}{j + 1} 0 a{n + 1} V{n + 1}{j + 1} ' - f'{formatter(1 / np.real(self.network.z0[0, n]))}\n') - f.write(f'F{n + 1}{j + 1}_inv a{n + 1} 0 V{n + 1}{j + 1}_inv ' - f'{formatter(1 / np.real(self.network.z0[0, n]))}\n') - - # add dummy voltage source (V=0) in series with Y_nj to measure current through transfer admittance - f.write(f'V{n + 1}{j + 1} nt{j + 1} nt{n + 1}{j + 1} 0\n') - f.write(f'V{n + 1}{j + 1}_inv nt{j + 1} nt{n + 1}{j + 1}_inv 0\n') - - # add corresponding transfer admittance Y_nj, which is modulating the control current - # the transfer admittance is a parallel circuit (sum) of individual admittances - f.write(f'* transfer admittances for S{n + 1}{j + 1}\n') - - # start with proportional and constant term of the model + # Start with proportional and constant term of the model # H(s) = d + s * e model # Y(s) = G + s * C equivalent admittance g = self.constant_coeff[i_response] c = self.proportional_coeff[i_response] - # add R for constant term + # R for constant term if g < 0: - f.write(f'R{n + 1}{j + 1} nt{n + 1}{j + 1}_inv 0 {formatter(np.abs(1 / g))}\n') + f.write(f'R{n + 1}_{j + 1} nt_n_{j + 1} nt_c_{n + 1} {np.abs(1 / g)}\n') elif g > 0: - f.write(f'R{n + 1}{j + 1} nt{n + 1}{j + 1} 0 {formatter(1 / g)}\n') + f.write(f'R{n + 1}_{j + 1} nt_p_{j + 1} nt_c_{n + 1} {1 / g}\n') - # add C for proportional term + # C for proportional term if c < 0: - f.write(f'C{n + 1}{j + 1} nt{n + 1}{j + 1}_inv 0 {formatter(np.abs(c))}\n') + f.write(f'C{n + 1}_{j + 1} nt_n_{j + 1} nt_c_{n + 1} {np.abs(c)}\n') elif c > 0: - f.write(f'C{n + 1}{j + 1} nt{n + 1}{j + 1} 0 {formatter(c)}\n') + f.write(f'C{n + 1}_{j + 1} nt_p_{j + 1} nt_c_{n + 1} {c}\n') - # add pairs of poles and residues + # Transfer admittances represented by poles and residues for i_pole in range(len(self.poles)): pole = self.poles[i_pole] residue = self.residues[i_response, i_pole] - node = get_new_subckt_identifier() + f' nt{n + 1}{j + 1}' + node = get_new_subckt_identifier() if np.real(residue) < 0.0: - # multiplication with -1 required, otherwise the values for RLC would be negative - # this gets compensated by inverting the transfer current direction for this subcircuit + # Multiplication with -1 required, otherwise the values for RLC would be negative. + # This gets compensated by inverting the transfer current direction for this subcircuit residue = -1 * residue - node += '_inv' + node += f' nt_n_{j + 1} nt_c_{n + 1}' + else: + node += f' nt_p_{j + 1} nt_c_{n + 1}' if np.imag(pole) == 0.0: - # real pole; add rl_admittance + # Real pole; Add rl_admittance l = 1 / np.real(residue) r = -1 * np.real(pole) / np.real(residue) - f.write(node + f' 0 rl_admittance res={formatter(r)} ind={formatter(l)}\n') + f.write(node + f' rl_admittance res={r} ind={l}\n') else: - # complex pole of a conjugate pair; add rcl_vccs_admittance - l = 1 / (2 * np.real(residue)) - b = -2 * (np.real(residue) * np.real(pole) + np.imag(residue) * np.imag(pole)) + # Complex pole of a conjugate pair; Add rcl_vccs_admittance r = -1 * np.real(pole) / np.real(residue) c = 2 * np.real(residue) / (np.abs(pole) ** 2) - gm_add = b * l * c - if gm_add < 0: - m = -1 - else: - m = 1 - f.write(node + f' 0 rcl_vccs_admittance res={formatter(r)} cap={formatter(c)} ' - f'ind={formatter(l)} gm={formatter(np.abs(gm_add))} mult={int(m)}\n') - - f.write('.ENDS s_equivalent\n') - - f.write('*\n') - - # subcircuit for an active RCL+VCCS equivalent admittance Y(s) of a complex-conjugate pole-residue pair H(s) - # Residue: c = c' + j * c" - # Pole: p = p' + j * p" - # H(s) = c / (s - p) + conj(c) / (s - conj(p)) - # = (2 * c' * s - 2 * (c'p' + c"p")) / (s ** 2 - 2 * p' * s + |p| ** 2) - # Y(S) = (1 / L * s + b) / (s ** 2 + R / L * s + 1 / (L * C)) - f.write('.SUBCKT rcl_vccs_admittance n_pos n_neg res=1k cap=1n ind=100p gm=1m mult=1\n') + l = 1 / (2 * np.real(residue)) + b = -2 * (np.real(residue) * np.real(pole) + np.imag(residue) * np.imag(pole)) + gm = b * l * c + f.write(node + f' rcl_vccs_admittance res={r} cap={c} ind={l} gm={gm}\n') + + f.write(f'.ENDS {fitted_model_name}\n\n') + + # Subcircuit for an RLCG equivalent admittance of a complex-conjugate pole-residue pair + f.write('.SUBCKT rcl_vccs_admittance n_pos n_neg res=1e3 cap=1e-9 ind=100e-12 gm=1e-3\n') f.write('L1 n_pos 1 {ind}\n') f.write('C1 1 2 {cap}\n') f.write('R1 2 n_neg {res}\n') - f.write('G1 n_pos n_neg 1 2 {gm * mult}\n') - f.write('.ENDS rcl_vccs_admittance\n') - - f.write('*\n') + f.write('G1 n_pos n_neg 1 2 {gm}\n') + f.write('.ENDS rcl_vccs_admittance\n\n') - # subcircuit for a passive RL equivalent admittance Y(s) of a real pole-residue pair H(s) - # H(s) = c / (s - p) - # Y(s) = 1 / L / (s + s * R / L) - f.write('.SUBCKT rl_admittance n_pos n_neg res=1k ind=100p\n') + # Subcircuit for an RL equivalent admittance of a real pole-residue pair + f.write('.SUBCKT rl_admittance n_pos n_neg res=1e3 ind=100e-12\n') f.write('L1 n_pos 1 {ind}\n') f.write('R1 1 n_neg {res}\n') - f.write('.ENDS rl_admittance\n') + f.write('.ENDS rl_admittance\n\n')