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
simcats/_simulation.py CHANGED
@@ -2,19 +2,20 @@
2
2
  This module contains the simulation class, that can be used to perform charge stability diagram (CSD) simulations.
3
3
  Additionally, it provides default configurations for this class.
4
4
 
5
- @author: f.hader
5
+ @author: f.hader, b.papajewski
6
6
  """
7
7
 
8
8
  from copy import deepcopy
9
9
  from functools import partial
10
- from typing import Union, Tuple, Dict, List
10
+ from typing import Union, Tuple, Dict, List, Optional
11
+ import warnings
11
12
 
12
13
  import numpy as np
13
14
 
14
15
  from simcats.distortions import SensorResponseDistortionInterface, OccupationDistortionInterface, \
15
16
  SensorPotentialDistortionInterface
16
17
  from simcats.ideal_csd import IdealCSDInterface
17
- from simcats.sensor import SensorInterface, SensorGeneric
18
+ from simcats.sensor import SensorInterface, SensorGeneric, SensorScanSensorInterface
18
19
 
19
20
  __all__ = []
20
21
 
@@ -30,14 +31,16 @@ class Simulation:
30
31
  """
31
32
 
32
33
  def __init__(
33
- self,
34
- volt_limits_g1: Union[np.ndarray, None] = None,
35
- volt_limits_g2: Union[np.ndarray, None] = None,
36
- ideal_csd_config: Union[IdealCSDInterface, None] = None,
37
- sensor: SensorInterface = SensorGeneric(),
38
- occupation_distortions: Union[List[OccupationDistortionInterface], None] = None,
39
- sensor_potential_distortions: Union[List[SensorPotentialDistortionInterface], None] = None,
40
- sensor_response_distortions: Union[List[SensorResponseDistortionInterface], None] = None,
34
+ self,
35
+ volt_limits_g1: Optional[np.ndarray] = None,
36
+ volt_limits_g2: Optional[np.ndarray] = None,
37
+ volt_limits_sensor_g1: Optional[np.ndarray] = None,
38
+ volt_limits_sensor_g2: Optional[np.ndarray] = None,
39
+ ideal_csd_config: Optional[IdealCSDInterface] = None,
40
+ sensor: SensorInterface = SensorGeneric(),
41
+ occupation_distortions: Optional[List[OccupationDistortionInterface]] = None,
42
+ sensor_potential_distortions: Optional[List[SensorPotentialDistortionInterface]] = None,
43
+ sensor_response_distortions: Optional[List[SensorResponseDistortionInterface]] = None,
41
44
  ):
42
45
  """
43
46
  Initializes an object of the class to perform the simulation of charge stability diagrams (CSDs) for a double
@@ -48,37 +51,52 @@ class Simulation:
48
51
  smaller voltages, the distortions is also applied in this direction.
49
52
 
50
53
  Args:
51
- volt_limits_g1 (Union[np.ndarray, None]): Voltage limits of (plunger) gate 1 (second-/x-axis). Defines the
54
+ volt_limits_g1 (Optional[np.ndarray]): Voltage limits of (plunger) gate 1 (second-/x-axis). Defines the
52
55
  range in which data can be queried during the simulation. If set to None, all voltage values are
53
56
  allowed. This can potentially lead to problems, if the structures are not available for some regions
54
57
  and no restriction is applied. Default is None. \n
55
58
  Example: \n
56
59
  [min_V1, max_V1]
57
- volt_limits_g2 (Union[np.ndarray, None]): Voltage limits of (plunger) gate 2 (first-/y-axis). Defines the
60
+ volt_limits_g2 (Optional[np.ndarray]): Voltage limits of (plunger) gate 2 (first-/y-axis). Defines the
58
61
  range in which data can be queried during the simulation. If set to None, all voltage values are
59
62
  allowed. This can potentially lead to problems, if the structures are not available for some regions
60
63
  and no restriction is applied. Default is None. \n
61
64
  Example: \n
62
65
  [min_V2, max_V2]
63
- ideal_csd_config (Union[IdealCSDInterface, None]): Implementation of the IdealCSDInterface (from the
64
- ideal_csd module). Used to generate the ideal CSD data during the simulation. Default is None.
66
+ volt_limits_sensor_g1 (Optional[np.ndarray]): Voltage limits of sensor gate 1 (second-/x-axis). Defines the
67
+ range in which data can be queried during the simulation. If set to None, all voltage values are
68
+ allowed. This can potentially lead to problems, if the structures are not available for some regions
69
+ and no restriction is applied. Default is None. \n
70
+ Example: \n
71
+ [min_V1, max_V1]
72
+ volt_limits_sensor_g2 (Optional[np.ndarray]): Voltage limits of sensor gate 2 (first-/y-axis). Defines the
73
+ range in which data can be queried during the simulation. If set to None, all voltage values are
74
+ allowed. This can potentially lead to problems, if the structures are not available for some regions
75
+ and no restriction is applied. Default is None. \n
76
+ Example: \n
77
+ [min_V2, max_V2]
78
+ ideal_csd_config (Optional[IdealCSDInterface]): Implementation of the IdealCSDInterface (from the ideal_csd
79
+ module). Used to generate the ideal CSD data during the simulation. Default is None.
65
80
  sensor (SensorInterface): Implementation of the SensorInterface (from the sensor module). Used to calculate
66
81
  the sensor potential & response based on ideal CSD data. Default is SensorGeneric().
67
- occupation_distortions (Union[List[OccupationDistortionInterface], None]): List of implementations of the
82
+ occupation_distortions (Optional[List[OccupationDistortionInterface]]): List of implementations of the
68
83
  OccupationDistortionInterface. This distortion type affects the occupations and lead transitions of the
69
84
  CSD (the "structure"). The supplied implementations are applied in the order they appear in the list.
70
85
  Default is None.
71
- sensor_potential_distortions (Union[List[SensorPotentialDistortionInterface], None]): List of
72
- implementations of the SensorPotentialDistortionInterface. This distortion type affects the sensor
73
- potential, which is calculated based on the occupations and (plunger) gate voltages. The supplied
74
- implementations are applied in the order they appear in the list. Default is None.
75
- sensor_response_distortions (Union[List[SensorResponseDistortionInterface], None]): List of implementations
76
- of the SensorResponseDistortionInterface. This distortions type affects the sensor response, which is
86
+ sensor_potential_distortions (Optional[List[SensorPotentialDistortionInterface]]): List of implementations
87
+ of the SensorPotentialDistortionInterface. This distortion type affects the sensor potential, which is
88
+ calculated based on the occupations and (plunger) gate voltages. The supplied implementations are
89
+ applied in the order they appear in the list. Default is None.
90
+ sensor_response_distortions (Optional[List[SensorResponseDistortionInterface]]): List of implementations of
91
+ the SensorResponseDistortionInterface. This distortions type affects the sensor response, which is
77
92
  calculated based on the sensor potential. The supplied implementations are applied in the order they
78
93
  appear in the list. Default is None.
79
94
  """
