S-parameter analysis

ground 0 

load "resistor.osdi"
load "capacitor.osdi"

model r resistor
model c capacitor
model vsrc vsource
model vccs vccs

r1 (1 0) r r=1k
r2 (2 0) r r=2k
c1 (1 2) c c=1u
g1 (2 0 1 0) vccs gain=1e-3

// Port 1, 50 Ohm, interface plane at 1,0
vp1 (a1 0) vsrc dc=0
rp1 (a1 1) r r=50

// Port 2, 75 Ohm, interface plane at 2,0
vp2 (a2 0) vsrc dc=0
rp2 (a2 2) r r=75

control
  abort always
  print hierarchy
  print unknowns
  
  // options rawfile="binary" reltol=1e-14 vntol=1e-12 op_debug=0 // smsig_debug=102
  analysis sp1 acsp ports=["vp1", "rp1", "vp2", "rp2"] from=1 to=100M mode="dec" points=10

  postprocess(PYTHON, "runme.py")
endc

embed "runme.py" <<<FILE
from rawfile import rawread
from runtest import *
import numpy as np
import sys
import diocalc

tests = []

sp1 = rawread('sp1.raw').get()

# Manually compute Y and S, input data
r1 = 1e3
r2 = 2e3
c1 = 1e-6
g1 = 1e-3
z0 = np.array([50.0, 75.0])

# Frequency and j omega vectors
f = np.real(sp1["frequency"])
jw = 1j*2*np.pi*f

# Y parameters, last index is frequency
Y = np.array([
  [ 1/r1 + jw*c1, -jw*c1        ], 
  [ -jw*c1+g1,     1/r2 + jw*c1 ]
])

# Make first index the frequeny index
Y = np.transpose(Y, (2, 0, 1))
K, N, _ = Y.shape

# Construct identity matrices
I = np.broadcast_to(np.eye(N, dtype=complex), (K, N, N))

# Construct diagonal Z0 matrices
G = np.broadcast_to(np.diag(z0), (K, N, N))

# Construct 1/(2 sqrt(Z0)) matrices
F = np.broadcast_to(np.diag(1/(2*np.sqrt(z0))), (K, N, N))

# Convert to S 
#   S = (F(I - G Y)) (F(I + G Y))^-1 = A B^-1
# Solve X B = A
#   BT XT = AT
ZY = G @ Y
A = F @ (I - ZY)
B = F @ (I + ZY)
S = np.linalg.solve(
    B.transpose(0, 2, 1),
    A.transpose(0, 2, 1)
).transpose(0, 2, 1)

s11exact = S[:,0,0]
s12exact = S[:,0,1]
s21exact = S[:,1,0]
s22exact = S[:,1,1]

s11 = sp1["s(1,1)"]
s12 = sp1["s(1,2)"]
s21 = sp1["s(2,1)"]
s22 = sp1["s(2,2)"]

status = (relDiff(s11, s11exact, 1e-9) < 1e-3).all()
tests.append(status)
print("s11", status)

status = (relDiff(s12, s12exact, 1e-9) < 1e-3).all()
tests.append(status)
print("s12", status)

status = (relDiff(s21, s21exact, 1e-9) < 1e-3).all()
tests.append(status)
print("s21", status)

status = (relDiff(s22, s22exact, 1e-9) < 1e-3).all()
tests.append(status)
print("s22", status)

if isTest():
    sys.exit(not all(tests))
else:
    import matplotlib.pyplot as plt 

    fig1 = plt.figure()
    fig1.suptitle('S parameters')
    fig1.set_dpi(100)
    fig1.add_subplot(1, 1, 1, projection='polar')
    
    fig1.axes[0].plot(np.angle(s11), np.abs(s11), marker=".", label="s11")
    fig1.axes[0].plot(np.angle(s12), np.abs(s12), marker=".", label="s12")
    fig1.axes[0].plot(np.angle(s21), np.abs(s21), marker=".", label="s21")
    fig1.axes[0].plot(np.angle(s22), np.abs(s22), marker=".", label="s22")
    # Matplotlib forces negative radius lower limit if there is only a single point at the origin. 
    # Fix this. 
    fig1.axes[0].set_rlim(0, 1)  
    fig1.legend(loc="lower right")

    plt.show()
>>>FILE
