├── .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 | ![image](http://i.imgur.com/4Jduvhq.png) 97 | * Instability regions for different frequency, and a minimum gain circle. 98 | ![image](http://i.imgur.com/z4I0RfC.png) 99 | * Gain surface and instability regions (red -> unstable). 100 | ![image](http://i.imgur.com/NVDWINe.png) 101 | * Gain surface and instability regions (green -> stable). 102 | ![image](http://i.imgur.com/BYHQPQs.png) 103 | * Gains vs. frequency 104 | ![image](http://i.imgur.com/xw9HkZ5.png) 105 | * Reflection coefficient on Smith chart 106 | ![image](http://i.imgur.com/Oh4HyTW.png) 107 | * Stability factors vs. frequency 108 | ![image](http://i.imgur.com/LwxYfIt.png) 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 | --------------------------------------------------------------------------------