Notebook for designing an L-section matching network.
Sat Mar 21 15:50:57 CET 2020
Francesco Urbani https://urbanij.github.io/
This notebooks serves as an implementation of the analytic calculation of an L-section lumped parameters matching network.
See calculation: https://urbanij.github.io/projects/matching_networks/calc.html
See usage demo: https://urbanij.github.io/projects/matching_networks/demo.html
This notebook is part of the syRF project.
https://urbanij.github.io/syRF/#Lumped-parameters
Resources:
Tools:
if $$ R_1\left(R_1-R_2\right) + X_1^2 \geq 0 $$
if $$ R_2\left(R_2-R_1\right) + X_2^2 \geq 0 $$
import sys
print("Python version")
print (sys.version)
# import required libraries
import quantiphy
import numpy as np
print(quantiphy.__version__)
print(np.__version__)
PI = np.pi
class ReactiveComponent:
"""
Reactive component class
=====
Args:
reactance
=====
Examples:
>>> ReactiveComponent(1000, f=1e6)
Inductor:
X = 1 kΩ ⇔ B = -1 mS
L = 159.15 uH (@ 1 MHz)
>>> ReactiveComponent(0, f=12e6)
wire:
X = 0 Ω ⇔ B = -inf
--------------------------------
Z = R + jX (impedance = resistance + j*reactance)
Y = G + jB (admittance = conductance + j*susceptance)
"""
def __init__(self, reactance, f=None):
self._frequency = f
self._reactance = quantiphy.Quantity(str(reactance) + 'Ω')
self._component_type = "L" if reactance > 0 else "C" if reactance < 0 else "wire"
if f != None:
assert f > 0
self._frequency = quantiphy.Quantity(str(f) + 'Hz')
if self._component_type == "L":
inductor_val = (self._reactance/(2*PI*self._frequency)).real
self._component_value = quantiphy.Quantity(str(inductor_val) + 'H')
elif self._component_type == "C":
capacitor_val = (-1/(2*PI*self._frequency*self._reactance)).real
self._component_value = quantiphy.Quantity(str(capacitor_val) + 'F')
else:
self._component_value = 0
else:
self._component_value = None
def __eq__(self, other):
return self._frequency == other._frequency and \
self._reactance == other._reactance
def get_freq(self):
"Return frequency"
return self._frequency
def get_susceptance(self):
"Return equivalent susceptance value"
if self._reactance != 0:
return quantiphy.Quantity(str(-1/self._reactance) + 'S')
else:
return -np.inf
def __repr__(self, a=3):
rv = "Inductor" if self._component_type == "L" else "Capacitor" if self._component_type == "C" else "wire"
rv += ":\n\t"
rv += f"X = {self._reactance} ⇔ B = {self.get_susceptance() }"
rv += f"\n\t\033[1m{self._component_type}\033[0;0m = \033[1m{self._component_value}\033[0;0m (@ {self._frequency})" if self._frequency != None and self._component_type!='wire' else ""
return rv
print(ReactiveComponent(14, f=13e6))
print(ReactiveComponent(-21, f=31e8))
print(ReactiveComponent(0, f=12e6))
class Solution:
def __init__(self, config_type, shunt_elem: ReactiveComponent, series_elem: ReactiveComponent):
assert shunt_elem.get_freq() == series_elem.get_freq()
self._config_type = config_type
self._shunt_elem = shunt_elem
self._series_elem = series_elem
def __eq__(self, other):
return self._config_type == other._config_type and \
self._shunt_elem == other._shunt_elem and \
self._series_elem == other._series_elem
def __repr__(self):
if self._config_type == "shunt-series":
return f"{self._config_type}\n\tShunt {self._shunt_elem}\n\tSeries {self._series_elem}\n"
else:
return f"{self._config_type}\n\tSeries {self._series_elem}\n\tShunt {self._shunt_elem}\n"
# example
Solution("shunt-series", ReactiveComponent(20, 200e5), ReactiveComponent(-24.2, 200e5))
class L_section_matching:
"""
L section matching network
============
args:
input_impedance:
the impedance you want to have.
output_impedance:
the impedance you want to transform the input impedance to.
returns:
a list of solutions (2 to 4 different results depending on
the point on the Smith Chart you start from)
Examples:
>>> mn1 = L_section_matching(input_impedance=102+8j, output_impedance=10, frequency=100e6)
>>> mn1.match()
>>> mn1
From (102+8j) Ω to 10 Ω
#solutions: 2
shunt-series
Shunt Inductor:
X = 34.612 Ω ⇔ B = -28.891 mS
L = 55.087 nH (@ 100 MHz)
Series Capacitor:
X = -30.435 Ω ⇔ B = 32.857 mS
C = 52.294 pF (@ 100 MHz)
shunt-series
Shunt Capacitor:
X = -32.873 Ω ⇔ B = 30.42 mS
C = 48.415 pF (@ 100 MHz)
Series Inductor:
X = 30.435 Ω ⇔ B = -32.857 mS
L = 48.438 nH (@ 100 MHz)
"""
def __init__(self, input_impedance, output_impedance, frequency=None):
assert input_impedance.real >= 0 and output_impedance.real >= 0
self._Z1 = input_impedance
self._Z2 = output_impedance
self._normalized_impedance = input_impedance/output_impedance
self._frequency = frequency
self._solutions = []
self._input_reactance = ReactiveComponent(input_impedance.imag, frequency)
def match(self):
R1 = self._Z1.real
X1 = self._Z1.imag
R2 = self._Z2.real
X2 = self._Z2.imag
if R1*(R1 - R2) + X1**2 >= 0:
"""
shunt - series configuration (down coversion)
jXser
+--------+
+-----------+----+ +----+
| | +--------+
| |
+++ +++ 1
Z1 = | | | | jXshu Z2 = R2 + jX2 = jXser + -----------------
R1 + jX1 | | | | 1 1
+++ +++ ----- + --------
| | jXshu R1 + jX1
| |
+-----------+------------------+
"""
Xshu_1 = (R1*X2 + R2*X1 - R1*(X2 - ((R2*(R1**2 - R2*R1 + X1**2))/R1)**(1/2)))/(R1 - R2)
Xser_1 = X2 - ((R2*(R1**2 - R2*R1 + X1**2))/R1)**(1/2)
Xshu_2 = (R1*X2 + R2*X1 - R1*(X2 + ((R2*(R1**2 - R2*R1 + X1**2))/R1)**(1/2)))/(R1 - R2)
Xser_2 = X2 + ((R2*(R1**2 - R2*R1 + X1**2))/R1)**(1/2)
sol1 = Solution(
config_type="shunt-series",
shunt_elem=ReactiveComponent(Xshu_1, f=self._frequency),
series_elem=ReactiveComponent(Xser_1, f=self._frequency)
)
sol2 = Solution(
config_type="shunt-series",
shunt_elem=ReactiveComponent(Xshu_2, f=self._frequency),
series_elem=ReactiveComponent(Xser_2, f=self._frequency)
)
self._solutions.append(sol1)
if sol2 != sol1: # do not duplicate solutions
self._solutions.append(sol2)
if R2*(R2 - R1) + X2**2 >= 0:
""" series - shunt configuration (up conversion)
jXser
+--------+
+-------+ +-----+--------------+
| +--------+ |
| |
+++ +++ 1
Z1 = | | | | jXshu Z2 = R2 + jX2 = -------------------------
R1 + jX1 | | | | 1 1
+++ +++ ---- + ----------------
| | jXshu R1 + jX1 + jXser
| |
+----------------------+---------------+
"""
Xshu_1 = (R1*X2 + (R1*R2*(R2**2 - R1*R2 + X2**2))**(1/2))/(R1 - R2)
Xser_1 = -(R2*X1 - (R1*R2*(R2**2 - R1*R2 + X2**2))**(1/2))/R2
Xshu_2 = (R1*X2 - (R1*R2*(R2**2 - R1*R2 + X2**2))**(1/2))/(R1 - R2)
Xser_2 = -(R2*X1 + (R1*R2*(R2**2 - R1*R2 + X2**2))**(1/2))/R2
sol1 = Solution(
config_type="series-shunt",
shunt_elem=ReactiveComponent(Xshu_1, f=self._frequency),
series_elem=ReactiveComponent(Xser_1, f=self._frequency)
)
sol2 = Solution(
config_type="series-shunt",
shunt_elem=ReactiveComponent(Xshu_2, f=self._frequency),
series_elem=ReactiveComponent(Xser_2, f=self._frequency)
)
self._solutions.append(sol1)
if sol2 != sol1: # do not duplicate solutions
self._solutions.append(sol2)
def get_solutions(self):
rv = ""
for sol in self._solutions:
rv += f"{sol._shunt_elem._component_type}={sol._shunt_elem._component_value}, "
rv += f"{sol._series_elem._component_type}={sol._series_elem._component_value}, "
return rv
def get_input_reactance(self):
return self._input_reactance
def plot(self):
pass
delta_f = 2000
freq = np.linspace(self._frequency-delta_f/2, self._frequency+delta_f/2, 2000)
for solution in self._solutions:
print(solution)
if solution._config_type=="shunt-series":
if solution._shunt_elem._component_type == "L":
if solution._series_elem._component_type == "C":
Z2 = 1/(2*PI*freq*solution._series_elem._component_value.real)
plt.plot(freq, Z2)
def __repr__(self):
rv = f"From {self._Z1} Ω to {self._Z2} Ω\n\n"
#rv += f"normalized output = {self._Z1}Ω/{self._Z2}Ω = {self._normalized_impedance}\n\n"
rv += f"#solutions: {len(self._solutions)}\n\n"
for solution in self._solutions:
rv += f"{solution}"
return rv
mn1 = L_section_matching(input_impedance=102+8j, output_impedance=10, frequency=100e6)
mn1.match()
mn1
mn2 = L_section_matching(input_impedance=90+32j, output_impedance=100, frequency=100e6)
mn2.match()
mn2
mn = L_section_matching(input_impedance=150, output_impedance=100, frequency=100e6)
mn.match()
assert mn.get_solutions() == 'L=337.62 nH, C=22.508 pF, C=7.5026 pF, L=112.54 nH, '
mn
# 150 Ω to 100 Ω ok
mn = L_section_matching(input_impedance=12, output_impedance=430, frequency=1220e6)
mn.match()
assert mn.get_solutions() == 'C=1.7906 pF, L=9.2393 nH, L=9.5045 nH, C=1.842 pF, '
mn
# 12 to 430 ok
mn = L_section_matching(input_impedance=322+39j, output_impedance=10, frequency=100e6)
mn.match()
assert mn.get_solutions() == 'L=94.43 nH, C=28.28 pF, C=28.004 pF, L=89.57 nH, '
mn
# ok 322+39j => 10
mn = L_section_matching(input_impedance=32+39j, output_impedance=10-123j, frequency=300e6)
mn.match()
mn
# nope ? http://leleivre.com/rf_lcmatch.html
mn = L_section_matching(input_impedance=90+30j, output_impedance=100, frequency=100e6)
mn.match()
assert mn.get_solutions() == 'C=5.3052 pF, wire=0, C=5.3052 pF, wire=0, L=477.46 nH, C=26.526 pF, '
mn
mn = L_section_matching(input_impedance=90+312j, output_impedance=100, frequency=100e6)
mn.match()
mn
input_imp = 200 # Ω
output_imp = (0.15-1.5j) # mS
output_imp *= 1e-3 # S
output_imp = 1/output_imp # Ω
f = 200e6 # Hz
mn = L_section_matching(input_impedance=input_imp, output_impedance=output_imp, frequency=f)
mn.match()
mn
The following has nothing to do with the calculation, I just needed to turn back the output into plain text by removing the bold part.
import re
def plain_text(text):
# 7-bit C1 ANSI sequences
ansi_escape = re.compile(r'''
\x1B # ESC
(?: # 7-bit C1 Fe (except CSI)
[@-Z\\-_]
| # or [ for CSI, followed by a control sequence
\[
[0-?]* # Parameter bytes
[ -/]* # Intermediate bytes
[@-~] # Final byte
)
''', re.VERBOSE)
result = ansi_escape.sub('', text)
return result
# testing how to remove the bold ascii styling from
string_with_nonASCII = 'Capacitor:\n\tX = -21 Ω ⇔ B = 47.619 mS\n\t\x1b[1mC\x1b[0;0m = \x1b[1m2.4448 pF\x1b[0;0m (@ 3.1 GHz)'
print(string_with_nonASCII)
print(plain_text(string_with_nonASCII))