simcats 1.1.0__py3-none-any.whl → 2.0.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.
Files changed (42) hide show
  1. simcats/__init__.py +4 -3
  2. simcats/_default_configs.py +129 -13
  3. simcats/_simulation.py +451 -69
  4. simcats/config_samplers/_GaAs_v1_random_variations_v3_config_sampler.py +1059 -0
  5. simcats/config_samplers/__init__.py +9 -0
  6. simcats/distortions/_distortion_interfaces.py +1 -1
  7. simcats/distortions/_dot_jumps.py +8 -6
  8. simcats/distortions/_random_telegraph_noise.py +4 -4
  9. simcats/distortions/_transition_blurring.py +5 -5
  10. simcats/distortions/_white_noise.py +2 -2
  11. simcats/ideal_csd/geometric/_generate_lead_transition_mask.py +3 -3
  12. simcats/ideal_csd/geometric/_get_electron_occupation.py +5 -5
  13. simcats/ideal_csd/geometric/_ideal_csd_geometric.py +5 -5
  14. simcats/ideal_csd/geometric/_ideal_csd_geometric_class.py +9 -9
  15. simcats/ideal_csd/geometric/_tct_bezier.py +5 -5
  16. simcats/sensor/__init__.py +10 -6
  17. simcats/sensor/{_generic_sensor.py → _sensor_generic.py} +1 -1
  18. simcats/sensor/_sensor_interface.py +164 -11
  19. simcats/sensor/_sensor_rise_glf.py +229 -0
  20. simcats/sensor/_sensor_scan_sensor_generic.py +929 -0
  21. simcats/sensor/barrier_function/__init__.py +9 -0
  22. simcats/sensor/barrier_function/_barrier_function_glf.py +280 -0
  23. simcats/sensor/barrier_function/_barrier_function_interface.py +43 -0
  24. simcats/sensor/barrier_function/_barrier_function_multi_glf.py +157 -0
  25. simcats/sensor/deformation/__init__.py +9 -0
  26. simcats/sensor/deformation/_sensor_peak_deformation_circle.py +109 -0
  27. simcats/sensor/deformation/_sensor_peak_deformation_interface.py +65 -0
  28. simcats/sensor/deformation/_sensor_peak_deformation_linear.py +77 -0
  29. simcats/support_functions/__init__.py +11 -3
  30. simcats/support_functions/_generalized_logistic_function.py +146 -0
  31. simcats/support_functions/_linear_algebra.py +171 -0
  32. simcats/support_functions/_parameter_sampling.py +108 -19
  33. simcats/support_functions/_pixel_volt_transformation.py +24 -0
  34. simcats/support_functions/_reset_offset_mu_sens.py +43 -0
  35. {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info}/METADATA +93 -29
  36. simcats-2.0.0.dist-info/RECORD +53 -0
  37. {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info}/WHEEL +1 -1
  38. simcats-1.1.0.dist-info/RECORD +0 -37
  39. /simcats/sensor/{_gaussian_sensor_peak.py → _sensor_peak_gaussian.py} +0 -0
  40. /simcats/sensor/{_lorentzian_sensor_peak.py → _sensor_peak_lorentzian.py} +0 -0
  41. {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info/licenses}/LICENSE +0 -0
  42. {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,929 @@
1
+ """This module contains an implementation of the SensorScanSensorInterface for simulating sensor scans.
2
+ This is a sensor that also enables sensor scans to be measured. The sensor function of this sensor is able to use
3
+ generic sensor peaks.
4
+
5
+ @author: b.papajewski
6
+ """
7
+
8
+ import copy
9
+ import numbers
10
+ import sys
11
+ import warnings
12
+ from copy import deepcopy
13
+ from typing import List, Union, Dict, Tuple, Optional
14
+
15
+ import numpy as np
16
+
17
+ from simcats.sensor import SensorPeakInterface, SensorScanSensorInterface, SensorRiseInterface
18
+ from simcats.sensor.barrier_function._barrier_function_interface import BarrierFunctionInterface
19
+ from simcats.sensor.deformation import SensorPeakDeformationInterface
20
+ from simcats.support_functions import signed_dist_points_line, pixel_to_volt_1d
21
+
22
+ __all__ = []
23
+
24
+
25
+ class SensorScanSensorGeneric(SensorScanSensorInterface):
26
+ """Generic implementation of the SensorScanSensorInterface."""
27
+
28
+ def __init__(
29
+ self,
30
+ barrier_functions: Union[
31
+ BarrierFunctionInterface, Tuple[BarrierFunctionInterface, BarrierFunctionInterface]],
32
+ sensor_peak_function: Union[SensorPeakInterface, List[SensorPeakInterface]],
33
+ alpha_sensor_gate: np.ndarray,
34
+ alpha_gate: np.ndarray = np.array([[0, 0], [0, 0], [0, 0]]),
35
+ alpha_dot: np.ndarray = np.array([[0, 0], [0, 0], [0, 0]]),
36
+ offset_mu_sens: np.ndarray = np.array([0, 0, 0]),
37
+ final_rise: Optional[SensorRiseInterface] = None,
38
+ sensor_peak_deformations: Dict[int, SensorPeakDeformationInterface] = {}
39
+ ):
40
+ """Initializes an object of the class for the simulation of the sensor response for a sensor scan.
41
+
42
+ Args:
43
+ barrier_functions (Union[BarrierFunctionInterface, Tuple[BarrierFunctionInterface, BarrierFunctionInterface]]):
44
+ Barrier functions model the responses of the barriers.
45
+ sensor_peak_function (Union[SensorPeakInterface, List[SensorPeakInterface], None]): An implementation of the
46
+ SensorPeakInterface. It is also possible to supply a list of such peaks, if a sensor with multiple peaks
47
+ should be simulated.
48
+ alpha_dot (np.ndarray): Lever-arm of the dots on the sensor potential. With these, the influence of the
49
+ occupation of the two dots on the three potentials (sensor dot, barrier1, and barrier2) is specified.
50
+ The lever-arms are specified as follows np.array([lever-arms sensor dot, lever-arms barrier1,
51
+ lever-arms barrier2]). The individual three lever-arms are each specified as a numpy array. The first
52
+ value describes the influence of the electron occupation of the first dot on the corresponding potential
53
+ and the second the influence of the electron occupation of the second dot. The values should be
54
+ negative. The default value is np.array([[0, 0], [0, 0], [0, 0]]). The lever-arms can also be passed as
55
+ a numpy array with two elements. The lever-arms are then used for the influence on all three potentials.
56
+ If None is passed instead of a numpy array the default value of np.array([[0, 0], [0, 0], [0, 0]]) is
57
+ used.
58
+ alpha_gate (np.ndarray): Lever-arms of the double dot (plunger) gates to the three potentials (sensor dot,
59
+ barrier1, and barrier2). The values should be positive. The lever-arms are specified as follows
60
+ np.array([lever-arms sensor dot, lever-arms barrier1, lever-arms barrier2]). The individual three
61
+ lever-arms are each specified as a numpy array with two elements. The first value describes the
62
+ influence of the voltage of the (plunger) gate of the first dot on the potential, with the second value
63
+ defined analogously for the (plunger) gate of the second dot. Default is
64
+ np.array([[0, 0], [0, 0], [0, 0]]). The lever-arms can also be passed as a numpy array with two
65
+ elements. The lever-arms are then used for the influence on all three potentials. If None is passed
66
+ instead of a numpy array the default value of np.array([[0, 0], [0, 0], [0, 0]]) is used.
67
+ alpha_sensor_gate (np.ndarray): Lever-arms of the sensor dot gates to the three potentials (sensor dot,
68
+ barrier1, and barrier2). The values should be positive. The lever-arms are specified as follows
69
+ np.array([lever-arms sensor dot, lever-arms barrier1, lever-arms barrier2]). The individual three
70
+ lever-arms are each specified as a numpy array with two elements. The first value describes the
71
+ influence of the voltage of the first gate of the sensor dot to the potential and analogously for the
72
+ second.
73
+ offset_mu_sens (np.ndarray): Offset of the electrochemical potential of the three potentials (sensor dot,
74
+ barrier1, and barrier2). This offset can have various causes.
75
+ final_rise (Optional[SensorRiseInterface]): An implementation of the SensorRiseInterface interface. This
76
+ interface represents the rise of the sensor function to the maximum sensor response when both barriers
77
+ are open. This rise is also shifted so that it fits appropriately with the open barriers. The default
78
+ value is None. If None is used, no rise is added to the sensor function.
79
+ sensor_peak_deformations Dict[int, SensorPeakDeformationInterface]:
80
+ Dictionary that specifies the deformations of the wavefronts.
81
+ The key of the dictionary is an integer that specifies the wavefront to which the deformation belongs.
82
+ This value refers to the wavefront number as listed in sensor_peak_function. The value of the dictionary
83
+ is the actual deformation object used for a wavefront.
84
+ """
85
+ self.sensor_peak_function = sensor_peak_function
86
+ self.sensor_peak_deformations = sensor_peak_deformations
87
+ self.final_rise = final_rise
88
+
89
+ if self.sensor_peak_function is not None:
90
+ for idx, peak_func in enumerate(self.sensor_peak_function):
91
+ if sensor_peak_deformations is not None and idx in sensor_peak_deformations:
92
+ deformation = sensor_peak_deformations[idx]
93
+ deformation.sensor = self
94
+
95
+ self.alpha_dot = alpha_dot
96
+ self.alpha_gate = alpha_gate
97
+ self.alpha_sensor_gate = alpha_sensor_gate
98
+ self.offset_mu_sens = offset_mu_sens
99
+
100
+ self.barrier_functions = barrier_functions
101
+
102
+ @property
103
+ def sensor_peak_function(self) -> Union[SensorPeakInterface, List[SensorPeakInterface], None]:
104
+ """Returns the current sensor peak function configuration of the sensor.
105
+
106
+ This configuration can then be adjusted and is directly used as a new configuration, as the object is
107
+ returned as call by reference.
108
+
109
+ Returns:
110
+ list[SensorPeakInterface]: A list of SensorPeakInterface implementations.
111
+ """
112
+ return self.__sensor_peak_function
113
+
114
+ @sensor_peak_function.setter
115
+ def sensor_peak_function(self, sensor_peak_function: Union[SensorPeakInterface, List[SensorPeakInterface], None]):
116
+ """Updates the sensor peak function configuration of the sensor according to the supplied values.
117
+
118
+ Args:
119
+ sensor_peak_function (Union[SensorPeakInterface, List[SensorPeakInterface], None]): An implementation of the
120
+ SensorPeakInterface. It is also possible to supply a list of such peaks, if a sensor with multiple
121
+ peaks should be simulated.
122
+ """
123
+ # check datatype of sensor_peak_function
124
+ if isinstance(sensor_peak_function, SensorPeakInterface):
125
+ self.__sensor_peak_function = [deepcopy(sensor_peak_function)]
126
+ elif isinstance(sensor_peak_function, list) and all(
127
+ isinstance(x, SensorPeakInterface) for x in sensor_peak_function
128
+ ):
129
+ self.__sensor_peak_function = deepcopy(sensor_peak_function)
130
+ elif sensor_peak_function is None:
131
+ self.__sensor_peak_function = None
132
+ else:
133
+ raise ValueError(
134
+ "The provided sensor_peak_function configuration is not supported. Must be either an "
135
+ "implementation of the SensorPeakInterface, a list of such, or None."
136
+ )
137
+
138
+ @property
139
+ def final_rise(self) -> Optional[SensorRiseInterface]:
140
+ """Returns the current sensor rise configuration of the sensor.
141
+
142
+ This configuration can then be adjusted and is directly used as a new configuration, as the object is
143
+ returned as call by reference.
144
+
145
+ Returns:
146
+ Optional[SensorRiseInterface]: An implementation of the SensorRiseInterface interface. This interface
147
+ represents the rise of the sensor function to the maximum sensor response when both barriers are open. Can
148
+ also return None, if no final rise is used.
149
+ """
150
+ return self.__final_rise
151
+
152
+ @final_rise.setter
153
+ def final_rise(self, final_rise: Optional[SensorRiseInterface]):
154
+ """Updates the sensor rise of the sensor.
155
+
156
+ Args:
157
+ final_rise (Optional[SensorRiseInterface]): An implementation of the SensorRiseInterface interface. This
158
+ interface represents the rise of the sensor function to the maximum sensor response when both barriers
159
+ are open. This rise is also shifted so that it fits appropriately with the open barriers. The default
160
+ value is None. If None is used, no rise is added to the sensor function.
161
+ """
162
+ if isinstance(final_rise, SensorRiseInterface) or final_rise is None:
163
+ self.__final_rise = final_rise
164
+ else:
165
+ raise ValueError(
166
+ "The provided final_rise is not supported. Must be an implementation of the SensorPeakInterface or None."
167
+ )
168
+
169
+ @property
170
+ def barrier_functions(self) -> Tuple[BarrierFunctionInterface, BarrierFunctionInterface]:
171
+ """This function returns the barrier functions.
172
+
173
+ Returns:
174
+ Tuple[BarrierFunctionInterface, BarrierFunctionInterface]: Current barrier functions.
175
+ """
176
+ return self.__barrier_functions
177
+
178
+ @barrier_functions.setter
179
+ def barrier_functions(self, barrier_functions: Union[
180
+ BarrierFunctionInterface, Tuple[BarrierFunctionInterface, BarrierFunctionInterface]]):
181
+ """This function updates the current barrier functions.
182
+ Either one or two barrier function must be passed. If two functions are passed the first is the barrier function
183
+ for the barrier 1 and the second for barrier 2. When only one function is passed the barrier function is used
184
+ for both barriers.
185
+
186
+ Args:
187
+ barrier_functions (Union[BarrierFunctionInterface, Tuple[BarrierFunctionInterface, BarrierFunctionInterface]]):
188
+ New barrier functions. This can either be a single barrier function that is then used for both barriers,
189
+ or a tuple of two barrier functions. When a tuple is passed, the first element is the barrier function
190
+ for the first gate and so on for the second element.
191
+ """
192
+ if isinstance(barrier_functions, BarrierFunctionInterface):
193
+ self.__barrier_functions = [deepcopy(barrier_functions), deepcopy(barrier_functions)]
194
+ elif isinstance(barrier_functions, (list, tuple)) and all(
195
+ isinstance(x, BarrierFunctionInterface) for x in barrier_functions) and len(barrier_functions) == 2:
196
+ self.__barrier_functions = barrier_functions
197
+ else:
198
+ raise ValueError(
199
+ "The provided barrier_functions are invalid. The barrier functions must be a tuple with two "
200
+ "BarrierFunctionInterface objects or a single BarrierFunctionInterface object."
201
+ )
202
+
203
+ @property
204
+ def alpha_dot(self) -> np.ndarray:
205
+ """Returns the current alpha dot (dot lever-arms) configuration of the sensor.
206
+ With these, the influence of the occupation of the two dots on the three potentials (sensor dot, barrier1, and
207
+ barrier2) is specified. The lever-arms are specified as follows np.array([lever-arms sensor dot, lever-arms
208
+ barrier1, lever-arms barrier2]). The individual three lever-arms are each specified as a numpy array with two
209
+ elements. The first value describes the influence of the electron occupation of the first dot on the
210
+ corresponding potential and the second the influence of the electron occupation of the second dot. The values
211
+ should be negative.
212
+
213
+ Returns:
214
+ np.ndarray: The three pairs of alpha dot lever-arms as a numpy array. The numpy array has the shape (3,2).
215
+ """
216
+ return self.__alpha_dot
217
+
218
+ @alpha_dot.setter
219
+ def alpha_dot(self, alpha_dot: np.ndarray):
220
+ """Updates the alpha dot (dot lever-arms) configuration of the sensor.
221
+
222
+ With these the influence of the occupations to the three potentials (sensor dot, barrier1, and barrier2)
223
+ is specified.
224
+
225
+ Args:
226
+ alpha_dot (np.ndarray): Lever-arms to the dots. With these, the influence of the occupation of the two dots
227
+ on the three potentials (sensor dot, barrier1, and barrier2) is specified. This should be a numpy array
228
+ with shape (3,2), (2,) or None. The values should be negative.
229
+ For an array with the shape (3,2) the array is specified as follows: np.array([lever-arms sensor dot,
230
+ lever-arms barrier1, lever-arms barrier2]). The sub arrays contain the influence of both dots on one of
231
+ the potentials. For an array with the shape (2,) the same lever-arms are used for all three potentials.
232
+ If None is passed its assumed that all alpha dot lever-arms are zero.
233
+
234
+ Raises:
235
+ ValueError: A ValueError is raised if the passed alpha_dot is invalid. The alpha_dot is invalid when a numpy
236
+ array with a shape other than (2,) or (3, 2) or None is provided, or when any datatype other than a
237
+ numpy array or None is passed.
238
+ """
239
+ # check if alpha_dot is passed in an acceptable format
240
+ if isinstance(alpha_dot, np.ndarray):
241
+ if alpha_dot.shape == (2,):
242
+ self.__alpha_dot = np.array([alpha_dot, alpha_dot, alpha_dot])
243
+ return
244
+ elif alpha_dot.shape == (3, 2):
245
+ self.__alpha_dot = alpha_dot
246
+ return
247
+ elif alpha_dot is None:
248
+ self.__alpha_dot = np.array([[0, 0], [0, 0], [0, 0]])
249
+ return
250
+
251
+ # Raise error if none of the cases for valid alpha_dot formats from before is true
252
+ raise ValueError("The provided alpha_dot configuration is not supported. Must be None or a numpy array of "
253
+ "shape (2,) or (3,2).")
254
+
255
+ @property
256
+ def alpha_gate(self) -> np.ndarray:
257
+ """Returns the alpha gate (gate lever-arms) configuration of the sensor.
258
+ Lever-arms of the double dot (plunger) gates to the three potentials (sensor dot,
259
+ barrier1, and barrier2). The values should be positive. The lever-arms are specified as follows
260
+ np.array([lever-arms sensor dot, lever-arms barrier1, lever-arms barrier2]). The individual three
261
+ lever-arms are each specified as a numpy array with two elements. The first value describes the
262
+ influence of the voltage of the (plunger) gate of the first dot on the potential, with the second value
263
+ defined analogously for the (plunger) gate of the second dot.
264
+
265
+ Returns:
266
+ Returns the three pairs of alpha gate lever-arms as a numpy array. The numpy array has the shape (3,2).
267
+ """
268
+ return self.__alpha_gate
269
+
270
+ @alpha_gate.setter
271
+ def alpha_gate(self, alpha_gate: np.ndarray):
272
+ """Updates the alpha gate (gate lever-arms) configuration of the sensor.
273
+
274
+ With these the influence of the plunger gates to the three potentials (sensor dot, barrier1, and barrier2)
275
+ is specified.
276
+
277
+ Args:
278
+ alpha_gate (np.ndarray): Lever-arms of the double dot (plunger) gates to the three potentials (sensor dot,
279
+ barrier1, and barrier2). All alpha gate values should be positive. This should be a numpy array with
280
+ shape (3,2) or (2,) or None. For an array with the shape (3,2) the array is specified as follows:
281
+ np.array([lever-arms sensor dot, lever-arms barrier1, lever-arms barrier2]). The sub arrays contain the
282
+ influence of both gates on one of the potentials. For an array with the shape (2,) the same lever-arms
283
+ are used for all three potentials. If None is passed its assumed that all alpha dot lever-arms are zero.
284
+
285
+ Raises:
286
+ ValueError: A ValueError is raised if the passed alpha_gate is invalid. The alpha_gate is invalid when a
287
+ numpy array with a shape other than (2,) or (3, 2) or None is provided, or when any datatype other than
288
+ a numpy array or None is passed.
289
+ """
290
+ # check if alpha_gate is passed in an acceptable format
291
+ if isinstance(alpha_gate, np.ndarray):
292
+ if alpha_gate.shape == (2,):
293
+ self.__alpha_gate = np.array([alpha_gate, alpha_gate, alpha_gate])
294
+ return
295
+ elif alpha_gate.shape == (3, 2):
296
+ self.__alpha_gate = alpha_gate
297
+ return
298
+ elif alpha_gate is None:
299
+ self.__alpha_gate = np.array([[0, 0], [0, 0], [0, 0]])
300
+ return
301
+
302
+ # Raise error if none of the cases for valid alpha_gate formats from before is true
303
+ raise ValueError("The provided alpha_gate configuration is not supported. Must be None or a numpy array of "
304
+ "shape (2,) or (3,2).")
305
+
306
+ @property
307
+ def alpha_sensor_gate(self) -> np.ndarray:
308
+ """Returns the alpha sensor gate (sensor gate lever-arms) configuration of the sensor.
309
+ With theses the influence of the sensor dot gates to the three potentials (sensor dot, barrier1, and barrier2)
310
+ is specified. The values should be positive. The lever-arms are specified as follows np.array([lever-arms sensor
311
+ dot, lever-arms barrier1,lever-arms barrier2]). The individual three lever-arms are each specified as a numpy
312
+ array with two elements. The first value describes the influence of the voltage of the first gate of the sensor
313
+ dot to the potential and analogously for the second.
314
+
315
+ Returns:
316
+ Returns the three pairs of alpha sensor gate lever-arms as a numpy array. The numpy array has the shape
317
+ (3,2).
318
+ """
319
+ return self.__alpha_sensor_gate
320
+
321
+ @alpha_sensor_gate.setter
322
+ def alpha_sensor_gate(self, alpha_sensor_gate: np.ndarray):
323
+ """Method to update the current alpha sensor gate (sensor gate lever-arms) configuration.
324
+
325
+ With these the influence of the sensor dot gates to the three potentials (sensor dot, barrier1, and barrier2)
326
+ is specified.
327
+
328
+ Args:
329
+ alpha_sensor_gate (np.ndarray): Lever-arms of the sensor dot (barrier) gates to the three potentials
330
+ (sensor dot, barrier1, and barrier2). All alpha sensor gate values should be positive. This should be a
331
+ numpy array with shape (3,2). The lever-arms are specified as follows:
332
+ np.array([lever-arms sensor dot, lever-arms barrier1, lever-arms barrier2]). The sub arrays contain the
333
+ influence of gates on one of the potentials.
334
+
335
+ Raises:
336
+ ValueError: A ValueError is raised if the passed alpha_sensor_gate is invalid. The alpha_sensor_gate is
337
+ invalid when a numpy array with a shape other than (3, 2) is provided or when any datatype other than
338
+ a numpy array is passed.
339
+ """
340
+ # check if alpha_sensor_gate is passed in an acceptable format
341
+ if isinstance(alpha_sensor_gate, np.ndarray) and alpha_sensor_gate.shape == (3, 2):
342
+ self.__alpha_sensor_gate = alpha_sensor_gate
343
+ return
344
+
345
+ # Raise error if none of the cases for valid alpha_sensor_gate formats from before is true
346
+ raise ValueError(
347
+ "The provided alpha_sensor_gate configuration is not supported. Must be None or a numpy array of shape "
348
+ "(3,2).")
349
+
350
+ @property
351
+ def offset_mu_sens(self) -> np.ndarray:
352
+ """Returns the current offset_mu_sens configuration of the sensor.
353
+
354
+ This configuration can then be adjusted and set as new configuration.
355
+
356
+ Returns:
357
+ np.ndarray: Electrochemical potential offset of the sensor dot and both barriers for zero electrons in the
358
+ dots and no applied voltage at the gates. The first element contains the sensor dot offset, the second the
359
+ offset of the potential of barrier 1 and last the offset of barrier 2
360
+ """
361
+ return self.__offset_mu_sens
362
+
363
+ @offset_mu_sens.setter
364
+ def offset_mu_sens(self, offset_mu_sens: Union[float, np.ndarray]):
365
+ """Updates the offset_mu_sens configuration of the sensor according to the supplied value.
366
+
367
+ Args:
368
+ offset_mu_sens (Union[float, np.ndarray]): Electrochemical potential offset of the sensor dot and both
369
+ barriers for zero electrons in the dots and no applied voltage at the gates. If a float is passed the
370
+ same offset is used for all three potentials. Otherwise, if a numpy array is passed, the first element
371
+ contains the sensor dot offset, the second the offset of the potential of barrie 1 and the last that of
372
+ barrier 2.
373
+ """
374
+ # check datatype of offset_mu_sens
375
+ if isinstance(offset_mu_sens, np.ndarray) and offset_mu_sens.shape == (3,):
376
+ self.__offset_mu_sens = offset_mu_sens
377
+ elif isinstance(offset_mu_sens, (float, int)):
378
+ self.__offset_mu_sens = np.array([offset_mu_sens, offset_mu_sens, offset_mu_sens])
379
+ else:
380
+ raise ValueError("The provided offset_mu_sens configuration is not supported. Must either be a float value "
381
+ "or a numpy array of shape (3,).")
382
+
383
+ def __repr__(self):
384
+ return (
385
+ self.__class__.__name__
386
+ + f"(barrier_functions={self.barrier_functions},sensor_peak_function={self.__sensor_peak_function}, "
387
+ f"alpha_sensor_gate={repr(self.alpha_sensor_gate)}, alpha_gate={repr(self.alpha_gate)}, "
388
+ f"alpha_dot={repr(self.alpha_dot)}, offset_mu_sens={repr(self.__offset_mu_sens)}, "
389
+ f"final_rise={self.__final_rise}, sensor_peak_deformations={self.sensor_peak_deformations})"
390
+ )
391
+
392
+ def _peak_func(self,
393
+ mu_sens: Union[float, np.ndarray],
394
+ volt_g1: Union[float, np.ndarray],
395
+ volt_g2: Union[float, np.ndarray],
396
+ volt_sensor_g1: Union[float, np.ndarray],
397
+ volt_sensor_g2: Union[float, np.ndarray],
398
+ middle_line_point: Tuple[float, float]
399
+ ) -> np.ndarray:
400
+ """Help method to calculate the sensor response itself if deformations are applied.
401
+
402
+ This method calculates the distance between the point for which the response is to be calculated and the middle
403
+ line. This distance is then used to calculate a deformed potential for each peak. This is then used to calculate
404
+ the sensor response for each peak, which is added up. The middle line follows the direction of the sensor peaks
405
+ (i.e., the sensor dot potential) and is perpendicular to the wavefronts defined by the peaks.
406
+
407
+ The arrays `mu_sens`, `volt_x`, `volt_y`, `volt_x_sensor` and `volt_x_sensor` must have the same data type and
408
+ if they are numpy arrays they all must have the same dimension.
409
+
410
+ Args:
411
+ mu_sens (Union[float, np.ndarray]): The sensor potential, passed either as a 2-dimensional numpy array
412
+ with the axis mapping to the scan axis or as a float.
413
+ volt_g1 (Union[float, np.ndarray]): Voltages for each pixel applied to the gate g1. The voltages are either
414
+ passed as a 2-dimensional numpy array or as a float.
415
+ volt_g2 (Union[float, np.ndarray]): Voltages for each pixel applied to the gate g2. The voltages are either
416
+ passed as a 2-dimensional numpy array or as a float.
417
+ volt_sensor_g1 (Union[float, np.ndarray]): Voltages for each pixel applied to the gate sensor g1. The
418
+ voltages are either passed as a 2-dimensional numpy array or as a float.
419
+ volt_sensor_g2 (Union[float, np.ndarray]): Voltages for each pixel applied to the gate sensor g2. The
420
+ voltages are either passed as a 2-dimensional numpy array or as a float.
421
+ middle_line_point (Tuple[float, float]): The base point of the middle line. This point is specified as a
422
+ tuple with two floats.
423
+
424
+ Returns:
425
+ Union[float, np.ndarray]: The response of the sensor itself.
426
+ """
427
+ if self.__sensor_peak_function is None:
428
+ return mu_sens
429
+ else:
430
+ m = self.alpha_sensor_gate[0, 1] / self.alpha_sensor_gate[0, 0]
431
+
432
+ resp_sum = 0
433
+
434
+ dist = signed_dist_points_line(points=np.array([[volt_sensor_g1, volt_sensor_g2]]),
435
+ line_points=np.array(
436
+ [middle_line_point, middle_line_point + np.array([1, m])]))[0]
437
+
438
+ for idx, p in enumerate(self.__sensor_peak_function):
439
+ old_mu0 = p.mu0
440
+ if idx in self.sensor_peak_deformations.keys():
441
+ p.mu0 = self.sensor_peak_deformations[idx].calc_mu(dist=dist, mu0=p.mu0)
442
+ resp_sum += p.sensor_function(mu_sens)
443
+ p.mu0 = old_mu0
444
+ else:
445
+ resp_sum += p.sensor_function(mu_sens)
446
+
447
+ return resp_sum
448
+
449
+ def sensor_response(self, mu_sens: np.ndarray) -> np.ndarray:
450
+ """This function returns the sensor response for a given electrochemical potential.
451
+
452
+ Args:
453
+ mu_sens (np.ndarray): The given sensor potential.
454
+
455
+ Returns:
456
+ np.ndarray: The response, calculated from the given potential. It is stored in a numpy array with the axis
457
+ mapping to the CSD axis. For a two-dimensional scan the response is a two-dimensional numpy array and
458
+ for a one-dimensional scan it is a one-dimensional numpy array.
459
+ """
460
+ sensor_potential, barrier1_potential, barrier2_potential = deepcopy(mu_sens)
461
+
462
+ # check if it is needed to apply peak deformations
463
+ apply_deformations = len(self.sensor_peak_deformations) != 0
464
+
465
+ sweep_only_sensor = self._volt_limits_g1[0] == self._volt_limits_g1[-1] and \
466
+ self._volt_limits_g2[0] == self._volt_limits_g2[-1]
467
+ sensor_swept = self._volt_limits_g1[0] == self._volt_limits_g1[-1] or \
468
+ self._volt_limits_g2[0] == self._volt_limits_g2[-1]
469
+
470
+ if apply_deformations and sweep_only_sensor:
471
+ # Calculate sigma when deformations have to be applied
472
+ sens_resp = np.zeros_like(sensor_potential)
473
+
474
+ # Calculation of the middle point for deformations
475
+ middle_point = self._calc_point_from_barrier_potentials(
476
+ pot_bar1=self.barrier_functions[0].pinch_off,
477
+ pot_bar2=self.barrier_functions[1].pinch_off,
478
+ sensor_swept=sensor_swept
479
+ )
480
+
481
+ if sensor_potential.ndim == 1:
482
+ for pixel_idx in range(sensor_potential.shape[0]):
483
+ voltage_y = pixel_to_volt_1d(pixel=pixel_idx, pixel_num=sensor_potential.shape[0] - 1,
484
+ volt_limits=self._volt_limits_g2)
485
+ voltage_y_sensor = pixel_to_volt_1d(pixel=pixel_idx, pixel_num=sensor_potential.shape[0] - 1,
486
+ volt_limits=self._volt_limits_sensor_g2)
487
+ voltage_x = pixel_to_volt_1d(pixel=pixel_idx, pixel_num=sensor_potential.shape[0] - 1,
488
+ volt_limits=self._volt_limits_g1)
489
+ voltage_x_sensor = pixel_to_volt_1d(pixel=pixel_idx, pixel_num=sensor_potential.shape[0] - 1,
490
+ volt_limits=self._volt_limits_sensor_g1)
491
+ sens_resp[pixel_idx] = self._peak_func(mu_sens=sensor_potential[pixel_idx], volt_g1=voltage_x,
492
+ volt_g2=voltage_y, volt_sensor_g1=voltage_x_sensor,
493
+ volt_sensor_g2=voltage_y_sensor,
494
+ middle_line_point=middle_point)
495
+
496
+ elif sensor_potential.ndim == 2:
497
+ for y in range(sensor_potential.shape[0]):
498
+ voltage_y = pixel_to_volt_1d(pixel=y, pixel_num=sensor_potential.shape[0] - 1,
499
+ volt_limits=self._volt_limits_g2)
500
+ voltage_y_sensor = pixel_to_volt_1d(pixel=y, pixel_num=sensor_potential.shape[0] - 1,
501
+ volt_limits=self._volt_limits_sensor_g2)
502
+ for x in range(sensor_potential.shape[1]):
503
+ voltage_x = pixel_to_volt_1d(pixel=x, pixel_num=sensor_potential.shape[1] - 1,
504
+ volt_limits=self._volt_limits_g1)
505
+ voltage_x_sensor = pixel_to_volt_1d(pixel=x, pixel_num=sensor_potential.shape[1] - 1,
506
+ volt_limits=self._volt_limits_sensor_g1)
507
+ sens_resp[y, x] = self._peak_func(mu_sens=sensor_potential[y, x],
508
+ volt_g1=voltage_x,
509
+ volt_g2=voltage_y,
510
+ volt_sensor_g1=voltage_x_sensor,
511
+ volt_sensor_g2=voltage_y_sensor,
512
+ middle_line_point=middle_point)
513
+
514
+ else:
515
+ raise ValueError("Invalid dimension of the sensor potential")
516
+
517
+ else:
518
+ if apply_deformations and not sweep_only_sensor:
519
+ warnings.warn(
520
+ "Deformations can only swept when only the sensor gates are swept! Currently at least one "
521
+ "different gate is swept.")
522
+
523
+ # Quicker way to calculate sens_resp when no deformations have to be applied
524
+ if self.__sensor_peak_function is None:
525
+ sens_resp = sensor_potential
526
+ else:
527
+ sens_resp = np.sum([p.sensor_function(sensor_potential) for p in self.__sensor_peak_function], axis=0)
528
+
529
+ if self.final_rise is not None:
530
+ # Calculation of the middle point
531
+ g1_voltage = self._volt_limits_g1[0]
532
+ g2_voltage = self._volt_limits_g2[0]
533
+
534
+ if self._occupations.ndim == 2:
535
+ occ = (self._occupations[0, 0], self._occupations[0, 1])
536
+ elif self._occupations.ndim == 3:
537
+ occ = (self._occupations[0, 0, 0], self._occupations[0, 0, 1])
538
+ else:
539
+ raise ValueError(
540
+ "Invalid dimension of occupations! The dimension of the occupations must be one larger than "
541
+ "the scan dimension")
542
+
543
+ fully_conductive_point = self._calc_point_from_barrier_potentials(
544
+ pot_bar1=self.barrier_functions[0].fully_conductive,
545
+ pot_bar2=self.barrier_functions[1].fully_conductive,
546
+ sensor_swept=sensor_swept
547
+ )
548
+
549
+ fully_conductive_potential = self.sensor_potential(
550
+ occupations=np.array((occ,)),
551
+ volt_limits_g1=g1_voltage,
552
+ volt_limits_g2=g2_voltage,
553
+ volt_limits_sensor_g1=fully_conductive_point[0],
554
+ volt_limits_sensor_g2=fully_conductive_point[1],
555
+ )
556
+
557
+ final_rise_resp = self.final_rise.sensor_function(sensor_potential,
558
+ offset=fully_conductive_potential[0])
559
+
560
+ sens_resp = np.maximum(sens_resp, final_rise_resp)
561
+
562
+ barrier1_resp = self.barrier_functions[0].get_value(barrier1_potential)
563
+ barrier2_resp = self.barrier_functions[1].get_value(barrier2_potential)
564
+
565
+ if np.min(sens_resp) < 0 or np.min(barrier1_resp) < 0 or np.min(barrier2_resp) < 0:
566
+ warnings.warn("At least one of the three responses is negative. This will probably not produce a realistic "
567
+ "scan! The minima of the barrier and sensor peak functions should be 0.")
568
+
569
+ with warnings.catch_warnings():
570
+ warnings.simplefilter("ignore")
571
+ # combine the individual response values -> they are combined like resistors in a series circuit
572
+ numerator = sens_resp * barrier1_resp * barrier2_resp
573
+ denominator = barrier1_resp * barrier2_resp + sens_resp * barrier1_resp + sens_resp * barrier2_resp
574
+ return np.divide(numerator, denominator, out=np.zeros_like(numerator), where=denominator != 0)
575
+
576
+ def _calc_point_from_barrier_potentials(
577
+ self,
578
+ pot_bar1: float,
579
+ pot_bar2: float,
580
+ sensor_swept: bool = True
581
+ ) -> Tuple[float, float]:
582
+ """Calculate a point in voltage space from given barrier potentials.
583
+
584
+ This function calculates the corresponding point in gate voltage space for given barrier potentials, taking into
585
+ account voltage limits, gate influences, and occupations. The method applies potential offsets and converts
586
+ barrier potentials to gate voltages.
587
+
588
+ Args:
589
+ pot_bar1 (float): Target potential for barrier 1.
590
+ pot_bar2 (float): Target potential for barrier 2.
591
+ sensor_swept (bool): Boolean that indicates whether the sensor gates are swept and a sensor scan is
592
+ simulated or gates g1 and g2 are swept and a CSD is simulated. The boolean is true when a sensor scan is
593
+ simulated and false otherwise.
594
+
595
+ Returns:
596
+ Tuple[float, float]: The calculated point coordinates (g1, g2) in gate voltage space.
597
+ """
598
+
599
+ if self._occupations.ndim == 2:
600
+ occ = (self._occupations[0, 0], self._occupations[0, 1])
601
+ elif self._occupations.ndim == 3:
602
+ occ = (self._occupations[0, 0, 0], self._occupations[0, 0, 1])
603
+ else:
604
+ raise ValueError(
605
+ "Invalid dimension of occupations! The dimension of the occupations must be one larger than "
606
+ "the scan dimension")
607
+
608
+ # Create copies to avoid modifying original values
609
+ pot_bar1 = float(pot_bar1)
610
+ pot_bar2 = float(pot_bar2)
611
+
612
+ if sensor_swept:
613
+ voltages = np.array([self._volt_limits_g1[0], self._volt_limits_g2[0]])
614
+ gate_offset = np.dot(self.alpha_gate[1:], voltages)
615
+ else:
616
+ voltages = np.array([self._volt_limits_sensor_g1[0], self._volt_limits_sensor_g1[0]])
617
+ gate_offset = np.dot(self.alpha_sensor_gate[1:], voltages)
618
+
619
+ # Calculate potential offsets for both barriers
620
+ offset1 = (gate_offset[0] +
621
+ self.offset_mu_sens[1] +
622
+ self.alpha_dot[1, 0] * occ[0] +
623
+ self.alpha_dot[1, 1] * occ[0])
624
+
625
+ offset2 = (gate_offset[1] +
626
+ self.offset_mu_sens[2] +
627
+ self.alpha_dot[2, 0] * occ[0] +
628
+ self.alpha_dot[2, 1] * occ[1])
629
+
630
+ # Apply offsets to barrier potentials
631
+ pot_bar1 -= offset1
632
+ pot_bar2 -= offset2
633
+
634
+ # Calculate point from potential (inline implementation)
635
+ if np.abs(self.alpha_sensor_gate[1, 1]) < sys.float_info.epsilon: # case: slope of gate 1 line is infinity
636
+ v1_pinch_off = pot_bar1 / self.alpha_sensor_gate[1, 0]
637
+ v2_pinch_off_left = ((-self.alpha_sensor_gate[2, 0] * v1_pinch_off + pot_bar2) /
638
+ self.alpha_sensor_gate[2, 1])
639
+ point = (v1_pinch_off, v2_pinch_off_left)
640
+ else:
641
+ v1_pinch_off = (((pot_bar2) * self.alpha_sensor_gate[1, 1] -
642
+ (pot_bar1) * self.alpha_sensor_gate[2, 1]) /
643
+ (self.alpha_sensor_gate[2, 0] * self.alpha_sensor_gate[1, 1] -
644
+ self.alpha_sensor_gate[1, 0] * self.alpha_sensor_gate[2, 1]))
645
+ v2_pinch_off_left = ((-self.alpha_sensor_gate[1, 0] * v1_pinch_off + pot_bar1) /
646
+ self.alpha_sensor_gate[1, 1])
647
+ point = (v1_pinch_off, v2_pinch_off_left.item())
648
+
649
+ return point
650
+
651
+ def sensor_potential(self,
652
+ occupations: np.ndarray,
653
+ volt_limits_g1: Union[np.ndarray, float, None] = None,
654
+ volt_limits_g2: Union[np.ndarray, float, None] = None,
655
+ volt_limits_sensor_g1: Union[np.ndarray, float, None] = None,
656
+ volt_limits_sensor_g2: Union[np.ndarray, float, None] = None
657
+ ) -> np.ndarray:
658
+ """Calculates the electrochemical potential at the sensor dot and both barriers.
659
+
660
+ This is done in dependency on the electron occupation in the double dot, the voltages applied at the double
661
+ dot (plunger) gates, and voltages applied at the gates of sensor.
662
+ Either the double dot gates or the sensor gates can be swept.
663
+
664
+ Args:
665
+ occupations (np.ndarray): Occupation in left and right dot per applied voltage combination. The occupation
666
+ numbers are stored in a 3-dimensional numpy array. The first two dimensions map to the axis of the CSD,
667
+ while the third dimension indicates the dot of the corresponding occupation value.
668
+ volt_limits_g1 (Union[np.ndarray, float, None]): Voltages applied to the first (plunger) gate of the double
669
+ dot. When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy
670
+ array with the minimum and maximum of the sweep.
671
+ volt_limits_g2 (Union[np.ndarray, float, None]): Voltages applied to the second (plunger) gate of the double
672
+ dot. When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy
673
+ array with the minimum and maximum of the sweep.
674
+ volt_limits_sensor_g1 (Union[np.ndarray, float, None]): Voltages applied to the first sensor gate.
675
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
676
+ with the minimum and maximum of the sweep.
677
+ volt_limits_sensor_g2 (Union[np.ndarray, float, None]): voltages applied to the second sensor gate.
678
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
679
+ with the minimum and maximum of the sweep.
680
+
681
+ Returns:
682
+ np.ndarray: The electrochemical potential of the sensor dot, and both barrier. It is returned as a
683
+ three-dimensional array with the shape (3, occupations.shape[0], occupations.shape[1]). The first dimension
684
+ corresponds to the three potentials involved. It is structured as follows:
685
+ [sensor dot potential, barrier 1 potential, barrier 2 potential]
686
+ The remaining two dimensions correspond to the swept voltages.
687
+ """
688
+ if not occupations.ndim in [2, 3]:
689
+ raise ValueError("The occupations matrix has to have either two or three dimensions.")
690
+
691
+ if len(self.sensor_peak_deformations) > 0:
692
+ if isinstance(volt_limits_g1, np.ndarray) or isinstance(volt_limits_g2, np.ndarray):
693
+ warnings.warn("Deformations should only be used with sweeps of sensor gates! But at least one of "
694
+ "plunger gates is sweept!")
695
+
696
+ if (isinstance(volt_limits_g1, np.ndarray) or isinstance(volt_limits_g2, np.ndarray)) and (
697
+ (isinstance(volt_limits_sensor_g1, np.ndarray) or isinstance(volt_limits_sensor_g2, np.ndarray))):
698
+ raise ValueError("Either only the plunger gates of the double quantum dots or the gates of the sensor dots "
699
+ "can be swept. Both types of gates cannot be swept simultaneously.")
700
+
701
+ volt_limits_list = [volt_limits_g1, volt_limits_g2, volt_limits_sensor_g1, volt_limits_sensor_g2]
702
+ volt_limits_list = copy.deepcopy(volt_limits_list)
703
+ for i in range(len(volt_limits_list)):
704
+ if volt_limits_list[i] is None:
705
+ volt_limits_list[i] = np.array([0, 0])
706
+ elif isinstance(volt_limits_list[i], numbers.Real):
707
+ volt_limits_list[i] = np.array([volt_limits_list[i], volt_limits_list[i]])
708
+ volt_limits_g1, volt_limits_g2, volt_limits_sensor_g1, volt_limits_sensor_g2 = volt_limits_list
709
+
710
+ self._volt_limits_g1 = volt_limits_g1
711
+ self._volt_limits_g2 = volt_limits_g2
712
+ self._volt_limits_sensor_g1 = volt_limits_sensor_g1
713
+ self._volt_limits_sensor_g2 = volt_limits_sensor_g2
714
+ self._occupations = occupations
715
+
716
+ # Calculation of the potentials
717
+ # Voltage matrix for 2D scans
718
+ if occupations.ndim == 3:
719
+ voltages_g1 = np.linspace(volt_limits_g1[0], volt_limits_g1[1], num=occupations.shape[1])
720
+ voltages_g2 = np.linspace(volt_limits_g2[0], volt_limits_g2[1], num=occupations.shape[0])
721
+ voltages = [
722
+ [[voltages_g1[j], voltages_g2[i]] for j in range(len(voltages_g1))] for i in range(len(voltages_g2))
723
+ ]
724
+ voltages = np.array(voltages)
725
+
726
+ voltages_sensor_g1 = np.linspace(volt_limits_sensor_g1[0], volt_limits_sensor_g1[1],
727
+ num=occupations.shape[1])
728
+ voltages_sensor_g2 = np.linspace(volt_limits_sensor_g2[0], volt_limits_sensor_g2[1],
729
+ num=occupations.shape[0])
730
+ voltages_sensor = [
731
+ [[voltages_sensor_g1[j], voltages_sensor_g2[i]] for j in range(len(voltages_sensor_g1))] for i in
732
+ range(len(voltages_sensor_g2))
733
+ ]
734
+ voltages_sensor = np.array(voltages_sensor)
735
+ # Voltage matrix for 1D scans
736
+ elif occupations.ndim == 2:
737
+ voltages_g1 = np.linspace(volt_limits_g1[0], volt_limits_g1[1], num=occupations.shape[0])
738
+ voltages_g2 = np.linspace(volt_limits_g2[0], volt_limits_g2[1], num=occupations.shape[0])
739
+ voltages = [[voltages_g1[i], voltages_g2[i]] for i in range(len(voltages_g1))]
740
+ voltages = np.array(voltages)
741
+
742
+ voltages_sensor_g1 = np.linspace(volt_limits_sensor_g1[0], volt_limits_sensor_g1[1],
743
+ num=occupations.shape[0])
744
+ voltages_sensor_g2 = np.linspace(volt_limits_sensor_g2[0], volt_limits_sensor_g2[1],
745
+ num=occupations.shape[0])
746
+ voltages_sensor = [[voltages_sensor_g1[i], voltages_sensor_g2[i]] for i in range(len(voltages_sensor_g1))]
747
+ voltages_sensor = np.array(voltages_sensor)
748
+
749
+ with warnings.catch_warnings():
750
+ warnings.simplefilter("ignore")
751
+ mu_sens = (occupations.dot(self.__alpha_dot[0]) + voltages.dot(self.__alpha_gate[0]) +
752
+ voltages_sensor.dot(self.alpha_sensor_gate[0]) + self.offset_mu_sens[0])
753
+ barrier1_pot = (occupations.dot(self.__alpha_dot[1]) + voltages.dot(self.__alpha_gate[1]) +
754
+ voltages_sensor.dot(self.alpha_sensor_gate[1]) + self.offset_mu_sens[1])
755
+ barrier2_pot = (occupations.dot(self.__alpha_dot[2]) + voltages.dot(self.__alpha_gate[2]) +
756
+ voltages_sensor.dot(self.alpha_sensor_gate[2]) + self.offset_mu_sens[2])
757
+
758
+ return np.array([mu_sens, barrier1_pot, barrier2_pot])
759
+
760
+ def get_sensor_scan_labels(self,
761
+ volt_limits_g1: Union[np.ndarray, float, None],
762
+ volt_limits_g2: Union[np.ndarray, float, None],
763
+ volt_limits_sensor_g1: Union[np.ndarray, float, None],
764
+ volt_limits_sensor_g2: Union[np.ndarray, float, None],
765
+ potential: np.ndarray) -> (
766
+ np.ndarray, np.ndarray):
767
+ """This method returns the labels of the sensor scans.
768
+
769
+ There are two labels for sensor scans: the conductive area mask and the Coulomb peak mask. Both masks are numpy
770
+ arrays that consist of integers.
771
+ The conductive area mask marks the non-conductive area, sensor oscillation regime, and fully conductive area.
772
+ The non-conductive area is marked with 0. This is the area in which no electron can tunnel or flow through the
773
+ sensor dot. The sensor oscillation regime is the area in which the barriers are open enough for oscillations to
774
+ occur, as electrons tunnel periodically. This area is marked with 1. In the third area, the conductive area,
775
+ both barriers are fully open and transport occurs as a continuous current rather than through a well-defined
776
+ sensor dot. The fully conductive area is marked with 2.
777
+ The Coulomb peak mask marks the peaks of the Coulomb peak as integers. The wave fronts are marked with values
778
+ higher or equal to one. All maxima belonging to the same Coulomb peak are marked with the same integer.
779
+ Depending on the potential value, the various Coulomb peaks are marked with ascending values.
780
+
781
+ Args:
782
+ volt_limits_g1 (Union[np.ndarray, float, None]): Voltages applied to the first double dot (plunger) gate.
783
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
784
+ with the minimum and maximum of the sweep. None can also be passed if no voltage is applied.
785
+ Currently supports only scalar input (`float`) or `None`.
786
+ Array input is reserved for future sweep functionality.
787
+ volt_limits_g2 (Union[np.ndarray, float, None]): Voltages applied to the second double dot(plunger) gate.
788
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
789
+ with the minimum and maximum of the sweep. None can also be passed if no voltage is applied.
790
+ Currently supports only scalar input (`float`) or `None`.
791
+ Array input is reserved for future sweep functionality.
792
+ volt_limits_sensor_g1 (Union[np.ndarray, float, None]): Voltages applied to the first sensor gate.
793
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
794
+ with the minimum and maximum of the sweep. None can also be passed if no voltage is applied.
795
+ volt_limits_sensor_g2 (Union[np.ndarray, float, None]): Voltages applied to the second sensor gate.
796
+ When a fixed voltage is applied this is a float and when this gate should be swept it is a numpy array
797
+ with the minimum and maximum of the sweep. None can also be passed if no voltage is applied.
798
+ potential (np.ndarray): Numpy array that contains the potential of the area for which labels should be
799
+ generated. This potential must correspond to the voltages specified for volt_limits_g1,
800
+ volt_limits_g2, volt_limits_sensor_g1 and volt_limits_sensor_g2.
801
+
802
+ Returns:
803
+ (np.ndarray, np.ndarray): Tuple with the two numpy arrays of the two labels of sensor scan.
804
+ The returned tuple looks like: (conductive area mask, coulomb peak mask). Both arrays have the same shape
805
+ as the provided potential.
806
+ """
807
+
808
+ sensor_pot, barrier1_pot, barrier2_pot = potential
809
+
810
+ if self._occupations.ndim == 2:
811
+ occ = (self._occupations[0, 0], self._occupations[0, 1])
812
+ elif self._occupations.ndim == 3:
813
+ occ = (self._occupations[0, 0, 0], self._occupations[0, 0, 1])
814
+ else:
815
+ raise ValueError(
816
+ "Invalid dimension of occupations! The dimension of the occupations must be one larger than "
817
+ "the scan dimension")
818
+
819
+ apply_deformations = len(self.sensor_peak_deformations) != 0
820
+
821
+ if not ((volt_limits_g1 is None or isinstance(volt_limits_g1, float)) and
822
+ (volt_limits_g2 is None or isinstance(volt_limits_g2, float))):
823
+ raise ValueError("The volt limits have to be fixed values or None when sensor scan labels are generated!")
824
+
825
+ fully_conductive_point = self._calc_point_from_barrier_potentials(
826
+ pot_bar1=self.barrier_functions[0].fully_conductive,
827
+ pot_bar2=self.barrier_functions[1].fully_conductive
828
+ )
829
+
830
+ fully_conductive_potential = self.sensor_potential(
831
+ occupations=np.array((occ,)),
832
+ volt_limits_g1=volt_limits_g1,
833
+ volt_limits_g2=volt_limits_g1,
834
+ volt_limits_sensor_g1=fully_conductive_point[0],
835
+ volt_limits_sensor_g2=fully_conductive_point[1],
836
+ )
837
+
838
+ coulomb_peak_mask = np.zeros_like(sensor_pot, dtype=np.uint8)
839
+ conductive_mask = np.zeros_like(sensor_pot, dtype=np.uint8)
840
+
841
+ m = self.alpha_sensor_gate[0, 1] / self.alpha_sensor_gate[0, 0]
842
+
843
+ # Create array with same voltages tuples with the same resolution as the sensor potential
844
+ if potential.ndim == 3:
845
+ x_values = np.linspace(volt_limits_sensor_g1[0], volt_limits_sensor_g1[-1], sensor_pot.shape[1])
846
+ y_values = np.linspace(volt_limits_sensor_g2[0], volt_limits_sensor_g2[-1], sensor_pot.shape[0])
847
+ x_grid, y_grid = np.meshgrid(x_values, y_values)
848
+ xy_array = np.dstack((x_grid, y_grid))
849
+ if potential.ndim == 2:
850
+ x_values = np.linspace(volt_limits_sensor_g1[0], volt_limits_sensor_g1[1], num=sensor_pot.shape[0])
851
+ y_values = np.linspace(volt_limits_sensor_g2[0], volt_limits_sensor_g2[1], num=sensor_pot.shape[0])
852
+ xy_array = [[x_values[i], y_values[i]] for i in range(len(x_values))]
853
+ xy_array = np.array(xy_array)
854
+
855
+ # Reshape all points to (n,2) to use it with distance calculation
856
+ xy_array = xy_array.reshape(-1, 2) # Shape: (num_x_values * num_y_values, 2)
857
+
858
+ if apply_deformations:
859
+ middle_point = self._calc_point_from_barrier_potentials(
860
+ pot_bar1=self.barrier_functions[0].pinch_off,
861
+ pot_bar2=self.barrier_functions[1].pinch_off
862
+ )
863
+
864
+ # Calculate distances from the middle line
865
+ dist_array = signed_dist_points_line(points=xy_array,
866
+ line_points=np.array(
867
+ [middle_point, middle_point + np.array([1, m])]))
868
+
869
+ # Reshape dist_array to the initial shape (shape of the potential)
870
+ dist_array = dist_array.reshape(sensor_pot.shape)
871
+
872
+ all_peaks = copy.deepcopy(self.sensor_peak_function)
873
+ if isinstance(all_peaks, SensorPeakInterface):
874
+ all_peaks = [all_peaks]
875
+
876
+ # Sensor peaks are sorted according to their mu0 so that the increases in the masks function correctly
877
+ # Peaks with a smaller mu0 overwrite the range of the previous peaks. The peaks, relating to the mu0, must
878
+ # therefore be run through from large to small
879
+ peaks_sorted_with_id = sorted(zip(all_peaks, range(len(all_peaks))), key=lambda x: x[0].mu0)
880
+ peaks_sorted_with_id = list((idx, peak, id) for idx, (peak, id) in enumerate(peaks_sorted_with_id))
881
+
882
+ for idx, peak, peak_id in reversed(list(peaks_sorted_with_id)):
883
+ distorted_pot: np.ndarray
884
+ if idx in self.sensor_peak_deformations.keys():
885
+ deformation = self.sensor_peak_deformations[idx]
886
+ distorted_pot = sensor_pot + (
887
+ peak.mu0 - deformation.calc_mu(dist=dist_array, mu0=self.sensor_peak_function[idx].mu0))
888
+ else:
889
+ distorted_pot = sensor_pot
890
+
891
+ if potential.ndim == 3:
892
+ # 2d scan
893
+ diffs = np.diff(sensor_pot, axis=1)
894
+ average_diff = np.mean(diffs)
895
+ threshold = np.abs(average_diff)
896
+
897
+ # Caculation of the distance of to the middle of a peak
898
+ min_indices = np.argmin(np.abs(distorted_pot - peak.mu0), axis=1)
899
+
900
+ is_boundary = (min_indices == 0) | (min_indices == conductive_mask.shape[1] - 1)
901
+ row_indices = np.arange(conductive_mask.shape[0])
902
+ boundary_distances = np.abs(distorted_pot[row_indices, min_indices] - peak.mu0)
903
+
904
+ valid_mask = ~is_boundary | (boundary_distances < threshold)
905
+ coulomb_peak_mask[row_indices[valid_mask], min_indices[valid_mask]] = peak_id + 1
906
+
907
+ else:
908
+ # 1d scan
909
+ diffs = np.diff(sensor_pot)
910
+ average_diff = np.mean(diffs)
911
+ threshold = np.abs(average_diff)
912
+
913
+ # Caculation of the distance of to the middle of a peak
914
+ min_index = np.argmin(np.abs(distorted_pot - peak.mu0))
915
+
916
+ if min_index in [0, conductive_mask.shape[0] - 1]:
917
+ if np.abs(distorted_pot[min_index] - peak.mu0) < threshold:
918
+ coulomb_peak_mask[min_index] = peak_id + 1
919
+ else:
920
+ coulomb_peak_mask[min_index] = peak_id + 1
921
+
922
+ conductive_mask[((barrier1_pot > self.barrier_functions[0].pinch_off) & (
923
+ barrier2_pot > self.barrier_functions[1].pinch_off))] = 1
924
+
925
+ conductive_mask[(sensor_pot > fully_conductive_potential[0]) & conductive_mask == 1] = 2
926
+
927
+ coulomb_peak_mask[conductive_mask != 1] = 0
928
+
929
+ return conductive_mask, coulomb_peak_mask