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.
- simcats/__init__.py +4 -3
- simcats/_default_configs.py +129 -13
- simcats/_simulation.py +451 -69
- simcats/config_samplers/_GaAs_v1_random_variations_v3_config_sampler.py +1059 -0
- simcats/config_samplers/__init__.py +9 -0
- simcats/distortions/_distortion_interfaces.py +1 -1
- simcats/distortions/_dot_jumps.py +8 -6
- simcats/distortions/_random_telegraph_noise.py +4 -4
- simcats/distortions/_transition_blurring.py +5 -5
- simcats/distortions/_white_noise.py +2 -2
- simcats/ideal_csd/geometric/_generate_lead_transition_mask.py +3 -3
- simcats/ideal_csd/geometric/_get_electron_occupation.py +5 -5
- simcats/ideal_csd/geometric/_ideal_csd_geometric.py +5 -5
- simcats/ideal_csd/geometric/_ideal_csd_geometric_class.py +9 -9
- simcats/ideal_csd/geometric/_tct_bezier.py +5 -5
- simcats/sensor/__init__.py +10 -6
- simcats/sensor/{_generic_sensor.py → _sensor_generic.py} +1 -1
- simcats/sensor/_sensor_interface.py +164 -11
- simcats/sensor/_sensor_rise_glf.py +229 -0
- simcats/sensor/_sensor_scan_sensor_generic.py +929 -0
- simcats/sensor/barrier_function/__init__.py +9 -0
- simcats/sensor/barrier_function/_barrier_function_glf.py +280 -0
- simcats/sensor/barrier_function/_barrier_function_interface.py +43 -0
- simcats/sensor/barrier_function/_barrier_function_multi_glf.py +157 -0
- simcats/sensor/deformation/__init__.py +9 -0
- simcats/sensor/deformation/_sensor_peak_deformation_circle.py +109 -0
- simcats/sensor/deformation/_sensor_peak_deformation_interface.py +65 -0
- simcats/sensor/deformation/_sensor_peak_deformation_linear.py +77 -0
- simcats/support_functions/__init__.py +11 -3
- simcats/support_functions/_generalized_logistic_function.py +146 -0
- simcats/support_functions/_linear_algebra.py +171 -0
- simcats/support_functions/_parameter_sampling.py +108 -19
- simcats/support_functions/_pixel_volt_transformation.py +24 -0
- simcats/support_functions/_reset_offset_mu_sens.py +43 -0
- {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info}/METADATA +93 -29
- simcats-2.0.0.dist-info/RECORD +53 -0
- {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info}/WHEEL +1 -1
- simcats-1.1.0.dist-info/RECORD +0 -37
- /simcats/sensor/{_gaussian_sensor_peak.py → _sensor_peak_gaussian.py} +0 -0
- /simcats/sensor/{_lorentzian_sensor_peak.py → _sensor_peak_lorentzian.py} +0 -0
- {simcats-1.1.0.dist-info → simcats-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {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
|