itkdb-gtk 0.9.0__py3-none-any.whl → 0.10.0__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.

Potentially problematic release.


This version of itkdb-gtk might be problematic. Click here for more details.

@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env python3
2
+ """A collection of utilities for sensor data."""
3
+ import io
4
+ from pathlib import Path
5
+ import numpy as np
6
+ import warnings
7
+ import tempfile
8
+ from itkdb_gtk import ITkDButils
9
+
10
+ #
11
+ # The following is taken from
12
+ # https://gitlab.cern.ch/atlas-itk/sw/db/production_database_scripts.git
13
+ #
14
+ mm = 1e-3
15
+ cm = 1e-2
16
+ UpperV = 500 # For sensor to pass, I must be < Imax up until this voltage and no breakdown must be detected before then.
17
+ StabilityV = 700 # Voltage with multiple current readings to check stability
18
+
19
+ AreaDict = {
20
+ "Unknown": (97.621 - 0.450) * (97.950 - 0.550) * mm * mm,
21
+ #
22
+ "ATLAS12": 95.7 * 95.64 * mm * mm,
23
+ "ATLAS17LS": (97.621 - 0.450) * (97.950 - 0.550) * mm * mm,
24
+ #
25
+ "ATLAS18R0": 89.9031 * cm * cm,
26
+ "ATLAS18R1": 89.0575 * cm * cm,
27
+ "ATLAS18R2": 74.1855 * cm * cm,
28
+ "ATLAS18R3": 80.1679 * cm * cm,
29
+ "ATLAS18R4": 87.4507 * cm * cm,
30
+ "ATLAS18R5": 91.1268 * cm * cm,
31
+ #
32
+ "ATLAS18SS": 93.6269 * cm * cm,
33
+ "ATLAS18LS": 93.6269 * cm * cm,
34
+ #
35
+ "ATLASDUMMY18": (97.621 - 0.450) * (97.950 - 0.550) * mm * mm,
36
+ }
37
+
38
+
39
+ NO_BD_CONST = 9.99e99
40
+
41
+ def LocateMicroDischarge(
42
+ I,
43
+ V,
44
+ sm_window=2,
45
+ bd_limit=5.5,
46
+ allow_running_bd=True,
47
+ use_additional_cond=False,
48
+ tolerence=0.05,
49
+ voltage_span=4,
50
+ fit_window=5,
51
+ ):
52
+ """
53
+ Function for BDV estimation - if questions please contact Vera Latonova (vera.latonova@cern.ch).
54
+ I,V must have same shape and voltages must be in ascending order,
55
+ same indexes of I&V arrays must correspond each other,
56
+ only invalid data or holdstep should be stripped before
57
+ but it is not necessary. Measurments U=0&I=0 are removed.
58
+ If there is same or higher amount of same voltages in row than
59
+ sm_window, from this sequence we cannot estimete dI/dV and
60
+ we have to remove this averaged point.
61
+
62
+ It is assumed that only parameter use_additional_cond would be
63
+ changed by caller. Changing of other parameters may affect
64
+ BDV unexpectedly.
65
+
66
+
67
+ @param[in] I - array of currents without any cut
68
+ @param[in] V - array of voltages, ascending order, without any cut
69
+ @param[in] sm_window - size of smoothing window
70
+ @param[in] bd_limit - BD limit for |U| < 500V
71
+ @param[in] allow_running_bd - allow increase bd_limit for |U| > 500
72
+ @param[in] use_additional_cond - use additional BD contition
73
+ @param[in] tolerence - configuration of additional condition
74
+ @param[in] voltage_span - max width of hump on spectra which may be neglected
75
+ in voltage steps in additional contition
76
+ @param[in] fit_window - number of points used for linear fit before BD voltage
77
+
78
+ @return BD voltage (always positive) or NO_BD_CONST = 9.99e99 if not found.
79
+ """
80
+
81
+ # add nan to the end of array
82
+ V = np.abs(V)
83
+ I = np.abs(I)
84
+
85
+ # skip zeros
86
+ ind = np.where(np.logical_or(I != 0, V != 0))
87
+ V = V[ind]
88
+ I = I[ind]
89
+
90
+ V_ = np.append(V, np.nan * np.ones(sm_window - 1))
91
+ I_ = np.append(I, np.nan * np.ones(sm_window - 1))
92
+
93
+ # make 2D array of I's, V's each row_ind shifted by row_ind index
94
+ # i.e from array [1,3,5] we make (for sm_window=2) 2D array
95
+ # [ 1,3,5,nan]
96
+ # [nan,5,1,3]
97
+ # than get average from each column -> I_avg, V_avg
98
+ r = np.arange(sm_window)
99
+
100
+ V2 = np.outer(np.ones(sm_window), V_)
101
+ row_ind, col_ind = np.ogrid[: V2.shape[0], : V2.shape[1]]
102
+ col_ind = col_ind - r[:, np.newaxis]
103
+ V2 = V2[row_ind, col_ind]
104
+ # strip fields with nans
105
+ V2 = np.transpose(V2[:, (sm_window - 1) : -(sm_window - 1)])
106
+
107
+ I2 = np.outer(np.ones(sm_window), I_)
108
+ row_ind, col_ind = np.ogrid[: I2.shape[0], : I2.shape[1]]
109
+ col_ind = col_ind - r[:, np.newaxis]
110
+ I2 = I2[row_ind, col_ind]
111
+ I2 = np.transpose(I2[:, (sm_window - 1) : -(sm_window - 1)])
112
+
113
+ # get V & I averages
114
+ try:
115
+ V_avg = np.average(V2, axis=1)
116
+ I_avg = np.average(I2, axis=1)
117
+ except ZeroDivisionError:
118
+ # not enough data
119
+ return NO_BD_CONST
120
+
121
+ # find dI / dV array
122
+ # I'm not able to write this without cycle
123
+ dIdV = np.array([])
124
+ for i in range(V2.shape[0]):
125
+ with warnings.catch_warnings():
126
+ warnings.filterwarnings("error")
127
+ try:
128
+ dIdV = np.append(dIdV, np.polyfit(V2[i, :], I2[i, :], 1)[0])
129
+ except (np.RankWarning, TypeError):
130
+ dIdV = np.append(dIdV, np.nan)
131
+
132
+ # stripping U[n] == U[n+1] (i.e. hodlsetp) => fit cannot be sucessful =>
133
+ # dIdV is nan @holdstep
134
+ ind = np.where(np.isfinite(dIdV))
135
+ I_avg = I_avg[ind]
136
+ V_avg = V_avg[ind]
137
+ dIdV = dIdV[ind]
138
+
139
+ # get running BDV limit & compare
140
+ bd_limit_running = bd_limit + np.where(
141
+ allow_running_bd and V_avg > 500, (V_avg - 500.0) / 100.0, 0
142
+ )
143
+ V_avg_BD_ind = dIdV / (I_avg / V_avg) > bd_limit_running
144
+ V_avg_BD = V_avg[V_avg_BD_ind]
145
+
146
+ # Estimate BDV
147
+ BDV = np.array([])
148
+
149
+ # no break-down
150
+ if V_avg_BD.shape == (0,):
151
+ return NO_BD_CONST
152
+
153
+ # if V_avg_BD_ind[0] == True ... BDV <- V[0]
154
+ # for others V_avg_BD_ind[n] == True BDV <- (V_avg[n] + V_avg[n-1])/2
155
+ if V_avg_BD_ind[0]:
156
+ BDV = np.append(BDV, V[0])
157
+ V_avg_BD_ind[0] = False
158
+
159
+ BDV = np.append(
160
+ BDV,
161
+ (V_avg[np.where(V_avg_BD_ind)] + V_avg[np.where(V_avg_BD_ind)[0] - 1]) / 2.0,
162
+ )
163
+
164
+ ###########################################################################
165
+ ## Application of additional condition ####################################
166
+ ###########################################################################
167
+ if not use_additional_cond:
168
+ return BDV[0]
169
+
170
+ # get index if V <= BDV
171
+ B = np.where(np.less.outer(BDV, V))
172
+ col_ind = np.mgrid[: BDV.shape[0], : V.shape[0]][1]
173
+ col_ind[B[0], B[1]] = 0
174
+ V_BDV_ind = np.max(col_ind, axis=1)
175
+
176
+ back_ok_v_ind = 0
177
+ while True:
178
+ with warnings.catch_warnings():
179
+ warnings.filterwarnings("error")
180
+ try:
181
+ a, b = np.polyfit(
182
+ V[
183
+ max(back_ok_v_ind, V_BDV_ind[0] - fit_window) : max(
184
+ back_ok_v_ind, V_BDV_ind[0]
185
+ )
186
+ ],
187
+ I[
188
+ max(back_ok_v_ind, V_BDV_ind[0] - fit_window) : max(
189
+ back_ok_v_ind, V_BDV_ind[0]
190
+ )
191
+ ],
192
+ 1,
193
+ )
194
+ except (np.RankWarning, TypeError):
195
+ return BDV[0]
196
+
197
+ ind = np.where(1 - (a * V + b) / I <= tolerence)[0]
198
+ try:
199
+ back_ok_v_ind = np.min(ind[ind > V_BDV_ind[0] + 1])
200
+ except ValueError:
201
+ # sensor is not going back
202
+ return BDV[0]
203
+ # hump is too long -- it cannot be skipped
204
+ if back_ok_v_ind - V_BDV_ind[0] > voltage_span:
205
+ return BDV[0]
206
+
207
+ # skip BDVs inside hump
208
+ ind = BDV >= V[back_ok_v_ind]
209
+ BDV = BDV[ind]
210
+ V_BDV_ind = V_BDV_ind[ind]
211
+ if V_avg_BD.shape == (0,):
212
+ return NO_BD_CONST
213
+
214
+ return NO_BD_CONST
215
+
216
+
217
+ def scale_iv(I, T1, T2):
218
+ """Normalize corrent to given temperature (T2)
219
+
220
+ Args:
221
+ I (array): Current
222
+ T1 (float): Original temperature
223
+ T2 (float): New temperature.
224
+
225
+ Return:
226
+ Array with scaled currents.
227
+
228
+ """
229
+ factor = (T2 / T1) ** 2 * np.exp((-1.21 / 8.62) * (1 / T2 - 1 / T1))
230
+ return factor * I
231
+
232
+
233
+ def sensor_data_to_json(session, mdata, mod_type, logger=None):
234
+ """Read a Sensor data file and return the corresponding JSon for hte PDB.
235
+
236
+ Args:
237
+ mdata: Data as returned by :function:`read_sensor_file`.
238
+ logger: an object that writes messages via a write_message method
239
+ """
240
+ # Check if we are dealing with sensors or modules
241
+ is_module = "Module_SN" in mdata
242
+
243
+ if is_module:
244
+ test = ITkDButils.get_test_skeleton(session, "MODULE", mdata["TestType"])
245
+ tp = "ATLAS18{}".format(mod_type[0:2])
246
+ area = AreaDict[tp] / cm**2
247
+ SN = mdata["Module_SN"]
248
+
249
+ else:
250
+ test = ITkDButils.get_test_skeleton(session, "SENSOR", mdata["TestType"])
251
+ area = AreaDict[mod_type] / cm**2
252
+ SN = mdata["Component"]
253
+
254
+ if logger:
255
+ logger.write_message("Analyzing {}\n".format(SN))
256
+
257
+ # The data arrays
258
+ V = np.abs(mdata["curve"]["V"])
259
+ I = np.abs(mdata["curve"]["I"])
260
+ passed = True
261
+
262
+ # Find Current @ 500V
263
+ indx = np.where(V == 500)[0]
264
+ i_500 = I[indx][0] / area
265
+ if logger:
266
+ logger.write_message("I @ 500V = {:.2f} nA/cm2\n".format(i_500))
267
+
268
+ # Compute current stability
269
+ IStability = abs(I[abs(V) == StabilityV])
270
+ IVariation = -1
271
+ if np.size(IStability) > 1: # Maybe make == 4?
272
+ IVariation = abs(np.std(IStability) / np.mean(IStability))
273
+
274
+ if logger:
275
+ logger.write_message("I stability = {:.6f} nA\n".format(IVariation))
276
+
277
+ # Search for Micro discharges
278
+ # Check for micro-discharge in non-normalized current,
279
+ # removing duplicate Voltage entries (e.g. for stability measurements)
280
+ comments = []
281
+ defects = []
282
+ UniqueVs, UniqueIndices = np.unique(V, return_index=True)
283
+ MicroDischargeV = LocateMicroDischarge(I[UniqueIndices], UniqueVs)
284
+ if MicroDischargeV < np.max(V):
285
+ comments.append("Found micro discharge: {:.1f} V\n".format(MicroDischargeV))
286
+ if logger:
287
+ logger.write_message(comments[-1])
288
+
289
+ if MicroDischargeV < UpperV:
290
+ txt = "microdischarge happening before {:.1f}V.".format(UpperV)
291
+ defects.append({
292
+ "name": "MicroDischarge",
293
+ "description": txt,
294
+ "properties": {}
295
+ }
296
+ )
297
+ if logger:
298
+ logger.write_message("...{}. FAILED\n".format(txt))
299
+
300
+ passed = False
301
+ else:
302
+ if MicroDischargeV == NO_BD_CONST:
303
+ MicroDischargeV = 700.0
304
+
305
+ test["component"] = SN
306
+ test["institution"] = mdata["Institute"]
307
+ test["runNumber"] = mdata["RunNumber"]
308
+ test["date"] = ITkDButils.get_db_date(
309
+ "{} {}".format(mdata["Date"], mdata["Time"])
310
+ )
311
+ test["passed"] = passed
312
+ test["problems"] = False
313
+ test["properties"]["VBIAS_SMU"] = mdata["Vbias_SMU"]
314
+ test["properties"]["RSERIES"] = mdata["Rseries"]
315
+ test["properties"]["TEST_DMM"] = mdata["Test_DMM"]
316
+ test["properties"]["RSHUNT"] = mdata["Rshunt"]
317
+ test["properties"]["RUNNUMBER"] = mdata["RunNumber"]
318
+ test["properties"]["COMMENTS"] = mdata["Comments"]
319
+ test["properties"]["ALGORITHM_VERSION"] = "0.0.0"
320
+ if is_module:
321
+ test["properties"]["SOFTWARE_TYPE_VERSION"] = "pyProbe"
322
+ test["properties"]["MODULE_STAGE"] = mdata["Module_Stage"]
323
+
324
+ test["results"]["TEMPERATURE"] = mdata["Temperature"]
325
+ test["results"]["HUMIDITY"] = mdata["Humidity"]
326
+ test["results"]["VBD"] = MicroDischargeV
327
+ test["results"]["I_500V"] = i_500
328
+ test["results"]["VOLTAGE"] = -np.abs(V)
329
+ test["results"]["CURRENT"] = -np.abs(I)
330
+ test["results"]["RMS_STABILITY"] = IVariation
331
+ test["results"]["SHUNT_VOLTAGE"] = np.zeros(V.shape)
332
+ test["defects"] = defects
333
+ test["comments"] = comments
334
+
335
+ return test
336
+
337
+
338
+ def read_sensor_file(fnam):
339
+ """Read a data file. Return dictionary with all teh data."""
340
+ labels = []
341
+ metadata = {}
342
+ with open(fnam, "r", encoding="utf-8") as ifile:
343
+ first = True
344
+ for line in ifile:
345
+ if first:
346
+ first = False
347
+ ipos = line.rfind(".")
348
+ metadata["fname"] = line[:ipos]
349
+ continue
350
+
351
+ if line.find("Voltage [V]") >= 0 or line.find("Voltage[V]") >= 0:
352
+ labels = line.split("\t")
353
+ break
354
+
355
+ rc = line.find(":")
356
+ if rc >= 0:
357
+ key = line[:rc].strip()
358
+ val = line[rc + 1 :].strip()
359
+ if key in ["Temperature", "Humidity"]:
360
+ metadata[key] = float(val)
361
+ else:
362
+ metadata[key] = val
363
+
364
+ V = []
365
+ I = []
366
+ S = []
367
+ for line in ifile:
368
+ data = [float(s) for s in line.split()]
369
+ V.append(data[0])
370
+ I.append(data[1])
371
+ try:
372
+ S.append(data[2])
373
+ except IndexError:
374
+ S.append(0.0)
375
+
376
+ metadata["curve"] = {
377
+ "V": np.abs(np.array(V)),
378
+ "I": np.abs(np.array(I)),
379
+ "S": np.abs(np.array(S)),
380
+ "labels": labels,
381
+ }
382
+ return metadata
383
+
384
+
385
+ def save_sensor_data(fnam, mdata, name=None):
386
+ """Save sensor dat in file with the proper format.
387
+
388
+ Args:
389
+ fnam: file name or file object.
390
+ mdata (dict): data as returned by :function:`read_sensor_file`
391
+
392
+ """
393
+ if hasattr(fnam, "write") and callable(fnam.write):
394
+ data_out = fnam
395
+ if name:
396
+ fnam = name
397
+ else:
398
+ fnam = build_file_name(mdata)
399
+
400
+ else:
401
+ data_out = open(fnam, 'w', encoding="utf-8")
402
+ fnam = Path(fnam).name
403
+
404
+ is_module = "Module_SN" in mdata
405
+ if is_module:
406
+ SN = mdata["Module_SN"]
407
+ else:
408
+ SN = mdata["Component"]
409
+
410
+ if is_module:
411
+ items = [
412
+ "Type",
413
+ "Wafer",
414
+ "Module_SN",
415
+ "Module_Stage",
416
+ "Date",
417
+ "Time",
418
+ "Institute",
419
+ "TestType",
420
+ "Vbias_SMU",
421
+ "Rseries",
422
+ "Test_DMM",
423
+ "Rshunt",
424
+ "Software type and version, fw version",
425
+ "RunNumber",
426
+ "Temperature",
427
+ "Humidity",
428
+ "Comments",
429
+ ]
430
+ else:
431
+ items = [
432
+ "Type",
433
+ "Batch",
434
+ "Wafer",
435
+ "Component",
436
+ "Date",
437
+ "Time",
438
+ "Institute",
439
+ "TestType",
440
+ "Vbias_SMU",
441
+ "Rseries",
442
+ "Test_DMM",
443
+ "Rshunt",
444
+ "RunNumber",
445
+ "Temperature",
446
+ "Humidity",
447
+ "Comments",
448
+ ]
449
+
450
+ data_out.write("{}\n".format(fnam))
451
+ for key in items:
452
+ if key == "Module_SN" or key == "Component":
453
+ data_out.write("{}: {}\n".format(key, SN))
454
+ else:
455
+ data_out.write("{}: {}\n".format(key, mdata[key]))
456
+
457
+ labels = [lbl for lbl in mdata["curve"]["labels"]]
458
+ if len(labels) < 3:
459
+ labels.append("Shunt_voltage [mV]")
460
+
461
+ for il, label in enumerate(labels):
462
+ if il:
463
+ data_out.write("\t")
464
+ data_out.write(label)
465
+ data_out.write("\n")
466
+
467
+
468
+ # The data arrays
469
+ V = np.abs(mdata["curve"]["V"])
470
+ I = np.abs(mdata["curve"]["I"])
471
+ S = np.abs(mdata["curve"]["S"])
472
+
473
+ ndata = len(V)
474
+ for i in range(ndata):
475
+ data_out.write("{:10.2f}\t{:10.2f}\t{:10.2f}\n".format(V[i], I[i], S[i]))
476
+
477
+ print(data_out.name)
478
+ data_out.close()
479
+
480
+ def build_file_name(mdata):
481
+ """Create a file name from the data."""
482
+ is_module = "Module_SN" in mdata
483
+ if is_module:
484
+ SN = mdata["Module_SN"]
485
+ fnam = "{}_{}_IV_{}".format(SN, mdata["Module_Stage"], mdata["RunNumber"])
486
+ else:
487
+ SN = mdata["Component"]
488
+ fnam = "{}-W{}_IV_{}".format(mdata["Batch"], mdata["Wafer"], mdata["RunNumber"])
489
+
490
+ return fnam