80
95
  self.volt_limits_g1 = volt_limits_g1
81
96
  self.volt_limits_g2 = volt_limits_g2
97
+ self.volt_limits_sensor_g1 = volt_limits_sensor_g1
98
+ self.volt_limits_sensor_g2 = volt_limits_sensor_g2
99
+
82
100
  self.ideal_csd_config = ideal_csd_config
83
101
  self.sensor = sensor
84
102
  self.occupation_distortions = occupation_distortions
@@ -86,13 +104,15 @@ class Simulation:
86
104
  self.sensor_response_distortions = sensor_response_distortions
87
105
 
88
106
  def measure(
89
- self,
90
- sweep_range_g1: np.ndarray,
91
- sweep_range_g2: np.ndarray,
92
- resolution: Union[int, np.ndarray] = np.array([100, 100]),
107
+ self,
108
+ sweep_range_g1: np.ndarray,
109
+ sweep_range_g2: np.ndarray,
110
+ volt_sensor_g1: Optional[float] = None,
111
+ volt_sensor_g2: Optional[float] = None,
112
+ resolution: Union[int, np.ndarray] = np.array([100, 100]),
93
113
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]:
94
114
  """
95
- Simulates the measurement of the specified voltage sweep in the desired resolution. If two resolutions are
115
+ Simulates CSD measurement for the specified voltage sweep(s) at the desired resolution. If two resolutions are
96
116
  supplied, a 2D scan is performed, and if only one resolution is supplied, a 1D scan is performed.
97
117
 
98
118
  Args:
@@ -102,6 +122,12 @@ class Simulation:
102
122
  sweep_range_g2 (np.ndarray): Voltage sweep range of (plunger) gate 2 (first-/y-axis). \n
103
123
  Example: \n
104
124
  [min_V2, max_V2]
125
+ volt_sensor_g1 (Optional[float]): Voltage applied at sensor gate 1 (second-/x-axis). \n
126
+ Example: \n
127
+ [min_V1, max_V1]
128
+ volt_sensor_g2 (Optional[float]): Voltage applied at sensor gate 2 (first-/y-axis). \n
129
+ Example: \n
130
+ [min_V2, max_V2]
105
131
  resolution (Union[int, np.ndarray]): The desired resolution (in pixels) for the two gates. If only one value
106
132
  is supplied, a 1D sweep is performed. Then, both gates are swept simultaneously. Default is
107
133
  np.array([100, 100]). \n
@@ -132,30 +158,58 @@ class Simulation:
132
158
  # Check if voltage range is restricted. If it is restricted, check if the start & stop voltage
133
159
  # of the scan are in the allowed range.
134
160
  if (
135
- self.__volt_limits_g1 is not None
136
- and (
161
+ self.__volt_limits_g1 is not None
162
+ and (
137
163
  np.min(sweep_range_g1) < self.__volt_limits_g1[0]
138
164
  or np.min(sweep_range_g1) > self.__volt_limits_g1[1]
139
165
  or np.max(sweep_range_g1) < self.__volt_limits_g1[0]
140
166
  or np.max(sweep_range_g1) > self.__volt_limits_g1[1]
141
- )
142
- or (
167
+ )
168
+ or (
143
169
  self.__volt_limits_g2 is not None
144
170
  and (
145
- np.min(sweep_range_g2) < self.__volt_limits_g2[0]
146
- or np.min(sweep_range_g2) > self.__volt_limits_g2[1]
147
- or np.max(sweep_range_g2) < self.__volt_limits_g2[0]
148
- or np.max(sweep_range_g2) > self.__volt_limits_g2[1]
171
+ np.min(sweep_range_g2) < self.__volt_limits_g2[0]
172
+ or np.min(sweep_range_g2) > self.__volt_limits_g2[1]
173
+ or np.max(sweep_range_g2) < self.__volt_limits_g2[0]
174
+ or np.max(sweep_range_g2) > self.__volt_limits_g2[1]
149
175
  )
150
- )
176
+ )
151
177
  ):
152
178
  raise ValueError(
153
- f"The voltages defined by sweep_range_g1 ({sweep_range_g1}) must be in the range of the limits defined"
179
+ f"The voltages defined by sweep_range_g1 ({sweep_range_g1}) must be in the range of the limits defined "
154
180
  f"by volt_limits_g1 ({self.__volt_limits_g1}) and the voltages defined by sweep_range_g2 "
155
181
  f"({sweep_range_g2}) must be in the range of the limits defined by volt_limits_g2 "
156
182
  f"({self.__volt_limits_g2})."
157
183
  )
158
184
 
185
+ if (
186
+ self.__volt_limits_sensor_g1 is not None
187
+ and (
188
+ volt_sensor_g1 < self.__volt_limits_sensor_g1[0]
189
+ or volt_sensor_g1 > self.__volt_limits_sensor_g1[1]
190
+ )
191
+ or (
192
+ self.__volt_limits_sensor_g2 is not None
193
+ and (
194
+ volt_sensor_g2 < self.__volt_limits_sensor_g2[0]
195
+ or volt_sensor_g2 > self.__volt_limits_sensor_g2[1]
196
+ )
197
+ )
198
+ ):
199
+ raise ValueError(
200
+ f"The voltages defined by volt_sensor_g1 ({volt_sensor_g1}) must be in the range of the limits defined "
201
+ f"by volt_limits_sensor_g1 ({self.__volt_limits_sensor_g1}) and the voltages defined by volt_sensor_g2 "
202
+ f"({volt_sensor_g2}) must be in the range of the limits defined by volt_limits_sensor_g2 "
203
+ f"({self.__volt_limits_sensor_g2})."
204
+ )
205
+
206
+ if (volt_sensor_g1 is None) != (volt_sensor_g2 is None):
207
+ raise ValueError(
208
+ f"It is not permitted to pass only one of the two sensor gate voltages. Either both parameters "
209
+ f"volt_sensor_g1 ({volt_sensor_g1}) and volt_sensor_g2 ({volt_sensor_g2}) must be specified or both "
210
+ f"must be equal to None."
211
+ )
212
+
159
213
  # check if the resolution has at most two entries
160
214
  if not type(resolution) == int and len(resolution) > 2:
161
215
  raise ValueError(
@@ -165,9 +219,9 @@ class Simulation:
165
219
 
166
220
  # Check if one gate is kept at a fixed voltage if a 2D scan is requested. This is only possible in 1D scans.
167
221
  if (
168
- not type(resolution) == int
169
- and len(resolution) == 2
170
- and (sweep_range_g1[0] == sweep_range_g1[1] or sweep_range_g2[0] == sweep_range_g2[1])
222
+ not type(resolution) == int
223
+ and len(resolution) == 2
224
+ and (sweep_range_g1[0] == sweep_range_g1[1] or sweep_range_g2[0] == sweep_range_g2[1])
171
225
  ):
172
226
  raise ValueError(
173
227
  f"At least one of the voltage ranges 'sweep_range_g1' and 'sweep_range_g2' defines a fixed voltage. "
@@ -203,21 +257,39 @@ class Simulation:
203
257
  next distortions.
204
258
  """
