Commit 9245ec8da86ed48de1a7bb70cd322f2abfca8301
0 parents
Exists in
master
first commit
Showing 3 changed files with 650 additions and 0 deletions Side-by-side Diff
README
... | ... | @@ -0,0 +1 @@ |
1 | +Handle PNA-X, N522xA or N523x device from Keysight. Tested on N5234 device. |
n523x_utils/extract_peak.py
... | ... | @@ -0,0 +1,286 @@ |
1 | +#!/usr/bin/python3 | |
2 | +# -*- coding: utf-8 -*- | |
3 | + | |
4 | +"""package n523xA_utils | |
5 | +author Benoit Dubois | |
6 | +copyright FEMTO ENGINEERING | |
7 | +license GPL v3.0+ | |
8 | +brief Acquire data trace from N5234A or N5230A device. | |
9 | + Try to detrend trace in order to extract mode shape. | |
10 | +""" | |
11 | + | |
12 | +# Ctrl-c closes the application | |
13 | +import signal | |
14 | +signal.signal(signal.SIGINT, signal.SIG_DFL) | |
15 | + | |
16 | +import sys | |
17 | +import os.path as path | |
18 | +import logging | |
19 | +from pyqtgraph.parametertree import Parameter, ParameterTree | |
20 | +import pyqtgraph as pg | |
21 | +import numpy as np | |
22 | +import numpy.polynomial.polynomial as poly | |
23 | +import scipy.signal as scs | |
24 | +from PyQt4.QtCore import pyqtSlot, pyqtSignal, QDir, QFileInfo | |
25 | +from PyQt4.QtGui import QApplication, QMainWindow, QWidget, QVBoxLayout, \ | |
26 | + QHBoxLayout, QMessageBox, QFileDialog | |
27 | + | |
28 | +import n523x as vna | |
29 | + | |
30 | +CONSOLE_LOG_LEVEL = logging.DEBUG | |
31 | +FILE_LOG_LEVEL = logging.WARNING | |
32 | + | |
33 | +APP_NAME = "ExtractPeak" | |
34 | + | |
35 | +#=============================================================================== | |
36 | +class MyVna(vna.N523x): | |
37 | + | |
38 | + def connect(self, try_=3): | |
39 | + """Overloaded vna.Vna method beacause, VNA seems to refuse connection | |
40 | + without reasons. | |
41 | + :param try_: number of connection attempt (int). | |
42 | + """ | |
43 | + for i in range(try_): | |
44 | + if super().connect() is True: | |
45 | + break | |
46 | + | |
47 | +#=============================================================================== | |
48 | +PARAMS = [ | |
49 | + {'name': 'Load data', 'type': 'group', 'children': [ | |
50 | + {'name': 'VNA', 'type': 'group', 'children': [ | |
51 | + {'name': 'IP', 'type': 'str', 'value': vna.DEFAULT_IP}, | |
52 | + {'name': 'Port', 'type': 'int', 'value': vna.DEFAULT_PORT}, | |
53 | + {'name': 'Acquisition', 'type': 'action'}, | |
54 | + ]}, | |
55 | + {'name': 'File', 'type': 'group', 'children': [ | |
56 | + {'name': 'Filename', 'type': 'str'}, | |
57 | + {'name': 'Open', 'type': 'action'}, | |
58 | + ]}, | |
59 | + ]}, | |
60 | + {'name': 'Fit', 'type': 'group', 'children': [ | |
61 | + {'name': 'Order', 'type': 'int', 'value': 3, 'limits': (1, 15)}, | |
62 | + {'name': 'Filtering', 'type': 'bool', 'value': False}, | |
63 | + {'name': 'Run fit', 'type': 'action'}, | |
64 | + ]}, | |
65 | + {'name': 'Plot', 'type': 'group', 'children': []}, | |
66 | +] | |
67 | + | |
68 | +class PeakUi(QMainWindow): | |
69 | + """Ui of extract peak application. | |
70 | + """ | |
71 | + | |
72 | + def __init__(self): | |
73 | + """Constructor. | |
74 | + :returns: None | |
75 | + """ | |
76 | + super().__init__() | |
77 | + self.setWindowTitle("Fit Peak") | |
78 | + self.setCentralWidget(self._central_widget()) | |
79 | + self.region = pg.LinearRegionItem() | |
80 | + self.region.setZValue(10) | |
81 | + # Add the LinearRegionItem to the ViewBox, but tell the ViewBox to | |
82 | + # exclude this item when doing auto-range calculations. | |
83 | + self.mplot1.addItem(self.region, ignoreBounds=True) | |
84 | + | |
85 | + def _central_widget(self): | |
86 | + """Generates central widget. | |
87 | + :returns: central widget of UI (QWidget) | |
88 | + """ | |
89 | + self.p = Parameter.create(name='params', type='group', children=PARAMS) | |
90 | + self.ptree = ParameterTree() | |
91 | + self.ptree.setParameters(self.p, showTop=False) | |
92 | + self.mplot1 = pg.PlotWidget() | |
93 | + self.mplot2 = pg.PlotWidget() | |
94 | + plot_lay = QVBoxLayout() | |
95 | + plot_lay.addWidget(self.mplot1) | |
96 | + plot_lay.addWidget(self.mplot2) | |
97 | + main_lay = QHBoxLayout() | |
98 | + main_lay.addWidget(self.ptree) | |
99 | + main_lay.addLayout(plot_lay) | |
100 | + main_lay.setStretchFactor(plot_lay, 2) | |
101 | + central_widget = QWidget() | |
102 | + central_widget.setLayout(main_lay) | |
103 | + return central_widget | |
104 | + | |
105 | +#=============================================================================== | |
106 | +class PeakApp(QApplication): | |
107 | + """Peak application. | |
108 | + """ | |
109 | + | |
110 | + # Emit data key value | |
111 | + acquire_done = pyqtSignal() | |
112 | + fit_done = pyqtSignal(object, object) | |
113 | + | |
114 | + def __init__(self, args): | |
115 | + """Constructor. | |
116 | + :returns: None | |
117 | + """ | |
118 | + super().__init__(args) | |
119 | + self._ui = PeakUi() | |
120 | + self._data = None | |
121 | + self._cdata_name = None | |
122 | + self._dplot = None # Data plot | |
123 | + self._fdplot = None # Fitted data plot | |
124 | + self._pplot = None # (Extracted) Peak plot | |
125 | + self._ui.p.param( | |
126 | + 'Load data', 'VNA', 'Acquisition').sigActivated.connect( | |
127 | + self.acquire_data_from_device) | |
128 | + self._ui.p.param('Load data', 'File', 'Open').sigActivated.connect( | |
129 | + self.acquire_data_from_file) | |
130 | + self._ui.p.param('Fit', 'Run fit').sigActivated.connect( | |
131 | + lambda: self.fit_peak(self._ui.p.param('Fit', 'Order').value(), | |
132 | + self._ui.p.param('Fit', 'Filtering').value())) | |
133 | + self.acquire_done.connect(self.handle_acq_data) | |
134 | + self.fit_done.connect(self.display_fit) | |
135 | + self._ui.show() | |
136 | + | |
137 | + @pyqtSlot() | |
138 | + def acquire_data_from_device(self): | |
139 | + """Acquire data process. | |
140 | + :returns: None | |
141 | + """ | |
142 | + ip = self._ui.p.param('Load data', 'VNA', 'IP').value() | |
143 | + port = self._ui.p.param('Load data', 'VNA', 'Port').value() | |
144 | + # Acquisition itself | |
145 | + dev = MyVna(ip, port) | |
146 | + dev.connect() | |
147 | + dev.write("FORMAT:DATA REAL,64") | |
148 | + try: | |
149 | + datas = dev.get_measurements() | |
150 | + except Exception as ex: | |
151 | + logging.error("Problem during acquisition: %s", str(ex)) | |
152 | + QMessageBox.warning(self._ui, "Acquisition problem", | |
153 | + "Problem during acquisition: {}".format(ex), | |
154 | + QMessageBox.Ok) | |
155 | + return | |
156 | + self._data = {dev.measurement_number_to_name(idx+1): | |
157 | + data for idx, data in enumerate(datas)} | |
158 | + self.acquire_done.emit() | |
159 | + | |
160 | + @pyqtSlot() | |
161 | + def acquire_data_from_file(self): | |
162 | + filename = QFileDialog().getOpenFileName( | |
163 | + parent=None, | |
164 | + caption="Choose .s2p file to load", | |
165 | + directory=QDir.currentPath(), | |
166 | + filter="s2p files (*.s2p);;Any files (*)") | |
167 | + if filename == '': | |
168 | + return | |
169 | + try: | |
170 | + data = np.transpose(np.loadtxt(filename, comments=('!', '#'))) | |
171 | + except Exception as ex: | |
172 | + logging.error("Problem when reading file: %s", str(ex)) | |
173 | + QMessageBox.warning(self._ui, "Acquisition problem", | |
174 | + "Problem when reading file: {}".format(ex), | |
175 | + QMessageBox.Ok) | |
176 | + return | |
177 | + self._data = {QFileInfo(filename).baseName(): data} | |
178 | + self.acquire_done.emit() | |
179 | + | |
180 | + @pyqtSlot() | |
181 | + def handle_acq_data(self): | |
182 | + self._ui.p.param('Plot').clearChildren() | |
183 | + for name_, data in self._data.items(): | |
184 | + self._ui.p.param('Plot').addChild( | |
185 | + Parameter.create(name=name_, type='action')) | |
186 | + for child in self._ui.p.param('Plot').children(): | |
187 | + child.sigActivated.connect(self.set_cdata) | |
188 | + self.set_cdata(self._ui.p.param('Plot').children()[0]) | |
189 | + | |
190 | + @pyqtSlot(object) | |
191 | + def set_cdata(self, param): | |
192 | + """ | |
193 | + :param param: Parameter object | |
194 | + """ | |
195 | + self._cdata_name = param.name() | |
196 | + self.display_data() | |
197 | + | |
198 | + @pyqtSlot() | |
199 | + def display_data(self): | |
200 | + if self._dplot is not None: | |
201 | + self._ui.mplot1.removeItem(self._dplot) | |
202 | + if self._fdplot is not None: | |
203 | + self._ui.mplot1.removeItem(self._fdplot) | |
204 | + if self._pplot is not None: | |
205 | + self._ui.mplot2.removeItem(self._pplot) | |
206 | + self._dplot = self._ui.mplot1.plot( | |
207 | + self._data[self._cdata_name][0, :], | |
208 | + self._data[self._cdata_name][1, :], pen="w") | |
209 | + self._ui.region.setRegion([self._data[self._cdata_name][0, 0], | |
210 | + self._data[self._cdata_name][0, -1]]) | |
211 | + | |
212 | + @pyqtSlot(int, bool) | |
213 | + def fit_peak(self, order, filter_=False): | |
214 | + name_ = self._cdata_name | |
215 | + x_min, x_max = self._ui.region.getRegion() | |
216 | + x_min_id = np.where(self._data[name_][0, :] >= x_min)[0][0] | |
217 | + x_max_id = np.where(self._data[name_][0, :] <= x_max)[0][-1] | |
218 | + x_n = np.concatenate([self._data[name_][0, 1:x_min_id], | |
219 | + self._data[name_][0, x_max_id:-1]]) | |
220 | + # Suppress mean to get better fitting: | |
221 | + x_n0 = x_n - self._data[name_][0, :].mean() | |
222 | + y_n = np.concatenate([self._data[name_][1, 1:x_min_id], | |
223 | + self._data[name_][1, x_max_id:-1]]) | |
224 | + try: | |
225 | + coefs = poly.polyfit(x_n0, y_n, order) | |
226 | + except TypeError: # when user has not selected region to exclude | |
227 | + QMessageBox.information(self._ui, | |
228 | + "ROI omited", | |
229 | + "Select region to exclude before fit") | |
230 | + return | |
231 | + xfit = self._data[name_][0, :] - self._data[name_][0, :].mean() | |
232 | + yfit = poly.polyval(xfit, coefs) | |
233 | + res = self._data[name_][1, :] - yfit | |
234 | + if filter_ is True: | |
235 | + b, a = scs.butter(3, 0.005) | |
236 | + res = scs.filtfilt(b, a, res, padlen=150) | |
237 | + self.fit_done.emit(yfit, res) | |
238 | + | |
239 | + @pyqtSlot(object, object) | |
240 | + def display_fit(self, yfit, res): | |
241 | + name_ = self._cdata_name | |
242 | + if self._fdplot is not None: | |
243 | + self._ui.mplot1.removeItem(self._fdplot) | |
244 | + if self._pplot is not None: | |
245 | + self._ui.mplot2.removeItem(self._pplot) | |
246 | + self._fdplot = self._ui.mplot1.plot( | |
247 | + self._data[name_][0, :], yfit, pen='g') | |
248 | + self._pplot = self._ui.mplot2.plot( | |
249 | + self._data[name_][0, :]-self._data[name_][0, :].mean(), | |
250 | + res, pen='g') | |
251 | + | |
252 | +#============================================================================== | |
253 | +def configure_logging(): | |
254 | + """Configures logs. | |
255 | + """ | |
256 | + home = path.expanduser("~") | |
257 | + log_file = "." + APP_NAME + ".log" | |
258 | + abs_log_file = path.join(home, log_file) | |
259 | + date_fmt = "%d/%m/%Y %H:%M:%S" | |
260 | + log_format = "%(asctime)s %(levelname) -8s %(filename)s " + \ | |
261 | + " %(funcName)s (%(lineno)d): %(message)s" | |
262 | + logging.basicConfig(level=FILE_LOG_LEVEL, \ | |
263 | + datefmt=date_fmt, \ | |
264 | + format=log_format, \ | |
265 | + filename=abs_log_file, \ | |
266 | + filemode='w') | |
267 | + console = logging.StreamHandler() | |
268 | + # define a Handler which writes messages to the sys.stderr | |
269 | + console.setLevel(CONSOLE_LOG_LEVEL) | |
270 | + # set a format which is simpler for console use | |
271 | + console_format = '%(levelname) -8s %(filename)s (%(lineno)d): %(message)s' | |
272 | + formatter = logging.Formatter(console_format) | |
273 | + # tell the handler to use this format | |
274 | + console.setFormatter(formatter) | |
275 | + # add the handler to the root logger | |
276 | + logging.getLogger('').addHandler(console) | |
277 | + | |
278 | +#============================================================================== | |
279 | +def main(): | |
280 | + configure_logging() | |
281 | + app = PeakApp(sys.argv) | |
282 | + sys.exit(app.exec_()) | |
283 | + | |
284 | +#============================================================================== | |
285 | +if __name__ == '__main__': | |
286 | + main() |
n523x_utils/n523x.py
... | ... | @@ -0,0 +1,363 @@ |
1 | +# -*- coding: utf-8 -*- | |
2 | + | |
3 | +"""package n523x_utils | |
4 | +author Benoit Dubois | |
5 | +copyright FEMTO ENGINEERING | |
6 | +license GPL v3.0+ | |
7 | +brief Handle PNA-X, N522xA or N523x device from Keysight. | |
8 | + Tested on N5234 device. | |
9 | +""" | |
10 | + | |
11 | +import time | |
12 | +import socket | |
13 | +import struct | |
14 | +import logging | |
15 | +import numpy as np | |
16 | + | |
17 | +DEFAULT_IP = "192.168.0.11" | |
18 | +DEFAULT_PORT = 5025 | |
19 | + | |
20 | +#============================================================================== | |
21 | +class N523x(object): | |
22 | + """Handle PNA-X, N522xA or N523x device from Keysight. | |
23 | + """ | |
24 | + | |
25 | + IFBW = (1, 2, 3, 5, 7, 10, 15, 20, 30, 50, 70, 100, 150, 200, 300, 500, 700, | |
26 | + 1e3, 1.5e3, 2e3, 3e3, 5e3, 7e3, 10e3, 15e3, 20e3, 30e3, 50e3, 70e3, | |
27 | + 100e3, 150e3, 200e3, 280e3, 360e3, 600e3) | |
28 | + | |
29 | + def __init__(self, ip, port=DEFAULT_PORT, timeout=1.0): | |
30 | + """Constructor. | |
31 | + :param ip: IP address of device (str) | |
32 | + :param port: Ethernet port of device (int) | |
33 | + :param timeout: Timeout on connection instance (float) | |
34 | + :returns: None | |
35 | + """ | |
36 | + self.ip = ip | |
37 | + self.port = port | |
38 | + self._timeout = timeout | |
39 | + self._sock = None | |
40 | + | |
41 | + def connect(self): | |
42 | + """Connect to device. | |
43 | + """ | |
44 | + try: | |
45 | + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
46 | + self._sock.settimeout(self._timeout) | |
47 | + self._sock.connect((self.ip, self.port)) | |
48 | + except ValueError as ex: | |
49 | + logging.error("Connection parameters out of range: %s", str(ex)) | |
50 | + return False | |
51 | + except socket.timeout: | |
52 | + logging.error("Timeout on connection") | |
53 | + return False | |
54 | + except Exception as ex: | |
55 | + logging.error("Unexpected exception during connection with " + \ | |
56 | + "VNA: %s", str(ex)) | |
57 | + return False | |
58 | + else: | |
59 | + logging.debug("Connected to VNA") | |
60 | + return True | |
61 | + | |
62 | + def write(self, data): | |
63 | + """"Ethernet writing process. | |
64 | + :param data: data writes to device (str) | |
65 | + :returns: None | |
66 | + """ | |
67 | + try: | |
68 | + self._sock.send((data + "\n").encode('utf-8')) | |
69 | + except socket.timeout: | |
70 | + logging.error("Timeout") | |
71 | + except Exception as ex: | |
72 | + logging.error(str(ex)) | |
73 | + logging.debug("write " + data.strip('\n')) | |
74 | + | |
75 | + def read(self, length=100): | |
76 | + """Read process. | |
77 | + :param length: length of message to read (int) | |
78 | + :returns: Message reads from device (str) | |
79 | + """ | |
80 | + try: | |
81 | + retval = self._sock.recv(length).decode('utf-8').strip('\n') | |
82 | + except socket.timeout: | |
83 | + logging.error("Timeout") | |
84 | + return '' | |
85 | + except Exception as ex: | |
86 | + logging.error(str(ex)) | |
87 | + raise ex | |
88 | + logging.debug("read: " + retval) | |
89 | + return retval | |
90 | + | |
91 | + def raw_read(self, length=512): | |
92 | + """Raw read process. | |
93 | + :param length: length of message to read (int) | |
94 | + :returns: Message reads from device (bytes) | |
95 | + """ | |
96 | + try: | |
97 | + data = self._sock.recv(length) | |
98 | + except socket.timeout: | |
99 | + logging.error("Timeout") | |
100 | + return bytes() | |
101 | + except Exception as ex: | |
102 | + logging.error(str(ex)) | |
103 | + raise ex | |
104 | + return data | |
105 | + | |
106 | + def bin_read(self): | |
107 | + """Read binary data then decode them to ascii. | |
108 | + The reading process is specific to the transfert of binary data | |
109 | + with these VNA devices: <header><data><EOT>, with: | |
110 | + - <header>: #|lenght of bytes_count (one byte)|bytes_count | |
111 | + - <data>: "REAL,64" (float64) binary data | |
112 | + - <EOT>: '\n' character. | |
113 | + Note: The data transfert format must be selected to "REAL,64"" before | |
114 | + using this method | |
115 | + """ | |
116 | + header_max_length = 11 | |
117 | + raw_data = self.raw_read(header_max_length) | |
118 | + if raw_data.find(b'#') != 0: | |
119 | + logging.error("Data header not valid") | |
120 | + return | |
121 | + byte_count_nb = int(raw_data[1:2]) | |
122 | + byte_count = int(raw_data[2:2+byte_count_nb]) | |
123 | + # Note : Read 'byte_count' bytes but only | |
124 | + # 2 + byte_count_nb + byte_count - header_max_length | |
125 | + # needs to be readen. | |
126 | + # This tip can be used because EOF ('\n') is transmited at the end of | |
127 | + # the message and thus stop reception of data. | |
128 | + while len(raw_data) < byte_count: | |
129 | + raw_data += self.raw_read(byte_count) | |
130 | + nb_elem = int(byte_count / 8) | |
131 | + data = np.asarray(struct.unpack(">{:d}d".format(nb_elem), | |
132 | + raw_data[2+byte_count_nb:-1])) | |
133 | + return data | |
134 | + | |
135 | + def query(self, msg, length=100): | |
136 | + """Basic query process: write query then read response. | |
137 | + """ | |
138 | + self.write(msg) | |
139 | + return self.read(length) | |
140 | + | |
141 | + def reset(self): | |
142 | + """Reset device. | |
143 | + """ | |
144 | + self.write("*RST") | |
145 | + | |
146 | + @property | |
147 | + def id(self): | |
148 | + """Return ID of device. | |
149 | + """ | |
150 | + return self.query("*IDN?") | |
151 | + | |
152 | + def get_span(self, cnum=1): | |
153 | + return self.query("SENS{}:FOM:RANG:SEGM:FREQ:SPAN?".format(cnum)) | |
154 | + | |
155 | + def set_span(self, value, cnum=1): | |
156 | + self.write("SENS{}:FREQ:SPAN {}".format(cnum, value)) | |
157 | + | |
158 | + def get_start(self, cnum=1): | |
159 | + return self.query("SENS{}:FREQ:START?".format(cnum)) | |
160 | + | |
161 | + def set_start(self, value, cnum=1): | |
162 | + self.write("SENS{}:FREQ:START {}".format(cnum, value)) | |
163 | + | |
164 | + def get_stop(self, cnum=1): | |
165 | + return self.query("SENS{}:FREQ:STOP?".format(cnum)) | |
166 | + | |
167 | + def set_stop(self, value, cnum=1): | |
168 | + self.write("SENS{}:FREQ:STOP {}".format(cnum, value)) | |
169 | + | |
170 | + def get_center_freq(self, cnum=1): | |
171 | + return self.query("SENS{}:FREQ:CENT?".format(cnum)) | |
172 | + | |
173 | + def set_center_freq(self, value, cnum=1): | |
174 | + self.write("SENS{}:FREQ:CENT {}".format(cnum, value)) | |
175 | + | |
176 | + def get_points(self, cnum=1): | |
177 | + return self.query("SENS{}:SWE:POINTS?".format(cnum)) | |
178 | + | |
179 | + def set_points(self, value, cnum=1): | |
180 | + self.write("SENS{}:SWE:POINTS {}".format(cnum, value)) | |
181 | + | |
182 | + def get_sweep_type(self, cnum=1): | |
183 | + return self.query("SENS{}:SWE:TYPE?".format(cnum)) | |
184 | + | |
185 | + def set_sweep_type(self, value, cnum=1): | |
186 | + self.write("SENS{}:SWE:TYPE {}".format(cnum, value)) | |
187 | + | |
188 | + @staticmethod | |
189 | + def read_s2p(filename): | |
190 | + return np.loadtxt(filename, comments=['!', '#']) | |
191 | + | |
192 | + def get_window_numbers(self): | |
193 | + """Return the number of existing windows. | |
194 | + """ | |
195 | + data = self.query("DISP:CAT?") | |
196 | + if data is None: | |
197 | + return [] | |
198 | + return [int(x) for x in data.replace('\"', '').split(',')] | |
199 | + | |
200 | + def get_measurement_catalog(self, channel=''): | |
201 | + """Returns ALL measurement numbers, or measurement numbers | |
202 | + from a specified channel | |
203 | + :param channel: Channel number to catalog. If not specified, | |
204 | + all measurement numbers are returned. | |
205 | + :returns: ALL measurement numbers, or measurement numbers | |
206 | + from a specified channel | |
207 | + """ | |
208 | + data = self.query("SYST:MEAS:CAT? {}".format(channel)) | |
209 | + if data is None: | |
210 | + return [] | |
211 | + return [int(x) for x in data.replace('\"', '').split(',')] | |
212 | + | |
213 | + def measurement_number_to_trace(self, nb=None): | |
214 | + """Returns the trace number of the specified measurement number. | |
215 | + Trace numbers restart for each window while measurement numbers are | |
216 | + always unique. | |
217 | + :param n: Measurement number for which to return the trace number. | |
218 | + If unspecified, value is set to 1. | |
219 | + """ | |
220 | + return self.query("SYST:MEAS{}:TRAC?".format(nb)) | |
221 | + | |
222 | + def measurement_number_to_name(self, nb=None): | |
223 | + """Returns the name of the specified measurement. | |
224 | + :param n: Measurement number for which to return the measurement name. | |
225 | + If unspecified, value is set to 1. | |
226 | + """ | |
227 | + return self.query("SYST:MEAS{}:NAME?".format(nb)).replace('\"', '') | |
228 | + | |
229 | + def set_measurement(self, name, fast=True): | |
230 | + """ Sets the selected measurement. Most CALC: commands require that | |
231 | + this command be sent before a setting change is made. One measurement | |
232 | + on each channel can be selected at the same time. | |
233 | + :param name: Name of the measurement. CASE-SENSITIVE. Do NOT include | |
234 | + the parameter name that is returned with Calc:Par:Cat? | |
235 | + :param fast: Optional. The PNA display is NOT updated. Therefore, | |
236 | + do not use this argument when an operator is using the | |
237 | + PNA display. Otherwise, sending this argument results | |
238 | + in much faster sweep speeds. There is NO other reason | |
239 | + to NOT send this argument. | |
240 | + """ | |
241 | + if name is None: | |
242 | + logging.error("Requiered name parameter") | |
243 | + raise ValueError("Requiered name parameter") | |
244 | + cnum = int(name[2]) | |
245 | + self.write("CALC{}:PAR:SEL {}{}" | |
246 | + .format(cnum, name, ",fast" if fast is True else None)) | |
247 | + | |
248 | + def get_measurement(self, name): | |
249 | + """Get a data measurement. | |
250 | + Note that VNA must be configured to transfer data in float 64 format | |
251 | + before using the method. | |
252 | + :param name: Name of the measurement. CASE-SENSITIVE. Do NOT include | |
253 | + the parameter name that is returned with Calc:Par:Cat? | |
254 | + :return: Array of measurement data. | |
255 | + """ | |
256 | + cnum = int(name[2]) | |
257 | + self.set_measurement(name) | |
258 | + if self.get_sweep_type() != "LIN": | |
259 | + raise NotImplementedError | |
260 | + datax = np.linspace(float(self.get_start(cnum)), | |
261 | + float(self.get_stop(cnum)), | |
262 | + int(self.get_points(cnum))) | |
263 | + self.write("CALC{}:DATA? FDATA".format(cnum)) | |
264 | + datay = self.bin_read() | |
265 | + retval = np.asarray([datax, datay]) | |
266 | + return retval | |
267 | + | |
268 | + def get_snp(self, cnum=1): | |
269 | + """Get snp data. | |
270 | + :param cnum: Channel number of the measurement. There must be a selected | |
271 | + measurement on that channel. If unspecified, <cnum> is set to 1. | |
272 | + :return: Array of data. | |
273 | + """ | |
274 | + self.write("CALC{}:DATA:SNP:PORT? \"1,2\"".format(cnum)) | |
275 | + return np.asarray(self.bin_read()).reshape(9, -1) | |
276 | + | |
277 | + def get_measurements(self): | |
278 | + """Find current measurements, get data then prepare a list of array | |
279 | + [freq, data] for each measurement. | |
280 | + :returns: list of measurements | |
281 | + """ | |
282 | + datas = [] | |
283 | + meas_nb = self.get_measurement_catalog() | |
284 | + for nb in meas_nb: | |
285 | + name = self.measurement_number_to_name(nb) | |
286 | + datas.append(self.get_measurement(name)) | |
287 | + return datas | |
288 | + | |
289 | + def get_snps(self): | |
290 | + """Get all snp traces. | |
291 | + """ | |
292 | + meas_nb = self.get_measurement_catalog() | |
293 | + datas = [] | |
294 | + for measurement in meas_nb: | |
295 | + datas.append(self.get_snp(measurement)) | |
296 | + return datas | |
297 | + | |
298 | + def write_snps(self, filename=None): | |
299 | + """Write all snp traces. | |
300 | + """ | |
301 | + if filename is None: | |
302 | + filename = time.strftime("%Y%m%d-%H%M%S", time.gmtime()) | |
303 | + meas_nb = self.get_measurement_catalog() | |
304 | + for measurement in meas_nb: | |
305 | + data = np.asarray(self.get_snp(measurement)).reshape(9, -1) | |
306 | + np.savetxt(filename + '_' + str(measurement) + '.s2p', | |
307 | + data, | |
308 | + delimiter='\t', | |
309 | + header="#f(Hz)\tReal(S11)\tImag(S11)" + | |
310 | + "\tReal(S21)\tImag(S21)" + | |
311 | + "\tReal(S12)\tImag(S12)" + | |
312 | + "\tReal(S22)\tImag(S22)") | |
313 | + | |
314 | + | |
315 | +#============================================================================== | |
316 | +def main(): | |
317 | + """Main of test program | |
318 | + :returns: None | |
319 | + """ | |
320 | + import matplotlib.pyplot as plt | |
321 | + | |
322 | + acp_type = 'data' # 'data' or 'snp' | |
323 | + | |
324 | + dev = N523x(DEFAULT_IP, timeout=0.5) | |
325 | + | |
326 | + if dev.connect() is False: | |
327 | + print("Connection error") | |
328 | + return | |
329 | + | |
330 | + print("Connected to", dev.id) | |
331 | + | |
332 | + dev.write("FORMAT:DATA REAL,64") | |
333 | + | |
334 | + if acp_type == 'snp': | |
335 | + dev.write("MMEM:STOR:TRAC:FORM:SNP MA") | |
336 | + datas = dev.get_snp() | |
337 | + plt.figure(1) | |
338 | + nb_rows = datas.shape[0] | |
339 | + for idx, data in enumerate(datas): | |
340 | + plt.subplot(nb_rows*100+10+idx) | |
341 | + plt.plot(datas[0], data, 'b') | |
342 | + plt.show() | |
343 | + | |
344 | + else: | |
345 | + datas = dev.get_measurements() | |
346 | + plt.figure(1) | |
347 | + nb_rows = len(datas) | |
348 | + for idx, data in enumerate(datas): | |
349 | + plt.subplot(nb_rows*100+10+idx+1) | |
350 | + plt.ylabel(dev.measurement_number_to_name(idx+1)) | |
351 | + try: | |
352 | + plt.plot(data[0], data[1], 'b', ) | |
353 | + except Exception: | |
354 | + pass | |
355 | + plt.show() | |
356 | + | |
357 | +#============================================================================== | |
358 | +if __name__ == '__main__': | |
359 | + CONSOLE_LOG_LEVEL = logging.DEBUG | |
360 | + CONSOLE_FORMAT = '%(levelname) -8s %(filename)s (%(lineno)d): %(message)s' | |
361 | + logging.basicConfig(format=CONSOLE_FORMAT, level=CONSOLE_LOG_LEVEL) | |
362 | + | |
363 | + main() |