├── .gitattributes
├── twoport
├── __init__.py
├── utils.py
├── s2p.py
├── networks.py
├── smithplot.py
└── twoport.py
├── BFR93A.s2p
├── test.py
├── .gitignore
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/twoport/__init__.py:
--------------------------------------------------------------------------------
1 | # This file is part of PyTwoPort.
2 | # Copyright (C) 2014 by Roger
3 | #
4 | # PyTwoPort is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # PyTwoPort is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with pyrlsdr. If not, see .
16 |
17 |
18 | from twoport import *
19 | from s2p import load_snp
20 | from smithplot import SmithChart
21 | import networks
22 | import utils
23 |
24 | __all__ = ['OnePort', 'TwoPort', 'plot_gains', 'load_snp', 'SmithChart', \
25 | 'networks', 'utils', '_11', '_12', '_21', '_22']
--------------------------------------------------------------------------------
/twoport/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | import numpy as np
3 |
4 | def db(val):
5 | return 10*np.log10(abs(val))
6 |
7 | def un_db(val):
8 | return 10**(val/10)
9 |
10 | def to_eng_form(x):
11 | si_prefixes = {9:'P', 9:'G', 6:'M', 3:'k', 0:'',
12 | -3:'m', -6:'u', -9:'n', -12:'p', -15:'f'}
13 |
14 | for e in sorted(si_prefixes, reverse=True):
15 | if x >= 10**e:
16 | return x/10**e, si_prefixes[e]
17 |
18 | return x, ''
19 |
20 | def resolve_reactance(x, f):
21 | w = 2*np.pi*f
22 |
23 | if x >= 0:
24 | unit = 'H'
25 | lc = x/w
26 | else:
27 | unit = 'F'
28 | lc = 1/(w*x)
29 |
30 | return abs(lc), unit
31 |
32 | def resolve_reactance_str(x, f):
33 | value, unit = resolve_reactance(x, f)
34 | value_eng, prefix = to_eng_form(value)
35 |
36 | return '{:>3.2f} {}{}'.format(value_eng, prefix, unit)
37 |
38 | def group_delay(two_port):
39 | f = two_port.f
40 | s21 = two_port.s[_21]
41 |
42 | d = -diff(unwrap(angle(s21)))/diff(f)/(2*pi)
43 |
44 | return r_[d[0], d]
45 |
46 | def main():
47 | print(resolve_reactance_str(125, 100e6))
48 |
49 | if __name__ == '__main__':
50 | main()
51 |
--------------------------------------------------------------------------------
/BFR93A.s2p:
--------------------------------------------------------------------------------
1 | ! Filename: BFR93AB.S2P Version: 2.0
2 | ! Philips part #: BFR93A Date: May 1990
3 | ! Bias condition: Vce=5V, Ic=10mA
4 | !
5 | # MHz S MA R 50
6 | ! Freq S11 S21 S12 S22 !GUM [dB]
7 | 40 .704 -26.0 24.576 161.0 .014 77.0 .945 -13.9 ! 40.5
8 | 100 .617 -58.0 19.893 139.4 .030 64.2 .795 -28.7 ! 32.4
9 | 200 .509 -98.1 13.897 119.3 .044 57.3 .580 -40.4 ! 25.9
10 | 300 .456 -119.1 10.223 107.7 .055 56.0 .462 -44.2 ! 22.2
11 | 400 .433 -133.1 7.994 100.7 .063 57.6 .396 -45.6 ! 19.7
12 | 500 .435 -144.4 6.667 94.6 .072 58.9 .357 -45.6 ! 18.0
13 | 600 .414 -152.5 5.609 90.6 .081 61.1 .333 -45.7 ! 16.3
14 | 700 .394 -156.2 4.879 87.2 .091 62.8 .318 -46.0 ! 15.0
15 | 800 .407 -163.5 4.286 83.8 .100 64.0 .309 -45.7 ! 13.9
16 | 900 .389 -168.2 3.888 80.8 .110 65.1 .303 -46.0 ! 12.9
17 | 1000 .390 -173.0 3.469 77.6 .120 66.0 .294 -46.2 ! 11.9
18 | 1200 .392 179.2 2.959 72.2 .140 66.8 .281 -47.2 ! 10.5
19 | 1400 .402 175.0 2.583 66.9 .160 67.1 .275 -50.0 ! 9.3
20 | 1600 .392 170.1 2.312 63.7 .181 67.5 .275 -51.7 ! 8.3
21 | 1800 .382 161.7 2.053 59.3 .202 67.0 .275 -53.5 ! 7.3
22 | 2000 .405 158.4 1.935 55.6 .223 66.8 .265 -54.8 ! 6.8
23 | 2200 .410 149.9 1.767 52.0 .244 66.5 .243 -57.7 ! 6.0
24 | 2400 .432 150.7 1.656 48.1 .267 65.4 .232 -64.9 ! 5.5
25 | 2600 .431 146.4 1.547 45.2 .287 64.1 .231 -71.0 ! 4.9
26 | 2800 .408 142.6 1.474 41.8 .310 63.5 .233 -74.8 ! 4.4
27 | 3000 .440 136.9 1.399 39.0 .331 61.8 .218 -77.7 ! 4.1
28 |
--------------------------------------------------------------------------------
/twoport/s2p.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | from twoport import *
3 | from numpy import *
4 |
5 | units_multipliers = {'hz':1, 'khz':1e3, 'mhz':1e6, 'ghz':1e9}
6 | format_converters = {'ma': lambda m, a: m*exp(1j*radians(a)),
7 | 'ri': lambda r, i: r + 1j*i,
8 | 'db': lambda db, a: (10**(db/20))*exp(1j*radians(a))}
9 |
10 | def load_snp(file_name):
11 | # default options per file specification
12 | options = ['ghz', 's', 'ma', 'r', '50']
13 |
14 | with open(file_name, mode='r') as fh:
15 | # need to skip comments until we get to the options line
16 | while True:
17 | line = fh.readline()
18 |
19 | if line[0] == '#':
20 | break
21 | elif line[0] != '!':
22 | raise Exception('unknown file format')
23 |
24 | # extract options into variables, using defaults when necessary
25 | options_raw = line.lower().split()[1:]
26 | options[:len(options_raw)] = options_raw
27 | units, type, format, null, z0 = options
28 |
29 | # only S-parameters supported
30 | if type != 's' or format not in format_converters.keys():
31 | raise Exception('unsupported data format')
32 |
33 | # use NumPy to load actual data
34 | data = loadtxt(fh, comments='!', unpack=True)
35 |
36 | # split data into frequency and magnitude/angle pairs
37 | if len(data) == 3: # one-port
38 | f, s11a, s11b = data
39 | s = zeros(len(f), dtype='complex')
40 | elif len(data) == 9: # two-port
41 | f, s11a, s11b, s21a, s21b, s12a, s12b, s22a, s22b = data
42 | s = zeros((len(f), 2, 2), dtype='complex')
43 | else:
44 | raise Exception('unsupported data format')
45 |
46 | # scale frequency units
47 | f *= units_multipliers[units]
48 |
49 | # load into array and create port
50 | if len(data) == 3: # one-port
51 | s[:] = format_converters[format](s11a, s11b)
52 |
53 | return OnePort(s=s, f=f, z0=z0)
54 | else:
55 | s[_11] = format_converters[format](s11a, s11b)
56 | s[_12] = format_converters[format](s12a, s12b)
57 | s[_21] = format_converters[format](s21a, s21b)
58 | s[_22] = format_converters[format](s22a, s22b)
59 |
60 | return TwoPort(s=s, f=f, z0=z0)
61 |
62 | load_s2p = load_snp
63 |
64 | def main():
65 | pass
66 |
67 | if __name__ == '__main__':
68 | main()
69 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | import pylab as pyl
3 | from twoport import *
4 | from twoport.networks import *
5 |
6 |
7 | def basic():
8 | f = linspace(100e6, 1000e6)
9 |
10 | r = Resistor(100)
11 | c = Capacitor(10e-12, f=f)
12 | terminator = Resistor(50)
13 |
14 | filter = Series(c) * Shunt(r)
15 | input_equiv = filter.inp()
16 |
17 | g = filter.transducer_gain(terminator, terminator)
18 |
19 | print filter[200e6]
20 | print 'Zin @ 200 MHz:', input_equiv[200e6].z
21 |
22 | pyl.figure()
23 | pyl.plot(f/1e6, utils.db(g))
24 | pyl.xlabel('Frequency (MHz)')
25 | pyl.ylabel('Gain (dB)')
26 |
27 | pyl.show()
28 |
29 | def amplifier():
30 | file_name = 'BFR93A.s2p'
31 | analysis_freq = 400e6
32 |
33 | ## load a transistor model
34 | q = load_snp(file_name)
35 | f = linspace(q.f[0], q.f[-1], 1000) # new f axis with 1000 points
36 | q = q[f] # interpolate to new f axis
37 |
38 | ## plot input reflection coefficient (S11) on Smith chart
39 | pyl.figure()
40 | sc = SmithChart(show_cursor=False, labels=True)
41 | sc.plot_s_param(q.inp().s)
42 |
43 | # calculate various stability factors
44 | k = q.rollett_k()
45 | kt = q.rollett_kt()
46 | mu = q.mu_stability_source()
47 |
48 | pyl.figure()
49 | pyl.plot(f/1e6, kt, label='EL Tan\'s modified Rollett $k$')
50 | pyl.plot(f/1e6, mu, label='$\mu_i$')
51 | pyl.xlabel('Frequency (MHz)')
52 | pyl.ylabel('Stability factor')
53 | pyl.axhline(1.0, color='black') # line at stability limit
54 | pyl.legend(loc='lower right')
55 |
56 | ## plot gains
57 |
58 | # We can do this manually using:
59 | # terminator = Resistor(50)
60 | # gt = q.transducer_gain(terminator, terminator)
61 | # gmsg = q.max_stable_gain()
62 | # gmax = q.max_gain()
63 | # etc. or just:
64 |
65 | pyl.figure()
66 | plot_gains(q)
67 |
68 | ## plot stability circles
69 |
70 | q_desired = q[analysis_freq]
71 |
72 | pyl.figure()
73 | sc = SmithChart(show_cursor=False, labels=True)
74 | sc.two_port = q_desired # need this for cursors to work
75 |
76 | sc.draw_stability_circles(q_desired, plane='both')
77 | sc.plot_gain_circles(q_desired, surface=True, gain_cursor=True)
78 |
79 | # enable cursor
80 | # note green circle will show optimal termination at other (load) port
81 | sc.enable_cursor()
82 |
83 | pyl.show()
84 |
85 |
86 | def main():
87 | basic()
88 | amplifier()
89 |
90 | if __name__ == '__main__':
91 | main()
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 |
46 | [Dd]ebug/
47 | [Rr]elease/
48 | x64/
49 | build/
50 | [Bb]in/
51 | [Oo]bj/
52 |
53 | # MSTest test Results
54 | [Tt]est[Rr]esult*/
55 | [Bb]uild[Ll]og.*
56 |
57 | *_i.c
58 | *_p.c
59 | *.ilk
60 | *.meta
61 | *.obj
62 | *.pch
63 | *.pdb
64 | *.pgc
65 | *.pgd
66 | *.rsp
67 | *.sbr
68 | *.tlb
69 | *.tli
70 | *.tlh
71 | *.tmp
72 | *.tmp_proj
73 | *.log
74 | *.vspscc
75 | *.vssscc
76 | .builds
77 | *.pidb
78 | *.log
79 | *.scc
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 |
101 | # TeamCity is a build add-in
102 | _TeamCity*
103 |
104 | # DotCover is a Code Coverage Tool
105 | *.dotCover
106 |
107 | # NCrunch
108 | *.ncrunch*
109 | .*crunch*.local.xml
110 |
111 | # Installshield output folder
112 | [Ee]xpress/
113 |
114 | # DocProject is a documentation generator add-in
115 | DocProject/buildhelp/
116 | DocProject/Help/*.HxT
117 | DocProject/Help/*.HxC
118 | DocProject/Help/*.hhc
119 | DocProject/Help/*.hhk
120 | DocProject/Help/*.hhp
121 | DocProject/Help/Html2
122 | DocProject/Help/html
123 |
124 | # Click-Once directory
125 | publish/
126 |
127 | # Publish Web Output
128 | *.Publish.xml
129 | *.pubxml
130 |
131 | # NuGet Packages Directory
132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
133 | #packages/
134 |
135 | # Windows Azure Build Output
136 | csx
137 | *.build.csdef
138 |
139 | # Windows Store app package directory
140 | AppPackages/
141 |
142 | # Others
143 | sql/
144 | *.Cache
145 | ClientBin/
146 | [Ss]tyle[Cc]op.*
147 | ~$*
148 | *~
149 | *.dbmdl
150 | *.[Pp]ublish.xml
151 | *.pfx
152 | *.publishsettings
153 |
154 | # RIA/Silverlight projects
155 | Generated_Code/
156 |
157 | # Backup & report files from converting an old project file to a newer
158 | # Visual Studio version. Backup files are not needed, because we have git ;-)
159 | _UpgradeReport_Files/
160 | Backup*/
161 | UpgradeLog*.XML
162 | UpgradeLog*.htm
163 |
164 | # SQL Server files
165 | App_Data/*.mdf
166 | App_Data/*.ldf
167 |
168 | #############
169 | ## Windows detritus
170 | #############
171 |
172 | # Windows image file caches
173 | Thumbs.db
174 | ehthumbs.db
175 |
176 | # Folder config file
177 | Desktop.ini
178 |
179 | # Recycle Bin used on file shares
180 | $RECYCLE.BIN/
181 |
182 | # Mac crap
183 | .DS_Store
184 |
185 |
186 | #############
187 | ## Python
188 | #############
189 |
190 | *.py[co]
191 |
192 | # Packages
193 | *.egg
194 | *.egg-info
195 | dist/
196 | build/
197 | eggs/
198 | parts/
199 | var/
200 | sdist/
201 | develop-eggs/
202 | .installed.cfg
203 |
204 | # Installer logs
205 | pip-log.txt
206 |
207 | # Unit test / coverage reports
208 | .coverage
209 | .tox
210 |
211 | #Translations
212 | *.mo
213 |
214 | #Mr Developer
215 | .mr.developer.cfg
216 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PyTwoPort
2 | =========
3 |
4 | A Python library for analyzing linear two-port electrical network. Includes useful functionality for aiding in the design of small-signal RF amplifiers (e.g. Smith charts and stability analysis).
5 |
6 | Documentation and comments are a bit sparse, but start by looking at test.py (and below).
7 |
8 | Features
9 | =========
10 |
11 | * Support for one- and two-port networks (N-port functionality can be added at some point).
12 | * Conversion between Z, Y, S, T and ABCD parameters, including spline based interpolation of new frequency points.
13 | * Combine networks in parallel, series or cascade.
14 | * Convert one-port networks to parallel or series two-port networks, and one-port networks to driving point one-port networks.
15 | * Functions for calculating various gains (tranducer gain, maximum stable gain, etc.)
16 | * Functions for computing various stability factors (Rollett's, mu, etc.) as well as the positions of stability circles.
17 | * Ability to import S1P and S2P data files.
18 | * Smith chart plotting, with interactive data cursors.
19 | * Gain and stability circle plotting on Smith charts.
20 |
21 | Dependencies
22 | =========
23 |
24 | * Python 2.7
25 | * NumPy
26 | * SciPy (used just for interpolation)
27 | * Matplotlib (for plotting)
28 |
29 | Usage
30 | =========
31 |
32 | A two-port network can be initialized using `TwoPort(f=f, =)` where `` is one of the supported
33 | network types (`z`, `y`, `s`, etc.), `` is a numpy array and `f` is a numpy array of frequencies (in which case
34 | `` should be 3D with a matrix corresponding to each frequency point). The `load_snp(file_name)` can also be
35 | used to import data from a S1P/S2P file.
36 |
37 | If you want to build a network from scratch using lumped elements, then you can combine inductors, capacitors,
38 | resistors, etc. in series and parallel like this:
39 |
40 | ```python
41 | f = linspace(100e6, 1000e6) # need to define frequency points for some devices
42 |
43 | L = Inductor(100e-9, f=f)
44 | C = Capacitor(100e-12, f=f)
45 | R = Resistor(50)
46 |
47 | network = (L // C).to_series() * R.to_shunt()
48 | ```
49 |
50 | A schematic of this network is shown [here](http://i.imgur.com/IlgTtqF.png).
51 | The `to_series()` function converts a one-port to a series connected two-port (similarly for `to_shunt()`),
52 | `*` combines two one-ports in cascade and `//` two networks in parallel (`+` for series).
53 | Additional elements/networks (transformers, transmission lines, etc.) are defined in networks.py.
54 |
55 | The two-port parameters are now available in any format, e.g.:
56 |
57 | ```python
58 | print network.z[_11] # _11 is a helper so we can avoid indexing with z[:, 0, 0]
59 | print network.s[_21]
60 | print network[500e6].y # returns matrix for f=500 MHz
61 | ```
62 |
63 | If a network is only defined at a few frequency points, PyTwoPort can automatically interpolate new points using:
64 |
65 | ```python
66 | network = network[400e6:450e6:5e6] # gives 5 MHz resolution
67 | ```
68 |
69 | Note that the first and second indices can be omitted. Interpolation is done with SciPy's 1D spline function.
70 |
71 | Two port networks can also be reduced to equivalent input or output (driving point) one-port networks:
72 |
73 | ```python
74 | print network.input_equiv()
75 | print network.output_equiv()
76 | ```
77 |
78 | Here the opposite port remains open circuited.
79 |
80 | Other things you can do with a two-port network include calculating gains, stability factors, etc.
81 |
82 | ```python
83 | terminator = Resistor(150) # this is our source/load termination
84 |
85 | print network.transducer_gain(terminator, terminator) # gain with this termination
86 | print network.s[_21] # this would be the gain into 50 Ohm loads
87 | print network.rollett_k() # stability factor (< 1.0 implies potential instability)
88 | ```
89 |
90 | Sample plots
91 | =========
92 |
93 | A few sample plots (see test.py for the code):
94 |
95 | * Instability regions for source and load termination for an active two-port network.
96 | 
97 | * Instability regions for different frequency, and a minimum gain circle.
98 | 
99 | * Gain surface and instability regions (red -> unstable).
100 | 
101 | * Gain surface and instability regions (green -> stable).
102 | 
103 | * Gains vs. frequency
104 | 
105 | * Reflection coefficient on Smith chart
106 | 
107 | * Stability factors vs. frequency
108 | 
109 |
110 | License
111 | =========
112 | All of the code contained here is licensed by the GNU General Public License v3.
113 |
114 | Copyright (C) 2014 by Roger https://github.com/roger-
115 |
--------------------------------------------------------------------------------
/twoport/networks.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | from pylab import *
3 | from twoport import *
4 | from utils import *
5 |
6 |
7 | class Series(TwoPort):
8 | def __init__(self, one_port):
9 | self.z0 = one_port.z0
10 | self.f = one_port.f
11 | self.description = ('Series ' + one_port.description) if one_port.description else None
12 | self.components = [one_port]
13 |
14 | if 0:
15 | z = one_port.z
16 | s = zeros((len(asarray(z).reshape(-1)), 2, 2), dtype='complex')
17 | s[_11] = s[_22] = z/(z + 2*self.z0)
18 | s[_12] = s[_21] = 2*self.z0/(z + 2*self.z0)
19 |
20 | s = one_port.s
21 | s_new = zeros((len(asarray(s).reshape(-1)), 2, 2), dtype='complex')
22 | s_new[_11] = s_new[_22] = (1 + s)/(3 - s)
23 | s_new[_12] = s_new[_21] = 2*(1 - s)/(3 - s)
24 |
25 | TwoPort.__init__(self, s=s_new)
26 |
27 | class Shunt(TwoPort):
28 | def __init__(self, one_port):
29 | self.z0 = one_port.z0
30 | self.f = one_port.f
31 | self.description = ('Shunt ' + one_port.description) if one_port.description else None
32 | self.components = [one_port]
33 |
34 | if 0:
35 | y = one_port.y
36 | s = zeros((len(asarray(y).reshape(-1)), 2, 2), dtype='complex')
37 | s[_11] = s[_22] = -y/(y + 2/self.z0)
38 | s[_12] = s[_21] = (2/self.z0)/(y + 2/self.z0)
39 |
40 | s = one_port.s
41 | s_new = zeros((len(asarray(s).reshape(-1)), 2, 2), dtype='complex')
42 | s_new[_11] = s_new[_22] = (s - 1)/(3 + s)
43 | s_new[_12] = s_new[_21] = 2*(s + 1)/(s + 3)
44 |
45 | TwoPort.__init__(self, s=s_new)
46 |
47 | class Capacitor(OnePort):
48 | def __init__(self, val, f=None):
49 | if f is not None:
50 | self.f = f
51 |
52 | y = 1j*2*pi*self.f*val
53 | self.description = '{:>3.2f} {}F capacitor'.format(*to_eng_form(val))
54 |
55 | OnePort.__init__(self, y=y)
56 |
57 | class Inductor(OnePort):
58 | def __init__(self, val, f=None, q=inf):
59 | if f is not None:
60 | self.f = f
61 |
62 | z = (1j + 1/q)*2*pi*self.f*val
63 | self.description = '{:>3.2f} {}H inductor'.format(*to_eng_form(val))
64 |
65 | OnePort.__init__(self, z=z)
66 |
67 | class Resistor(OnePort):
68 | def __init__(self, val, f=None):
69 | if f is not None:
70 | self.f = f
71 |
72 | # FIXME: work for multivalued R's
73 | if 0:
74 | self.description = '{:>3.2f} {}ohm resistor'.format(*to_eng_form(val))
75 |
76 | OnePort.__init__(self, r=val)
77 |
78 | class Terminator(OnePort):
79 | def __init__(self):
80 | self.description = 'Terminator'
81 |
82 | OnePort.__init__(self, s=0)
83 |
84 | class ImpedanceInverter(TwoPort):
85 | def __init__(self, gain=1):
86 | abcd = zeros((1, 2, 2), dtype='complex')
87 | abcd[_12] = 1j*gain
88 | abcd[_21] = 1j/gain
89 |
90 | self.description = 'Impedance inverter'
91 |
92 | TwoPort.__init__(self, abcd=abcd)
93 |
94 | class Transformer(TwoPort):
95 | def __init__(self, n_pri=1, n_sec=1):
96 | s = zeros((1, 2, 2), dtype='complex')
97 | n = n_pri/n_sec
98 |
99 | s[_11] = n**2 - 1
100 | s[_22] = -s[_11]
101 | s[_21] = s[_12] = 2*n
102 | s /= (n**2 + 1)
103 |
104 | self.description = '{}:{} transformer'.format(n_pri, n_sec)
105 |
106 | TwoPort.__init__(self, s=s)
107 |
108 | class PhaseShifter(TwoPort):
109 | def __init__(self, phase_rad, f=TwoPort.f):
110 | s = zeros((len(phase_rad), 2, 2), dtype='complex')
111 | s[_11] = s[_22] = 0
112 | s[_12] = s[_21] = exp(1j*phase_rad)
113 |
114 | TwoPort.__init__(self, s=s, f=f)
115 |
116 | class TransmissionLine(TwoPort):
117 | '''
118 | Loss-less transmission line
119 | '''
120 | def __init__(self, length, vf=1, z0=TwoPort.z0, f=TwoPort.f):
121 | c = 2.99792458e8
122 | beta_l = length*2*pi*f/(c*vf)
123 |
124 | abcd = zeros((len(beta_l), 2, 2), dtype='complex')
125 | abcd[_11] = abcd[_22] = cos(beta_l)
126 | abcd[_12] = 1j*z0*sin(beta_l)
127 | abcd[_21] = 1j*sin(beta_l)/z0
128 |
129 | self.description = '{} m transmission line'.format(length)
130 |
131 | TwoPort.__init__(self, abcd=abcd, f=f)
132 |
133 | class Amplifier(TwoPort):
134 | def __init__(self, gain_db, f=TwoPort.f):
135 | s = zeros((len(f), 2, 2), dtype='complex')
136 | s[_21] = un_db(gain_db)
137 | self.description = '{} dB amplifier'.format(gain_db)
138 |
139 | TwoPort.__init__(self, s=s, f=f)
140 |
141 | def pi_attenuator(atten_db, z=50):
142 | k = 10**(atten_db/10)
143 |
144 | rp = z*(sqrt(k) + 1)/(sqrt(k) - 1)
145 | rs = z*(k - 1)/(2*sqrt(k))
146 |
147 | return Shunt(Resistor(rp))*Series(Resistor(rs))*Shunt(Resistor(rp))
148 |
149 | def main1():
150 | f = linspace(1e6, 2e9, 2000)
151 |
152 | load = OnePort(z=75/2)
153 | source = OnePort(z=50)
154 |
155 | connector = TransmissionLine(1/100, 0.66, z0=75, f=f)
156 | cable = TransmissionLine(0.5, 0.66, z0=50, f=f)
157 |
158 | net = cable*connector
159 |
160 | plot(f/1e6, db(net.g_t(source, load)))
161 |
162 | net = cable
163 |
164 | plot(f/1e6, db(net.g_t(source, load)))
165 |
166 | ylabel('Gain (dB)')
167 | ylabel('Frequency (MHz)')
168 |
169 | show()
170 |
171 | def main2():
172 | f = linspace(1e6, 1e9, 1000)
173 |
174 | tl = TransmissionLine(10, 0.66, z0=50, f=f)*\
175 | TransmissionLine(0.1, 0.54, z0=40, f=f)
176 | n = tl*Resistor(50)
177 |
178 |
179 | figure()
180 | plot(f, abs(n.inp().z))
181 |
182 | figure()
183 | plot(f/1e6, db(tl.s[_21]))
184 |
185 | show()
186 |
187 | if __name__ == '__main__':
188 | main1()
--------------------------------------------------------------------------------
/twoport/smithplot.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | from numpy import asarray, array, dot, zeros, inf, identity
3 | from numpy.linalg import inv
4 | import numpy as np
5 | from pylab import *
6 | from twoport import *
7 | from networks import *
8 | from utils import *
9 | from matplotlib.patches import Circle, FancyArrowPatch # for drawing smith chart
10 | from matplotlib.lines import Line2D # for drawing smith chart
11 | from matplotlib.text import Text
12 | from itertools import chain
13 |
14 | # TODO:
15 | # * Label points by freq
16 | # * Label source/load cursors
17 | # clip to rect
18 |
19 | class SmithChart(object):
20 | z0 = 50
21 | current_plane = None
22 | two_port = None
23 | show_matched_termination_cursor = True
24 | gain_cursor = False
25 |
26 | def __init__(self, ax=None, show_cursor=True, admittance=True, labels=False):
27 | self.ax = ax if ax else gca()
28 | self.fig = self.ax.figure
29 |
30 | self.draw_smith_chart(admittance, labels)
31 |
32 | if show_cursor:
33 | self.enable_cursor()
34 |
35 | def _impedance_circle_coords(self, intercept_x_coords, intercept_angles):
36 | # find coords for constant R circles
37 | rs = 2/(1 - asarray(intercept_x_coords)) - 1 # convert to desired resistances
38 | r_circle_coords = [((r/(r+1), 0), 1/(r + 1)) for r in rs]
39 |
40 | # find coords for constant X circles
41 | intercept_angles = np.radians(intercept_angles)
42 | xs = np.sin(intercept_angles)/(1 - np.cos(intercept_angles)) # convert to desired reactances
43 | x_circle_coords = [((1, 1/x), abs(1/x)) for x in xs]
44 |
45 | return r_circle_coords, x_circle_coords, rs, xs
46 |
47 | def draw_impedance_circles(self, intercept_x_coords, intercept_angles, labels=False):
48 | r_circle_coords, x_circle_coords, rs, xs = self._impedance_circle_coords(intercept_x_coords, intercept_angles)
49 |
50 | for center, radius in chain(r_circle_coords, x_circle_coords):
51 | c = Circle(center, radius, **self.patch_options_dark)
52 |
53 | c.set_clip_path(self.smith_circle)
54 | c.set_clip_box(self.ax.bbox)
55 | self.ax.add_patch(c)
56 |
57 | if labels:
58 | for x, r in zip(intercept_x_coords, rs):
59 | self.ax.text(x + 0.04, 0.03, '%.0f' % round(self.z0*r), **self.label_options)
60 |
61 | for a, x in zip(intercept_angles, xs):
62 | r = (a - 90) if x > 0 else (a + 90)
63 | a = np.radians(a)
64 | d = 1.04
65 |
66 | self.ax.text(d*cos(a), d*sin(a), '%.0fj' % round(self.z0*x), rotation=r, **self.label_options)
67 |
68 | def draw_admittance_circles(self, intercept_x_coords, intercept_angles, labels=False):
69 | r_circle_coords, x_circle_coords, rs, xs = self._impedance_circle_coords(intercept_x_coords, intercept_angles)
70 |
71 | # admittance circles have same coords as impedance ones, except flipped
72 | # on the y-axis
73 | for (x, y), radius in chain(r_circle_coords, x_circle_coords):
74 | c = Circle((-x, -y), radius, **self.patch_options_light)
75 |
76 | c.set_clip_path(self.smith_circle)
77 | c.set_clip_box(self.ax.bbox)
78 | self.ax.add_patch(c)
79 |
80 | if labels:
81 | for x, r in zip(intercept_x_coords, rs):
82 | self.ax.text(-x, 0, '%.1f' % (1/(50*r)), **self.label_options)
83 |
84 | for a, x in zip(intercept_angles, xs):
85 | r = (a - 90) if x < 0 else (a + 90)
86 | a = np.radians(a)
87 |
88 | self.ax.text(cos(pi - a), sin(pi - a), '%.1f' % (1/(50*x)), rotation=r, **self.label_options)
89 |
90 | def draw_vswr_circles(self, vswr_radii, labels=False):
91 | for r in vswr_radii:
92 | c = Circle((0, 0), r, ls='dashed', **self.patch_options_light)
93 |
94 | c.set_clip_path(self.smith_circle)
95 | c.set_clip_box(self.ax.bbox)
96 | self.ax.add_patch(c)
97 |
98 | if labels:
99 | for r in vswr_radii:
100 | if r > 0:
101 | vswr = (1 + r)/(1 - r)
102 | self.ax.text(0, r, '%.1f' % vswr, **self.label_options)
103 |
104 | def draw_chart_axes(self):
105 | # make outer circle
106 | self.smith_circle = Circle((0, 0), 1, transform=self.ax.transData, fc='none',
107 | **self.patch_options_axis)
108 | self.ax.add_patch(self.smith_circle)
109 |
110 | # make constant r=1 circle
111 | z0_circle = Circle((0.5, 0), 0.5, transform=self.ax.transData, fc='none',
112 | **self.patch_options_axis)
113 | z0_circle.set_clip_path(self.smith_circle)
114 | z0_circle.set_clip_box(self.ax.bbox)
115 | self.ax.add_patch(z0_circle)
116 |
117 | # make x-axis
118 | line = Line2D([-1,1],[0,0], **self.patch_options_axis)
119 | line.set_clip_path(self.smith_circle)
120 | line.set_clip_box(self.ax.bbox)
121 | self.ax.add_line(line)
122 |
123 | def draw_smith_chart(self, admittance, labels):
124 | # plot options for constant z/y circles and axes
125 | self.patch_options_light = {'fc':'none', 'color':'#474959', 'alpha':0.2, 'lw':1}
126 | self.patch_options_dark = {'fc':'none', 'color':'#474959', 'alpha':0.5, 'lw':1}
127 | self.patch_options_axis = {'color':'black', 'alpha':0.8, 'lw':1.5}
128 |
129 | # options for z/y circle labels
130 | self.label_options = {'ha':'center', 'va':'center', 'size':'9', 'alpha':0.5}#,
131 | #'bbox':dict(fc='white', ec='none', alpha=0.5)}
132 | #self.label_options = {'ha':'center', 'va':'center', 'size':'10', 'alpha':0.5}
133 |
134 | # x-axis coordinates where constant R circles will intersect
135 | intercept_x_coords = arange(-0.75, 1, 0.25)
136 |
137 | # angles where constant X circles will intersect (in degrees relative
138 | # to positive x-axis)
139 | intercept_angles = arange(40, 360, 40)
140 |
141 | # radii for vswr circles
142 | vswr_radii = arange(0, 1, 0.2)
143 |
144 | self.draw_chart_axes()
145 | self.draw_impedance_circles(intercept_x_coords, intercept_angles, labels)
146 | self.draw_admittance_circles(intercept_x_coords, intercept_angles, labels=0)
147 | self.draw_vswr_circles(vswr_radii, labels)
148 |
149 | self.ax.grid(0)
150 | self.ax.axis('equal')
151 | self.ax.axis(np.array([-1.1, 1.1, -1.1, 1.1]))
152 |
153 | self.save_background()
154 |
155 | def enable_cursor(self):
156 | self.cid_on_mouse_move = self.fig.canvas.mpl_connect('motion_notify_event', self._on_mouse_move)
157 | self.fig.canvas.mpl_connect('button_press_event', self._on_mouse_press)
158 | #self.cid_on_mouse_leave = self.fig.canvas.mpl_connect('axes_leave_event', self._on_mouse_leave)
159 | #self.cid_resize = self.fig.canvas.mpl_connect('resize_event', self._resize)
160 | self.cid_draw = self.fig.canvas.mpl_connect('draw_event', self._draw)
161 |
162 | self.cursor_text = Text(0.7, 0.1, 'n/a',
163 | horizontalalignment='left',
164 | verticalalignment='center',
165 | #bbox=dict(facecolor='white', alpha=1, ec='gray', pad=10),
166 | color='white',
167 | fontsize='small',
168 | alpha=0.8,
169 | backgroundcolor='gray',
170 | family='monospace',
171 | clip_box=self.ax.bbox,
172 | animated=True,
173 | transform = self.ax.transAxes)
174 | self.ax.add_artist(self.cursor_text)
175 |
176 | self.circle_matched_term = Circle((100,100), 0.02, alpha=0.5, color='green', animated=True)
177 | self.ax.add_patch(self.circle_matched_term)
178 |
179 | def plot_circle(self, center, radius, text='', text_color='black', circle_color='red',\
180 | filled=False, circle_alpha=0.2, linestyle='solid', hatch=None):
181 | text_alpha = 0.7
182 |
183 | text_x_offset = 0.15
184 | text_y_offset = 0.1
185 |
186 | # calculate position for text
187 | a, b = center
188 |
189 | # find closet point to edge of circle
190 | x = a*(1 - radius/sqrt(a**2 + b**2))
191 | y = b*(1 - radius/sqrt(a**2 + b**2))
192 | dist_to_circle = sqrt(x**2 + y**2)
193 |
194 | #print 'dist to sphere: ', sqrt(x**2 + y**2)
195 |
196 | if (x**2 + y**2) == 1:
197 | text_x_offset *= -1
198 | text_y_offset *= -1
199 |
200 | #textpos = ((a - x)/radius + text_x_offset, (b - y)/radius + text_y_offset)
201 | textpos = (x/dist_to_circle + text_x_offset, y/dist_to_circle + text_y_offset)
202 | else:
203 | textpos = (x + text_x_offset, y + text_y_offset)
204 |
205 | #print 'textpos: ', textpos
206 |
207 | # make actual circle
208 | c = Circle(center, radius, fc=circle_color, ec='white', alpha=circle_alpha, fill=filled, linestyle=linestyle, hatch=hatch)
209 | self.ax.add_patch(c)
210 |
211 | self.ax.annotate(text, (x, y), color=text_color,\
212 | fontsize='small', alpha=text_alpha, xycoords='data', xytext=textpos,\
213 | arrowprops=dict(arrowstyle="->", alpha=text_alpha,\
214 | connectionstyle="arc3,rad=0.17"))
215 |
216 | def draw_stability_circles(self, two_port, plane='both', label=None):
217 | filled = True
218 |
219 | if plane == 'source' or plane == 'both':
220 | for i, (center, radius, circle_stable) in enumerate(zip(*two_port.source_stability_circle())):
221 | if circle_stable:
222 | hatch = None
223 | color = 'green'
224 | else:
225 | hatch = '/'
226 | color = 'red'
227 |
228 | if label is None:
229 | label_actual = '\n(%0.2f MHz)' % (two_port.f[i]/1e6) if two_port.f is not None else ''
230 | label_actual = 'Source' + label_actual
231 | else:
232 | label_actual = label
233 |
234 | self.plot_circle((center.real, center.imag), radius, text=label_actual, filled=filled,\
235 | text_color='black', circle_color=color, hatch=hatch)
236 |
237 | if plane == 'load' or plane == 'both':
238 | for i, (center, radius, circle_stable) in enumerate(zip(*two_port.load_stability_circle())):
239 | if circle_stable:
240 | hatch = None
241 | color = 'green'
242 | else:
243 | hatch = '/'
244 | color = 'red'
245 |
246 | if label is None:
247 | label_actual = '\n(%0.2f MHz)' % (two_port.f[i]/1e6) if two_port.f is not None else ''
248 | label_actual = 'Load' + label_actual
249 | else:
250 | label_actual = label
251 |
252 | self.plot_circle((center.real, center.imag), radius, text=label_actual, filled=filled,\
253 | text_color='black', circle_color=color, hatch=hatch)
254 |
255 | self.save_background()
256 |
257 | def plot_gain_circles(self, two_port, gains_db=None, plane='source', \
258 | gain_cursor=True, surface=False):
259 | self.two_port = two_port
260 | self.gain_cursor = gain_cursor
261 | self.current_plane = plane
262 |
263 | max_gain = self.two_port.g_max()
264 |
265 | if (self.two_port.kt() < 1).any():
266 | max_gain = self.two_port.max_double_sided_mismatched_gain()
267 |
268 | if gains_db == None:
269 | gains_db = np.trunc(10*db(max_gain)[0]*linspace(0.5, 1, 4))/10
270 | if surface:
271 | filled = False
272 | circle_alpha = 0.7
273 | linestyle = 'dashed'
274 | else:
275 | filled = True
276 | circle_alpha = 0.2
277 | linestyle = 'solid'
278 |
279 | for g in gains_db:
280 | if plane == 'source':
281 | center, radius = self.two_port.available_gain_circle(un_db(g))
282 | self.ax.set_title('Source plane')
283 | elif plane == 'load':
284 | center, radius = self.two_port.operating_gain_circle(un_db(g))
285 | self.ax.set_title('Load plane')
286 |
287 | text = str(g) + ' dB'
288 |
289 | self.plot_circle((center[0].real, center[0].imag), radius[0], text=text, filled=filled,\
290 | text_color='black', circle_color='orange', circle_alpha=circle_alpha,\
291 | linestyle=linestyle)
292 |
293 | if not surface:
294 | self.save_background()
295 |
296 | return
297 |
298 | num_segments = 1000
299 |
300 | i, r = meshgrid(linspace(-1.1, 1.1, num_segments), linspace(-1.1, 1.1, num_segments))
301 | gamma = i + 1j*r
302 | term = OnePort(s=gamma.reshape(-1))
303 |
304 | if plane == 'source':
305 | g = self.two_port.g_a(term)
306 | elif plane == 'load':
307 | g = self.two_port.g_p(term)
308 |
309 | g = db(g).reshape(num_segments, num_segments)
310 | g[abs(gamma) > 1] = nan
311 |
312 | im = self.ax.imshow(g, origin='lower', extent=(-1.1, 1.1, -1.1, 1.1), interpolation='bicubic', alpha=0.9)
313 | im.set_clim(gains_db[0], db(max_gain))
314 |
315 | self.save_background()
316 |
317 | def _on_mouse_press(self, event):
318 | if event.button == 3:
319 | print 'clearing'
320 | self.fig.canvas.restore_region(self.background, bbox=self.ax.bbox)
321 |
322 | def _on_mouse_move(self, event):
323 | if event.xdata is None or event.ydata is None:
324 | return
325 |
326 | gamma = event.xdata + 1j*event.ydata
327 | cursor_port = OnePort(s=gamma)
328 |
329 | self.fig.canvas.restore_region(self.background, bbox=self.ax.bbox)
330 |
331 | self.update_gain_cursor(cursor_port)
332 | self.update_info_box(cursor_port)
333 |
334 | #self.fig.canvas.draw()
335 | self.fig.canvas.blit(self.ax.bbox)
336 |
337 | def update_gain_cursor(self, cursor_port):
338 | if not self.gain_cursor or self.two_port is None:
339 | return
340 |
341 | if self.current_plane == 'source':
342 | term_matched = (cursor_port*self.two_port).out().conj()
343 | term_resulting = (self.two_port*term_matched).inp()
344 | else:
345 | term_matched = (self.two_port*cursor_port).inp().conj()
346 | term_resulting = (term_matched*self.two_port).out()
347 |
348 | self.circle_matched_term.center = real(term_matched.s), imag(term_matched.s)
349 |
350 | if abs(term_matched.s) > 1 or abs(term_resulting.s) > 1:
351 | self.circle_matched_term.set_color('r')
352 | else:
353 | self.circle_matched_term.set_color('g')
354 |
355 | self.ax.draw_artist(self.circle_matched_term)
356 |
357 | def update_info_box(self, cursor_port):
358 | z = cursor_port.z
359 | y = cursor_port.y
360 |
361 | data = '$\\Gamma$ = {:>7.2f} $\\angle$ {:>+8.3f}$^{{\\circ}}$'.format(float(abs(cursor_port.s)), float(degrees(angle(cursor_port.s))))
362 | data += '\nVSWR = {:>7.2f}'.format(float(cursor_port.vswr()))
363 | data += '\nMM = {:>7.2f} dB'.format(float(db(cursor_port.mismatch())))
364 | data += '\nZ = {:>7.2f}{:>+8.2f}j '.format(float(real(z)), float(imag(z)))
365 |
366 | if self.two_port is not None:
367 | if self.current_plane == 'source':
368 | term_matched = (cursor_port*self.two_port).out().conj()
369 | gain = self.two_port.g_t(cursor_port, term_matched)
370 | else:
371 | term_matched = (self.two_port*cursor_port).inp().conj()
372 | gain = self.two_port.g_t(term_matched, cursor_port)
373 |
374 | x = float(imag(z))
375 | w = 2*pi*self.two_port.f[0]
376 |
377 | if x > 0:
378 | hf = 'H'
379 | lc = x/w
380 | else:
381 | hf = 'F'
382 | lc = 1/(w*x)
383 |
384 | val, prefix = to_eng_form(abs(lc))
385 | data += '({:>3.2f} {}{}) '.format(val, prefix, hf)
386 | data += '$\\Omega$'
387 |
388 | data += '\n = {:>7.2f} // {:>+8.2f}j '.format(float(1/real(y)), float(-1/imag(y)))
389 | if self.two_port is not None:
390 | x = float(-1/imag(y))
391 | w = 2*pi*self.two_port.f[0]
392 | if x > 0:
393 | hf = 'H'
394 | lc = x/w
395 | else:
396 | hf = 'F'
397 | lc = 1/(w*x)
398 |
399 | val, prefix = to_eng_form(abs(lc))
400 | data += '({:>3.2f} {}{}) '.format(val, prefix, hf)
401 | data += '$\\Omega$'
402 |
403 | if self.gain_cursor and self.two_port is not None:
404 | gain_str = '{:>7.2f} dB'.format(float(db(gain))) if gain > 0 else 'n/a'
405 | data += '\nG = ' + gain_str
406 |
407 | self.cursor_text.set_text(data)
408 | self.ax.draw_artist(self.cursor_text)
409 |
410 | def plot_s_param(self, s, color='blue'):
411 | self.plot_line(s, color)
412 |
413 | self.save_background()
414 |
415 | def plot_line(self, points, color='blue'):
416 | r, t = abs(points), angle(points)
417 |
418 | l = Line2D(r*cos(t), r*sin(t), color=color)
419 | self.ax.add_line(l)
420 |
421 | def save_background(self):
422 | self.fig.canvas.draw()
423 | self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox)
424 |
425 | def _on_mouse_leave(self, event):
426 | pass
427 |
428 | def _resize(self, event):
429 | self.save_background()
430 | print 'resize\n'
431 | #self.ax.draw(None)
432 | #self.fig.canvas.draw()
433 | #self.fig.canvas.restore_region(self.background, bbox=self.ax.bbox)
434 |
435 | def _draw(self, event):
436 | self.fig.canvas.mpl_disconnect(self.cid_draw)
437 | #self.fig.canvas.restore_region(self.background, bbox=self.ax.bbox)
438 | self.save_background()
439 | self.cid_draw = self.fig.canvas.mpl_connect('draw_event', self._draw)
440 |
441 |
442 | # TODO: fix cursor for s param plot
443 |
444 | def main():
445 | f = linspace(1e6, 2e9, 2000)
446 |
447 | load = OnePort(z=75/2)
448 | source = OnePort(z=50)
449 |
450 | figure()
451 |
452 |
453 | connector = TransmissionLine(2/100, 0.66, z0=75, f=f)
454 | cable = TransmissionLine(1, 0.66, z0=50, f=f)
455 |
456 | net = cable
457 |
458 | plot(f/1e6, db(net.g_t(source, load)), label='no adapter')
459 |
460 | net = cable*connector
461 |
462 | plot(f/1e6, db(net.g_t(source, load)), label='w/ 2 cm 75 $\Omega$ adapter')
463 |
464 |
465 |
466 | connector = TransmissionLine(15/100, 0.66, z0=75, f=f)
467 |
468 | net = cable*connector
469 |
470 | plot(f/1e6, db(net.g_t(source, load)), label='w/ 15 cm 75 $\Omega$ pigtail')
471 |
472 |
473 | ylabel('Gain (dB)')
474 | xlabel('Frequency (MHz)')
475 | legend(loc=3)
476 |
477 |
478 | figure()
479 | sc = SmithChart(labels=1, show_cursor=0)
480 | sc.plot_s_param((net*load).inp().s)
481 |
482 | show()
483 |
484 |
485 | if __name__ == '__main__':
486 | main()
487 |
--------------------------------------------------------------------------------
/twoport/twoport.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | import numpy as np
3 | from numpy import asarray, ndarray, pi, array, dot, zeros, inf, identity, real, imag, sign, abs
4 | from numpy.linalg import inv
5 | #from pylab import *
6 | import pylab as mpl
7 | from scipy import interpolate
8 | from utils import *
9 |
10 | __all__ = ['OnePort', 'TwoPort',
11 | '_11', '_12', '_21', '_22', 'plot_gains']
12 |
13 | # 2x2 identity matrix (for convenience)
14 | I = identity(2, dtype='complex')
15 |
16 | # few helper variables to simplify access to the two-port arrays. Now we can use
17 | # y[_11] instead of y[:, 0, 0], for example
18 | _11 = slice(None, None, None), 0, 0
19 | _12 = slice(None, None, None), 0, 1
20 | _21 = slice(None, None, None), 1, 0
21 | _22 = slice(None, None, None), 1, 1
22 |
23 | def pick_f(port1, port2):
24 | if port1.f is None:
25 | return port2.f
26 | elif port2.f is None:
27 | return port1.f
28 | elif (port1.f == port2.f).all():
29 | return port1.f
30 | else:
31 | raise Exception('frequency axes incompatible')
32 |
33 | def plot_gains(two_port):
34 | mpl.plot(two_port.f/1e6, db(two_port.g_msg()), label='$MSG$')
35 | mpl.plot(two_port.f/1e6, db(two_port.g_max()), label='$G_\mathrm{max}$')
36 | mpl.plot(two_port.f/1e6, db(abs(two_port.s[_21])**2), label='$S_{21}$')
37 |
38 | mpl.legend()
39 | mpl.xlabel('Frequency (MHz)')
40 | mpl.ylabel('Gain (dB)')
41 | mpl.grid(which='both')
42 |
43 | class NPort(object):
44 | z0 = 50
45 | f = None
46 | description = None
47 | components = []
48 |
49 | def series(self, one_port):
50 | raise NotImplementedError
51 |
52 | def parallel(self, one_port):
53 | raise NotImplementedError
54 |
55 | def input_equiv(self):
56 | raise NotImplementedError
57 |
58 | def output_equiv(self):
59 | raise NotImplementedError
60 |
61 | def interpolate(self, new_points):
62 | raise NotImplementedError
63 |
64 | def __getitem__(self, new_points):
65 | # if network is frequency independent, ignore indexing
66 | if self.f is None:
67 | return self
68 |
69 | if isinstance(new_points, slice):
70 | start = new_points.start
71 | stop = new_points.stop
72 | step = new_points.step
73 |
74 | if not start:
75 | start = self.f[0]
76 | if not stop:
77 | stop = self.f[-1]
78 | if not step:
79 | step = self.f[1] - self.f[0]
80 |
81 | new_points = np.arange(start, stop, step)
82 | else:
83 | new_points = np.asarray(new_points)
84 | new_points.shape = -1
85 |
86 | # interpolation requires monotonic frequency axis
87 | if (np.diff(self.f) <= 0).any():
88 | raise Exception('frequency axis must be monotonically increasing')
89 |
90 | # make sure we aren't extrapolating
91 | if new_points.min() < self.f.min() or new_points.max() > self.f.max():
92 | raise Exception('cannot extrapolate to specified frequency range')
93 |
94 | return self.interpolate(new_points)
95 |
96 | class OnePort(NPort):
97 | def __init__(self, f=None, **kwargs):
98 | param_type = list(kwargs.keys())[0].lower()
99 | param_value = list(kwargs.values())[0]
100 | param_value = asarray(param_value, dtype='complex')
101 |
102 | if f is not None:
103 | self.f = f
104 |
105 | if param_type in ('z', 'r'):
106 | self._from_z(param_value)
107 | elif param_type == 'y':
108 | self._from_y(param_value)
109 | elif param_type == 's':
110 | self._from_s(param_value)
111 | else:
112 | raise Exception('unknown type: ' + param_type)
113 |
114 | '''elif param_type == 'l':
115 | self._from_z(2j*pi*self.f*param_value)
116 | elif param_type == 'c':
117 | self._from_y(2j*pi*self.f*param_value)'''
118 |
119 | def _from_s(self, s):
120 | s = asarray(s, dtype='complex').reshape(-1)
121 | self.s_params = s
122 |
123 | def _from_z(self, z):
124 | z = asarray(z, dtype='complex').reshape(-1)
125 | self.s_params = (z - self.z0)/(z + self.z0)
126 |
127 | def _from_y(self, y):
128 | y = asarray(y, dtype='complex').reshape(-1)
129 | self._from_z(1/y)
130 |
131 | def _to_z(self):
132 | s = self.s_params
133 |
134 | return self.z0*(1 + s)/(1 - s)
135 |
136 | def _to_s(self):
137 | return self.s_params
138 |
139 | def _to_y(self):
140 | return 1/self._to_z()
141 |
142 | def cascade(self, two_port):
143 | if not isinstance(two_port, TwoPort):
144 | raise Exception('can only cascade one-port with two-port network')
145 |
146 | # convert instance to two-port
147 | from networks import Shunt
148 |
149 | return Shunt(self).cascade(two_port)
150 |
151 | def series(self, one_port):
152 | z = self._to_z() + one_port._to_z()
153 | f = pick_f(self, one_port)
154 |
155 | return OnePort(z=z, f=f)
156 |
157 | def parallel(self, one_port):
158 | y = self._to_y() + one_port._to_y()
159 | f = pick_f(self, one_port)
160 |
161 | return OnePort(y=y, f=f)
162 |
163 | def input_equiv(self):
164 | return self
165 |
166 | def output_equiv(self):
167 | return self
168 |
169 | def s_in(self):
170 | return self.s_params
171 |
172 | def s_out(self):
173 | return self.s_params
174 |
175 | def conj(self):
176 | new_port = OnePort(s=self.s_params.conj(), f=self.f)
177 |
178 | return new_port
179 |
180 | def vswr(self):
181 | return (1 + abs(self.s_params))/(1 - abs(self.s_params))
182 |
183 | def interpolate(self, new_points):
184 | new_s = zeros(len(new_points), dtype='complex')
185 | s = self.s_params
186 |
187 | interp_real = interpolate.InterpolatedUnivariateSpline(self.f, s.real)
188 | interp_imag = interpolate.InterpolatedUnivariateSpline(self.f, s.imag)
189 |
190 | new_s.real = interp_real(new_points)
191 | new_s.imag = interp_imag(new_points)
192 |
193 | new_port = OnePort(s=new_s, f=new_points)
194 | new_port.description = self.description
195 | new_port.components = [c.interpolate(new_points) for c in self.components]
196 |
197 | return new_port
198 |
199 | def mismatch(self, load=None):
200 | ss = self.s_out()
201 | sl = 0 if load is None else load.s_in()
202 |
203 | return 1 - abs((sl - ss.conj())/(1 - ss*sl))**2
204 |
205 | def parallel_equiv(self):
206 | return 1/real(self.y), -1j/imag(self.y)
207 |
208 | def to_shunt(self):
209 | '''
210 | Convert one-port to shunt connected two-port
211 | '''
212 | from networks import Shunt
213 |
214 | return Shunt(self)
215 |
216 | def to_series(self):
217 | '''
218 | Convert one-port to series connected two-port
219 | '''
220 | from networks import Series
221 |
222 | return Series(self)
223 |
224 | def __repr__(self):
225 | desc = self.description if self.description else 'One-port network'
226 |
227 | if self.f is not None:
228 | freqs = ': f = {} {}Hz'.format(*to_eng_form(self.f[0]))
229 |
230 | if len(self.f) > 1:
231 | freqs += ' - {} {}Hz'.format(*to_eng_form(self.f[-1]))
232 | else:
233 | freqs = ': '
234 |
235 | if len(self.s) == 1:
236 | z = self.z[0]
237 |
238 | details = 'Z = {0:.2f}+{1:.2f}j'.format(float(z.real), float(z.imag))
239 | if abs(z.imag) > 0:
240 | rp, xp = self.parallel_equiv()
241 | details += ' = {0:.2f}//{1:.2f}j ('.format(float(rp.real), float(xp.imag))
242 | details += resolve_reactance_str(float(xp.imag), self.f[0]) + '), '
243 | else:
244 | details += ', '
245 | details += 'VSWR = {0:.2f}, '.format(float(self.vswr()))
246 | details += 'MM = {0:.2f} dB'.format(float(db(self.mismatch())))
247 | else:
248 | details = ''
249 |
250 | return '<' + desc + freqs + details + '>'
251 |
252 | s = property(_to_s, _from_s)
253 | y = property(_to_y, _from_y)
254 | z = property(_to_z, _from_z)
255 |
256 | inp = input_equiv
257 | out = output_equiv
258 | __mul__ = cascade
259 | __add__ = series
260 | __floordiv__ = parallel
261 |
262 | class TwoPortBase(NPort):
263 | def __init__(self, f=None, **kwargs):
264 | '''for param_type in ('s', 'y', 'z', 't', 'abcd'):
265 | if param_type in kwargs:
266 | from_func = getattr(self, '_from_' + param_type)
267 | from_func(kwargs[param_type])
268 | break
269 | else:
270 | self.s_params = None'''
271 |
272 | if 's' in kwargs:
273 | self.s = kwargs['s']
274 | elif 'y' in kwargs:
275 | self.y = kwargs['y']
276 | elif 'z' in kwargs:
277 | self.z = kwargs['z']
278 | elif 't' in kwargs:
279 | self.t = kwargs['t']
280 | elif 'abcd' in kwargs:
281 | self.abcd = kwargs['abcd']
282 | else:
283 | self.s_params = None
284 |
285 | if f is not None:
286 | self.f = f
287 |
288 | def _from_s(self, s_params):
289 | s_params = asarray(s_params, dtype='complex').reshape(-1, 2, 2)
290 | self.s_params = s_params
291 |
292 | def _from_y(self, y_params):
293 | y_params = asarray(y_params, dtype='complex').reshape(-1, 2, 2)
294 | self.s_params = array([dot((I - self.z0*yi), inv(I + self.z0*yi)) for yi in y_params])
295 |
296 | def _from_z(self, z_params):
297 | z_params = asarray(z_params, dtype='complex').reshape(-1, 2, 2)
298 | self.s_params = array([(zi - self.z0*I).dot(inv(zi + self.z0*I)) for zi in z_params])
299 |
300 | def _from_abcd(self, abcd_params):
301 | abcd_params = asarray(abcd_params, dtype='complex').reshape(-1, 2, 2)
302 | self.s_params = zeros(abcd_params.shape, dtype='complex')
303 |
304 | a = abcd_params[_11]
305 | b = abcd_params[_12]
306 | c = abcd_params[_21]
307 | d = abcd_params[_22]
308 |
309 | self.s_params[_11] = a + b/self.z0 - c*self.z0 - d
310 | self.s_params[_12] = 2*(a*d - b*c)
311 | self.s_params[_21] = 2
312 | self.s_params[_22] = -a + b/self.z0 - c*self.z0 + d
313 |
314 | D = (a + b/self.z0 + c*self.z0 + d).reshape(-1, 1, 1)
315 |
316 | self.s_params /= D
317 |
318 | def _from_t(self, t_params):
319 | t_params = asarray(t_params, dtype='complex').reshape(-1, 2, 2)
320 | self.s_params = zeros(t_params.shape, dtype='complex')
321 |
322 | t11 = t_params[_11]
323 | t12 = t_params[_12]
324 | t21 = t_params[_21]
325 | t22 = t_params[_22]
326 |
327 | self.s_params[_11] = t12/t22
328 | self.s_params[_12] = (t11*t22 - t12*t21)/t22
329 | self.s_params[_21] = 1/t22
330 | self.s_params[_22] = -t21/t22
331 |
332 | def _to_abcd(self):
333 | abcd_params = zeros(self.s_params.shape, dtype='complex')
334 |
335 | s11 = self.s_params[_11]
336 | s12 = self.s_params[_12]
337 | s21 = self.s_params[_21]
338 | s22 = self.s_params[_22]
339 |
340 | abcd_params[_11] = ((1 + s11)*(1 - s22) + s12*s21)/(2*s21)
341 | abcd_params[_12] = ((1 + s11)*(1 + s22) - s12*s21)*self.z0/(2*s21)
342 | abcd_params[_21] = ((1 - s11)*(1 - s22) - s12*s21)/(2*s21*self.z0)
343 | abcd_params[_22] = ((1 - s11)*(1 + s22) + s12*s21)/(2*s21)
344 |
345 | return abcd_params
346 |
347 | def _to_t(self):
348 | t_params = zeros(self.s_params.shape, dtype='complex')
349 |
350 | s11 = self.s_params[_11]
351 | s12 = self.s_params[_12]
352 | s21 = self.s_params[_21]
353 | s22 = self.s_params[_22]
354 | d = self.det_s()
355 |
356 | t_params[_11] = -d/s21
357 | t_params[_12] = s11/s21
358 | t_params[_21] = -s22/s21
359 | t_params[_22] = 1/s21
360 |
361 | return t_params
362 |
363 | def _to_s(self):
364 | return self.s_params
365 |
366 | def _to_z(self):
367 | return array([inv(I - si).dot(I + si) for si in self.s_params.reshape(-1, 2, 2)])*self.z0
368 |
369 | def _to_y(self):
370 | return array([inv(I + si).dot(I - si) for si in self.s_params.reshape(-1, 2, 2)])/self.z0
371 |
372 | def det_s(self):
373 | s = self.s_params
374 |
375 | return s[_11]*s[_22] - s[_12]*s[_21]
376 |
377 | def s_in(self, load=None):
378 | if load is not None:
379 | sl = load.s_in()
380 | else:
381 | sl = 1
382 |
383 | s = self.s
384 |
385 | return s[_11] + s[_12]*s[_21]*sl/(1 - s[_22]*sl)
386 |
387 | def s_out(self, source=None):
388 | if source is not None:
389 | ss = source.s_out()
390 | else:
391 | ss = 1
392 |
393 | s = self.s
394 |
395 | return s[_22] + s[_12]*s[_21]*ss/(1 - s[_11]*ss)
396 |
397 | def flip_ports(self):
398 | s = self.s_params
399 | s_new = zeros(s.shape, dtype='complex')
400 |
401 | s_new[_11] = s[_22]
402 | s_new[_12] = s[_21]
403 | s_new[_21] = s[_12]
404 | s_new[_22] = s[_11]
405 |
406 | return TwoPort(s=s_new, f=self.f)
407 |
408 | def input_equiv(self):
409 | s_in = self.s_in()
410 |
411 | return OnePort(s=s_in, f=self.f)
412 |
413 | def output_equiv(self):
414 | s_out = self.s_out()
415 |
416 | return OnePort(s=s_out, f=self.f)
417 |
418 | def interpolate(self, new_points):
419 | new_s = zeros((len(new_points), 2, 2), dtype='complex')
420 | s = self.s_params
421 |
422 | for param in (_11, _12, _21, _22):
423 | interp_real = interpolate.InterpolatedUnivariateSpline(self.f, s[param].real)
424 | interp_imag = interpolate.InterpolatedUnivariateSpline(self.f, s[param].imag)
425 |
426 | new_s[param].real = interp_real(new_points)
427 | new_s[param].imag = interp_imag(new_points)
428 |
429 | new_port = TwoPort(s=new_s, f=new_points)
430 | new_port.description = self.description
431 | new_port.components = [c[new_points] for c in self.components]
432 |
433 | return new_port
434 |
435 | def cascade(self, two_port):
436 | if isinstance(two_port, OnePort):
437 | from networks import Shunt
438 |
439 | return self.cascade(Shunt(two_port))
440 |
441 | # handle case where one two port is defined at a single point
442 | if len(self.s) == 1:
443 | tp1 = self.t[0]
444 | t = array([dot(tp1, tp2) for tp2 in two_port.t])
445 | elif len(two_port.s) == 1:
446 | tp2 = two_port.t[0]
447 | t = array([dot(tp1, tp2) for tp1 in self.t])
448 | else:
449 | t = array([dot(tp1, tp2) for (tp1, tp2) in zip(self.t, two_port.t)])
450 |
451 | f = pick_f(self, two_port)
452 |
453 | return Cascade(self, two_port, t=t, f=f)
454 |
455 | def series(self, two_port):
456 | z = self._to_z() + two_port._to_z()
457 | f = pick_f(self, two_port)
458 |
459 | return TwoPort(z=z, f=f)
460 |
461 | def parallel(self, two_port):
462 | y = self._to_y() + two_port._to_y()
463 | f = pick_f(self, two_port)
464 |
465 | return TwoPort(y=y, f=f)
466 |
467 | '''def cascadexx(self, two_port):
468 | if isinstance(two_port, OnePort):
469 | y = self.y
470 | y[_22] += two_port.y
471 |
472 | return TwoPort(y=y, f=self.f)
473 |
474 | # handle case where one two port is defined at a single point
475 | if len(self.s) == 1:
476 | tp1 = self.abcd[0]
477 | abcd = array([dot(tp1, tp2) for tp2 in two_port.abcd])
478 | elif len(two_port.s) == 1:
479 | tp2 = two_port.abcd[0]
480 | abcd = array([dot(tp1, tp2) for tp1 in self.abcd])
481 | else:
482 | abcd = array([dot(tp1, tp2) for (tp1, tp2) in zip(self.abcd, two_port.abcd)])
483 |
484 | return TwoPort(abcd=abcd, f=self.f)'''
485 |
486 | def __repr__(self):
487 | desc = self.description if self.description else 'Two-port network'
488 | desc += ': '
489 |
490 | if self.f is not None:
491 | freqs = 'f = {} {}Hz'.format(*to_eng_form(self.f[0]))
492 |
493 | if len(self.f) > 1:
494 | freqs += ' - {} {}Hz'.format(*to_eng_form(self.f[-1]))
495 | else:
496 | freqs = ''
497 |
498 | details = ', '
499 |
500 | g = self.g_t()
501 | ind_max = np.argmax(g)
502 | details += '|S21| = {0:.2f} dB'.format(db(g[ind_max]))
503 | if self.f is not None and len(self.f) > 1:
504 | details += ' (@ {} {}Hz), '.format(*to_eng_form(self.f[ind_max]))
505 | else:
506 | details += ', '
507 |
508 | g = self.g_msg()
509 | ind_max = np.argmax(g)
510 | details += 'MSG = {0:.2f} dB'.format(db(g[ind_max]))
511 | if self.f is not None and len(self.f) > 1:
512 | details += ' (@ {} {}Hz), '.format(*to_eng_form(self.f[ind_max]))
513 | else:
514 | details += ', '
515 |
516 | if (self.mu_s() < 1).any():
517 | details += 'potentially unstable (k_min = {}), '.format(self.rollett_kt().min())
518 | else:
519 | g = self.g_max()
520 | ind_max = np.argmax(g)
521 | details += 'g_max = {0:.2f} dB'.format(db(g[ind_max]))
522 | if self.f is not None and len(self.f) > 1:
523 | details += ' (@ {} {}Hz)'.format(*to_eng_form(self.f[ind_max]))
524 |
525 | return '<' + desc + freqs + details + '>'
526 |
527 | s = property(_to_s, _from_s)
528 | y = property(_to_y, _from_y)
529 | z = property(_to_z, _from_z)
530 | t = property(_to_t, _from_t)
531 | abcd = property(_to_abcd, _from_abcd)
532 |
533 | inp = input_equiv
534 | out = output_equiv
535 | #__rrshift__ = cascade
536 | __mul__ = cascade
537 | __add__ = series
538 | __floordiv__ = parallel
539 |
540 | class TwoPort(TwoPortBase):
541 | def vswr_in(self, load=None):
542 | s_in = self.s_in(load)
543 |
544 | return (1 + abs(s_in))/(1 - abs(s_in))
545 |
546 | def max_stable_gain(self):
547 | s12, s21 = self.s[_12], self.s[_21]
548 |
549 | return abs(s21/s12)
550 |
551 | def transducer_gain(self, source=None, load=None):
552 | ss = 0 if source is None else source.s_out()
553 | sl = 0 if load is None else load.s_in()
554 |
555 | s = self.s
556 |
557 | return ((1 - abs(ss)**2)/abs(1 - self.s_in(load)*ss)**2) * abs(self.s[_21])**2 * ((1 - abs(sl)**2)/abs(1 - self.s[_22]*sl)**2)
558 |
559 | def available_gain(self, source=None):
560 | ss = 0 if source is None else source.s_out()
561 |
562 | s = self.s
563 |
564 | return ((1 - abs(ss)**2)/abs(1 - s[_11]*ss)**2)*abs(s[_21]**2)/(1 - abs(self.s_out(source))**2)
565 |
566 | def power_gain(self, load=None):
567 | sl = 0 if load is None else load.s_in()
568 |
569 | s = self.s
570 |
571 | return ((1 - abs(sl)**2)/abs(1 - s[_22]*sl)**2)*abs(s[_21]**2)/(1 - abs(self.s_in(load))**2)
572 |
573 | def max_gain(self):
574 | k = self.rollett_k()
575 | g_msg = self.max_stable_gain()
576 |
577 | g_max = zeros(len(k))
578 | g_max[k >= 1] = g_msg[k >= 1]*(k[k >= 1] - np.sqrt(k[k >= 1]**2 - 1))
579 | g_max[k < 1] = g_msg[k < 1]
580 |
581 | return g_max
582 |
583 | def max_single_sided_matched_gain(self):
584 | k = self.rollett_k()
585 | g_msg = self.max_stable_gain()
586 |
587 | return 2*k*g_msg
588 |
589 | def max_double_sided_mismatched_gain(self):
590 | k = self.rollett_k()
591 | g_msg = self.max_stable_gain()
592 |
593 | return 2*(k + 1)*g_msg
594 |
595 | def matched_source(self):
596 | s = self.s
597 | d = self.det_s()
598 |
599 | b = 1 + abs(s[_11])**2 - abs(s[_22])**2 - abs(d)**2
600 | c = s[_11] - d*(s[_22].conj())
601 |
602 | ss = (b - sign(b)*np.sqrt(b**2 - 4*abs(c)**2))/(2*c)
603 |
604 | return OnePort(s=ss, f=self.f)
605 |
606 | def matched_load(self):
607 | s = self.s
608 | d = self.det_s()
609 |
610 | b = 1 + abs(s[_22])**2 - abs(s[_11])**2 - abs(d)**2
611 | c = s[_22] - d*(s[_11].conj())
612 |
613 | sl = (b - sign(b)*np.sqrt(b**2 - 4*abs(c)**2))/(2*c)
614 |
615 | return OnePort(s=sl, f=self.f)
616 |
617 | '''def mismatch(self, load):
618 | ss = self.s_out()
619 | sl = load.s_in()
620 |
621 | return 1 - abs((sl - ss.conj())/(1 - ss*sl))**2'''
622 |
623 | def mu_stability_source(self):
624 | s = self.s_params
625 | d = self.det_s()
626 |
627 | return (1 - abs(s[_11])**2)/(abs(s[_12]*s[_21]) + abs(s[_22] - d*(s[_11].conj())))
628 |
629 | def mu_stability_load(self):
630 | s = self.s_params
631 | d = self.det_s()
632 |
633 | return (1 - abs(s[_22])**2)/(abs(s[_12]*s[_21]) + abs(s[_11] - d*(s[_22].conj())))
634 |
635 | def rollett_k(self):
636 | s = self.s_params
637 | d = self.det_s()
638 |
639 | return (1 - abs(s[_11])**2 - abs(s[_22])**2 + abs(d)**2)/(2*abs(s[_12]*s[_21]))
640 |
641 | def rollett_kt(self):
642 | '''
643 | EL Tan's single parameter modified Rollett stability factor
644 | '''
645 | s = self.s_params
646 | d = self.det_s()
647 |
648 | return (1 - abs(s[_11])**2 - abs(s[_22])**2 + abs(d)**2 + 0.5*(1 - abs(d)**2 - abs(1 - abs(d)**2)))/(2*abs(s[_12]*s[_21]))
649 |
650 | def input_tunability(self, load):
651 | s = self.s_params
652 | d = self.det_s()
653 | sl = load.s_in()
654 |
655 | return abs(s[_12]*s[_21]*sl)/abs(1 - s[_22]*sl)/abs(s[_11] - d*sl)
656 |
657 | def source_stability_circle(self):
658 | s = self.s_params
659 |
660 | det = s[_11]*s[_22] - s[_12]*s[_21]
661 |
662 | c = s[_11] - det*(s[_22].conj())
663 | d = (abs(c)**2 - abs(s[_12]*s[_21])**2)/(1 - abs(s[_22])**2)
664 |
665 | center = c.conj()/d
666 | radius = abs(s[_12]*s[_21])/abs(d)
667 | circle_stable = d < 0
668 |
669 | return center, radius, circle_stable
670 |
671 | def load_stability_circle(self):
672 | s = self.s_params
673 |
674 | det = s[_11]*s[_22] - s[_12]*s[_21]
675 |
676 | c = s[_22] - det*(s[_11].conj())
677 | d = abs(s[_22])**2 - abs(det)**2
678 |
679 | center = c.conj()/d
680 | radius = abs(s[_12]*s[_21])/abs(d)
681 | circle_stable = d < 0
682 |
683 | return center, radius, circle_stable
684 |
685 | def operating_gain_circle(self, G):
686 | s = self.s
687 |
688 | det = s[_11]*s[_22] - s[_12]*s[_21]
689 | g = G/(abs(s[_21])**2)
690 |
691 | c = s[_22] - det*(s[_11].conj())
692 | d = abs(s[_22])**2 - abs(det)**2
693 | k = self.rollett_k()
694 |
695 | center = g*(c.conj())/(1 + g*d)
696 | radius = np.sqrt(g**2*abs(s[_12]*s[_21])**2 - 2*g*k*abs(s[_12]*s[_21]) + 1)/abs(1 + g*d)
697 |
698 | return center, radius
699 |
700 | def available_gain_circle(self, G):
701 | s = self.s_params
702 |
703 | det = s[_11]*s[_22] - s[_12]*s[_21]
704 | g = G/abs(s[_21])**2
705 |
706 | c = s[_11] - det*(s[_22].conj())
707 | d = abs(s[_11])**2 - abs(det)**2
708 | k = self.rollett_k()
709 |
710 | center = g*(c.conj())/(1 + g*d)
711 | radius = np.sqrt((g*abs(s[_12]*s[_21]))**2 - 2*g*k*abs(s[_12]*s[_21]) + 1)/abs(1 + g*d)
712 |
713 | return center, radius
714 |
715 | def change_ref_plane(self, delay_11, delay_22):
716 | pd_11 = np.exp(1j*2*pi*self.f*delay_11)
717 | pd_22 = np.exp(1j*2*pi*self.f*delay_22)
718 |
719 | correction = np.zeros(self.s.shape, dtype='complex')
720 | correction[_11] = pd_11**2
721 | correction[_22] = pd_22**2
722 | correction[_12] = pd_11*pd_22
723 | correction[_21] = pd_11*pd_22
724 |
725 | return TwoPort(s=self.s*correction, f=self.f)
726 |
727 | g_t = transducer_gain
728 | g_a = available_gain
729 | g_p = power_gain
730 | g_msg = max_stable_gain
731 | g_msm = max_single_sided_matched_gain
732 | g_mdm = max_double_sided_mismatched_gain
733 | g_max = max_gain
734 | mu_s = mu_stability_source
735 | mu_l = mu_stability_load
736 | k = rollett_k
737 | kt = rollett_kt
738 |
739 | class Cascade(TwoPort):
740 | def __init__(self, left_port, right_port, *args, **kwargs):
741 | self.components = []
742 | if isinstance(left_port, Cascade) and left_port.components:
743 | self.components += left_port.components
744 | else:
745 | self.components += [left_port]
746 | if isinstance(right_port, Cascade) and right_port.components:
747 | self.components += right_port.components
748 | else:
749 | self.components += [right_port]
750 |
751 | self.description = 'Cascade of {} two-port networks'.format(len(self.components))
752 |
753 | TwoPort.__init__(self, *args, **kwargs)
754 |
755 | def merge(self):
756 | self.components = []
757 |
758 | def main():
759 | r = OnePort(z=50+40j, f=[1e9]) // OnePort(z=50)
760 | print(repr(r))
761 |
762 | r = OnePort(z=25) + OnePort(z=25)
763 | print(r.z)
764 |
765 | z = TwoPort(s=[[0.4, 0.1], [3, 0.2]])
766 |
767 | rl = z.matched_load()
768 | rs = z.matched_source()
769 |
770 | print(z.rollett_k())
771 | print(z.g_max(), z.g_t(rs, rl))
772 | print(rs.z, rl.z)
773 |
774 |
775 | if __name__ == '__main__':
776 | main()
777 |
--------------------------------------------------------------------------------