mmcb-rs232-avt 1.0.14__py3-none-any.whl → 1.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mmcb_rs232/__init__.py +0 -0
- mmcb_rs232/common.py +2605 -0
- mmcb_rs232/detect.py +1053 -0
- mmcb_rs232/dmm.py +126 -0
- mmcb_rs232/dmm_interface.py +162 -0
- mmcb_rs232/iv.py +2868 -0
- mmcb_rs232/lexicon.py +580 -0
- mmcb_rs232/psuset.py +938 -0
- mmcb_rs232/psustat.py +705 -0
- mmcb_rs232/psuwatch.py +540 -0
- mmcb_rs232/sequence.py +483 -0
- mmcb_rs232/ult80.py +500 -0
- {mmcb_rs232_avt-1.0.14.dist-info → mmcb_rs232_avt-1.0.18.dist-info}/METADATA +1 -1
- mmcb_rs232_avt-1.0.18.dist-info/RECORD +17 -0
- mmcb_rs232_avt-1.0.18.dist-info/top_level.txt +1 -0
- mmcb_rs232_avt-1.0.14.dist-info/RECORD +0 -5
- mmcb_rs232_avt-1.0.14.dist-info/top_level.txt +0 -1
- {mmcb_rs232_avt-1.0.14.dist-info → mmcb_rs232_avt-1.0.18.dist-info}/WHEEL +0 -0
- {mmcb_rs232_avt-1.0.14.dist-info → mmcb_rs232_avt-1.0.18.dist-info}/entry_points.txt +0 -0
mmcb_rs232/iv.py
ADDED
|
@@ -0,0 +1,2868 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generates IV and (optional) IT plots using power supplies connected by RS232.
|
|
4
|
+
|
|
5
|
+
Tests will be run concurrently on *ALL* detected PSUs unless
|
|
6
|
+
otherwise specified.
|
|
7
|
+
|
|
8
|
+
Supported power supplies:
|
|
9
|
+
|
|
10
|
+
hvpsu : keithley 2410, 2614b; ISEG SHQ 222M, 224M
|
|
11
|
+
|
|
12
|
+
Safety:
|
|
13
|
+
|
|
14
|
+
To protect devices under test, the script ensures that the rate of change of
|
|
15
|
+
voltage is strictly limited to no more than one 10V step per second.
|
|
16
|
+
|
|
17
|
+
The script sets the current limit on the power supply, and will stop the
|
|
18
|
+
test early if the leakage current exceeds a slightly lower figure.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import collections
|
|
23
|
+
import concurrent.futures as cf
|
|
24
|
+
import contextlib
|
|
25
|
+
import csv
|
|
26
|
+
import ctypes
|
|
27
|
+
import datetime
|
|
28
|
+
import functools
|
|
29
|
+
import itertools
|
|
30
|
+
import logging
|
|
31
|
+
import math
|
|
32
|
+
import multiprocessing as mp # Process, Queue
|
|
33
|
+
import os
|
|
34
|
+
import pathlib
|
|
35
|
+
import platform
|
|
36
|
+
import random
|
|
37
|
+
import socket
|
|
38
|
+
import statistics as stat
|
|
39
|
+
import sys
|
|
40
|
+
import threading
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
import matplotlib
|
|
44
|
+
# agg is used only for writing plots to files, not to the window manager
|
|
45
|
+
# this option is set to avoid problems running the script on remote hosts
|
|
46
|
+
# over ssh, matplotlib.use must be called in this exact position
|
|
47
|
+
matplotlib.use('agg')
|
|
48
|
+
import matplotlib.pyplot as plt
|
|
49
|
+
from matplotlib.ticker import EngFormatter
|
|
50
|
+
import serial
|
|
51
|
+
import zmq
|
|
52
|
+
|
|
53
|
+
from yoctopuce import yocto_api as yapi
|
|
54
|
+
from yoctopuce import yocto_temperature as ytemp
|
|
55
|
+
from yoctopuce import yocto_humidity as yhumi
|
|
56
|
+
|
|
57
|
+
from mmcb import common
|
|
58
|
+
from mmcb import lexicon
|
|
59
|
+
from mmcb import sequence
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
##############################################################################
|
|
63
|
+
# command line option handler
|
|
64
|
+
##############################################################################
|
|
65
|
+
|
|
66
|
+
def check_settling_time(val):
|
|
67
|
+
"""
|
|
68
|
+
Check settling time is reasonable.
|
|
69
|
+
|
|
70
|
+
--------------------------------------------------------------------------
|
|
71
|
+
args
|
|
72
|
+
val : float
|
|
73
|
+
allowable percentage deviation from mean
|
|
74
|
+
--------------------------------------------------------------------------
|
|
75
|
+
returns : int
|
|
76
|
+
--------------------------------------------------------------------------
|
|
77
|
+
"""
|
|
78
|
+
val = float(val)
|
|
79
|
+
if not 0.25 <= val <= 10:
|
|
80
|
+
raise argparse.ArgumentTypeError(
|
|
81
|
+
f'{val}: settling time should between 0.25 and 10 seconds.')
|
|
82
|
+
return val
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check_minutes(val):
|
|
86
|
+
"""
|
|
87
|
+
check basic validity of minutes value
|
|
88
|
+
|
|
89
|
+
--------------------------------------------------------------------------
|
|
90
|
+
args
|
|
91
|
+
val : float
|
|
92
|
+
allowable percentage deviation from mean
|
|
93
|
+
--------------------------------------------------------------------------
|
|
94
|
+
returns : int
|
|
95
|
+
--------------------------------------------------------------------------
|
|
96
|
+
"""
|
|
97
|
+
val = int(val)
|
|
98
|
+
if not 0 <= val <= 2880:
|
|
99
|
+
raise argparse.ArgumentTypeError(
|
|
100
|
+
f'{val}: '
|
|
101
|
+
'hold time in minutes should be less than 2880 (48 hours)')
|
|
102
|
+
return val
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_percent(val):
|
|
106
|
+
"""
|
|
107
|
+
check basic validity of percentage value
|
|
108
|
+
|
|
109
|
+
--------------------------------------------------------------------------
|
|
110
|
+
args
|
|
111
|
+
val : float
|
|
112
|
+
allowable percentage deviation from mean
|
|
113
|
+
--------------------------------------------------------------------------
|
|
114
|
+
returns : float
|
|
115
|
+
--------------------------------------------------------------------------
|
|
116
|
+
"""
|
|
117
|
+
val = float(val)
|
|
118
|
+
if val >= 1000:
|
|
119
|
+
raise argparse.ArgumentTypeError(
|
|
120
|
+
f'{val}: '
|
|
121
|
+
'percentage deviation from the mean should be less than 1000')
|
|
122
|
+
return val
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_psunum(val):
|
|
126
|
+
"""
|
|
127
|
+
check basic validity of number
|
|
128
|
+
|
|
129
|
+
--------------------------------------------------------------------------
|
|
130
|
+
args
|
|
131
|
+
val : int
|
|
132
|
+
number of power supplies to emulate
|
|
133
|
+
--------------------------------------------------------------------------
|
|
134
|
+
returns : int
|
|
135
|
+
--------------------------------------------------------------------------
|
|
136
|
+
"""
|
|
137
|
+
val = int(val)
|
|
138
|
+
if val <= 0:
|
|
139
|
+
raise argparse.ArgumentTypeError(
|
|
140
|
+
f'{val}: '
|
|
141
|
+
'number of emulated power supplies should be 1 or more')
|
|
142
|
+
return val
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_samples(val):
|
|
146
|
+
"""
|
|
147
|
+
check basic validity of number of samples
|
|
148
|
+
|
|
149
|
+
--------------------------------------------------------------------------
|
|
150
|
+
args
|
|
151
|
+
val : int
|
|
152
|
+
number of samples
|
|
153
|
+
--------------------------------------------------------------------------
|
|
154
|
+
returns : int
|
|
155
|
+
--------------------------------------------------------------------------
|
|
156
|
+
"""
|
|
157
|
+
val = int(val)
|
|
158
|
+
if not 10 <= val <= 100:
|
|
159
|
+
raise argparse.ArgumentTypeError(
|
|
160
|
+
f'{val}: '
|
|
161
|
+
'number of samples should be between 10 and 100')
|
|
162
|
+
return val
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_stepsize(val):
|
|
166
|
+
"""
|
|
167
|
+
Expect int step size
|
|
168
|
+
|
|
169
|
+
--------------------------------------------------------------------------
|
|
170
|
+
args
|
|
171
|
+
val : int
|
|
172
|
+
step size in volts
|
|
173
|
+
--------------------------------------------------------------------------
|
|
174
|
+
returns : int
|
|
175
|
+
--------------------------------------------------------------------------
|
|
176
|
+
"""
|
|
177
|
+
val = abs(int(val))
|
|
178
|
+
if not 1 <= val <= 10:
|
|
179
|
+
raise argparse.ArgumentTypeError(
|
|
180
|
+
f'{val}: value should be between 1 and 10')
|
|
181
|
+
return val
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def check_voltage(val):
|
|
185
|
+
"""
|
|
186
|
+
check basic validity of relative movement value
|
|
187
|
+
|
|
188
|
+
--------------------------------------------------------------------------
|
|
189
|
+
args
|
|
190
|
+
val : int
|
|
191
|
+
bias voltage
|
|
192
|
+
--------------------------------------------------------------------------
|
|
193
|
+
returns : int
|
|
194
|
+
--------------------------------------------------------------------------
|
|
195
|
+
"""
|
|
196
|
+
val = int(val)
|
|
197
|
+
if not -1100 <= val <= 1100:
|
|
198
|
+
raise argparse.ArgumentTypeError(
|
|
199
|
+
f'{val}: '
|
|
200
|
+
'voltage should be between -1100V and 1100V')
|
|
201
|
+
return val
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def check_arguments(settings):
|
|
205
|
+
"""
|
|
206
|
+
handle command line options
|
|
207
|
+
|
|
208
|
+
--------------------------------------------------------------------------
|
|
209
|
+
args
|
|
210
|
+
settings : dictionary
|
|
211
|
+
contains core information about the test environment
|
|
212
|
+
--------------------------------------------------------------------------
|
|
213
|
+
returns
|
|
214
|
+
settings : no explicit return, mutable type amended in place
|
|
215
|
+
--------------------------------------------------------------------------
|
|
216
|
+
"""
|
|
217
|
+
parser = argparse.ArgumentParser(
|
|
218
|
+
description='Script to generate IV (and optionally IT) plots using\
|
|
219
|
+
Keithley 2410 and 2614b power supplies over RS232. ISEG SHQ\
|
|
220
|
+
222M and 224M power supplies may also be used though these are slow\
|
|
221
|
+
to converge to set voltages (and may never converge when the set\
|
|
222
|
+
voltage is in the region -10 <= V <= 10) hence the test may take a\
|
|
223
|
+
long time to complete. If detected, data from YoctoPuce humidity and\
|
|
224
|
+
PT100 temperature sensors will be incorporated into exported data and\
|
|
225
|
+
(appropriate) plots. Tests will be run concurrently on *ALL* detected\
|
|
226
|
+
high-voltage PSUs unless specifically excluded using --alias. Support\
|
|
227
|
+
requests to: Alan Taylor, Physics Dept.,\
|
|
228
|
+
University of Liverpool, avt@hep.ph.liv.ac.uk')
|
|
229
|
+
parser.add_argument(
|
|
230
|
+
'voltage', nargs='?', metavar='voltage',
|
|
231
|
+
help='value in volts',
|
|
232
|
+
type=check_voltage, default=None)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
'--stepsize', nargs=1, metavar='stepsize',
|
|
235
|
+
help='Use a constant step size between 1V and 10V, the sign is not\
|
|
236
|
+
important as the script will decide this itself. The default if\
|
|
237
|
+
this option is not specified is to use smaller\
|
|
238
|
+
step sizes when close to zero volts,\
|
|
239
|
+
from 0 to +/-10 with 1V steps,\
|
|
240
|
+
then -10 to +/-50 with 5V steps,\
|
|
241
|
+
then -50 to +/-N with 10V steps.',
|
|
242
|
+
type=check_stepsize, default=None)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
'--range', nargs=1, metavar='filename',
|
|
245
|
+
help='Rather than using the step size parameters detailed in\
|
|
246
|
+
the --stepsize option above, read a range and step definition from\
|
|
247
|
+
a CSV file. Multiple ranges with different step sizes may be defined.\
|
|
248
|
+
The file format is as follows. Comments starting with # are\
|
|
249
|
+
supported - on any line if a # is found, it and any text to the right\
|
|
250
|
+
are ignored. Blank lines are allowed. Valid lines contain either (1)\
|
|
251
|
+
a single value that defines the default step size in volts that\
|
|
252
|
+
applies over the entire range from 0V to the maximum test voltage\
|
|
253
|
+
(only provide this type of line once, valid value is any integer from\
|
|
254
|
+
1 to 10), and (2) three comma-separated values: start, stop, step\
|
|
255
|
+
(provide as many of these as required - the step size should be\
|
|
256
|
+
between 1 and 10). The sign of any of the supplied numbers will be\
|
|
257
|
+
ignored since the script uses the range and step definitions to\
|
|
258
|
+
build a generic number sequence appropriate for forward or reverse\
|
|
259
|
+
bias operation.',
|
|
260
|
+
type=common.check_file_exists, default=None)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
'--omitreturn',
|
|
263
|
+
action='store_true',
|
|
264
|
+
help='Only record data outbound (typically 0V to -nV) and do not\
|
|
265
|
+
record data for the return (typically -nV to 0V). By default the\
|
|
266
|
+
script records data for both outbound and return.')
|
|
267
|
+
parser.add_argument(
|
|
268
|
+
'--atlas',
|
|
269
|
+
action='store_true',
|
|
270
|
+
help='Enable various adjustments for the ATLAS project. When returning\
|
|
271
|
+
to the original voltage after testing, add in an additional delay\
|
|
272
|
+
between each voltage step to help avoid the current limit being\
|
|
273
|
+
exceeded.')
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
'--hold', nargs=1, metavar='minutes',
|
|
276
|
+
help='After the initial outbound IV test, perform an IT test for the\
|
|
277
|
+
given number of minutes, logging the current values and timestamps.\
|
|
278
|
+
A value of 0 will hold indefinitely until the user manually\
|
|
279
|
+
terminates the test with by pressing the \'q\' key followed by enter',
|
|
280
|
+
type=check_minutes, default=None)
|
|
281
|
+
|
|
282
|
+
group1 = parser.add_mutually_exclusive_group()
|
|
283
|
+
group1.add_argument(
|
|
284
|
+
'-f', '--front',
|
|
285
|
+
action='store_true',
|
|
286
|
+
help='Use front output on PSU.')
|
|
287
|
+
group1.add_argument(
|
|
288
|
+
'-r', '--rear',
|
|
289
|
+
action='store_true',
|
|
290
|
+
help='Use rear output on PSU.')
|
|
291
|
+
|
|
292
|
+
# --json requires --itk_serno to be set
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
'--itk_serno', nargs=1, metavar='itk_serno',
|
|
295
|
+
help='ATLAS ITk Pixel module serial number. Add this detail to plot\
|
|
296
|
+
titles and to the JSON file output (see --json).',
|
|
297
|
+
default='')
|
|
298
|
+
parser.add_argument(
|
|
299
|
+
'--json',
|
|
300
|
+
action='store_true',
|
|
301
|
+
help='Write a JSON file for each PSU channel that acquires data.')
|
|
302
|
+
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
'--reset',
|
|
305
|
+
action='store_true',
|
|
306
|
+
help='Reset PSU before setting voltage. This is a diagnostic feature\
|
|
307
|
+
to address topical issues, and should not normally be required')
|
|
308
|
+
parser.add_argument(
|
|
309
|
+
'--forwardbias',
|
|
310
|
+
action='store_true',
|
|
311
|
+
help='Allow the script to create voltage sequences containing\
|
|
312
|
+
positive voltages. As a safety feature, by default the script only\
|
|
313
|
+
allows reverse bias, limiting the output to voltages from 0 to -nV and\
|
|
314
|
+
preventing positive voltage outputs.')
|
|
315
|
+
parser.add_argument(
|
|
316
|
+
'--settle',
|
|
317
|
+
action='store_true',
|
|
318
|
+
help='At each voltage step, monitor PSU current values until they\
|
|
319
|
+
become stable, then capture final value. If the value does not\
|
|
320
|
+
stabilise, a mean value of the last few values is returned.\
|
|
321
|
+
Parameters are --pc and --samples. Explicitly setting either of\
|
|
322
|
+
those parameters implies --settle.')
|
|
323
|
+
parser.add_argument(
|
|
324
|
+
'--settling_time', nargs=1, metavar='settling_time',
|
|
325
|
+
help='Manually set the time between setting the voltage and measuring\
|
|
326
|
+
the voltage and current.',
|
|
327
|
+
type=check_settling_time, default=[2.0])
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
'--pc', nargs=1, metavar='percentage',
|
|
330
|
+
help='the percentage deviation of individual values from the mean\
|
|
331
|
+
that is deemed acceptable, the default value is 5). Setting this\
|
|
332
|
+
value implies --settle.',
|
|
333
|
+
type=check_percent, default=None)
|
|
334
|
+
parser.add_argument(
|
|
335
|
+
'--samples', nargs=1, metavar='samples',
|
|
336
|
+
help='samples is the total number of times data is captured from the\
|
|
337
|
+
PSU for each voltage step. Sensible values are probably less than\
|
|
338
|
+
100. The default (and minimum) value is 10. Setting this value\
|
|
339
|
+
implies --settle.',
|
|
340
|
+
type=check_samples, default=None)
|
|
341
|
+
parser.add_argument(
|
|
342
|
+
'--sense', nargs=1, metavar='sensor_name',
|
|
343
|
+
help='Specify the name of the environmental sensor to receive data\
|
|
344
|
+
from as it appears in sense.py logs. This option ONLY applies to the\
|
|
345
|
+
ATLAS inner tracker (ITK) pixels multi-module cycling box.',
|
|
346
|
+
default=None)
|
|
347
|
+
parser.add_argument(
|
|
348
|
+
'-l', '--limit', nargs=1, metavar='threshold',
|
|
349
|
+
help='The threshold value above which limiting action will be taken.\
|
|
350
|
+
This is the compliance value to be set on the PSU channel, when this\
|
|
351
|
+
value is reached the PSU channel changes from being a constant voltage\
|
|
352
|
+
source to become a constant current source. The default value if unset\
|
|
353
|
+
is 10uA; the minimum possible value is 1nA. Values can be specified\
|
|
354
|
+
with either scientific (10e-9) or engineering notation (10n).',
|
|
355
|
+
type=common.check_current, default=[10e-6])
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
'--label', nargs=1, metavar='plot_label',
|
|
358
|
+
help='if just a single PSU is being used, use this text on the plot\
|
|
359
|
+
instead of the PSU serial number. Use speech marks around the label if\
|
|
360
|
+
it contains spaces.',
|
|
361
|
+
default=None)
|
|
362
|
+
parser.add_argument(
|
|
363
|
+
'--alias', nargs=1, metavar='filename',
|
|
364
|
+
help='Substitute power supply channel identifiers for human readable\
|
|
365
|
+
descriptions in plots and log files, and allow power supply channels\
|
|
366
|
+
to be individually disabled. This option reads in a CSV file where\
|
|
367
|
+
each line consists of five fields: enable, model, serial number,\
|
|
368
|
+
channel identifier, and description. Use the model and serial number\
|
|
369
|
+
as reported by detect.py. Anything after a # is treated as a comment.\
|
|
370
|
+
If the enable field is left empty that will enable the power supply\
|
|
371
|
+
channel (use no/off/disable to disable it). The channel identifier\
|
|
372
|
+
should be omitted for single channel power supplies.',
|
|
373
|
+
type=common.check_file_exists, default=None)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
'--svg', action='store_true',
|
|
376
|
+
help='Plot to the Scalable Vector Graphics (SVG) file format instead\
|
|
377
|
+
of the default Portable Network Graphics (PNG). For the kind of\
|
|
378
|
+
sparse plots typically generated by this script, SVG files are\
|
|
379
|
+
better quality, have smaller file sizes, and render faster than PNG.')
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
'-i', '--initial', action='store_true',
|
|
382
|
+
help='Return power supply to its initial voltage after completion of\
|
|
383
|
+
test.')
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
'-d', '--debug', nargs=1, metavar='number',
|
|
386
|
+
help='Allow testing of the script when a power supply is not\
|
|
387
|
+
available. The number indicates the quantity of single channel power\
|
|
388
|
+
supplies to emulate.',
|
|
389
|
+
type=check_psunum, default=None)
|
|
390
|
+
|
|
391
|
+
args = parser.parse_args()
|
|
392
|
+
|
|
393
|
+
if args.json and not args.itk_serno:
|
|
394
|
+
parser.error("--json requires --itk_serno to be set")
|
|
395
|
+
|
|
396
|
+
handle_complex_arguments(settings, args)
|
|
397
|
+
handle_simple_arguments(settings, args)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def handle_complex_arguments(settings, args):
|
|
401
|
+
"""
|
|
402
|
+
handle command line arguments that:
|
|
403
|
+
|
|
404
|
+
(1) cannot be combined
|
|
405
|
+
(2) affect the values of other arguments
|
|
406
|
+
(3) have values that require transformation
|
|
407
|
+
|
|
408
|
+
--------------------------------------------------------------------------
|
|
409
|
+
args
|
|
410
|
+
settings : dictionary
|
|
411
|
+
contains core information about the test environment
|
|
412
|
+
args : argparse.Namespace
|
|
413
|
+
command line arguments and values
|
|
414
|
+
--------------------------------------------------------------------------
|
|
415
|
+
returns
|
|
416
|
+
settings : no explicit return, mutable type amended in place
|
|
417
|
+
--------------------------------------------------------------------------
|
|
418
|
+
"""
|
|
419
|
+
# do not change settings if none given
|
|
420
|
+
if args.front:
|
|
421
|
+
settings['rear'] = False
|
|
422
|
+
elif args.rear:
|
|
423
|
+
settings['rear'] = True
|
|
424
|
+
|
|
425
|
+
if args.voltage is not None:
|
|
426
|
+
# perform basic safety checks
|
|
427
|
+
if not args.forwardbias and args.voltage > 0:
|
|
428
|
+
sys.exit('to enable positive voltages use --forwardbias')
|
|
429
|
+
elif args.forwardbias and args.voltage < 0:
|
|
430
|
+
sys.exit('to enable negative voltages omit --forwardbias')
|
|
431
|
+
else:
|
|
432
|
+
settings['voltage'] = args.voltage
|
|
433
|
+
else:
|
|
434
|
+
# set sensible defaults
|
|
435
|
+
settings['voltage'] = 80 if args.forwardbias else -80
|
|
436
|
+
|
|
437
|
+
if args.settle or args.samples or args.pc:
|
|
438
|
+
settings['settle'] = True
|
|
439
|
+
|
|
440
|
+
if args.atlas:
|
|
441
|
+
settings['atlas'] = True
|
|
442
|
+
|
|
443
|
+
if args.pc:
|
|
444
|
+
settings['pc'] = args.pc[0] / 100
|
|
445
|
+
|
|
446
|
+
if args.alias:
|
|
447
|
+
filename = args.alias[0]
|
|
448
|
+
common.read_aliases(settings, filename)
|
|
449
|
+
|
|
450
|
+
if args.range:
|
|
451
|
+
filename = args.range[0]
|
|
452
|
+
sequence.read_user_range_step_file(settings, filename)
|
|
453
|
+
|
|
454
|
+
if args.stepsize:
|
|
455
|
+
settings['stepsize'] = args.stepsize[0]
|
|
456
|
+
if args.atlas:
|
|
457
|
+
print('--stepsize overrides default behaviour for --atlas')
|
|
458
|
+
|
|
459
|
+
settings['current_limit'] = args.limit[0]
|
|
460
|
+
settings['settling_time'] = args.settling_time[0]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def handle_simple_arguments(settings, args):
|
|
464
|
+
"""
|
|
465
|
+
handle command line arguments that require a simple assignment
|
|
466
|
+
|
|
467
|
+
These assignments could be performed manually which would make the
|
|
468
|
+
code trivial to follow but rather long. The implementation here works well
|
|
469
|
+
as the number of (simple) command line arguments grows.
|
|
470
|
+
|
|
471
|
+
--------------------------------------------------------------------------
|
|
472
|
+
args
|
|
473
|
+
settings : dictionary
|
|
474
|
+
contains core information about the test environment
|
|
475
|
+
args : argparse.Namespace
|
|
476
|
+
command line arguments and values
|
|
477
|
+
--------------------------------------------------------------------------
|
|
478
|
+
returns
|
|
479
|
+
settings : no explicit return, mutable type amended in place
|
|
480
|
+
--------------------------------------------------------------------------
|
|
481
|
+
"""
|
|
482
|
+
# collate all possible command line arguments and their respective values
|
|
483
|
+
all_command_line_arguments = ((argument, getattr(args, argument))
|
|
484
|
+
for argument in dir(args))
|
|
485
|
+
|
|
486
|
+
# limit the selection to command line options that:
|
|
487
|
+
#
|
|
488
|
+
# (1) have values that can be stored without transformation
|
|
489
|
+
# (2) the user has actually specified
|
|
490
|
+
stored_as_boolean = frozenset(
|
|
491
|
+
('forwardbias', 'initial', 'json', 'omitreturn', 'reset', 'svg')
|
|
492
|
+
)
|
|
493
|
+
stored_as_list = frozenset(
|
|
494
|
+
('debug', 'hold', 'label', 'samples', 'sense', 'itk_serno')
|
|
495
|
+
)
|
|
496
|
+
simple_assignments = stored_as_boolean.union(stored_as_list)
|
|
497
|
+
shortlist = ((argument, value)
|
|
498
|
+
for argument, value in all_command_line_arguments
|
|
499
|
+
if argument in simple_assignments and value)
|
|
500
|
+
|
|
501
|
+
# assign given arguments and values to settings
|
|
502
|
+
for argument, value in shortlist:
|
|
503
|
+
try:
|
|
504
|
+
settings[argument] = value[0]
|
|
505
|
+
except TypeError:
|
|
506
|
+
# object is not subscriptable (does not belong to stored_as_list)
|
|
507
|
+
settings[argument] = value
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
##############################################################################
|
|
511
|
+
# logging debug messages
|
|
512
|
+
##############################################################################
|
|
513
|
+
|
|
514
|
+
def write_debug_information_to_log():
|
|
515
|
+
"""
|
|
516
|
+
Write basic information about the environment to the log file (but not to
|
|
517
|
+
the screen).
|
|
518
|
+
|
|
519
|
+
--------------------------------------------------------------------------
|
|
520
|
+
args : none
|
|
521
|
+
--------------------------------------------------------------------------
|
|
522
|
+
returns : none
|
|
523
|
+
--------------------------------------------------------------------------
|
|
524
|
+
"""
|
|
525
|
+
# Ensure that the command line invocation could be copied and pasted back
|
|
526
|
+
# into the terminal by wrapping strings with spaces with speech marks.
|
|
527
|
+
sav = (
|
|
528
|
+
f'"{a}"' if ' ' in a else a
|
|
529
|
+
for a in sys.argv
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
common.log_with_colour(logging.DEBUG, f'invocation: {" ".join(sav)}')
|
|
533
|
+
common.log_with_colour(logging.DEBUG, f'platform: {platform.platform()}')
|
|
534
|
+
messages = [
|
|
535
|
+
f'python: {platform.python_version()}',
|
|
536
|
+
f'pyserial: {serial.VERSION}',
|
|
537
|
+
f'matplotlib: {matplotlib.__version__}'
|
|
538
|
+
]
|
|
539
|
+
common.log_with_colour(logging.DEBUG, ', '.join(messages))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
##############################################################################
|
|
543
|
+
# utilities
|
|
544
|
+
##############################################################################
|
|
545
|
+
|
|
546
|
+
def scale_timestamps(timestamps):
|
|
547
|
+
"""
|
|
548
|
+
Amend timestamps so the start of the experiment is zero, and scale values
|
|
549
|
+
so they are sensible for human-readable plot axes.
|
|
550
|
+
|
|
551
|
+
--------------------------------------------------------------------------
|
|
552
|
+
args
|
|
553
|
+
timestamps : list of floats
|
|
554
|
+
values in seconds since the epoch
|
|
555
|
+
e.g.
|
|
556
|
+
[1627383466.9587026, 1627383472.445601, 1627383476.924957, ...]
|
|
557
|
+
--------------------------------------------------------------------------
|
|
558
|
+
returns
|
|
559
|
+
values : list of floats
|
|
560
|
+
e.g.
|
|
561
|
+
[0.0, 0.09144830703735352, 0.16610424121220907, ...]
|
|
562
|
+
units : string
|
|
563
|
+
'minutes' or 'hours'
|
|
564
|
+
--------------------------------------------------------------------------
|
|
565
|
+
"""
|
|
566
|
+
mintimestamp = min(timestamps)
|
|
567
|
+
minutes_of_available_data = (max(timestamps) - mintimestamp) / 60
|
|
568
|
+
|
|
569
|
+
if minutes_of_available_data > 180:
|
|
570
|
+
scale = 3600
|
|
571
|
+
units = 'hours'
|
|
572
|
+
else:
|
|
573
|
+
scale = 60
|
|
574
|
+
units = 'minutes'
|
|
575
|
+
|
|
576
|
+
values = [(t - mintimestamp) / scale for t in timestamps]
|
|
577
|
+
|
|
578
|
+
return values, units
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
##############################################################################
|
|
582
|
+
# set psu values
|
|
583
|
+
##############################################################################
|
|
584
|
+
|
|
585
|
+
def configure_psu(settings, pipeline, ser, dev):
|
|
586
|
+
"""
|
|
587
|
+
Set the initial conditions for the power supply.
|
|
588
|
+
|
|
589
|
+
Keithley: With the range being used, voltages can only be specified to two
|
|
590
|
+
decimal places, though setting digits in the second decimal place is
|
|
591
|
+
unreliable (read back values do not always match) so limit values to be
|
|
592
|
+
set to 1 decimal place.
|
|
593
|
+
|
|
594
|
+
--------------------------------------------------------------------------
|
|
595
|
+
args
|
|
596
|
+
settings : dictionary
|
|
597
|
+
contains core information about the test environment
|
|
598
|
+
pipeline : instance of class Production
|
|
599
|
+
contains all the queues through which the production pipeline
|
|
600
|
+
processes communicate
|
|
601
|
+
ser : serial.Serial
|
|
602
|
+
reference for serial port
|
|
603
|
+
dev : instance of class common.Channel()
|
|
604
|
+
contains details of a device and its serial port
|
|
605
|
+
--------------------------------------------------------------------------
|
|
606
|
+
returns : bool, float, float
|
|
607
|
+
success (True if the device was found, False otherwise)
|
|
608
|
+
measured_voltage
|
|
609
|
+
measured_current
|
|
610
|
+
--------------------------------------------------------------------------
|
|
611
|
+
"""
|
|
612
|
+
# constrain the voltage to be set to the given number of decimal places
|
|
613
|
+
voltage = common.decimal_quantize(0, settings['decimal_places'])
|
|
614
|
+
|
|
615
|
+
if settings['reset'] and dev.manufacturer == 'keithley':
|
|
616
|
+
# reset to default conditions, output off
|
|
617
|
+
command_string = lexicon.power(dev.model, 'reset')
|
|
618
|
+
common.send_command(pipeline, ser, dev, command_string)
|
|
619
|
+
|
|
620
|
+
# allow device time to complete reset before sending more commands
|
|
621
|
+
time.sleep(0.5)
|
|
622
|
+
|
|
623
|
+
# set voltage to zero and set compliance
|
|
624
|
+
if dev.manufacturer == 'keithley':
|
|
625
|
+
# limit the number of characters sent
|
|
626
|
+
# e.g. reduce 7.999999999999999e-05 to '8.00e-05'
|
|
627
|
+
compliance = f'{settings["current_limit"]:.2e}'
|
|
628
|
+
command_string = lexicon.power(dev.model, 'configure',
|
|
629
|
+
voltage, compliance,
|
|
630
|
+
channel=dev.channel)
|
|
631
|
+
common.send_command(pipeline, ser, dev, command_string)
|
|
632
|
+
|
|
633
|
+
elif dev.manufacturer == 'iseg':
|
|
634
|
+
command_string = lexicon.power(dev.model, 'set auto ramp',
|
|
635
|
+
channel=dev.channel)
|
|
636
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
637
|
+
|
|
638
|
+
command_string = lexicon.power(dev.model, 'set char delay')
|
|
639
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
640
|
+
|
|
641
|
+
volts_per_second = 20
|
|
642
|
+
command_string = lexicon.power(dev.model,
|
|
643
|
+
'set voltage max rate of change',
|
|
644
|
+
volts_per_second,
|
|
645
|
+
channel=dev.channel)
|
|
646
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
647
|
+
|
|
648
|
+
# change front/rear routing if requested, otherwise do not change it
|
|
649
|
+
if settings['rear'] is not None and dev.model == '2410':
|
|
650
|
+
destination = 'REAR' if settings['rear'] else 'FRON'
|
|
651
|
+
command_string = lexicon.power('2410', 'set route', destination)
|
|
652
|
+
common.send_command(pipeline, ser, dev, command_string)
|
|
653
|
+
|
|
654
|
+
# set output on (Keithley only, ISEG SHQ hardware only switch on front panel)
|
|
655
|
+
if dev.manufacturer == 'keithley':
|
|
656
|
+
command_string = lexicon.power(dev.model, 'output on', channel=dev.channel)
|
|
657
|
+
common.send_command(pipeline, ser, dev, command_string)
|
|
658
|
+
|
|
659
|
+
# allow a little time for the current to settle before reading
|
|
660
|
+
time.sleep(1)
|
|
661
|
+
|
|
662
|
+
return read_psu_and_verify(settings, pipeline, ser, voltage, dev)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def in_compliance(ser, pipeline, dev):
|
|
666
|
+
"""
|
|
667
|
+
Check if the power supply channel reports it is in compliance - i.e. the
|
|
668
|
+
set current limit has been exceeded - and it is now acting as a constant
|
|
669
|
+
current source instead of a constant voltage source.
|
|
670
|
+
|
|
671
|
+
Note that for the ISEG SHQ with the uA range selected, a current overflow
|
|
672
|
+
(OVERFLOW on display) transient is often seen when ramping down to a low
|
|
673
|
+
voltage, may need to check a second time to be sure.
|
|
674
|
+
|
|
675
|
+
--------------------------------------------------------------------------
|
|
676
|
+
args
|
|
677
|
+
ser : serial.Serial
|
|
678
|
+
reference for serial port
|
|
679
|
+
pipeline : instance of class Production
|
|
680
|
+
contains all the queues through which the production pipeline
|
|
681
|
+
processes communicate
|
|
682
|
+
dev : instance of class common.Channel()
|
|
683
|
+
contains details of a device and its serial port
|
|
684
|
+
--------------------------------------------------------------------------
|
|
685
|
+
returns
|
|
686
|
+
incomp : bool
|
|
687
|
+
True if the PSU is in compliance
|
|
688
|
+
--------------------------------------------------------------------------
|
|
689
|
+
"""
|
|
690
|
+
incomp = report_status = False
|
|
691
|
+
command_string = lexicon.power(dev.model, 'check compliance', channel=dev.channel)
|
|
692
|
+
response = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
693
|
+
|
|
694
|
+
if dev.model == '2410':
|
|
695
|
+
if response in {'0', '1'}:
|
|
696
|
+
incomp = response == '1'
|
|
697
|
+
else:
|
|
698
|
+
report_status = True
|
|
699
|
+
|
|
700
|
+
elif dev.model == '2614b':
|
|
701
|
+
if response in {'true', 'false'}:
|
|
702
|
+
incomp = response == 'true'
|
|
703
|
+
else:
|
|
704
|
+
report_status = True
|
|
705
|
+
|
|
706
|
+
elif dev.manufacturer == 'iseg':
|
|
707
|
+
if '=ERR' in response:
|
|
708
|
+
incomp = '=ERR' in response
|
|
709
|
+
# clear the error by repeating the reading (must read response)
|
|
710
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
711
|
+
|
|
712
|
+
if report_status:
|
|
713
|
+
message = f'{dev.ident}, problem reading compliance status'
|
|
714
|
+
common.log_with_colour(logging.WARNING, message)
|
|
715
|
+
|
|
716
|
+
return incomp
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def read_psu_set_voltage(pipeline, ser, dev):
|
|
720
|
+
"""
|
|
721
|
+
Read the voltage that the PSU has been asked to output (rather than the
|
|
722
|
+
actual instantaneous voltage measured at the output terminals).
|
|
723
|
+
|
|
724
|
+
--------------------------------------------------------------------------
|
|
725
|
+
args
|
|
726
|
+
pipeline : instance of class Production
|
|
727
|
+
contains all the queues through which the production pipeline
|
|
728
|
+
processes communicate
|
|
729
|
+
ser : serial.Serial
|
|
730
|
+
reference for serial port
|
|
731
|
+
dev : instance of class Channel()
|
|
732
|
+
contains details of a device and its serial port
|
|
733
|
+
--------------------------------------------------------------------------
|
|
734
|
+
returns
|
|
735
|
+
set_volt : float or None
|
|
736
|
+
float if the value could be read or None if it could not
|
|
737
|
+
--------------------------------------------------------------------------
|
|
738
|
+
"""
|
|
739
|
+
set_volt = None
|
|
740
|
+
|
|
741
|
+
command_string = lexicon.power(dev.model, 'read set voltage', channel=dev.channel)
|
|
742
|
+
local_buffer = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
743
|
+
|
|
744
|
+
if dev.manufacturer == 'iseg' and dev.model == 'shq':
|
|
745
|
+
set_volt = common.iseg_value_to_float(local_buffer)
|
|
746
|
+
|
|
747
|
+
elif dev.manufacturer == 'keithley' and dev.model in {'2410', '2614b'}:
|
|
748
|
+
if dev.model == '2410':
|
|
749
|
+
# e.g. '-5.000000E+00,-1.005998E-10,+9.910000E+37,+1.185742E+04,+2.150800E+04'
|
|
750
|
+
item = next(iter(local_buffer.split(',')))
|
|
751
|
+
else:
|
|
752
|
+
# e.g. '-5.00000e+00'
|
|
753
|
+
item = local_buffer
|
|
754
|
+
|
|
755
|
+
try:
|
|
756
|
+
set_volt = float(item)
|
|
757
|
+
except (TypeError, ValueError):
|
|
758
|
+
common.log_with_colour(logging.WARNING, f'{dev.ident}, problem reading set voltage')
|
|
759
|
+
|
|
760
|
+
return set_volt
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def read_psu_and_verify(settings, pipeline, ser, desired_voltage, dev):
|
|
764
|
+
"""
|
|
765
|
+
Verify that the set voltage read back from the psu matches the
|
|
766
|
+
desired value, then return the measured voltage and current.
|
|
767
|
+
|
|
768
|
+
--------------------------------------------------------------------------
|
|
769
|
+
args
|
|
770
|
+
settings : dictionary
|
|
771
|
+
contains core information about the test environment
|
|
772
|
+
pipeline : instance of class Production
|
|
773
|
+
contains all the queues through which the production pipeline
|
|
774
|
+
processes communicate
|
|
775
|
+
ser : serial.Serial
|
|
776
|
+
reference for serial port
|
|
777
|
+
desired_voltage : decimal.Decimal
|
|
778
|
+
voltage that was set
|
|
779
|
+
dev : instance of class common.Channel()
|
|
780
|
+
contains details of a device and its serial port
|
|
781
|
+
--------------------------------------------------------------------------
|
|
782
|
+
returns
|
|
783
|
+
success : bool
|
|
784
|
+
True if a matching voltage was read back
|
|
785
|
+
measured_voltage : float
|
|
786
|
+
measured_current : float
|
|
787
|
+
--------------------------------------------------------------------------
|
|
788
|
+
"""
|
|
789
|
+
set_volt = read_psu_set_voltage(pipeline, ser, dev)
|
|
790
|
+
if set_volt is not None:
|
|
791
|
+
set_voltage = common.decimal_quantize(set_volt, settings['decimal_places'])
|
|
792
|
+
|
|
793
|
+
if set_voltage == desired_voltage:
|
|
794
|
+
success = True
|
|
795
|
+
else:
|
|
796
|
+
success = False
|
|
797
|
+
common.log_with_colour(logging.WARNING,
|
|
798
|
+
'set voltage differs from voltage read back')
|
|
799
|
+
|
|
800
|
+
measured_voltage, measured_current = common.read_psu_measured_vi(pipeline, ser, dev)
|
|
801
|
+
else:
|
|
802
|
+
success = False
|
|
803
|
+
measured_voltage = measured_current = None
|
|
804
|
+
|
|
805
|
+
return success, measured_voltage, measured_current
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def current_limit(settings, pipeline, measured_current,
|
|
809
|
+
measured_voltage, set_voltage, ser, dev):
|
|
810
|
+
"""
|
|
811
|
+
Test whether the measured leakage current has exceeded one of two limits,
|
|
812
|
+
both related to the maximum leakage current value supplied as an argument
|
|
813
|
+
to command line option --limit <n>.
|
|
814
|
+
|
|
815
|
+
(1) Soft limit: has the measured leakage current exceeded a threshold
|
|
816
|
+
value, calculated as a percentage of <n>
|
|
817
|
+
|
|
818
|
+
(2) Hard limit: has the power supply channel - having had its current limit
|
|
819
|
+
set to <n> - reported its compliance bit has been set.
|
|
820
|
+
|
|
821
|
+
General notes on Keithley 2410 behaviour:
|
|
822
|
+
|
|
823
|
+
When the device reports compliance, current is clamped to a value below -
|
|
824
|
+
not at - the set limit, e.g. for this contrived scenario using a 2nA
|
|
825
|
+
limit:
|
|
826
|
+
|
|
827
|
+
INFO : 2410 4343654, IV, -10, V, -9.578194e+00, V, -1.954002e-09, A
|
|
828
|
+
WARNING : 2410 4343654 reports compliance
|
|
829
|
+
INFO : 2410 4343654, IV, -15, V, -1.058404e+01, V, -1.954872e-09, A
|
|
830
|
+
WARNING : 2410 4343654 reports compliance
|
|
831
|
+
WARNING : 2410 4343654 set and measured voltage differ
|
|
832
|
+
|
|
833
|
+
And sometimes the device does not report compliance, even when it is
|
|
834
|
+
clearly operating as a current source (10nA limit here):
|
|
835
|
+
|
|
836
|
+
INFO : 2410 4343654, IV, -15, V, -1.290832e+01, V, -9.956312e-09, A
|
|
837
|
+
WARNING : 2410 4343654 leakage current exceeded soft limit
|
|
838
|
+
WARNING : 2410 4343654 set and measured voltage differ
|
|
839
|
+
|
|
840
|
+
The hard limit test employs an additional check of whether the measured
|
|
841
|
+
voltage differs from the set voltage, an indication that the power supply
|
|
842
|
+
may have changed from being a constant voltage source to a constant
|
|
843
|
+
current source. This additional check is necessary since bit 14 of the
|
|
844
|
+
measurement event register - which indicates compliance - seems to be set
|
|
845
|
+
as the leakage current approaches the compliance limit, rather than
|
|
846
|
+
actually reaching it.
|
|
847
|
+
|
|
848
|
+
Using miniterm to monitor bit 14 with :STAT:MEAS?, and gradually
|
|
849
|
+
increasing the reverse bias voltage it can be seen that bit 14 is set
|
|
850
|
+
before the compliance limit is reached and before the "Cmpl" text flashes
|
|
851
|
+
on the front panel display.
|
|
852
|
+
|
|
853
|
+
--------------------------------------------------------------------------
|
|
854
|
+
args
|
|
855
|
+
settings : dictionary
|
|
856
|
+
contains core information about the test environment
|
|
857
|
+
pipeline : instance of class Production
|
|
858
|
+
contains all the queues through which the production pipeline
|
|
859
|
+
processes communicate
|
|
860
|
+
measured_current : float
|
|
861
|
+
measured_voltage : float
|
|
862
|
+
set_voltage : float
|
|
863
|
+
ser : serial.Serial
|
|
864
|
+
reference for serial port
|
|
865
|
+
dev : instance of class common.Channel()
|
|
866
|
+
contains details of a device and its serial port
|
|
867
|
+
--------------------------------------------------------------------------
|
|
868
|
+
returns : bool
|
|
869
|
+
True if either limit has been exceeded, False otherwise
|
|
870
|
+
--------------------------------------------------------------------------
|
|
871
|
+
"""
|
|
872
|
+
# the soft limit is set as a percentage of the value provided by command
|
|
873
|
+
# line option --limit (or default value)
|
|
874
|
+
soft_limit = abs(measured_current) >= abs(settings['current_limit'])
|
|
875
|
+
if soft_limit:
|
|
876
|
+
common.log_with_colour(logging.WARNING,
|
|
877
|
+
f'{dev.ident} leakage current exceeded soft limit')
|
|
878
|
+
|
|
879
|
+
# always run this test even if the soft limit has been exceeded, as it's
|
|
880
|
+
# valuable to have the power supply channel compliance status reported in
|
|
881
|
+
# the log for debugging purposes
|
|
882
|
+
hard_limit = in_compliance(ser, pipeline, dev)
|
|
883
|
+
if hard_limit:
|
|
884
|
+
common.log_with_colour(logging.WARNING, f'{dev.ident} reports compliance')
|
|
885
|
+
|
|
886
|
+
if dev.manufacturer != 'iseg':
|
|
887
|
+
# setting exact voltages for ISEG devices may be troublesome
|
|
888
|
+
|
|
889
|
+
asv = abs(set_voltage)
|
|
890
|
+
margin = 1 if asv > 10 else max(asv * 0.1, 0.1)
|
|
891
|
+
lower_bound = asv - margin
|
|
892
|
+
upper_bound = asv + margin
|
|
893
|
+
within_bounds = lower_bound < abs(measured_voltage) < upper_bound
|
|
894
|
+
|
|
895
|
+
if not within_bounds:
|
|
896
|
+
common.log_with_colour(logging.WARNING,
|
|
897
|
+
f'{dev.ident} set and measured voltage differ')
|
|
898
|
+
|
|
899
|
+
if dev.model == '2410':
|
|
900
|
+
# see the function docstring above for why this additional check is
|
|
901
|
+
# necessary
|
|
902
|
+
hard_limit = hard_limit and not within_bounds
|
|
903
|
+
|
|
904
|
+
return soft_limit or hard_limit
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
##############################################################################
|
|
908
|
+
# threads
|
|
909
|
+
##############################################################################
|
|
910
|
+
|
|
911
|
+
def check_key(graceful_quit):
|
|
912
|
+
"""
|
|
913
|
+
Identify whether the user has decided to stop testing early by pressing
|
|
914
|
+
'q' then 'enter' (both key presses required to avoid an accidental quit),
|
|
915
|
+
and set a flag in shared memory as True indicating this has occurred.
|
|
916
|
+
|
|
917
|
+
The status of this flag can be read from within the individual threads
|
|
918
|
+
operating the power supplies, which can then handle exiting gracefully.
|
|
919
|
+
|
|
920
|
+
--------------------------------------------------------------------------
|
|
921
|
+
args
|
|
922
|
+
graceful_quit : multiprocessing.Value(ctypes.c_bool, False)
|
|
923
|
+
shared memory containing a single boolean value to indicate
|
|
924
|
+
whether the user has requested the script terminate early
|
|
925
|
+
--------------------------------------------------------------------------
|
|
926
|
+
returns : no explicit return
|
|
927
|
+
graceful_quit : multiprocessing shared memory may be amended
|
|
928
|
+
--------------------------------------------------------------------------
|
|
929
|
+
"""
|
|
930
|
+
while True:
|
|
931
|
+
try:
|
|
932
|
+
line = sys.stdin.read(1).lower()
|
|
933
|
+
except AttributeError:
|
|
934
|
+
pass
|
|
935
|
+
else:
|
|
936
|
+
if line == 'q':
|
|
937
|
+
common.log_with_colour(logging.INFO, 'user requested graceful quit')
|
|
938
|
+
graceful_quit.value = True
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def liveplot(pipeline, zmq_skt):
|
|
942
|
+
"""
|
|
943
|
+
Receive data from all power supplies under test, and send the data to
|
|
944
|
+
an external plotter (liveplot.py).
|
|
945
|
+
|
|
946
|
+
To avoid dropped data packets ZeroMQ REQ/REP is the pattern used, though
|
|
947
|
+
the desired behaviour is really PUB/SUB. PUB/SUB is unreliable in its
|
|
948
|
+
basic form, and can't be used. With REQ/REP liveplot.py is obliged to
|
|
949
|
+
reply to the message this thread sends, but that reply can be discarded.
|
|
950
|
+
|
|
951
|
+
--------------------------------------------------------------------------
|
|
952
|
+
args
|
|
953
|
+
pipeline : instance of class Production
|
|
954
|
+
contains all the queues through which the production pipeline
|
|
955
|
+
processes communicate
|
|
956
|
+
zmq_skt : zmq.sugar.socket.Socket
|
|
957
|
+
ZeroMQ socket to communicate with an external live-plotting script
|
|
958
|
+
--------------------------------------------------------------------------
|
|
959
|
+
returns : none
|
|
960
|
+
--------------------------------------------------------------------------
|
|
961
|
+
"""
|
|
962
|
+
while True:
|
|
963
|
+
message_tx = pipeline.liveplot.get()
|
|
964
|
+
|
|
965
|
+
# sentinel value to quit
|
|
966
|
+
if message_tx is None:
|
|
967
|
+
break
|
|
968
|
+
|
|
969
|
+
# send message then attempt to receive reply
|
|
970
|
+
try:
|
|
971
|
+
zmq_skt.send_json(message_tx)
|
|
972
|
+
except zmq.error.ZMQError:
|
|
973
|
+
# liveplot.py is not running
|
|
974
|
+
pass
|
|
975
|
+
else:
|
|
976
|
+
# receive and discard reply from liveplot.py
|
|
977
|
+
with contextlib.suppress(zmq.error.ZMQError):
|
|
978
|
+
zmq_skt.recv_json()
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def sense(pipeline, zmq_skt, settings):
|
|
982
|
+
"""
|
|
983
|
+
Receive data packets from sense.py via ZeroMQ REQ/REP, and place them on a
|
|
984
|
+
queue for processing later.
|
|
985
|
+
|
|
986
|
+
If a zmq.error.ZMQError exception occurs while attempting to bind to a
|
|
987
|
+
socket, this is almost always caused by a previous invocation of this
|
|
988
|
+
script having been forcibly terminated. When the current invocation tries
|
|
989
|
+
to bind, this will fail.
|
|
990
|
+
|
|
991
|
+
In this case the user should find the PID owning the port currently, in
|
|
992
|
+
the case below this is 32539:
|
|
993
|
+
|
|
994
|
+
pi@raspberrypi:~/dev/1d-phantom/utilities $ sudo netstat -ltnp
|
|
995
|
+
Active Internet connections (only servers)
|
|
996
|
+
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
|
|
997
|
+
tcp 0 0 0.0.0.0:5556 0.0.0.0:* LISTEN 32539/python3
|
|
998
|
+
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 776/sshd
|
|
999
|
+
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 27597/cupsd
|
|
1000
|
+
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 992/exim4
|
|
1001
|
+
tcp6 0 0 :::22 :::* LISTEN 776/sshd
|
|
1002
|
+
tcp6 0 0 ::1:631 :::* LISTEN 27597/cupsd
|
|
1003
|
+
tcp6 0 0 ::1:25 :::* LISTEN 992/exim4
|
|
1004
|
+
|
|
1005
|
+
pi@raspberrypi:~/dev/1d-phantom/utilities $ ps -ef | grep 32539
|
|
1006
|
+
pi 1977 1624 0 17:33 pts/19 00:00:00 grep --color=auto 32539
|
|
1007
|
+
pi 32539 31217 0 16:07 pts/13 00:00:26 python3 ./iv.py -5 --sense hyt221_x
|
|
1008
|
+
pi 32546 32539 0 16:07 pts/13 00:00:00 python3 ./iv.py -5 --sense hyt221_x
|
|
1009
|
+
|
|
1010
|
+
And we can regain access by killing the PID:
|
|
1011
|
+
|
|
1012
|
+
kill -9 32539
|
|
1013
|
+
|
|
1014
|
+
--------------------------------------------------------------------------
|
|
1015
|
+
args
|
|
1016
|
+
pipeline : instance of class Production
|
|
1017
|
+
contains all the queues through which the production pipeline
|
|
1018
|
+
processes communicate
|
|
1019
|
+
zmq_skt : zmq.sugar.socket.Socket
|
|
1020
|
+
ZeroMQ socket to communicate with external environmental sensing
|
|
1021
|
+
script
|
|
1022
|
+
settings : dictionary
|
|
1023
|
+
contains core information about the test environment
|
|
1024
|
+
--------------------------------------------------------------------------
|
|
1025
|
+
returns : no explicit return
|
|
1026
|
+
pipeline.sense_dict : multiprocessing.Manager().dict()
|
|
1027
|
+
repository for the most recently acquired environmental data
|
|
1028
|
+
received from sense.py
|
|
1029
|
+
--------------------------------------------------------------------------
|
|
1030
|
+
"""
|
|
1031
|
+
# configure communication with sense.py
|
|
1032
|
+
# Use REP/REQ (with handshaking) instead of PUB/SUB to avoid packet loss
|
|
1033
|
+
port = 5556
|
|
1034
|
+
try:
|
|
1035
|
+
zmq_skt.bind(f'tcp://*:{port}')
|
|
1036
|
+
except zmq.error.ZMQError as zerr:
|
|
1037
|
+
message = f'ZeroMQ: {zerr} when binding to port {port}'
|
|
1038
|
+
common.log_with_colour(logging.WARNING, message)
|
|
1039
|
+
message = 'ZeroMQ: find PID of current owner with: sudo netstat -ltnp'
|
|
1040
|
+
common.log_with_colour(logging.WARNING, message)
|
|
1041
|
+
else:
|
|
1042
|
+
try:
|
|
1043
|
+
while True:
|
|
1044
|
+
# receive message from sense.py, this intentionally blocks hence
|
|
1045
|
+
# why this is a daemon thread
|
|
1046
|
+
message = zmq_skt.recv_json()
|
|
1047
|
+
|
|
1048
|
+
if message is not None and settings['sense'] is not None:
|
|
1049
|
+
valid = {k: v for k, v in message.items() if settings['sense'] in k}
|
|
1050
|
+
if valid:
|
|
1051
|
+
pipeline.sense_dict = valid
|
|
1052
|
+
|
|
1053
|
+
# send a minimal message back to sense.py for handshake
|
|
1054
|
+
zmq_skt.send_json(None)
|
|
1055
|
+
except zmq.error.ZMQError:
|
|
1056
|
+
zmq_skt.close()
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
##############################################################################
|
|
1060
|
+
# generation of fake data for --debug <n> command line option
|
|
1061
|
+
##############################################################################
|
|
1062
|
+
|
|
1063
|
+
def debug_generate_data(settings, ivtdat):
|
|
1064
|
+
"""
|
|
1065
|
+
--------------------------------------------------------------------------
|
|
1066
|
+
args
|
|
1067
|
+
settings : dictionary
|
|
1068
|
+
contains core information about the test environment
|
|
1069
|
+
ivtdat : instance of class Packet
|
|
1070
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1071
|
+
--------------------------------------------------------------------------
|
|
1072
|
+
returns
|
|
1073
|
+
ivtdat : no explicit return, mutable type amended in place
|
|
1074
|
+
--------------------------------------------------------------------------
|
|
1075
|
+
"""
|
|
1076
|
+
# outbound iv
|
|
1077
|
+
debug_iv_test(settings, ivtdat)
|
|
1078
|
+
|
|
1079
|
+
# hold stability
|
|
1080
|
+
debug_hold_stability(settings, ivtdat)
|
|
1081
|
+
|
|
1082
|
+
# return iv
|
|
1083
|
+
if not settings['omitreturn']:
|
|
1084
|
+
debug_iv_test(settings, ivtdat, reverse=True)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def debug_hold_stability(settings, ivtdat):
|
|
1088
|
+
"""
|
|
1089
|
+
Generate simulated hold stability data.
|
|
1090
|
+
|
|
1091
|
+
--------------------------------------------------------------------------
|
|
1092
|
+
args
|
|
1093
|
+
settings : dictionary
|
|
1094
|
+
contains core information about the test environment
|
|
1095
|
+
ivtdat : instance of class Packet
|
|
1096
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1097
|
+
--------------------------------------------------------------------------
|
|
1098
|
+
returns
|
|
1099
|
+
ivtdat : no explicit return, mutable type amended in place
|
|
1100
|
+
--------------------------------------------------------------------------
|
|
1101
|
+
"""
|
|
1102
|
+
hold = settings['hold']
|
|
1103
|
+
voltage = settings['voltage']
|
|
1104
|
+
|
|
1105
|
+
if hold is not None and hold >= 1:
|
|
1106
|
+
ivtdat.hold_voltage = voltage
|
|
1107
|
+
variation = random.uniform(1, 1.05)
|
|
1108
|
+
|
|
1109
|
+
for minute in range(hold):
|
|
1110
|
+
ivtdat.hold_current.append(noisy(diode(voltage, variation)))
|
|
1111
|
+
ivtdat.hold_timestamp.append(noisy(minute))
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def debug_iv_test(settings, ivtdat, reverse=False):
|
|
1115
|
+
"""
|
|
1116
|
+
Generate simulated IV test data with the voltage sequence in the order
|
|
1117
|
+
given.
|
|
1118
|
+
|
|
1119
|
+
--------------------------------------------------------------------------
|
|
1120
|
+
args
|
|
1121
|
+
settings : dictionary
|
|
1122
|
+
contains core information about the test environment
|
|
1123
|
+
ivtdat : instance of class Packet
|
|
1124
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1125
|
+
reverse : boolean
|
|
1126
|
+
reverse number sequence if True, False otherwise
|
|
1127
|
+
--------------------------------------------------------------------------
|
|
1128
|
+
returns
|
|
1129
|
+
ivtdat : no explicit return, mutable type amended in place
|
|
1130
|
+
--------------------------------------------------------------------------
|
|
1131
|
+
"""
|
|
1132
|
+
start = 0
|
|
1133
|
+
stop = settings['voltage']
|
|
1134
|
+
variation = random.uniform(1, 1.1)
|
|
1135
|
+
|
|
1136
|
+
if reverse:
|
|
1137
|
+
# this is a ramp-down IV
|
|
1138
|
+
# manually mark the point where the ramp-down data starts
|
|
1139
|
+
ivtdat.set_voltage.append('split')
|
|
1140
|
+
ivtdat.measured_current.append('split')
|
|
1141
|
+
ivtdat.measured_voltage.append('split')
|
|
1142
|
+
ivtdat.measured_timestamp.append('split')
|
|
1143
|
+
start, stop = stop, start
|
|
1144
|
+
|
|
1145
|
+
variablestep = settings['stepsize'] is None
|
|
1146
|
+
for voltage in sequence.test_run(start, stop, settings['stepsize'],
|
|
1147
|
+
variablestep=variablestep,
|
|
1148
|
+
bespoke_sequence_lut=settings['bespoke_sequence_lut']):
|
|
1149
|
+
vpsu = noisy(voltage)
|
|
1150
|
+
ipsu = diode(vpsu, variation)
|
|
1151
|
+
|
|
1152
|
+
ivtdat.set_voltage.append(voltage)
|
|
1153
|
+
ivtdat.measured_current.append(ipsu)
|
|
1154
|
+
ivtdat.measured_voltage.append(vpsu)
|
|
1155
|
+
ivtdat.measured_timestamp.append(time.time())
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def diode(voltage, variation):
|
|
1159
|
+
"""
|
|
1160
|
+
A very rough approximation of the leakage current of a reverse biased
|
|
1161
|
+
diode.
|
|
1162
|
+
|
|
1163
|
+
--------------------------------------------------------------------------
|
|
1164
|
+
args
|
|
1165
|
+
voltage : numeric
|
|
1166
|
+
variation : float
|
|
1167
|
+
this value, when held constant over a range of voltages, allows
|
|
1168
|
+
this function to be used to generate a series of subtly different
|
|
1169
|
+
curves, approximating the variation that might be expected in a
|
|
1170
|
+
test environment
|
|
1171
|
+
--------------------------------------------------------------------------
|
|
1172
|
+
returns : float
|
|
1173
|
+
leakage current
|
|
1174
|
+
--------------------------------------------------------------------------
|
|
1175
|
+
"""
|
|
1176
|
+
return pow(abs(float(voltage)), 0.25) * -0.5e-8 * variation
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def noisy(value):
|
|
1180
|
+
"""
|
|
1181
|
+
Add a small amount of noise to the supplied argument.
|
|
1182
|
+
|
|
1183
|
+
--------------------------------------------------------------------------
|
|
1184
|
+
args
|
|
1185
|
+
value : float
|
|
1186
|
+
--------------------------------------------------------------------------
|
|
1187
|
+
returns : float
|
|
1188
|
+
--------------------------------------------------------------------------
|
|
1189
|
+
"""
|
|
1190
|
+
adjust = value * 0.005
|
|
1191
|
+
return random.uniform(value - adjust, value + adjust)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
##############################################################################
|
|
1195
|
+
# temperature and humidity sensing
|
|
1196
|
+
##############################################################################
|
|
1197
|
+
|
|
1198
|
+
def add_environmental_string(text, sensor_readings, unit):
|
|
1199
|
+
"""
|
|
1200
|
+
Append temperature data to log string.
|
|
1201
|
+
|
|
1202
|
+
--------------------------------------------------------------------------
|
|
1203
|
+
args
|
|
1204
|
+
text : string
|
|
1205
|
+
e.g. '2614b 4428182 a, IV, -2, V, -1.99294e+00, V, 4.76837e-14, A'
|
|
1206
|
+
sensor_readings : dict
|
|
1207
|
+
e.g. {'PT100MK1-DC3D6': 22.31, 'PT100MK1-DC392': 19.16}
|
|
1208
|
+
unit : string
|
|
1209
|
+
'\u00b0C' or 'RH%' for temperature and humidity respectively
|
|
1210
|
+
--------------------------------------------------------------------------
|
|
1211
|
+
returns : string
|
|
1212
|
+
e.g.
|
|
1213
|
+
('2614b 4428182 a, IV, -2, V, -1.99294e+00, V, 4.76837e-14, A, '
|
|
1214
|
+
'PT100MK1-DC3D6, 22.31, °C, PT100MK1-DC392, 19.45, °C')
|
|
1215
|
+
--------------------------------------------------------------------------
|
|
1216
|
+
"""
|
|
1217
|
+
strings = (f'{name}, {reading:.2f}, {unit}'
|
|
1218
|
+
for name, reading in sensor_readings.items())
|
|
1219
|
+
|
|
1220
|
+
return ', '.join(itertools.chain([text], strings))
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def get_environmental_data(settings, pipeline):
|
|
1224
|
+
"""
|
|
1225
|
+
Get environmental data from the appropriate source.
|
|
1226
|
+
|
|
1227
|
+
--------------------------------------------------------------------------
|
|
1228
|
+
args
|
|
1229
|
+
settings : dictionary
|
|
1230
|
+
contains core information about the test environment
|
|
1231
|
+
pipeline : instance of class Production
|
|
1232
|
+
contains all the queues through which the production pipeline
|
|
1233
|
+
processes communicate
|
|
1234
|
+
--------------------------------------------------------------------------
|
|
1235
|
+
returns
|
|
1236
|
+
temp : dict
|
|
1237
|
+
humi : dict
|
|
1238
|
+
--------------------------------------------------------------------------
|
|
1239
|
+
"""
|
|
1240
|
+
if settings['sense'] is None:
|
|
1241
|
+
# read directly from Yoctopuce sensors
|
|
1242
|
+
temp = read_environment(settings['temperature_sensors'], pipeline)
|
|
1243
|
+
humi = read_environment(settings['humidity_sensors'], pipeline)
|
|
1244
|
+
elif settings['sense'] and pipeline.sense_dict:
|
|
1245
|
+
# receive data sent by sense.py using ZeroMQ
|
|
1246
|
+
temp = {
|
|
1247
|
+
k.split('_temperature')[0]: v
|
|
1248
|
+
for k, v in pipeline.sense_dict.items()
|
|
1249
|
+
if '_temperature' in k
|
|
1250
|
+
}
|
|
1251
|
+
humi = {
|
|
1252
|
+
k.split('_relative_humidity')[0]: v
|
|
1253
|
+
for k, v in pipeline.sense_dict.items()
|
|
1254
|
+
if '_relative_humidity' in k
|
|
1255
|
+
}
|
|
1256
|
+
else:
|
|
1257
|
+
temp = {}
|
|
1258
|
+
humi = {}
|
|
1259
|
+
|
|
1260
|
+
return temp, humi
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def register_hub(location):
|
|
1264
|
+
"""
|
|
1265
|
+
Initialise the Yoctopuce API to use sensor modules from the given
|
|
1266
|
+
location.
|
|
1267
|
+
|
|
1268
|
+
If argument location contains an IP address, the call to
|
|
1269
|
+
yapi.YAPI.RegisterHub will take 20 seconds to time-out if there is
|
|
1270
|
+
no response.
|
|
1271
|
+
|
|
1272
|
+
It cannot be inferred from a return value of yapi.YAPI.SUCCESS that
|
|
1273
|
+
sensor modules will be found in the given location.
|
|
1274
|
+
|
|
1275
|
+
--------------------------------------------------------------------------
|
|
1276
|
+
args
|
|
1277
|
+
location : string
|
|
1278
|
+
either 'usb' or an ip address e.g. '192.168.0.200'
|
|
1279
|
+
--------------------------------------------------------------------------
|
|
1280
|
+
returns
|
|
1281
|
+
registered : boolean
|
|
1282
|
+
True if the API initialisation was successful, False otherwise.
|
|
1283
|
+
--------------------------------------------------------------------------
|
|
1284
|
+
"""
|
|
1285
|
+
registered = False
|
|
1286
|
+
errmsg = yapi.YRefParam()
|
|
1287
|
+
|
|
1288
|
+
try:
|
|
1289
|
+
registered = yapi.YAPI.RegisterHub(location, errmsg) == yapi.YAPI.SUCCESS
|
|
1290
|
+
except ImportError:
|
|
1291
|
+
common.log_with_colour(logging.WARNING, 'yoctopuce: unable to import YAPI shared library')
|
|
1292
|
+
if sys.platform == 'darwin':
|
|
1293
|
+
message = ('allow libyapi.dylib in macOS System Preferences, '
|
|
1294
|
+
'Security and Privacy, General')
|
|
1295
|
+
common.log_with_colour(logging.WARNING, f'yoctopuce: {message}')
|
|
1296
|
+
|
|
1297
|
+
else:
|
|
1298
|
+
if not registered:
|
|
1299
|
+
common.log_with_colour(logging.WARNING, f'yoctopuce: {errmsg.value}')
|
|
1300
|
+
|
|
1301
|
+
return registered
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def yoctopuce_api_set(settings):
|
|
1305
|
+
"""
|
|
1306
|
+
Initialise the Yoctopuce API to use sensor modules attached to a
|
|
1307
|
+
YoctoHub-Ethernet if one is present. Otherwise, defer to USB connected
|
|
1308
|
+
sensor modules.
|
|
1309
|
+
|
|
1310
|
+
--------------------------------------------------------------------------
|
|
1311
|
+
args
|
|
1312
|
+
settings : dictionary
|
|
1313
|
+
contains core information about the test environment
|
|
1314
|
+
--------------------------------------------------------------------------
|
|
1315
|
+
returns
|
|
1316
|
+
registered : boolean
|
|
1317
|
+
True if the API initialisation was successful, False otherwise.
|
|
1318
|
+
--------------------------------------------------------------------------
|
|
1319
|
+
"""
|
|
1320
|
+
registered = False
|
|
1321
|
+
|
|
1322
|
+
# Preferentially configure the Yoctopuce API to access sensor modules via
|
|
1323
|
+
# a YoctoHub-Ethernet at the given IP address.
|
|
1324
|
+
#
|
|
1325
|
+
# The call to register_hub will take 10 seconds to time out if there is no
|
|
1326
|
+
# response from a YoctoHub-Ethernet at the given IP address, so check that
|
|
1327
|
+
# a basic connection attempt is successful before calling.
|
|
1328
|
+
#
|
|
1329
|
+
# The YoctoHub-Ethernet should respond to connection attempts on
|
|
1330
|
+
# ports 80 and 4444.
|
|
1331
|
+
ip_address = settings['temperature_ip']
|
|
1332
|
+
ports = {80, 4444}
|
|
1333
|
+
if any(port_responsive(ip_address, port) for port in ports):
|
|
1334
|
+
registered = register_hub(ip_address)
|
|
1335
|
+
|
|
1336
|
+
# fall back to allowing the API to access Yoctopuce modules connected to
|
|
1337
|
+
# local USB ports
|
|
1338
|
+
if not registered:
|
|
1339
|
+
registered = register_hub('usb')
|
|
1340
|
+
|
|
1341
|
+
return registered
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def detect_yoctopuce_sensors(settings):
|
|
1345
|
+
"""
|
|
1346
|
+
Detect environmental sensors applicable to the test environment.
|
|
1347
|
+
|
|
1348
|
+
PT100 sensors are used for temperature monitoring, and a Yocto-Meteo-V2
|
|
1349
|
+
is used for humidity monitoring. The Sensiron SHT35 based Yocto-Meteo-V2
|
|
1350
|
+
also contains pressure and temperature sensors which are ignored; pressure
|
|
1351
|
+
isn't important for this experiment, and its temperature sensor can't
|
|
1352
|
+
measure below -45°C which limits its utility when working with dry ice.
|
|
1353
|
+
|
|
1354
|
+
This is run before any IV testing threads are run, hence the API can be
|
|
1355
|
+
accessed without using the lock.
|
|
1356
|
+
|
|
1357
|
+
--------------------------------------------------------------------------
|
|
1358
|
+
args
|
|
1359
|
+
settings : dictionary
|
|
1360
|
+
contains core information about the test environment
|
|
1361
|
+
--------------------------------------------------------------------------
|
|
1362
|
+
returns
|
|
1363
|
+
settings : no explicit return, mutable type amended in place
|
|
1364
|
+
--------------------------------------------------------------------------
|
|
1365
|
+
"""
|
|
1366
|
+
if not yoctopuce_api_set(settings):
|
|
1367
|
+
return
|
|
1368
|
+
|
|
1369
|
+
# collect temperature sensors
|
|
1370
|
+
# ytemp.YTemperature.FirstTemperature() will find *all* modules
|
|
1371
|
+
# capable of reading temperature including dedicated temperature
|
|
1372
|
+
# modules (Yocto-PT100) as well as modules that acquire multiple
|
|
1373
|
+
# parameters (Yocto-Meteo-V2)
|
|
1374
|
+
sensors = {}
|
|
1375
|
+
sensor = ytemp.YTemperature.FirstTemperature()
|
|
1376
|
+
while sensor is not None:
|
|
1377
|
+
if sensor.isOnline():
|
|
1378
|
+
name = sensor.get_friendlyName().partition('.')[0]
|
|
1379
|
+
|
|
1380
|
+
sensors[name] = sensor
|
|
1381
|
+
common.log_with_colour(logging.INFO,
|
|
1382
|
+
f'yoctopuce: temperature sensor found: {name}')
|
|
1383
|
+
|
|
1384
|
+
sensor = sensor.nextTemperature()
|
|
1385
|
+
|
|
1386
|
+
# write found sensors to settings, display for user
|
|
1387
|
+
if sensors:
|
|
1388
|
+
settings['temperature_sensors'] = sensors
|
|
1389
|
+
else:
|
|
1390
|
+
common.log_with_colour(logging.WARNING,
|
|
1391
|
+
'yoctopuce: no temperature sensors found')
|
|
1392
|
+
|
|
1393
|
+
# collect humidity sensors
|
|
1394
|
+
sensors = {}
|
|
1395
|
+
sensor = yhumi.YHumidity.FirstHumidity()
|
|
1396
|
+
while sensor is not None:
|
|
1397
|
+
if sensor.isOnline():
|
|
1398
|
+
name = sensor.get_friendlyName().partition('.')[0]
|
|
1399
|
+
sensors[name] = sensor
|
|
1400
|
+
common.log_with_colour(logging.INFO,
|
|
1401
|
+
f'yoctopuce: humidity sensor found: {name}')
|
|
1402
|
+
sensor = sensor.nextHumidity()
|
|
1403
|
+
|
|
1404
|
+
# write found humidity sensors to settings, display for user
|
|
1405
|
+
if sensors:
|
|
1406
|
+
settings['humidity_sensors'] = sensors
|
|
1407
|
+
else:
|
|
1408
|
+
common.log_with_colour(logging.WARNING,
|
|
1409
|
+
'yoctopuce: no humidity sensors found')
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def port_responsive(host, port):
|
|
1413
|
+
"""
|
|
1414
|
+
Establish whether the port at the given ip address is responsive to a
|
|
1415
|
+
connection attempt. A timeout period of 2 seconds is used.
|
|
1416
|
+
|
|
1417
|
+
In essence, check if the node is present without having to handle the
|
|
1418
|
+
platform dependent nature of ping.
|
|
1419
|
+
--------------------------------------------------------------------------
|
|
1420
|
+
args
|
|
1421
|
+
host : string
|
|
1422
|
+
local IPV4 address e.g. '192.168.0.200'
|
|
1423
|
+
port : int
|
|
1424
|
+
port number, e.g. 22
|
|
1425
|
+
--------------------------------------------------------------------------
|
|
1426
|
+
returns : bool
|
|
1427
|
+
True if node is responsive, False otherwise
|
|
1428
|
+
--------------------------------------------------------------------------
|
|
1429
|
+
"""
|
|
1430
|
+
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
1431
|
+
skt.settimeout(2)
|
|
1432
|
+
|
|
1433
|
+
with contextlib.closing(skt) as sock:
|
|
1434
|
+
return sock.connect_ex((host, port)) == 0
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def read_environment(sensor_details, pipeline):
|
|
1438
|
+
"""
|
|
1439
|
+
Read environmental data (temperature or humidity) from given Yoctopuce
|
|
1440
|
+
sensors.
|
|
1441
|
+
|
|
1442
|
+
This function is called from within threads, and there is a risk that the
|
|
1443
|
+
Yoctopuce API could be accessed concurrently, hence the presence of the
|
|
1444
|
+
lock here.
|
|
1445
|
+
|
|
1446
|
+
--------------------------------------------------------------------------
|
|
1447
|
+
args
|
|
1448
|
+
sensor_details : dictionary
|
|
1449
|
+
{sensor_name: sensor_identifier, ...}
|
|
1450
|
+
pipeline : instance of class Production
|
|
1451
|
+
contains all the queues through which the production pipeline
|
|
1452
|
+
processes communicate
|
|
1453
|
+
--------------------------------------------------------------------------
|
|
1454
|
+
returns
|
|
1455
|
+
results : dict
|
|
1456
|
+
e.g. {'PT100MK1-DC3D6': 22.31, 'PT100MK1-DC392': 19.16}
|
|
1457
|
+
--------------------------------------------------------------------------
|
|
1458
|
+
"""
|
|
1459
|
+
results = {}
|
|
1460
|
+
|
|
1461
|
+
if sensor_details is not None:
|
|
1462
|
+
with pipeline.yapiaccess:
|
|
1463
|
+
results = {name: sensor.get_currentValue()
|
|
1464
|
+
for name, sensor in sensor_details.items()
|
|
1465
|
+
if sensor.isOnline()}
|
|
1466
|
+
|
|
1467
|
+
return results
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def report_environmental_issues(ivtdat, dev, test_iv, outbound=True):
|
|
1471
|
+
"""
|
|
1472
|
+
--------------------------------------------------------------------------
|
|
1473
|
+
args
|
|
1474
|
+
ivtdat : instance of class Packet
|
|
1475
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1476
|
+
dev : instance of class common.Channel()
|
|
1477
|
+
contains details of a device and its serial port
|
|
1478
|
+
test_iv : bool
|
|
1479
|
+
True = data from IV test, False = data from IT test
|
|
1480
|
+
outbound : bool
|
|
1481
|
+
For lists that may contain a 'split' marker, outbound = True
|
|
1482
|
+
selects items before the marker, False selects items after the
|
|
1483
|
+
marker. For other lists, this is ignored.
|
|
1484
|
+
--------------------------------------------------------------------------
|
|
1485
|
+
returns : none
|
|
1486
|
+
entries may be made in the log file
|
|
1487
|
+
--------------------------------------------------------------------------
|
|
1488
|
+
"""
|
|
1489
|
+
tolerance_symbol = chr(177)
|
|
1490
|
+
degree_symbol = chr(176)
|
|
1491
|
+
# allow up to +/- 5% around mean
|
|
1492
|
+
ten_percent = 0.05
|
|
1493
|
+
categories = {'temperature': f'{degree_symbol}C', 'humidity': 'RH%'}
|
|
1494
|
+
|
|
1495
|
+
for category, units in categories.items():
|
|
1496
|
+
# measurement is in the following generic form:
|
|
1497
|
+
# {'PT100MK1-DC3D6': [21.25, 21.25, 21.26, 21.26, ..., 21.24],
|
|
1498
|
+
# 'PT100MK1-DC392': [20.9, 20.92, 20.92, 20.94, ..., 20.94]}
|
|
1499
|
+
measurement = ivtdat.extract_environmental_data(category, test_iv, outbound)
|
|
1500
|
+
|
|
1501
|
+
for sensor, readings in measurement.items():
|
|
1502
|
+
try:
|
|
1503
|
+
mean = stat.mean(readings)
|
|
1504
|
+
except stat.StatisticsError:
|
|
1505
|
+
continue
|
|
1506
|
+
|
|
1507
|
+
outliers = (r for r in readings if not math.isclose(r, mean,
|
|
1508
|
+
rel_tol=ten_percent,
|
|
1509
|
+
abs_tol=0.5))
|
|
1510
|
+
if any(outliers):
|
|
1511
|
+
message = (f'{dev.ident}, {sensor} ({category} {units}), '
|
|
1512
|
+
f'{tolerance_symbol}5% tolerance exceeded')
|
|
1513
|
+
common.log_with_colour(logging.WARNING, message)
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
##############################################################################
|
|
1517
|
+
# run test profile
|
|
1518
|
+
##############################################################################
|
|
1519
|
+
|
|
1520
|
+
def get_iv_data_from_psu(dev, settings, pipeline, graceful_quit):
|
|
1521
|
+
"""
|
|
1522
|
+
Apply the given range of bias voltages with given step size and read back
|
|
1523
|
+
the respective leakage currents.
|
|
1524
|
+
|
|
1525
|
+
Exit early if leakage current exceeds software current limit.
|
|
1526
|
+
--------------------------------------------------------------------------
|
|
1527
|
+
args
|
|
1528
|
+
dev : instance of class common.Channel()
|
|
1529
|
+
contains details of a device and its serial port
|
|
1530
|
+
settings : dictionary
|
|
1531
|
+
contains core information about the test environment
|
|
1532
|
+
pipeline : instance of class Production
|
|
1533
|
+
contains all the queues through which the production pipeline
|
|
1534
|
+
processes communicate
|
|
1535
|
+
graceful_quit : multiprocessing.Value(ctypes.c_bool, False)
|
|
1536
|
+
shared memory containing a single boolean value to indicate
|
|
1537
|
+
whether the user has requested the script terminate early
|
|
1538
|
+
--------------------------------------------------------------------------
|
|
1539
|
+
returns
|
|
1540
|
+
ivtdat : instance of class Packet
|
|
1541
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1542
|
+
--------------------------------------------------------------------------
|
|
1543
|
+
"""
|
|
1544
|
+
ivtdat = common.Packet(
|
|
1545
|
+
dev.manufacturer,
|
|
1546
|
+
dev.model,
|
|
1547
|
+
dev.serial_number,
|
|
1548
|
+
dev.channel,
|
|
1549
|
+
dev.ident,
|
|
1550
|
+
settings['itk_serno'],
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
##########################################################################
|
|
1554
|
+
# DEBUG - ignore power supplies, generate data internally
|
|
1555
|
+
##########################################################################
|
|
1556
|
+
if settings['debug'] is not None:
|
|
1557
|
+
debug_generate_data(settings, ivtdat)
|
|
1558
|
+
return ivtdat
|
|
1559
|
+
##########################################################################
|
|
1560
|
+
|
|
1561
|
+
with serial.Serial(port=dev.port) as ser:
|
|
1562
|
+
ser.apply_settings(dev.config)
|
|
1563
|
+
|
|
1564
|
+
if dev.model == '2410':
|
|
1565
|
+
command_string = lexicon.power(dev.model, 'clear event registers')
|
|
1566
|
+
common.send_command(pipeline, ser, dev, command_string)
|
|
1567
|
+
|
|
1568
|
+
# obtain starting voltage
|
|
1569
|
+
initial_voltage = read_psu_set_voltage(pipeline, ser, dev)
|
|
1570
|
+
if initial_voltage is not None:
|
|
1571
|
+
common.log_with_colour(logging.INFO,
|
|
1572
|
+
(f'{dev.ident}, initial psu voltage is '
|
|
1573
|
+
f'{common.si_prefix(initial_voltage)}V'))
|
|
1574
|
+
|
|
1575
|
+
###################################################################
|
|
1576
|
+
# (1) gradually move from starting voltage to one step from zero
|
|
1577
|
+
target_voltage = 0
|
|
1578
|
+
start = True
|
|
1579
|
+
transition_voltage(settings, pipeline, initial_voltage,
|
|
1580
|
+
target_voltage, ser, dev, start)
|
|
1581
|
+
|
|
1582
|
+
# set to zero volts, configure range, set compliance etc...
|
|
1583
|
+
configure_psu(settings, pipeline, ser, dev)
|
|
1584
|
+
|
|
1585
|
+
###################################################################
|
|
1586
|
+
common.log_with_colour(logging.WARNING,
|
|
1587
|
+
(f'{dev.ident}, during IV test phase, '
|
|
1588
|
+
'press \'q\' then \'enter\' '
|
|
1589
|
+
'to exit the test early'))
|
|
1590
|
+
|
|
1591
|
+
###################################################################
|
|
1592
|
+
# (2) gather data for IV plot
|
|
1593
|
+
# (part 1) ramp-up IV test (typically 0V to -nV)
|
|
1594
|
+
proceed_with_testing, early_termination_voltage = \
|
|
1595
|
+
run_iv_test(settings, pipeline, ser, ivtdat, graceful_quit, dev)
|
|
1596
|
+
|
|
1597
|
+
if proceed_with_testing:
|
|
1598
|
+
# report environmental status only if variance is excessive
|
|
1599
|
+
report_environmental_issues(ivtdat, dev, test_iv=True, outbound=True)
|
|
1600
|
+
|
|
1601
|
+
# (part 2) hold for number of minutes specified
|
|
1602
|
+
hold_stability(settings, pipeline, graceful_quit, ser,
|
|
1603
|
+
read_psu_set_voltage(pipeline, ser, dev),
|
|
1604
|
+
ivtdat, dev)
|
|
1605
|
+
|
|
1606
|
+
# report environmental status only if variance is excessive
|
|
1607
|
+
report_environmental_issues(ivtdat, dev, test_iv=False)
|
|
1608
|
+
|
|
1609
|
+
if not settings['omitreturn']:
|
|
1610
|
+
# (part 3) ramp-down IV test (typically -nV to 0V)
|
|
1611
|
+
run_iv_test(settings, pipeline, ser, ivtdat, graceful_quit, dev,
|
|
1612
|
+
intercept=early_termination_voltage, reverse=True)
|
|
1613
|
+
|
|
1614
|
+
# report environmental status only if variance is excessive
|
|
1615
|
+
report_environmental_issues(ivtdat, dev, test_iv=True, outbound=False)
|
|
1616
|
+
|
|
1617
|
+
###################################################################
|
|
1618
|
+
# (3) bring the power supply's voltage back to its initial state
|
|
1619
|
+
# if it's safe to do so, otherwise move to 0V
|
|
1620
|
+
if proceed_with_testing and settings['initial']:
|
|
1621
|
+
target_voltage = initial_voltage
|
|
1622
|
+
log_message = 'returning to initial psu voltage'
|
|
1623
|
+
else:
|
|
1624
|
+
target_voltage = 0
|
|
1625
|
+
log_message = 'moving to 0V'
|
|
1626
|
+
|
|
1627
|
+
voltage_after_iv = read_psu_set_voltage(pipeline, ser, dev)
|
|
1628
|
+
if voltage_after_iv != initial_voltage:
|
|
1629
|
+
if voltage_after_iv is not None:
|
|
1630
|
+
common.log_with_colour(logging.INFO, f'{dev.ident}, {log_message}')
|
|
1631
|
+
start = False
|
|
1632
|
+
transition_voltage(settings, pipeline, voltage_after_iv,
|
|
1633
|
+
target_voltage, ser, dev, start)
|
|
1634
|
+
else:
|
|
1635
|
+
if target_voltage == 0:
|
|
1636
|
+
log_message = log_message.replace('moving', 'move')
|
|
1637
|
+
else:
|
|
1638
|
+
log_message = log_message.replace('returning', 'return')
|
|
1639
|
+
|
|
1640
|
+
common.log_with_colour(logging.ERROR, f'{dev.ident}, cannot {log_message}')
|
|
1641
|
+
else:
|
|
1642
|
+
common.log_with_colour(logging.WARNING,
|
|
1643
|
+
f'{dev.ident}, cannot be read from, check output')
|
|
1644
|
+
|
|
1645
|
+
return ivtdat
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
def hold_stability(settings, pipeline, graceful_quit, ser, voltage, ivtdat, dev):
|
|
1649
|
+
"""
|
|
1650
|
+
Perform an IT test for the given duration.
|
|
1651
|
+
|
|
1652
|
+
FIXME : this function needs to use time.monotonic() for sleep delays only
|
|
1653
|
+
|
|
1654
|
+
--------------------------------------------------------------------------
|
|
1655
|
+
args
|
|
1656
|
+
settings : dictionary
|
|
1657
|
+
contains core information about the test environment
|
|
1658
|
+
pipeline : instance of class Production
|
|
1659
|
+
contains all the queues through which the production pipeline
|
|
1660
|
+
processes communicate
|
|
1661
|
+
graceful_quit : multiprocessing.Value(ctypes.c_bool, False)
|
|
1662
|
+
shared memory containing a single boolean value to indicate
|
|
1663
|
+
whether the user has requested the script terminate early
|
|
1664
|
+
ser : serial.Serial
|
|
1665
|
+
reference for serial port
|
|
1666
|
+
voltage : float
|
|
1667
|
+
ivtdat : instance of class Packet
|
|
1668
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1669
|
+
dev : instance of class common.Channel()
|
|
1670
|
+
contains details of a device and its serial port
|
|
1671
|
+
--------------------------------------------------------------------------
|
|
1672
|
+
returns
|
|
1673
|
+
ivtdat : no explicit return, mutable type amended in place
|
|
1674
|
+
--------------------------------------------------------------------------
|
|
1675
|
+
"""
|
|
1676
|
+
hold_minutes = settings['hold']
|
|
1677
|
+
if hold_minutes is None:
|
|
1678
|
+
return
|
|
1679
|
+
|
|
1680
|
+
indefinite = hold_minutes == 0
|
|
1681
|
+
sample_period_seconds = 5
|
|
1682
|
+
|
|
1683
|
+
###########################################################################
|
|
1684
|
+
# display voltage hold (I-T) summary
|
|
1685
|
+
###########################################################################
|
|
1686
|
+
|
|
1687
|
+
hold_sta_raw = datetime.datetime.now(datetime.UTC)
|
|
1688
|
+
hold_end_raw = hold_sta_raw + datetime.timedelta(minutes=hold_minutes)
|
|
1689
|
+
hold_end = hold_end_raw.isoformat().split('.')[0].replace('T', ' ')
|
|
1690
|
+
|
|
1691
|
+
if indefinite:
|
|
1692
|
+
tdesc = 'indefinitely'
|
|
1693
|
+
else:
|
|
1694
|
+
suffix = '' if hold_minutes == 1 else 's'
|
|
1695
|
+
tdesc = f'for {hold_minutes} minute{suffix} until {hold_end} UTC'
|
|
1696
|
+
|
|
1697
|
+
common.log_with_colour(
|
|
1698
|
+
logging.INFO,
|
|
1699
|
+
f'{dev.ident}, IT, holding at {voltage:.0f}V {tdesc}'
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
###########################################################################
|
|
1703
|
+
|
|
1704
|
+
ivtdat.hold_voltage = voltage
|
|
1705
|
+
timestamp_mono = time.monotonic()
|
|
1706
|
+
end_time_mono = timestamp_mono + hold_minutes * 60
|
|
1707
|
+
|
|
1708
|
+
while timestamp_mono < end_time_mono or indefinite:
|
|
1709
|
+
if graceful_quit.value:
|
|
1710
|
+
break
|
|
1711
|
+
|
|
1712
|
+
_, ipsu = common.read_psu_measured_vi(pipeline, ser, dev)
|
|
1713
|
+
|
|
1714
|
+
# add environmental data if available
|
|
1715
|
+
temp, humi = get_environmental_data(settings, pipeline)
|
|
1716
|
+
|
|
1717
|
+
ivtdat.hold_current.append(ipsu)
|
|
1718
|
+
ivtdat.hold_timestamp.append(time.time())
|
|
1719
|
+
ivtdat.hold_temperature.append(temp)
|
|
1720
|
+
ivtdat.hold_humidity.append(humi)
|
|
1721
|
+
|
|
1722
|
+
text = f'{dev.ident}, IT, {ipsu:>13.6e}, A'
|
|
1723
|
+
text = add_environmental_string(text, temp, '\u00b0C')
|
|
1724
|
+
common.log_with_colour(
|
|
1725
|
+
logging.INFO,
|
|
1726
|
+
add_environmental_string(text, humi, 'RH%')
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
timestamp_mono = common.rate_limit(
|
|
1730
|
+
timestamp_mono, sample_period_seconds
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def rate_limit_delay(settings, previous_voltage, next_voltage):
|
|
1735
|
+
"""
|
|
1736
|
+
Supply a delay sufficient for the rate of change of voltage not to exceed
|
|
1737
|
+
10V/s in standard configuration, or 2V/s using the ATLAS workaround.
|
|
1738
|
+
|
|
1739
|
+
In principle, this function allows the delay between small voltage steps
|
|
1740
|
+
(1V, 5V) to be shorter, reducing the time to perform the whole test, which
|
|
1741
|
+
has value when using iv.py and a supporting script to cycle the
|
|
1742
|
+
bias voltage between 0V and -1100V several hundred times.
|
|
1743
|
+
|
|
1744
|
+
However, since this script may be used with different power supplies with
|
|
1745
|
+
varying performance - to be cautious - a minimum settling time is enforced.
|
|
1746
|
+
If HV-cycling becomes common usage, it may be beneficial to write minimum
|
|
1747
|
+
settling times for each power supply type (derived from empirical testing)
|
|
1748
|
+
in the cache file written by detect.py, to take advantage of time savings.
|
|
1749
|
+
|
|
1750
|
+
Note that this is NOT the settling time between setting the voltage and
|
|
1751
|
+
reading back V/I. That is handled in common.set_psu_voltage_and_read().
|
|
1752
|
+
|
|
1753
|
+
--------------------------------------------------------------------------
|
|
1754
|
+
args
|
|
1755
|
+
settings : dictionary
|
|
1756
|
+
contains core information about the test environment
|
|
1757
|
+
previous_voltage : numeric
|
|
1758
|
+
next_voltage : numeric
|
|
1759
|
+
--------------------------------------------------------------------------
|
|
1760
|
+
returns
|
|
1761
|
+
duration : numeric
|
|
1762
|
+
value in seconds
|
|
1763
|
+
--------------------------------------------------------------------------
|
|
1764
|
+
"""
|
|
1765
|
+
if settings['atlas']:
|
|
1766
|
+
duration = 5
|
|
1767
|
+
else:
|
|
1768
|
+
max_voltage_step = 10
|
|
1769
|
+
|
|
1770
|
+
try:
|
|
1771
|
+
delta_v = abs(next_voltage - previous_voltage)
|
|
1772
|
+
except TypeError:
|
|
1773
|
+
# previous_voltage is None
|
|
1774
|
+
delta_v = max_voltage_step
|
|
1775
|
+
|
|
1776
|
+
duration = delta_v / max_voltage_step
|
|
1777
|
+
|
|
1778
|
+
return duration
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def run_iv_test(settings, pipeline, ser, ivtdat, graceful_quit, dev,
|
|
1782
|
+
intercept=None, reverse=False):
|
|
1783
|
+
"""
|
|
1784
|
+
Run IV test with the voltage sequence in the order given.
|
|
1785
|
+
|
|
1786
|
+
Notes on function time delays:
|
|
1787
|
+
|
|
1788
|
+
(1) Before the measurement: settling time. The time between setting the
|
|
1789
|
+
voltage and subsequently measuring the voltage and current.
|
|
1790
|
+
|
|
1791
|
+
(2) After the measurement: rate limit. An additional delay that serves to
|
|
1792
|
+
limit the average rate of change in volts per second.
|
|
1793
|
+
|
|
1794
|
+
--------------------------------------------------------------------------
|
|
1795
|
+
args
|
|
1796
|
+
settings : dictionary
|
|
1797
|
+
contains core information about the test environment
|
|
1798
|
+
pipeline : instance of class Production
|
|
1799
|
+
contains all the queues through which the production pipeline
|
|
1800
|
+
processes communicate
|
|
1801
|
+
ser : serial.Serial
|
|
1802
|
+
reference for serial port
|
|
1803
|
+
ivtdat : instance of class Packet
|
|
1804
|
+
contains data for a given power supply channel's IV and IT curves
|
|
1805
|
+
graceful_quit : multiprocessing.Value(ctypes.c_bool, False)
|
|
1806
|
+
shared memory containing a single boolean value to indicate
|
|
1807
|
+
whether the user has requested the script terminate early
|
|
1808
|
+
dev : instance of class common.Channel()
|
|
1809
|
+
contains details of a device and its serial port
|
|
1810
|
+
intercept : int or None
|
|
1811
|
+
if this function is being called a second time after the previous
|
|
1812
|
+
run terminated early because of a software current over-limit
|
|
1813
|
+
event, the voltage the previous run terminated at can be
|
|
1814
|
+
supplied here to avoid subjecting the chip to a potentially
|
|
1815
|
+
damaging large step voltage change
|
|
1816
|
+
reverse : boolean
|
|
1817
|
+
reverse number sequence if True, False otherwise
|
|
1818
|
+
--------------------------------------------------------------------------
|
|
1819
|
+
returns
|
|
1820
|
+
proceed_with_testing : boolean
|
|
1821
|
+
True when at least one voltage did not trigger a software current
|
|
1822
|
+
limit event, False otherwise
|
|
1823
|
+
last_successful_voltage : int or None
|
|
1824
|
+
None if test terminates normally, or int if not
|
|
1825
|
+
ivtdat : no explicit return, mutable type amended in place
|
|
1826
|
+
--------------------------------------------------------------------------
|
|
1827
|
+
"""
|
|
1828
|
+
prv = pri = None
|
|
1829
|
+
buf = collections.deque([], 3)
|
|
1830
|
+
|
|
1831
|
+
start = 0
|
|
1832
|
+
stop = settings['voltage'] if intercept is None else intercept
|
|
1833
|
+
if reverse:
|
|
1834
|
+
# this is a ramp-down IV, manually mark the point where the ramp-down
|
|
1835
|
+
# data starts
|
|
1836
|
+
ivtdat.set_voltage.append('split')
|
|
1837
|
+
ivtdat.measured_current.append('split')
|
|
1838
|
+
ivtdat.sigma_current.append('split')
|
|
1839
|
+
ivtdat.measured_voltage.append('split')
|
|
1840
|
+
ivtdat.measured_timestamp.append('split')
|
|
1841
|
+
ivtdat.measured_temperature.append('split')
|
|
1842
|
+
ivtdat.measured_humidity.append('split')
|
|
1843
|
+
start, stop = stop, start
|
|
1844
|
+
suffix = '(return)'
|
|
1845
|
+
else:
|
|
1846
|
+
suffix = '(outbound)'
|
|
1847
|
+
|
|
1848
|
+
common.log_with_colour(logging.INFO, f'{dev.ident}, IV, running test {suffix}')
|
|
1849
|
+
|
|
1850
|
+
early_termination = False
|
|
1851
|
+
last_successful_voltage = None
|
|
1852
|
+
padding = len(str(settings['voltage']))
|
|
1853
|
+
variablestep = settings['stepsize'] is None
|
|
1854
|
+
for voltage in sequence.test_run(start, stop, settings['stepsize'],
|
|
1855
|
+
variablestep=variablestep,
|
|
1856
|
+
bespoke_sequence_lut=settings['bespoke_sequence_lut']):
|
|
1857
|
+
if graceful_quit.value:
|
|
1858
|
+
break
|
|
1859
|
+
|
|
1860
|
+
# set parameters for voltage/time rate limiting
|
|
1861
|
+
time_0 = time.monotonic()
|
|
1862
|
+
duration = rate_limit_delay(settings, last_successful_voltage, voltage)
|
|
1863
|
+
|
|
1864
|
+
# obtain readings from power supply
|
|
1865
|
+
# set voltage -> settling time -> read back values
|
|
1866
|
+
vpsu, ipsu = common.set_psu_voltage_and_read(
|
|
1867
|
+
settings, pipeline, voltage, ser, dev, settings['settling_time']
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
if settings['settle']:
|
|
1871
|
+
vpsu, ipsu, sigma_current = settle(settings, pipeline, ser, vpsu, ipsu, dev)
|
|
1872
|
+
else:
|
|
1873
|
+
sigma_current = 0.0
|
|
1874
|
+
|
|
1875
|
+
if vpsu is None or ipsu is None:
|
|
1876
|
+
graceful_quit.value = True
|
|
1877
|
+
break
|
|
1878
|
+
|
|
1879
|
+
# send data packet to liveplot.py
|
|
1880
|
+
pipeline.liveplot.put([dev.ident, voltage, ipsu, reverse])
|
|
1881
|
+
|
|
1882
|
+
# display summary of power supply readings
|
|
1883
|
+
text = (f'{dev.ident}, IV, '
|
|
1884
|
+
f'{voltage:>{padding}d}, V, '
|
|
1885
|
+
f'{vpsu:>13.6e}' + ', V, '
|
|
1886
|
+
f'{ipsu:>13.6e}' + ', A')
|
|
1887
|
+
|
|
1888
|
+
# add environmental data if available
|
|
1889
|
+
temp, humi = get_environmental_data(settings, pipeline)
|
|
1890
|
+
|
|
1891
|
+
text = add_environmental_string(text, temp, '\u00b0C')
|
|
1892
|
+
common.log_with_colour(logging.INFO,
|
|
1893
|
+
add_environmental_string(text, humi, 'RH%'))
|
|
1894
|
+
|
|
1895
|
+
# current limit protection (absolute threshold)
|
|
1896
|
+
over_threshold = current_limit(settings, pipeline,
|
|
1897
|
+
ipsu, vpsu, voltage, ser, dev)
|
|
1898
|
+
|
|
1899
|
+
# rate of change of leakage current (breakdown detection)
|
|
1900
|
+
# only check when voltage is moving away from 0V
|
|
1901
|
+
if not reverse and not over_threshold:
|
|
1902
|
+
if prv is not None:
|
|
1903
|
+
try:
|
|
1904
|
+
gradient = abs((pri - ipsu) / (prv - vpsu))
|
|
1905
|
+
except ZeroDivisionError:
|
|
1906
|
+
gradient = 0
|
|
1907
|
+
common.log_with_colour(logging.INFO,
|
|
1908
|
+
(f'{dev.ident}, IV, '
|
|
1909
|
+
f'previous ({prv:.6e}V) and prevailing ({vpsu:.6e}V) '
|
|
1910
|
+
'voltages are identical'))
|
|
1911
|
+
|
|
1912
|
+
if len(buf) == buf.maxlen and gradient > stat.mean(buf) * 20 and abs(ipsu) > 2e-09:
|
|
1913
|
+
common.log_with_colour(logging.INFO,
|
|
1914
|
+
(f'{dev.ident}, IV, '
|
|
1915
|
+
'leakage current excessive rate of change'))
|
|
1916
|
+
buf.append(gradient)
|
|
1917
|
+
prv, pri = vpsu, ipsu
|
|
1918
|
+
|
|
1919
|
+
# store data in all cases
|
|
1920
|
+
ivtdat.set_voltage.append(voltage)
|
|
1921
|
+
ivtdat.measured_current.append(ipsu)
|
|
1922
|
+
ivtdat.sigma_current.append(sigma_current)
|
|
1923
|
+
ivtdat.measured_voltage.append(vpsu)
|
|
1924
|
+
ivtdat.measured_timestamp.append(time.time())
|
|
1925
|
+
ivtdat.measured_temperature.append(temp)
|
|
1926
|
+
ivtdat.measured_humidity.append(humi)
|
|
1927
|
+
|
|
1928
|
+
if not over_threshold and voltage != 0:
|
|
1929
|
+
# mark this as a safe voltage to quickly return to,
|
|
1930
|
+
# in case there is a problem at the next voltage
|
|
1931
|
+
last_successful_voltage = voltage
|
|
1932
|
+
|
|
1933
|
+
if not reverse and over_threshold:
|
|
1934
|
+
# one or more limits have been exceeded on the outbound IV
|
|
1935
|
+
# return to last safe voltage level and exit from this test early
|
|
1936
|
+
#
|
|
1937
|
+
# do not force exit for return IV, since the voltage is reducing
|
|
1938
|
+
# anyway
|
|
1939
|
+
early_termination = True
|
|
1940
|
+
if last_successful_voltage is not None:
|
|
1941
|
+
common.log_with_colour(logging.INFO,
|
|
1942
|
+
(f'{dev.ident}, IV, '
|
|
1943
|
+
'returning to last safe voltage '
|
|
1944
|
+
f'({last_successful_voltage:d}V)'))
|
|
1945
|
+
common.set_psu_voltage(settings, pipeline, last_successful_voltage,
|
|
1946
|
+
ser, dev)
|
|
1947
|
+
break
|
|
1948
|
+
|
|
1949
|
+
# voltage/second rate limit
|
|
1950
|
+
tdif = time.monotonic() - time_0
|
|
1951
|
+
if tdif < duration:
|
|
1952
|
+
time.sleep(duration - tdif)
|
|
1953
|
+
|
|
1954
|
+
if (early_termination and last_successful_voltage is None) or graceful_quit.value:
|
|
1955
|
+
common.log_with_colour(logging.INFO, f'{dev.ident}, IV, halting further testing')
|
|
1956
|
+
proceed_with_testing = False
|
|
1957
|
+
else:
|
|
1958
|
+
proceed_with_testing = True
|
|
1959
|
+
|
|
1960
|
+
return proceed_with_testing, last_successful_voltage
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def settle(settings, pipeline, ser, initial_voltage, initial_current, dev):
|
|
1964
|
+
"""
|
|
1965
|
+
This function averages successive readings to smooth noisy IV curves.
|
|
1966
|
+
|
|
1967
|
+
When used with large sample counts, it can also be used to obtain more
|
|
1968
|
+
reliable results from devices that require long settling times, where
|
|
1969
|
+
current values read shortly after a change of voltage are not likely to be
|
|
1970
|
+
representative of stable operating conditions.
|
|
1971
|
+
|
|
1972
|
+
Method:
|
|
1973
|
+
|
|
1974
|
+
Measured current and voltage values are read from the PSU in quick
|
|
1975
|
+
succession and loaded into a fixed-size N-element LIFO queue. Once the
|
|
1976
|
+
queue is full, a test is made to see if all contained values are within a
|
|
1977
|
+
given distance from their mean value. If the test passes, the function
|
|
1978
|
+
terminates early.
|
|
1979
|
+
|
|
1980
|
+
If the test fails, another current/voltage pair is loaded (discarding the
|
|
1981
|
+
oldest entry in the process) and the test is repeated. This process is
|
|
1982
|
+
repeated a user-specified number of times.
|
|
1983
|
+
|
|
1984
|
+
In both cases, the mean values of the voltages and currents contained in
|
|
1985
|
+
the queues are returned.
|
|
1986
|
+
|
|
1987
|
+
The function is tolerant to problems reading values from the PSU, though
|
|
1988
|
+
any such occurrence should be rare.
|
|
1989
|
+
|
|
1990
|
+
--------------------------------------------------------------------------
|
|
1991
|
+
args
|
|
1992
|
+
settings : dictionary
|
|
1993
|
+
contains core information about the test environment
|
|
1994
|
+
pipeline : instance of class Production
|
|
1995
|
+
contains all the queues through which the production pipeline
|
|
1996
|
+
processes communicate
|
|
1997
|
+
ser : serial.Serial
|
|
1998
|
+
reference for serial port
|
|
1999
|
+
initial_voltage : float
|
|
2000
|
+
initial_current : float
|
|
2001
|
+
dev : instance of class common.Channel()
|
|
2002
|
+
contains details of a device and its serial port
|
|
2003
|
+
--------------------------------------------------------------------------
|
|
2004
|
+
returns
|
|
2005
|
+
final_value : tuple
|
|
2006
|
+
(True, float, float) if satisfactory result found otherwise
|
|
2007
|
+
(False, None, None)
|
|
2008
|
+
--------------------------------------------------------------------------
|
|
2009
|
+
"""
|
|
2010
|
+
# no need to clear these deques since stale values will be pushed out
|
|
2011
|
+
# before any calculations are performed
|
|
2012
|
+
dev.measured_voltages.append(initial_voltage)
|
|
2013
|
+
dev.measured_currents.append(initial_current)
|
|
2014
|
+
|
|
2015
|
+
rel_tol = settings['pc']
|
|
2016
|
+
max_samples = settings['samples']
|
|
2017
|
+
max_attempts = max_samples * 2
|
|
2018
|
+
samples_received = 1
|
|
2019
|
+
attempts = 1
|
|
2020
|
+
|
|
2021
|
+
while samples_received < max_samples and attempts < max_attempts:
|
|
2022
|
+
attempts += 1
|
|
2023
|
+
|
|
2024
|
+
volt, curr = common.read_psu_measured_vi(pipeline, ser, dev)
|
|
2025
|
+
if volt is None:
|
|
2026
|
+
continue
|
|
2027
|
+
|
|
2028
|
+
samples_received += 1
|
|
2029
|
+
dev.measured_voltages.append(volt)
|
|
2030
|
+
dev.measured_currents.append(curr)
|
|
2031
|
+
|
|
2032
|
+
if samples_received >= dev.window_size:
|
|
2033
|
+
meai = stat.mean(dev.measured_currents)
|
|
2034
|
+
similar = (math.isclose(i, meai, rel_tol=rel_tol) for i in dev.measured_currents)
|
|
2035
|
+
if all(similar):
|
|
2036
|
+
break
|
|
2037
|
+
|
|
2038
|
+
if samples_received >= dev.window_size:
|
|
2039
|
+
final_value = stat.mean(dev.measured_voltages), stat.mean(dev.measured_currents), stat.stdev(dev.measured_currents)
|
|
2040
|
+
else:
|
|
2041
|
+
final_value = None, None, None
|
|
2042
|
+
common.log_with_colour(logging.WARNING, f'{dev.ident}, underflow')
|
|
2043
|
+
|
|
2044
|
+
return final_value
|
|
2045
|
+
|
|
2046
|
+
|
|
2047
|
+
def transition_voltage(settings, pipeline, initial_voltage,
|
|
2048
|
+
target_voltage, ser, dev, start):
|
|
2049
|
+
"""
|
|
2050
|
+
Handle voltage transitions (1) before commencement of IV testing, and (2)
|
|
2051
|
+
after.
|
|
2052
|
+
|
|
2053
|
+
--------------------------------------------------------------------------
|
|
2054
|
+
args
|
|
2055
|
+
settings : dictionary
|
|
2056
|
+
contains core information about the test environment
|
|
2057
|
+
pipeline : instance of class Production
|
|
2058
|
+
contains all the queues through which the production pipeline
|
|
2059
|
+
processes communicate
|
|
2060
|
+
initial_voltage : float
|
|
2061
|
+
set voltage at present
|
|
2062
|
+
target_voltage : float
|
|
2063
|
+
voltage to transition to
|
|
2064
|
+
ser : serial.Serial
|
|
2065
|
+
reference for serial port
|
|
2066
|
+
dev : instance of class common.Channel()
|
|
2067
|
+
contains details of a device and its serial port
|
|
2068
|
+
start : bool
|
|
2069
|
+
True if this is the initial voltage to 0V transition,
|
|
2070
|
+
or the end-of-test 0V to initial voltage transition
|
|
2071
|
+
--------------------------------------------------------------------------
|
|
2072
|
+
returns : none
|
|
2073
|
+
--------------------------------------------------------------------------
|
|
2074
|
+
"""
|
|
2075
|
+
# don't perform transition if voltages are closer than 1mV
|
|
2076
|
+
if math.isclose(initial_voltage, target_voltage, abs_tol=0.001):
|
|
2077
|
+
return
|
|
2078
|
+
|
|
2079
|
+
message = (f'{dev.ident}, transitioning '
|
|
2080
|
+
f'from {common.si_prefix(initial_voltage)}V '
|
|
2081
|
+
f'to {common.si_prefix(target_voltage)}V')
|
|
2082
|
+
common.log_with_colour(logging.INFO, message)
|
|
2083
|
+
|
|
2084
|
+
if dev.manufacturer == 'keithley':
|
|
2085
|
+
number_sequence = sequence.to_start if start else sequence.to_original
|
|
2086
|
+
timestamp = None
|
|
2087
|
+
duration = 5 if settings['atlas'] else 1
|
|
2088
|
+
step = 10
|
|
2089
|
+
|
|
2090
|
+
for voltage in number_sequence(initial_voltage, target_voltage, step):
|
|
2091
|
+
timestamp = common.rate_limit(timestamp, duration)
|
|
2092
|
+
common.set_psu_voltage(settings, pipeline, voltage, ser, dev)
|
|
2093
|
+
|
|
2094
|
+
if start:
|
|
2095
|
+
time.sleep(duration)
|
|
2096
|
+
|
|
2097
|
+
elif dev.manufacturer == 'iseg':
|
|
2098
|
+
# use power supply's internal ramp feature to avoid having manage
|
|
2099
|
+
# the ISEG SHQ's tardy response time
|
|
2100
|
+
|
|
2101
|
+
# read voltage rate of change
|
|
2102
|
+
command_string = lexicon.power(dev.model,
|
|
2103
|
+
'read max rate of change',
|
|
2104
|
+
channel=dev.channel)
|
|
2105
|
+
max_vroc = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
2106
|
+
|
|
2107
|
+
# set voltage rate of change for ramp
|
|
2108
|
+
volts_per_second = 2 if settings['atlas'] else 10
|
|
2109
|
+
command_string = lexicon.power(dev.model,
|
|
2110
|
+
'set voltage max rate of change',
|
|
2111
|
+
volts_per_second,
|
|
2112
|
+
channel=dev.channel)
|
|
2113
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
2114
|
+
|
|
2115
|
+
# auto ramp will progress towards the given voltage at the given rate
|
|
2116
|
+
command_string = lexicon.power(dev.model,
|
|
2117
|
+
'set voltage',
|
|
2118
|
+
target_voltage,
|
|
2119
|
+
channel=dev.channel)
|
|
2120
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
2121
|
+
|
|
2122
|
+
# wait to reach given voltage
|
|
2123
|
+
common.wait_for_voltage_to_stabilise(ser, pipeline, dev, target_voltage)
|
|
2124
|
+
|
|
2125
|
+
# revert voltage rate of change to original value
|
|
2126
|
+
command_string = lexicon.power(dev.model,
|
|
2127
|
+
'set voltage max rate of change',
|
|
2128
|
+
max_vroc,
|
|
2129
|
+
channel=dev.channel)
|
|
2130
|
+
common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
|
|
2131
|
+
|
|
2132
|
+
common.log_with_colour(logging.INFO, f'{dev.ident}, transition complete')
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
##############################################################################
|
|
2136
|
+
# save files
|
|
2137
|
+
##############################################################################
|
|
2138
|
+
|
|
2139
|
+
def save_data(data, filename):
|
|
2140
|
+
"""
|
|
2141
|
+
Write recorded data to mass storage.
|
|
2142
|
+
|
|
2143
|
+
--------------------------------------------------------------------------
|
|
2144
|
+
args
|
|
2145
|
+
data : instance of common.Consignment()
|
|
2146
|
+
contains data for the whole data acquisition session
|
|
2147
|
+
filename : string
|
|
2148
|
+
filename without extension
|
|
2149
|
+
--------------------------------------------------------------------------
|
|
2150
|
+
returns : none
|
|
2151
|
+
--------------------------------------------------------------------------
|
|
2152
|
+
"""
|
|
2153
|
+
logging.info('Saving data')
|
|
2154
|
+
|
|
2155
|
+
# compressed python pickle format, for post-processing
|
|
2156
|
+
common.data_write(data, f'{filename}.pbz2')
|
|
2157
|
+
|
|
2158
|
+
# generic csv
|
|
2159
|
+
with open(f'{filename}.csv', 'w', encoding='utf-8') as csvfile:
|
|
2160
|
+
row = csv.writer(csvfile)
|
|
2161
|
+
common.write_consignment_csv(data, row)
|
|
2162
|
+
|
|
2163
|
+
|
|
2164
|
+
##############################################################################
|
|
2165
|
+
# plot (power supply data)
|
|
2166
|
+
##############################################################################
|
|
2167
|
+
|
|
2168
|
+
def create_plots(settings, data, filename, session):
|
|
2169
|
+
"""
|
|
2170
|
+
Create and write plots to mass storage.
|
|
2171
|
+
|
|
2172
|
+
--------------------------------------------------------------------------
|
|
2173
|
+
args
|
|
2174
|
+
settings : dictionary
|
|
2175
|
+
contains core information about the test environment
|
|
2176
|
+
data : instance of common.Consignment()
|
|
2177
|
+
contains data for the whole data acquisition session
|
|
2178
|
+
filename : string
|
|
2179
|
+
filename without extension
|
|
2180
|
+
session : string
|
|
2181
|
+
date and time string, e.g. '20210719_143439'
|
|
2182
|
+
--------------------------------------------------------------------------
|
|
2183
|
+
returns : none
|
|
2184
|
+
--------------------------------------------------------------------------
|
|
2185
|
+
"""
|
|
2186
|
+
logging.info('Creating plots')
|
|
2187
|
+
|
|
2188
|
+
# create directory for plots
|
|
2189
|
+
directory = f'{filename}_plots'
|
|
2190
|
+
if not os.path.exists(directory):
|
|
2191
|
+
os.makedirs(directory)
|
|
2192
|
+
|
|
2193
|
+
# create list of colours from a ten-entry colour map
|
|
2194
|
+
#
|
|
2195
|
+
# the blue colour at index 0 is used for the temperature y-axis in function
|
|
2196
|
+
# save_individual_stability_plots(), so omit this initial blue from the
|
|
2197
|
+
# palette used for plotting other curves
|
|
2198
|
+
#
|
|
2199
|
+
# see: https://matplotlib.org/3.1.1/gallery/color/colormap_reference.html
|
|
2200
|
+
colors = [plt.cm.tab10(x) for x in range(1, 10)]
|
|
2201
|
+
|
|
2202
|
+
# set matplotlib defaults
|
|
2203
|
+
matplotlib.rcParams.update({
|
|
2204
|
+
# remove chartjunk
|
|
2205
|
+
'axes.spines.top': False,
|
|
2206
|
+
'axes.spines.right': False,
|
|
2207
|
+
# fontsize of the x and y labels
|
|
2208
|
+
'axes.labelsize': 'medium',
|
|
2209
|
+
# fontsize of the axes title
|
|
2210
|
+
'axes.titlesize': 'medium',
|
|
2211
|
+
# fontsize of the tick labels
|
|
2212
|
+
'xtick.labelsize': 'small',
|
|
2213
|
+
'ytick.labelsize': 'small',
|
|
2214
|
+
# fontsize of plot-line labels
|
|
2215
|
+
'legend.fontsize': 'xx-small'})
|
|
2216
|
+
|
|
2217
|
+
# plot all IV curves
|
|
2218
|
+
save_combined_iv_plot(settings, colors, data, directory, session)
|
|
2219
|
+
save_individual_iv_plots(settings, colors, data, directory, session)
|
|
2220
|
+
|
|
2221
|
+
# plot all leakage current stability curves
|
|
2222
|
+
save_combined_stability_plot(settings, colors, data, directory, session)
|
|
2223
|
+
save_individual_stability_plots(settings, colors, data, directory, session)
|
|
2224
|
+
|
|
2225
|
+
# plot environmental data for iv outbound, it, and iv return as appropriate
|
|
2226
|
+
save_environmental_plots(settings, data, directory, session)
|
|
2227
|
+
|
|
2228
|
+
|
|
2229
|
+
def save_combined_iv_plot(settings, colors, data, directory, session):
|
|
2230
|
+
"""
|
|
2231
|
+
Create a single plot containing all the IV curves from all power supply
|
|
2232
|
+
channels.
|
|
2233
|
+
|
|
2234
|
+
--------------------------------------------------------------------------
|
|
2235
|
+
args
|
|
2236
|
+
settings : dictionary
|
|
2237
|
+
contains core information about the test environment
|
|
2238
|
+
colors : list
|
|
2239
|
+
matplotlib colour map
|
|
2240
|
+
data : instance of common.Consignment()
|
|
2241
|
+
contains data for the whole data acquisition session
|
|
2242
|
+
directory : string
|
|
2243
|
+
directory to write plot to
|
|
2244
|
+
session : string
|
|
2245
|
+
date and time string, e.g. '20210719_143439'
|
|
2246
|
+
--------------------------------------------------------------------------
|
|
2247
|
+
returns : none
|
|
2248
|
+
--------------------------------------------------------------------------
|
|
2249
|
+
"""
|
|
2250
|
+
if len(data.packets) < 2:
|
|
2251
|
+
return
|
|
2252
|
+
|
|
2253
|
+
fig, axis = plt.subplots(1, 1)
|
|
2254
|
+
|
|
2255
|
+
for packet, color in zip(data.packets, itertools.cycle(colors)):
|
|
2256
|
+
outbound_v, return_v = common.list_split(packet.set_voltage)
|
|
2257
|
+
outbound_i, return_i = common.list_split(packet.measured_current)
|
|
2258
|
+
lt_out = '.-' if len(outbound_v) <= 30 else '-'
|
|
2259
|
+
|
|
2260
|
+
if return_v or return_i:
|
|
2261
|
+
# outbound and return curves
|
|
2262
|
+
lt_ret = '.--' if len(return_v) <= 30 else '--'
|
|
2263
|
+
axis.plot(outbound_v, outbound_i, lt_out,
|
|
2264
|
+
linewidth=0.5, markersize=1, color=color,
|
|
2265
|
+
label=f'{packet.ident} outbound')
|
|
2266
|
+
axis.plot(return_v, return_i, lt_ret,
|
|
2267
|
+
linewidth=0.5, markersize=1, color=color,
|
|
2268
|
+
label=f'{packet.ident} return')
|
|
2269
|
+
else:
|
|
2270
|
+
# outbound curve only
|
|
2271
|
+
axis.plot(outbound_v, outbound_i, '-',
|
|
2272
|
+
linewidth=0.5, markersize=1, color=color,
|
|
2273
|
+
label=packet.ident)
|
|
2274
|
+
|
|
2275
|
+
if len(data.packets) <= 12:
|
|
2276
|
+
axis.legend()
|
|
2277
|
+
|
|
2278
|
+
if not data.forwardbias:
|
|
2279
|
+
axis.invert_xaxis()
|
|
2280
|
+
axis.invert_yaxis()
|
|
2281
|
+
|
|
2282
|
+
axis.set_xlabel('bias voltage (V)')
|
|
2283
|
+
axis.set_ylabel('leakage current (A)')
|
|
2284
|
+
|
|
2285
|
+
title_items = [
|
|
2286
|
+
'IV',
|
|
2287
|
+
data.label,
|
|
2288
|
+
', '.join(x for x in [pathlib.Path(session).name, settings['itk_serno']] if x),
|
|
2289
|
+
]
|
|
2290
|
+
plot_title = '\n'.join(x for x in title_items if x)
|
|
2291
|
+
axis.set_title(plot_title)
|
|
2292
|
+
|
|
2293
|
+
axis.yaxis.set_major_formatter(EngFormatter(places=1))
|
|
2294
|
+
axis.xaxis.set_major_formatter(EngFormatter(places=0))
|
|
2295
|
+
|
|
2296
|
+
plt.tight_layout()
|
|
2297
|
+
common.save_plot(settings, os.path.join(directory, 'iv_all'))
|
|
2298
|
+
|
|
2299
|
+
plt.close(fig)
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
def save_individual_iv_plots(settings, colors, data, directory, session):
|
|
2303
|
+
"""
|
|
2304
|
+
Create a separate plot for each individual power supply channel.
|
|
2305
|
+
each plot contains outbound and return IV curves.
|
|
2306
|
+
|
|
2307
|
+
--------------------------------------------------------------------------
|
|
2308
|
+
args
|
|
2309
|
+
settings : dictionary
|
|
2310
|
+
contains core information about the test environment
|
|
2311
|
+
colors : list
|
|
2312
|
+
matplotlib colour map
|
|
2313
|
+
data : instance of common.Consignment()
|
|
2314
|
+
contains data for the whole data acquisition session
|
|
2315
|
+
directory : string
|
|
2316
|
+
directory to write plot to
|
|
2317
|
+
session : string
|
|
2318
|
+
date and time string, e.g. '20210719_143439'
|
|
2319
|
+
--------------------------------------------------------------------------
|
|
2320
|
+
returns : none
|
|
2321
|
+
--------------------------------------------------------------------------
|
|
2322
|
+
"""
|
|
2323
|
+
for packet, color in zip(data.packets, itertools.cycle(colors)):
|
|
2324
|
+
fig, axis = plt.subplots(1, 1)
|
|
2325
|
+
|
|
2326
|
+
outbound_v, return_v = common.list_split(packet.set_voltage)
|
|
2327
|
+
outbound_i, return_i = common.list_split(packet.measured_current)
|
|
2328
|
+
lt_out = '.-' if len(outbound_v) <= 30 else '-'
|
|
2329
|
+
|
|
2330
|
+
if return_v or return_i:
|
|
2331
|
+
# outbound and return curves
|
|
2332
|
+
lt_ret = '.--' if len(return_v) <= 30 else '--'
|
|
2333
|
+
axis.plot(outbound_v, outbound_i, lt_out,
|
|
2334
|
+
linewidth=0.5, markersize=1, color=color,
|
|
2335
|
+
label='outbound')
|
|
2336
|
+
axis.plot(return_v, return_i, lt_ret,
|
|
2337
|
+
linewidth=0.5, markersize=1, color=color,
|
|
2338
|
+
label='return')
|
|
2339
|
+
else:
|
|
2340
|
+
# outbound curve only
|
|
2341
|
+
axis.plot(outbound_v, outbound_i, lt_out,
|
|
2342
|
+
linewidth=0.5, markersize=1, color=color)
|
|
2343
|
+
|
|
2344
|
+
if len(return_v) > 1:
|
|
2345
|
+
axis.legend()
|
|
2346
|
+
|
|
2347
|
+
if not data.forwardbias:
|
|
2348
|
+
axis.invert_xaxis()
|
|
2349
|
+
axis.invert_yaxis()
|
|
2350
|
+
|
|
2351
|
+
axis.set_xlabel('bias voltage (V)')
|
|
2352
|
+
axis.set_ylabel('leakage current (A)')
|
|
2353
|
+
|
|
2354
|
+
title_items = [
|
|
2355
|
+
f'IV, {packet.ident}',
|
|
2356
|
+
data.label,
|
|
2357
|
+
', '.join(x for x in [pathlib.Path(session).name, settings['itk_serno']] if x),
|
|
2358
|
+
]
|
|
2359
|
+
plot_title = '\n'.join(x for x in title_items if x)
|
|
2360
|
+
axis.set_title(plot_title)
|
|
2361
|
+
|
|
2362
|
+
axis.yaxis.set_major_formatter(EngFormatter(places=1))
|
|
2363
|
+
axis.xaxis.set_major_formatter(EngFormatter(places=0))
|
|
2364
|
+
|
|
2365
|
+
plt.tight_layout()
|
|
2366
|
+
|
|
2367
|
+
# replace spaces so files are easier to work with on the command line
|
|
2368
|
+
safe = packet.ident.replace(' ', '_')
|
|
2369
|
+
common.save_plot(settings, os.path.join(directory, f'iv_{safe}'))
|
|
2370
|
+
|
|
2371
|
+
plt.close(fig)
|
|
2372
|
+
|
|
2373
|
+
|
|
2374
|
+
def save_combined_stability_plot(settings, colors, data, directory, session):
|
|
2375
|
+
"""
|
|
2376
|
+
Create a single plot containing all the IT curves from all power supply
|
|
2377
|
+
channels.
|
|
2378
|
+
|
|
2379
|
+
--------------------------------------------------------------------------
|
|
2380
|
+
args
|
|
2381
|
+
settings : dictionary
|
|
2382
|
+
contains core information about the test environment
|
|
2383
|
+
colors : list
|
|
2384
|
+
matplotlib colour map
|
|
2385
|
+
data : instance of common.Consignment()
|
|
2386
|
+
contains data for the whole data acquisition session
|
|
2387
|
+
directory : string
|
|
2388
|
+
directory to write plot to
|
|
2389
|
+
session : string
|
|
2390
|
+
date and time string, e.g. '20210719_143439'
|
|
2391
|
+
--------------------------------------------------------------------------
|
|
2392
|
+
returns : none
|
|
2393
|
+
--------------------------------------------------------------------------
|
|
2394
|
+
"""
|
|
2395
|
+
test_not_requested = data.hold is None
|
|
2396
|
+
insufficient_packets = len(data.packets) < 2
|
|
2397
|
+
some_data_missing = not all(packet.hold_voltage for packet in data.packets)
|
|
2398
|
+
if test_not_requested or insufficient_packets or some_data_missing:
|
|
2399
|
+
return
|
|
2400
|
+
|
|
2401
|
+
fig, axis = plt.subplots(1, 1)
|
|
2402
|
+
|
|
2403
|
+
for packet, color in zip(data.packets, itertools.cycle(colors)):
|
|
2404
|
+
initial_timestamp = min(packet.hold_timestamp)
|
|
2405
|
+
itt = [(x - initial_timestamp) / 60 for x in packet.hold_timestamp]
|
|
2406
|
+
linetype = '.-' if len(packet.hold_timestamp) <= 30 else '-'
|
|
2407
|
+
axis.plot(itt, packet.hold_current, linetype,
|
|
2408
|
+
label=packet.ident, linewidth=0.5, markersize=1, color=color)
|
|
2409
|
+
|
|
2410
|
+
axis.yaxis.set_major_formatter(EngFormatter(places=3))
|
|
2411
|
+
axis.invert_yaxis()
|
|
2412
|
+
axis.set_ylabel('leakage current (A)')
|
|
2413
|
+
axis.set_xlabel('time (minutes)')
|
|
2414
|
+
axis.legend()
|
|
2415
|
+
|
|
2416
|
+
# title of plot
|
|
2417
|
+
title_items = [
|
|
2418
|
+
f'IT, {data.packets[0].hold_voltage:.0f}V',
|
|
2419
|
+
data.label,
|
|
2420
|
+
', '.join(x for x in [pathlib.Path(session).name, settings['itk_serno']] if x),
|
|
2421
|
+
]
|
|
2422
|
+
plot_title = '\n'.join(x for x in title_items if x)
|
|
2423
|
+
axis.set_title(plot_title)
|
|
2424
|
+
|
|
2425
|
+
plt.tight_layout()
|
|
2426
|
+
common.save_plot(settings, os.path.join(directory, 'it_all'))
|
|
2427
|
+
|
|
2428
|
+
plt.close(fig)
|
|
2429
|
+
|
|
2430
|
+
|
|
2431
|
+
def save_individual_stability_plots(settings, colors, data, directory, session):
|
|
2432
|
+
"""
|
|
2433
|
+
Create a separate plot for each individual power supply channel.
|
|
2434
|
+
each plot contains outbound and return IT curves.
|
|
2435
|
+
|
|
2436
|
+
--------------------------------------------------------------------------
|
|
2437
|
+
args
|
|
2438
|
+
settings : dictionary
|
|
2439
|
+
contains core information about the test environment
|
|
2440
|
+
colors : list
|
|
2441
|
+
matplotlib colour map
|
|
2442
|
+
data : instance of common.Consignment()
|
|
2443
|
+
contains data for the whole data acquisition session
|
|
2444
|
+
directory : string
|
|
2445
|
+
directory to write plot to
|
|
2446
|
+
session : string
|
|
2447
|
+
date and time string, e.g. '20210719_143439'
|
|
2448
|
+
--------------------------------------------------------------------------
|
|
2449
|
+
returns : none
|
|
2450
|
+
--------------------------------------------------------------------------
|
|
2451
|
+
"""
|
|
2452
|
+
if data.hold is None or not all(packet.hold_voltage for packet in data.packets):
|
|
2453
|
+
return
|
|
2454
|
+
|
|
2455
|
+
for packet, color in zip(data.packets, itertools.cycle(colors)):
|
|
2456
|
+
fig, axis = plt.subplots(1, 1)
|
|
2457
|
+
|
|
2458
|
+
initial_timestamp = min(packet.hold_timestamp)
|
|
2459
|
+
itt = [(x - initial_timestamp) / 60 for x in packet.hold_timestamp]
|
|
2460
|
+
linetype = '.-' if len(packet.hold_timestamp) <= 30 else '-'
|
|
2461
|
+
axis.plot(itt, packet.hold_current,
|
|
2462
|
+
linetype, linewidth=0.5, markersize=1, color=color)
|
|
2463
|
+
|
|
2464
|
+
axis.yaxis.set_major_formatter(EngFormatter(places=3))
|
|
2465
|
+
axis.invert_yaxis()
|
|
2466
|
+
axis.set_ylabel('leakage current (A)')
|
|
2467
|
+
axis.set_xlabel('time (minutes)')
|
|
2468
|
+
|
|
2469
|
+
# title of plot
|
|
2470
|
+
title_items = [
|
|
2471
|
+
f'IT, {packet.hold_voltage:.0f}V, {packet.ident}',
|
|
2472
|
+
data.label,
|
|
2473
|
+
', '.join(x for x in [pathlib.Path(session).name, settings['itk_serno']] if x),
|
|
2474
|
+
]
|
|
2475
|
+
plot_title = '\n'.join(x for x in title_items if x)
|
|
2476
|
+
axis.set_title(plot_title)
|
|
2477
|
+
|
|
2478
|
+
plt.tight_layout()
|
|
2479
|
+
|
|
2480
|
+
# replace spaces so files are easier to work with on the command line
|
|
2481
|
+
safe = packet.ident.replace(' ', '_')
|
|
2482
|
+
common.save_plot(settings, os.path.join(directory, f'it_{safe}'))
|
|
2483
|
+
|
|
2484
|
+
plt.close(fig)
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
##############################################################################
|
|
2488
|
+
# plot (environmental data)
|
|
2489
|
+
##############################################################################
|
|
2490
|
+
|
|
2491
|
+
def save_environmental_plots(settings, data, directory, session):
|
|
2492
|
+
"""
|
|
2493
|
+
Plot environmental data for each power supply channel on the same plot for
|
|
2494
|
+
iv outbound, it, and iv return.
|
|
2495
|
+
|
|
2496
|
+
--------------------------------------------------------------------------
|
|
2497
|
+
args
|
|
2498
|
+
settings : dictionary
|
|
2499
|
+
contains core information about the test environment
|
|
2500
|
+
data : instance of common.Consignment()
|
|
2501
|
+
contains data for the whole data acquisition session
|
|
2502
|
+
directory : string
|
|
2503
|
+
directory to write plot to
|
|
2504
|
+
session : string
|
|
2505
|
+
date and time string, e.g. '20210719_143439'
|
|
2506
|
+
--------------------------------------------------------------------------
|
|
2507
|
+
returns : none
|
|
2508
|
+
--------------------------------------------------------------------------
|
|
2509
|
+
"""
|
|
2510
|
+
# three plots: iv outbound, it, iv return
|
|
2511
|
+
options = [
|
|
2512
|
+
(True, True, 'IV outbound'),
|
|
2513
|
+
(False, True, 'IT'),
|
|
2514
|
+
(True, False, 'IV return')]
|
|
2515
|
+
|
|
2516
|
+
# data.packets may contain multiple PSU channels
|
|
2517
|
+
for packet in data.packets:
|
|
2518
|
+
for test_iv, outbound, title in options:
|
|
2519
|
+
|
|
2520
|
+
# Extract data for this individual plot. it_y1 and it_y1 are dicts
|
|
2521
|
+
# and may contain data for multiple sensors
|
|
2522
|
+
it_x = packet.extract_timestamp_data(test_iv, outbound)
|
|
2523
|
+
it_y1 = packet.extract_environmental_data('temperature', test_iv, outbound)
|
|
2524
|
+
it_y2 = packet.extract_environmental_data('humidity', test_iv, outbound)
|
|
2525
|
+
|
|
2526
|
+
# assemble axes to plot
|
|
2527
|
+
y_axes = {'temperature (°C)': it_y1, 'humidity (RH%)': it_y2}
|
|
2528
|
+
y_axes_to_plot = {k: v for k, v in y_axes.items() if v}
|
|
2529
|
+
if len(y_axes_to_plot) > 1:
|
|
2530
|
+
# set matplotlib defaults
|
|
2531
|
+
matplotlib.rcParams.update({'axes.spines.right': True})
|
|
2532
|
+
|
|
2533
|
+
# omit plots with incomplete data
|
|
2534
|
+
no_x_axis = not it_x
|
|
2535
|
+
no_y_axis = not y_axes_to_plot
|
|
2536
|
+
if no_x_axis or no_y_axis:
|
|
2537
|
+
continue
|
|
2538
|
+
|
|
2539
|
+
# adjust timestamps so zero is the start of the experiment, and
|
|
2540
|
+
# they are scaled for readability.
|
|
2541
|
+
it_x, units = scale_timestamps(it_x)
|
|
2542
|
+
|
|
2543
|
+
# plot
|
|
2544
|
+
fig, host = plt.subplots(figsize=(8, 4))
|
|
2545
|
+
fig.subplots_adjust(right=0.75)
|
|
2546
|
+
lwi = 0.4
|
|
2547
|
+
msi = 0.6
|
|
2548
|
+
tkw = {'size': 4, 'width': 1.5}
|
|
2549
|
+
|
|
2550
|
+
# y-axes for temperature, humidity or both?
|
|
2551
|
+
for index, (axis_title, axis_data) in enumerate(y_axes_to_plot.items()):
|
|
2552
|
+
|
|
2553
|
+
if index == 0:
|
|
2554
|
+
color = 'g'
|
|
2555
|
+
for sensor_name, sensor_data in axis_data.items():
|
|
2556
|
+
|
|
2557
|
+
# handle any missing initial sensor data
|
|
2558
|
+
it_x_local = it_x[-len(sensor_data):]
|
|
2559
|
+
|
|
2560
|
+
host.step(it_x_local, sensor_data, color=color, where='post',
|
|
2561
|
+
linewidth=lwi, markersize=msi, label=sensor_name)
|
|
2562
|
+
|
|
2563
|
+
host.yaxis.label.set_color(color)
|
|
2564
|
+
host.tick_params(axis='y', colors=color, **tkw)
|
|
2565
|
+
host.set_ylabel(axis_title)
|
|
2566
|
+
else:
|
|
2567
|
+
color = 'r'
|
|
2568
|
+
par2 = host.twinx()
|
|
2569
|
+
for sensor_name, sensor_data in axis_data.items():
|
|
2570
|
+
|
|
2571
|
+
# handle any missing initial sensor data
|
|
2572
|
+
it_x_local = it_x[-len(sensor_data):]
|
|
2573
|
+
|
|
2574
|
+
par2.step(it_x_local, sensor_data, color=color, where='post',
|
|
2575
|
+
linewidth=lwi, markersize=msi, label=sensor_name)
|
|
2576
|
+
|
|
2577
|
+
par2.yaxis.label.set_color(color)
|
|
2578
|
+
par2.tick_params(axis='y', colors=color, **tkw)
|
|
2579
|
+
par2.set_ylabel(axis_title)
|
|
2580
|
+
|
|
2581
|
+
host.set_xlabel(f'elapsed time ({units})')
|
|
2582
|
+
|
|
2583
|
+
host.tick_params(axis='x', **tkw)
|
|
2584
|
+
|
|
2585
|
+
title_items = [f'{title} environment, {session}', data.label, settings['itk_serno']]
|
|
2586
|
+
plot_title = '\n'.join(x for x in title_items if x)
|
|
2587
|
+
host.set_title(plot_title)
|
|
2588
|
+
|
|
2589
|
+
plt.tight_layout()
|
|
2590
|
+
|
|
2591
|
+
filename = f'{title}_env'.lower().replace(' ', '_')
|
|
2592
|
+
common.save_plot(settings, os.path.join(directory, filename))
|
|
2593
|
+
|
|
2594
|
+
|
|
2595
|
+
##############################################################################
|
|
2596
|
+
# main
|
|
2597
|
+
##############################################################################
|
|
2598
|
+
|
|
2599
|
+
def main():
|
|
2600
|
+
"""
|
|
2601
|
+
Collects IV and IT data from high-voltage power supplies over RS232.
|
|
2602
|
+
"""
|
|
2603
|
+
# date and time to the nearest second when the script was started
|
|
2604
|
+
session = common.timestamp_to_utc(time.time())
|
|
2605
|
+
|
|
2606
|
+
# ensure all files are contained in a directory
|
|
2607
|
+
if not os.path.exists(session):
|
|
2608
|
+
os.makedirs(session)
|
|
2609
|
+
session = os.path.join(session, session)
|
|
2610
|
+
|
|
2611
|
+
settings = {
|
|
2612
|
+
'alias': None,
|
|
2613
|
+
'atlas': False,
|
|
2614
|
+
'bespoke_sequence_lut': None,
|
|
2615
|
+
'current_limit': None,
|
|
2616
|
+
'debug': None,
|
|
2617
|
+
'decimal_places': 1,
|
|
2618
|
+
'forwardbias': False,
|
|
2619
|
+
'hold': None,
|
|
2620
|
+
'humidity_sensors': None,
|
|
2621
|
+
'ignore': None,
|
|
2622
|
+
'initial': False,
|
|
2623
|
+
'itk_serno': '',
|
|
2624
|
+
'json': False,
|
|
2625
|
+
'label': None,
|
|
2626
|
+
'omitreturn': False,
|
|
2627
|
+
'pc': 0.05,
|
|
2628
|
+
'rear': None,
|
|
2629
|
+
'reset': None,
|
|
2630
|
+
'samples': 10,
|
|
2631
|
+
'sense': None,
|
|
2632
|
+
'settle': False,
|
|
2633
|
+
# default is set in the command line handler
|
|
2634
|
+
'settling_time': None,
|
|
2635
|
+
'stepsize': None,
|
|
2636
|
+
'svg': False,
|
|
2637
|
+
'temperature_ip': '192.168.0.200',
|
|
2638
|
+
'temperature_sensors': None,
|
|
2639
|
+
'voltage': 0,
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
##########################################################################
|
|
2643
|
+
# read command line arguments
|
|
2644
|
+
##########################################################################
|
|
2645
|
+
|
|
2646
|
+
check_arguments(settings)
|
|
2647
|
+
|
|
2648
|
+
##########################################################################
|
|
2649
|
+
# establish which power supply channels to use, or generate some fake
|
|
2650
|
+
# channels if the user has requested debug
|
|
2651
|
+
##########################################################################
|
|
2652
|
+
|
|
2653
|
+
if settings['debug'] is not None:
|
|
2654
|
+
# create <n> number of power supplies on unique ports, to match the
|
|
2655
|
+
# value given in --debug <n>
|
|
2656
|
+
psus = None
|
|
2657
|
+
channels = []
|
|
2658
|
+
for serial_number in range(settings['debug']):
|
|
2659
|
+
port = f'debug_port_{serial_number}'
|
|
2660
|
+
serno = str(1234567 + serial_number)
|
|
2661
|
+
|
|
2662
|
+
channels.append(common.Channel(port=port, config=None,
|
|
2663
|
+
serial_number=serno, model='debug',
|
|
2664
|
+
manufacturer='debug', channel=[],
|
|
2665
|
+
category='debug', release_delay=None,
|
|
2666
|
+
alias=None))
|
|
2667
|
+
else:
|
|
2668
|
+
# read all high-voltage power supplies from cache
|
|
2669
|
+
psus = common.cache_read(['hvpsu'])
|
|
2670
|
+
|
|
2671
|
+
# convert the serial port centric cache entries to power supply
|
|
2672
|
+
# channels, and remove any channels the user doesn't want to use
|
|
2673
|
+
all_channels = common.ports_to_channels(settings, psus)
|
|
2674
|
+
channels = common.exclude_channels(settings, all_channels)
|
|
2675
|
+
|
|
2676
|
+
if not channels:
|
|
2677
|
+
sys.exit('no devices to test')
|
|
2678
|
+
|
|
2679
|
+
##########################################################################
|
|
2680
|
+
# zeromq simple request-reply for interaction with external scripts
|
|
2681
|
+
##########################################################################
|
|
2682
|
+
|
|
2683
|
+
context = zmq.Context(io_threads=2)
|
|
2684
|
+
|
|
2685
|
+
# liveplot.py interaction
|
|
2686
|
+
zmq_skt = context.socket(zmq.REQ)
|
|
2687
|
+
# zmq.RCVTIMEO and zmq.SNDTIMEO specified in units of milliseconds
|
|
2688
|
+
zmq_skt.setsockopt(zmq.RCVTIMEO, 200)
|
|
2689
|
+
zmq_skt.setsockopt(zmq.SNDTIMEO, 200)
|
|
2690
|
+
zmq_skt.setsockopt(zmq.DELAY_ATTACH_ON_CONNECT, 1)
|
|
2691
|
+
port = 5555
|
|
2692
|
+
try:
|
|
2693
|
+
zmq_skt.connect(f'tcp://localhost:{port}')
|
|
2694
|
+
except zmq.error.ZMQError as zerr:
|
|
2695
|
+
message = f'ZeroMQ: {zerr} when connecting to port {port}'
|
|
2696
|
+
common.log_with_colour(logging.WARNING, message)
|
|
2697
|
+
message = 'ZeroMQ: find PID of current owner with: sudo netstat -ltnp'
|
|
2698
|
+
common.log_with_colour(logging.WARNING, message)
|
|
2699
|
+
|
|
2700
|
+
# sense.py interaction
|
|
2701
|
+
zmq_skt2 = context.socket(zmq.REP)
|
|
2702
|
+
|
|
2703
|
+
##########################################################################
|
|
2704
|
+
# enable logging to file and screen
|
|
2705
|
+
##########################################################################
|
|
2706
|
+
|
|
2707
|
+
log = f'{session}.log'
|
|
2708
|
+
logging.basicConfig(
|
|
2709
|
+
filename=log,
|
|
2710
|
+
level=logging.DEBUG,
|
|
2711
|
+
format='%(asctime)s : %(levelname)s : %(message)s')
|
|
2712
|
+
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
|
2713
|
+
|
|
2714
|
+
# enable logging to screen
|
|
2715
|
+
console = logging.StreamHandler()
|
|
2716
|
+
console.setLevel(logging.INFO)
|
|
2717
|
+
formatter = logging.Formatter('%(levelname)s : %(message)s')
|
|
2718
|
+
console.setFormatter(formatter)
|
|
2719
|
+
logging.getLogger('').addHandler(console)
|
|
2720
|
+
|
|
2721
|
+
# set logging time to UTC to match session timestamp
|
|
2722
|
+
logging.Formatter.converter = time.gmtime
|
|
2723
|
+
|
|
2724
|
+
##########################################################################
|
|
2725
|
+
# set up resources for threads
|
|
2726
|
+
##########################################################################
|
|
2727
|
+
|
|
2728
|
+
graceful_quit = mp.Value(ctypes.c_bool, False)
|
|
2729
|
+
|
|
2730
|
+
class Production:
|
|
2731
|
+
"""
|
|
2732
|
+
Queues and locks to support threaded operation.
|
|
2733
|
+
|
|
2734
|
+
RS232 will not tolerate concurrent access. portaccess is used to
|
|
2735
|
+
prevent more than one thread trying to interact with the same RS232
|
|
2736
|
+
port at the same time for multi-channel power supplies. For
|
|
2737
|
+
simplicity, locks are created for all power supplies, even if they
|
|
2738
|
+
only have a single channel.
|
|
2739
|
+
|
|
2740
|
+
The Python Yoctopuce API is not thread safe.
|
|
2741
|
+
"""
|
|
2742
|
+
keypress_queue = mp.Queue()
|
|
2743
|
+
liveplot = mp.Queue()
|
|
2744
|
+
# maxsize is set to 1 to ensure the environmental data available to
|
|
2745
|
+
# this script is the most recently acquired from sense.py. We don't
|
|
2746
|
+
# care about older data acquisitions.
|
|
2747
|
+
sense = mp.Queue(maxsize=1)
|
|
2748
|
+
sense_dict = mp.Manager().dict()
|
|
2749
|
+
portaccess = {port: threading.Lock()
|
|
2750
|
+
for port in {channel.port for channel in channels}}
|
|
2751
|
+
yapiaccess = threading.Lock()
|
|
2752
|
+
|
|
2753
|
+
pipeline = Production()
|
|
2754
|
+
|
|
2755
|
+
# input_thread: keypress detection for early test termination
|
|
2756
|
+
input_thread = threading.Thread(target=check_key, daemon=True,
|
|
2757
|
+
args=(graceful_quit, ))
|
|
2758
|
+
input_thread.start()
|
|
2759
|
+
|
|
2760
|
+
# livp: collate sampled data points and send them to an external plotter
|
|
2761
|
+
livp = threading.Thread(target=liveplot, args=(pipeline, zmq_skt))
|
|
2762
|
+
livp.start()
|
|
2763
|
+
|
|
2764
|
+
# sens: receive environmental data from sense.py
|
|
2765
|
+
sens = threading.Thread(target=sense, daemon=True, args=(pipeline, zmq_skt2, settings))
|
|
2766
|
+
sens.start()
|
|
2767
|
+
|
|
2768
|
+
##########################################################################
|
|
2769
|
+
# Commit command line invocation and core environment to log
|
|
2770
|
+
##########################################################################
|
|
2771
|
+
|
|
2772
|
+
write_debug_information_to_log()
|
|
2773
|
+
|
|
2774
|
+
##########################################################################
|
|
2775
|
+
# Check status of outputs and interlock (inhibit) on all power supplies
|
|
2776
|
+
##########################################################################
|
|
2777
|
+
|
|
2778
|
+
common.initial_power_supply_check(settings, pipeline, psus, channels)
|
|
2779
|
+
|
|
2780
|
+
##########################################################################
|
|
2781
|
+
# detect presence of temperature and humidity sensors
|
|
2782
|
+
##########################################################################
|
|
2783
|
+
|
|
2784
|
+
if not settings['debug']:
|
|
2785
|
+
if not settings['sense']:
|
|
2786
|
+
detect_yoctopuce_sensors(settings)
|
|
2787
|
+
else:
|
|
2788
|
+
message = 'direct access to Yoctopuce sensors disabled'
|
|
2789
|
+
common.log_with_colour(logging.INFO, message)
|
|
2790
|
+
message = ('listening for environmental data from '
|
|
2791
|
+
f'sensor: {settings["sense"]}')
|
|
2792
|
+
common.log_with_colour(logging.INFO, message)
|
|
2793
|
+
|
|
2794
|
+
# sense.py typically transmits data every 5 seconds. There's a
|
|
2795
|
+
# chance that if it and this script are started at the same time,
|
|
2796
|
+
# when this script records its first data point, no environmental data
|
|
2797
|
+
# will have been received yet. Provide a basic mitigation for this.
|
|
2798
|
+
for _ in itertools.repeat(None, 5):
|
|
2799
|
+
if pipeline.sense_dict:
|
|
2800
|
+
break
|
|
2801
|
+
time.sleep(1)
|
|
2802
|
+
else:
|
|
2803
|
+
message = f'no data matching {settings["sense"]} being received from sense.py'
|
|
2804
|
+
common.log_with_colour(logging.WARNING, message)
|
|
2805
|
+
|
|
2806
|
+
##########################################################################
|
|
2807
|
+
# run iv test on all power supply channels concurrently
|
|
2808
|
+
##########################################################################
|
|
2809
|
+
|
|
2810
|
+
pipeline.liveplot.put('reset')
|
|
2811
|
+
|
|
2812
|
+
if channels:
|
|
2813
|
+
common.log_with_colour(logging.INFO, 'Collecting IV data')
|
|
2814
|
+
|
|
2815
|
+
environmental_data_present = bool(settings['temperature_sensors'])\
|
|
2816
|
+
or bool(settings['humidity_sensors'])
|
|
2817
|
+
consignment = common.Consignment(settings['label'], settings['alias'],
|
|
2818
|
+
settings['forwardbias'], settings['hold'],
|
|
2819
|
+
environmental_data_present)
|
|
2820
|
+
|
|
2821
|
+
_gidfp_pf = functools.partial(get_iv_data_from_psu, settings=settings,
|
|
2822
|
+
pipeline=pipeline, graceful_quit=graceful_quit)
|
|
2823
|
+
with cf.ThreadPoolExecutor() as executor:
|
|
2824
|
+
board_iv = (executor.submit(_gidfp_pf, channel) for channel in channels)
|
|
2825
|
+
for future in cf.as_completed(board_iv):
|
|
2826
|
+
consignment.packets.append(future.result())
|
|
2827
|
+
|
|
2828
|
+
##########################################################################
|
|
2829
|
+
# release resources for YoctoPuce API and threads
|
|
2830
|
+
##########################################################################
|
|
2831
|
+
|
|
2832
|
+
if not settings['debug'] and not settings['sense']:
|
|
2833
|
+
yapi.YAPI.FreeAPI()
|
|
2834
|
+
|
|
2835
|
+
# terminate thread for sending data to external plotter
|
|
2836
|
+
pipeline.liveplot.put(None)
|
|
2837
|
+
livp.join()
|
|
2838
|
+
|
|
2839
|
+
##########################################################################
|
|
2840
|
+
# save data and generate plots
|
|
2841
|
+
##########################################################################
|
|
2842
|
+
|
|
2843
|
+
if not channels:
|
|
2844
|
+
return
|
|
2845
|
+
|
|
2846
|
+
consignment.remove_bad_packets()
|
|
2847
|
+
|
|
2848
|
+
# ensure data is arranged in serial number order
|
|
2849
|
+
# this keeps the line colours of tested chips consistent across plots
|
|
2850
|
+
consignment.packets.sort()
|
|
2851
|
+
|
|
2852
|
+
# add the user's label to the base filename
|
|
2853
|
+
filename = session
|
|
2854
|
+
if consignment.safe_label:
|
|
2855
|
+
filename = f'{session}_{consignment.safe_label}'
|
|
2856
|
+
|
|
2857
|
+
if settings['json']:
|
|
2858
|
+
consignment.write_json_files(session)
|
|
2859
|
+
|
|
2860
|
+
save_data(consignment, filename)
|
|
2861
|
+
create_plots(settings, consignment, filename, session)
|
|
2862
|
+
|
|
2863
|
+
logging.info('Finished')
|
|
2864
|
+
|
|
2865
|
+
|
|
2866
|
+
##############################################################################
|
|
2867
|
+
if __name__ == '__main__':
|
|
2868
|
+
main()
|