Microwave filters GUI 2.0.3

- Main Page
- Related Pages
- Packages
- Classes
- Files

- File List

mwfiltersgui.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ## @file
5 ## @todo Linux executables created with cx_freeze report sys.stdout.encoding equal to None. To avoid program crash when trying to write non-ascii characters, these characters are replaced by '?'. It would be better to enforce writting uft-8 to stdout regardless of sys.stdout.encoding .
6 ## @todo Possibility to change styles with a combobox. Not obvious in pyQt.
7 ##@todo Check end-of-line in a Mac computer. It should work if platform.system() returns something different than 'Windows', 'Microsoft', 'Vista' or 'Linux'.
8
9 ##
10 # @package mwfiltersgui Graphical User Interface
11 # <p>@author J.M. Rius, J. Mateu, J.M. Tamayo, C. Collado, A. Padilla and J. O'Callaghan.
12 # <p>Dpt. Signal Tehory and Communications, Universitat Politècnica de Catalunya (UPC),
13 # @version 2.0.3
14 # @date June 11, 2013
15 # <p>Acknowledgement: The software was developed in the frame of contract 21398/08/NL/GLC with the European Space Agency (ESA). Technical Offer was Christoph Ernst.
16 # Further features were developed under contract UPC-C7767 with Thales Alenia Space España (TAS-E).
17 # Contributions to the definition of the software functionality and testing have been made by Christoph Ernst, Mónica Martínez Mendoza and other
18 # ESA-ESTEC personnel, and Santiago Sobrino and Luis Roglá from TAS-E.
19 # <p>Copyright: &copy; 2009 Universitat Politècnica de Catalunya (UPC). <br>Contact: lossyfilters@tsc.upc.edu
20 # <p>This program is free software; you can redistribute it and/or modify it under the terms of
21 # the GNU General Public License as published by the Free Software Foundation; either
22 # version 3 of the License, or (at your option) any later version.
23 # <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
24 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
25 # See the GNU General Public License for more details.
26 # <p>You should have received a copy of the GNU General Public License along with this program
27 # in file "LICENSE.GPL3"; if not, download it from http://www.gnu.org/licenses/gpl-3.0.html .
28 # <p>Additional permission under GNU GPL version 3 section 7:
29 # <p>If you modify this Program, or any covered work, by linking or combining it with the "Extra Filters Library" (libextrafilters),
30 # the "Lossy Filters Library" (liblossyfilters) or the "License Check Library" (libchecklicense), or modified versions of that libraries,
31 # containing parts covered by the terms described in files LICENSE.LIBEXTRAFILTERS and LICENSE.LIBLOSSYFILTERS,
32 # the licensors of this Program grant you additional permission to convey the resulting work.
33 # <p>Lossyfilters software GUI uses the open-source dbplot.py module, which is based in part on the work of the Qwt project
34 # (http://qwt.sf.net) and has been released by UPC under the terms of the GNU GPL version 3.
35 #
36
37 import sys
38 import getopt
39 import os.path
40 import platform
41 import codecs
42 import types
43 import copy
44 from math import log10, ceil, pi, sin, sqrt
45 from functools import partial
46 from PyQt4.QtCore import *
47 from PyQt4.QtGui import *
48 import PyQt4.Qwt5 as Qwt
49 from dbplot import DbPlot
50 import numpy as np
51 import scipy as sc
52 from scipy import interpolate
53 import Ui_mainwindow
54 import Ui_parameditdlg
55 import Ui_couplingmatrixdlg
56 import Ui_matrixeditdlg
57 import Ui_tempvariationdlg
58
59 ##
60 # Global variable useful only for Mac OS X
61 MAC = "qt_mac_set_native_menubar" in dir()
62
63 from libcommonfunc import complexStr, listStr, myPrint, setGlobalVariablesLibCommonFunc, Parameters, readParamFile, printHeader, Sparameters, CouplingMatrices, FrequencyTransformBP, parseError, synthError, licenseError, criticalErrorMsg, warningMsg, saveSignificantDigits, saveSignificantDigitsEnergy
64 from libfreefilters import CharPolys, setGlobalVariablesLibFreeFilters #, importedExtraFilters
65
66 # Try to import the libextrafilters. If it is not available, set importedExtraFilters flag to False.
67 try:
68 if os.path.exists('libextrafilters.pyc'): existingExtraFilters = True
69 else: existingExtraFilters = False
70 from libextrafilters import TZoptimMask
71 except ImportError:
72 ##
73 # Variable set to True if the non-free libextrafilters is available. Otherwise set to False.
74 importedExtraFilters = False
75 else:
76 importedExtraFilters = True
77
78 # Try to import the liblossyfilters. If it is not available, set importedLossyFilters flag to False.
79 try:
80 if os.path.exists('liblossyfilters.pyc'): existingLossyFilters = True
81 else: existingLossyFilters = False
82 from liblossyfilters import predistortion, nonUniformQ, importedCheckLicense, existingCheckLicense
83 except ImportError:
84 ##
85 # Variable set to True if the non-free liblossyfilters is available. Otherwise set to False.
86 importedLossyFilters = False
87 else:
88 importedLossyFilters = True
89
90
91 ##
92 #
93 # Class derived from DbPlot to allow adding some widgets for plotting energy and power.
94 #
95 class EnergyDbPlot(DbPlot):
96 # def __init__(self, XData, leftYData, rightXData = None, rightYData = None, leftWidth = None, rightWidth = None, leftYmin = None, leftYmax = None, rightYmin = None, rightYmax = None, Xmin = None, Xmax = None, windowTitle = None, tabTitle = None, title=None, leftYNames=None, rightYNames=None, leftYVisible=None, rightYVisible=None, XLabel=None, leftYLabel=None, rightYLabel=None, leftYColors=None, rightYColors=None, textLabel=None, Xunits=None, leftYunits=None, rightYunits = None, parent=None):
97 ##
98 #
99 # Derived class constructor.
100 #
101 def __init__(self, *posPars, **keyPars):
102 super(EnergyDbPlot, self).__init__(*posPars, **keyPars)
103
104 horLayout = QHBoxLayout()
105 horLayout.addStretch()
106 horLayout.addWidget(QLabel("Excitation:"))
107 self.excitation_comboBox = QComboBox()
108 self.excitation_comboBox.addItems(['Direct (S->L)', 'Reverse (L->S)'])
109 horLayout.addWidget(self.excitation_comboBox)
110 horLayout.addSpacing(20)
111 horLayout.addWidget(QLabel("Input power:"))
112 self.inputPower_doubleSpinBox = QDoubleSpinBox()
113 self.inputPower_doubleSpinBox.setRange(0, 1e9)
114 self.inputPower_doubleSpinBox.setValue(1)
115 horLayout.addWidget(self.inputPower_doubleSpinBox)
116 horLayout.addWidget(QLabel("W"))
117
118 # horLayout.addStretch()
119 # horLayout.addWidget(QLabel("Input power:"))
120 # self.inputPower_lineEdit = QLineEdit('1')
121 # realPositiveValidator = QRegExpValidator( reRealPositive, self)
122 # self.inputPower_lineEdit.setValidator(realPositiveValidator)
123 # horLayout.addWidget(self.inputPower_lineEdit)
124 # horLayout.addWidget(QLabel("W"))
125
126 self.verticalLayout.insertLayout(0, horLayout)
127
128 # Connect inputPower and excitation widgets to slot
129 self.connect(self.inputPower_doubleSpinBox, SIGNAL("valueChanged(double)"), self.updateInputPower)
130 self.connect(self.excitation_comboBox, SIGNAL("currentIndexChanged(int)"), self.updateExcitation)
131
132 ##
133 # List of leftYData for reverse excitation. List for all Tabs.
134 self.leftYDataRev = []
135
136 ##
137 # List of rightYData for reverse excitation. List for all Tabs.
138 self.rightYDataRev = []
139
140
141 ##
142 #
143 # Update energy and power plots after inputPower value has been changed by the user.
144 # Y-axis are autoscaled after update.
145 #
146 def updateInputPower(self, value):
147
148 for nTab in range(self.Ntabs):
149 newLeftYData = [ value*data for data in np.array(self.leftYData[nTab]) ]
150 newRightYData = [ value*data for data in self.rightYData[nTab] ]if self.rightYData[nTab] is not None else None
151
152 self.update(nTab, XData = self.XData[nTab], rightXData = self.rightXData[nTab], leftYData = newLeftYData, rightYData = newRightYData )
153 self.autoScaleSomeAxis(nTab, autoScaleBottomX = False, autoScaleLeftY = True, autoScaleRightY = True)
154
155
156 ##
157 #
158 # Update energy and power plots after excitation value has been changed by the user.
159 # Y-axis are autoscaled after update.
160 #
161 def updateExcitation(self, value):
162
163 for nTab in range(self.Ntabs):
164 if value == 0:
165 newLeftYData = self.leftYData[nTab]
166 newRightYData = self.rightYData[nTab]
167 elif value == 1:
168 newLeftYData = self.leftYDataRev[nTab]
169 newRightYData = self.rightYDataRev[nTab]
170
171 self.update(nTab, XData = self.XData[nTab], rightXData = self.rightXData[nTab], leftYData = newLeftYData, rightYData = newRightYData )
172 self.autoScaleSomeAxis(nTab, autoScaleBottomX = False, autoScaleLeftY = True, autoScaleRightY = True)
173
174
175 ##
176 #
177 # Set YData for reverse excitation.
178 # This method appends data to a list for all tabs, so it must be called for all tabs sequentially in tab order.
179 # @param leftYDataRev = Left-y data for reverse excitation.
180 # @param rightYDataRev = Right-y data for reverse excitation.
181 #
182 def appendReverseData(self, leftYDataRev, rightYDataRev):
183 self.leftYDataRev.append(leftYDataRev)
184 self.rightYDataRev.append(rightYDataRev)
185
186
187 ##
188 #
189 # Replace YData for reverse excitation.
190 # @param nTab = Tab index.
191 # @param leftYDataRev = Left-y data for reverse excitation.
192 # @param rightYDataRev = Right-y data for reverse excitation.
193 #
194 def replaceReverseData(self, nTab, leftYDataRev, rightYDataRev):
195 self.leftYDataRev[nTab] = leftYDataRev
196 self.rightYDataRev[nTab] = rightYDataRev
197
198 # Show the direct or reverse data depending on the excitation widget status
199 self.updateExcitation(self.excitation_comboBox.currentIndex())
200
201
202 ##
203 #
204 # Widget to ask the user for temperature variation parameters.
205 #
206 class TempVarDlg(QDialog, Ui_tempvariationdlg.Ui_TempVarDlg):
207
208 def __init__(self, parent=None):
209 super(TempVarDlg, self).__init__(parent)
210 # self.setAttribute(Qt.WA_DeleteOnClose) # Do not set Qt.WA_DeleteOnClose in modal dump dialogs: you would not be able to read the data after accepting the dialog
211 self.setupUi(self)
212
213
214 ##
215 #
216 # Specification mask class.
217 #
218 class SpecMask(object):
219
220 ##
221 #
222 # Read specification mask from file.
223 # @param fileName = File Name
224 # @param parent = Parent window for the temperature variation dialog
225 #
226 def __init__(self, fileName, parent=None):
227
228 # Start helper functions
229
230 ##
231 #
232 # Open a widget to get temperature variation parameters:
233 # Tamb = Ambient Temperature: Temperature at which the filter is built. At ambient temperature, the measured [S] parameters correspond to the designed [S] parameters.
234 # Tmax = Maximum operating temperature. Due to thermal expansion, the filter at Tmax is actually larger than the filter built at Tamb and the [S] parameters are shifted to the left (to lower frequencies).
235 # Tmin = Minimum operating temperature. Due to thermal expansion, the filter at Tmin is actually smaller than the filter built at Tamb and the [S] parameters are shifted to the right (to higher frequencies).
236 # alpha = Thermal expansion coefficient, expressed in parts per million per degree Celsius. Example: 22 ppm/C for alumminium.
237 # @return freqShiftRight = Relative frequency shift of the mask to the right = alpha*(Tmax-Tamb)*1e-6.
238 # @return freqShiftLeft = Relative frequency shift of the mask to the left = alpha*(Tamb-Tmin)*1e-6.
239 #
240 def getTempVar():
241 temVarDlg = TempVarDlg(parent)
242 if temVarDlg.exec_():
243 if temVarDlg.temVar_groupBox.isChecked() == True:
244 alpha = temVarDlg.coef_doubleSpinBox.value()*1e-6
245 Tamb = temVarDlg.Tamb_doubleSpinBox.value()
246 Tmax = temVarDlg.Tmax_doubleSpinBox.value()
247 Tmin = temVarDlg.Tmin_doubleSpinBox.value()
248 return alpha*(Tmax-Tamb), alpha*(Tamb-Tmin)
249 else:
250 return 0, 0
251 else:
252 return None, None
253
254
255 ##
256 #
257 # Helper function that returns next item that does not start with '!' of iterLines iterator.
258 # Items returned by iterLines iterator must be strings.
259 # @return line = First item that does not start with '!'
260 #
261 def nextNonComment():
262 line = file.readline()
263 while len(line)>0 and (line.startswith('!') or len(line.strip()) == 0): line = file.readline()
264 return line
265
266 ##
267 #
268 # Helper function that returns the conversion factor corresponding to 'units' argument.
269 # @param units = Units. Must be a key in the unitsFactorDict dictionary. String.
270 # @return factor = Frequency conversion factor
271 #
272 def getUnitsFactor(units):
273 try: factor = unitsFactorDict[units]
274 except KeyError, err: raise parseError, "Incorrect frequency units '%s', should be one of: %s" % (units, str(unitsFactorDict.keys()))
275 return factor
276
277 ##
278 #
279 # Helper function that returns the data read form specification mask file.
280 # The units conversion factors are applied.
281 # @param line = String to process (line of specification mask file).
282 # @return data = Numpy array containing the data.
283 # @return line = String containing next non-comment line of specification mask file after the data, normally starting with '#'.
284 #
285 def getData(line):
286 # Get unit factors
287 unitsFactors = np.array([ getUnitsFactor(units) for units in unitsList ] )
288
289 data = []
290 while len(line)>0 and not line.startswith('#'): # Check len(line)>0 to detect EOF
291
292 # Get data and check dimensions
293 xArray = np.array( [ float(x) for x in line.split() ] )
294 if len(xArray) != len(unitsFactors): raise parseError, "Number of data columns different than number of units in Mask format line (%s)" % str([parType] + unitsList)
295 data.append(xArray * unitsFactors)
296
297 #Get next line
298 line = nextNonComment()
299 data = np.array(data).squeeze()
300
301 if data.ndim == 0: data = float(data) # An scalar
302
303 return (data, line)
304
305 # End helper functions
306
307 fbasename = os.path.basename(fileName)
308
309 ##
310 # File name
311 self.fileName = fbasename
312
313 ##
314 # Parent window
315 self.mainWindow = parent
316
317 ##
318 # Central frequency
319 self.f0 = None
320
321 ##
322 # Bandwidth
323 self.BW = None
324
325 ##
326 # Specification for S11 (return losses) in dB. Float.
327 self.S11spec = None
328
329 ##
330 # Value of S21 at f0
331 self.S21_F0 = None
332
333 ##
334 # Value of S21 over bandwidth
335 self.S21_BW = None
336
337 ##
338 # Specification for S21 variation over bandwidth (insertion losses) in dB pk-pk.
339 # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
340 self.S21inbandSpecDelta = None
341
342 ##
343 # Specification for S21 out of band rejection in dB.
344 # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
345 self.S21outbandSpec = None
346
347 ##
348 # Group delay mask (sec).
349 # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
350 self.GDspec = None
351
352 ##
353 # Frequency samples in the pass band for optimization
354 self.freqInbandOptim = None
355
356 ##
357 # Frequency samples in the rejection band for optimization
358 self.freqOutBandOptim = None
359
360 ##
361 # Array with all frequency samples for optimization, in Hz, the first self.NfreqInBandOptim correspond to self.freqInbandOptim and the rest ot self.freqOutBandOptim
362 self.evalFoptim = None
363
364 ##
365 # Array with all frequency samples for optimization, in normalized s-plane, the first self.NfreqInBandOptim correspond to self.freqInbandOptim and the rest ot self.freqOutBandOptim
366 self.evalSoptim = None
367
368 ##
369 # Number of samples in self.evalSoptim corresponding to the pass band = len(self.freqInbandOptim)
370 self.NfreqInBandOptim = None
371
372 ##
373 # S11 mask samples in the pass band for optimization
374 self.S11optim = None
375
376 ##
377 # S21 mask samples in the pass band for optimization
378 self.S21inBandOptim = None
379
380 ##
381 # S21 mask samples in the rejection band for optimization
382 self.S21outBandOptim = None
383
384
385 try:
386 file = open(fileName, 'r')
387 except IOError, err:
388 criticalErrorMsg( 'Cannot open Mask specification file:<p>' + unicode(err) )
389 myPrint('Failed to open Mask specification file: %s' % fbasename)
390 return
391
392 freqShiftRight, freqShiftLeft = getTempVar()
393 if freqShiftLeft is None and freqShiftRight is None:
394 freqShiftLeft, freqShiftRight = 0, 0 # Cancelling or closing the dialog is equal to no temperature variation
395 myPrint('Mask temperature freq shift (relative) = %.2e to the right and %.2e to the left' % (freqShiftRight, freqShiftLeft))
396
397 ##
398 # Factor to multiply the frequencies that shift to the right
399 self.freqFactorRight = 1+freqShiftRight
400
401 ##
402 # Factor to multiply the frequencies that shift to the left
403 self.freqFactorLeft = 1-freqShiftLeft
404
405 myPrint('Reading [S] Mask specification file: %s' % fbasename)
406
407 unitsFactorDict = { 'Hz': 1, 'kHz': 1e3, 'MHz': 1e6, 'GHz': 1e9, 'dB': 1, 's': 1, 'ms': 1e-3, 'us': 1e-6, 'ns': 1e-9 }
408
409 try:
410 line = nextNonComment()
411 while len(line) > 0: # while not EOF
412 if not line.lstrip().startswith('#'): raise parseError, 'Incorrect file format'
413 optionList = line[1:].split() # Leading '#' removed
414 parType, unitsList = optionList[0], optionList[1:]
415 line = nextNonComment()
416
417 if parType.upper() == 'f0'.upper(): self.f0, line = getData(line)
418 elif parType.upper() == 'BW'.upper(): self.BW, line = getData(line)
419 elif parType.upper() == 'S11_BW'.upper(): self.S11spec, line = getData(line)
420 elif parType.upper() == 'S21_f0'.upper(): self.S21_F0, line = getData(line)
421 elif parType.upper() == 'S21_BW'.upper(): self.S21_BW, line = getData(line)
422 elif parType.upper() == 'S21_f_inband'.upper(): self.S21inbandSpecDelta, line = getData(line)
423 elif parType.upper() == 'GD_f'.upper(): self.GDspec, line = getData(line)
424 elif parType.upper() == 'S21_f_outband'.upper(): self.S21outbandSpec, line = getData(line)
425 file.close()
426
427 except (parseError, ValueError), err:
428 criticalErrorMsg( 'ERROR parsing specifications mask file:<p>' + unicode(err) )
429 myPrint('Failed to read file: %s' % fbasename)
430 else:
431 myPrint('Loaded file: %s' % fbasename)
432
433
434 ##
435 #
436 # Process specification data to obtain the mask curves.
437 # The mask values are computed from specification relative values adding the relevant [S] parameter data.
438 # @param SP = Sparameters class instance
439 #
440 def specToMask(self, SP):
441
442 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!
443 # BEGIN: Helper function
444
445 ##
446 #
447 # Apply frequency shift to mask.
448 # @param mask = Mask numpy array
449 # @param type = 'outer' or 'inner'.
450 # 'outer' masks expand: the right freq shift is applied to the left portion of the data and the left frequency shift to the right portion.
451 # 'inner' masks collapse: the left freq shift is applied to the left portion of the data and the right frequency shift to the right portion.
452 #
453 def applyFreqShift(mask, type):
454 if mask is not None:
455 M = mask.shape[0]
456
457 assert type in ['outer', 'inner']
458 if type == 'outer':
459 (leftPartfactor, rightPartFactor) = (self.freqFactorRight, self.freqFactorLeft)
460 elif type == 'inner':
461 (leftPartfactor, rightPartFactor) = (self.freqFactorLeft, self.freqFactorRight)
462
463 # Apply factors. Works for M even or odd using integer division (5/2=2).
464 # Use int() just in case future versions of python implement floating point division.
465 mask[:int(M/2), 0] *= leftPartfactor
466 mask[int((M+1)/2):, 0] *= rightPartFactor
467
468 # END: Helper function
469 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!
470
471 ##
472 # Mask for S11 (return losses) as a function of frequency. Numpy array.
473 self.S11mask = None
474
475 if self.S11spec is not None:
476 self.S11mask = np.array([ (self.f0 - self.BW/2, -self.S11spec), (self.f0 + self.BW/2, -self.S11spec) ])
477
478 ##
479 # Mask for S21 inband (insertion losses)
480 self.S21inBandMask = None
481
482 maxS21 = max( 20*np.log10(np.abs(SP.S21)) )
483
484 if self.S21_BW is not None and self.S21_F0 is not None:
485 S21inband = np.array([ (self.f0 - self.BW/2, -self.S21_BW), (self.f0, -self.S21_F0), (self.f0 + self.BW/2, -self.S21_BW) ])
486 else: S21inband = np.array([[],[]]).T
487
488 if self.S21inbandSpecDelta is not None:
489 S11variation = self.dataSymmetrizeDeltaF0(self.S21inbandSpecDelta, 0)
490 S11variation[:, 1] = maxS21 - S11variation[:, 1]
491 else: S11variation = np.array([[],[]]).T
492
493 self.S21inBandMask = np.concatenate(( S21inband, S11variation ) )
494 if len(self.S21inBandMask) > 0: self.freqSort(self.S21inBandMask, 0)
495 else: self.S21inBandMask = None
496
497 ##
498 # Mask for out-of-band rejection as a function of frequency. Numpy array.
499 self.S21outBandMask = None
500
501 if self.S21outbandSpec is not None:
502 self.S21outBandMask = self.dataSymmetrizeDeltaF0(self.S21outbandSpec, 0, hideBW = True)
503 self.S21outBandMask[:, 1] = maxS21 - self.S21outBandMask[:, 1]
504
505 ##
506 # Mask for group delay (upper).
507 self.GDmaskUpper = None
508
509 ##
510 # Mask for group delay (lower).
511 self.GDmaskLower = None
512
513 if self.GDspec is not None:
514 # Group delay at the centre frequency (average of GD between f0-BW/4 and f0+BW/4)
515 indBW = (SP.freq < self.f0 + self.BW/4 ) & (SP.freq > self.f0 - self.BW/4)
516 GDf0 = np.mean(SP.groupDelay[indBW])
517
518 self.GDmaskUpper = self.dataSymmetrizeDeltaF0(self.GDspec, 0)
519 self.GDmaskUpper[:, 1] = GDf0 + self.GDmaskUpper[:, 1]
520
521 self.GDmaskLower = self.dataSymmetrizeDeltaF0(self.GDspec, 0)
522 self.GDmaskLower[:, 1] = GDf0 - self.GDmaskLower[:, 1]
523
524
525 # Apply mask shift with temperature
526 for maskAndType in [ (self.S21outBandMask, 'outer'), (self.S11mask, 'inner'), (self.S21inBandMask, 'inner'), (self.GDmaskUpper, 'inner'), (self.GDmaskLower, 'inner') ]:
527 applyFreqShift(*maskAndType)
528
529 # print self
530
531
532 ##
533 #
534 # Create mask samples to be used as a reference in optimization.
535 # Half the samples go to the passband and half to the rejection band (symmetrically).
536 # The frequency samples for optimization are obtained by linear interpolation of the mask data.
537 # Only the magnitude of [S] is optimized to comply with the S11 and S21 masks.
538 # Group delay in the mask file, if any, is not used in optimization,
539 #
540 # @param FT = FrequencyTransform class instance, containing f0 and BW parameters.
541 # @param Nsamples = Total number of samples. Half go to the passband and half to the rejection band (symmetrically).
542 #
543 def maskSamplesOptimization(self, FT, Nsamples):
544
545 if self.S11mask is None:
546 raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S11_BW' optional parameter in the mask file."
547
548 if self.S21outBandMask is None:
549 raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S21_f_outband' optional parameter in the mask file."
550
551 if self.S21_F0 is None or self.S21_BW is None:
552 raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S21_F0' and 'S21_BW' optional parameters in the mask file."
553
554 assert self.S21inBandMask is not None # Cannot be None if self.S21_F0 and self.S21_BW are not None
555
556 # Epsilon to guarantee that optimization frequency samples are within the mask interval, with posible roundoff errorsº
557 eps = self.f0/1e12
558
559 # Frequency sampling points in the pass band
560 NpassBand = int(Nsamples/2)
561 minFreqInBand = np.min( np.concatenate([ self.S11mask[:, 0], self.S21inBandMask[:, 0], self.GDmaskUpper[:, 0], self.GDmaskLower[:, 0] ]) ) + eps
562 maxFreqInBand = np.max( np.concatenate([ self.S11mask[:, 0], self.S21inBandMask[:, 0], self.GDmaskUpper[:, 0], self.GDmaskLower[:, 0] ]) ) - eps
563 self.freqInbandOptim = np.linspace(minFreqInBand, maxFreqInBand, NpassBand)
564
565 # Frequency sampling points in the rejection band
566 # We have to remove the NaN samples from self.S21outBandMask,
567 NS21out = len(self.S21outBandMask)
568 assert NS21out % 2 == 0, 'Number of samples in S21outBandMask must be even' # Number of out band mask samples is always even, since the mask is symmetric
569 nonNaN = range(0, NS21out/2-1) + range(NS21out/2+1, NS21out) # Indices on no-NaN samples
570
571 minFreqOutBandLower = np.min(self.S21outBandMask[:NS21out/2, 0]) + eps
572 maxFreqOutBandLower = np.max(self.S21outBandMask[:NS21out/2, 0]) - eps
573 minFreqOutBandUpper = np.min(self.S21outBandMask[1+NS21out/2:, 0]) +eps
574 maxFreqOutBandUpper = np.max(self.S21outBandMask[1+NS21out/2:, 0]) - eps
575
576 NoutBand = int((Nsamples+1)/2)
577 freqOutBandLower = np.linspace(minFreqOutBandLower, maxFreqOutBandLower, int(NoutBand/2) )
578 freqOutBandUpper = np.linspace(minFreqOutBandUpper, maxFreqOutBandUpper, int((NoutBand+1)/2) )
579 self.freqOutBandOptim = np.concatenate([ freqOutBandLower, freqOutBandUpper ])
580
581 # Convenience data to save computation in the cost function
582 self.evalFoptim = np.concatenate([ self.freqInbandOptim, self.freqOutBandOptim ]) # Necessary for BW optimization: the FT will change with BW
583 self.evalSoptim = 1j * FT.normFreq( self.evalFoptim )
584 self.NfreqInBandOptim = len(self.freqInbandOptim)
585
586 # Interpolating functions
587 interpS11 = interpolate.interp1d(self.S11mask[:, 0], self.S11mask[:, 1], bounds_error = False)
588 interpS21inBand = interpolate.interp1d(self.S21inBandMask[:, 0], self.S21inBandMask[:, 1], bounds_error = False)
589 interpS21outBand = interpolate.interp1d(self.S21outBandMask[nonNaN, 0], self.S21outBandMask[nonNaN, 1], bounds_error = False)
590
591 # Interpolated masks
592 self.S11optim = interpS11(self.freqInbandOptim)
593 self.S21inBandOptim = interpS21inBand(self.freqInbandOptim)
594 self.S21outBandOptim = interpS21outBand(self.freqOutBandOptim)
595
596
597 ##
598 #
599 # Sort and symmetrise data with respect to centre frequency self.f0 and add self.f0 to frequency column of array.
600 # Frequencies in the input array are delta with respect to centre frequency self.f0.
601 # Frequencies in the result are absolute.
602 # @param inArray = Input numpy array.
603 # @param freqCol = Column index of frequency values.
604 # @param hideBW = Flag to hide the centre of the mask. If True, the mask is not displayed at the centre, between the symmetric data points. Default False.
605 # @return outArray = Output numpy array
606 #
607 def dataSymmetrizeDeltaF0(self, inArray, freqCol, hideBW = False):
608 array1 = inArray.copy()
609 array2 = array1.copy()
610 array1[:, freqCol] = self.f0 - array1[:, freqCol]
611 array2[:, freqCol] = self.f0 + array2[:, freqCol]
612
613 if hideBW:
614 closestFreq = min(inArray[:, 0]) # Minimum delta freq, which corresponds to the freq closest to f0
615 hideCenter = np.array([ [self.f0 - closestFreq, np.nan], [self.f0 + closestFreq, np.nan] ])
616 outArray = np.concatenate((array1, hideCenter, array2))
617 else:
618 outArray = np.concatenate((array1, array2))
619
620 self.freqSort(outArray, freqCol)
621 return outArray
622
623
624 ##
625 #
626 # Sort array by frequency (inplace).
627 # The input array is modified inplace
628 # @param inArray = Input numpy array.
629 # @param freqCol = Column index of frequency values.
630 #
631 def freqSort(self, inArray, freqCol):
632 sortedIndices = inArray[:,freqCol].argsort()
633 inArray[:, :] = inArray[sortedIndices, :]
634
635
636 def __str__(self):
637 st = ''
638 st += '*** Specifications ***\n\n'
639 st += 'Central frequency:\n' + str(self.f0) + '\n\n'
640 st += 'Bandwidth:\n' + str(self.BW) + '\n\n'
641 st += 'Specification for S11 (return losses):\n' + str(self.S11spec) + '\n\n'
642 st += 'Value of S21 at f0 (insertion losses):\n' + str(self.S21_F0) + '\n\n'
643 st += 'Value of S21 over bandwidth (insertion losses):\n' + str(self.S21_BW) + '\n\n'
644 st += 'S21 variation over bandwidth (insertion losses):\n' + str(self.S21inbandSpecDelta) + '\n\n'
645 st += 'S21 out of band rejection: \n' + str(self.S21outbandSpec) + '\n\n'
646 st += 'Group delay mask:\n' + str(self.GDspec) + '\n\n'
647
648 st += '*** Masks ***\n\n'
649 st += 'Mask for S11 (return losses): \n' + str(self.S11mask) + '\n\n'
650 st += 'Mask for S21 in-band (insertion losses)\n' + str(self.S21inBandMask) + '\n\n'
651 st += 'Mask for S21 for out-of-band rejection: \n' + str(self.S21outBandMask) + '\n\n'
652 st += 'Mask for GD (upper): \n' + str(self.GDmaskUpper) + '\n\n'
653 st += 'Mask for GD (lower): \n' + str(self.GDmaskLower) + '\n\n'
654
655 return st
656
657
658 ##
659 #
660 # [S] parameters class, as read from Touchstone file.
661 # It contains the minimum number of attributes to plot the [S] parameters in the results comparison window.
662 # Of course, the attributes share the same name as the corresponding ones in libcommonfunc.Sparameters class.
663 #
664 class Stouchstone(object):
665
666 ##
667 #
668 # Set frequency axis and S parameters, including phase and group delay, read from a Touchstone file.
669 # Touchstone is a trademark of Agilent Corporation
670 # @param fileName = File Name
671 #
672 def __init__(self, fileName):
673
674 fbasename = os.path.basename(fileName)
675
676 try:
677 file = open(fileName, 'r')
678 except IOError, err:
679 criticalErrorMsg( 'Cannot open [S] Touchstone file:<p>' + unicode(err) )
680 myPrint('Failed to open [S] Touchstone file: %s' % fbasename)
681 return
682
683 myPrint('Reading [S] from Touchstone file: %s' % fbasename)
684
685 freqFactor = 1e9
686 dataFormat = 'MA'
687 freqfactorDict = { 'HZ': 1, 'KHZ': 1e3, 'MHZ': 1e6, 'GHZ': 1e9}
688 optionsLoaded = False
689 freq, S11, S21, S12, S22 = [], [], [], [], []
690 Sread = False
691
692 try:
693 stList = file.readlines()
694 for st in stList:
695 if len(st.lstrip()) == 0: continue # Empty line
696 if st.lstrip().startswith('!'):
697 if Sread: break # If S parameters have been already read, this comment line will start noise specifications, and we want to quit here.
698 else: continue # A comment
699
700 if st.lstrip().startswith('#'): # The options string
701 if optionsLoaded: raise parseError, "There are more than one options line."
702
703 optionList = st.split()
704
705 # Check number of options
706 if len(optionList) != 6: raise parseError, "Incorrect number of items in option line."
707
708 # Parse frequency units
709 units = optionList[1].upper()
710 try: freqFactor = freqfactorDict[units]
711 except KeyError, err: raise parseError, "Incorrect frequency units '%s'" % units
712
713 # Parse type of parameter
714 type = optionList[2]
715 if type != 'S': raise parseError, "Type of parameter is not 'S'."
716
717 # Parse data format
718 dataFormat = optionList[3].upper()
719 if dataFormat not in ['MA', 'DB', 'RI']: raise parseError, "Data format '%s' is not 'MA', 'DB', or 'RI'." % dataFormat
720
721 # Parse reference impedance
722 if optionList[4] != 'R': raise parseError, "Options line does not include 'R' parameter."
723 impedance = optionList[5]
724
725 optionsLoaded = True
726
727 else:
728 try: f, S11a, S11b, S21a, S21b, S12a, S12b, S22a, S22b = [ float(n) for n in st.split()]
729 except ValueError: raise parseError, "Data line '%s' contains incorrect number of items" % st
730
731 freq.append(f * freqFactor)
732
733 if dataFormat == 'RI':
734 S11.append( complex(S11a, S11b) )
735 S21.append( complex(S21a, S21b) )
736 S12.append( complex(S12a, S12b) )
737 S22.append( complex(S22a, S22b) )
738 if dataFormat == 'MA':
739 S11.append( S11a * np.exp(1j * S11b * pi/180) )
740 S21.append( S21a * np.exp(1j * S21b * pi/180) )
741 S12.append( S12a * np.exp(1j * S12b * pi/180) )
742 S22.append( S22a * np.exp(1j * S22b * pi/180) )
743 if dataFormat == 'DB':
744 S11.append( 10**(S11a/20) * np.exp(1j * S11b * pi/180) )
745 S21.append( 10**(S21a/20) * np.exp(1j * S21b * pi/180) )
746 S12.append( 10**(S12a/20) * np.exp(1j * S12b * pi/180) )
747 S22.append( 10**(S22a/20) * np.exp(1j * S22b * pi/180) )
748 Sread = True
749
750 file.close()
751
752 except parseError, err:
753 criticalErrorMsg( 'ERROR parsing [S] Touchstone file:<p>' + unicode(err) )
754 myPrint('Failed to read file: %s' % fbasename)
755 else:
756 myPrint('Loaded file: %s' % fbasename)
757
758 ##
759 # Frequency axis (Hz)
760 self.freq = np.array(freq)
761
762 ##
763 # Parameter \f$ S_{11} \f$
764 self.S11 = np.array(S11)
765
766 ##
767 # Parameter \f$ S_{12} \f$
768 self.S21 = np.array(S21)
769
770 ##
771 # Parameter \f$ S_{21} \f$
772 self.S12 = np.array(S12)
773
774 ##
775 # Parameter \f$ S_{22} \f$
776 self.S22 = np.array(S22)
777
778 ##
779 # Phase of return losses (rad)
780 self.phaseS11 = np.unwrap(np.angle(self.S11))
781
782 ##
783 # Phase of inverse transfer function (rad)
784 self.phaseS12 = np.unwrap(np.angle(self.S12))
785
786 ##
787 # Phase of transfer function (rad)
788 self.phaseS21 = np.unwrap(np.angle(self.S21))
789
790 ##
791 # Phase of inverse return losses (rad)
792 self.phaseS22 = np.unwrap(np.angle(self.S22))
793
794 ##
795 # Frequency axis with sampling points in the middle of self.freq sampling.
796 # It is necessary to plot self.groupDelay2
797 self.freq2 = (self.freq[:-1] + self.freq[1:])/2
798
799 ##
800 # Group delay for the unnormalized filter (sec), sampled at self.freq2
801 self.groupDelay2 = -np.diff(self.phaseS21) / (2*pi* np.diff(self.freq))
802
803 # Set causality principle. There may be spurious negative peaks due to computing phase at transmission zeros.
804 self.groupDelay2[ self.groupDelay2<0 ] = 0
805
806
807 ##
808 #
809 # Subclass for:
810 # - reimplementation of SinzeHint and MinimumSizeHint
811 # - Methods for tables of transmission zeros
812 #
813 class myTableWidget(QTableWidget):
814
815 def __init__(self, name = None, mainDlg = None, parent=None):
816 super(myTableWidget, self).__init__(parent)
817
818 ##
819 # Table name.
820 self.name = name
821
822 ##
823 # Main dialog to which this table belongs, used only where applicable.
824 self.mainDlg = mainDlg
825
826 ##
827 # Frequency units, used only where applicable.
828 self.units = None
829
830 ##
831 # Filter center frequency.
832 # It will be used for computation of 'f-f0' column and for autoupdate of 'Imag' and 'f-f0' colums when one of them is edited.
833 self.f0 = None
834
835 ##
836 # Flag to inform that changes in table cells are due to user interaction
837 self.updatedByUser = True
838
839
840 ##
841 # Store units in self.untis, to use in TZ table headers.
842 # @param units = Units string.
843 #
844 def setUnits(self, units):
845 self.units = units
846
847
848 ##
849 #
850 # Set the value in Center Frequency widget as self.f0.
851 # It will be used for computation of 'f-f0' column and for autoupdate of 'Imag' and 'f-f0' colums when one of them is edited.
852 # @param updateImag: If updateImag is True, then the 'Imag' column of the whole table is updated according to f0 and to the f-f0 column.
853 # If updateImag is False, f0 is only stored in self.f0 and the 'Imag' column is not updated.
854 #
855 def setf0(self, updateImag = True):
856 self.f0 = float(self.mainDlg.f0_lineEdit.text()) * 10**(3*self.mainDlg.f0UnitsIndex)
857
858 if updateImag: # Update TZ Imag column according to f0 and values of f-f0 column
859
860 M = self.rowCount()
861 freqFactor = pow(10, 3*self.mainDlg.transmissionZerosUnitsIndex)
862
863 for m in range(M):
864 item = self.item(m, 2)
865 stIm = str(item.text()).strip() if item is not None else ''
866
867 if stIm == '':
868 newItem = QTableWidgetItem('')
869 self.setItem(m, 1, newItem)
870 else:
871 im = float(stIm) * freqFactor
872 newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im + self.f0) / freqFactor) )
873 newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
874 self.setItem(m, 1, newItem)
875
876
877 ##
878 #
879 # Resize rows and columns to fit contents
880 #
881 def updateTableSize(self):
882 self.resizeRowsToContents()
883 self.resizeColumnsToContents()
884 self.setMaximumSize( self.maximumSizeHint() )
885
886
887 ##
888 #
889 # Table cell has been modified by user, take necessary actions.
890 # If Imag or Imag-f0 columns have been modified, updated the companion cell
891 #
892 def tableCellChanged(self, row, col):
893
894 if not self.updatedByUser: return # Do nothing if it changes are not due to user
895
896 # Avoid infinite recursion
897 self.updatedByUser = False
898
899 freqFactor = pow(10, 3*self.mainDlg.transmissionZerosUnitsIndex)
900
901 self.setCurrentCell(row, col)
902 item = self.currentItem()
903 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter) # It may be an item automatically created with default alignment
904 # Here item string is a valid float or an empty string '', since it has been filtered before
905 stIm = str(item.text()).strip() if item is not None else ''
906
907 if stIm == '': # If updated cell has nothing, put also nothing in the companion cell
908 newItem = QTableWidgetItem('')
909 if col == 1: self.setItem(row, 2, newItem)
910 elif col == 2: self.setItem(row, 1, newItem)
911 else:
912 im = float(stIm) * freqFactor
913 if col == 1: # Imag has been changed
914 newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im - self.f0) / freqFactor) )
915 newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
916 self.setItem(row, 2, newItem)
917 elif col == 2: # f-f0 has been changed
918 newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im + self.f0) / freqFactor) )
919 newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
920 self.setItem(row, 1, newItem)
921
922 self.updateTableSize()
923 self.mainDlg.updateTotaltransmissionZeros()
924
925 self.updatedByUser = True
926
927
928 ##
929 #
930 # Append new row for a new zero
931 #
932 def addRow(self):
933
934 N = self.rowCount()
935 self.setRowCount(N+1)
936
937 self.setCurrentCell(N, 1)
938 self.resizeRowsToContents()
939 self.resizeColumnsToContents()
940 self.setMaximumSize( self.maximumSizeHint() )
941 self.mainDlg.updateDlgHeight()
942
943
944 def sizeHint(self):
945 return maximumSizeHint()
946
947
948 def maximumSizeHint(self):
949 #size = super(myTableWidget, self).sizeHint()
950 size = QTableWidget.sizeHint(self)
951
952 # Compute space for table with headers and ScrollBars (just in case they appear).
953 # At this point, the size of the ScrollBar is given by self.horizontalScrollBar().height(),
954 # for both the horizontal and vertical ScrollBars.
955 # self.verticalScrollBar().width() contains and absurd vale here.
956 width = self.verticalHeader().width() + self.horizontalScrollBar().height()
957 for c in range(self.columnCount()): width += self.columnWidth( c )
958 size.setWidth( width + 8 )
959
960 height = self.horizontalHeader().height() + self.horizontalScrollBar().height()
961 for r in range(self.rowCount()): height += self.rowHeight( r )
962 size.setHeight( height + 4 )
963
964 # if self.name is not None: print self.name
965 # print 'HSB:', self.horizontalScrollBar().isVisible(), self.horizontalScrollBar().width(), self.horizontalScrollBar().height()
966 # print 'VSB:', self.verticalScrollBar().isVisible(), self.verticalScrollBar().width(), self.verticalScrollBar().height()
967
968 return size
969
970 ##
971 #@todo Reimplment myTableWidget.resizeEvent() function to reduce size of table when there are no ScrollBars
972
973 # def resizeEvent(self, event):
974 # ''' Reimplementation of the resize event to check state of the toolbars.
975 # '''
976 # if self.horizontalScrollBar().isVisible() or self.verticalScrollBar().isVisible():
977 # maxSize = self.maximumSizeHint()
978 # if self.horizontalScrollBar().isVisible(): maxSize.setHeight(maxSize.height() - self.horizontalScrollBar().height())
979 # if self.verticalScrollBar().isVisible(): maxSize.setWidth(maxSize.height() - self.horizontalScrollBar().height())
980 # self.setMaximumSize(maxSize)
981 #
982 # if self.name is not None: print self.name
983 # print 'event new size:' , event.size().width(), event.size().height()
984 # print 'event old size:' , event.oldSize().width(), event.oldSize().height()
985 # print 'self:', self.width(), self.height()
986 # print 'HSB:', self.horizontalScrollBar().isVisible(), self.horizontalScrollBar().width(), self.horizontalScrollBar().height()
987 # print 'VSB:', self.verticalScrollBar().isVisible(), self.verticalScrollBar().width(), self.verticalScrollBar().height()
988
989
990 def sizeHint(self):
991 return self.maximumSizeHint()
992
993
994 ##
995 #
996 # GUI main window class.
997 #
998 class MainWindow(QMainWindow, Ui_mainwindow.Ui_MainWindow):
999
1000 ##
1001 #
1002 # Constructor to initialize main window.
1003 # @param fileName = Name of file to automatically open after creating the window. If equal to None, no file is open.
1004 # @param parent = Parent widget, in this case is None.
1005 #
1006 def __init__(self, fileName, parent=None):
1007 super(MainWindow, self).__init__(parent)
1008 self.setupUi(self)
1009
1010 ##
1011 # Variable to store the modified or unmodifed status of parameters class instance P.
1012 self.dirty = False
1013
1014 ##
1015 # Variable that will avoid cleanup if set to True.
1016 # It is used when a method that calls self.clenaup() is called by the GUI and not by the user.
1017 # The user may want to avoid cleanup in some situations.
1018 self.noCleanup = False
1019
1020 ##
1021 # Name of the parameters file to process.
1022 # When main Window is created, self.fileName is a copy of the file name given in the command line when calling the application.
1023 # Later self.fileName can be modified when opening a parameter file with the GUI.
1024 self.fileName = fileName
1025
1026 ##
1027 # Application name, to be accesible outside this module.
1028 self.applicationName = applicationName
1029
1030 self.settings = QSettings()
1031 self.restoreGeometry(self.settings.value("Geometry").toByteArray())
1032 self.restoreState(self.settings.value("State").toByteArray())
1033
1034 ##
1035 # Last open directory name
1036 self.lastDir = unicode(self.settings.value('LastDirectoryOpened', QVariant('.')).toString())
1037
1038 # Set window title , initially there is no modified file to save
1039 self.setWindowTitle(applicationName)
1040 self.setWindowModified(False)
1041
1042 # Disable selections in log_listWidget
1043 self.log_listWidget.setSelectionMode(QAbstractItemView.NoSelection)
1044
1045 # Add results selection comboBox to toolBar
1046 self.results_comboBox = QComboBox(self.toolBar_2)
1047 self.results_comboBox.setObjectName("results_comboBox")
1048 self.results_comboBox.setToolTip("Load result from list")
1049 self.results_comboBox.setStatusTip("Load result from list")
1050 self.results_comboBox.setWhatsThis("Load result from list")
1051 self.connect(self.results_comboBox, SIGNAL("currentIndexChanged(int)"), self.on_results_comboBox_currentIndexChanged)
1052 self.toolBar_2.insertWidget(self.action_ListRemove, self.results_comboBox)
1053
1054 # Add what's this action
1055 self.toolBar_3.insertAction(self.action_FileExit, QWhatsThis.createAction(self.toolBar_3))
1056
1057 ##
1058 # Parameters class instance containing filter and synthesis parameters.
1059 # Read from the parameters file and -maybe- modified with the Parameter Edit Dialog.
1060 self.P = None
1061
1062 ##
1063 # Copy of the CharPolys class instance containing characteristic polynomials.
1064 self.CP = None
1065
1066 ##
1067 # Copy of the CouplingMatrices class instance containing coupling matrices.
1068 self.CM = None
1069
1070 ##
1071 # Copy of the SParameters class instance containing [S] parameters.
1072 self.SP = None
1073
1074 ##
1075 # Copy of the FrequencyTransform class instance containing the frequency transform.
1076 self.FT = None
1077
1078 ##
1079 # Specification Mask class instance.
1080 self.SM = None
1081
1082 ##
1083 # Flag that is True when a successful computation has been done.
1084 # When the flag is False, no results are plotted.
1085 self.computed = False
1086
1087 ##
1088 # QMainWindow that contains the parameter edit window.
1089 self.paramEdit = None
1090
1091 ##
1092 # QwtPlot that contains the Magnitude and Phase S-parameter plot.
1093 self.SPlotMagPhase = None
1094
1095 ##
1096 # QwtPlot that compares S-parameters obtained from CP from those obtained from CM.
1097 self.SPlotError = None
1098
1099 ##
1100 # QwtPlot that compares S-parameters for different results.
1101 self.SPlotCompare = None
1102
1103 ##
1104 # QwtPlot that contains stored energy and dissipated power
1105 self.EnergyPlot = None
1106
1107 ##
1108 # QwtPlot that contains S-parameters for sensitivity analysis
1109 self.SPlotSensitivity = None
1110
1111 ##
1112 # QMainWindow that contains the coupling matrix table.
1113 self.CMdialog = None
1114
1115 ##
1116 # List of results to compare
1117 self.listResults = [ ]
1118
1119 ##
1120 # Last index of results selection comboBox
1121 self.indexResults = None
1122
1123 ##
1124 # Flag that controls if frequency axis must be autoscaled. It will become true after changing the sweep min and max frequencies.
1125 self.autoScaleFreq = False
1126
1127 ##
1128 # Store the mainWindow class instance into global variable 'mainWindow' defined in the libcommonfunc.py and libfreefilters.py
1129 # in order to let the console interface functions in libcommonfunc.py and libfreefilters.py access the GUI mainWindow.
1130 # The GUI methods access the mainWindow class through ParamEditDlg.mainWindow and MainWindow self attributes.
1131 setGlobalVariablesLibCommonFunc(mainWindow = self)
1132 setGlobalVariablesLibFreeFilters(mainWindow = self)
1133
1134 # Print startup output in log window. Must import here the preGuiOutput variable, to catch its current value.
1135 from libcommonfunc import preGuiOutput
1136 myPrint(preGuiOutput)
1137
1138 # Read file if there is one to read
1139 if self.fileName is not None:
1140 self.fileRead(self.fileName)
1141 if autoRun:
1142 self.action_Execute.trigger()
1143 if autoMatrix:
1144 self.action_CouplingMatrix.trigger()
1145 if autoEditMatrix:
1146 self.CMdialog.action_Edit.setChecked(True)
1147
1148
1149 ##
1150 #
1151 # This function checks if there are unsaved changes and, if there are, aks the user to save them.
1152 # The function is executed by the GUI when there is an action that will destroy the parameter class P: Close application, Open file, New file, etc.
1153 # @return ok (True / False) = If True the use has agreed to continue, if False the user cancels the operation.
1154 #
1155 def okToContinue(self):
1156 if self.dirty:
1157 if self.fileName is not None:
1158 message = "File %s has unsaved changes\nSave?" % os.path.basename(self.fileName)
1159 else:
1160 message = "New file with unsaved changes\nSave?"
1161 reply = QMessageBox.question(self, "%s - Unsaved Changes" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
1162 if reply == QMessageBox.Cancel:
1163 return False
1164 elif reply == QMessageBox.Yes:
1165 self.on_action_FileSaveAs_triggered(False)
1166 return True
1167
1168
1169 ##
1170 #
1171 # Print message and update window title.
1172 # Prints message in the log widget at the main window background and also at the status bar for 5 sec.
1173 # Updates main window title with open file name and modified state.
1174 # @param message = Message to print (string).
1175 #
1176 def updateStatus(self, message):
1177 self.statusBar().showMessage(message, 5000)
1178 self.log_listWidget.addItem(message)
1179 self.log_listWidget.setCurrentRow(self.log_listWidget.count()-1)
1180 if self.fileName is not None:
1181 self.setWindowTitle("%s - %s[*]" % (applicationName , os.path.basename(self.fileName)) )
1182 elif self.P is not None and self.P.empty is not True:
1183 self.setWindowTitle("%s - Unnamed[*]" % applicationName)
1184 else:
1185 self.setWindowTitle("%s[*]" % applicationName)
1186 self.setWindowModified(self.dirty)
1187
1188
1189 ##
1190 #
1191 # Reads parameters from a file.
1192 # @param fname = File name.
1193 #
1194 def fileRead(self, fname):
1195 # Initally set filename to None just in case the file parsing fails and the file is not read.
1196 self.fileName = None
1197 self.P = readParamFile(fname)
1198 fbasename = os.path.basename(fname)
1199 # Update self.lastDir
1200 self.lastDir = os.path.dirname(fname)
1201
1202 if self.P is None:
1203 QMessageBox.warning(self, "WARNING",
1204 """<p align="center">It is not possible to open file %s<br>
1205 because it contains an invalid entry:</p>
1206 <p align="center">Please open the file with a text editor and fix it.
1207 </p>
1208 """ % fbasename )
1209 message = "Failed to read file: %s" % fbasename
1210 else:
1211 self.fileName = fname
1212 message = "Loaded file: %s" % fbasename
1213 self.updateStatus(message)
1214
1215 # Create Frequency Transform class instance for this parameters
1216 if self.P is not None:
1217 self.FT = FrequencyTransformBP(self.P)
1218
1219 # Open parameter editor dialog
1220 if self.P is not None:
1221 self.paramEdit = ParamEditDlg(self)
1222 self.paramEdit.show()
1223 self.paramEdit.raise_()
1224 #self.paramEdit.activateWindow()
1225
1226
1227 ##
1228 #
1229 # Close SPlotMagPhase plot window, if it exists.
1230 #
1231 def closeSPlotMagPhaseWindow(self):
1232 # Close CouplingMatrix dialog if it exists
1233 if self.SPlotMagPhase is not None and not self.SPlotMagPhase.closed:
1234 self.SPlotMagPhase.close()
1235 self.SPlotMagPhase = None
1236
1237
1238 ##
1239 #
1240 # Close SPlotCompare plot windows, if they exist.
1241 #
1242 def closeResultsWindow(self):
1243 if self.SPlotCompare is not None and not self.SPlotCompare.closed:
1244 self.SPlotCompare.close()
1245 self.SPlotCompare = None
1246
1247
1248 ##
1249 #
1250 # Close Coupling Matrix window and CM error plot window, if they exist.
1251 #
1252 def closeCMWindows(self):
1253 # Close CouplingMatrix dialog if it exists
1254 if self.CMdialog is not None:
1255 self.CMdialog.close()
1256 self.CMdialog = None
1257
1258 # Close [S] error plot if it exists
1259 if self.SPlotError is not None and not self.SPlotError.closed:
1260 self.SPlotError.close()
1261 self.SPlotError = None
1262
1263 # Close energy plot if it exists
1264 if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1265 self.EnergyPlot.close()
1266 self.EnergyPlot = None
1267
1268
1269 ##
1270 #
1271 # Clean up previous results and any open plot or coupling matrix window.
1272 # Clean up is disabled if self.nocleanup is True, in that case this method does nothing.
1273 # Sets self.dirty to False.
1274 #
1275 def cleanup(self):
1276 if self.noCleanup is True: return
1277
1278 self.closeSPlotMagPhaseWindow()
1279 self.closeCMWindows()
1280 (self.P, self.CP, self.CM, self.SP, self.FT) = (None, None, None, None, None)
1281
1282
1283 ##
1284 #
1285 # Plot [S] parameters magnitude and phase graph, if they are available.
1286 #
1287 def plotSParameters(self):
1288
1289 if self.SP is not None:
1290 if self.SPlotMagPhase is None or self.SPlotMagPhase.closed is True:
1291
1292 # Set dynamic range to the lowest value of S21 at sweep range ends.
1293 linRange = min([ abs(self.SP.S21[0]), abs(self.SP.S21[-1] )])
1294 if linRange > 1e-10: dynamicRange = -20*log10(linRange)
1295 else: dynamicRange = 200
1296 dynamicRange = 10*ceil(dynamicRange/10)
1297
1298 self.SPlotMagPhase = DbPlot( parent=self,
1299 windowTitle = '[S] parameters',
1300 tabTitle = '[S] + Group delay',
1301 title = '[S] parameters (from Characteristic Polynomials)',
1302 XData = self.SP.freq / 1e9,
1303 leftYData = [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1304 rightYData = [ 1e9 * self.SP.groupDelay ],
1305 leftYmin = -dynamicRange,
1306 leftYmax = 0,
1307 leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
1308 leftYVisible = [True, True, False],
1309 XLabel = 'Frequency (GHz)',
1310 leftYLabel = 'dB',
1311 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'ns',
1312 rightYLabel = 'Time (ns.)' ,
1313 rightYNames = ['Group delay'],
1314 rightYVisible = [True]
1315 )
1316 self.SPlotMagPhase.addTab( tabTitle = '[S] + Phase',
1317 title = '[S] parameters (from Characteristic Polynomials)',
1318 XData = self.SP.freq / 1e9,
1319 leftYData = [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22)) ],
1320 rightYData = [ self.SP.phaseS21 * 180/np.pi, self.SP.phaseS11 * 180/np.pi, self.SP.phaseS22 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1321 leftYmin = -dynamicRange,
1322 leftYmax = 0,
1323 leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
1324 leftYVisible = [True, True, False],
1325 XLabel = 'Frequency (GHz)',
1326 leftYLabel = 'dB',
1327 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg',
1328 rightYLabel = 'degrees' ,
1329 rightYNames = ['Phase(S<sub>21</sub>)', 'Phase(S<sub>11</sub>)', 'Phase(S<sub>22</sub>)', 'Deviation S<sub>21</sub> from <br>linear phase'],
1330 rightYVisible = [True, False, False, False]
1331 )
1332 self.SPlotMagPhase.addTab( tabTitle = 'Group delay + Phase',
1333 title = '[S] parameters (from Characteristic Polynomials)',
1334 XData = self.SP.freq / 1e9,
1335 leftYData = [1e9 * self.SP.groupDelay],
1336 rightYData = [ self.SP.phaseS21 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1337 leftYNames = ['Group delay'],
1338 XLabel = 'Frequency (GHz)',
1339 leftYLabel = 'Time (ns.)',
1340 Xunits = 'GHz', leftYunits = 'ns', rightYunits = 'deg',
1341 rightYLabel = 'degrees' ,
1342 rightYNames = ['Phase(S<sub>21</sub>)', 'Deviation from <br>linear phase'],
1343 rightYVisible = [True, False]
1344 )
1345
1346 else:
1347 self.SPlotMagPhase.update(0, self.SP.freq / 1e9, [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1348 rightYData=[ 1e9 * self.SP.groupDelay] , autoScaleBottomX = self.autoScaleFreq)
1349
1350 self.SPlotMagPhase.update(1, self.SP.freq / 1e9, [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1351 rightYData=[ self.SP.phaseS21 * 180/np.pi, self.SP.phaseS11 * 180/np.pi, self.SP.phaseS22 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1352 autoScaleBottomX = self.autoScaleFreq )
1353
1354 self.SPlotMagPhase.update(2, self.SP.freq / 1e9, [1e9 * self.SP.groupDelay], rightYData=[ self.SP.phaseS21 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1355 autoScaleBottomX = self.autoScaleFreq )
1356
1357 self.autoScaleFreq = False
1358
1359 # Plot masks, or update if there are any
1360 if self.SM is not None:
1361 self.SPlotMagPhase.deleteMasks()
1362 self.plotMasks()
1363
1364
1365 ##
1366 #
1367 # Add masks to [S] plots.
1368 #
1369 def plotMasks(self):
1370 if self.SP is None: return # If self.SP does not exist, there are no results to plot yet.
1371
1372 self.SM.specToMask(self.SP)
1373
1374 maskNameRL = 'Return losses\nmask'
1375 maskNameRJ = 'Rejection\nmask'
1376 maskNameIL = 'Insertion losses\nmask'
1377 maskNameGDL = 'Group delay\nlower mask'
1378 maskNameGDU = 'Group delay\nupper mask'
1379
1380 if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False and self.SPlotMagPhase.masks[0] == []: # Do not add masks if there are already
1381 if self.SM.S11mask is not None:
1382 self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1383 self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1384
1385 if self.SM.S21outBandMask is not None:
1386 self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1387 self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1388
1389 if self.SM.S21inBandMask is not None:
1390 self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1391 self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1392
1393 if self.SM.GDmaskLower is not None:
1394 self.SPlotMagPhase.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1395 self.SPlotMagPhase.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1396
1397 self.SPlotMagPhase.changeCurveVisibility(0, [True, False, False], [False], { maskNameRL: True, maskNameRJ: False, maskNameIL: False } )
1398 self.SPlotMagPhase.changeCurveVisibility(1, [False, True, False], [False, False, False, False], { maskNameRL: False, maskNameRJ: True, maskNameIL: True} )
1399 self.SPlotMagPhase.changeCurveVisibility(2, [True], [False, False], { maskNameGDL: True, maskNameGDU: True } )
1400
1401 if self.SPlotError is not None and self.SPlotError.closed is False and self.SPlotError.masks[0] == []: # Do not add masks if there are already
1402 if self.SM.S11mask is not None:
1403 self.SPlotError.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1404
1405 if self.SM.S21outBandMask is not None:
1406 self.SPlotError.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1407
1408 if self.SM.S21inBandMask is not None:
1409 self.SPlotError.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1410
1411 if self.SM.GDmaskLower is not None:
1412 self.SPlotError.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = QColor(0, 175, 0), type = 'Lower', axis = 'left', transpar = 64 )
1413 self.SPlotError.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = QColor(0, 175, 0), type = 'Upper', axis = 'left', transpar = 64 )
1414
1415 if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False and self.SPlotSensitivity.masks[0] == []: # Do not add masks if there are already
1416 if self.SM.S11mask is not None:
1417 self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1418 self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1419
1420 if self.SM.S21outBandMask is not None:
1421 self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1422 self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1423
1424 if self.SM.S21inBandMask is not None:
1425 self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1426 self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1427
1428 if self.SM.GDmaskLower is not None:
1429 self.SPlotSensitivity.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1430 self.SPlotSensitivity.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1431
1432 self.SPlotSensitivity.changeCurveVisibility(0, [True, False, False], [False], { maskNameRL: True, maskNameRJ: False, maskNameIL: False } )
1433 self.SPlotSensitivity.changeCurveVisibility(1, [False, True, False], [False, False, False], { maskNameRL: False, maskNameRJ: True, maskNameIL: True} )
1434 self.SPlotSensitivity.changeCurveVisibility(2, [True], [False], { maskNameGDL: True, maskNameGDU: True } )
1435
1436
1437 ##
1438 #
1439 # Update all plot windows and CM window if they exist.
1440 #
1441 def updateWindows(self):
1442 SP = self.SP
1443
1444 if SP is None:
1445 self.closeSPlotMagPhaseWindow()
1446 self.closeCMWindows()
1447 else:
1448 if self.SPlotMagPhase is not None and not self.SPlotMagPhase.closed:
1449 self.plotSParameters()
1450
1451 if self.SPlotError is not None and not self.SPlotError.closed:
1452 self.CMdialog.on_action_PlotS_triggered(True)
1453
1454 if self.CMdialog is not None:
1455 self.CMdialog.close()
1456 self.CMdialog = CouplingMatrixDlg(self.CM, self)
1457 self.CMdialog.show()
1458
1459 if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1460 self.CMdialog.on_action_PlotEnergy_triggered(True)
1461
1462 if self.SPlotSensitivity is not None and not self.SPlotSensitivity.closed:
1463 self.CMdialog.on_runSensitivity_pushButton_clicked(True)
1464
1465 if self.paramEdit is not None:
1466 self.paramEdit.P = self.P
1467 self.paramEdit.updateDialogParams()
1468
1469
1470 def addResultToList(self, resultArg):
1471 name = ''
1472 while len(name ) == 0:
1473 input = QInputDialog.getText (self , 'Result name', 'Give a name to current result:')
1474 if input[1] is False:
1475 return
1476 name = input[0]
1477 if len(name) == 0:
1478 QMessageBox.warning(self, "WARNING", 'Result name cannot be an empty string.')
1479
1480 # BE CAREFUL: Add new elements to the tuple AT THE END.
1481 # Elements will be accessed later as SP = result[3]
1482 result = copy.deepcopy( resultArg )
1483
1484 index = self.results_comboBox.findText(name, Qt.MatchExactly)
1485
1486 if index >= 0: # name exists in list
1487 message = "Result with same name already exists in list\nReplace?"
1488 reply = QMessageBox.question(self, "%s - Result exists" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
1489 if reply == QMessageBox.No: # Exit without doing anything
1490 return False
1491 elif reply == QMessageBox.Yes:
1492 self.listResults[index] = result
1493 myPrint("Result '%s' replaced in list" % name)
1494 else: # Append result to list
1495 self.listResults.append(result)
1496 self.noCleanup = True
1497 self.results_comboBox.addItem(name) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1498 index = self.results_comboBox.count() - 1
1499 self.results_comboBox.setCurrentIndex(index) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1500 self.noCleanup = False
1501 self.indexResults = index
1502 myPrint("Result '%s' appended to list" % name)
1503
1504
1505 ##@{
1506 # @name Functions automatically excuted when the user interacts with the GUI
1507
1508 ##
1509 #
1510 # Reimplementation of the window close function.
1511 #
1512 def closeEvent(self, event):
1513 # Check if there are unsaved changes and ask user what to do.
1514 if self.okToContinue():
1515 # Save window geometry and state
1516 self.settings = QSettings()
1517 self.settings.setValue("Geometry", QVariant(self.saveGeometry()))
1518 self.settings.setValue("State", QVariant(self.saveState()))
1519 # Save last open directory name
1520 self.settings.setValue('LastDirectoryOpened', QVariant(self.lastDir))
1521
1522 # Accept event -> close window
1523 event.accept()
1524 else:
1525 # Ignore event -> do not close window
1526 event.ignore()
1527
1528
1529 @pyqtSignature("bool")
1530 ##
1531 #
1532 # Show "HelpAbout"" dialog from help menu.
1533 # This function is automatically executed by the GUI when the user selects the menu Help/About.
1534 # The information shown is read from the file docstring.
1535 #
1536 def on_action_HelpAbout_triggered(self, checked):
1537 st = __doc__
1538 st = st.replace('@package', 'Package')
1539 st = st.replace('@author', 'Authors:')
1540 st = st.replace('@date', 'Date:')
1541 st = st.replace('@version', 'Version:')
1542
1543 st = u"""<b>Lossy Filters Synthesis</b>
1544 <p>%s
1545 <p>Python %s - Qt %s - PyQt %s - Numpy %s - Scipy %s - QWT %s on %s %s""" % (st, platform.python_version(),QT_VERSION_STR, PYQT_VERSION_STR,
1546 np.version.version, sc.version.version, Qwt.QWT_VERSION_STR, platform.system(), platform.release())
1547
1548 QMessageBox.about(self, "About %s" % applicationName, st)
1549
1550
1551 @pyqtSignature("bool")
1552 ##
1553 #
1554 # Show "HelpHelp"" dialog from help menu.
1555 # This function is automatically executed by the GUI when the user selects the menu Help/Help.
1556 # The information shown is read file Help/GUI_help.html.
1557 #
1558 def on_action_HelpHelp_triggered(self):
1559 form = HelpForm("GUI_help.html", self)
1560 form.show()
1561 #QMessageBox.about(self, "Help %s" % applicationName, "Hello, no help for you")
1562
1563
1564 @pyqtSignature("bool")
1565 ##
1566 #
1567 # Close application.
1568 # This function is automatically executed by the GUI when the user triggers the FileExit action.
1569 #
1570 def on_action_FileExit_triggered(self, checked):
1571 self.close()
1572
1573
1574 @pyqtSignature("bool")
1575 ##
1576 #
1577 # Execute synthesis computation.
1578 # This function is automatically executed by the GUI when the user triggers the Execute action.
1579 #
1580 def on_action_Execute_triggered(self, checked):
1581
1582 # Update self.P with values in the parameter editor, if it is open
1583 if self.paramEdit is not None and self.paramEdit.readDialogParams() == False:
1584 return # Do not compute with invalid parameters. The error message will be displayed by self.paramEdit.readDialogParams()
1585
1586 if self.P is not None and self.P.empty is False:
1587 self.updateStatus("\nFilter synthesis started")
1588 app.setOverrideCursor(Qt.WaitCursor)
1589 self.CP, self.CM, self.SP, self.computed = runSynthesis(self.P, self.FT, symmetrizeZeros)
1590 app.restoreOverrideCursor()
1591
1592 if self.computed:
1593 self.updateStatus("Filter synthesis finished\n")
1594
1595 # Plot [S] parameters
1596 self.plotSParameters()
1597
1598 # Reload coupling matrix window, if it was open.
1599 if self.CMdialog is not None:
1600 self.CMdialog.close()
1601 self.CMdialog = CouplingMatrixDlg(self.CM, self)
1602 self.CMdialog.show()
1603
1604 # Update SPlotError, if it was open.
1605 if self.SPlotError is not None and not self.SPlotError.closed:
1606 if self.CMdialog is not None:
1607 self.CMdialog.on_action_PlotS_triggered(True)
1608 else:
1609 self.SPlotError.close()
1610
1611 # Update EnergyPlot, if it was open.
1612 if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1613 if self.CMdialog is not None:
1614 self.CMdialog.on_action_PlotEnergy_triggered(True)
1615 else:
1616 self.EnergyPlot.close()
1617 else:
1618 self.updateStatus("Filter synthesis aborted")
1619
1620 else:
1621 self.updateStatus("There are no filter parameters loaded")
1622 QMessageBox.warning(self, "WARNING", 'There are no filter parameters loaded. Open a parameter file or create a new one.')
1623
1624 if self.paramEdit is not None:
1625 # Reload parameters into GUI widgets, they may have been modified by the computation
1626 self.paramEdit.updateDialogParams(autoFreqUnits = False)
1627 self.paramEdit.recomputed = True
1628
1629
1630 @pyqtSignature("bool")
1631 ##
1632 #
1633 # Opens dialog to display and rotate coupling matrices.
1634 # This function is automatically executed by the GUI when the user triggers the 'Coupling Matrix' action.
1635 #
1636 def on_action_CouplingMatrix_triggered(self, checked):
1637 # Open CouplingMatrix dialog
1638 if self.CM is not None:
1639 if self.CMdialog is None: self.CMdialog = CouplingMatrixDlg(self.CM, self)
1640 self.CMdialog.show()
1641 else:
1642 QMessageBox.warning(self, "WARNING", 'No available coupling matrix to display. Open a parameter file and run synthesis.')
1643
1644
1645 @pyqtSignature("bool")
1646 ##
1647 #
1648 # Opens dialog to edit parameters.
1649 # This function is automatically executed by the GUI when the user triggers the Edit action.
1650 #
1651 def on_action_Edit_triggered(self, checked):
1652 if self.paramEdit is not None:
1653 QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1654 else:
1655 # Open parameter editor dialog
1656 if self.P is not None:
1657 self.paramEdit = ParamEditDlg(self)
1658 self.paramEdit.show()
1659 self.paramEdit.raise_()
1660 #self.paramEdit.activateWindow()
1661 else:
1662 QMessageBox.warning(self, "WARNING", 'No open parameter file to edit. Open a parameter file or create a new one.')
1663
1664
1665 @pyqtSignature("bool")
1666 ##
1667 #
1668 # Save parameters to file.
1669 # This function is automatically executed by the GUI when the user triggers the FileSave action.
1670 #
1671 def on_action_FileSave_triggered(self, checked):
1672 if self.P is not None: # Check if there are parameters to save
1673 if self.fileName is not None: # Check if there is a filename
1674 # Open parameter file and write P on it. Use codecs module for transparent utf-8 encoding.
1675 fout = codecs.open(self.fileName, 'w', 'utf-8')
1676 fout.write( unicode(self.P) )
1677 fout.close()
1678 self.dirty = False
1679 self.setWindowModified(self.dirty)
1680 self.updateStatus("Parameters saved to file: %s" % self.fileName)
1681 else:
1682 self.on_action_FileSaveAs_triggered(False)
1683 else:
1684 QMessageBox.warning(self, "WARNING", "There are no parameters to save.")
1685
1686 @pyqtSignature("bool")
1687 ##
1688 #
1689 # Ask for a file name and save parameters to file.
1690 # This function is automatically executed by the GUI when the user triggers the FileSaveAs action.
1691 #
1692 def on_action_FileSaveAs_triggered(self, checked):
1693 if self.P is not None: # Check if there are parameters to save
1694
1695 # Set save directory to directory of current file
1696 dir = os.path.dirname(self.fileName) if self.fileName is not None else "."
1697
1698 # Ask for file name
1699 formats = ["*.par" ]
1700 fname = unicode(QFileDialog.getSaveFileName(self, "%s - Save Parameter File" % applicationName, dir, "Parameter files (%s)" % " ".join(formats)))
1701
1702 # If fname is valid, save parameters. Otherwise (File Save dialog cancelled by user) do nothing
1703 if fname:
1704 fbasename = os.path.basename(fname)
1705 if "." not in fbasename:
1706 fname += '.par'
1707 self.fileName = fname
1708 self.on_action_FileSave_triggered(False)
1709 else:
1710 QMessageBox.warning(self, "WARNING", "There are no parameters to save.")
1711
1712 @pyqtSignature("bool")
1713 ##
1714 #
1715 # Ask for a file name and load parameters from this file.
1716 # This function is automatically executed by the GUI when the user triggers the FileOpen action.
1717 #
1718 def on_action_FileOpen_triggered(self, checked):
1719 if self.paramEdit is not None:
1720 QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1721 else:
1722 # Check if there are unsaved changes
1723 if not self.okToContinue():
1724 return
1725
1726 # Cleaup results and plots
1727 self.cleanup()
1728
1729 # Set open directory to directory of current file or to lastDir if there is no open file
1730 dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1731
1732 # Open file
1733 formats = ["*.par *.dat" ]
1734 fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open Parameter File" % applicationName, dir, "Parameter files (%s)" % " ".join(formats)))
1735
1736 # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
1737 if fname: self.fileRead(fname)
1738
1739
1740 @pyqtSignature("bool")
1741 ##
1742 #
1743 # If checked: Ask for a file name and load specification mask from this file. Plot the mask.
1744 # If unchecked: Delete the mask data from memory and delete the mask curve from the plots.
1745 # This function is automatically executed by the GUI when the user triggers the Mask action.
1746 #
1747 def on_action_Mask_triggered(self, checked):
1748 if checked:
1749 # Set open directory to directory of current .par file or to lastDir if there is no open file
1750 dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1751
1752 # Open file
1753 formats = ["*.msk" ]
1754 fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open Mask Specification File" % applicationName, dir, "Mask spec files (%s)" % " ".join(formats)))
1755
1756 if fname:
1757 self.action_Mask.setChecked(True) # Necessary when this method is called by the program, not the user
1758 self.SM = SpecMask(fname, self)
1759
1760 # Plot mask if there is an open [S] plot window
1761 self.plotMasks()
1762
1763 else: # Unckeck toolbar button
1764 self.action_Mask.setChecked(False)
1765
1766
1767 else:
1768 self.action_Mask.setChecked(False) # Necessary when this method is called by the program, not the user
1769 self.SM = None
1770
1771 # Remove existing masks
1772 if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False:
1773 self.SPlotMagPhase.deleteMasks()
1774
1775 if self.SPlotError is not None and self.SPlotError.closed is False:
1776 self.SPlotError.deleteMasks()
1777
1778 if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False:
1779 self.SPlotSensitivity.deleteMasks()
1780
1781 # Restore default curve visibility
1782 if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False:
1783 self.SPlotMagPhase.changeCurveVisibility(0, [True, True, False], [True])
1784 self.SPlotMagPhase.changeCurveVisibility(1, [True, True, False], [True, False, False, False])
1785 self.SPlotMagPhase.changeCurveVisibility(2, [True], [True, False])
1786
1787 if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False:
1788 self.SPlotSensitivity.changeCurveVisibility(0, [True, True, False], [True])
1789 self.SPlotSensitivity.changeCurveVisibility(1, [True, True, False], [True, False, False])
1790 self.SPlotSensitivity.changeCurveVisibility(2, [True], [True])
1791
1792
1793 @pyqtSignature("bool")
1794 ##
1795 #
1796 # Ask for a file name and load [S] from this file in Touchstone format. Plot the [S] parameters in the results comparison window.
1797 # This function is automatically executed by the GUI when the user triggers the Sopen action.
1798 #
1799 def on_action_Sopen_triggered(self, checked):
1800
1801 # Set open directory to directory of current .par file or to lastDir if there is no open file
1802 dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1803
1804 # Open file
1805 formats = ["*.s2p" ]
1806 fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open [S] Touchstone File" % applicationName, dir, "Touchstone files (%s)" % " ".join(formats)))
1807
1808 # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
1809 if fname:
1810 Sts = Stouchstone(fname)
1811 self.addResultToList( (None, None, None, Sts, None) )
1812 self.on_action_Compare_triggered(True)
1813
1814
1815 @pyqtSignature("bool")
1816 ##
1817 #
1818 # Creates an empty instance of Parameters Class and opens dialog to edit parameters.
1819 # This function is automatically executed by the GUI when the user triggers the FileNew action.
1820 #
1821 def on_action_FileNew_triggered(self, checked):
1822 if self.paramEdit is not None:
1823 QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1824 else:
1825 # Check if there are unsaved changes
1826 if not self.okToContinue():
1827 return
1828
1829 # Cleaup results and plots
1830 self.cleanup()
1831
1832 self.P = Parameters(None)
1833 self.fileName = None
1834 self.updateStatus('New empty parameter set created')
1835 self.on_action_Edit_triggered(False)
1836
1837
1838 @pyqtSignature("bool")
1839 ##
1840 #
1841 # Store current result in self.results_comboBox list and self.listResults.
1842 # @param checked = SIGNAL/SLOT parameter.
1843 #
1844 def on_action_ListAdd_triggered(self, checked):
1845 if self.P is None:
1846 QMessageBox.warning(self, "WARNING", 'No result to store. Open a parameter file and run synthesis first.')
1847 return
1848
1849 if self.CP is None or self.CM is None or self.SP is None or self.FT is None:
1850 QMessageBox.warning(self, "WARNING", 'No result to store. Run synthesis first.')
1851 return
1852
1853 # If parameterEditDlg is open and not recomputed, compute with dialog parameters.
1854 if self.paramEdit is not None and self.paramEdit.dialogModified is True and self.paramEdit.recomputed is False:
1855 message = "Parameters have been modified and it is necessary to recompute\nRecompute synthesis?"
1856 reply = QMessageBox.question(self, "%s - Recompute" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
1857 if reply in [QMessageBox.Cancel, QMessageBox.No]:
1858 return
1859 elif reply == QMessageBox.Yes:
1860 self.paramEdit.on_compute_pushButton_clicked(True)
1861
1862 self.addResultToList( (self.P, self.CP, self.CM, self.SP, self.FT) )
1863
1864
1865 @pyqtSignature("bool")
1866 ##
1867 #
1868 # Remove result matrix from self.results_comboBox and self.listResults lists.
1869 #
1870 def on_action_ListRemove_triggered(self, checked):
1871 if self.results_comboBox.count() == 0:
1872 QMessageBox.warning(self, "WARNING", 'Results list is empty' )
1873 return
1874
1875 index = self.results_comboBox.currentIndex()
1876 name = str(self.results_comboBox.currentText())
1877
1878 #print 'Remove result %d: %s' % (index, name)
1879
1880 message = "Are you sure you what to remove result '%s' from list?\nRemove?" % name
1881 reply = QMessageBox.question(self, "%s - Remove result" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
1882
1883 if reply == QMessageBox.Yes:
1884 self.results_comboBox.removeItem(index)
1885 del self.listResults[index]
1886 myPrint("Result '%s' removed from list" % name)
1887
1888 if self.results_comboBox.count() > 0:
1889 # Load the result corresponding to the new index in CM_comboBox
1890 self.on_results_comboBox_currentIndexChanged(self.results_comboBox.currentIndex())
1891
1892 self.indexResults = self.results_comboBox.currentIndex()
1893
1894
1895 @pyqtSignature("int")
1896 ##
1897 #
1898 # Load a new result.
1899 # This function is automatically executed by the GUI when the user changes the results list comboBox.
1900 # It does nothing if self.noCleanup is True.
1901 #
1902 def on_results_comboBox_currentIndexChanged(self, index):
1903 # This method is automatically called when on_action_ListAdd_triggered() appends an item to the comboBox list, or when results_comboBox.setCurrentIndex() is called below.
1904 # self.noCleanup will be True. In that case we not want to do nohing.
1905 if self.noCleanup: return
1906
1907 # Get result
1908 index = self.results_comboBox.currentIndex()
1909 name = str(self.results_comboBox.currentText())
1910 result = self.listResults[index]
1911
1912 # Check if result has P, CP, CM, and FT. Otherwise it cannot be loaded.
1913 if not all([result[0], result[1], result[2], result[4] ]):
1914 QMessageBox.warning(self, "WARNING", """Result '%s' was read from [S] parameters Touchstone file and has no Characteristic Polynomials nor Coupling Matrices.<p>
1915 It cannot be loaded.""" % name)
1916 # Restore last index to comboBox
1917 self.noCleanup = True
1918 self.results_comboBox.setCurrentIndex(self.indexResults) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1919 self.noCleanup = False
1920 return
1921
1922 # Check if parameters need saving.
1923 if not self.okToContinue():
1924 QMessageBox.warning(self, "WARNING", 'User cancelled action.<p>Result has not been loaded from list.' )
1925 # Restore last index to comboBox
1926 self.noCleanup = True
1927 self.results_comboBox.setCurrentIndex(self.indexResults) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1928 self.noCleanup = False
1929 return
1930
1931 # Load results
1932 self.indexResults = index
1933
1934 # BE CAREFUL: Add new elements to the tuple AT THE END.
1935 # Elements will be accessed later as SP = result[3]
1936 (self.P, self.CP, self.CM, self.SP, self.FT) = copy.deepcopy( result )
1937
1938 # Update plots and edit windows if they are open.
1939 self.updateWindows()
1940
1941 myPrint("Result '%s' loaded from list" % name)
1942
1943 @pyqtSignature("bool")
1944 ##
1945 #
1946 # Compare results in list.
1947 # This function is automatically executed by the GUI when the user toggles the 'Compare results' button.
1948 #
1949 def on_action_Compare_triggered(self, checked):
1950 # Since this action does not update windows, it closes all windows first and, if checked, creates new windows.
1951 self.closeResultsWindow()
1952
1953 if len(self.listResults) == 0:
1954 QMessageBox.warning(self, "WARNING", 'Results list is empty. Nothing to plot.')
1955 self.action_Compare.setChecked(False)
1956 return
1957
1958 # Compute [S] parameters from current coupling matrix
1959 myPrint('Displayed results comparison:')
1960
1961 # BE CAREFUL if somebody has inserted new elements in tuple
1962 freq = [ result[3].freq / 1e9 for result in self.listResults ]
1963 dataS11 = [ 20*np.log10(np.abs(result[3].S11)) for result in self.listResults ]
1964 dataS21 = [ 20*np.log10(np.abs(result[3].S21)) for result in self.listResults ]
1965
1966 freqGD = [ result[3].freq2 / 1e9 for result in self.listResults ]
1967 dataGD = [ 1e9 * result[3].groupDelay2 for result in self.listResults ]
1968
1969 phaseS11 = [ result[3].phaseS11 * 180/np.pi for result in self.listResults ]
1970 phaseS21 = [ result[3].phaseS21 * 180/np.pi for result in self.listResults ]
1971
1972 resultNamesMag = [ unicode(self.results_comboBox.itemText(n) + ' (dB)') for n in range(self.results_comboBox.count()) ]
1973 resultNamesPhase = [ unicode(self.results_comboBox.itemText(n) + ' (phase)') for n in range(self.results_comboBox.count()) ]
1974 resultNamesGD = [ unicode(self.results_comboBox.itemText(n) + ' (GD)') for n in range(self.results_comboBox.count()) ]
1975 rightYVisible = [ False for n in range(self.results_comboBox.count())]
1976
1977 # Set dynamic range to the lowest value of S21 at sweep range ends.
1978 dynamicRange = 0
1979 for S21 in dataS21: dynamicRange = max(dynamicRange , -20*log10( min([ abs(S21[0]), abs(S21[-1] )])) )
1980 dynamicRange = 10*ceil(dynamicRange/10)
1981
1982 self.SPlotCompare = DbPlot(parent=self,
1983 windowTitle = '[S] comparison',
1984 tabTitle = 'S11',
1985 title = 'S<sub>11</sub> compared for different results',
1986 XData = freq, leftYData = dataS11,
1987 rightYData = phaseS11,
1988 rightYLabel = 'Phase(S<sub>11</sub>) (deg)' , rightYNames = resultNamesPhase, rightYVisible = rightYVisible,
1989 leftYmax = 0, leftYmin = -dynamicRange, leftYNames = resultNamesMag, XLabel = 'Frequency (GHz)', leftYLabel = 'dB',
1990 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg' )
1991
1992 self.SPlotCompare.addTab(tabTitle = 'S21',
1993 title = 'S<sub>21</sub> compared for different results',
1994 XData = freq, leftYData = dataS21,
1995 rightYData = phaseS21,
1996 rightYLabel = 'Phase(S<sub>21</sub>) (deg)' , rightYNames = resultNamesPhase, rightYVisible = rightYVisible,
1997 leftYmax = 0, leftYmin = -dynamicRange, leftYNames = resultNamesMag,
1998 XLabel = 'Frequency (GHz)', leftYLabel = 'dB',
1999 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg' )
2000
2001 self.SPlotCompare.addTab(tabTitle = 'Group delay',
2002 title = 'Group delay compared for different results',
2003 XData = freqGD, leftYData = dataGD,
2004 leftYNames = resultNamesGD, XLabel = 'Frequency (GHz)', leftYLabel = 'Group delay (ns.)',
2005 Xunits = 'GHz', leftYunits = 'ns' )
2006
2007
2008 ##@}
2009
2010 # End class: MainWindow #
2011 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2012
2013
2014 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2015 # Begin class: HelpForm
2016
2017 ##
2018 #
2019 # GUI dialog to display help
2020 #
2021 class HelpForm(QDialog):
2022
2023 ##
2024 #
2025 # Constructor: Creates dialog window to display html help file.
2026 # @param page = html file name, relative to the program folder.
2027 # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2028 #
2029 def __init__(self, page, parent=None):
2030 super(HelpForm, self).__init__(parent)
2031 self.setAttribute(Qt.WA_DeleteOnClose)
2032 self.setAttribute(Qt.WA_GroupLeader)
2033
2034 ##
2035 # Copy of the mainWindow class instance.
2036 # It is necessary to store a copy since it must be used to access actions and resources.
2037 self.mainWindow = parent
2038
2039 # These actions have been already created in Qt Designer
2040 # backAction = QAction(QIcon(":/back.png"), self.tr("&Back"), self)
2041 # homeAction = QAction(QIcon(":/home.png"), self.tr("&Home"), self)
2042 backAction = self.mainWindow.action_Back
2043 homeAction = self.mainWindow.action_Home
2044
2045 backAction.setShortcut(QKeySequence.Back)
2046 homeAction.setShortcut(self.tr("Home"))
2047 self.pageLabel = QLabel()
2048
2049 toolBar = QToolBar()
2050 toolBar.addAction(backAction)
2051 toolBar.addAction(homeAction)
2052 toolBar.addWidget(self.pageLabel)
2053 self.textBrowser = QTextBrowser()
2054
2055 layout = QVBoxLayout()
2056 layout.addWidget(toolBar)
2057 layout.addWidget(self.textBrowser, 1)
2058 self.setLayout(layout)
2059
2060 self.connect(backAction, SIGNAL("triggered()"), self.textBrowser, SLOT("backward()"))
2061 self.connect(homeAction, SIGNAL("triggered()"), self.textBrowser, SLOT("home()"))
2062 self.connect(self.textBrowser, SIGNAL("sourceChanged(QUrl)"), self.updatePageTitle)
2063
2064 self.textBrowser.setSearchPaths([":/help/Help"]) # Path ":/" is the resource file root, path "./" is directory where the application was called
2065 self.textBrowser.setSource(QUrl(page))
2066 self.resize(1024, 700)
2067 self.setWindowTitle(self.tr("%1 Help").arg(QApplication.applicationName()))
2068
2069
2070 def updatePageTitle(self):
2071 self.pageLabel.setText(self.textBrowser.documentTitle())
2072
2073 # End class: HelpForm
2074 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2075
2076 ##
2077 #
2078 # Dialog to set the type of a coupling.
2079 #
2080 class SetCouplingDlg(QDialog):
2081
2082 ##
2083 #
2084 # Create dialog to set the type of a coupling.
2085 # @param item = Topology table item to modify
2086 # @param topolText = Topology table text, corresponding to ComboBox items
2087 # @param topolColors = Topology table colors, corresponding to ComboBox items
2088 # @param parent = parent widget (topology table)
2089 #
2090 def __init__(self, item, topolText, topolColors, parent=None):
2091 super(SetCouplingDlg, self).__init__(parent)
2092 self.setAttribute(Qt.WA_DeleteOnClose)
2093
2094 ##
2095 # Topology table widget
2096 self.mainWindow = parent
2097
2098 ##
2099 # Topology table item to set
2100 self.item = item
2101
2102 ##
2103 # ComboBox item text for different kinds of coupling
2104 self.typeLabels = ['Uncoupled', 'Real', 'Real+', 'Real-', 'Imag+', 'Complex', 'Complex+', 'Complex-']
2105
2106 ##
2107 # Topology table text, corresponding to ComboBox items
2108 self.topolText = topolText
2109
2110 ##
2111 # Topology table color, corresponding to ComboBox items
2112 self.topolColors = topolColors
2113
2114 self.label = QLabel('Type of coupling:')
2115 self.comboBox = QComboBox(self)
2116 self.comboBox.addItems(self.typeLabels)
2117
2118 # Set table item value as comboBox current item
2119 entriesDict = dict(zip(self.topolText, self.typeLabels))
2120 self.comboBox.setCurrentIndex( self.comboBox.findText( entriesDict[ str(item.text()) ] ) )
2121
2122 layout = QVBoxLayout(self)
2123 layout.addWidget(self.label)
2124 layout.addWidget(self.comboBox)
2125
2126 self.setWindowTitle("Cross couplings")
2127 self.setToolTip("""Choose the type of cross coupling:
2128 The real part is a conventional (inductive or capacitive)
2129 coupling and the imaginary part a resistive coupling.
2130 The imag part will always be positive (passive circuit),
2131 while the real part can forced to positive, negative or
2132 have any sign.""")
2133
2134 self.connect(self.comboBox, SIGNAL("currentIndexChanged(int)"), self.typeSelected)
2135
2136
2137 ##
2138 #
2139 # Set type selected in self.comboBox to self.item
2140 #
2141 def typeSelected(self, index):
2142 row = self.mainWindow.topology_tableWidget.currentRow()
2143 col = self.mainWindow.topology_tableWidget.currentColumn()
2144
2145 if row != col: # Do not change main diagonal
2146 itemReciprocal = self.mainWindow.topology_tableWidget.item(col, row)
2147 for it in [self.item, itemReciprocal]:
2148 it.setText(self.topolText[index])
2149 it.setBackground(QBrush(QColor(self.topolColors[index])))
2150
2151 self.close()
2152
2153
2154 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2155 # Begin class: MatrixEditDlg
2156
2157 ##
2158 #
2159 # GUI dialog to edit coupling matrix
2160 #
2161 class MatrixEditDlg(QDialog, Ui_matrixeditdlg.Ui_MatrixEditDlg):
2162
2163 ##
2164 #
2165 # Constructor: Creates dialog window to edit current coupling matrix.
2166 # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2167 #
2168 def __init__(self, parent):
2169 super(MatrixEditDlg, self).__init__(parent)
2170 # Caution: do not set Qt.WA_DeleteOnClose attribute. The class instance must not be deleted when closing the dialog.
2171
2172 ##
2173 # Copy of the coupling matrix mainWindow class instance.
2174 # It is necessary to store a copy since it must be accessed by some member functions.
2175 self.mainWindow = parent
2176
2177 self.setupUi(self)
2178
2179 ##
2180 # String that explains the last edit action. This is what is pushed into the edit action list after edit actions.
2181 self.lastEditAction = ''
2182
2183 ##
2184 # Shallow copy of the CM.bkpMatQ list, for easier access.
2185 self.bkpMatQ = self.mainWindow.CM.bkpMatQ
2186
2187 ##
2188 # Shallow copy of the CM.bkpActionslist, for easier access.
2189 self.bkpActions = self.mainWindow.CM.bkpActions
2190
2191 # Initialize edit list with previous list of edit actions
2192 assert self.edit_listWidget.count() == 0 # List should be empty here. We check it because list is not destructed on close.
2193 self.edit_listWidget.insertItems(0, self.bkpActions)
2194
2195 ##
2196 # Variable to store a list of widgets with bad input.
2197 # Only when this list is empty, he "Accept" and "Compute" buttons are enabled.
2198 self.badInput = [ ]
2199
2200 # Set maximum row and column indices in matrixEditDlg spinBoxes
2201 self.setMatrixSize()
2202
2203 # Prepare rotation angle lineEdit. Initial value equal to zero and cannot be empty.
2204 self.rot_angle_lineEdit.setText('0')
2205 self.rot_angle_lineEdit.setValidator( QRegExpValidator( QRegExp(stComplex), self) )
2206
2207 # Prepare optimization Q. Initial value equal to zero and cannot be empty.
2208 self.Q_doubleSpinBox.setValue( np.mean(np.abs(self.mainWindow.CM.MatQ.Q)) )
2209
2210 # Prepare Qeff for prescribed flatness
2211 self.flatnessQeff_lineEdit.setValidator( QRegExpValidator( reRealPositiveInf, self) )
2212
2213 # Optimization comboBox and stackedWidget
2214 optimMethod = 0
2215 self.optim_comboBox.setCurrentIndex(optimMethod)
2216 self.optim_stackedWidget.setCurrentIndex(optimMethod)
2217
2218 # Matrix topology table
2219 self.topology_tableWidget = myTableWidget(name = "topology", parent = self.optim_page)
2220 self.topology_tableWidget.setColumnCount(0)
2221 self.topology_tableWidget.setRowCount(0)
2222
2223 self.topology_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
2224 self.topology_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
2225
2226 self.topology_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
2227
2228 self.topology_tableWidget.setToolTip("""<b>Coupling matrix topology: </b>
2229 <p>
2230 Define here the position of purely real, purely imaginary
2231 or complex non-diagonal coupling matrix entries.
2232 <p>
2233 Diagonal entries corresponding to resonant nodes are always complex, with imaginary
2234 part determined by finite Q of resonators and real part a degree of freedom.
2235 <p>
2236 Diagonal entries corresponding to non-resonant nodes are always purely imaginary negative.
2237 <p>
2238 Real part of non-diagonal entries can be forced to be positive (R+ or C+)
2239 or negative (R- or C-), to have a electric or a magnetic coupling.
2240 <p>
2241 Imaginary part of complex or purely imaginary non-diagonal entries is always positive
2242 to have a passive circuit.
2243 """)
2244
2245 self.verticalLayout_13.addWidget(self.topology_tableWidget, 100) # stretchFactor=100
2246 self.verticalLayout_13.addStretch()
2247
2248 # Make topology table as large as possible
2249 self.verticalLayout_13.setStretchFactor(self.topology_tableWidget, 100)
2250
2251 # Make edit toolBox as large as possible, to fit the topology table
2252 self.verticalLayout_12.setStretchFactor(self.edit_toolBox, 10)
2253
2254 # Connect slot
2255 self.connect(self.topology_tableWidget, SIGNAL("itemClicked(QTableWidgetItem*)"), self.topologyItemClicked)
2256
2257 ##
2258 # Text corresponding to couplings type in topology table
2259 self.topolText = [ '0', 'R', 'R+', 'R-', 'I+', 'C', 'C+', 'C-', '']
2260
2261 ##
2262 # Colors corresponding to couplings type in topology table
2263 self.topolColors = [ Qt.white, Qt.blue, Qt.green, Qt.cyan, Qt.yellow, Qt.darkRed, Qt.red, Qt.magenta, Qt.black ]
2264
2265 # Fill in Matrix topology table
2266 self.udpateTopology()
2267
2268 ##
2269 # Variable to store the modified/unmodifed status of the dialog widgets.
2270 # The dialogChanged() method is automatically called when the user modifies the edit widgets contents and sets this variable to True.
2271 # This variable must be set to False at the end of the __init__ dialog constructor,
2272 # since dialogChanged() is called when the constructor sets the edit widgets contents.
2273 # This attribute is defined in this class only for compatibility with the dialogChanged() method,
2274 # which has been copied literally (except for the name of pushButtons) from ParamEditDlg class.
2275 self.dialogModified = False
2276
2277
2278 ##
2279 #
2280 # Fill in Matrix topology table from values in self.mainWindow.CM.MatQ
2281 #
2282 def udpateTopology(self):
2283 table = self.topology_tableWidget
2284 labels = self.mainWindow.rowIndices
2285
2286 M = self.mainWindow.CM.MatQ.M
2287
2288 size = len(labels)
2289 table.setColumnCount(size)
2290 table.setRowCount(size)
2291 table.setHorizontalHeaderLabels( labels )
2292 table.setVerticalHeaderLabels( labels )
2293
2294 # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
2295 # eps = self.mainWindow.CM.matrixElementsEps('Matrix topology')
2296 eps = 10** -self.mainWindow.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
2297
2298 for m in range(size):
2299 for n in range(size):
2300 if n != m:
2301 if np.abs(M[m, n].real) < eps:
2302 if np.abs(M[m, n].imag) < eps: type = 0 # Uncoupled
2303 else: type = 4 # Imaginary (positive)
2304 else:
2305 if np.abs(M[m, n].imag) < eps:
2306 if M[m, n].real > 0: type = 2 # Real positive
2307 else: type = 3 # Real negative
2308 else:
2309 if M[m, n].real > 0: type = 6 # Complex positive
2310 else: type = 7 # Complex negative
2311 else: type = 8 # Main diagonal
2312
2313 st = self.topolText[type]
2314 brush = self.topolColors[type]
2315 item = QTableWidgetItem(st)
2316 item.setTextAlignment(Qt.AlignHCenter|Qt.AlignVCenter)
2317 item.setBackground(brush)
2318
2319 # # This is set for the whole table in MatrixEditDlg.__init__()
2320 # if n != m:
2321 # item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable)
2322 # else:
2323 # item.setFlags( Qt.NoItemFlags )
2324
2325 table.setItem(m, n, item)
2326
2327 table.resizeRowsToContents()
2328 table.resizeColumnsToContents()
2329 table.setMaximumSize( table.maximumSizeHint() )
2330
2331
2332 ##
2333 #
2334 # Return matrices of flags that describe the topology of the real part and the imaginary part of the coupling matrix,
2335 # based on the entries in the topology table.
2336 # @return topologyReal = Numpy matrix of Bool type that has True values for elements with non-zero real part.
2337 # @return topologyImag = Numpy matrix of Bool type that has True values for elements with non-zero imag part.
2338 # @return topologySignReal = Int numpy array having -1,1 or 0 values at the position of coupling matrix entries with negative, positive or any real part, respectively.
2339 # @return topologySignImag = Int numpy array having -1,1 or 0 values at the position of coupling matrix entries with negative, positive or any imaginary part, respectively.
2340 #
2341 def getTopologyFlags(self):
2342 table = self.topology_tableWidget
2343 Nrows = table.rowCount()
2344 Ncols = table.columnCount()
2345 assert Nrows == Ncols
2346
2347 topologyReal = np.zeros( (Nrows,Ncols), dtype = bool)
2348 topologyImag = np.zeros( (Nrows,Ncols), dtype = bool)
2349 topologySignReal = np.zeros((Nrows,Ncols), dtype='int') # Int numpy array having -1,1 or 0 values at the position of coupling matrix entries that must be negative, positive or any, respectively.
2350 topologySignImag = np.zeros((Nrows,Ncols), dtype='int') # Int numpy array having -1,1 or 0 values at the position of coupling matrix entries that must be negative, positive or any, respectively.
2351
2352 for m in range(Nrows):
2353 for n in range(Ncols):
2354 if n==m: continue # Diagonal elements are not set in the GUI
2355 st = str(table.item(m, n).text())
2356 type = self.topolText.index(st)
2357 topologyReal[m, n] = type in [1, 2, 3, 5, 6, 7]
2358 topologyImag[m, n] = type in [4, 5, 6, 7]
2359 if topologyImag[m, n]: topologySignImag[m, n] = 1 # All non-diagonal imaginary parts must be positive to have a passive filter.
2360 if type in [2, 6]: topologySignReal[m, n] = 1 # Positive real part
2361 elif type in [3, 7]: topologySignReal[m, n] = -1 # Negative real part
2362 elif type in [1, 5]: topologySignReal[m, n] = 0 # Any real part
2363 else: topologySignReal[m, n] = 0 # Zero real part
2364
2365 return topologyReal, topologyImag, topologySignReal, topologySignImag
2366
2367 ##
2368 #
2369 # Update the contents of self.topology_tableWidget after the user has clicked an item.
2370 #
2371 def topologyItemClicked(self, item):
2372 row = self.topology_tableWidget.currentRow()
2373 col = self.topology_tableWidget.currentColumn()
2374 self.topology_tableWidget.setFocus(Qt.MouseFocusReason)
2375
2376 if row != col: # Do not change main diagonal
2377 couplingsDlg = SetCouplingDlg(item, self.topolText, self.topolColors, self)
2378 couplingsDlg.exec_()
2379
2380
2381 ##
2382 #
2383 # Set maximum row and column indices in matrixEditDlg spinBoxes, according to size of self.mainWindow.CM.MatQ.M.
2384 #
2385 def setMatrixSize(self):
2386 # Set row and column indices comboBoxes
2387 (M, N) = self.mainWindow.CM.MatQ.M.shape
2388
2389 ind = self.rot_pivotM_comboBox.currentIndex()
2390 self.rot_pivotM_comboBox.clear()
2391 self.rot_pivotM_comboBox.addItems(self.mainWindow.rowIndices)
2392 self.rot_pivotM_comboBox.setCurrentIndex(ind)
2393
2394 ind = self.rot_pivotN_comboBox.currentIndex()
2395 self.rot_pivotN_comboBox.clear()
2396 self.rot_pivotN_comboBox.addItems(self.mainWindow.rowIndices)
2397 self.rot_pivotN_comboBox.setCurrentIndex(ind)
2398
2399 ind = self.ann_pivotM_comboBox.currentIndex()
2400 self.ann_pivotM_comboBox.clear()
2401 self.ann_pivotM_comboBox.addItems(self.mainWindow.rowIndices)
2402 self.ann_pivotM_comboBox.setCurrentIndex(ind)
2403
2404 ind = self.ann_pivotN_comboBox.currentIndex()
2405 self.ann_pivotN_comboBox.clear()
2406 self.ann_pivotN_comboBox.addItems(self.mainWindow.rowIndices)
2407 self.ann_pivotN_comboBox.setCurrentIndex(ind)
2408
2409 ind = self.ann_elemM_comboBox.currentIndex()
2410 self.ann_elemM_comboBox.clear()
2411 self.ann_elemM_comboBox.addItems(self.mainWindow.rowIndices)
2412 self.ann_elemM_comboBox.setCurrentIndex(ind)
2413
2414 ind = self.ann_elemN_comboBox.currentIndex()
2415 self.ann_elemN_comboBox.clear()
2416 self.ann_elemN_comboBox.addItems(self.mainWindow.rowIndices)
2417 self.ann_elemN_comboBox.setCurrentIndex(ind)
2418
2419 ind = self.scale_node_comboBox.currentIndex()
2420 self.scale_node_comboBox.clear()
2421 self.scale_node_comboBox.addItems(self.mainWindow.rowIndices)
2422 self.scale_node_comboBox.setCurrentIndex(ind)
2423 ##@{
2424 # @name Functions automatically excuted when the user interacts with the GUI
2425
2426
2427 ##
2428 #
2429 # Custom slot in Qt designer connected to widgets "changed" signal.
2430 # This function is automatically executed by the GUI every time the user changes dialog values,
2431 # and sets the self.dialogModified variable to True.
2432 #
2433 def dialogChanged(self):
2434 self.dialogModified = True
2435 if hasattr(self.sender(), 'validator'):
2436 validator = self.sender().validator()
2437 if validator is not None:
2438 result = validator.validate(self.sender().text(), 0)[0]
2439 palette = self.sender().palette()
2440 if result == 1:
2441 palette.setColor(QPalette.Text, Qt.red)
2442 self.sender().setPalette(palette)
2443 self.badInput.append(self.sender())
2444 self.sender().setFocus(Qt.OtherFocusReason) # It does nothing, because it is executed before focus changes to another widget.
2445 if result == 2:
2446 palette.setColor(QPalette.Text, self.mainWindow.palette().text().color())
2447 self.sender().setPalette(palette)
2448 while self.sender() in self.badInput: self.badInput.remove(self.sender())
2449
2450 # For debugging
2451 #print [ unicode(n.text()) for n in self.badInput]
2452
2453 if len(self.badInput) > 0:
2454 self.apply_pushButton.setEnabled(False)
2455 self.undo_pushButton.setEnabled(False)
2456 else:
2457 self.apply_pushButton.setEnabled(True)
2458 self.undo_pushButton.setEnabled(True)
2459
2460 @pyqtSignature("QListWidgetItem*")
2461 ##
2462 #
2463 # Load coupling matrix saved in backup list
2464 #
2465 def on_edit_listWidget_itemDoubleClicked(self, item):
2466 st = item.text()
2467 index = self.edit_listWidget.row(item)
2468 self.mainWindow.CM.MatQ = self.bkpMatQ[index+1].copy() # Deep copy because MatQ will be modified by edit actions. The first matrix in list was loaded before any edit actions.
2469 self.mainWindow.loadMatrixQ(self.mainWindow.CM.MatQ)
2470 self.lastEditAction = "Matrix reverted to backup state: '%s'" % st
2471 self.mainWindow.pushEditAction()
2472
2473
2474 @pyqtSignature("bool")
2475 ##
2476 #
2477 # Undo last edit action from edit actions list. Pops the edit action list and the backup Qmatrices list.
2478 #
2479 def on_undo_pushButton_clicked(self, checked):
2480 # Number of edit actions saved
2481 count = self.edit_listWidget.count()
2482 assert count == len( self.bkpMatQ)-1
2483
2484 if count == 0:
2485 QMessageBox.warning(self, "WARNING", "There are no edit actions to undo.")
2486 else:
2487 badMatQ = self.bkpMatQ.pop() # Discard last modified matrix from the backup list
2488 del badMatQ # Just in case garbage collection fails.
2489 MatQ = self.bkpMatQ[-1].copy() # Get the last valid matrix. Deep copy because MatQ will be modified by edit actions.
2490 self.mainWindow.CM.MatQ = MatQ # Update the CM.MatQ attribute with reference to the new MatQ.
2491 self.mainWindow.loadMatrixQ(MatQ) # Load the new MatQ in table widgets.
2492 item = self.edit_listWidget.takeItem(count-1) # Remove last edit action from edit actions list.
2493 self.bkpActions.pop()
2494 myPrint("Undo: Matrix Edit action '%s'" % item.text())
2495 del item # According to Qt4 doc, taken items must be deleted manually.
2496
2497
2498 @pyqtSignature("bool")
2499 ##
2500 #
2501 # Clear edit actions list. Clears edit action list and the backup Qmatrices list.
2502 #
2503 def on_clear_pushButton_clicked(self, checked):
2504
2505 message = "Do you really want to clear the action list for undo?"
2506 reply = QMessageBox.question(self, "%s - Clear actions list" % applicationName, message, QMessageBox.Yes | QMessageBox.Cancel)
2507 if reply == QMessageBox.Cancel:
2508 return
2509
2510 self.mainWindow.CM.bkpMatQ = [ self.mainWindow.CM.MatQ.copy() ]
2511 self.mainWindow.CM.bkpActions = [ ]
2512
2513 self.bkpMatQ = self.mainWindow.CM.bkpMatQ
2514 self.bkpActions = self.mainWindow.CM.bkpActions
2515
2516 # Remove all list items
2517 while self.edit_listWidget.count() > 0:
2518 item = self.edit_listWidget.takeItem(0)
2519 del item
2520
2521
2522 @pyqtSignature("bool")
2523 ##
2524 #
2525 # Apply selected action to the matrix.
2526 # The coupling matrix self.dirty attribute is set to True.
2527 #
2528 def on_apply_pushButton_clicked(self, checked):
2529 MatQ = self.mainWindow.CM.MatQ
2530
2531 try:
2532 action = unicode(self.edit_toolBox.itemText(self.edit_toolBox.currentIndex()))
2533 if action == u'Rotate matrix':
2534 if self.rot_trigo_radioButton.isChecked(): rotationType = 'trigonometric'
2535 elif self.rot_hyper_radioButton.isChecked(): rotationType = 'hyperbolic'
2536 else: raise synthError, "No rotation type selected"
2537 i = self.rot_pivotM_comboBox.currentIndex()
2538 j = self.rot_pivotN_comboBox.currentIndex()
2539 rotAng = complex( str(self.rot_angle_lineEdit.text()).replace(' ', '').replace('i', 'j') ) *pi/180.0 # Rotate angle is in deg in the GUI
2540 # (M, N) = MatQ.M.shape
2541 # if i>=M or j>=N: raise synthError, 'Pivot indices (%d,%d) larger than matrix size' % (i, j)
2542 MatQ.rotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2543 # MatQ.mpRotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2544 self.lastEditAction = '%s matrix rotation with pivot (%s,%s) and angle %g %+gj deg' % (rotationType.title(), self.mainWindow.rowIndices[i], self.mainWindow.rowIndices[j], rotAng.real *180/pi, rotAng.imag*180/pi ) # Rotate angle is in deg in the GUI
2545
2546 elif action == 'Annihilate element':
2547 if self.ann_trigo_radioButton.isChecked(): rotationType = 'trigonometric'
2548 elif self.ann_hyper_radioButton.isChecked(): rotationType = 'hyperbolic'
2549 else: raise synthError, "No rotation type selected"
2550 i = self.ann_pivotM_comboBox.currentIndex()
2551 j = self.ann_pivotN_comboBox.currentIndex()
2552 m = self.ann_elemM_comboBox.currentIndex()
2553 n = self.ann_elemN_comboBox.currentIndex()
2554 # (M, N) = MatQ.M.shape
2555 # if i>=M or j>=N: raise synthError, 'Pivot indices (%d,%d) larger than matrix size' % (i, j)
2556 # if m>=M or n>=N: raise synthError, 'Element indices (%d,%d) larger than matrix size' % (i, j)
2557 rotAng = MatQ.rotAngleEliminate(rotationType, i, j, m, n)
2558 MatQ.rotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2559
2560 self.lastEditAction = '%s matrix rotation with pivot (%s,%s) and angle %g %+gj deg to annihilate element (%s,%s)' % (rotationType.title(), self.mainWindow.rowIndices[i], self.mainWindow.rowIndices[j], rotAng.real *180/pi, rotAng.imag*180/pi, self.mainWindow.rowIndices[m], self.mainWindow.rowIndices[n]) # Rotate angle is in deg in the GUI
2561
2562 elif action == 'Add nodes':
2563 if self.add_source_radioButton.isChecked():
2564 position = 'source'
2565 self.lastEditAction = 'Node added at source side'
2566 elif self.add_load_radioButton.isChecked():
2567 position = 'load'
2568 self.lastEditAction = 'Node added at load side'
2569 elif self.add_both_radioButton.isChecked():
2570 position = 'both'
2571 self.lastEditAction = 'Nodes added at both sides'
2572 else: raise synthError, "Add nodes: No new node position selected"
2573 MatQ.inflateMatrix(position)
2574
2575 elif action == 'Node scaling':
2576 factor = self.scale_factor_doubleSpinBox.value()
2577 node = self.scale_node_comboBox.currentIndex()
2578 MatQ.scaleNode(node, factor)
2579 self.lastEditAction = 'Node [%s] scaled with factor %g' % (self.mainWindow.rowIndices[node], factor)
2580
2581 elif action == 'Prescribed flatness':
2582 Qeff = float(self.flatnessQeff_lineEdit.text())
2583 MatQ.addLossesQeff(Qeff)
2584 self.lastEditAction = 'Prescribed flatness with Qeff = %g' % (Qeff)
2585
2586 elif action == 'Optimization':
2587
2588 Q = self.Q_doubleSpinBox.value()
2589 topologyReal, topologyImag, topologySignReal, topologySignImag = self.getTopologyFlags()
2590
2591 algor = self.algor_comboBox.currentText()
2592 maxIter = self.maxIter_spinBox.value()
2593 Nsamples = self.Nsamples_spinBox.value()
2594 weights = [ self.wS11_doubleSpinBox.value(), self.wS22_doubleSpinBox.value(), self.wS21_doubleSpinBox.value(), self.wGD_doubleSpinBox.value() ]
2595
2596 optimMethod = unicode(self.optim_comboBox.currentText())
2597 if optimMethod == u'Characteristic Polynomials':
2598 S11DR = self.S11DR_doubleSpinBox.value()
2599 S21DR = self.S21DR_doubleSpinBox.value()
2600 app.setOverrideCursor(Qt.WaitCursor)
2601 MatQ.uniformQ(self.mainWindow.mainWindow.P, self.mainWindow.mainWindow.CP, weights, Q, algor, maxIter, Nsamples, S11DR, S21DR, topologyReal, topologyImag, topologySignReal, topologySignImag)
2602 app.restoreOverrideCursor()
2603 self.lastEditAction = 'Matrix optimization (CP) with parameters: alg=%s, maxIter=%d, Nsamples=%d, Q=%g, w=%s, N=%d, S11DR=%g, S21DR=%g' % (algor, maxIter, Nsamples, Q, listStr(weights, 2), Nsamples, S11DR, S21DR)
2604
2605 elif optimMethod == u'Specification mask':
2606 SM = self.mainWindow.mainWindow.SM
2607
2608 if SM is None:
2609 reply = QMessageBox.question(self, "WARNING", 'No specification mask loaded.<p>Do you want to open an specification mask file now?', QMessageBox.Yes| QMessageBox.Cancel)
2610 if reply == QMessageBox.Yes:
2611 self.mainWindow.mainWindow.on_action_Mask_triggered(True)
2612 SM = self.mainWindow.mainWindow.SM
2613
2614 if SM is not None:
2615 app.setOverrideCursor(Qt.WaitCursor)
2616 MatQ.uniformQMask(self.mainWindow.mainWindow.P, self.mainWindow.mainWindow.CP, weights, Q, algor, maxIter, Nsamples, SM, topologyReal, topologyImag, topologySignReal, topologySignImag)
2617 app.restoreOverrideCursor()
2618 self.lastEditAction = 'Matrix optimization (mask) with parameters: alg=%s, maxIter=%d, Nsamples=%d, Q=%g, w=%s, mask=%s' % (algor, maxIter, Nsamples, Q, listStr(weights, 2), SM.fileName)
2619 else:
2620 myPrint('Matrix optimization (mask) cancelled by user')
2621 return # Matrix edit action has been canceled and we do not want to run the code below
2622
2623 else:
2624 QMessageBox.warning(self, "WARNING", 'Unknown optimization method: %s' % optimMethod)
2625
2626
2627
2628 else:
2629 raise synthError, "Unknown matrix edit action '%s'" % action
2630
2631 MatQ.Qresonators()
2632 self.mainWindow.loadMatrixQ(MatQ)
2633 self.mainWindow.pushEditAction()
2634 self.mainWindow.dirty = True
2635 self.mainWindow.setWindowModified(self.mainWindow.dirty)
2636
2637 except synthError, err:
2638 app.restoreOverrideCursor()
2639 QMessageBox.critical(self, "ERROR", '<p align="center">Matrix editor error:<br>%s</p>' % err )
2640 return
2641
2642
2643 ##
2644 #
2645 # Reimplementation of the window close function.
2646 # Sets couplingMatrixDlg toolbar Edit action to unchecked.
2647 #
2648 def closeEvent(self, event):
2649 self.mainWindow.action_Edit.setChecked(False)
2650 event.accept()
2651
2652 ##@}
2653
2654
2655 # End class: MatrixEditDlg
2656 #!!!!!!!!!!!!!!!!!!!!!!!!!#
2657
2658
2659 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2660 # Begin class: PredisZeorsDlg
2661
2662 ##
2663 #
2664 # Dialog to select predistortion zeros.
2665 #
2666 class PredisZeorsDlg(QDialog):
2667
2668 ##
2669 #
2670 # Create dialog to select visible curves.
2671 # @roots_NRP = Roots of F(s)F(-s) that are in the left-hand s-plane
2672 # @zerosArrang = Predistiortion zeros arrangement (list of bool).
2673 # @param parent = Parent window (default None).
2674 #
2675 def __init__(self, roots_NRP, zerosArrang, parent=None):
2676 super(PredisZeorsDlg, self).__init__(parent)
2677
2678 ##
2679 # Handle to the dbplot main window.
2680 self.mainWindow = parent
2681
2682 self.zerosArrang = zerosArrang
2683
2684 layV = QVBoxLayout(self) # Vertical layout containing everything
2685 title = QLabel("<b>Predistortion synthesis:</b>")
2686 title.setAlignment(Qt.AlignHCenter)
2687 layV.addWidget(title)
2688 layV.addWidget(QLabel("Select zeros of F(s)F(-s) in the"))
2689 layV.addWidget(QLabel("left s-plane that will go to F(s)"))
2690
2691 layH1 = QHBoxLayout() # Horizontal layout containing grid and stretch
2692 grid = QGridLayout()
2693
2694 for n in range(self.mainWindow.P.N):
2695 grid.addWidget(QLabel(complexStr(roots_NRP[n], 3)), n, 0, Qt.AlignRight)
2696 checkBox = QCheckBox()
2697 checkBox.setChecked(self.zerosArrang[n])
2698 # self.connect(checkBox, SIGNAL("stateChanged(int)"), lambda value: self.setZeroToFs(n, bool(value)))
2699 self.connect(checkBox, SIGNAL("stateChanged(int)"), partial(self.setZeroToFs, n) )
2700 grid.addWidget(checkBox, n, 1, Qt.AlignHCenter)
2701
2702 layH1.addLayout(grid)
2703 layH1.addStretch()
2704
2705 layV.addLayout(layH1)
2706 layV.addStretch()
2707
2708 #buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
2709 buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
2710 buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
2711 self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
2712 self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
2713
2714 layV.addWidget(buttonBox)
2715
2716 self.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
2717 self.setWindowTitle("Zeros F(s)")
2718 self.setToolTip("""Select predistiortion zeros of F(s)F(-s) in the left-hand s-plane that will go to F(s).
2719 If they are not selected, they will go to F(-s).
2720 The zeros of F(s)F(-s) in the right-hand s-plane will go to the opposite polynomial.""")
2721
2722 def setZeroToFs(self, n, zeroToFs):
2723 self.zerosArrang[n] = bool(zeroToFs)
2724
2725 # End class: PredisZeorsDlg
2726 #!!!!!!!!!!!!!!!!!!!!!!!!!#
2727
2728
2729 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2730 # Begin class: SensitivitySPar
2731
2732 ##
2733 #
2734 # Class for sensitivity analysis.
2735 #
2736 class SensitivityAnalysis(object):
2737
2738 ##
2739 #
2740 # Constructor: creates SensitivityAnalisys class instance.
2741 # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2742 #
2743 def __init__(self, parent):
2744 ##
2745 # Copy of the coupling matrix mainWindow class instance.
2746 # It is necessary to store a copy since it must be accessed by some member functions.
2747 self.mainWindow = parent
2748
2749 ##
2750 # QwtPlot that contains the Magnitude and Phase S-parameter plot for all random realizations.
2751 self.SPlotMagPhase = None
2752
2753 ##
2754 # Numpy array with columns containing S11 for each random realization.
2755 self.S11M = None
2756
2757 ##
2758 # Numpy array with columns containing S21 for each random realization.
2759 self.S21M = None
2760
2761 ##
2762 # Numpy array with columns containing S22 for each random realization.
2763 self.S22M = None
2764
2765 ##
2766 # Numpy array with columns containing phase of S11 for each random realization.
2767 self.phaseS11M = None
2768
2769 ##
2770 # Numpy array with columns containing phase of S21 for each random realization.
2771 self.phaseS21M = None
2772
2773 ##
2774 # Numpy array with columns containing phase of S22 for each random realization.
2775 self.phaseS22M = None
2776
2777 ##
2778 # Numpy array with columns containing groupDelay for each random realization.
2779 self.groupDelayM = None
2780
2781 ##
2782 # [S] parameters class instance, used to compute the [S] parameters after random variations in coupling matrix.
2783 self.SP = self.mainWindow.CM.SP
2784
2785
2786 ##
2787 #
2788 # Run Monte Carlo analysis with random variations in coupling matrix elements.
2789 # Calls self.randomVariations() to obtain a randomly modified coupling matrix with maximum element variations determined by function arguments.
2790 # @param N = Number of Monte Carlo random realizations.
2791 # @param inout = Maximum random variation in % of nominal value, for input/output (source/load) couplings. It is applied to elements in first and last rows and columns.
2792 # @param freq = Maximum random variation in % of nominal value, for resonators resonant frequency.
2793 # @param Q = Maximum random variation in % of nominal value, for resonators quality factor.
2794 # @param inductive = Maximum random variation in % of nominal value, for inductive couplings. It is applied to positive real parts of out-of-diagonal matrix elements.
2795 # @param capacitive = Maximum random variation in % of nominal value, for capacitive couplings. It is applied to negative real parts of out-of-diagonal matrix elements.
2796 # @param resistive = Maximum random variation in % of nominal value, for resistive couplings. It is applied to imaginary parts of out-of-diagonal matrix elements.
2797 #
2798 def runAnalysis(self, N, inout, freq, Q, inductive, capacitive, resistive):
2799 app.setOverrideCursor(Qt.WaitCursor)
2800 MatQ = self.mainWindow.CM.MatQ # Pointer to non-modified CouplingMatrix
2801 # Prepare arrays having the [S] parameters of random realizations in columns
2802 nFreq = len(self.SP.freq)
2803 nFreq2 = nFreq-1 # Number samples for group delay computed from M, groupDelayM
2804 self.S11M, self.S21M, self.S22M = np.zeros((nFreq, N), dtype=np.complex128), np.zeros((nFreq, N), dtype=np.complex128), np.zeros((nFreq, N), dtype=np.complex128)
2805 self.phaseS11M, self.phaseS21M, self.phaseS22M, self.groupDelayM = np.zeros((nFreq, N)), np.zeros((nFreq, N)), np.zeros((nFreq, N)), np.zeros((nFreq2, N))
2806
2807 maxModReal, maxModImag = np.zeros(MatQ.M.shape), np.zeros(MatQ.M.shape)
2808 maxModQn, maxModF0 = np.zeros(MatQ.Q.shape), np.zeros(MatQ.Q.shape)
2809 first = MatQ.extraNodesS # Index of first resonant node
2810 last = MatQ.M.shape[0] - MatQ.extraNodesL # Index of last resonant node + 1
2811 Qn = MatQ.Q
2812 f0n = MatQ.FT.unormFreq(np.diag(MatQ.M)[first:last].real)
2813
2814 for n in range(N):
2815 # Copy of current coupling matrix, to which random variations are applied
2816 modMatQ = self.randomVariations(MatQ, inout, inductive, capacitive, resistive, freq, Q )
2817 self.SP.fromCouplingMatrix(modMatQ) # Do not update CM_error here, matrix elements have been modified ramdomly
2818
2819 # Compute maximum variations, just for verification
2820 maxModReal = np.maximum(maxModReal, 100* np.abs(modMatQ.M.real / MatQ.M.real - 1) )
2821 maxModImag = np.maximum(maxModImag, 100* np.abs(modMatQ.M.imag / MatQ.M.imag - 1) )
2822 modQn = modMatQ.Q
2823 modF0n = MatQ.FT.unormFreq(np.diag(modMatQ.M)[first:last].real)
2824 maxModQn = np.maximum(maxModQn, 100*np.abs(modQn/Qn - 1) )
2825 maxModF0 = np.maximum(maxModF0, 100*np.abs(modF0n/f0n - 1) )
2826
2827 self.S11M[:, n], self.S21M[:, n], self.S22M[:, n], self.groupDelayM[:, n] = self.SP.S11M, self.SP.S21M, self.SP.S22M, self.SP.groupDelayM
2828 self.phaseS11M[:, n], self.phaseS21M[:, n], self.phaseS22M[:, n] = self.SP.phaseS11M, self.SP.phaseS21M, self.SP.phaseS22M
2829
2830 # Print maximum variations
2831 # Replace NaN's and Inf's with zero
2832 maxModReal[np.isnan(maxModReal)] = 0
2833 maxModReal[np.isinf(maxModReal)] = 0
2834 maxModImag[np.isnan(maxModImag)] = 0
2835 maxModImag[np.isinf(maxModImag)] = 0
2836
2837 rowIndices = self.mainWindow.rowIndices
2838 myPrint('\nSENSITIVITY ANALYSIS:')
2839 myPrint('Maximum variation achieved (%), Couplings Real part:')
2840 for m in range(maxModReal.shape[0]):
2841 for n in range(maxModReal.shape[1]):
2842 if n==m: continue
2843 if maxModReal[m, n] > 0: myPrint('(%3s,%3s): %6.3g%%' % (rowIndices[m], rowIndices[n], maxModReal[m, n]))
2844
2845 myPrint('Maximum variation achieved (%), Couplings Imaginary part:')
2846 for m in range(maxModImag.shape[0]):
2847 for n in range(maxModImag.shape[1]):
2848 if n==m: continue
2849 if maxModImag[m, n] > 0: myPrint('(%3s,%3s): %6.3g%%' % (rowIndices[m], rowIndices[n], maxModImag[m, n]))
2850
2851 myPrint('Maximum variation achieved (%), Resonators:')
2852 for m in range(maxModQn.shape[0]):
2853 myPrint( 'R%d: fres %6.3g%%, Q %6.3g%%' % (m+1, maxModF0[m], maxModQn[m]) )
2854
2855 app.restoreOverrideCursor()
2856 self.plotSensitivity()
2857
2858
2859 ##
2860 #
2861 # Returns a randomly modified coupling matrix with maximum element variations determined by function arguments. The random variation has uniform distribution.
2862 # @param MatQ = Coupling matrix to which random variations are applied (MatrixQ class instance).
2863 # It is not modified, and the modified (deep) copy is returned.
2864 # @param inout = Maximum random variation in % of nominal value, for input/output (source/load) couplings. It is applied to elements in first and last rows and columns.
2865 # @param inductive = Maximum random variation in % of nominal value, for inductive couplings. It is applied to positive real parts of out-of-diagonal matrix elements \f$ \Re M_{ij} > 0 \qquad i \neq j \f$ .
2866 # @param capacitive = Maximum random variation in % of nominal value, for capacitive couplings. It is applied to negative real parts of out-of-diagonal matrix elements \f$ \Re M_{ij} < 0 \qquad i \neq j \f$ .
2867 # @param resistive = Maximum random variation in % of nominal value, for resistive couplings. It is applied to imaginary parts of out-of-diagonal matrix elements \f$ \Im M_{ij} > 0 \qquad i \neq j \f$ .
2868 # @param f0var = Maximum random variation in % of nominal value, for resonators resonant frequency.
2869 # @param Qvar = Maximum random variation in % of nominal value, for resonators quality factor.
2870 # @return modMatQ = Modified coupling matrix, based on a deep copy of the MatQ argument.
2871 #
2872 # In order to obtain the randomly modified resonant frequency, the real part of diagonal elements corresponding to resonating nodes is unnormalized.
2873 # The random variation is applied to the unnormalized frequency, and it is normalized again to obtain the modified real part of the matrix element.
2874 #
2875 # Diagonal elements corresponding to non-resonant nodes are not modified.
2876 #
2877 # The modified Q of resonators is obtained applying a random variation to the resonator Q of the original matrix (before applying the random variation to resistive couplings).
2878 # Then, the resistive couplings are randomly modified and the new imaginary part of the diagonal element is computed accounting for the new values of the resistive couplings.
2879 #
2880 # In general, for each node \f$ n \f$, the quality factor is
2881 # \f[
2882 # Q_{n} = \frac{1}{G_n \, \mathrm{FBW}}
2883 # \f]
2884 # where the loss conductance of the unloaded resonator is:
2885 # \f[
2886 # G_{n} = - \sum_{m=1}^{N} \Im M_{mn}
2887 # \f]
2888 #
2889 # The losses corresponding to the new \f$ Q'_n \f$ for resonator \f$ n \f$ are:
2890 # \f[
2891 # G'_n = \frac{1}{Q_n' \, \mathrm{FBW}}
2892 # \f]
2893 # But, after the random variation of resistive couplings the losses that we have are
2894 # \f[
2895 # G''_n = - \sum_{m=1}^N \Im M''_{nm}
2896 # \f]
2897 # so, in order to achieve a quality factor \f$ Q' \f$, the modified value of the imaginary part of the diagonal element must be:
2898 # \f[
2899 # \Im M'_{nn} = \Im M''_{nn} + G''_n - G'_n
2900 # \f]
2901 #
2902 def randomVariations(self, MatQ, inout, inductive, capacitive, resistive, f0var, Qvar):
2903 rand = lambda x: 1 + (2*np.random.rand(1)[0]-1)*x/100 # Random number with uniform distribution in [-1-x/100, 1+x/100]
2904
2905 # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
2906 # eps = MatQ.parent.matrixElementsEps('Sensitivity analysis')
2907 eps = 10** -self.mainWindow.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
2908
2909 modMatQ = MatQ.copy()
2910 FBW = MatQ.FT.FBW # Fractional bandwidth
2911
2912 M = modMatQ.M
2913 N = M.shape[0]
2914 first = MatQ.extraNodesS # Index of first resonant node
2915 last = N - MatQ.extraNodesL # Index of last resonant node + 1
2916
2917 # In the code below, varRe and varIm are the maximum random variation of real and imag parts of M[m,n]
2918 for m in range(N):
2919 for n in range(m+1, N):
2920 #if n==m: continue # Main diagonal will be computed later, after we have computed the variations of resistive couplings
2921 if m==0 or m==N-1 or n==0 or n==N-1: # Input and output couplings (source and load)
2922 if abs(M[m, n]) > eps: varRe, varIm = inout, inout
2923 else: varRe, varIm = 0, 0
2924 else: # Couplings between resonators
2925 if M[m, n].real > eps: varRe = inductive
2926 elif M[m, n].real < -eps: varRe = capacitive
2927 else: varRe, varIm = 0, 0
2928
2929 if abs(M[m, n].imag) > eps: varIm = resistive
2930 else: varIm = 0
2931
2932 M[m, n] = complex(M[m, n].real * rand(varRe), M[m, n].imag * rand(varIm))
2933 M[n, m] = M[m, n]
2934
2935 for n in range(first, last):
2936 # Variation in Q
2937 Qn = MatQ.Q[n-first] # Original Qn (before modifing resisitve couplings)
2938 modQn = Qn * rand(Qvar) # Modified Q after random variation
2939 losses = M[:,n].imag.sum() # Losses, accounting the variations in resistive couplings
2940 modLosses = -1/(FBW * modQn) # Desired losses, according to modified Q
2941 imDiag = MatQ.M[n, n].imag + modLosses - losses # New imaginary part for desired losses
2942
2943 # Variation in f0
2944 f0n = MatQ.FT.unormFreq(M[n, n].real) # Resonator frequency
2945 modf0n = f0n * rand(f0var)
2946 reDiag = MatQ.FT.normFreq(modf0n)
2947
2948 M[n, n] = complex(reDiag, imDiag)
2949
2950 modMatQ.Qresonators()
2951 return modMatQ
2952
2953
2954 ##
2955 #
2956 # Plot [S] parameters magnitude and phase graph, if they are available.
2957 # The plotted data is self.S11, self.S21 and self.S22, which contain the [S] parameters for all random realizations, in columns.
2958 #
2959 def plotSensitivity(self):
2960
2961 SPlotSensitivity = self.mainWindow.mainWindow.SPlotSensitivity
2962
2963 if SPlotSensitivity is None or SPlotSensitivity.closed is True:
2964
2965 # Set dynamic range to the lowest value of S21 at sweep range ends.
2966 dynamicRange = -20*log10( min(np.concatenate( (abs(self.S21M[0, :]), abs(self.S21M[-1, :] )) )) )
2967 dynamicRange = 10*ceil(dynamicRange/10)
2968
2969 SPlotSensitivity = DbPlot( parent=self.mainWindow,
2970 windowTitle = 'Sensitivity analysis results',
2971 tabTitle = '[S] + Group delay',
2972 title = '[S] parameters (from Characteristic Polynomials)',
2973 XData = self.SP.freq / 1e9,
2974 leftYData = [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
2975 rightXData = self.SP.freq2 / 1e9,
2976 rightYData = [ 1e9 * self.groupDelayM ],
2977 leftYmin = -dynamicRange,
2978 leftYmax = 0,
2979 leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
2980 leftYVisible = [True, True, False],
2981 XLabel = 'Frequency (GHz)',
2982 leftYLabel = 'dB',
2983 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'ns',
2984 rightYLabel = 'Time (ns.)' ,
2985 rightYNames = ['Group delay'],
2986 rightYVisible = [True]
2987 )
2988 SPlotSensitivity.addTab( tabTitle = '[S] + Phase',
2989 title = '[S] parameters (from Characteristic Polynomials)',
2990 XData = self.SP.freq / 1e9,
2991 leftYData = [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M)) ],
2992 rightYData = [ self.phaseS21M * 180/np.pi, self.phaseS11M * 180/np.pi, self.phaseS22M * 180/np.pi ],
2993 leftYmin = -dynamicRange,
2994 leftYmax = 0,
2995 leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
2996 leftYVisible = [True, True, False],
2997 XLabel = 'Frequency (GHz)',
2998 leftYLabel = 'dB',
2999 Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg',
3000 rightYLabel = 'degrees' ,
3001 rightYNames = ['Phase(S<sub>21</sub>)', 'Phase(S<sub>11</sub>)', 'Phase(S<sub>22</sub>)'],
3002 rightYVisible = [True, False, False]
3003 )
3004 SPlotSensitivity.addTab( tabTitle = 'Group delay + Phase',
3005 title = '[S] parameters (from Characteristic Polynomials)',
3006 XData = self.SP.freq2 / 1e9,
3007 leftYData = [1e9 * self.groupDelayM],
3008 rightXData = self.SP.freq / 1e9,
3009 rightYData = [ self.phaseS21M * 180/np.pi ],
3010 leftYNames = ['Group delay'],
3011 XLabel = 'Frequency (GHz)',
3012 leftYLabel = 'Time (ns.)',
3013 Xunits = 'GHz', leftYunits = 'ns', rightYunits = 'deg',
3014 rightYLabel = 'degrees' ,
3015 rightYNames = ['Phase(S<sub>21</sub>)'],
3016 rightYVisible = [True]
3017 )
3018
3019 self.mainWindow.mainWindow.SPlotSensitivity = SPlotSensitivity
3020
3021 else:
3022 SPlotSensitivity.update(0, self.SP.freq / 1e9, [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
3023 rightXData = self.SP.freq2 / 1e9, rightYData=[ 1e9 * self.groupDelayM], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3024
3025 SPlotSensitivity.update(1, self.SP.freq / 1e9, [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
3026 rightYData=[ self.phaseS21M * 180/np.pi, self.phaseS11M * 180/np.pi, self.phaseS22M * 180/np.pi], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3027
3028 SPlotSensitivity.update(2, self.SP.freq2 / 1e9, [1e9 * self.groupDelayM], rightXData = self.SP.freq / 1e9, rightYData=[ self.phaseS21M * 180/np.pi ], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3029
3030 self.mainWindow.mainWindow.autoScaleFreq = False
3031
3032 # Plot masks, or update if there are any
3033 if self.mainWindow.mainWindow.SM is not None:
3034 SPlotSensitivity.deleteMasks()
3035 self.mainWindow.mainWindow.plotMasks()
3036
3037 # End class: SensitivityAnalysis
3038 #!!!!!!!!!!!!!!!!!!!!!!!!!#
3039
3040
3041 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
3042 # Begin class: CouplingMatrixDlg #
3043
3044 ##
3045 #
3046 # GUI dialog to show coupling matrices
3047 #
3048 class CouplingMatrixDlg(QMainWindow, Ui_couplingmatrixdlg.Ui_CouplingMatrixDlg):
3049
3050 ##
3051 #
3052 # Constructor: Creates dialog window with a table to display coupling matrices and buttons to save or store to disk, select matrix from list, plot [S] parameters or edit current matrix.
3053 # @param CM = CouplingMatrix class instance.
3054 # @param parent = Parent widget, in this case is mainWindow.
3055 #
3056 def __init__(self, CM, parent=None):
3057 super(CouplingMatrixDlg, self).__init__(parent)
3058 self.setAttribute(Qt.WA_DeleteOnClose) # Necessary for Modless windows.
3059
3060 ##
3061 # Copy of the mainWindow class instance.
3062 # It is necessary to store a copy since it must be accessed by some member functions.
3063 self.mainWindow = parent
3064 self.setupUi(self)
3065
3066 ##
3067 # Matrix edit dialog. It will be a Modeless Live dialog, that is only hidden when closed.
3068 self.matrixEditDlg = None
3069
3070 ##
3071 # Sensitivity analysis dialog. It will be a Modeless Live dialog, that is only hidden when closed.
3072 self.sensitivityDlg = None
3073
3074 ##
3075 # Shallow copy of the mainWindow CM class instance, for easier access.
3076 self.CM = CM
3077
3078 ##
3079 # Attribute that is set to True when CM.MatQ has been modified by user edit actions and not saved in list.
3080 self.dirty = False
3081 self.setWindowModified(self.dirty)
3082
3083 ##
3084 # Attribute that is set to True when changes to matrix must be recorded.
3085 # It is disabled, for example, when the user does and incorrect matrix edit and the program automatically restores the old matrix value.
3086 self.recordChanges = True
3087
3088 ##
3089 # Attribute that is set to True before automatic calls to on_action_CM_comboBox_currentIndexChanged() to avoid loading a matrix.
3090 self.noLoadCM = False
3091
3092 # Add CM selection comboBox to toolBar
3093 self.recordChanges = False # Do not record automatic calls to on_CMcomboBox_currentIndexChanged()
3094 self.CM_comboBox = QComboBox(self.toolBar_2)
3095 self.CM_comboBox.setObjectName("CM_comboBox")
3096 self.CM_comboBox.setToolTip("Select matrix from list")
3097 self.CM_comboBox.setStatusTip("Select matrix from list")
3098 self.connect(self.CM_comboBox, SIGNAL("currentIndexChanged(int)"), self.on_CM_comboBox_currentIndexChanged)
3099 self.toolBar_2.insertWidget(self.action_ListRemove, self.CM_comboBox)
3100
3101 # Low pass and Q Tab
3102 tabLPQ = QWidget()
3103 self.tabWidget.addTab(tabLPQ, "Low-pass matrix and Q")
3104
3105 # Horizontal layout that contains a vertical stack with matrix name, matrix table and Q table
3106 self.horizontalLayout_3 = QHBoxLayout(tabLPQ)
3107 self.horizontalLayout_3.addStretch()
3108
3109 # Vertical layout with matrix name, matrix table and Q table
3110 self.verticalLayout_CM = QVBoxLayout()
3111
3112 # Matrix name, with horizontal layout
3113 self.horizontalLayout_4 = QHBoxLayout()
3114
3115 # Create and Matrix Name
3116 self.CM_nameLabel = QLabel('Coupling matrix name:')
3117 self.horizontalLayout_4.addWidget(self.CM_nameLabel)
3118 self.CM_name = QLineEdit('')
3119 self.horizontalLayout_4.addWidget(self.CM_name)
3120 self.horizontalLayout_4.addStretch()
3121 self.CM_prec_label = QLabel('Precision:')
3122 self.horizontalLayout_4.addWidget(self.CM_prec_label)
3123 self.CM_prec = QSpinBox()
3124 self.CM_prec.setRange(3, 14)
3125
3126 prec = self.mainWindow.CM.matrixElementsEps('Display Coupling Matrix')
3127 # self.CM_prec.setValue(-int ( round(log10(prec)) ))
3128 self.CM_prec.setValue(4) # CE prefers to see less significant digits by default.
3129
3130 self.connect(self.CM_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3131 self.horizontalLayout_4.addWidget(self.CM_prec)
3132 self.verticalLayout_CM.addLayout(self.horizontalLayout_4)
3133
3134 # Matrix table
3135 self.CM_tableWidget = myTableWidget(name = "Coupling Matrix", parent = self.centralwidget)
3136 self.CM_tooltip = "To manually edit coupling matrix elements,\nfirst open matrix editor by clicking the toolbar button."
3137 self.CM_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CM_tooltip, None, QApplication.UnicodeUTF8))
3138 self.verticalLayout_CM.addWidget(self.CM_tableWidget)
3139
3140 # Q table, with horizontal layout
3141 self.horizontalLayout_2 = QHBoxLayout()
3142 self.horizontalLayout_2.addStretch()
3143 self.Q_tableWidget = myTableWidget(name = "Resonators Q", parent = self.centralwidget)
3144 self.Q_tooltip = "To manually edit Q vector elements,\nfirst open matrix editor by clicking the toolbar button."
3145 self.Q_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.Q_tooltip, None, QApplication.UnicodeUTF8))
3146 self.horizontalLayout_2.addWidget(self.Q_tableWidget)
3147 self.Qprec_Layout = QHBoxLayout()
3148 self.Q_prec_label = QLabel('Precision:')
3149 self.Qprec_Layout.addWidget(self.Q_prec_label)
3150 self.Q_prec = QSpinBox()
3151 self.Q_prec.setRange(3, 14)
3152 self.Q_prec.setValue(5)
3153 self.connect(self.Q_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3154 self.Qprec_Layout.addWidget(self.Q_prec)
3155 self.horizontalLayout_2.addLayout(self.Qprec_Layout)
3156 self.horizontalLayout_2.addStretch()
3157 self.verticalLayout_CM.addLayout(self.horizontalLayout_2)
3158
3159 self.verticalLayout_CM.addStretch()
3160
3161 self.horizontalLayout_3.addLayout(self.verticalLayout_CM)
3162
3163 self.horizontalLayout_3.addStretch(1)
3164
3165 # End Low-pass and Q Tab
3166
3167 # Band-pass Tab
3168 tabBP = QWidget()
3169 self.tabWidget.addTab(tabBP, "Coupling bandwidth matrix (MHz)")
3170
3171 # Horizontal layout that contains a vertical stack with matrix name and matrix table
3172 self.horizontalLayout_3bp = QHBoxLayout(tabBP)
3173 self.horizontalLayout_3bp.addStretch()
3174
3175 # Vertical layout with matrix name, matrix table and Q table
3176 self.verticalLayout_CMbp = QVBoxLayout()
3177
3178 # Matrix name, with horizontal layout
3179 self.horizontalLayout_4bp = QHBoxLayout()
3180
3181 # Create and Matrix Name
3182 self.CMbp_nameLabel = QLabel('Coupling matrix name:')
3183 self.horizontalLayout_4bp.addWidget(self.CMbp_nameLabel)
3184 self.CMbp_name = QLabel('')
3185 self.horizontalLayout_4bp.addWidget(self.CMbp_name)
3186 self.horizontalLayout_4bp.addStretch()
3187 self.CMbp_prec_label = QLabel('Precision:')
3188 self.horizontalLayout_4bp.addWidget(self.CMbp_prec_label)
3189 self.CMbp_prec = QSpinBox()
3190 self.CMbp_prec.setRange(3, 14)
3191 self.CMbp_prec.setValue(5)
3192 self.connect(self.CMbp_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3193 self.horizontalLayout_4bp.addWidget(self.CMbp_prec)
3194 self.verticalLayout_CMbp.addLayout(self.horizontalLayout_4bp)
3195
3196 # Matrix table
3197 self.CMbp_tableWidget = myTableWidget(name = 'Coupling Bandwidth Matrix', parent = self.centralwidget)
3198 self.CMbp_tooltip = "To manually edit coupling matrix elements,\nfirst open matrix editor by clicking the toolbar button."
3199 self.CMbp_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CMbp_tooltip, None, QApplication.UnicodeUTF8))
3200 self.verticalLayout_CMbp.addWidget(self.CMbp_tableWidget)
3201
3202 # Impedances label
3203 # Only for Jordi Mateu's version of bandpass matrix or bandwidth coupling matrix
3204 # Not relevant in CMSDemo version of bandpass matrix or bandwidth coupling matrix
3205 # self.verticalLayout_CMbp.addWidget(QLabel('Z<sub>0</sub> = Z<sub>S</sub> = Z<sub>L</sub> = 50 Ohm'))
3206
3207 self.verticalLayout_CMbp.addStretch()
3208
3209 self.horizontalLayout_3bp.addLayout(self.verticalLayout_CMbp)
3210
3211 self.horizontalLayout_3bp.addStretch(1)
3212
3213 # End Band-pass Tab
3214
3215 # Widget properties
3216 self.CM_name.setReadOnly(True)
3217
3218 # Format Matrix name as a frameless QLineEdit with bold font and background colour.
3219 self.CM_name.setFrame(False)
3220 font = self.CM_name.font()
3221 font.setBold(True)
3222 self.CM_name.setFont(font)
3223 palette = self.CM_name.palette()
3224 palette.setColor(QPalette.Base, self.palette().window().color())
3225 self.CM_name.setPalette(palette)
3226
3227 # Set bold font also for band-pass matrix name
3228 self.CMbp_name.setFont(font)
3229
3230 self.CM_tableWidget.setColumnCount(0)
3231 self.CM_tableWidget.setRowCount(0)
3232 self.Q_tableWidget.setColumnCount(0)
3233 self.Q_tableWidget.setRowCount(0)
3234 self.CMbp_tableWidget.setColumnCount(0)
3235 self.CMbp_tableWidget.setRowCount(0)
3236
3237 self.CM_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3238 self.Q_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3239 self.CMbp_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3240
3241 # Layout policy and stretch
3242 self.CM_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3243 self.Q_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3244 self.CMbp_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3245 # self.CM_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
3246 # self.Q_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
3247 # self.CMbp_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))
3248 # self.horizontalLayout_3.setStretchFactor(self.CM_tableWidget, 100)
3249 # self.horizontalLayout_2.setStretchFactor(self.Q_tableWidget, 100)
3250 # self.verticalLayout.setStretchFactor(self.CM_tableWidget, 100)
3251 # self.verticalLayout.setStretchFactor(self.Q_tableWidget, 100)
3252
3253 # Connect table modified signal
3254 self.connect(self.CM_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), self.CM_itemChanged)
3255 self.connect(self.CM_tableWidget, SIGNAL("currentCellChanged(int, int, int, int)"), self.CM_currentCellChanged )
3256 self.connect(self.Q_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), self.Q_itemChanged)
3257 self.connect(self.Q_tableWidget, SIGNAL("currentCellChanged(int, int, int, int)"), self.Q_currentCellChanged )
3258 #self.connect(self.CM_name, SIGNAL("textEdited(QString)"), self.matrixQChanged)
3259 self.connect(self.CM_name, SIGNAL("editingFinished()"), self.matrixQChanged)
3260
3261 # Add available matrices to CM_comboBox
3262 for MatQ in CM.listM:
3263 self.CM_comboBox.addItem(MatQ.name)
3264
3265 # Set the current coupling matrix to the previous one
3266 self.CM_comboBox.setCurrentIndex(self.CM.indexCM)
3267
3268 # Set tables READ-ONLY
3269 self.CM_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3270 self.Q_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3271 self.CMbp_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3272
3273 # Load coupling matrix and Q tables
3274 self.loadMatrixQ(CM.MatQ)
3275
3276 self.setWindowModified(False)
3277
3278 # Start recording changes.
3279 self.recordChanges = True
3280
3281 # Create matrix edit dialog to allow loading matrices from the coupling matrix window.
3282 self.matrixEditDlg = MatrixEditDlg(self)
3283 self.connect(self.CM_prec, SIGNAL("valueChanged(int)"), self.matrixEditDlg.udpateTopology)
3284
3285 ##
3286 # SensitivityAnalisys class instance.
3287 self.SA = SensitivityAnalysis(self)
3288
3289
3290 ##
3291 #
3292 # Reimplementation of the window close function.
3293 # Sets mainWindow.CMdialog to None.
3294 #
3295 def closeEvent(self, event):
3296 # Check if matrix has been modified and not saved
3297 if not self.okToContinue():
3298 event.ignore()
3299 else:
3300 self.mainWindow.CMdialog = None
3301
3302 # Close [S] error plot if it exists
3303 if self.mainWindow.SPlotError is not None and not self.mainWindow.SPlotError.closed:
3304 self.mainWindow.SPlotError.close()
3305 self.mainWindow.SPlotError = None
3306
3307 # Close energy plot if it exists
3308 if self.mainWindow.EnergyPlot is not None and not self.mainWindow.EnergyPlot.closed:
3309 self.mainWindow.EnergyPlot.close()
3310 self.mainWindow.EnergyPlot = None
3311
3312 # Close sensitivity plot if it exists
3313 if self.mainWindow.SPlotSensitivity is not None and not self.mainWindow.SPlotSensitivity.closed:
3314 self.mainWindow.SPlotSensitivity.close()
3315 self.mainWindow.SPlotSensitivity = None
3316
3317 event.accept()
3318
3319
3320 ##
3321 #
3322 # Load a N+2 coupling matrix into a QTableWidget.
3323 # All actions that modify the coupling matrix, end up executing this method, so the [S] error plot is updated if it exists.
3324 # @param MatQ = MatrixQ class instance.
3325 # @param loadMatrix = Load contents of coupling matrix. Default True.
3326 # @param loadQ = Load contents of Q vector. Default True.
3327 #
3328 def loadMatrixQ(self, MatQ, loadMatrix=True, loadQ=True):
3329
3330 # Compute Coupling Bandwidth version of current matrix
3331 MatQ.lp2bp2cbw()
3332
3333 if loadMatrix:
3334
3335 # Load matrix name
3336 #self.CM_name.setText('<b>' + MatQ.name + '</b>')
3337 self.CM_name.setText(MatQ.name)
3338 self.CMbp_name.setText(MatQ.name)
3339
3340 # Load low-pass M table
3341 table = self.CM_tableWidget
3342 table.clear()
3343 M = MatQ.M
3344 size = M.shape[0]
3345 first = MatQ.extraNodesS
3346 last = size - MatQ.extraNodesL
3347
3348 table.clear()
3349 table.setColumnCount(size)
3350 table.setRowCount(size)
3351
3352 # Row and column header labels
3353 labels = ['S']
3354 for n in range(1, first ): labels.append('NR' + str(n))
3355 labels.extend( [ 'R'+str(elem) for elem in range(1, last-first+1) ] )
3356 for n in range(first, first+MatQ.extraNodesL-1 ): labels.append('NR' + str(n))
3357 labels.append('L')
3358 table.setHorizontalHeaderLabels( labels )
3359 table.setVerticalHeaderLabels( labels )
3360 self.rowIndices = labels # Will be used in matrix rotation pivot indices comboBox
3361 MatQ.rowIndices = labels # Will be used for saving energy and power
3362
3363 for m in range(size):
3364 for n in range(size):
3365 item = QTableWidgetItem( complexStr(M[m, n], self.CM_prec.value()) )
3366 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3367 table.setItem(m, n, item)
3368
3369 table.resizeRowsToContents()
3370 table.resizeColumnsToContents()
3371 table.setMaximumSize( table.maximumSizeHint() )
3372
3373 # Load band-pass M table
3374 table = self.CMbp_tableWidget
3375 table.clear()
3376 Mcbw = MatQ.Mcbw
3377
3378 table.clear()
3379 table.setColumnCount(size)
3380 table.setRowCount(size)
3381
3382 # Row and column header labels
3383 table.setHorizontalHeaderLabels( labels )
3384 table.setVerticalHeaderLabels( labels )
3385
3386 for m in range(size):
3387 for n in range(size):
3388 item = QTableWidgetItem( complexStr(Mcbw[m, n], self.CMbp_prec.value()) )
3389 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3390 table.setItem(m, n, item)
3391
3392 table.resizeRowsToContents()
3393 table.resizeColumnsToContents()
3394 table.setMaximumSize( table.maximumSizeHint() )
3395
3396
3397 # Update matrixEditDlg
3398 if self.matrixEditDlg is not None:
3399 self.matrixEditDlg.setMatrixSize() # Must be called after setting self.rowIndices
3400 self.matrixEditDlg.udpateTopology()
3401 self.matrixEditDlg.Q_doubleSpinBox.setValue( np.mean(np.abs(MatQ.Q)) )
3402
3403 if loadQ:
3404 # Load Q table
3405 Q = MatQ.Q
3406 N = Q.shape[0] # Number of resonators
3407
3408 table = self.Q_tableWidget
3409 table.clear()
3410 table.setColumnCount(N)
3411 table.setRowCount(1)
3412 table.setHorizontalHeaderLabels( [ str(elem) for elem in range(1, N+1) ])
3413 table.setVerticalHeaderLabels( [ 'Q' ] )
3414
3415 for n in range(N):
3416 item = QTableWidgetItem( '%.*g' % (self.Q_prec.value(), Q[n]) )
3417 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3418 table.setItem(0, n, item)
3419
3420 table.resizeRowsToContents()
3421 table.resizeColumnsToContents()
3422 table.setMaximumSize( table.maximumSizeHint() )
3423
3424 # Update [S] parameters plot, if it exists
3425 if self.mainWindow.SPlotError is not None and (self.mainWindow.SPlotError.closed is False):
3426 self.on_action_PlotS_triggered(True)
3427
3428 # Update energy plot, if it exists
3429 if self.mainWindow.EnergyPlot is not None and (self.mainWindow.EnergyPlot.closed is False):
3430 self.on_action_PlotEnergy_triggered(True)
3431
3432
3433 ##
3434 #
3435 # This function checks if matrix has been modified and not saved to list. If it has been modified, asks the user to save the matrix.
3436 # The function is executed by the GUI when there is an action that will destroy CM.MatQ: Load matrix from list or read matrix from disk.
3437 # @return ok (True / False) = If True the use has agreed to continue, if False the user cancels the operation.
3438 #
3439 def okToContinue(self):
3440 if self.dirty:
3441 message = "Coupling Matrix has been modified, but not saved to list\nSave?"
3442 reply = QMessageBox.question(self, "%s - Unsaved Changes" % applicationName, message, QMessageBox.Yes | QMessageBox.No| QMessageBox.Cancel)
3443 if reply == QMessageBox.Cancel:
3444 return False
3445 elif reply == QMessageBox.Yes:
3446 self.on_action_ListAdd_triggered(True, programmed=True)
3447 return True
3448
3449 ##@{
3450 # @name Functions automatically excuted when the user interacts with the GUI
3451
3452
3453 ##
3454 #
3455 # Slot that is executed whenever current CM cell is changed
3456 #
3457 def CM_currentCellChanged(self, m, n, mold, nold):
3458 if self.action_Edit.isChecked() is False: return # Do nothing if we not in edit mode
3459
3460 table = self.CM_tableWidget
3461 item = table.currentItem() # table.item(m, n)
3462 if item is None: return
3463
3464 self.recordChanges = False
3465 st = complexStr(self.CM.MatQ.M[m, n], 14)
3466 item.setText(st)
3467
3468 itemOld = table.item(mold, nold)
3469 stOld = complexStr(self.CM.MatQ.M[mold, nold], self.CM_prec.value())
3470 if itemOld is not None: itemOld.setText(stOld)
3471
3472 self.recordChanges = True
3473
3474 table.resizeRowsToContents()
3475 table.resizeColumnsToContents()
3476 table.setMaximumSize( table.maximumSizeHint() )
3477
3478
3479 ##
3480 #
3481 # Slot that is executed whenever current Q cell is changed
3482 #
3483 def Q_currentCellChanged(self, m, n, mold, nold):
3484 if self.action_Edit.isChecked() is False: return # Do nothing if not in edit mode
3485
3486 table = self.Q_tableWidget
3487 item = table.currentItem()
3488 if item is None: return
3489
3490 self.recordChanges = False
3491 st = complexStr(self.CM.MatQ.Q[n], 14)
3492 item.setText(st)
3493
3494 # # for some reason, itemOld is not the last item, but the one-before-last, but (mold, nold) are correct anyway
3495 # itemOld = table.item(mold, nold)
3496 # stOld = complexStr(self.CM.MatQ.M[mold, nold], self.CM_prec.value())
3497 # if itemOld is not None: itemOld.setText(stOld)
3498
3499 self.recordChanges = True
3500
3501 table.resizeRowsToContents()
3502 table.resizeColumnsToContents()
3503 table.setMaximumSize( table.maximumSizeHint() )
3504
3505
3506 ##
3507 #
3508 # Slot that is executed whenever CM.MatQ is changed by a matrix edit action.
3509 # The self.dirty attribute is set to True.
3510 #
3511 def matrixQChanged(self):
3512 MatQ = self.CM.MatQ
3513
3514 if self.sender() is self.CM_name:
3515 st = unicode(self.CM_name.text())
3516 if st != MatQ.name:
3517 MatQ.name = st
3518 MatQ.fileExt = None
3519 self.matrixEditDlg.lastEditAction = 'Matrix name changed to: %s' % st
3520 else: # Nothing has been modified here.
3521 return
3522
3523 self.dirty = True
3524 self.setWindowModified(self.dirty)
3525
3526 # Update Q
3527 self.recordChanges = False
3528 MatQ.Qresonators()
3529 self.loadMatrixQ(MatQ, loadMatrix=False)
3530 self.recordChanges = True
3531
3532 self.pushEditAction()
3533
3534
3535 ##
3536 #
3537 # Update the contents of self.CM.MatQ after the user has edited the CM table widget.
3538 #
3539 def CM_itemChanged(self, item):
3540 # Do nothing is recording changes is disabled.
3541 if self.recordChanges == False: return
3542
3543 row = self.CM_tableWidget.currentRow()
3544 col = self.CM_tableWidget.currentColumn()
3545 M = self.CM.MatQ.M
3546
3547 if row>=0 and col>=0:
3548 st = str(item.text().trimmed())
3549
3550 # Python complex() function needs 'j' imaginary unit.
3551 if st.find('i') != -1:
3552 st = st.replace('i','j')
3553 # Update GUI table with complex number using 'j' instead of 'i'.
3554 self.recordChanges = False # Disable recording this matrix change
3555 item.setText(st)
3556 self.recordChanges = True # Re-enable recording.
3557
3558 try:
3559 value = complex(st)
3560 except ValueError, err:
3561 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect value error. String:<br>%s<br>is not a correct complex number representation.</p>' % (st))
3562
3563 # Put back the old value in last edited cell
3564 self.recordChanges = False # Disable recording this matrix change
3565 item.setText(complexStr(M[row, col], 4))
3566 self.recordChanges = True # Re-enable recording.
3567 #@todo Set the selected cell back to the cell that was erroneously edited. The following code does not work as it is expected to.
3568 # # Select back last edited cell
3569 # self.CM_tableWidget.setCurrentCell(row, col)
3570 # self.CM_tableWidget.setRangeSelected (QTableWidgetSelectionRange(row, col, row, col), True)
3571 # item.setSelected(True)
3572 # self.CM_tableWidget.setCurrentItem(item)
3573 # self.CM_tableWidget.scrollToItem(item)
3574
3575 else:
3576 #print "(%d,%d) = %s = (%g,%g)" % (row, col, st, value.real, value.imag)
3577 M[row, col] = value
3578 if row == col:
3579 self.matrixEditDlg.lastEditAction = 'CM Matrix entry edited: (%d,%d) = %s' % (row, col, st)
3580 else: # For non diagonal elements, change also the reciprocal.
3581 M[col, row] = value
3582 itemSym = QTableWidgetItem(st)
3583 itemSym.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3584 self.recordChanges = False # Disable recording this matrix change
3585 self.CM_tableWidget.setItem(col, row, itemSym)
3586 self.recordChanges = True # Re-enable recording.
3587 self.matrixEditDlg.lastEditAction = 'CM Matrix entry edited: (%d,%d) and (%d,%d) = %s' % (row, col, col, row, st)
3588 self.matrixQChanged()
3589
3590 # Update coupling bandwidth matrix
3591 self.CM.MatQ.lp2bp2cbw()
3592 item = QTableWidgetItem( complexStr(self.CM.MatQ.Mcbw[row, col], self.CMbp_prec.value()) )
3593 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3594 self.CMbp_tableWidget.setItem(row, col, item)
3595 if row != col:
3596 itemSym = QTableWidgetItem( complexStr(self.CM.MatQ.Mcbw[col, row], self.CMbp_prec.value()) )
3597 itemSym.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3598 self.CMbp_tableWidget.setItem(col, row, itemSym)
3599
3600
3601 ##
3602 #
3603 # Update the contents of self.CM.MatQ after the user has edited the Q table widget.
3604 #
3605 # For each node \f$ i \f$, the quality factor is
3606 # \f[
3607 # Q_{i} = \frac{1}{G_i \, \mathrm{FBW}}
3608 # \f]
3609 # where the loss conductance of the unloaded resonator is:
3610 # \f[
3611 # G_{i} = - \sum_{j=1}^{N} \Im M_{ji} = - \Im M_{ii} - \sum_{i \neq j} \Im M_{ji}
3612 # \f]
3613 #
3614 # If we change the \f$ Q_i \f$ of a resonator, the new value of \f$ \Im M_{ii} \f$ is:
3615 # \f[
3616 # \Im M_{ii} = - G_{i} - \sum_{i \neq j} \Im M_{ji} = \frac{-1}{Q_{i} \, \mathrm{FBW}} - \sum_{i \neq j} \Im M_{ji}
3617 # \f]
3618 #
3619 def Q_itemChanged(self, item):
3620 # Do nothing is recording changes is disabled.
3621 if self.recordChanges == False: return
3622
3623 row = self.Q_tableWidget.currentRow()
3624 col = self.Q_tableWidget.currentColumn()
3625 MatQ = self.CM.MatQ
3626 M = MatQ.M
3627 Q = MatQ.Q
3628
3629 if row>=0 and col>=0:
3630 st = str(item.text().trimmed())
3631
3632 try:
3633 value = float(st)
3634 except ValueError, err:
3635 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect value error. String:<br>%s<br>is not a correct real number representation.</p>' % (st))
3636
3637 # Put back the old value in last edited cell
3638 self.recordChanges = False # Disable recording this matrix change
3639 item.setText('%.5g' % Q[col])
3640 self.recordChanges = True # Re-enable recording.
3641 else:
3642 #print "(%d,%d) = %s = %g" % (row, col, st, value)
3643 #oldLosses = -1 / (Q[col] * MatQ.FT.FBW)
3644 #newLosses = -1 / (value * MatQ.FT.FBW)
3645 #deltaM = newLosses - oldLosses
3646 Q[col] = value
3647 self.matrixEditDlg.lastEditAction = 'Q vector entry edited: (%d) = %s' % (col, st)
3648
3649 # Update diagonal entry of CM matrix
3650 colM = col + MatQ.extraNodesS
3651 losses = -1 / (value * MatQ.FT.FBW)
3652 sumImagColNoDiag = M[:, colM].imag.sum() - M[colM, colM].imag
3653 imDiag = losses - sumImagColNoDiag
3654
3655 M[colM, colM] = complex(M[colM, colM] .real, imDiag)
3656 itemM = QTableWidgetItem(complexStr(M[colM, colM], 4 ))
3657 itemM.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3658 self.recordChanges = False # Disable recording this matrix change
3659 self.CM_tableWidget.setItem(colM, colM, itemM)
3660 self.recordChanges = True # Re-enable recording.
3661 self.matrixQChanged()
3662
3663
3664 @pyqtSignature("bool")
3665 ##
3666 #
3667 # Run sensitivity analysis
3668 # This function is automatically executed by the GUI when the user pushes the 'Run' button in the sensitivity groupBox.
3669 #
3670 def on_runSensitivity_pushButton_clicked(self, checked):
3671 inout = self.inout_doubleSpinBox.value()
3672 inductive = self.inductive_doubleSpinBox.value()
3673 capacitive = self.capacitive_doubleSpinBox.value()
3674 resistive = self.resistive_doubleSpinBox.value()
3675 freq = self.freq_doubleSpinBox.value()
3676 Q = self.Q_doubleSpinBox.value()
3677 N = self.N_spinBox.value()
3678 self.SA.runAnalysis(N, inout, freq, Q, inductive, capacitive, resistive)
3679
3680
3681 @pyqtSignature("bool")
3682 ##
3683 #
3684 # Enable or disable matrix edit.
3685 # This function is automatically executed by the GUI when the user toggles the 'Edit' button.
3686 #
3687 def on_action_Edit_toggled(self, checked):
3688 if checked:
3689 self.CM_tableWidget.setEditTriggers(QAbstractItemView.AnyKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
3690 self.CM_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
3691 self.CM_tableWidget.setToolTip("")
3692 self.Q_tableWidget.setEditTriggers(QAbstractItemView.AnyKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
3693 self.Q_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
3694 self.Q_tableWidget.setToolTip("")
3695 self.CM_name.setReadOnly(False)
3696 self.CM_name.deselect()
3697
3698 # Format Matrix name as default.
3699 self.CM_name.setFrame(True)
3700 font = self.CM_name.font()
3701 font.setBold(False)
3702 self.CM_name.setFont(font)
3703 palette = self.CM_name.palette()
3704 palette.setColor(QPalette.Base, self.palette().base().color())
3705 self.CM_name.setPalette(palette)
3706
3707 self.recordChanges = True
3708
3709 self.matrixEditDlg.show()
3710 else:
3711 self.CM_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3712 self.CM_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3713 self.CM_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CM_tooltip, None, QApplication.UnicodeUTF8))
3714 self.Q_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3715 self.Q_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3716 self.Q_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.Q_tooltip, None, QApplication.UnicodeUTF8))
3717 self.CM_name.setReadOnly(True)
3718 self.CM_name.deselect()
3719
3720 # Format Matrix name as a frameless QLineEdit with bold font and background colour.
3721 self.CM_name.setFrame(False)
3722 font = self.CM_name.font()
3723 font.setBold(True)
3724 self.CM_name.setFont(font)
3725 palette = self.CM_name.palette()
3726 palette.setColor(QPalette.Base, self.palette().window().color())
3727 self.CM_name.setPalette(palette)
3728
3729 self.matrixEditDlg.close()
3730
3731 # Update the matrix to set the precision after editing matrix entries
3732 self.loadMatrixQ(self.CM.MatQ)
3733
3734
3735 @pyqtSignature("bool")
3736 ##
3737 #
3738 # Enable or disable sensitivity analysis.
3739 # This function is automatically executed by the GUI when the user toggles the 'Sensitivity analysis' groupBox.
3740 #
3741 def on_sensitivity_groupBox_toggled(self, checked):
3742 if checked:
3743 pass
3744 else:
3745 pass
3746
3747
3748 @pyqtSignature("bool")
3749 ##
3750 #
3751 # Load current matrix from disk.
3752 #
3753 def on_action_Read_triggered(self, checked):
3754 # Check if there are unsaved changes
3755 if not self.okToContinue():
3756 return
3757
3758 # Set open directory to directory of current file or to lastDir if there is no open file
3759 dir = self.mainWindow.P.outDir
3760
3761 # Ask for file name
3762 formats = ["*.cm", "*.tcm", "*.fcm", "*.fcm_uniQ", "*.pfi", "*.ina", "*.ins", "*.arrow", "*.cds", "*.triplet", "*.cqs" ]
3763 fileName = unicode(QFileDialog.getOpenFileName(self, "%s - Open Coupling Matrix File" % applicationName, dir, "Coupling matrices files (%s)" % " ".join(formats)))
3764
3765 # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
3766 if fileName:
3767 try:
3768 self.CM.MatQ = self.CM.readCouplingMatrix(fileName)
3769 except parseError, err:
3770 QMessageBox.critical(self, "ERROR", '<p align="center">Error reading coupling matrix file:<br>%s</p>' % err)
3771 return False
3772 self.loadMatrixQ(self.CM.MatQ)
3773 self.matrixEditDlg.lastEditAction = "Matrix '%s' read from disk file '%s'" % (self.CM.MatQ.name, os.path.basename(fileName) )
3774 self.pushEditAction()
3775
3776
3777 @pyqtSignature("bool")
3778 ##
3779 #
3780 # Save all matrices to disk, using default file names and extensions.
3781 #
3782 def on_action_SaveAll_triggered(self, checked):
3783 self.CM.saveCouplingMatrices(self.mainWindow.P, self.mainWindow.SP, saveSignificantDigits, saveSignificantDigitsEnergy)
3784
3785
3786 @pyqtSignature("bool")
3787 ##
3788 #
3789 # Save current matrix in disk.
3790 #
3791 def on_action_Save_triggered(self, checked):
3792 # Ask for file name
3793 formats = ["*.cm", "*.tcm", "*.fcm", "*.fcm_uniQ", "*.pfi", "*.ina", "*.ins", "*.arrow", "*.cds", "*.triplet", "*.cqs" ]
3794 dir = self.mainWindow.P.outDir
3795 fileName = unicode(QFileDialog.getSaveFileName(self, "%s - Save Coupling Matrix File" % applicationName, dir, "Coupling matrices files (%s)" % " ".join(formats)))
3796
3797 # If fileName is valid, save parameters. Otherwise (File Save dialog cancelled by user) do nothing
3798 if fileName:
3799 fileBaseName = os.path.basename(fileName)
3800 if "." not in fileBaseName:
3801 fileName += '.cm'
3802 # + self.CM.MatQ.fileExt
3803
3804 self.CM.MatQ.saveMat(fileName, self.mainWindow.P, self.mainWindow.SP, saveSignificantDigits, saveSignificantDigitsEnergy)
3805 myPrint("Coupling Matrix saved to file: %s" % fileName)
3806
3807
3808 @pyqtSignature("bool")
3809 ##
3810 #
3811 # Store current matrix in self.CM_comboBox list.
3812 # @param checked = SIGNAL/SLOT parameter.
3813 # @param programmed = Flag that is true when this function is executed by the program, not the user, to avoid setting CM_comboBox CurrentIndex. Default False.
3814 #
3815 def on_action_ListAdd_triggered(self, checked, programmed=False):
3816 MatQ = self.CM.MatQ
3817 name = MatQ.name
3818 index = self.CM_comboBox.findText(name, Qt.MatchExactly)
3819
3820 if index >= 0: # name exists in list
3821 message = "Matrix with same name already exists in list\nReplace?"
3822 reply = QMessageBox.question(self, "%s - Matrix exists" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
3823 if reply == QMessageBox.No: # Exit without doing anything
3824 return False
3825 elif reply == QMessageBox.Yes:
3826 self.CM.listM[index] = MatQ.copy()
3827 myPrint("Coupling matrix '%s' replaced in list" % name)
3828 if self.matrixEditDlg is not None and self.recordChanges:
3829 self.matrixEditDlg.lastEditAction = "Matrix '%s' replaced in list" % name
3830 self.pushEditAction()
3831 else: # Append matrix to list
3832 self.CM.listM.append(MatQ.copy())
3833 self.CM_comboBox.addItem(name)
3834 index = self.CM_comboBox.count() - 1
3835 self.CM.indexCM = index
3836 myPrint("Coupling matrix '%s' appended to list" % name)
3837 if self.matrixEditDlg is not None and self.recordChanges:
3838 self.matrixEditDlg.lastEditAction = "Matrix '%s' appended to list" % name
3839 self.pushEditAction()
3840
3841 self.dirty = False
3842 self.setWindowModified(self.dirty)
3843
3844 if programmed is False:
3845 # Call setCurrentIndex AFTER setting self.dirty = False, because on_CM_comboBox_currentIndexChanged will be automatically and it calls self.okToContinue() .
3846 self.recordChanges = False
3847 self.CM_comboBox.setCurrentIndex(index)
3848 self.recordChanges = True
3849
3850
3851 @pyqtSignature("bool")
3852 ##
3853 #
3854 # Remove current matrix from self.CM_comboBox and CM.listM lists.
3855 #
3856 def on_action_ListRemove_triggered(self, checked):
3857 if self.CM_comboBox.count() == 0:
3858 QMessageBox.warning(self, "WARNING", 'Coupling matrices list is empty' )
3859 return
3860
3861 index = self.CM_comboBox.currentIndex()
3862 name = str(self.CM_comboBox.currentText())
3863
3864 message = "Are you sure you what to remove matrix '%s' from list?\nRemove?" % name
3865 reply = QMessageBox.question(self, "%s - Remove matrix" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
3866
3867 if reply == QMessageBox.Yes:
3868 self.recordChanges = False # Disable changes to avoid CM_comboBox.removeItem re-loading the same matrix
3869 self.CM_comboBox.removeItem(index)
3870 del self.CM.listM[index]
3871 self.recordChanges = True
3872 myPrint("Coupling matrix '%s' removed from list" % name)
3873
3874 if self.CM_comboBox.count() > 0:
3875 # Load the matrix corresponding to the new index in CM_comboBox
3876 self.on_CM_comboBox_currentIndexChanged(self.CM_comboBox.currentIndex())
3877 self.CM.indexCM = self.CM_comboBox.currentIndex()
3878
3879
3880 @pyqtSignature("bool")
3881 ##
3882 #
3883 # Plot stored energy and dissipated power computed from current coupling matrix.
3884 #
3885 def on_action_PlotEnergy_triggered(self, checked):
3886 SP = self.mainWindow.SP
3887 SP.energyPowerFromCM(self.CM.MatQ)
3888 Nr = np.size(SP.energyRes,1) # Number of resonators
3889
3890 # Prepare a list of curves for potting
3891 energyCurvesList, energyRevCurvesList, powerCurvesList, powerRevCurvesList, resonatorNames = [], [], [], [], []
3892 for n in range(Nr):
3893 energyCurvesList.append(1e9 * SP.energyRes[:, n])
3894 energyRevCurvesList.append(1e9 * SP.energyResRev[:, n])
3895 powerCurvesList.append(SP.powerRes[:, n])
3896 powerRevCurvesList.append(SP.powerResRev[:, n])
3897 resonatorNames.append('Resonator %d' % (n+1) )
3898
3899 totalEnergyRes = 1e9 * SP.energyRes.sum(axis=1)
3900 totalEnergyResRev = 1e9 * SP.energyResRev.sum(axis=1)
3901 totalPowerRes = SP.powerRes.sum(axis=1)
3902 totalPowerResRev = SP.powerResRev.sum(axis=1)
3903 energyCurvesList.append(totalEnergyRes)
3904 energyRevCurvesList.append(totalEnergyResRev)
3905 powerCurvesList.append(totalPowerRes)
3906 powerRevCurvesList.append(totalPowerResRev)
3907 resonatorNames.append('Total')
3908
3909 # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
3910 eps = 10** -self.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
3911
3912 powerCoupCurves, powerCoupRevCurves = [], []
3913 powerCoupNames = []
3914 powerCoupTotal = np.zeros(SP.freq.shape)
3915 powerCoupRevTotal = 0
3916 for coup in SP.powerCoup:
3917 if np.abs(coup[1]) < eps: continue # Filter couplings with Gmn smaller than matrix precision
3918 (m, n) = coup[0]
3919 powerCoupTotal += coup[2]
3920 powerCoupRevTotal += coup[3]
3921 powerCoupCurves.append(coup[2])
3922 powerCoupRevCurves.append(coup[3])
3923 powerCoupNames.append('(%s,%s) = %.3g' % (self.rowIndices[m], self.rowIndices[n], coup[1] ))
3924
3925 powerCoupCurves.append(powerCoupTotal)
3926 powerCoupRevCurves.append(powerCoupRevTotal)
3927 powerCoupNames.append('Total')
3928
3929 # powerCoupCurves.append(totalPowerRes)
3930 # powerCoupRevCurves.append(totalPowerResRev)
3931 # powerCoupNames.append('Total at resonators')
3932
3933 # energyCoupCurves, energyCoupRevCurves = [], []
3934 # energyCoupNames = []
3935 # for coup in SP.energyCoup:
3936 # if np.abs(coup[1]) < eps: continue # Filter couplings with Bmn smaller than matrix precision
3937 # (m, n) = coup[0]
3938 # energyCoupCurves.append(1e9*coup[2])
3939 # energyCoupRevCurves.append(1e9*coup[3])
3940 # energyCoupNames.append('(%s,%s) = %.3g' % (self.rowIndices[m], self.rowIndices[n], coup[1] ))
3941
3942 # Check if an EnergyPlot plot exists to create a new one or to replace existing data.
3943 if self.mainWindow.EnergyPlot is None or self.mainWindow.EnergyPlot.closed:
3944 # Now that there is no open EnergyPlot window, create a new one.
3945 self.mainWindow.EnergyPlot = EnergyDbPlot(parent=self,
3946 windowTitle = 'Stored Energy and dissipated power',
3947 tabTitle = 'Energy',
3948 title = 'Stored energy in the resonators computed from CM',
3949 XData = SP.freq / 1e9,
3950 leftYData = energyCurvesList,
3951 leftYNames = resonatorNames,
3952 XLabel = 'Frequency (GHz)',
3953 leftYLabel = 'nJ',
3954 Xunits = 'GHz', leftYunits = 'nJ',
3955 )
3956 self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = energyRevCurvesList, rightYDataRev = None)
3957
3958 self.mainWindow.EnergyPlot.addTab(tabTitle = 'Power',
3959 title = 'Dissipated power in the resonators computed from CM',
3960 XData = SP.freq / 1e9,
3961 leftYData = powerCurvesList,
3962 leftYNames = resonatorNames,
3963 XLabel = 'Frequency (GHz)',
3964 leftYLabel = 'W',
3965 Xunits = 'GHz', leftYunits = 'W',
3966 )
3967 self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = powerRevCurvesList, rightYDataRev = None)
3968
3969 self.mainWindow.EnergyPlot.addTab( tabTitle = 'Couplings power',
3970 title = 'Dissipated power in the couplings computed from CM',
3971 XData = SP.freq / 1e9,
3972 leftYData = powerCoupCurves,
3973 leftYNames = powerCoupNames,
3974 XLabel = 'Frequency (GHz)',
3975 leftYLabel = 'W',
3976 Xunits = 'GHz', leftYunits = 'W',
3977 )
3978 self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = powerCoupRevCurves, rightYDataRev = None)
3979
3980 else:
3981 self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3982 leftYData = energyCurvesList,
3983 leftYNames = resonatorNames,
3984 replaceTab = 0 )
3985 self.mainWindow.EnergyPlot.replaceReverseData(0, leftYDataRev = energyRevCurvesList, rightYDataRev = None)
3986
3987 self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3988 leftYData = powerCurvesList,
3989 leftYNames = resonatorNames,
3990 replaceTab = 1 )
3991 self.mainWindow.EnergyPlot.replaceReverseData(1, leftYDataRev = powerRevCurvesList, rightYDataRev = None)
3992
3993 self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3994 leftYData = powerCoupCurves,
3995 leftYNames = powerCoupNames,
3996 replaceTab = 2 )
3997 self.mainWindow.EnergyPlot.replaceReverseData(2, leftYDataRev = powerCoupRevCurves, rightYDataRev = None)
3998
3999
4000 # self.mainWindow.EnergyPlot.addTab( tabTitle = 'Couplings energy',
4001 # title = 'Stored energy (W<sub>m</sub> - W<sub>e</sub>) at magnetic or electric couplings computed from CM',
4002 # XData = SP.freq / 1e9,
4003 # leftYData = energyCoupCurves,
4004 # leftYNames = energyCoupNames,
4005 # XLabel = 'Frequency (GHz)',
4006 # leftYLabel = 'nJ',
4007 # Xunits = 'GHz', leftYunits = 'nJ',
4008 # )
4009 # self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = energyCoupRevCurves, rightYDataRev = None)
4010
4011
4012 @pyqtSignature("bool")
4013 ##
4014 #
4015 # Plot [S] parameters computed from current coupling matrix.
4016 # S21 and S11 are plotted on top of [S] parameters computed from Characteristic Polynomials.
4017 #
4018 def on_action_PlotS_triggered(self, checked):
4019 SP = self.mainWindow.SP
4020
4021 # Compute [S] parameters from current coupling matrix
4022 myPrint('Displayed coupling matrix:')
4023 stErrorCM, CM_error = SP.fromCouplingMatrix(self.CM.MatQ) # Do not update CM.CM_error with these results, since the matrix may have been edited by the user.
4024
4025 # Check if an [S] parameter plot exists and, if not, create a new one with [S] computed from CPs
4026 if self.mainWindow.SPlotError is None or self.mainWindow.SPlotError.closed is True:
4027 # Set dynamic range to the lowest value of S21 at sweep range ends.
4028 dynamicRange = -20*log10( min([ abs(SP.S21[0]), abs(SP.S21[-1] ), abs(SP.S21M[0]), abs(SP.S21M[-1] )]))
4029 dynamicRange = 10*ceil(dynamicRange/10)
4030
4031 self.mainWindow.SPlotError = DbPlot(parent=self.mainWindow,
4032 windowTitle = '[S] parameters CM vs CP',
4033 tabTitle = 'S11',
4034 title = 'S11 parameter computed from CM and CP',
4035 XData = SP.freq / 1e9,
4036 leftYData = [20*np.log10(np.abs(SP.S11)), 20*np.log10(np.abs(SP.S11M)), 20*np.log10(np.abs(SP.S22)), 20*np.log10(np.abs(SP.S22M)) ],
4037 leftWidth = [2, 1, 2, 1],
4038 leftYmax = 0, leftYmin = -dynamicRange,
4039 leftYNames = ['S<sub>11</sub> from CP', 'S<sub>11</sub> from CM', 'S<sub>22</sub> from CP', 'S<sub>22</sub> from CM'] ,
4040 leftYVisible = [True, True, False, False],
4041 XLabel = 'Frequency (GHz)',
4042 leftYLabel = 'dB',
4043 Xunits = 'GHz', leftYunits = 'dB',
4044 rightYData = [ SP.phaseS11 * 180/np.pi, SP.phaseS11M * 180/np.pi, SP.phaseS22 * 180/np.pi, SP.phaseS22M * 180/np.pi ],
4045 rightWidth = [2, 1, 2, 1],
4046 rightYunits = 'deg', rightYLabel = 'degrees' ,
4047 rightYNames = ['Phase(S<sub>11</sub>) from CP', 'Phase(S<sub>11</sub>) from CM', 'Phase(S<sub>22</sub>) from CP', 'Phase(S<sub>22</sub>) from CM'],
4048 rightYVisible = [False, False, False, False],
4049 textLabel =stErrorCM
4050 )
4051
4052 self.mainWindow.SPlotError.addTab(tabTitle = 'S21',
4053 title = 'S21 parameter computed from CM and CP',
4054 XData = SP.freq / 1e9,
4055 leftYData = [20*np.log10(np.abs(SP.S21)), 20*np.log10(np.abs(SP.S21M)) ],
4056 leftWidth = [2, 1] ,
4057 leftYmax = 0, leftYmin = -dynamicRange,
4058 leftYNames = ['S<sub>21</sub> from CP', 'S<sub>21</sub> from CM'] ,
4059 XLabel = 'Frequency (GHz)',
4060 leftYLabel = 'dB',
4061 Xunits = 'GHz', leftYunits = 'dB',
4062 rightYData = [ SP.phaseS21 * 180/np.pi, SP.phaseS21M * 180/np.pi ],
4063 rightWidth = [2, 1],
4064 rightYunits = 'deg', rightYLabel = 'degrees' ,
4065 rightYNames = ['Phase(S<sub>21</sub>) from CP', 'Phase(S<sub>21</sub>) from CM'],
4066 rightYVisible = [False, False],
4067 )
4068
4069 self.mainWindow.SPlotError.addTab(tabTitle = 'Group delay',
4070 title = 'Group delay computed from CM and CP',
4071 XData = SP.freq2 / 1e9,
4072 leftYData = [ 1e9 * SP.groupDelay2, 1e9 * SP.groupDelayM ],
4073 leftWidth = [2, 1] ,
4074 leftYNames = ['Group delay from CP', 'Group delay from CM'],
4075 XLabel = 'Frequency (GHz)',
4076 leftYLabel = 'Group delay (ns.)',
4077 Xunits = 'GHz', leftYunits = 'ns',
4078 )
4079
4080 else:
4081 self.mainWindow.SPlotError.update(0, XData = SP.freq / 1e9, leftYData = [20*np.log10(np.abs(SP.S11)), 20*np.log10(np.abs(SP.S11M)), 20*np.log10(np.abs(SP.S22)), 20*np.log10(np.abs(SP.S22M))],
4082 rightYData = [ SP.phaseS11 * 180/np.pi, SP.phaseS11M * 180/np.pi, SP.phaseS22 * 180/np.pi, SP.phaseS22M * 180/np.pi ],
4083 textLabel =stErrorCM, autoScaleBottomX = self.mainWindow.autoScaleFreq )
4084
4085 self.mainWindow.SPlotError.update(1, XData = SP.freq / 1e9, leftYData = [20*np.log10(np.abs(SP.S21)), 20*np.log10(np.abs(SP.S21M))],
4086 rightYData = [ SP.phaseS21 * 180/np.pi, SP.phaseS21M * 180/np.pi ], autoScaleBottomX = self.mainWindow.autoScaleFreq)
4087
4088 self.mainWindow.SPlotError.update(2, XData = SP.freq2 / 1e9, leftYData = [1e9 * SP.groupDelay2, 1e9 * SP.groupDelayM], autoScaleBottomX = self.mainWindow.autoScaleFreq )
4089
4090 self.mainWindow.autoScaleFreq = False
4091
4092
4093 # Plot masks, or update if there are any
4094 if self.mainWindow.SM is not None:
4095 self.mainWindow.SPlotError.deleteMasks()
4096 self.mainWindow.plotMasks()
4097
4098
4099 @pyqtSignature("int")
4100 ##
4101 #
4102 # Load a new coupling matrix into the QTableWidget.
4103 # This function is automatically executed by the GUI when the user changes the coupling matrix type comboBox.
4104 #
4105 def on_CM_comboBox_currentIndexChanged(self, index):
4106 if self.noLoadCM: return
4107
4108 # Check if matrix has been modified and not saved
4109 if not self.okToContinue(): # self.recordChanges condition is necessary to avoid recursion
4110 QMessageBox.warning(self, "WARNING", 'User cancelled action.<p>Coupling Matrix has not been loaded from list.' )
4111 # Restore last index to comboBox
4112 self.noLoadCM = True
4113 self.CM_comboBox.setCurrentIndex(self.CM.indexCM) # Will can on_CM_comboBox_currentIndexChanged() recursively
4114 self.noLoadCM = False
4115 return
4116
4117 MatQ = self.CM.listM[index].copy() # Deep copy: we do not want to modify the precomputed matrices
4118 self.CM.MatQ = MatQ
4119 self.loadMatrixQ(MatQ)
4120 if self.recordChanges:
4121 self.CM.indexCM = index
4122
4123 self.dirty = False
4124 self.setWindowModified(self.dirty)
4125
4126 # After the matrix editor has been opened for the first time, loading a matrix from the list is an edit action than modifies CM.MatQ, so it must be recorded in the changes lists.
4127 if self.matrixEditDlg is not None and self.recordChanges:
4128 self.matrixEditDlg.lastEditAction = "Matrix %s loaded from list" % MatQ.name
4129 self.pushEditAction()
4130
4131
4132 def pushEditAction(self):
4133 self.matrixEditDlg.edit_listWidget.addItem(self.matrixEditDlg.lastEditAction)
4134 self.matrixEditDlg.bkpActions.append(self.matrixEditDlg.lastEditAction)
4135 self.matrixEditDlg.edit_listWidget.setCurrentRow(self.matrixEditDlg.edit_listWidget.count()-1) # Set edit_listWidget current row to last one
4136 myPrint("Done: Matrix edit action '%s'" % self.matrixEditDlg.lastEditAction)
4137 self.setWindowModified(self.dirty)
4138 self.CM_name.deselect()
4139
4140 # Push modified CM.MatQ to backup list. Must use deep copy, since CM.MatQ will be modified by edit actions.
4141 self.matrixEditDlg.bkpMatQ.append(self.CM.MatQ.copy())
4142
4143 ##@}
4144
4145 # End class: CouplingMatrixDlg #
4146 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
4147
4148 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
4149 # Begin class: ParamEditDlg #
4150
4151 ##
4152 #
4153 # GUI dialog to edit synthesis parameters
4154 #
4155 class ParamEditDlg(QMainWindow, Ui_parameditdlg.Ui_ParamEditDlg):
4156
4157 ##
4158 #
4159 # Constructor: Creates dialog window containing values of parameters.
4160 # Modifies MainWindow.P , using a shallow copy for easier access.
4161 # Saves a backup copy of the mainWindow Parameters and synthesis results, that will be restored if the user Discards the dialog.
4162 # @param parent = Parent widget, in this case is mainWindow.
4163 #
4164 def __init__(self, parent=None):
4165 super(ParamEditDlg, self).__init__(parent)
4166 self.setAttribute(Qt.WA_DeleteOnClose)
4167
4168 ##
4169 # Index of the Center Frequency Units comboBox.
4170 # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4171 self.f0UnitsIndex = None
4172
4173 ##
4174 # Index of the Bandwidth Units comboBox.
4175 # It is used to check if the user has changed the Bandwidth Units in the GUI and to update the Bandwidth value accordingly.
4176 self.BWUnitsIndex = None
4177
4178 ##
4179 # Index of the Quasieliptic zero frequency units comboBox.
4180 # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4181 self.qeZeroUnitsIndex = None
4182
4183 ##
4184 # Index of the Generalized Chebyshev zeros frequency units comboBox.
4185 # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4186 self.transmissionZerosUnitsIndex = None
4187
4188 ##
4189 # Index of the Minimum Frequency Units comboBox.
4190 # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4191 self.minFreqUnitsIndex = None
4192
4193 ##
4194 # Index of the Maximum Frequency Units comboBox.
4195 # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4196 self.maxFreqUnitsIndex = None
4197
4198 ##
4199 # Variable that becomes true when the user clicks the 'Discard' button to reject the dialog.
4200 # Its is necessary to use this variable, since now the dialog is a QMainWindow instead of a QDialog, and QMainWindow does not have a reject() slot.
4201 self.rejected = False
4202
4203 ##
4204 # Variable that is False when it is necessary to recompute synthesis because Parameters have been modified.
4205 self.recomputed = True
4206
4207 ##
4208 # Variable to store a list of widgets with bad input.
4209 # Only when this list is empty, he "Accept" and "Compute" buttons are enabled.
4210 self.badInput = [ ]
4211
4212 ##
4213 # Variable to store sweep value in order to check if it has been changed by the user, and then update frequency axis. Tuple (fmin, fmax)
4214 oldSweep = None
4215
4216 ##
4217 # Copy of the mainWindow class instance.
4218 # It is necessary to store a copy since it must be accessed by some member functions.
4219 self.mainWindow = parent
4220 self.setupUi(self)
4221
4222 # if importedExtraFilters:
4223 # self.topol_comboBox.removeItem(self.topol_comboBox.findText(u'Quasieliptic'))
4224 # self.filter_stackedWidget.removeWidget(self.filterQuasieliptic_page)
4225
4226 ##
4227 # Shallow copy just for easier access.
4228 self.P = self.mainWindow.P
4229
4230 ##
4231 # Backup copy of the mainWindow Parameters and synthesis results, that will be restored if the user Discards the dialog.
4232 self.resultBkp = copy.deepcopy( (self.mainWindow.P, self.mainWindow.CP, self.mainWindow.CM, self.mainWindow.SP, self.mainWindow.FT) )
4233
4234 ##
4235 # Initial dialog size as set in Qt designer
4236 self.dlgInitSize = self.size().height()
4237
4238 # Set active tab to 'Filter parameters'
4239 self.tabWidget.setCurrentIndex(0)
4240
4241 # Connect self.N_spinBox to a method that updates pre-distortion zeros arrangement.
4242 self.connect(self.N_spinBox, SIGNAL("valueChanged(int)"), self.updatePredisZerosArrang )
4243
4244 # Create adaptive predistortion weights table
4245 self.wFunc_horizontalLayout = QHBoxLayout()
4246 self.wFunc_horizontalLayout.addStretch()
4247 self.wFunc_tableWidget = myTableWidget(name = 'Adaptive predistortion weights', parent = self.adapPredis_groupBox)
4248 self.wFunc_tooltip = "Adaptive predistortion weights.\nThe shift is equal to the weight times sigma:\nweights = 1 lead to conventional pole shifting.\nTable entries are editable."
4249 self.wFunc_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.wFunc_tooltip, None, QApplication.UnicodeUTF8))
4250 self.wFunc_tableWidget.horizontalHeader().setVisible(False)
4251 self.wFunc_horizontalLayout.addWidget(self.wFunc_tableWidget)
4252 self.wFunc_horizontalLayout.addStretch()
4253 self.wFunc_verticalLayout.addLayout(self.wFunc_horizontalLayout)
4254 self.wFunc_verticalLayout.setAlignment(Qt.AlignHCenter)
4255 self.connect(self.N_spinBox, SIGNAL("valueChanged(int)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), True ) )
4256 self.connect(self.adapPredis_groupBox, SIGNAL("toggled(bool)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), True ) )
4257 self.connect(self.wFunc_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), False ))
4258
4259 # Create GenCheby transmission zeros List
4260 self.genChebyTZs_horizontalLayout = QHBoxLayout()
4261 self.genChebyTZs_horizontalLayout.addStretch()
4262 self.genChebyTZs_tableWidget = myTableWidget(name = 'Transmission zeros', mainDlg = self, parent = self.genChebyLP_groupBox)
4263 self.genChebyTZs_tableWidget.setUnits('MHz')
4264 self.genChebyTZs_tooltip = "Transmission zeros (complex or purely imaginary). \nReal part in normalized frequency units and imaginary part in MHz. Imaginary part cannot be 0 MHz. \nFor complex zeros, a symmetric zero with opposite real part is automatically added."
4265 self.genChebyTZs_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.genChebyTZs_tooltip, None, QApplication.UnicodeUTF8))
4266 self.genChebyTZs_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
4267 self.genChebyTZs_horizontalLayout.addWidget(self.genChebyTZs_tableWidget)
4268 self.genChebyTZs_horizontalLayout.addStretch()
4269 self.genCheby_verticalLayout.insertLayout(2, self.genChebyTZs_horizontalLayout)
4270 self.genCheby_verticalLayout.setAlignment(Qt.AlignHCenter)
4271
4272 self.connect(self.genChebyTZs_tableWidget, SIGNAL("cellChanged(int, int)"), self.genChebyTZs_tableWidget.tableCellChanged)
4273
4274 # Create GenCheby LP complex zeros List
4275 self.LPcomplexZeros_horizontalLayout = QHBoxLayout()
4276 self.LPcomplexZeros_horizontalLayout.addStretch()
4277 self.LPcomplexZeros_tableWidget = myTableWidget(name = 'Initial positions of complex zeros to optimize', mainDlg = self, parent = self.genChebyLP_groupBox)
4278 self.LPcomplexZeros_tableWidget.setUnits('MHz')
4279 self.LPcomplexZeros_tooltip = "Initial positions of complex zeros to optimize for linear phase equalization. \nReal part in normalized frequency units and imaginary part in MHz. \nImaginary part of a complex zero cannot be 0 MHz. \nIf imaginary part is empty, the zero is purely real.\nA symmetric zero with opposite real part is automatically added."
4280 self.LPcomplexZeros_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.LPcomplexZeros_tooltip, None, QApplication.UnicodeUTF8))
4281 self.LPcomplexZeros_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
4282 self.LPcomplexZeros_horizontalLayout.addWidget(self.LPcomplexZeros_tableWidget)
4283 self.LPcomplexZeros_horizontalLayout.addStretch()
4284 self.genChebyLPverticalLayout.insertLayout(4, self.LPcomplexZeros_horizontalLayout)
4285 self.genChebyLPverticalLayout.setAlignment(Qt.AlignHCenter)
4286
4287 self.connect(self.LPcomplexZeros_tableWidget, SIGNAL("cellChanged(int, int)"), self.LPcomplexZeros_tableWidget.tableCellChanged)
4288
4289 # Update TZs tables when changing f0
4290 self.connect(self.f0_lineEdit, SIGNAL("editingFinished()"), self.genChebyTZs_tableWidget.setf0 )
4291 self.connect(self.f0_lineEdit, SIGNAL("editingFinished()"), self.LPcomplexZeros_tableWidget.setf0 )
4292
4293 # Load parameters into GUI widgets
4294 self.updateDialogParams()
4295
4296 # Create validators. Validator regular expressions are global variables.
4297 #realValidator = QDoubleValidator(self)
4298 realValidator = QRegExpValidator( reReal , self)
4299 #realPositiveValidator = QDoubleValidator(self)
4300 #realPositiveValidator.setBottom(0)
4301 realPositiveValidator = QRegExpValidator( reRealPositive, self)
4302 realPositiveValidatorInf = QRegExpValidator( reRealPositiveInf, self)
4303 complexValidator = QRegExpValidator( reComplex, self)
4304 realListValidator = QRegExpValidator( reRealList, self)
4305 realPositiveListValidator = QRegExpValidator( reRealPositiveList, self)
4306 complexListValidator = QRegExpValidator( reComplexList, self)
4307
4308 # Set validators
4309 self.f0_lineEdit.setValidator(realPositiveValidator)
4310 self.BW_lineEdit.setValidator(realPositiveValidator)
4311 self.maxFreq_lineEdit.setValidator(realPositiveValidator)
4312 self.minFreq_lineEdit.setValidator(realPositiveValidator)
4313 self.chebyLr_lineEdit.setValidator(realPositiveValidator)
4314 self.qeLr_lineEdit.setValidator(realPositiveValidator)
4315 self.qeZero_lineEdit.setValidator(realPositiveValidator)
4316 self.genChebyLr_lineEdit.setValidator(realPositiveValidator)
4317 # self.transmissionZeros_lineEdit.setValidator(realPositiveListValidator)
4318 # self.LPcomplexZeros_lineEdit.setValidator(complexListValidator)
4319 # self.LPfracBW_lineEdit.setValidator(realPositiveValidator) # Now this widget is a doubleSpinBox
4320 # self.LPripple_lineEdit.setValidator(realPositiveValidator) # Now this widget is a doubleSpinBox
4321 self.Qo_lineEdit.setValidator(realPositiveValidatorInf)
4322 self.QoPredis_lineEdit.setValidator(realPositiveValidatorInf)
4323 self.Qef_lineEdit.setValidator(realPositiveValidatorInf)
4324 # self.wFunc_lineEdit.setValidator(realListValidator)
4325
4326 self.nuqK11c1_lineEdit.setValidator(realPositiveValidator)
4327 self.nuqK22c1_lineEdit.setValidator(realPositiveValidator)
4328 self.nuqK21c1_lineEdit.setValidator(realPositiveValidator)
4329 self.nuqK21c2_lineEdit.setValidator(realPositiveValidator)
4330 self.nuqK21c3_lineEdit.setValidator(realPositiveValidator)
4331
4332 self.nuqDelta_lineEdit.setValidator(realPositiveValidator)
4333
4334 ##
4335 # Variable to store the modified/unmodifed status of the dialog working copy of parameters self.P.
4336 # The dialogChanged() method is automatically called when the user modifies the edit widgets contents and sets this variable to True.
4337 # This variable must be set to False at the end of the __init__ dialog constructor,
4338 # since dialogChanged() is called when the constructor sets the edit widgets contents.
4339 self.dialogModified = False
4340
4341
4342 ##
4343 #
4344 # Updates paramEditDlg widgets with contents of local copy of P (self.P).
4345 # @param autoFreqUnits = If True, automatically update frequency units in ComboBoxes. It is True by default.
4346 #
4347 def updateDialogParams(self, autoFreqUnits = True):
4348
4349 # Local copy of parameters
4350 P = self.P
4351
4352 # Set output file and directory names
4353 if self.P.outName is not None: self.outName_lineEdit.setText(self.P.outName)
4354 if self.P.outDir is not None: self.outDir_lineEdit.setText(self.P.outDir)
4355
4356 # Set Synthesis parameters widgets
4357 if self.P.N is not None: self.N_spinBox.setValue(self.P.N)
4358
4359 if self.P.f0 is not None:
4360 (freq, units) = self.frequencyUnits(self.P.f0)
4361 autoUnitsIndex = self.f0_comboBox.findText(units)
4362
4363 if autoFreqUnits: # Set automatic frequency units
4364 self.f0_lineEdit.setText(str(freq))
4365 self.f0_comboBox.setCurrentIndex(autoUnitsIndex)
4366 self.f0UnitsIndex = autoUnitsIndex
4367 else: # Keep user frequency units
4368 userUnitsIndex = self.f0_comboBox.currentIndex()
4369 newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4370 self.f0_lineEdit.setText(str(newValue))
4371 self.f0UnitsIndex = userUnitsIndex
4372 else:
4373 self.f0UnitsIndex = self.f0_comboBox.currentIndex()
4374
4375 # Update f0 in TZs tables, for computation of f-f0 in 3rd column
4376 self.genChebyTZs_tableWidget.setf0(False)
4377 self.LPcomplexZeros_tableWidget.setf0(False)
4378
4379 if self.P.BW is not None:
4380 (freq, units) = self.frequencyUnits(self.P.BW)
4381 autoUnitsIndex = self.BW_comboBox.findText(units)
4382
4383 if autoFreqUnits: # Set automatic frequency units
4384 self.BW_lineEdit.setText(str(freq))
4385 self.BW_comboBox.setCurrentIndex(autoUnitsIndex)
4386 self.BWUnitsIndex = autoUnitsIndex
4387 else: # Keep user frequency units
4388 userUnitsIndex = self.BW_comboBox.currentIndex()
4389 newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4390 self.BW_lineEdit.setText(str(newValue))
4391 self.BWUnitsIndex = userUnitsIndex
4392 else:
4393 self.BWUnitsIndex = self.BW_comboBox.currentIndex()
4394
4395 if self.P.filterTransferFunc is not None:
4396 topol = self.P.filterTransferFunc.title() # Capital initials for safe comparison with GUI ComboBox items
4397 iTopol = self.topol_comboBox.findText(topol)
4398 else: iTopol = 0
4399
4400 if iTopol < 0:
4401 QMessageBox.warning(self, "WARNING", "Unknown Filter Topology '%s' replaced by default value." % self.P.filterTransferFunc)
4402 iTopol = 0
4403 self.topol_comboBox.setCurrentIndex(iTopol)
4404 self.filter_stackedWidget.setCurrentIndex(iTopol)
4405
4406 # Set filter topology widgets
4407 if self.P.chebyLr is not None: self.chebyLr_lineEdit.setText(str(self.P.chebyLr))
4408 if self.P.qeLr is not None: self.qeLr_lineEdit.setText(str(self.P.qeLr))
4409
4410 #if self.P.qeZero is not None: self.qeZero_lineEdit.setText(str(self.P.qeZero))
4411 if self.P.qeZero is not None:
4412 (freq, units) = self.frequencyUnits(self.P.qeZero)
4413 autoUnitsIndex = self.qeZero_comboBox.findText(units)
4414
4415 if autoFreqUnits: # Set automatic frequency units
4416 self.qeZero_lineEdit.setText(str(freq))
4417 self.qeZero_comboBox.setCurrentIndex(autoUnitsIndex)
4418 self.qeZeroUnitsIndex = autoUnitsIndex
4419 else: # Keep user frequency units
4420 userUnitsIndex = self.qeZero_comboBox.currentIndex()
4421 newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4422 self.qeZero_lineEdit.setText(str(newValue))
4423 self.qeZeroUnitsIndex = userUnitsIndex
4424 else:
4425 self.qeZeroUnitsIndex = self.qeZero_comboBox.currentIndex()
4426
4427 if self.P.genChebyLr is not None: self.genChebyLr_lineEdit.setText(str(P.genChebyLr))
4428
4429 #if self.P.transImagZeros is not None: self.transmissionZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.transImagZeros)))
4430
4431 # P.validation() ensures that when there are no TZs or LPZs, lists are not None, but empty.
4432
4433 listImaginaryPartsZeros = self.P.transImagZeros + np.imag( self.P.transComplexZeros + self.P.LPcomplexZeros ).tolist()
4434 if len(listImaginaryPartsZeros) > 0:
4435 (freq, units) = self.frequencyUnits(listImaginaryPartsZeros)
4436 autoUnitsIndex = self.transmissionZeros_comboBox.findText(units)
4437
4438 if autoFreqUnits: # Set automatic frequency units
4439 # self.transmissionZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(freq)))
4440 self.transmissionZeros_comboBox.setCurrentIndex(autoUnitsIndex)
4441 self.transmissionZerosUnitsIndex = autoUnitsIndex
4442 else: # Keep user frequency units
4443 userUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
4444 self.transmissionZerosUnitsIndex = userUnitsIndex
4445 else:
4446 self.transmissionZerosUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
4447
4448 if self.P.genChebyLP is not None: self.genChebyLP_groupBox.setChecked(bool(self.P.genChebyLP))
4449 # if self.P.LPcomplexZeros is not None: self.LPcomplexZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.LPcomplexZeros)))
4450
4451 if self.P.LPalgor is not None:
4452 algor = self.P.LPalgor.upper() # Uppercase letters for safe comparison with GUI ComboBox items
4453 iAlgor = self.LPalgor_comboBox.findText(algor)
4454 else:
4455 iAlgor = 4
4456
4457 if iAlgor < 0:
4458 QMessageBox.warning(self, "WARNING", "Unknown Linear Phase Optimization Algorithm '%s' replaced by default value." % self.P.LPalgor)
4459 iAlgor = 4
4460 self.LPalgor_comboBox.setCurrentIndex(iAlgor)
4461
4462 if self.P.LPmaxIter is not None: self.LPmaxIter_spinBox.setValue(self.P.LPmaxIter)
4463 if self.P.LPNsamples is not None: self.LPNsamples_spinBox.setValue(self.P.LPNsamples)
4464 if self.P.LPfracBW is not None: self.LPfracBW_doubleSpinBox.setValue(self.P.LPfracBW)
4465 if self.P.LPripple is not None: self.LPripple_doubleSpinBox.setValue(self.P.LPripple * 1e9)
4466
4467 # Update table Widgets with TZs. self.updateTZsTable() resizes, fills and adjusts the table.
4468 TZsList = np.concatenate( (1j*np.array(self.P.transImagZeros), np.array(self.P.transComplexZeros)) ).tolist()
4469 self.updateTZsTable(self.genChebyTZs_tableWidget, TZsList)
4470 self.updateTZsTable(self.LPcomplexZeros_tableWidget, self.P.LPcomplexZeros + self.P.LPrealZeros)
4471
4472 # Update total number of zeros in transmissionZeros_lcdNumber
4473 self.updateTotaltransmissionZeros()
4474
4475 # Set Synthesis parameters widgets
4476 if self.P.synthTech is not None:
4477 tech = self.P.synthTech.title() # Capital initials for safe comparison with GUI ComboBox items
4478 iTech = self.synthTech_comboBox.findText(tech)
4479 else:
4480 iTech = 0
4481
4482 if iTech < 0:
4483 QMessageBox.warning(self, "WARNING", "Unknown Synthesis Technique '%s' replaced by default value." % self.P.synthTech)
4484 iTech = 0
4485 self.synthTech_comboBox.setCurrentIndex(iTech)
4486 self.synthTech_stackedWidget.setCurrentIndex(iTech)
4487
4488 # Lossless parameters
4489 if self.P.finiteQo is not None: self.finiteQo_groupBox.setChecked(bool(self.P.finiteQo))
4490 if self.P.Qo is not None: self.Qo_lineEdit.setText(self.float2stInf(self.P.Qo))
4491
4492 # Predistortion parameters
4493 if self.P.zerosArrang is not None:
4494 if type(self.P.zerosArrang).__name__ == 'list': self.zerosArrang_comboBox.setCurrentIndex(0)
4495 else: self.zerosArrang_comboBox.setCurrentIndex(self.P.zerosArrang)
4496 if self.P.adapPredis is not None: self.adapPredis_groupBox.setChecked(bool(self.P.adapPredis))
4497 if self.P.QoPredis is not None: self.QoPredis_lineEdit.setText(self.float2stInf(self.P.QoPredis))
4498 if self.P.Qef is not None: self.Qef_lineEdit.setText(self.float2stInf(self.P.Qef))
4499
4500 # Update lineEdit with weights list
4501 #if self.P.wFunc is not None: self.wFunc_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.wFunc)))
4502
4503 # Update table Widget with weights list. self.updatewFuncTableSize() resizes, fills and adjusts the table.
4504 self.updatewFuncTableSize(self.P.wFunc, (self.P.N, 1), True)
4505
4506 # NU-Q Parameters
4507 if self.P.nuqK11c1 is not None: self.nuqK11c1_lineEdit.setText(str(self.P.nuqK11c1))
4508 if self.P.nuqK22c1 is not None: self.nuqK22c1_lineEdit.setText(str(self.P.nuqK22c1))
4509 if self.P.nuqK21c1 is not None: self.nuqK21c1_lineEdit.setText(str(self.P.nuqK21c1))
4510 if self.P.nuqK21c2 is not None: self.nuqK21c2_lineEdit.setText(str(self.P.nuqK21c2))
4511 if self.P.nuqK21c3 is not None: self.nuqK21c3_lineEdit.setText(str(self.P.nuqK21c3))
4512
4513 if self.P.nuqTech is not None:
4514 nuqTech= self.P.nuqTech # No capital initials in this case
4515 iNuqTech = self.nuqTech_comboBox.findText(nuqTech)
4516 else:
4517 iNuqTech = 0
4518
4519 if iNuqTech < 0:
4520 QMessageBox.warning(self, "WARNING", "Unknown Prescribed Insertion Loss technique '%s' replaced by default value." % self.P.nuqTech)
4521 iNuqTech = 0
4522 self.nuqTech_comboBox.setCurrentIndex(iNuqTech)
4523 self.nuqTech_stackedWidget.setCurrentIndex(iNuqTech)
4524
4525 if self.P.nuqDelta is not None: self.nuqDelta_lineEdit.setText(str(self.P.nuqDelta))
4526
4527 # Set sweep parameters widgets
4528 if self.P.minFreq is not None:
4529 (freq, units) = self.frequencyUnits(self.P.minFreq)
4530 autoUnitsIndex = self.minFreq_comboBox.findText(units)
4531
4532 if autoFreqUnits: # Set automatic frequency units
4533 self.minFreq_lineEdit.setText(str(freq))
4534 self.minFreq_comboBox.setCurrentIndex(autoUnitsIndex)
4535 self.minFreqUnitsIndex = autoUnitsIndex
4536 else: # Keep user frequency units
4537 userUnitsIndex = self.minFreq_comboBox.currentIndex()
4538 newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4539 self.minFreq_lineEdit.setText(str(newValue))
4540 self.minFreqUnitsIndex = userUnitsIndex
4541 else:
4542 self.minFreqUnitsIndex = self.minFreq_comboBox.currentIndex()
4543
4544 if self.P.maxFreq is not None:
4545 (freq, units) = self.frequencyUnits(self.P.maxFreq)
4546 autoUnitsIndex = self.maxFreq_comboBox.findText(units)
4547
4548 if autoFreqUnits: # Set automatic frequency units
4549 self.maxFreq_lineEdit.setText(str(freq))
4550 self.maxFreq_comboBox.setCurrentIndex(autoUnitsIndex)
4551 self.maxFreqUnitsIndex = autoUnitsIndex
4552 else: # Keep user frequency units
4553 userUnitsIndex = self.maxFreq_comboBox.currentIndex()
4554 newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4555 self.maxFreq_lineEdit.setText(str(newValue))
4556 self.maxFreqUnitsIndex = userUnitsIndex
4557 else:
4558 self.maxFreqUnitsIndex = self.maxFreq_comboBox.currentIndex()
4559
4560 if self.P.numFreq is not None: self.numFreq_spinBox.setValue(self.P.numFreq)
4561
4562 self.oldSweep = (P.minFreq, P.maxFreq)
4563
4564
4565 ##
4566 #
4567 # Update predistorsion weights table Widget with list.
4568 # @param list = list containing entries to set in tableWidget
4569 #
4570 def updatewFuncTableEntries(self, list):
4571 for n in range(len(list)):
4572 #item = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n]) )
4573 item = QTableWidgetItem( complexStr(list[n], saveSignificantDigits, eng = True) )
4574
4575 item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4576 self.wFunc_tableWidget.setItem(0, n, item)
4577
4578
4579 ##
4580 #
4581 # Read list from predistorsion weights table Widget entries.
4582 # If row or column count is equal to 1, the 2D list is collapsed to a 1D list.
4583 # @return list = list containing entries in tableWidget
4584 #
4585 def readwFuncTableEntries(self):
4586 table = self.wFunc_tableWidget
4587 M = table.rowCount()
4588 list = []
4589 for m in range(M):
4590 item = table.item(m, 0)
4591 try:
4592 if item is None: raise ValueError, 'Empty cell'
4593 value = float(item.text())
4594 except ValueError, err:
4595 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4596 if item is not None: item.setText('')
4597 else: list.append(value)
4598 if len(list) == 1: list = list[0]
4599
4600 return list
4601
4602
4603 ##
4604 #
4605 # Read list from TZs table Widget entries.
4606 # @param table = tableWidget to read
4607 # @return listImag = list containing purely imaginary zeros entries in tableWidget
4608 # @return listComplex = list containing complex zeros entries in tableWidget
4609 # @return listReal = list containing purely real zeros entries in tableWidget
4610 #
4611 def readTZsTableEntries(self, table):
4612
4613 M = table.rowCount()
4614 listImag = []
4615 listComplex = []
4616 listReal = []
4617
4618 for m in range(M):
4619 reItem = table.item(m, 0)
4620 stRe = str(reItem.text()).strip() if reItem is not None else ''
4621 try:
4622 re = float(stRe) if stRe != '' else 0
4623 except ValueError, err:
4624 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4625 table.currentItem().setText('')
4626 stRe = ''
4627 re = 0
4628
4629 if re == 0 and stRe != '': table.currentItem().setText('') # Replace zeros by empty strings in table cells
4630
4631 imItem = table.item(m, 1)
4632 stIm = str(imItem.text()).strip() if imItem is not None else ''
4633
4634 if stIm != '': # Complex zero or purely imaginary
4635 try:
4636 im = float(stIm)
4637 except ValueError, err:
4638 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4639 table.currentItem().setText('')
4640 continue
4641
4642 # DO NOT replace imaginary part by '': the former is a complex zero with 0 Hz imag part while the later is a real zero.
4643 # if im ==0: table.currentItem().setText('') # Replace zeros by empty strings in table cells
4644
4645 if re==0 and im==0: continue # Empty table row
4646
4647 stError = '<p align="center">Incorrect table entry at row %d:<br>%s</p><p align="center"><b>Please fix it.</b></p>'
4648 if im < 0:
4649 QMessageBox.critical(self, "ERROR", stError % (m+1, 'Imaginary part of a transmission zero cannot be negative Hz.'))
4650 if im == 0:
4651 QMessageBox.critical(self, "ERROR", stError % (m+1, 'Imaginary part of a transmission zero cannot be 0 Hz.'))
4652
4653 # append zero to list
4654 if re == 0: listImag.append(im * pow(10, 3*self.transmissionZerosUnitsIndex))
4655 else: listComplex.append(re + 1j*im * pow(10, 3*self.transmissionZerosUnitsIndex))
4656
4657 else: # Real zero
4658 if re==0: continue # Empty table row
4659 listReal.append(re)
4660
4661 return listImag, listComplex, listReal
4662
4663
4664 ##
4665 #
4666 # Updates a TZs tableWidget with contents of list, and
4667 # makes the number of rows equal to the number of zeros.
4668 # Adjusts widget size to table contents.
4669 # This function is called by the self.updateDialogParams() function.
4670 # @param table = tableWidget to update
4671 # @param list = list containing entries to set in tableWidget
4672 #
4673 def updateTZsTable(self, table, list):
4674 table.setRowCount(len(list))
4675 table.setColumnCount(3)
4676 stUnits = self.transmissionZeros_comboBox.itemText(self.transmissionZerosUnitsIndex)
4677 table.setHorizontalHeaderLabels( ['Real', 'Imag (%s)' % stUnits, 'Imag-f0 (%s)' % stUnits ] )
4678 table.setToolTip(unicode(table.toolTip()).replace(table.units, stUnits))
4679 table.setUnits(stUnits)
4680
4681 # Table is not being updated by the user!
4682 table.updatedByUser = False
4683
4684 for n in range(len(list)):
4685 if list[n].real != 0: reItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n].real) )
4686 else: reItem = QTableWidgetItem('')
4687 reItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4688 table.setItem(n, 0, reItem)
4689
4690 if list[n].imag != 0:
4691 imItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n].imag / pow(10, 3*self.transmissionZerosUnitsIndex)) )
4692 imItemf0 = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (list[n].imag - table.f0) / pow(10, 3*self.transmissionZerosUnitsIndex)) )
4693 else:
4694 imItem = QTableWidgetItem('')
4695 imItemf0 = QTableWidgetItem('')
4696 imItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4697 imItemf0.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4698 table.setItem(n, 1, imItem)
4699 table.setItem(n, 2, imItemf0)
4700
4701 table.updatedByUser = True
4702
4703 table.updateTableSize()
4704
4705
4706 ##
4707 #
4708 # Returns frequency expressed in GHz, MHz, 'kHz' or Hz.
4709 # @param freq = Frequency in Hz. Can be a float or a list of floats.
4710 # @return (newFreq, units).
4711 # @n newFreq = Frequency expressed in units.
4712 # @n units = 'GHz', 'MHz', 'kHz' or 'Hz'.
4713 #
4714 def frequencyUnits(self, freq):
4715 if freq == 0: return (0, 'Hz')
4716
4717 if type(freq) in [ types.FloatType, types.IntType, types.ComplexType ]: freq = [ freq ]
4718
4719 afreq = np.array(freq)
4720 freqExp = log10(np.prod(afreq)) / len(freq)
4721
4722 if freqExp >= 9:
4723 newFreq = afreq / 1e9
4724 units = 'GHz'
4725 elif freqExp >= 6:
4726 newFreq = afreq / 1e6
4727 units = 'MHz'
4728 elif freqExp >= 3:
4729 newFreq = afreq / 1e3
4730 units = 'kHz'
4731 else:
4732 newFreq = afreq
4733 units = 'Hz'
4734
4735 newFreq = newFreq.tolist()
4736 if len(newFreq) == 1: newFreq = newFreq[0]
4737
4738 return newFreq, units
4739
4740
4741 ##
4742 #
4743 # Convert string to float. 'INF', 'Inf', 'inf' are converted to numpy.inf.
4744 # @param st = String to convert.
4745 # @return value = Numerical value of string (float).
4746 #
4747 def st2floatInf(self, st):
4748 return np.inf if st.lower().lstrip().rstrip()=='inf' else float(st)
4749
4750
4751 ##
4752 #
4753 # Convert float to string. numpy.inf is converted to 'Inf'.
4754 # @param value = Float value to convert.
4755 # @return st = String.
4756 #
4757 def float2stInf(self, value):
4758 return 'Inf' if value==np.inf else str(value)
4759
4760
4761 ##
4762 #
4763 # Reads parameter values from dialog and store in self.P (dialog working copy of parameters).
4764 # Error control only for invalid parameter values that would crash the parser or the GUI.
4765 # Empty lineEdit boxes are allowed in parameters that will not be used in the computation.
4766 # More error control in lossyfilters.Parameters.validate() .
4767 # @return ok (True / False) = If True, parameters have been read correctly, if False, there has been an error.
4768 #
4769 def readDialogParams(self):
4770
4771 try:
4772 ##
4773 # key is used to store parameter name for error catching
4774 key = 'Filter / Filter order'
4775 self.P.N = self.N_spinBox.value()
4776
4777 key = 'Name of results output files'
4778 if self.outName_lineEdit.text() == u'': raise ValueError, 'Empty string'
4779 self.P.outName = unicode(self.outName_lineEdit.text())
4780
4781 key = 'Output directory'
4782 if self.outDir_lineEdit.text() == u'': self.P.outDir = None
4783 self.P.outDir = unicode(self.outDir_lineEdit.text())
4784
4785 key = 'Filter / Center Frequency'
4786 self.P.f0 = float(self.f0_lineEdit.text()) * 10**(3*self.f0UnitsIndex)
4787
4788 key = 'Filter / Bandwidth'
4789 self.P.BW = float(self.BW_lineEdit.text()) * 10**(3*self.BWUnitsIndex)
4790
4791 key = 'Filter / Filter Topology'
4792 self.P.filterTransferFunc = unicode(self.topol_comboBox.currentText())
4793
4794 key = 'Filter / Chebyshev / Return losses'
4795 if unicode(self.chebyLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Chebyshev':
4796 self.P.chebyLr = float(self.chebyLr_lineEdit.text())
4797
4798 key = 'Filter / Quasieliptic / Return losses'
4799 if unicode(self.qeLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Quasieliptic':
4800 self.P.qeLr = float(self.qeLr_lineEdit.text())
4801
4802 key = 'Filter / Quasieliptic / Transmission zero'
4803 if unicode(self.qeZero_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Quasieliptic':
4804 self.P.qeZero = float(self.qeZero_lineEdit.text()) * 10**(3*self.qeZeroUnitsIndex)
4805
4806 key = 'Filter / Generalized Chebyshev / Return losses'
4807 if unicode(self.genChebyLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Generalized Chebyshev':
4808 self.P.genChebyLr = float(self.genChebyLr_lineEdit.text())
4809
4810 key = 'Filter / Generalized Chebyshev / Transmission Zeros'
4811
4812 # Allow an empty transmission zero list. Generalized Chebyshev will be equivalent to Chebyshev design.
4813 if self.P.filterTransferFunc == u'Generalized Chebyshev':
4814
4815 #self.P.transImagZeros = [ n * 10**(3*self.transmissionZerosUnitsIndex) for n in map(float, str(self.transmissionZeros_lineEdit.text()).split(',')) ] if unicode(self.transmissionZeros_lineEdit.text()).lstrip() != u'' else [ ]
4816
4817 # Read transmission zeros and separate in list of complex and list of imaginary transmission zeros
4818 self.P.transImagZeros, self.P.transComplexZeros, transRealZeros = self.readTZsTableEntries(self.genChebyTZs_tableWidget)
4819 if len(transRealZeros) > 0: raise ValueError, 'Transmission zeros cannot be purely real,\nthey must have imaginary part > 0 Hz'
4820
4821 # For list of complex
4822 #self.P.transImagZeros = map(complex, str(self.transmissionZeros_lineEdit.text()).replace('i', 'j').split(',')) if unicode(self.transmissionZeros_lineEdit.text()) != u'' else [ ]
4823
4824 key = 'Filter / Generalized Chebyshev / Linear Phase'
4825 self.P.genChebyLP = int(self.genChebyLP_groupBox.isChecked())
4826
4827 if self.P.genChebyLP:
4828 # key = 'Filter / Generalized Chebyshev / Linear Phase / Complex transmission zeros'
4829 # self.P.LPcomplexZeros = map(complex, str(self.LPcomplexZeros_lineEdit.text()).replace(' ', '').replace('i', 'j').split(',')) if unicode(self.LPcomplexZeros_lineEdit.text()) != u'' else [ ]
4830
4831 # # Read a list of real numbers
4832 # key = 'Filter / Generalized Chebyshev / Linear Phase / Real transmission zeros'
4833 # self.P.LPrealZeros = map(float, str(self.LPrealZeros_lineEdit.text()).split(',')) if unicode(self.LPrealZeros_lineEdit.text()) != u'' else [ ]
4834
4835 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Algorithm'
4836 self.P.LPalgor = unicode(self.LPalgor_comboBox.currentText())
4837
4838 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Max iter'
4839 self.P.LPmaxIter = self.LPmaxIter_spinBox.value()
4840
4841 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / N samples'
4842 self.P.LPNsamples = self.LPNsamples_spinBox.value()
4843
4844 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Fractional bandwidth to optimize'
4845 self.P.LPfracBW = self.LPfracBW_doubleSpinBox.value()
4846
4847 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Group delay ripple'
4848 self.P.LPripple = self.LPripple_doubleSpinBox.value() / 1e9
4849
4850 key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Initial position of complex transmission zeros'
4851 imagZerosList, self.P.LPcomplexZeros, self.P.LPrealZeros = self.readTZsTableEntries(self.LPcomplexZeros_tableWidget)
4852 if len(imagZerosList) > 0: raise ValueError, 'Linear Phase optimization zeros cannot be purely imaginary'
4853
4854 key = 'Synthesis / Synthesis Technique'
4855 self.P.synthTech = unicode(self.synthTech_comboBox.currentText())
4856
4857 key = 'Synthesis / Minimum Insertion Loss / Finite Q'
4858 self.P.finiteQo = int(self.finiteQo_groupBox.isChecked())
4859
4860 key = 'Synthesis / Minimum Insertion Loss / Q of implementation'
4861 if unicode(self.Qo_lineEdit.text()) != u'' or ( self.P.synthTech == u'Minimum Insertion Loss' and self.P.finiteQo == 1 ):
4862 self.P.Qo = self.st2floatInf(unicode(self.Qo_lineEdit.text()))
4863
4864 key = 'Synthesis / Predistortion / Arrangement of the reflection zeros'
4865 if self.zerosArrang_comboBox.currentIndex() == 0:
4866 # If self.P.zerosArrang has been changed from 'int' to user-selected, create new list of bool, otherwise do not touch it
4867 if type(self.P.zerosArrang).__name__ == 'int':
4868 if self.P.zerosArrang == 1: self.P.zerosArrang = [ True for n in range(self.P.N) ]
4869 if self.P.zerosArrang == 2: self.P.zerosArrang = [ False for n in range(self.P.N) ]
4870 if self.P.zerosArrang == 3: self.P.zerosArrang = [ bool(n%2) for n in range(self.P.N) ]
4871 if self.P.zerosArrang == 4: self.P.zerosArrang = [ not bool(n%2) for n in range(self.P.N) ]
4872 else:
4873 self.P.zerosArrang = self.zerosArrang_comboBox.currentIndex()
4874
4875 key = 'Synthesis / Predistortion / Adaptive predistorsion'
4876 self.P.adapPredis = int(self.adapPredis_groupBox.isChecked())
4877
4878 key = 'Synthesis / Predistortion / Q to implement'
4879 if unicode(self.QoPredis_lineEdit.text()) != u'' or self.P.synthTech == u'Predistortion':
4880 self.P.QoPredis = self.st2floatInf(unicode(self.QoPredis_lineEdit.text()))
4881
4882 key = 'Synthesis / Predistortion / Q to emulate'
4883 if unicode(self.Qef_lineEdit.text()) != u'' or self.P.synthTech == u'Predistortion':
4884 self.P.Qef = self.st2floatInf(unicode(self.Qef_lineEdit.text()))
4885
4886 key = 'Synthesis / Predistortion / Weight function for the pole shifting'
4887
4888 if self.P.synthTech == u'Predistortion' and self.P.adapPredis == 1:
4889 # # Read weights from lineEdit into list
4890 # if unicode(self.wFunc_lineEdit.text()) != u'': self.P.wFunc = map(float, str(self.wFunc_lineEdit.text()).split(','))
4891
4892 # Read weights from tableWidget into list
4893 self.P.wFunc = self.readwFuncTableEntries()
4894
4895 # Convert one-element list to float. It is the amplitude of Ming-Yu sinusoidal function.
4896 if len(self.P.wFunc) == 1: self.P.wFunc = self.P.wFunc[0]
4897
4898 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique'
4899 self.P.nuqTech = unicode(self.nuqTech_comboBox.currentText())
4900
4901 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K11'
4902 if unicode(self.nuqK11c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4903 self.P.nuqK11c1 = float(self.nuqK11c1_lineEdit.text())
4904
4905 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K22'
4906 if unicode(self.nuqK22c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4907 self.P.nuqK22c1 = float(self.nuqK22c1_lineEdit.text())
4908
4909 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K21'
4910 if unicode(self.nuqK21c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4911 self.P.nuqK21c1 = float(self.nuqK21c1_lineEdit.text())
4912
4913 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21 / Insertion losses K21'
4914 if unicode(self.nuqK21c2_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21':
4915 self.P.nuqK21c2 = float(self.nuqK21c2_lineEdit.text())
4916
4917 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+pole/zero / Insertion losses K21'
4918 if unicode(self.nuqK21c3_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+pole/zero':
4919 self.P.nuqK21c3 = float(self.nuqK21c3_lineEdit.text())
4920
4921 key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+pole/zero / Delta'
4922 if unicode(self.nuqDelta_lineEdit.text()) != u'' or (self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+pole/zero' ):
4923 self.P.nuqDelta = float(self.nuqDelta_lineEdit.text())
4924
4925 key = 'Sweep / Minimum Frequency'
4926 self.P.minFreq = float(self.minFreq_lineEdit.text()) * 10**(3*self.minFreqUnitsIndex)
4927
4928 key = 'Sweep / Maximum Frequency'
4929 self.P.maxFreq = float(self.maxFreq_lineEdit.text()) * 10**(3*self.maxFreqUnitsIndex)
4930
4931 key = 'Sweep / Number of samples'
4932 self.P.numFreq = self.numFreq_spinBox.value()
4933
4934 if self.oldSweep != (self.P.minFreq, self.P.maxFreq): self.mainWindow.autoScaleFreq = True
4935 else: self.mainWindow.autoScaleFreq = False
4936
4937 self.oldSweep = (self.P.minFreq, self.P.maxFreq)
4938
4939 except ValueError, err:
4940 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect parameter error:<br>%s<br>%s<br></p><p align="center">Fix or click "Discard" button.</p>' % (key, err))
4941 return False
4942
4943 try:
4944 self.P.validate()
4945
4946 except parseError, err:
4947 QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect parameter error:<br>%s</p><p align="center">Fix or click "Discard" button.</p>' % err)
4948 return False
4949
4950 # Recompute FT with updated filter parameters
4951 self.mainWindow.FT = FrequencyTransformBP(self.P)
4952
4953 # Only at the end P can be considered non-empty
4954 self.P.empty = False
4955
4956
4957 return True
4958
4959
4960 ##@{
4961 # @name Functions automatically excuted when the user interacts with the GUI
4962
4963 ##
4964 #
4965 # Update the size of the predistortion zeros arrangement list after the user changes the filter order.
4966 # @param N = Filter order as in self.N_spinBox.value()
4967 #
4968 def updatePredisZerosArrang(self, N):
4969 #N = self.N_spinBox.value()
4970 if type(self.P.zerosArrang).__name__ == 'list':
4971 Nold = len(self.P.zerosArrang)
4972 if N > Nold: self.P.zerosArrang += [ False for n in range(N-Nold)]
4973 if N < Nold: self.P.zerosArrang = self.P.zerosArrang[0:N]
4974
4975
4976 ##
4977 #
4978 # Make the size of the adaptive predistorsion weights table equal to the order of the filter.
4979 # Calls self.updatewFuncTableEntries() function and adjusts widget size to table contents.
4980 # This function is automatically executed by the GUI every time the user changes the filter order in the GUI or activates the adaptive predistiortion checkbox,
4981 # but only if the synthesis method is 'Predistortion' and adaptive predistortion group box is checked.
4982 # and is called by the self.updateDialogParams() function.
4983 # @param list = list containing entries to set in tableWidget
4984 # @param dims = tuple (rows, cols) contaning number of rows and columns
4985 # @param updateEntries = If True, table entries are updated (calls self.updatewFuncTableEntries(list)).
4986 # When this function is called by itemChanged signal, this flag must be False to avoid infinite recursion.
4987 #
4988 def updatewFuncTableSize(self, list, dims, updateEntries):
4989 if unicode(self.synthTech_comboBox.currentText()) == u'Predistortion' and self.adapPredis_groupBox.isChecked():
4990
4991 if updateEntries:
4992 M = dims[0] # Filter order
4993 N = dims[1] # Equal to one column
4994 assert N==1
4995 if len(list) == 0:
4996 list = (1 - 0.5 * np.sin(np.arange(1., M+1)*pi/(M+1)) )
4997 self.wFunc_tableWidget.setRowCount(M)
4998 self.wFunc_tableWidget.setColumnCount(N)
4999 self.updatewFuncTableEntries(list)
5000
5001 self.wFunc_tableWidget.resizeRowsToContents()
5002 self.wFunc_tableWidget.resizeColumnsToContents()
5003 self.wFunc_tableWidget.setMaximumSize( self.wFunc_tableWidget.maximumSizeHint() )
5004
5005
5006 ##
5007 #
5008 # Count the number of Generalized Chebyshev transmission zeros set in the GUI widgets and update the lcdNumber widget.
5009 # The dialog window is automatically resized to fit the rows corresponding to TZs.
5010 # This function is automatically executed by the GUI every time the user changes one of the zeros lists in the GUI.
5011 #
5012 def updateTotaltransmissionZeros(self):
5013
5014 self.P.transImagZeros, self.P.transComplexZeros, transRealZeros = self.readTZsTableEntries(self.genChebyTZs_tableWidget)
5015 LPimagZeros, self.P.LPcomplexZeros, self.P.LPrealZeros = self.readTZsTableEntries(self.LPcomplexZeros_tableWidget)
5016
5017 totalZeros = len(self.P.transImagZeros) + 2*len(self.P.transComplexZeros)
5018
5019 if self.genChebyLP_groupBox.isChecked():
5020 totalZeros += len(LPimagZeros) + 2*len(self.P.LPcomplexZeros + self.P.LPrealZeros)
5021
5022 self.transmissionZeros_lcdNumber.display(totalZeros)
5023
5024 # Update dialog size to fit table
5025 self.updateDlgHeight()
5026
5027
5028 ##
5029 #
5030 # Update dialog size to fit table
5031 #
5032 def updateDlgHeight(self):
5033
5034 incTZsTable = (self.genChebyTZs_tableWidget.rowCount()+1.5) * self.genChebyTZs_tableWidget.rowHeight(0)
5035 incLPzerosTable = (self.LPcomplexZeros_tableWidget.rowCount()+1.5) * self.LPcomplexZeros_tableWidget.rowHeight(0)
5036
5037 dlgSize = self.size()
5038 dlgHeight= dlgSize.height()
5039 dlgMinHeight = self.dlgInitSize
5040 dlgOptSize = dlgMinHeight + incTZsTable + incLPzerosTable
5041 # print 'incTZs =', incTZsTable, '/ incLP =', incLPzerosTable, '/ Height =', dlgHeight, '/ MinHeight =', dlgMinHeight, '/ OptSize =', dlgOptSize
5042 # print 'TZ Row =', self.genChebyTZs_tableWidget.rowHeight(0), '/ LP Row =', self.LPcomplexZeros_tableWidget.rowHeight(1)
5043 # print 'TZscrollH = ', self.genChebyTZs_tableWidget.horizontalScrollBar().height(), '/ LPscrollH = ', self.LPcomplexZeros_tableWidget.horizontalScrollBar().height()
5044 # print ''
5045 #if dlgHeight < dlgOptSize:
5046 dlgSize.setHeight(dlgOptSize)
5047 self.resize(dlgSize)
5048
5049
5050 ##
5051 #
5052 # Custom slot in Qt designer conected to widgets "changed" signal.
5053 # This function is automatically executed by the GUI every time the user changes dialog values,
5054 # and sets the self.dialogModified variable to True and self.recomputed to False.
5055 #
5056 def dialogChanged(self):
5057 self.dialogModified = True
5058 self.recomputed = False
5059 if hasattr(self.sender(), 'validator'):
5060 validator = self.sender().validator()
5061 if validator is not None:
5062 result = validator.validate(self.sender().text(), 0)[0]
5063 palette = self.sender().palette()
5064 if result == 1:
5065 palette.setColor(QPalette.Text, Qt.red)
5066 self.sender().setPalette(palette)
5067 self.badInput.append(self.sender())
5068 self.sender().setFocus(Qt.OtherFocusReason) # It does nothing, because it is executed before focus changes to another widget.
5069 if result == 2:
5070 palette.setColor(QPalette.Text, self.mainWindow.palette().text().color())
5071 self.sender().setPalette(palette)
5072 while self.sender() in self.badInput: self.badInput.remove(self.sender())
5073
5074 # For debugging
5075 #print [ unicode(n.text()) for n in self.badInput]
5076
5077 if len(self.badInput) > 0:
5078 self.compute_pushButton.setEnabled(False)
5079 self.accept_pushButton.setEnabled(False)
5080 else:
5081 self.compute_pushButton.setEnabled(True)
5082 self.accept_pushButton.setEnabled(True)
5083
5084
5085 @pyqtSignature("")
5086 ##
5087 #
5088 # Opens getExistingDirectory Qt dialog in order to set P.outDir variable.
5089 #
5090 def on_outDir_pushButton_clicked(self):
5091 outDir = self.P.outDir if self.P.outDir is not None else os.path.dirname(sys.argv[0])
5092 self.P.outDir = QFileDialog.getExistingDirectory(self, QString(u"Output directory"), outDir)
5093 if self.P.outDir:
5094 self.outDir_lineEdit.setText(QDir.toNativeSeparators(self.P.outDir))
5095
5096
5097 @pyqtSignature("bool")
5098 ##
5099 #
5100 # Run imaginary TZ and bandwidth optimization to comply specification mask.
5101 #
5102 def on_TZoptim_pushButton_clicked(self, checked):
5103
5104 # Read parameters in order to update self.P and update self.mainWindow.FT. The updated parameters can still be discarded with the 'Discard' pushbutton.
5105 if self.readDialogParams() is False:
5106 QMessageBox.critical(self, "ERROR", 'Incorrect parameter in edit window.<p>Cannot run optimization with incorrect parameters.')
5107 return
5108
5109 algor = self.algor_comboBox.currentText()
5110 maxIter = self.maxIter_spinBox.value()
5111 Nsamples = self.Nsamples_spinBox.value()
5112 BWmaxvar = self.BWoptim_doubleSpinBox.value()
5113 weights = [ self.wS11_doubleSpinBox.value(), self.wS22_doubleSpinBox.value(), self.wS21_doubleSpinBox.value() ]
5114
5115 SM = self.mainWindow.SM
5116
5117 if SM is None:
5118 reply = QMessageBox.question(self, "WARNING", '<b>No specification mask loaded.</b><p>Do you want to open an specification mask file now?', QMessageBox.Yes| QMessageBox.Cancel)
5119 if reply == QMessageBox.Yes:
5120 self.mainWindow.on_action_Mask_triggered(True)
5121 SM = self.mainWindow.SM
5122
5123 runOptimization = False
5124 if SM is not None:
5125 if self.genChebyLP_groupBox.isChecked():
5126 reply = QMessageBox.question(self, "WARNING", '''<b>Linear phase optimization is enabled.</b><p>
5127 First, equalization zeros will be optimized for linear phase.<p>
5128 Second, imaginary transmission zeros will be optimized to comply mask, with the optimum equalization zeros as prescribed transmission zeros.<p>
5129 Third, if you have an open [S] parameter window, filter synthesis will be recomputed, which involves a new linear phase optimization with fixed prescribed transmission zeros.<p>
5130 Do you wish to proceed?''', QMessageBox.Yes| QMessageBox.Cancel)
5131 if reply == QMessageBox.Yes:
5132 runOptimization = True
5133 LPeqZeros = True
5134 else:
5135 runOptimization = True
5136 LPeqZeros = False
5137
5138 if runOptimization:
5139
5140 # First check if library libextrafilters.py has been imported
5141 if importedExtraFilters is False:
5142 if existingExtraFilters is False:
5143 criticalErrorMsg("""<b>ERROR in Filter Synthesis:</b><p>
5144 Chebyshev filter is in the Library of Extra Filters,<br>
5145 which is not installed in this system.
5146 <p>Contact lossyfilters@tsc.upc.edu to get the Library of Extra Filters.
5147 """)
5148 else:
5149 criticalErrorMsg("""<b>ERROR in Filter Synthesis:</b><p>
5150 Importing the 'Library of Extra Filters' failed. <br>
5151 You have a libextrafilters.pyc file, but it cannot be imported. <br>
5152 Possibly libextrafilters.pyc was compiled using a different Python version than yours.
5153 <p>Your installed Python version is: %s on %s
5154 <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5155 """ % (platform.python_version(), platform.system()) )
5156 # Return without running optimization
5157 return
5158
5159 app.setOverrideCursor(Qt.WaitCursor)
5160 try:
5161 TZoptimMask(self, self.P, self.mainWindow.FT, weights, BWmaxvar, algor, maxIter, Nsamples, SM, LPeqZeros)
5162 except synthError, err:
5163 criticalErrorMsg('<b>ERROR in Filter Synthesis:</b><p>' + unicode(err))
5164
5165 app.restoreOverrideCursor()
5166 myPrint('Imaginaty TZs and BW optimization to mask with parameters: alg=%s, maxIter=%d, Nsamples=%d, BWmaxvar=%g, w=%s, mask=%s' % (algor, maxIter, Nsamples, BWmaxvar, listStr(weights, 2), SM.fileName) )
5167 else:
5168 myPrint('Imaginaty TZs and BW optimization to mask cancelled by user')
5169
5170
5171 @pyqtSignature("bool")
5172 ##
5173 #
5174 # Run synthesis. If parameters are valid, self.mainWindow.on_action_Execute_triggered() will set self.recomputed to True.
5175 #
5176 def on_compute_pushButton_clicked(self, checked):
5177 # Launch synthesis from Main Window
5178 self.mainWindow.on_action_Execute_triggered(True)
5179
5180
5181 @pyqtSignature("bool")
5182 ##
5183 #
5184 # Add new zero (blank) to transmission zeros table.
5185 #
5186 def on_TZS_newZero_pushButton_clicked(self, checked):
5187 self.genChebyTZs_tableWidget.addRow()
5188
5189
5190 @pyqtSignature("bool")
5191 ##
5192 #
5193 # Add new zero (blank) to LP complex zeros table.
5194 #
5195 def on_LP_newZero_pushButton_clicked(self, checked):
5196 self.LPcomplexZeros_tableWidget.addRow()
5197
5198
5199 @pyqtSignature("bool")
5200 ##
5201 #
5202 # Add new zero with symmetrical imaginary part (in normalized frequency) to transmission zeros table.
5203 #
5204 def on_TZs_symm_pushButton_clicked(self, checked):
5205 table = self.genChebyTZs_tableWidget
5206 row = table.currentRow()
5207
5208 if row == -1:
5209 stError = '<p align="center">Please select a transmission zero to symmetrize<p align="center"</p>'
5210 QMessageBox.warning(self, "WARNING", stError)
5211 return
5212
5213 reItem = table.item(row, 0)
5214 imItem = table.item(row, 1)
5215 stRe = str(reItem.text()).strip() if reItem is not None else ''
5216 stIm = str(imItem.text()).strip() if imItem is not None else ''
5217 re = float(stRe) if stRe != '' else 0
5218 im = float(stIm) * pow(10, 3*self.transmissionZerosUnitsIndex) if stIm != '' else 0
5219
5220 # This does not work in case f0_lineEdit has changed, if you do not update FT first
5221 #imSym = self.mainWindow.FT.unormFreq( - self.mainWindow.FT.normFreq(im) )
5222 f0 = float(self.f0_lineEdit.text()) * 10**(3*self.f0UnitsIndex)
5223 imSym = f0**2 / im
5224
5225 N = table.rowCount()
5226 table.setRowCount(N+1)
5227 imSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, imSym / pow(10, 3*self.transmissionZerosUnitsIndex)) )
5228 imSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5229 table.setItem(N, 1, imSymItem)
5230 reSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, re) ) if re != 0 else QTableWidgetItem('')
5231 reSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5232 table.setItem(N, 0, reSymItem)
5233
5234 table.updateTableSize()
5235 self.updateTotaltransmissionZeros()
5236
5237
5238 @pyqtSignature("bool")
5239 ##
5240 #
5241 # Add new zero with symmetrical imaginary part (in normalized frequency) to transmission zeros table.
5242 #
5243 def on_LP_symm_pushButton_clicked(self, checked):
5244 table = self.LPcomplexZeros_tableWidget
5245 row = table.currentRow()
5246
5247 if row == -1:
5248 stError = '<p align="center">Please select a transmission zero to symmetrize<p align="center"</p>'
5249 QMessageBox.warning(self, "WARNING", stError)
5250 return
5251
5252 reItem = table.item(row, 0)
5253 imItem = table.item(row, 1)
5254 stRe = str(reItem.text()).strip() if reItem is not None else ''
5255 stIm = str(imItem.text()).strip() if imItem is not None else ''
5256 re = float(stRe) if stRe != '' else 0
5257 im = float(stIm) * pow(10, 3*self.transmissionZerosUnitsIndex) if stIm != '' else 0
5258
5259 imSym = self.mainWindow.FT.unormFreq( - self.mainWindow.FT.normFreq(im) )
5260
5261 N = table.rowCount()
5262 table.setRowCount(N+1)
5263 imSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, imSym / pow(10, 3*self.transmissionZerosUnitsIndex)) )
5264 imSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5265 table.setItem(N, 1, imSymItem)
5266 reSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, re) ) if re != 0 else QTableWidgetItem('')
5267 reSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5268 table.setItem(N, 0, reSymItem)
5269
5270 table.updateTableSize()
5271 self.updateTotaltransmissionZeros()
5272
5273
5274 ##
5275 #
5276 # Save current dialog parameters to mainWindow.P and close dialog.
5277 # This function is automatically executed by the GUI when the user clickes the 'Accept' button or closes the dialog.
5278 # Closing the dialog by killing the window is equivalent to clicking the 'Accept' button.
5279 #
5280 def closeEvent(self, event):
5281 if self.rejected:
5282 event.accept()
5283 else:
5284 # Copy dialog parameters to mainWindow.P only if there is no incorrect parameter
5285 if self.readDialogParams() is True:
5286
5287 if self.dialogModified:
5288 if not self.recomputed:
5289 message = "Parameters have been modified and it is necessary to recompute synthesis.<p>Continue?"
5290 reply = QMessageBox.question(self, "%s - Recompute" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
5291 if reply in [QMessageBox.Cancel, QMessageBox.No]:
5292 event.ignore()
5293 return
5294 elif reply == QMessageBox.Yes:
5295 self.on_compute_pushButton_clicked(True)
5296 # If parameters have been modified and closeEvent is not ignored, mainWindow.dirty must be True
5297 self.mainWindow.dirty = True
5298 self.mainWindow.setWindowModified(self.mainWindow.dirty)
5299
5300 self.mainWindow.updateStatus("Parameter values accepted")
5301 self.mainWindow.paramEdit = None
5302 event.accept()
5303 else:
5304 self.mainWindow.updateStatus("Incorrect parameters not accepted")
5305 event.ignore()
5306
5307
5308 @pyqtSignature("bool")
5309 ##
5310 #
5311 # Reject current dialog parameters and close dialog.
5312 # A backup copy of parameters and synthesis results is recovered.
5313 # This function is automatically executed by the GUI when the user clickes the 'Discard' button.
5314 #
5315 def on_discard_pushButton_clicked(self, checked):
5316 if self.dialogModified or self.recomputed:
5317 message = """Dialog discarded by user:<p>
5318 All parameters and synthesis results will be reverted to a previous state. To keep your parameters and results, click the "Accept" button in the Parameter Editor.<p>
5319 Revert parameters and results?"""
5320 reply = QMessageBox.question(self, "%s - Discard parameters" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
5321 if reply in [QMessageBox.Cancel, QMessageBox.No]:
5322 return
5323
5324 (self.mainWindow.P, self.mainWindow.CP, self.mainWindow.CM, self.mainWindow.SP, self.mainWindow.FT) = copy.deepcopy(self.resultBkp)
5325 self.mainWindow.updateStatus("Parameter values discarded by user")
5326 self.mainWindow.paramEdit = None
5327 self.mainWindow.updateWindows() # After setting self.mainWindow.paramEdit = None, to avoid updating paramEdit unnecessarily-
5328 self.rejected = True
5329 self.close()
5330
5331 # reject() only works if window is a QDialog.
5332 #self.reject()
5333
5334
5335 @pyqtSignature("int")
5336 ##
5337 #
5338 # Update lineEdit value when the units ComboBox is changed.
5339 # This function is automatically executed by the GUI when the user changes the filter frequency units ComboBox.
5340 #
5341 def on_f0_comboBox_currentIndexChanged(self, value):
5342 #newIndex = self.f0_comboBox.currentIndex()
5343 newIndex = value
5344 if self.f0UnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5345 newValue = float(self.f0_lineEdit.text()) * pow(10, 3*(self.f0UnitsIndex-newIndex))
5346 self.f0_lineEdit.setText(str(newValue))
5347 self.f0UnitsIndex = newIndex
5348
5349
5350 @pyqtSignature("int")
5351 ##
5352 #
5353 # Update lineEdit value when the units ComboBox is changed.
5354 # This function is automatically executed by the GUI when the user changes the filter bandwidht units ComboBox.
5355 #
5356 def on_BW_comboBox_currentIndexChanged(self, value):
5357 #newIndex = self.BW_comboBox.currentIndex()
5358 newIndex = value
5359 if self.BWUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5360 newValue = float(self.BW_lineEdit.text()) * pow(10, 3*(self.BWUnitsIndex-newIndex))
5361 self.BW_lineEdit.setText(str(newValue))
5362 self.BWUnitsIndex = newIndex
5363
5364
5365 @pyqtSignature("int")
5366 ##
5367 #
5368 # Update lineEdit value when the units ComboBox is changed.
5369 # This function is automatically executed by the GUI when the user changes the Quasieliptic zeros frequency units ComboBox.
5370 #
5371 def on_qeZero_comboBox_currentIndexChanged(self, value):
5372 #newIndex = self.qeZero_comboBox.currentIndex()
5373 newIndex = value
5374 if self.qeZeroUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5375 newValue = float(self.qeZero_lineEdit.text()) * pow(10, 3*(self.qeZeroUnitsIndex-newIndex))
5376 self.qeZero_lineEdit.setText(str(newValue))
5377 self.qeZeroUnitsIndex = newIndex
5378
5379
5380 @pyqtSignature("int")
5381 ##
5382 #
5383 # Update lineEdit value when the units ComboBox is changed.
5384 # This function is automatically executed by the GUI when the user changes the Generalized Chebyshev zeros frequency units ComboBox.
5385 #
5386 def on_transmissionZeros_comboBox_currentIndexChanged(self, value):
5387 # self.transmissionZerosUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
5388 self.transmissionZerosUnitsIndex = value
5389
5390 # Update table Widgets using new self.transmissionZerosUnitsIndex
5391 TZsList = np.concatenate( (1j*np.array(self.P.transImagZeros), np.array(self.P.transComplexZeros)) ).tolist()
5392 self.updateTZsTable(self.genChebyTZs_tableWidget, TZsList)
5393 self.updateTZsTable(self.LPcomplexZeros_tableWidget, self.P.LPcomplexZeros + self.P.LPrealZeros)
5394
5395
5396 @pyqtSignature("int")
5397 ##
5398 #
5399 # Update lineEdit value when the units ComboBox is changed.
5400 # This function is automatically executed by the GUI when the user changes the minimum sweep frequency units ComboBox.
5401 #
5402 def on_minFreq_comboBox_currentIndexChanged(self, value):
5403 #newIndex = self.minFreq_comboBox.currentIndex()
5404 newIndex = value
5405 if self.minFreqUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5406 newValue = float(self.minFreq_lineEdit.text()) * pow(10, 3*(self.minFreqUnitsIndex-newIndex))
5407 self.minFreq_lineEdit.setText(str(newValue))
5408 self.minFreqUnitsIndex = newIndex
5409
5410
5411 @pyqtSignature("int")
5412 ##
5413 #
5414 # Update lineEdit value when the units ComboBox is changed.
5415 # This function is automatically executed by the GUI when the user changes the maximum sweep frequency units ComboBox.
5416 #
5417 def on_maxFreq_comboBox_currentIndexChanged(self, value):
5418 #newIndex = self.maxFreq_comboBox.currentIndex()
5419 newIndex = value
5420 if self.maxFreqUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5421 newValue = float(self.maxFreq_lineEdit.text()) * pow(10, 3*(self.maxFreqUnitsIndex-newIndex))
5422 self.maxFreq_lineEdit.setText(str(newValue))
5423 self.maxFreqUnitsIndex = newIndex
5424
5425 ##@}
5426
5427 # End class: ParamEditDlg #
5428 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
5429
5430 # !!!!!!!!!!!!!!!!!!!!!!!#
5431 # Global functions: #
5432
5433 ##
5434 #
5435 # Runs parameter validation, filters synthesis and results plots.
5436 # @param P = Parameters class instance containing the parameter values that will be used in the computation. May be modified during computation.
5437 # @param FT = FrequencyTransform class instance.
5438 # @param symmetrizeZeros = Automatically symmetrize Generalized Chebyshev zeros in Hz in order to compute a folded matrix with uniform Q in resonators.
5439 # Equivalent to answering "Yes" in the GUI when it asks the user if he wants to symmetrize Generalized Chebyshev zeros in Hz.
5440 # @return CP = CharPolys class instance, containing Characteristic Polynomials. Losses are included.
5441 # @return CM = CouplingMatrices class instance, containing Coupling Matrices. Losses are included.
5442 # @return SP = Sparameters class instance, containing frequency axis and [S] data. Losses are included.
5443 # @return done = True if Synthesis has been done without errors, False otherwise.
5444 #
5445 def runSynthesis(P, FT, symmetrizeZeros):
5446 CP, CM, SP = None, None, None
5447
5448 try:
5449
5450 # Compute characteristic polynomials
5451 CP = CharPolys(P, FT, symmetrizeZeros)
5452 myPrint("Relative error in the computation of lossless characteristic polynomials = %g" % CP.rootsErr)
5453
5454 if P.synthTech.title() == u'Predistortion':
5455 if importedLossyFilters is False:
5456 if existingLossyFilters is False:
5457 raise synthError, """Pre-distorted characteristic polynomials are in the Library of Lossy Filters, which is not installed in this system.
5458 <p>Contact lossyfilters@tsc.upc.edu to get the Library of Lossy Filters.
5459 """
5460 else:
5461 raise synthError, """Importing the 'Library of Lossy Filters' failed. <br>
5462 You have a liblossyfilters.pyc file, but it cannot be imported. <br>
5463 Possibly liblossyfilters.pyc was compiled using a different Python version than yours.
5464 <p>Your installed Python version is: %s on %s
5465 <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5466 """ % (platform.python_version(), platform.system())
5467 elif importedCheckLicense is False:
5468 if existingCheckLicense is False:
5469 raise synthError, """There is no check_license.pyc file in your system.
5470 It should have been installed together with libextrafilters.pyc file,
5471 which actually is in your system.
5472 Please follow the 'Extra Filters Library' installation instructions.
5473 """
5474 else:
5475 raise synthError, """Importing the 'License Check Library' failed. <br>
5476 You have a libchecklicense.pyc file, but it cannot be imported. <br>
5477 Possibly libchecklicense.pyc was compiled using a different Python version than yours.
5478 <p>Your installed Python version is: %s on %s
5479 <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5480 """ % (platform.python_version(), platform.system())
5481 else:
5482 predistortion(P, CP, FT )
5483 myPrint("Relative error in the computation of pre-distorted characteristic polynomials = %g" % CP.rootsErr)
5484
5485 elif P.synthTech.title() == u'Prescribed Insertion Loss':
5486 if importedLossyFilters is False:
5487 if existingLossyFilters is False:
5488 raise synthError, """Prescribed insertion loss characteristic polynomials are in the Library of Lossy Filters, which is not installed in this system.
5489 <p>Contact lossyfilters@tsc.upc.edu to get the Library of Lossy Filters.
5490 """
5491 else:
5492 raise synthError, """Importing the 'Library of Lossy Filters' failed. <br>
5493 You have a liblossyfilters.pyc file, but it cannot be imported. <br>
5494 Possibly liblossyfilters.pyc was compiled using a different Python version than yours.
5495 <p>Your installed Python version is: %s on %s
5496 <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5497 """ % (platform.python_version(), platform.system())
5498 elif importedCheckLicense is False:
5499 if existingCheckLicense is False:
5500 raise synthError, """There is no check_license.pyc file in your system.
5501 It should have been installed together with libextrafilters.pyc file,
5502 which actually is in your system.
5503 Please follow the 'Extra Filters Library' installation instructions.
5504 """
5505 else:
5506 raise synthError, """Importing the 'License Check Library' failed. <br>
5507 You have a libchecklicense.pyc file, but it cannot be imported. <br>
5508 Possibly libchecklicense.pyc was compiled using a different Python version than yours.
5509 <p>Your installed Python version is: %s on %s
5510 <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5511 """ % (platform.python_version(), platform.system())
5512 else:
5513 nonUniformQ(P, CP)
5514
5515 # Compute [S] parameters from characteristic polynomials
5516 SP = Sparameters(P, CP, FT)
5517
5518 # Compute coupling matrix
5519 Yo = 1 # Normalized load
5520 CM = CouplingMatrices(P, CP, Yo, FT, SP)
5521 myPrint("Relative error in the computation of transversal coupling matrix = %g" % CM.CM_error)
5522
5523 # Compute error between [S] from coupling matrices and [S] from characteristic polynomials
5524 # Do not compute it in the case 'kS21+pole/zero', since the node corresponding to the added pole is removed from the CM, resulting in large errors.
5525 if (P.synthTech.title() != u'Prescribed Insertion Loss') or (P.nuqTech != u'kS21+pole/zero'):
5526 # for MatQ in CM.listM:
5527 for MatQ in [ CM.listM[0] ]: # It is slow to compute [S] for all the MatQ, do it only for the 1st one (TCM) just to estimate the error in CM
5528 myPrint(MatQ.name)
5529 stErrorCM, CM_error = SP.fromCouplingMatrix(MatQ)
5530 myPrint(stErrorCM + ', Relative error: %e\n' % CM_error)
5531 CM.updateCM_error(CM_error, st = 'Error in %s' % MatQ.name)
5532
5533 if P.outDir is not None:
5534 # Normalize and expand output directory name, just in case
5535 P.outDir = os.path.normpath(P.outDir)
5536 P.outDir = os.path.expanduser(P.outDir)
5537
5538 # Check if directory exists
5539 if not os.path.isdir(P.outDir):
5540 P.outDirName = None
5541 warningMsg(u"WARNING: Directory '%s' does not exist. <p>Results not saved" % P.outDir)
5542 else:
5543 # Create output name using outDir and outName
5544 P.outDirName = os.path.join(P.outDir, P.outName)
5545 else:
5546 P.outDirName = P.outName
5547
5548 if P.outDirName is not None:
5549 # Normalize output path just in case
5550 P.outDirName = os.path.normpath(P.outDirName)
5551
5552 # Save everything
5553 CP.saveCharPolys(P, saveSignificantDigits)
5554 SP.saveSparam(P, saveSignificantDigits)
5555 if saveDefaultMatrices:
5556 CM.saveCouplingMatrices(P, SP, saveSignificantDigits, saveSignificantDigitsEnergy)
5557
5558 except licenseError, err:
5559 criticalErrorMsg('License ERROR: <p>' + unicode(err))
5560 return None, None, None, False
5561
5562 except synthError, err:
5563 criticalErrorMsg('<b>ERROR in Filter Synthesis:</b><p>' + unicode(err))
5564 return None, None, None, False
5565
5566 return CP, CM, SP, True
5567
5568
5569 ##
5570 #
5571 # Print error message and command line help.
5572 # @param st = Error message string.
5573 # @param exit_code = program exit return code.
5574 #
5575 def usage(st, exit_code):
5576
5577 pgrm_name = os.path.basename(sys.argv[0])
5578 print st
5579 print 'usage: ' + pgrm_name + ' [-vh] [--help] [parameter_file]'
5580 print '\nOptions:'
5581 print '-v: Verbose output. Show more information (for debugging) at the log in the main window (or at the console with --nogui flag).'
5582 print '-s: Automatically symmetrize Generalized Chebyshev zeros in Hz in order to compute a folded matrix with uniform Q in resonators.'
5583 print '-r: Automatically run Synthesis. Needs simultaneous parameter file specification.'
5584 print '-m: Automatically open Coupling Matrices window. Needs simultaneous -r flag.'
5585 print '-e: Automatically open Matrix Edit window. Needs simultaneous -m flag.'
5586 print '-p N: Save all default coupling matrices to file after they are computed, using N significant digits. By default the matrices must be saved manually using the GUI.'
5587 print '-h, --help: help'
5588 print '--nogui: Command line interface'
5589 print '--style windows, motif, cleanlooks, plastique or cde'
5590 print '\nArguments:'
5591 print 'parameter_file (optional): Path to ASCII file containing parameter values\n'
5592 sys.exit(exit_code)
5593
5594 # End: Global functions: #
5595 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
5596
5597
5598 # !!!!!!!!!!!!!!!!!!!!!!!#
5599 # MAIN PROGRAM #
5600
5601 if __name__ == '__main__':
5602 """
5603 Main program
5604 """
5605
5606 ##
5607 # Program name as run by the user.
5608 applicationName = "Microwave filters GUI"
5609
5610 # Check input arguments syntax
5611 try:
5612 opts, args = getopt.getopt(sys.argv[1:], "hsvrmep:", ["style=", "help", "nogui"])
5613 except getopt.GetoptError:
5614 # print help information and exit:
5615 usage('Error: Bad command line parameters', 2)
5616
5617 QtArgs = list()
5618 QtArgs.append(sys.argv[0])
5619
5620 # Initialize command line flags class
5621 symmetrizeZeros = None
5622 verbose = autoRun = autoMatrix = autoEditMatrix = False
5623 QtArgsStyle = None
5624 nogui = False
5625 saveDefaultMatrices = False
5626
5627 for o, a in opts:
5628 if o == "-v":
5629 verbose = True
5630
5631 if o == "-s":
5632 symmetrizeZeros = True
5633
5634 if o == "-r":
5635 autoRun = True
5636
5637 if o == "-m":
5638 autoMatrix = True
5639
5640 if o == "-e":
5641 autoEditMatrix = True
5642
5643 if o == "-p":
5644 try:
5645 saveSignificantDigits = int(a)
5646 except ValueError:
5647 print "ERROR in command line flags:"
5648 print "Precision flag -p must take a numeric argument."
5649 print "To get help use -h flag:\n%s -h\n" % sys.argv[0]
5650 sys.exit(0)
5651 saveDefaultMatrices = True
5652
5653 if o in ("-h", "--help"):
5654 usage('Help:', 0)
5655
5656 if o == "--nogui":
5657 nogui = True
5658
5659 if o in ["--style"]:
5660 QtArgsStyle = a
5661
5662 setGlobalVariablesLibCommonFunc(verbose = verbose, nogui = nogui)
5663
5664 printHeader( applicationName, __doc__)
5665
5666 # myPrint('Significant digits to use when saving coupling matrices: %d' % saveSignificantDigits)
5667
5668 if nogui: # Command line interface
5669 if len(args) != 1: usage('Error: Incorrect number of arguments', 2)
5670
5671 ##
5672 # Name of the parameters file to process
5673 fileName = args[0]
5674
5675 ##
5676 # Parameters class instance containing filter and synthesis parameters read from the parameters file.
5677 P = readParamFile(fileName)
5678
5679 ##
5680 # Create frequency transform instance
5681 FT = FrequencyTransformBP(P)
5682
5683 # Execute the synthesis
5684 CP, CM, SP, done = runSynthesis(P, FT, symmetrizeZeros)
5685
5686 else: # Graphical User Interface (GUI)
5687 if QtArgsStyle is None: QtArgsStyle = 'cleanlooks'
5688 QtArgs.extend(['-style', QtArgsStyle ])
5689
5690 if len(args) > 1: usage('Error: Incorrect number of arguments', 2)
5691 else:
5692 if len(args) == 1: fileName = unicode(args[0])
5693 else: fileName = None # Start with blank parameter values
5694
5695 ##
5696 # Create validator strings and regular expressions. The are global variables to be accessible from everywhere.
5697 stFloatPoint = r'((\d+\.?\d*|\d*\.?\d+)([Ee][-+]?\d+)?)'
5698 stReal = r'(([-+]?)' + stFloatPoint + '\s*)'
5699 stRealPositive = r'((\+?)' + stFloatPoint + '\s*)'
5700 stComplex = r'(([-+]?' + stFloatPoint + r'|[-+]?(' + stFloatPoint + r')?[ij]|[-+]?' + stFloatPoint + r'\s*[-+]\s*(' + stFloatPoint + r')?[ij])\s*)'
5701 stRealList = stReal + r'(,\s*' + stReal + r')*'
5702 stRealPositiveList = stRealPositive + r'(,\s*' + stRealPositive + r')*'
5703 stComplexList = stComplex + r'(,\s*' + stComplex + r')*'
5704
5705 reReal = QRegExp( stReal + r'|^$')
5706 reRealPositive = QRegExp( stRealPositive + r'|^$')
5707 reRealPositiveInf = QRegExp( stRealPositive + r'|^$|inf|Inf')
5708 reComplex = QRegExp( stComplex + r'|^$')
5709 reRealList = QRegExp( stRealList + r'|^$' )
5710 reRealPositiveList = QRegExp( stRealPositiveList + r'|^$' )
5711 reComplexList = QRegExp( stComplexList + r'|^$' )
5712
5713
5714 # Start user interface
5715
5716 #QApplication.setStyle(QCleanlooksStyle())
5717 #QApplication.setStyle(QWindowsXPStyle())
5718
5719 ##
5720 # QApplication class instance, containing our application
5721 app = QApplication(QtArgs)
5722 app.setOrganizationName("UPC")
5723 app.setOrganizationDomain("http://www.tsc.upc.edu/hfs; http://www.tsc.upc.edu/antennalab")
5724 app.setApplicationName(applicationName)
5725 app.setWindowIcon(QIcon(":/icon/icons/icon_Spar-5.png"))
5726
5727 mainWindow = MainWindow(fileName)
5728 mainWindow.show()
5729 #mainWindow.activateWindow()
5730 app.exec_()
5731
5732
5733