205
259
  occupations, lead_transitions = generate_csd(volt_limits_g1, volt_limits_g2, resolution)
206
- noise_function(occupations, lead_transitions, volt_limits_g1, volt_limits_g2, generate_csd, freeze=True)
260
+ noise_function(occupations, lead_transitions, volt_limits_g1, volt_limits_g2, generate_csd,
261
+ freeze=True)
207
262
  return occupations, lead_transitions
208
- generate_csd = partial(generate_csd_with_noise, generate_csd=generate_csd, noise_function=i.noise_function)
263
+
264
+ generate_csd = partial(generate_csd_with_noise, generate_csd=generate_csd,
265
+ noise_function=i.noise_function)
209
266
 
210
267
  # calculate the sensor potential from the distorted occupations
211
- potential = self.__sensor.sensor_potential(
212
- occupations=occupations, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
213
- )
268
+ if isinstance(self.__sensor, SensorScanSensorInterface):
269
+ # additionally pass sensor voltages if sensor is capable of sensor scans
270
+ potential = self.__sensor.sensor_potential(
271
+ occupations=occupations, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2,
272
+ volt_limits_sensor_g1=volt_sensor_g1, volt_limits_sensor_g2=volt_sensor_g2
273
+ )
274
+ else:
275
+ potential = self.__sensor.sensor_potential(
276
+ occupations=occupations, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
277
+ )
214
278
 
215
279
  # Add distortions to the sensor potential
216
280
  if self.__sensor_potential_distortions is not None:
217
- for i in self.__sensor_potential_distortions:
218
- potential = i.noise_function(
219
- mu_sens=potential, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
220
- )
281
+ if occupations.ndim == 3 and potential.ndim == 2 or occupations.ndim == 2 and potential.ndim == 1:
282
+ for i in self.__sensor_potential_distortions:
283
+ potential = i.noise_function(
284
+ mu_sens=potential, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
285
+ )
286
+ # SensorScanSensor implementations have more than one potential that has to be distorted
287
+ elif occupations.ndim == 3 and potential.ndim == 3 or occupations.ndim == 2 and potential.ndim == 2:
288
+ for i in self.__sensor_potential_distortions:
289
+ for pot_num in range(potential.shape[0]):
290
+ potential[pot_num] = i.noise_function(
291
+ mu_sens=potential[pot_num], volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
292
+ )
221
293
 
