simcats-datasets 2.5.0__py3-none-any.whl → 2.6.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.
@@ -0,0 +1,471 @@
1
+ """Functions for calculating the Coulomb oscillation area boundaries in sensor scans.
2
+
3
+ @author: k.havemann
4
+ """
5
+ from copy import deepcopy
6
+ import math
7
+ from typing import Callable, Dict, List, Tuple, Union, Optional
8
+
9
+ import numpy as np
10
+
11
+ from simcats.sensor import SensorScanSensorGeneric
12
+
13
+ from simcats_datasets.support_functions.clip_line_to_rectangle import (clip_point_line_to_rectangle,
14
+ clip_slope_line_to_rectangle,
15
+ clip_infinite_slope_line_to_rectangle,
16
+ create_rectangle_corners,
17
+ is_point_inside_rectangle,
18
+ line_intersection)
19
+
20
+
21
+ def get_coulomb_oscillation_area_boundaries(metadata: Dict,
22
+ range_sensor_g1: Optional[np.ndarray] = None,
23
+ range_sensor_g2: Optional[np.ndarray] = None,
24
+ ) -> Tuple[np.ndarray, List[str]]:
25
+ """Function for calculating the boundaries of the Coulomb oscillation area in a sensor scan.
26
+
27
+ Calculates only the parts of the bounding box within the specified sensor gate 1 (g1) and gate 2 (g2) ranges
28
+ (range_sensor_g1, range_sensor_g2). The g1 and g2 pinch-off values are used for the left and lower boundaries and
29
+ the fully conductive values g1 and g2 are used for the right and upper boundaries.
30
+ In the 2D case, the boundaries are lines; in the 1D case, they are points.
31
+
32
+ Args:
33
+ metadata: Dictionary with sensor metadata.
34
+ range_sensor_g1: Range of sensor g1 within which the coulomb oscillation area is calculated.
35
+ If None, sweep_range_sensor_g1 of the sensor metadata is used.
36
+ range_sensor_g2: Range of sensor g2 within which the coulomb oscillation area is calculated.
37
+ If None, sweep_range_sensor_g2 of the sensor metadata is used.
38
+
39
+ Returns:
40
+ np.ndarray, List[str]: Returns an array with the line coordinates (2D)/point coordinates (1D) and list
41
+ containing strings with corresponding labels. Every row of the array represents one line as
42
+ [x_start, y_start, x_stop, y_stop] in 2D case and a point as [x, y, x, y] in 1D case. In addition, a list
43
+ of labels of the calculated boundaries is returned. If both the array and the list are empty, there are no
44
+ boundaries of the oscillation area inside the given voltage ranges.
45
+ """
46
+
47
+ line_points = []
48
+ labels = []
49
+ # list of labels for possible lines within the sensor g1 and g2 ranges
50
+ label_names = ["sensor_g1_pinch_off", "sensor_g2_fully_conductive",
51
+ "sensor_g1_fully_conductive", "sensor_g2_pinch_off"]
52
+ fixed_voltage = None # is None if neither of the two gate voltages is fixed during the simulation
53
+ fixed_g1 = False # specifies whether voltage at g1 is fixed
54
+
55
+ if range_sensor_g1 is None:
56
+ range_sensor_g1 = deepcopy(metadata["sweep_range_sensor_g1"])
57
+ if range_sensor_g2 is None:
58
+ range_sensor_g2 = deepcopy(metadata["sweep_range_sensor_g2"])
59
+
60
+ if type(metadata["resolution"]) is int: # 1D scan
61
+ # Setting up a two-dimensional voltage space for horizontal and vertical scans
62
+ if math.isclose(range_sensor_g1[0], range_sensor_g1[1], rel_tol=1e-3): # vertical scan
63
+ fixed_voltage = range_sensor_g1[0]
64
+ fixed_g1 = True
65
+ range_sensor_g1[0] -= 0.1
66
+ range_sensor_g1[1] += 0.1
67
+ elif math.isclose(range_sensor_g2[0], range_sensor_g2[1], rel_tol=1e-3): # horizontal scan
68
+ fixed_voltage = range_sensor_g2[0]
69
+ range_sensor_g2[0] -= 0.1
70
+ range_sensor_g2[1] += 0.1
71
+
72
+ # get rectangle corners of the gate 1 and gate 2 voltage ranges: [bottom_left, bottom_right, top_right, top_left]
73
+ corners = create_rectangle_corners(range_sensor_g1, range_sensor_g2)
74
+
75
+ # get pinch-off and fully conductive values from metadata
76
+ barrier_potentials = (metadata["sensor"].barrier_functions[0].pinch_off,
77
+ metadata["sensor"].barrier_functions[0].fully_conductive,
78
+ metadata["sensor"].barrier_functions[1].pinch_off,
79
+ metadata["sensor"].barrier_functions[1].fully_conductive)
80
+
81
+ # get lever arm from metadata
82
+ alpha_sensor_gate = metadata["sensor"].alpha_sensor_gate # 3x2 array
83
+ alpha_gate = metadata["sensor"].alpha_gate # 3x2 array
84
+ alpha_dot = metadata["sensor"].alpha_dot # 3x2 array
85
+
86
+ # get get_csd_data()-method from ideal_csd_config
87
+ generate_csd = None if metadata["ideal_csd_config"] is None else deepcopy(metadata["ideal_csd_config"].get_csd_data)
88
+
89
+ # calculate potential offset for both barrier gates
90
+ potential_offsets = calc_potential_offset(
91
+ volt_g1=metadata["volt_g1"],
92
+ volt_g2=metadata["volt_g2"],
93
+ alpha_gate=alpha_gate,
94
+ alpha_dot=alpha_dot,
95
+ generate_csd=generate_csd,
96
+ offset_mu_sens=metadata["sensor"].offset_mu_sens
97
+ )
98
+
99
+ # calculate intersection points of oscillation area boundaries (order of points: [g1pinchoff_g2pinchoff,
100
+ # g1pinchoff_g2fullyconductive, g1fullyconductive_g2fullyconductive, g1fullyconductive_g2pinchoff] resp.
101
+ # [lower_left, upper_left, upper_right, lower_right]
102
+ intersections = calc_oscillation_area_boundaries_intersect_points(
103
+ gate_potentials=barrier_potentials,
104
+ alpha_sensor_gate=alpha_sensor_gate,
105
+ potential_offset=potential_offsets)
106
+
107
+ # save oscillation area boundaries and their labels if they are within the sensor g1 and g2 ranges
108
+ # oscillation area as a quadrilateral
109
+ for i in range(4):
110
+ start_point = intersections[i]
111
+ end_point = intersections[(i + 1) % 4]
112
+
113
+ # use intersection points to extend and clip pinch off lines to the edge of the scan range:
114
+ if i == 0:
115
+ # extend pinch-off line gate 1 to the upper edge of the voltage range of gate 2:
116
+ start_point, end_point = extend_pinch_off_line_to_edge(start_point,
117
+ end_point,
118
+ rect_corners=corners,
119
+ is_gate2=False)
120
+ elif i == 3:
121
+ # extend pinch-off line gate 2 to the upper edge of the voltage range of gate 1:
122
+ start_point, end_point = extend_pinch_off_line_to_edge(start_point,
123
+ end_point,
124
+ rect_corners=corners,
125
+ is_gate2=True)
126
+ else:
127
+ # use intersection points to clip each line to the rectangle defined by sensor g1 and g2 ranges
128
+ start_point, end_point = clip_point_line_to_rectangle(start=start_point,
129
+ end=end_point,
130
+ rect_corners=corners)
131
+
132
+ if start_point is not None and end_point is not None:
133
+ line_points.append(np.array([start_point[0], start_point[1], end_point[0], end_point[1]]))
134
+ labels.append(label_names[i])
135
+
136
+ # add fully conductive line for triangular oscillation area shape:
137
+ fully_conductive_point = intersections[2]
138
+ fully_conductive_slope = - alpha_sensor_gate[0, 0] / alpha_sensor_gate[0, 1]
139
+ start_point, end_point = clip_infinite_slope_line_to_rectangle(slope=fully_conductive_slope,
140
+ point=fully_conductive_point,
141
+ rect_corners=corners)
142
+ if start_point is not None and end_point is not None:
143
+ # the line always goes from the bottom right to the top left:
144
+ if start_point[0] < end_point[0]:
145
+ start_point, end_point = end_point, start_point
146
+
147
+ intersect_pinch_off_g1 = None
148
+ intersect_pinch_off_g2 = None
149
+ if "sensor_g1_pinch_off" in labels:
150
+ pinch_off_g1_start = line_points[labels.index("sensor_g1_pinch_off")][:2]
151
+ pinch_off_g1_end = line_points[labels.index("sensor_g1_pinch_off")][2:]
152
+ intersect_pinch_off_g1 = line_intersection(p1=start_point, p2=end_point,
153
+ q1=pinch_off_g1_start, q2=pinch_off_g1_end)
154
+
155
+ if "sensor_g2_pinch_off" in labels:
156
+ pinch_off_g2_start = line_points[labels.index("sensor_g2_pinch_off")][:2]
157
+ pinch_off_g2_end = line_points[labels.index("sensor_g2_pinch_off")][2:]
158
+ intersect_pinch_off_g2 = line_intersection(p1=start_point, p2=end_point,
159
+ q1=pinch_off_g2_start, q2=pinch_off_g2_end)
160
+
161
+ if intersect_pinch_off_g1 is not None and is_point_inside_rectangle(intersect_pinch_off_g1,
162
+ rect_corners=corners):
163
+ end_point = intersect_pinch_off_g1
164
+ if intersect_pinch_off_g2 is not None and is_point_inside_rectangle(intersect_pinch_off_g2,
165
+ rect_corners=corners):
166
+ start_point = intersect_pinch_off_g2
167
+ # else: no change in start and end points
168
+
169
+ line_points.append(np.array([start_point[0], start_point[1], end_point[0], end_point[1]]))
170
+ labels.append("sensor_potential_fully_conductive")
171
+
172
+ # 1D Scans: convert lines of the two-dimensional voltage space to points in the one-dimensional measurement
173
+ if type(metadata["resolution"]) is int:
174
+ if fixed_voltage is not None: # scan is either horizontal or vertical
175
+ if fixed_g1:
176
+ range_sensor_g1[0] = fixed_voltage
177
+ range_sensor_g1[1] = fixed_voltage
178
+ else:
179
+ range_sensor_g2[0] = fixed_voltage
180
+ range_sensor_g2[1] = fixed_voltage
181
+
182
+ line_points, labels = calc_track_boundaries_intersect_points(volt_range_g1=range_sensor_g1,
183
+ volt_range_g2=range_sensor_g2,
184
+ line_points=np.array(line_points),
185
+ labels=labels)
186
+
187
+ return np.array(line_points), labels
188
+
189
+
190
+ def extend_pinch_off_line_to_edge(start: Tuple[float, float],
191
+ end: Tuple[float, float],
192
+ rect_corners: List[Tuple[float, float]],
193
+ is_gate2: bool) -> Tuple[Tuple[float, float], Tuple[float, float]]:
194
+ """Extend and clip the pinch-off line segment to the edge of the voltage ranges of the sensor scan.
195
+
196
+ The pinch-off lines are calculated as segments between the lower left corner of the oscillation range and the fully
197
+ conductive values of the gates. However, the lines are required beyond the fully conductive value up to the upper
198
+ edge of the voltage ranges of the two gates and are simultaneously trimmed to these.
199
+
200
+ Args:
201
+ start (Tuple[float, float]): The start point of the line segment ((x_start, y_start)).
202
+ end (Tuple[float, float]): The end point of the line segment ((x_end, y_end)).
203
+ rect_corners (np.ndarray): The corners of the boundary of the sensor scan.
204
+ is_gate2 (bool): True if the sensor scan line segment is the pinch-off line for gate 2. False if the sensor scan
205
+ line segment is the pinch-off line for gate 1.
206
+
207
+ Returns:
208
+ Tuple[Tuple[float, float], Tuple[float, float]]: Extended pinch-off line ((x_start, y_start), (x_end, y_end)).
209
+ """
210
+ # If necessary, swap the start and end points:
211
+ if (is_gate2 and start[0] > end[0]) or (not is_gate2 and start[1] > end[1]):
212
+ start, end = end, start
213
+
214
+ den = start[0] - end[0]
215
+ if math.isclose(den, 0.0): # vertical line with infinte slope
216
+ end = (end[0], np.max([corner[1] for corner in rect_corners]))
217
+ else: # all other cases
218
+ slope = (start[1] - end[1]) / den
219
+ start, end = clip_slope_line_to_rectangle(slope=slope,
220
+ point=start,
221
+ rect_corners=rect_corners,
222
+ is_start=is_gate2)
223
+
224
+ return start, end
225
+
226
+
227
+ def calc_oscillation_area_boundaries_intersect_points(gate_potentials: List[float],
228
+ alpha_sensor_gate: np.ndarray, # 3x2 array
229
+ potential_offset: Tuple[float, float],
230
+ ) -> List[Tuple[float, float]]:
231
+ """Function for calculating the 4 intersection point of the boundaries of the coulomb oscillation area.
232
+
233
+ Args:
234
+ gate_potentials: List containing pinch-off and fully conductive potentials of sensor gate 1 and gate 2.
235
+ Assumes it contains exactly 4 values in the specified order:
236
+ [g1_pinch_off, g1_fully_conductive, g2_pinch_off, g2_fully_conductive]
237
+ alpha_sensor_gate: Lever arm from the sensor dot (barrier) gates to the sensor potential.
238
+ potential_offset: Potential offset of the sensor barrier gates.
239
+
240
+ Returns:
241
+ List[Tuple[float, float]]: List of intersection points as Tuples (x_coord, y_coord).
242
+ """
243
+ intersection_points = []
244
+
245
+ if math.isclose(alpha_sensor_gate[1, 1], 0.0): # case: slope of gate 1 line is infinity
246
+ v1_pinch_off = (gate_potentials[0] - potential_offset[0]) / alpha_sensor_gate[1, 0]
247
+ v1_fully_conductive = (gate_potentials[1] - potential_offset[0]) / alpha_sensor_gate[1, 0]
248
+
249
+ v2_pinch_off_left = ((-alpha_sensor_gate[2, 0] * v1_pinch_off + gate_potentials[2] -
250
+ potential_offset[1]) / alpha_sensor_gate[2, 1])
251
+ v2_pinch_off_right = ((-alpha_sensor_gate[2, 0] * v1_fully_conductive + gate_potentials[2] -
252
+ potential_offset[1]) / alpha_sensor_gate[2, 1])
253
+ v2_fully_conductive_left = ((-alpha_sensor_gate[2, 0] * v1_pinch_off + gate_potentials[3] -
254
+ potential_offset[1]) / alpha_sensor_gate[2, 1])
255
+ v2_fully_conductive_right = ((-alpha_sensor_gate[2, 0] * v1_fully_conductive + gate_potentials[3] -
256
+ potential_offset[1]) / alpha_sensor_gate[2, 1])
257
+
258
+ intersection_points = [(v1_pinch_off, v2_pinch_off_left), # g1pinchoff_g2pinchoff
259
+ (v1_pinch_off, v2_fully_conductive_left), # g1pinchoff_g2fullyconductive
260
+ (v1_fully_conductive, v2_fully_conductive_right), # g1fullyconductive_g2fullyconductive
261
+ (v1_fully_conductive, v2_pinch_off_right)] # g1fullyconductive_g2pinchoff
262
+
263
+ else:
264
+ gate1_potentials = [val for val in gate_potentials[:2] for _ in range(2)]
265
+ # gate1_potentials: [g1_pinch_off, g1_pinch_off, g1_fully_conductive, g1_fully_conductive]
266
+ gate2_potentials = gate_potentials[2:] + gate_potentials[:1:-1]
267
+ # gate2_potentials: [g2_pinch_off, g2_fully_conductive, g2_fully_conductive, g2_pinch_off]
268
+ for g1_pot, g2_pot in zip(gate1_potentials, gate2_potentials):
269
+ v1 = (((g2_pot - potential_offset[1]) * alpha_sensor_gate[1, 1]
270
+ - (g1_pot - potential_offset[0]) * alpha_sensor_gate[2, 1]) /
271
+ (alpha_sensor_gate[2, 0] * alpha_sensor_gate[1, 1]
272
+ - alpha_sensor_gate[1, 0] * alpha_sensor_gate[2, 1]))
273
+ v2 = (-alpha_sensor_gate[1, 0] * v1 + (g1_pot - potential_offset[0])) / alpha_sensor_gate[1, 1]
274
+ intersection_points.append((v1.item(), v2.item()))
275
+ # intersection points as Tuples [g1pinchoff_g2pinchoff, g1pinchoff_g2fullyconductive,
276
+ # g1fullyconductive_g2fullyconductive, g1fullyconductive_g2pinchoff] resp.
277
+ # [lower_left, upper_left, upper_right, lower_right]
278
+ return intersection_points
279
+
280
+
281
+ def calc_potential_offset(volt_g1: float,
282
+ volt_g2: float,
283
+ alpha_gate: np.ndarray,
284
+ alpha_dot: np.ndarray,
285
+ generate_csd: Callable[
286
+ [np.ndarray, np.ndarray, Union[int, np.ndarray]], Tuple[np.ndarray, np.ndarray]],
287
+ offset_mu_sens: np.ndarray
288
+ ) -> Tuple[float, float]:
289
+ """Function for calculating the potential offset of the barrier gates of a sensor, which is given by the double dot
290
+ configurations.
291
+
292
+ Args:
293
+ volt_g1: Voltage of the double dot (plunger) gate 1.
294
+ volt_g2: Voltage of the double dot (plunger) gate 2.
295
+ alpha_gate: Lever arms from the double dot (plunger) gates to the sensor potential.
296
+ alpha_dot: Lever arms from the double dot occupations gates to the sensor potential.
297
+ generate_csd: Method for retrieving ideal data (occupancy numbers and a lead transition mask) for given gate
298
+ voltages.
299
+ offset_mu_sens: The potential offset for the sensor.
300
+
301
+ Returns:
302
+ Tuple(float, float): Potential offset of the sensor barrier gates potential (barrier1_off, barrier2_off).
303
+ """
304
+
305
+ if volt_g1 is None or volt_g2 is None:
306
+ return (0.0, 0.0)
307
+ else:
308
+ offset1 = volt_g1 * alpha_gate[1, 0] + volt_g2 * alpha_gate[1, 1] + offset_mu_sens[1]
309
+ offset2 = volt_g1 * alpha_gate[2, 0] + volt_g2 * alpha_gate[2, 1] + offset_mu_sens[2]
310
+ if generate_csd is not None:
311
+ # get double dot occupations
312
+ occupations_csd, lead_transitions = generate_csd(
313
+ volt_limits_g1=(volt_g1, volt_g1), volt_limits_g2=(volt_g2, volt_g2), resolution=(1)
314
+ )
315
+
316
+ offset1 += alpha_dot[1, 0] * occupations_csd[0, 0] + alpha_dot[1, 1] * occupations_csd[0, 1]
317
+ offset2 += alpha_dot[2, 0] * occupations_csd[0, 0] + alpha_dot[2, 1] * occupations_csd[0, 1]
318
+
319
+ return (offset1, offset2)
320
+
321
+
322
+ def calc_sensor_potential(sensor: SensorScanSensorGeneric,
323
+ sweep_range_sensor_g1: Union[np.ndarray, float],
324
+ sweep_range_sensor_g2: Union[np.ndarray, float],
325
+ resolution: Union[int, np.ndarray],
326
+ volt_g1: Optional[float] = None,
327
+ volt_g2: Optional[float] = None,
328
+ generate_csd: Optional[Callable[[np.ndarray, np.ndarray, Union[int, np.ndarray]], Tuple[np.ndarray, np.ndarray]]] = None) -> np.ndarray:
329
+ """Function for calculating the sensor potential.
330
+
331
+ Calculating the sensor potential for ranges of voltages of the two sensor barrier gates.
332
+
333
+ Args:
334
+ sensor (SensorScanSensorGeneric): Sensor for which the potential is to be calculated.
335
+ sweep_range_sensor_g1 (np.ndarray): Voltage sweep range of sensor gate 1 (second-/x-axis). \n
336
+ Example: \n
337
+ [min_V1, max_V1]
338
+ sweep_range_sensor_g2 (np.ndarray): Voltage sweep range of sensor gate 2 (first-/y-axis). \n
339
+ Example: \n
340
+ [min_V1, max_V1]
341
+ resolution (Union[int, np.ndarray]): Resolution of the sensor scan.
342
+ volt_g1 (Optional[float]): Voltage applied at (plunger) gate 1 (second-/x-axis).
343
+ volt_g2 (Optional[float]): Voltage applied at (plunger) gate 2 (first-/y-axis).
344
+ generate_csd: Method for retrieving ideal data (occupancy numbers and a lead transition mask) for given gate
345
+ voltages.
346
+
347
+ Returns:
348
+ np.ndarray: The sensor potential for different sensor barrier gate voltages as an array.
349
+ """
350
+ if generate_csd is None or volt_g1 is None or volt_g2 is None:
351
+ if type(resolution) == int:
352
+ occupations = np.zeros((resolution, 2))
353
+ else:
354
+ occupations = np.zeros((resolution[1], resolution[0], 2))
355
+ else:
356
+ # Perform simulation
357
+ occupations_csd, lead_transitions = generate_csd(
358
+ volt_limits_g1=(volt_g1, volt_g1), volt_limits_g2=(volt_g2, volt_g2), resolution=(1)
359
+ )
360
+
361
+ if type(resolution) == int:
362
+ occupations = np.ones((resolution, 2))
363
+ occupations[:, 0] = occupations[:, 0] * occupations_csd[0, 0]
364
+ occupations[:, 1] = occupations[:, 1] * occupations_csd[0, 1]
365
+ else:
366
+ occupations = np.ones((resolution[1], resolution[0], 2))
367
+ occupations[:, :, 0] = occupations[:, :, 0] * occupations_csd[0, 0]
368
+ occupations[:, :, 1] = occupations[:, :, 1] * occupations_csd[0, 1]
369
+
370
+ # calculate the sensor potential from the distorted occupations
371
+ potential = sensor.sensor_potential(
372
+ occupations=occupations, volt_limits_g1=volt_g1, volt_limits_g2=volt_g2,
373
+ volt_limits_sensor_g1=sweep_range_sensor_g1, volt_limits_sensor_g2=sweep_range_sensor_g2
374
+ )[0] # choose sensor potential from [sensor dot potential, barrier 1 potential, barrier 2 potential]
375
+
376
+ return potential
377
+
378
+
379
+ def calc_fully_conductive_potential(metadata: Dict) -> float:
380
+ """Function for calculating the sensor potential at the fully conductive point.
381
+
382
+ First determining the fully conductive point in the two-dimensional voltage space, then calculating the potential.
383
+
384
+ Args:
385
+ metadata: Dictionary with sensor metadata.
386
+
387
+ Returns:
388
+ float: The sensor potential at the fully conductive point.
389
+ """
390
+ # get pinch-off and fully conductive values from metadata
391
+ barrier_potentials = (metadata["sensor"].barrier_functions[0].pinch_off,
392
+ metadata["sensor"].barrier_functions[0].fully_conductive,
393
+ metadata["sensor"].barrier_functions[1].pinch_off,
394
+ metadata["sensor"].barrier_functions[1].fully_conductive)
395
+
396
+ # get get_csd_data()-method from ideal_csd_config
397
+ generate_csd = None if metadata["ideal_csd_config"] is None else deepcopy(metadata["ideal_csd_config"].get_csd_data)
398
+
399
+ # calculate potential offset for both barrier gates
400
+ potential_offsets = calc_potential_offset(
401
+ volt_g1=metadata["volt_g1"],
402
+ volt_g2=metadata["volt_g2"],
403
+ alpha_gate=metadata["sensor"].alpha_gate,
404
+ alpha_dot=metadata["sensor"].alpha_dot,
405
+ generate_csd=generate_csd,
406
+ offset_mu_sens=metadata["sensor"].offset_mu_sens
407
+ )
408
+
409
+ # calculation of fully conductive point in two-dimensional voltage space
410
+ fully_conductive = calc_oscillation_area_boundaries_intersect_points(
411
+ gate_potentials=barrier_potentials,
412
+ alpha_sensor_gate=metadata["sensor"].alpha_sensor_gate,
413
+ potential_offset=potential_offsets)[2]
414
+
415
+ # calculation of the sensor potential at the fully conductive point
416
+ fully_conductive_potential = calc_sensor_potential(sensor=metadata["sensor"],
417
+ sweep_range_sensor_g1=fully_conductive[0],
418
+ sweep_range_sensor_g2=fully_conductive[1],
419
+ resolution=1,
420
+ volt_g1=metadata["volt_g1"],
421
+ volt_g2=metadata["volt_g2"],
422
+ generate_csd=generate_csd)[0] # choose value from array
423
+
424
+ return fully_conductive_potential
425
+
426
+
427
+ def calc_track_boundaries_intersect_points(volt_range_g1: np.ndarray,
428
+ volt_range_g2: np.ndarray,
429
+ line_points: np.ndarray,
430
+ labels: List[Dict]) -> Tuple[np.ndarray, List[str]]:
431
+ """Calculates the intersection points of a one-dimensional, straight measurement track with the boundaries of the
432
+ Coulomb oscillation area in the two-dimensional sensor data space.
433
+
434
+ Args:
435
+ volt_range_g1 (np.ndarray): Range of sensor g1 of the one-dimensional measurement.
436
+ volt_range_g2 (np.ndarray): Range of sensor g2 of the one-dimensional measurement.
437
+ line_points (np.ndarray): Array containing all start and end points of the boundaries of the Coulomb
438
+ oscillation area. Every row of the array represents one line as [x_start, y_start, x_stop, y_stop].
439
+ labels (List[str]): Dictionary containing labels for each boundary of the Coulomb oscillation area given in
440
+ line_points.
441
+
442
+ Returns:
443
+ Tuple[np.ndarray, List[Dict]]: Returns an array of the intersection points of the one-dimensional, straight
444
+ measurement track with the boundaries of the Coulomb oscillation area. Each row of the array represents one
445
+ intersection point.
446
+ In addition, a list of labels of the calculated intersection points is returned.
447
+ If both the array and the List are empty, there are no intersections.
448
+ """
449
+ intersect_points = []
450
+ point_labels = []
451
+
452
+ # Calculate line coordinates of the track of the 1D scan:
453
+ track_start = (volt_range_g1[0], volt_range_g2[0])
454
+ track_end = (volt_range_g1[-1], volt_range_g2[-1])
455
+
456
+ # measurement is horizontal or vertical:
457
+ if math.isclose(volt_range_g1[-1], volt_range_g1[0]) or math.isclose(volt_range_g2[-1], volt_range_g2[0]):
458
+ for i, line in enumerate(line_points):
459
+ intersect_points.append(line[:2])
460
+ point_labels.append(labels[i])
461
+ else:
462
+ for i, line in enumerate(line_points):
463
+ # calculation intersection point:
464
+ intersection = line_intersection(p1=(line[0], line[1]), p2=(line[2], line[3]), q1=track_start, q2=track_end)
465
+
466
+ if intersection is not None:
467
+ # append point (x, y) as line format [x, y, x, y]
468
+ intersect_points.append(np.array([intersection[0], intersection[1], intersection[0], intersection[1]]))
469
+ point_labels.append(labels[i])
470
+
471
+ return np.array(intersect_points), point_labels
@@ -0,0 +1,62 @@
1
+ """Functions for handling metadata, e.g. reconstruct the dtypes of metadata loaded from h5 files.
2
+
3
+ @author: k.havemann
4
+ """
5
+ import warnings
6
+ from copy import deepcopy
7
+ from typing import Dict
8
+
9
+ import numpy as np
10
+
11
+ # required for eval to work
12
+ from numpy import array
13
+ from simcats.sensor import SensorGeneric, SensorScanSensorGeneric, SensorPeakLorentzian, SensorRiseGLF
14
+ from simcats.sensor.barrier_function import BarrierFunctionGLF
15
+ from simcats.ideal_csd import IdealCSDGeometric
16
+ from simcats.distortions import OccupationDotJumps
17
+
18
+
19
+ def reconstruct_metadata_types(metadata: Dict) -> Dict:
20
+ """Reconstruct metadata types from the sensor metadata dictionary.
21
+
22
+ Args:
23
+ metadata: Dictionary with sensor metadata.
24
+
25
+ Returns:
26
+ Dict: Dictionary with sensor metadata with reconstructed data types.
27
+ """
28
+ meta = deepcopy(metadata)
29
+ if isinstance(meta["resolution"], str): # 1D
30
+ meta["resolution"] = int(meta["resolution"])
31
+
32
+ meta["sweep_range_sensor_g1"] = np.array(meta["sweep_range_sensor_g1"])
33
+ meta["sweep_range_sensor_g2"] = np.array(meta["sweep_range_sensor_g2"])
34
+
35
+ if isinstance(meta["sensor"], str):
36
+ meta["sensor"] = eval(meta["sensor"])
37
+
38
+ if isinstance(meta["ideal_csd_config"], str):
39
+ meta["ideal_csd_config"] = eval(meta["ideal_csd_config"])
40
+
41
+ meta["volt_g1"] = float(meta["volt_g1"]) if meta["volt_g1"] != "None" else None
42
+ meta["volt_g2"] = float(meta["volt_g2"]) if meta["volt_g2"] != "None" else None
43
+
44
+ if meta["volt_limits_sensor_g1"] is not None:
45
+ meta["volt_limits_sensor_g1"] = np.array(meta["volt_limits_sensor_g1"])
46
+ else:
47
+ meta["volt_limits_sensor_g1"] = np.array([-1.0, 0.0])
48
+ warnings.warn(
49
+ f"No voltage limits are specified for the sensor gate 1 in the metadata. "
50
+ f"[-1.0; 0.0] is used as the voltage limits for sensor gate 1. "
51
+ f"This may not be consistent with the scan configuration.")
52
+
53
+ if meta["volt_limits_sensor_g2"] is not None:
54
+ meta["volt_limits_sensor_g2"] = np.array(meta["volt_limits_sensor_g2"])
55
+ else:
56
+ meta["volt_limits_sensor_g2"] = np.array([-1.0, 0.0])
57
+ warnings.warn(
58
+ f"No voltage limits are specified for the sensor gate 2 in the metadata. "
59
+ f"[-1.0; 0.0] is used as the voltage limits for sensor gate 2. "
60
+ f"This may not be consistent with the scan configuration.")
61
+
62
+ return meta