222
294
  # Add the sensor function
223
295
  csd = self.__sensor.sensor_response(potential)
@@ -229,10 +301,15 @@ class Simulation:
229
301
  sensor_response=csd, volt_limits_g1=sweep_range_g1, volt_limits_g2=sweep_range_g2
230
302
  )
231
303
 
232
- # create a dictionary containing all the metadata
233
304
  metadata = {
234
305
  "sweep_range_g1": deepcopy(sweep_range_g1),
235
306
  "sweep_range_g2": deepcopy(sweep_range_g2),
307
+ "volt_sensor_g1": deepcopy(volt_sensor_g1),
308
+ "volt_sensor_g2": deepcopy(volt_sensor_g2),
309
+ "volt_limits_g1": deepcopy(self.volt_limits_g1),
310
+ "volt_limits_g2": deepcopy(self.volt_limits_g2),
311
+ "volt_limits_sensor_g1": deepcopy(self.volt_limits_sensor_g1),
312
+ "volt_limits_sensor_g2": deepcopy(self.volt_limits_sensor_g2),
236
313
  "resolution": deepcopy(resolution),
237
314
  "ideal_csd_config": deepcopy(self.ideal_csd_config),
238
315
  "sensor": deepcopy(self.sensor),
@@ -243,8 +320,251 @@ class Simulation:
243
320
 
244
321
  return csd, occupations, lead_transitions, metadata
245
322
 
323
+ def measure_sensor_scan(
324
+ self,
325
+ sweep_range_sensor_g1: np.ndarray,
326
+ sweep_range_sensor_g2: np.ndarray,
327
+ volt_g1: Optional[float] = None,
328
+ volt_g2: Optional[float] = None,
329
+ resolution: Union[int, np.ndarray] = np.array([100, 100]),
330
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]:
331
+ """
332
+ Simulates the measurement of a sensor scan under the specified voltage sweep(s) at the desired resolution. If
333
+ two resolutions are supplied, a 2D scan is performed, and if only one resolution is supplied, a 1D scan is
334
+ performed.
335
+
336
+ Args:
337
+ sweep_range_sensor_g1 (np.ndarray): Voltage sweep range of sensor gate 1 (second-/x-axis). \n
338
+ Example: \n
339
+ [min_V1, max_V1]
340
+ sweep_range_sensor_g2 (np.ndarray): Voltage sweep range of sensor gate 2 (first-/y-axis). \n
341
+ Example: \n
342
+ [min_V2, max_V2]
343
+ volt_g1 (Optional[float]): Voltage applied at (plunger) gate 1 (second-/x-axis).
344
+ volt_g2 (Optional[float]): Voltage applied at (plunger) gate 2 (first-/y-axis).
345
+ resolution (Union[int, np.ndarray]): The desired resolution (in pixels) for the two gates. If only one value
346
+ is supplied, a 1D sweep is performed. Then, both gates are swept simultaneously. Default is
347
+ np.array([100, 100]). \n
348
+ Example: \n
349
+ [res_g1, res_g2]
350
+
351
+ Returns:
352
+ Tuple[np.ndarray, np.ndarray, np.ndarray, Dict]: Numpy array of the measured sensor signal, that is called
353
+ sensor scan. Numpy array, the conductive area mask as a label mask that indicates the non-conductive area,
354
+ sensor oscillation regime and fully conductive area. Numpy array as a second label mask, that marks the
355
+ peaks of the Coulomb peaks as integers. Dictionary with metadata (all parameters of the system & the
356
+ measurement). The axes of the three arrays match to the supplied sweep range. If the sweep is f.e. performed
357
+ from high to low voltage, the values in the array are also arranged in that way (lowest index would then map
358
+ to the highest voltage). In general: axis 0 = y-axis = sensor_g2 = sensor_V2 and axis 1 = x-axis = sensor_g1
359
+ = sensor_V1.
360
+ """
361
+ if not isinstance(self.__sensor, SensorScanSensorInterface):
362
+ raise ValueError(
363
+ "The current sensor does not support the measurement of sensor scans. Only sensors that implement the "
364
+ "SensorScanSensorInterface are supported."
365
+ )
366
+
367
+ # check if the sensor gate voltage ranges have the correct number of entries
368
+ if not (len(sweep_range_sensor_g1) == 2 and len(sweep_range_sensor_g2) == 2):
369
+ raise ValueError(
370
+ "The sweep ranges for the sensor gates g1 and g2 must consist of exactly 2 values each. At least "
371
+ f"one sweep range violates this."
372
+ )
373
+
374
+ # Check if sensor voltage range is restricted. If it is restricted, check if the start & stop voltage
375
+ # of the scan are in the allowed range.
376
+ if (
377
+ self.__volt_limits_sensor_g1 is not None
378
+ and (
379
+ np.min(sweep_range_sensor_g1) < self.__volt_limits_sensor_g1[0]
380
+ or np.min(sweep_range_sensor_g1) > self.__volt_limits_sensor_g1[1]
381
+ or np.max(sweep_range_sensor_g1) < self.__volt_limits_sensor_g1[0]
382
+ or np.max(sweep_range_sensor_g1) > self.__volt_limits_sensor_g1[1]
383
+ )
384
+ or (
385
+ self.__volt_limits_sensor_g2 is not None
386
+ and (
387
+ np.min(sweep_range_sensor_g2) < self.__volt_limits_sensor_g2[0]
388
+ or np.min(sweep_range_sensor_g2) > self.__volt_limits_sensor_g2[1]
389
+ or np.max(sweep_range_sensor_g2) < self.__volt_limits_sensor_g2[0]
390
+ or np.max(sweep_range_sensor_g2) > self.__volt_limits_sensor_g2[1]
391
+ )
392
+ )
393
+ ):
394
+ raise ValueError(
395
+ f"The voltages defined by sweep_range_sensor_g1 ({sweep_range_sensor_g1}) must be in the range of the "
396
+ f"limits defined by volt_limits_sensor_g1 ({self.__volt_limits_sensor_g1}) and the voltages defined by "
397
+ f"sweep_range_sensor_g2 ({sweep_range_sensor_g2}) must be in the range of the limits defined by "
398
+ f"volt_limits_sensor_g2 ({self.__volt_limits_sensor_g2})."
399
+ )
400
+
401
+ # Check if one sensor gate is kept at a fixed voltage if a 2D scan is requested.
402
+ # This is only possible in 1D scans.
403
+ if (
404
+ not type(resolution) == int
405
+ and len(resolution) == 2
406
+ and (sweep_range_sensor_g1[0] == sweep_range_sensor_g1[1] or sweep_range_sensor_g2[0] ==
407
+ sweep_range_sensor_g2[1])
408
+ ):
409
+ raise ValueError(
410
+ f"At least one of the voltage ranges 'sweep_range_sensor_g1' and 'sweep_range_sensor_g2' defines a "
411
+ f"fixed voltage. This is only supported for 1D sweeps (only one resolution), but two resolutions were "
412
+ f"specified."
413
+ )
414
+ if (
415
+ volt_g1 is not None
416
+ and self.__volt_limits_g1 is not None
417
+ and (
418
+ volt_g1 < self.__volt_limits_g1[0]
419
+ or volt_g1 > self.__volt_limits_g1[1]
420
+ )
421
+ or (
422
+ volt_g2 is not None
423
+ and self.__volt_limits_g2 is not None
424
+ and (
425
+ volt_g2 < self.__volt_limits_g2[0]
426
+ or volt_g2 > self.__volt_limits_g2[1]
427
+ )
428
+ )
429
+ ):
430
+ raise ValueError(
431
+ f"The voltages defined by volt_g1 ({volt_g1}) must be in the range of the "
432
+ f"limits defined by volt_limits_g1 ({self.__volt_limits_g1}) and the voltages defined by "
433
+ f"volt_g2 ({volt_g2}) must be in the range of the limits defined by "
434
+ f"volt_limits_g2 ({self.__volt_limits_g2})."
435
+ )
436
+ if self.ideal_csd_config and ((volt_g1 is None) or (volt_g2 is None)):
437
+ warnings.warn(
438
+ f"If an ideal_csd_config is specified, it is assumed that the two voltages volt_g1 ({volt_g2}) and "
439
+ f"volt_g2 ({volt_g2}) are specified and are not equal to None. \n"
440
+ f"The ideal_csd_config is ignored and an occupation of 0 is assumed."
441
+ )
442
+
443
+ if (volt_g1 is None) != (volt_g2 is None):
444
+ raise ValueError(
445
+ f"It is not permitted to transfer only one of the two double dot (plunger) gate voltages. Both "
446
+ f"parameters volt_g1 ({volt_g1}) and volt_g2 ({volt_g2}) must be specified or be equal to None."
447
+ )
448
+
449
+ # check if the resolution has at most two entries
450
+ if not type(resolution) == int and len(resolution) > 2:
451
+ raise ValueError(
452
+ f"The specified resolution ({resolution}) has more than two entries. The resolution must either be a "
453
+ f"single value (for 1D scans) or contain two entries (for 2D scans)."
454
+ )
455
+
456
+ # Check if one resolution is 1 (or smaller) for a 2D scan. A resolution of 1 indicates that the corresponding
457
+ # gate is kept at a fixed voltage. This only makes sense for a 1D scan
458
+ if not type(resolution) == int and len(resolution) == 2 and (resolution[0] <= 1 or resolution[1] <= 1):
459
+ raise ValueError(
460
+ f"The specified resolution ({resolution}) indicates that a 2D scan should be performed, but at least "
461
+ f"one of the two entries is smaller or equal to 1. A resolution of 1 means that the corresponding gate "
462
+ f"is not swept. Thus, it describes a 1D scan of a single gate. Please specify just one resolution and "
463
+ f"a fixed voltage for the corresponding gate, to perform a single gate sweep."
464
+ )
465
+
466
+ if self.ideal_csd_config is None or volt_g1 is None or volt_g2 is None:
467
+ if type(resolution) == int:
468
+ occupations = np.zeros((resolution, 2))
469
+ else:
470
+ occupations = np.zeros((resolution[1], resolution[0], 2))
471
+ undistorted_occupations = deepcopy(occupations)
472
+ else:
473
+ # setup clean data function pointer
474
+ generate_csd = deepcopy(self.__ideal_csd_config.get_csd_data)
475
+
476
+ # Perform simulation
477
+ occupations_csd, lead_transitions = generate_csd(
478
+ volt_limits_g1=(volt_g1, volt_g1), volt_limits_g2=(volt_g2, volt_g2), resolution=(1)
479
+ )
480
+
481
+ if len(resolution) == 1:
482
+ resolution = int(resolution[0])
483
+
484
+ if type(resolution) == int:
485
+ occupations = np.ones((resolution, 2))
486
+ occupations[:, 0] = occupations[:, 0] * occupations_csd[0, 0]
487
+ occupations[:, 1] = occupations[:, 1] * occupations_csd[0, 1]
488
+ else:
489
+ occupations = np.ones((resolution[1], resolution[0], 2))
490
+ occupations[:, :, 0] = occupations[:,:,0] * occupations_csd[0, 0]
491
+ occupations[:, :, 1] = occupations[:,:,1] * occupations_csd[0, 1]
492
+
493
+ undistorted_occupations = deepcopy(occupations)
494
+
495
+ # could apply occupation distortions here, if sweeping the DQD & Sensor gates simultaneously is implemented
496
+
497
+ # Calculate the sensor potential from the distorted occupations
498
+ potential = self.__sensor.sensor_potential(
499
+ occupations=occupations, volt_limits_g1=volt_g1, volt_limits_g2=volt_g2,
500
+ volt_limits_sensor_g1=sweep_range_sensor_g1, volt_limits_sensor_g2=sweep_range_sensor_g2
501
+ )
502
+
503
+ # Calculate the potential using the undistorted occupations for the calculation of the labels
504
+ potential_undistorted_occ = self.__sensor.sensor_potential(
505
+ occupations=undistorted_occupations, volt_limits_g1=volt_g1, volt_limits_g2=volt_g2,
506
+ volt_limits_sensor_g1=sweep_range_sensor_g1, volt_limits_sensor_g2=sweep_range_sensor_g2
507
+ )
508
+
509
+ # Calculate the labels
510
+ conductive_mask, coulomb_peak_mask = self.__sensor.get_sensor_scan_labels(
511
+ volt_limits_g1=volt_g1,
512
+ volt_limits_g2=volt_g2,
513
+ volt_limits_sensor_g1=sweep_range_sensor_g1,
514
+ volt_limits_sensor_g2=sweep_range_sensor_g2,
515
+ potential=potential_undistorted_occ
516
+ )
517
+
518
+ # Add distortions to the sensor potential
519
+ if self.__sensor_potential_distortions is not None:
520
+ if occupations.ndim == 3 and potential.ndim == 2 or occupations.ndim == 2 and potential.ndim == 1:
521
+ for i in self.__sensor_potential_distortions:
522
+ potential = i.noise_function(
523
+ mu_sens=potential,
524
+ volt_limits_g1=sweep_range_sensor_g1,
525
+ volt_limits_g2=sweep_range_sensor_g2
526
+ )
527
+ elif occupations.ndim == 3 and potential.ndim == 3 or occupations.ndim == 2 and potential.ndim == 2:
528
+ for i in self.__sensor_potential_distortions:
529
+ for pot_num in range(potential.shape[0]):
530
+ potential[pot_num] = i.noise_function(
531
+ mu_sens=potential[pot_num],
532
+ volt_limits_g1=sweep_range_sensor_g1,
533
+ volt_limits_g2=sweep_range_sensor_g2
534
+ )
535
+
536
+ # Add the sensor function
537
+ scan = self.__sensor.sensor_response(potential)
538
+
539
+ # add distortions to the sensor signal
540
+ if self.__sensor_response_distortions is not None:
541
+ for i in self.__sensor_response_distortions:
542
+ scan = i.noise_function(
543
+ sensor_response=scan, volt_limits_g1=sweep_range_sensor_g1, volt_limits_g2=sweep_range_sensor_g1
544
+ )
545
+
546
+ # create a dictionary containing all the metadata
547
+ metadata = {
548
+ "sweep_range_sensor_g1": deepcopy(sweep_range_sensor_g1),
549
+ "sweep_range_sensor_g2": deepcopy(sweep_range_sensor_g2),
550
+ "volt_g1": deepcopy(volt_g1),
551
+ "volt_g2": deepcopy(volt_g2),
552
+ "volt_limits_g1": deepcopy(self.volt_limits_g1),
553
+ "volt_limits_g2": deepcopy(self.volt_limits_g2),
554
+ "volt_limits_sensor_g1": deepcopy(self.volt_limits_sensor_g1),
555
+ "volt_limits_sensor_g2": deepcopy(self.volt_limits_sensor_g2),
556
+ "resolution": deepcopy(resolution),
557
+ "ideal_csd_config": deepcopy(self.ideal_csd_config),
558
+ "sensor": deepcopy(self.sensor),
559
+ "occupation_distortions": deepcopy(self.occupation_distortions),
560
+ "sensor_potential_distortions": deepcopy(self.sensor_potential_distortions),
561
+ "sensor_response_distortions": deepcopy(self.sensor_response_distortions),
562
+ }
563
+
564
+ return scan, conductive_mask, coulomb_peak_mask, metadata
565
+
246
566
  @property
247
- def volt_limits_g1(self) -> Union[np.ndarray, None]:
567
+ def volt_limits_g1(self) -> Optional[np.ndarray]:
248
568
  """
249
569
  Returns the current plunger 1 voltage limit configuration of the system. This configuration can then be adjusted
250
570
  and is directly used as new configuration, as the object is returned as call by reference
@@ -255,7 +575,7 @@ class Simulation:
255
575
  return self.__volt_limits_g1
256
576
 
257
577
  @volt_limits_g1.setter
258
- def volt_limits_g1(self, volt_limits_g1: Union[np.ndarray, None]) -> None:
578
+ def volt_limits_g1(self, volt_limits_g1: Optional[np.ndarray]) -> None:
259
579
  """
260
580
  Updates the plunger 1 voltage limit configuration of the system according to the supplied values.
261
581
 
@@ -264,7 +584,7 @@ class Simulation:
264
584
  """
265
585
  # check datatype of voltage limits
266
586
  if not (
267
- volt_limits_g1 is None or (isinstance(volt_limits_g1, (np.ndarray, list)) and volt_limits_g1.size == 2)
587
+ volt_limits_g1 is None or (isinstance(volt_limits_g1, (np.ndarray, list)) and volt_limits_g1.size == 2)
268
588
  ):
269
589
  raise ValueError(
270
590
  f"The provided volt_limits_g1 configuration is not supported. Must be either None or "
@@ -273,7 +593,7 @@ class Simulation:
273
593
  self.__volt_limits_g1 = deepcopy(volt_limits_g1)
274
594
 
275
595
  @property
276
- def volt_limits_g2(self) -> Union[np.ndarray, None]:
596
+ def volt_limits_g2(self) -> Optional[np.ndarray]:
277
597
  """
278
598
  Returns the current (plunger) gate 2 voltage limit configuration of the system. This configuration can then be
279
599
  adjusted and is directly used as new configuration, as the object is returned as call by reference
@@ -284,7 +604,7 @@ class Simulation:
284
604
  return self.__volt_limits_g2
285
605
 
286
606
  @volt_limits_g2.setter
287
- def volt_limits_g2(self, volt_limits_g2: Union[np.ndarray, None]) -> None:
607
+ def volt_limits_g2(self, volt_limits_g2: Optional[np.ndarray]) -> None:
288
608
  """
289
609
  Updates the plunger 2 voltage limit configuration of the system according to the supplied values.
290
610
 
@@ -293,7 +613,7 @@ class Simulation:
293
613
  """
294
614
  # check datatype of voltage limits
295
615
  if not (
296
- volt_limits_g2 is None or (isinstance(volt_limits_g2, (np.ndarray, list)) and volt_limits_g2.size == 2)
616
+ volt_limits_g2 is None or (isinstance(volt_limits_g2, (np.ndarray, list)) and volt_limits_g2.size == 2)
297
617
  ):
298
618
  raise ValueError(
299
619
  f"The provided volt_limits_g2 configuration is not supported. Must be either None or "
@@ -302,10 +622,70 @@ class Simulation:
302
622
  self.__volt_limits_g2 = deepcopy(volt_limits_g2)
303
623
 
304
624
  @property
305
- def ideal_csd_config(self) -> Union[IdealCSDInterface, None]:
625
+ def volt_limits_sensor_g1(self) -> Optional[np.ndarray]:
626
+ """
627
+ Returns the current sensor gate 1 voltage limit configuration of the system. This configuration can then be
628
+ adjusted and is directly used as new configuration, as the object is returned as call by reference
629
+
630
+ Returns:
631
+ np.ndarray: The allowed voltage range for sensor gate 1.
632
+ """
633
+ return self.__volt_limits_sensor_g1
634
+
635
+ @volt_limits_sensor_g1.setter
636
+ def volt_limits_sensor_g1(self, volt_limits_sensor_g1: Optional[np.ndarray]) -> None:
637
+ """
638
+ Updates the sensor gate 1 voltage limit configuration of the system according to the supplied values.
639
+
640
+ Args:
641
+ volt_limits_sensor_g1 (np.ndarray): The allowed voltage range for sensor gate 1.
642
+ """
643
+ # check datatype of voltage limits
644
+ if not (
645
+ volt_limits_sensor_g1 is None or (
646
+ isinstance(volt_limits_sensor_g1, (np.ndarray, list)) and volt_limits_sensor_g1.size == 2)
647
+ ):
648
+ raise ValueError(
649
+ f"The provided volt_limits_sensor_g1 configuration is not supported. Must be either None or "
650
+ "a numpy array of size 2."
651
+ )
652
+ self.__volt_limits_sensor_g1 = deepcopy(volt_limits_sensor_g1)
653
+
654
+ @property
655
+ def volt_limits_sensor_g2(self) -> Optional[np.ndarray]:
656
+ """
657
+ Returns the current sensor gate 2 voltage limit configuration of the system. This configuration can then be
658
+ adjusted and is directly used as new configuration, as the object is returned as call by reference
659
+
660
+ Returns:
661
+ np.ndarray: The allowed voltage range for sensor gate 2.
662
+ """
663
+ return self.__volt_limits_sensor_g2
664
+
665
+ @volt_limits_sensor_g2.setter
666
+ def volt_limits_sensor_g2(self, volt_limits_sensor_g2: Optional[np.ndarray]) -> None:
667
+ """
668
+ Updates the sensor gate 2 voltage limit configuration of the system according to the supplied values.
669
+
670
+ Args:
671
+ volt_limits_sensor_g2 (np.ndarray): The allowed voltage range for sensor gate 2.
672
+ """
673
+ # check datatype of voltage limits
674
+ if not (
675
+ volt_limits_sensor_g2 is None or (
676
+ isinstance(volt_limits_sensor_g2, (np.ndarray, list)) and volt_limits_sensor_g2.size == 2)
677
+ ):
678
+ raise ValueError(
679
+ f"The provided volt_limits_sensor_g2 configuration is not supported. Must be either None or "
680
+ "a numpy array of size 2."
681
+ )
682
+ self.__volt_limits_sensor_g2 = deepcopy(volt_limits_sensor_g2)
683
+
684
+ @property
685
+ def ideal_csd_config(self) -> Optional[IdealCSDInterface]:
306
686
  """
307
687
  Returns the current ideal CSD configuration of the system. This configuration can then be adjusted and
308
- is directly used as new configuration, as the object is returned as call by reference
688
+ is directly used as new configuration, as the object is returned as call by reference.
309
689
 
310
690
  Returns:
311
691
  IdealCSDInterface: Implementation of the IdealCSDInterface (part of module ideal_csd)
@@ -313,7 +693,7 @@ class Simulation:
313
693
  return self.__ideal_csd_config
314
694
 
315
695
  @ideal_csd_config.setter
316
- def ideal_csd_config(self, ideal_csd_config: Union[IdealCSDInterface, None]) -> None:
696
+ def ideal_csd_config(self, ideal_csd_config: Optional[IdealCSDInterface]) -> None:
317
697
  """
318
698
  Updates the ideal CSD configuration of the system according to the supplied implementation.
319
699
 
@@ -384,8 +764,8 @@ class Simulation:
384
764
  """
385
765
  # check occupation distortions config for implementations of the correct interface
386
766
  if not (
387
- occupation_distortions is None
388
- or all(isinstance(x, OccupationDistortionInterface) for x in occupation_distortions)
767
+ occupation_distortions is None
768
+ or all(isinstance(x, OccupationDistortionInterface) for x in occupation_distortions)
389
769
  ):
390
770
  raise ValueError(
391
771
  f"The provided occupation distortion configuration is not supported, as not all list "
@@ -405,7 +785,8 @@ class Simulation:
405
785
  return self.__sensor_potential_distortions
406
786
 
407
787
  @sensor_potential_distortions.setter
408
- def sensor_potential_distortions(self, sensor_potential_distortions: Union[List[SensorPotentialDistortionInterface], None]):
788
+ def sensor_potential_distortions(self, sensor_potential_distortions: Union[
789
+ List[SensorPotentialDistortionInterface], None]):
409
790
  """
410
791
  Updates the sensor potential distortion configuration of the system according to the supplied values.
411
792
 
@@ -414,8 +795,8 @@ class Simulation:
414
795
  """
415
796
  # check sensor potential distortions config for implementations of the correct interface
416
797
  if not (
417
- sensor_potential_distortions is None
418
- or all(isinstance(x, SensorPotentialDistortionInterface) for x in sensor_potential_distortions)
798
+ sensor_potential_distortions is None
799
+ or all(isinstance(x, SensorPotentialDistortionInterface) for x in sensor_potential_distortions)
419
800
  ):
420
801
  raise ValueError(
421
802
  f"The provided sensor potential distortion configuration is not supported, as not all "
@@ -435,7 +816,8 @@ class Simulation:
435
816
  return self.__sensor_response_distortions
436
817
 
437
818
  @sensor_response_distortions.setter
438
- def sensor_response_distortions(self, sensor_response_distortions: Union[List[SensorResponseDistortionInterface], None]) -> None:
819
+ def sensor_response_distortions(self, sensor_response_distortions: Union[
820
+ List[SensorResponseDistortionInterface], None]) -> None:
439
821
  """
440
822
  Updates the sensor response distortion configuration of the system according to the supplied values.
441
823
 
@@ -444,8 +826,8 @@ class Simulation:
444
826
  """
445
827
  # check sensor response distortions config for implementations of the correct interface
446
828
  if not (
447
- sensor_response_distortions is None
448
- or all(isinstance(x, SensorResponseDistortionInterface) for x in sensor_response_distortions)
829
+ sensor_response_distortions is None
830
+ or all(isinstance(x, SensorResponseDistortionInterface) for x in sensor_response_distortions)
449
831
  ):
450
832
  raise ValueError(
451
833
  f"The provided sensor response distortion configuration is not supported, as not all "
@@ -455,6 +837,6 @@ class Simulation:
455
837
 
456
838
  def __repr__(self):
457
839
  return (
458
- self.__class__.__name__
459
- + f"(volt_limits_g1={self.volt_limits_g1}, volt_limits_g2={self.volt_limits_g2}, ideal_csd_config={self.ideal_csd_config}, sensor={self.sensor}, occupation_distortions={self.occupation_distortions}, sensor_potential_distortions={self.sensor_potential_distortions}, sensor_response_distortions={self.sensor_response_distortions})"
840
+ self.__class__.__name__
841
+ + f"(volt_limits_g1={self.volt_limits_g1}, volt_limits_g2={self.volt_limits_g2}, volt_limits_sensor_g1={self.__volt_limits_sensor_g1}, volt_limits_sensor_g2={self.__volt_limits_sensor_g2},ideal_csd_config={self.ideal_csd_config}, sensor={self.sensor}, occupation_distortions={self.occupation_distortions}, sensor_potential_distortions={self.sensor_potential_distortions}, sensor_response_distortions={self.sensor_response_distortions})"
460
842
  )