analog-daddy 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.
File without changes
analog_daddy/conf.py ADDED
@@ -0,0 +1,47 @@
1
+
2
+ '''
3
+ This module contains the configuration/constants used throughout the project.
4
+ DO NOT WRITE THIS FILE MANUALLY. Instead use the write_config.py script
5
+ as follows:
6
+
7
+ from analog_daddy.write_config import write_config
8
+ write_config()
9
+ '''
10
+ # This file was generated on 10/11/2023 19:54:43 Zone: EST by write_config.py
11
+ # DON'T YOU DARE EDIT THIS FILE MANUALLY.
12
+ # Constants to avoid magic strings
13
+ PARAMETER_TAG = "Parameters:"
14
+ GM_KEY_START = "gm_"
15
+ LENGTH_KEY = "length"
16
+ GS_KEY = "gs"
17
+ DS_KEY = "ds"
18
+ SB_KEY = "sb"
19
+ DB_KEY = "db"
20
+ GB_KEY = "gb"
21
+ METHOD_KEY = "METHOD"
22
+ WARNING_KEY = "WARNING"
23
+
24
+ # Supply Voltage (Vdd)
25
+ VDD = 1.0
26
+
27
+ # for technologies that only allow stacking and multipliers.
28
+ # Is the length/width an integer?
29
+ IS_INTEGER = False
30
+
31
+ # Length Range
32
+ L_MIN = 4.5e-08
33
+ L_MAX = 1e-05
34
+ LENGTH_INCREMENT = 1e-08
35
+ # Width Range (Per-Finger)
36
+ W_MIN = 9e-08
37
+ W_MAX = 0.001
38
+
39
+ # Voltage Step Size = 20 mV. This is the default step size
40
+ # which will be selected for our voltage sweeps.
41
+ # Note that decreasing sizes would not lead to more accurate results
42
+ # since the lookup function interpolates.
43
+ VOLTAGE_STEP_SIZE = 0.02
44
+
45
+ # Decides the verbosity of the importer. If set to True, the importer will
46
+ # output information about the imported data in real-time.
47
+ IMPORTER_VERBOSE = True
@@ -0,0 +1,315 @@
1
+ """
2
+ This module contains functions for importing data from a pseudo CSV file.
3
+ """
4
+ import csv
5
+ import numpy as np
6
+ from analog_daddy.conf import (LENGTH_KEY, GS_KEY,
7
+ DS_KEY, SB_KEY, GM_KEY_START,
8
+ PARAMETER_TAG, IMPORTER_VERBOSE)
9
+
10
+ def nest_populated_dictionary(y: dict, x: dict) -> dict:
11
+ """
12
+ Takes a dictionary y with device-related keys, processes it to rearrange
13
+ keys based on unique devices, and nests dictionary x within each device key.
14
+
15
+ Parameters:
16
+ - y (dict): Dictionary with device-related keys.
17
+ - x (dict): Dictionary of independent variables.
18
+
19
+ Returns:
20
+ - dict: Dictionary with independent variables nested within each device key.
21
+ """
22
+ # Start by filtering keys that start with 'gm'
23
+ gm_keys = [key for key in y.keys() if key.startswith(GM_KEY_START)]
24
+
25
+ # Extract the device names by removing the 'gm_' prefix
26
+ unique_devices = {key.split(GM_KEY_START)[1] for key in gm_keys}
27
+
28
+ y_rearranged = {}
29
+
30
+ for device in unique_devices:
31
+ y_rearranged[device] = x.copy() # Nesting x within each device
32
+ for key in y.keys():
33
+ if device in key:
34
+ # Strip the device name (plus preceding underscore) to avoid redundancy
35
+ new_key = key.replace(f"_{device}", "")
36
+ y_rearranged[device][new_key] = y[key]
37
+
38
+ return y_rearranged
39
+
40
+ def extract_parameters_from_comment(comment: list) -> dict:
41
+ """
42
+ Extract parameters from a comment list and return them as a dictionary.
43
+
44
+ Parameters:
45
+ - comment (list): The comment list containing the PARAMETER_TAG and the parameters.
46
+
47
+ Returns:
48
+ - dict: A dictionary of extracted parameters.
49
+ """
50
+ parameters = {}
51
+
52
+ if not comment:
53
+ raise ValueError("The list is empty!")
54
+
55
+ if comment[0] == PARAMETER_TAG:
56
+ parts = comment[1].split(', ')
57
+ for part in parts:
58
+ try:
59
+ key, value = part.split('=')
60
+ parameters[key] = value.strip()
61
+ except ValueError:
62
+ raise ValueError("There are no parameters specified at all")
63
+ return parameters
64
+
65
+ def parse_value(value: str) -> float:
66
+ """
67
+ Convert a string value with possible SI (International System of Units) prefixes
68
+ to its corresponding float value.
69
+
70
+ For instance:
71
+ - "1k" becomes 1000.0
72
+ - "1M" becomes 1000000.0
73
+ - "1u" becomes 0.000001
74
+
75
+ Parameters:
76
+ - value (str): The string value to parse. It may end with an SI prefix.
77
+
78
+ Returns:
79
+ - float: The parsed float value.
80
+ """
81
+ # Dictionary to map SI prefixes to their multiplier values.
82
+ si_prefixes = {
83
+ 'T': 1e12, # Tera
84
+ 'G': 1e9, # Giga
85
+ 'M': 1e6, # Mega
86
+ 'k': 1e3, # Kilo
87
+ 'm': 1e-3, # Milli
88
+ 'u': 1e-6, # Micro
89
+ 'n': 1e-9, # Nano
90
+ 'p': 1e-12, # Pico
91
+ 'f': 1e-15, # Femto
92
+ 'a': 1e-18, # Atto (added)
93
+ }
94
+
95
+ # If the last character in the string is an SI prefix, multiply
96
+ # the number with the corresponding multiplier.
97
+ if value[-1] in si_prefixes:
98
+ return float(value[:-1]) * si_prefixes[value[-1]]
99
+ # If there's no SI prefix, simply convert the string to a float.
100
+ return float(value)
101
+
102
+ def strip_empty_strings(data: list) -> list:
103
+ """
104
+ Remove empty strings, trim whitespaces, and adjust sublists based on the first element.
105
+
106
+ This function processes each inner list within the main data list,
107
+ removes empty string entries, trims whitespaces from non-empty strings,
108
+ and adjusts sublists by removing the first two elements if the first element
109
+ is not PARAMETER_TAG.
110
+
111
+ Parameters:
112
+ - data (list): The data list containing rows with possibly empty strings.
113
+
114
+ Returns:
115
+ - list: The processed data list with empty strings removed and sublists adjusted.
116
+ """
117
+ cleaned_data = []
118
+ for sublist in data:
119
+ # Remove empty strings, trim whitespaces, and check the first element
120
+ cleaned_sublist = [item.strip() for item in sublist if item.strip() != '']
121
+ if cleaned_sublist and cleaned_sublist[0] != PARAMETER_TAG:
122
+ # If the first element is not PARAMETER_TAG, remove the first two elements
123
+ cleaned_sublist = cleaned_sublist[2:]
124
+ cleaned_data.append(cleaned_sublist)
125
+ return cleaned_data
126
+
127
+ class CSVReadingError(Exception):
128
+ pass
129
+
130
+ def read_pseudo_csv_file(file_path: str) -> list:
131
+ """
132
+ Read the pseudo CSV file and return its data as a list of lists.
133
+
134
+ Parameters:
135
+ - file_path (str): Path to the pseudo CSV file.
136
+
137
+ Returns:
138
+ - list: A list of rows, where each row is a list of string values.
139
+ """
140
+ data = []
141
+
142
+ try:
143
+ with open(file_path, 'r',encoding='utf-8') as csv_file:
144
+ reader = csv.reader(csv_file)
145
+ # Skip the first row (headers)
146
+ next(reader, None)
147
+
148
+ is_inside_parameters_section = False
149
+ current_parameters = None
150
+
151
+ for row in reader:
152
+ if len(row) == 0:
153
+ continue # Skip empty lines
154
+
155
+ if PARAMETER_TAG in row[0]:
156
+ is_inside_parameters_section = True
157
+ current_parameters = row[0].split(":")[1].strip()
158
+ elif is_inside_parameters_section:
159
+ data.append([PARAMETER_TAG, current_parameters])
160
+ is_inside_parameters_section = False
161
+ data.append(row)
162
+ else:
163
+ data.append(row)
164
+
165
+ except FileNotFoundError:
166
+ raise CSVReadingError(f"The file '{file_path}' was not found.")
167
+ except Exception as e:
168
+ raise CSVReadingError(f"Error reading the CSV file: {str(e)}")
169
+
170
+ return data
171
+
172
+ def separate_lines(data: list) -> (list, list):
173
+ """
174
+ Separate the provided data into parameter lines and data lines.
175
+ Parameters:
176
+ - data (list): The data list to process.
177
+
178
+ Returns:
179
+ - tuple: A tuple containing two lists - parameter lines and data lines.
180
+ """
181
+ param_lines = [line for line in data if PARAMETER_TAG in line[0]]
182
+ data_lines = [line for line in data if PARAMETER_TAG not in line[0]]
183
+ return param_lines, data_lines
184
+
185
+ def process_parameters(param_lines: list) -> dict:
186
+ """
187
+ Process the parameter lines to populate the x dictionary.
188
+ Parameters:
189
+ - param_lines (list): List of parameter lines.
190
+
191
+ Returns:
192
+ - dict: The x dictionary populated with parameters.
193
+ """
194
+ x = {DS_KEY: [], GS_KEY: [], LENGTH_KEY: [], SB_KEY: []}
195
+ for line in param_lines:
196
+ _, params = line
197
+ for param in params.split(", "):
198
+ key, value = param.split("=")
199
+ parsed_value = parse_value(value) # Assuming values are floating point numbers
200
+ if parsed_value not in x[key]:
201
+ x[key].append(parsed_value)
202
+ return x
203
+
204
+ def create_data_structure(data_lines: list, x: dict) -> dict:
205
+ """
206
+ Create the structure of the y dictionary based on x values.
207
+ Parameters:
208
+ - data_lines (list): List of data lines.
209
+ - x (dict): The x dictionary populated with parameters.
210
+
211
+ Returns:
212
+ - dict: The initialized y dictionary.
213
+ """
214
+ y = {}
215
+ for key, _ in data_lines:
216
+ if key not in y:
217
+ y[key] = [
218
+ [
219
+ [
220
+ [None for _ in x[SB_KEY]]
221
+ for _ in x[LENGTH_KEY]
222
+ ]
223
+ for _ in x[GS_KEY]
224
+ ]
225
+ for _ in x[DS_KEY]
226
+ ]
227
+ return y
228
+
229
+ def populate_data_values(combined_lines: list, param_lines: list, x: dict, y: dict) -> None:
230
+ """
231
+ Populate the y dictionary with data values using combined lines while leveraging
232
+ param_lines and data_lines for context.
233
+ Parameters:
234
+ - combined_lines (list): Combined list of parameter and data lines.
235
+ - param_lines (list): The separated parameter lines.
236
+ - data_lines (list): The separated data lines.
237
+ - x (dict): The x dictionary populated with parameters.
238
+ - y (dict): The initialized y dictionary.
239
+ """
240
+ current_parameters = None
241
+ # go through the data line by line.
242
+ idx = 0
243
+ for line in combined_lines:
244
+ if IMPORTER_VERBOSE:
245
+ print(f"Processing line {idx}")
246
+ idx += 1
247
+ try:
248
+ # Check if it's a parameter line
249
+ if line in param_lines:
250
+ current_parameters = extract_parameters_from_comment(line)
251
+ ds_index = x[DS_KEY].index(parse_value(current_parameters[DS_KEY]))
252
+ gs_index = x[GS_KEY].index(parse_value(current_parameters[GS_KEY]))
253
+ length_index = x[LENGTH_KEY].index(parse_value(current_parameters[LENGTH_KEY]))
254
+ sb_index = x[SB_KEY].index(parse_value(current_parameters[SB_KEY]))
255
+ # else it's a data line.
256
+ else:
257
+ key, value = line
258
+ try:
259
+ y[key][ds_index][gs_index][length_index][sb_index] = float(value)
260
+ except ValueError:
261
+ # added to save it as a special values instead of
262
+ # just doing nothing.
263
+ if IMPORTER_VERBOSE:
264
+ print(f"""You got a problem at {ds_index},
265
+ {gs_index}, {length_index}, {sb_index}
266
+ Search for 123456789 in the final dict.""")
267
+ y[key][ds_index][gs_index][length_index][sb_index] = 123456789
268
+
269
+ except Exception as e:
270
+ if 'value' in locals():
271
+ raise ValueError(f"Error processing line {line} with value {value}: {e}")
272
+ else:
273
+ raise ValueError(f"Error processing line {line}: {e}")
274
+
275
+ def process_data(file_path: str) -> (dict, dict):
276
+ """
277
+ Process the data from the provided file path to extract and organize parameters
278
+ and their associated values.
279
+ Parameters:
280
+ - file_path (str): The path to the pseudo CSV file containing the data.
281
+
282
+ Returns:
283
+ - tuple: A tuple containing two dictionaries (x, y) representing parameters
284
+ and their associated values.
285
+ """
286
+ # Read the data from the file and strip empty strings
287
+ combined_lines = strip_empty_strings(read_pseudo_csv_file(file_path))
288
+ # Separate parameter lines and data lines
289
+ param_lines, data_lines = separate_lines(combined_lines)
290
+ # Process parameter lines to get x dictionary
291
+ x = process_parameters(param_lines)
292
+ if IMPORTER_VERBOSE:
293
+ print("Done Processing Parameters")
294
+ # Initialize the y dictionary based on x values
295
+ y = create_data_structure(data_lines, x)
296
+ if IMPORTER_VERBOSE:
297
+ print("Done Initializing Data Structure")
298
+ # Populate the y dictionary with data values
299
+ populate_data_values(combined_lines, param_lines, x, y)
300
+ return x, y
301
+
302
+ def lists_to_arrays(data):
303
+ """
304
+ Recursively converts all lists in a dictionary to numpy arrays.
305
+ Args:
306
+ - data (dict): The dictionary to process.
307
+
308
+ Returns:
309
+ - dict: The processed dictionary with lists converted to numpy arrays.
310
+ """
311
+ if isinstance(data, list): # Base case: if it's a list, convert to np.array
312
+ return np.array(data)
313
+ if isinstance(data, dict): # If it's a dictionary, process each key-value pair
314
+ return {key: lists_to_arrays(value) for key, value in data.items()}
315
+ return data # If it's neither a list nor a dictionary, return it as is
@@ -0,0 +1,162 @@
1
+ """
2
+ This module contains the look_up function, which is used to extract a desired
3
+ subset from the 4-dimensional simulation data and potentially interpolates if
4
+ the requested points aren't directly on the simulation grid.
5
+ """
6
+ import numpy as np
7
+ from scipy.interpolate import RegularGridInterpolator, PchipInterpolator, interp1d
8
+ from analog_daddy.conf import (
9
+ LENGTH_KEY, GS_KEY, DS_KEY, SB_KEY,
10
+ DB_KEY, GB_KEY, METHOD_KEY, WARNING_KEY)
11
+
12
+ # don't warn user about bad divisions
13
+ np.seterr(divide="ignore", invalid="ignore")
14
+
15
+ def look_upVGS(data, **kwargs): #UNTESTED
16
+ # Set values either using kwargs.get
17
+ L = kwargs.get(LENGTH_KEY, min(data[LENGTH_KEY]))
18
+ vds = kwargs.get(DS_KEY, max(data[DS_KEY]) / 2)
19
+ vdb = kwargs.get(DB_KEY)
20
+ vgb = kwargs.get(GB_KEY)
21
+ gm_id = kwargs.get('gm_id')
22
+ id_w = kwargs.get('id_w')
23
+ vsb = kwargs.get(SB_KEY, 0)
24
+ method = kwargs.get(METHOD_KEY, 'pchip')
25
+
26
+ # Determine usage mode
27
+ if vgb is None and vdb is None:
28
+ mode = 1
29
+ elif vgb is not None and vdb is not None:
30
+ mode = 2
31
+ else:
32
+ print('Invalid syntax or usage mode!')
33
+ return []
34
+
35
+ # Check whether GM_ID or ID_W was passed to function
36
+ if id_w is not None:
37
+ ratio_string = 'id_w'
38
+ ratio_data = id_w
39
+ elif gm_id is not None:
40
+ ratio_string = 'gm_id'
41
+ ratio_data = gm_id
42
+ else:
43
+ print('look_upVGS: Invalid syntax or usage mode!')
44
+ return []
45
+
46
+ if mode == 1:
47
+ vgs = data[GS_KEY]
48
+ ratio = look_up(data, ratio_string, gs=vgs, ds=vds, sb=vsb, length=L)
49
+ else: # mode == 2
50
+ step = data[GS_KEY][0] - data[GS_KEY][1]
51
+ vsb = np.arange(max(data[SB_KEY]), min(data[SB_KEY]) - step, -step)
52
+ vgs = vgb - vsb
53
+ vds = vdb - vsb
54
+ ratio = look_up(data, ratio_string, gs=vgs, ds=vds, sb=vsb, length=np.ones(len(vsb)) * L)
55
+
56
+ # Interpolation loop
57
+ s = np.shape(ratio)
58
+ output = np.full((s[1], len(ratio_data)), np.nan)
59
+ for j in range(s[1]):
60
+ ratio_range = ratio[:, j]
61
+ VGS_range = vgs
62
+ if ratio_string == 'gm_id':
63
+ idx = np.argmax(ratio_range)
64
+ VGS_range = VGS_range[idx:]
65
+ ratio_range = ratio_range[idx:]
66
+ if max(ratio_data) > ratio_range[0]:
67
+ print('look_upVGS: gm_id input larger than maximum!')
68
+ f = interp1d(ratio_range, VGS_range, kind=method, bounds_error=False, fill_value=np.nan)
69
+ output[j, :] = f(ratio_data)
70
+
71
+ return output.flatten()
72
+
73
+ # result = look_upVGS(nch, GM_ID=[10,15], ds=0.6, sb=0.1, length=0.1)
74
+
75
+ def look_up(data, outvar, **kwargs):
76
+ """
77
+ Extracts a desired subset from the 4-dimensional simulation data and
78
+ potentially interpolates if the requested points aren't directly on the simulation grid.
79
+ """
80
+ # Making the outvar lowercase to avoid case sensitivity
81
+ outvar = outvar.lower()
82
+ # Extracting parameters from kwargs or using default values
83
+ length = kwargs.get(LENGTH_KEY, min(data[LENGTH_KEY]))
84
+ vgs = kwargs.get(GS_KEY, data[GS_KEY])
85
+ vds = kwargs.get(DS_KEY, max(data[DS_KEY])/2)
86
+ vsb = kwargs.get(SB_KEY, 0)
87
+ method = kwargs.get(METHOD_KEY, 'linear')
88
+ warning = kwargs.get(WARNING_KEY, 'on')
89
+
90
+ # Determine mode
91
+ if '_' in outvar and any('_' in key for key in kwargs.keys()):
92
+ # both output and input are ratios
93
+ mode = 3
94
+ elif '_' in outvar:
95
+ # output is a ratio
96
+ mode = 2
97
+ else:
98
+ # no ratio. simple lookup.
99
+ mode = 1
100
+
101
+ # Data extraction and interpolation for mode 1 and mode 2
102
+ if mode in [1, 2]:
103
+
104
+ # Extract ydata based on outvar
105
+ if '_' in outvar:
106
+ numerator, denominator = outvar.split('_')
107
+ # making ydata as a ratio as requested.
108
+ ydata = data[numerator] / data[denominator]
109
+ else:
110
+ ydata = data[outvar]
111
+
112
+ # Linear Interpolation using RegularGridInterpolator
113
+ if len(data[SB_KEY]) > 1:
114
+ interpolator = RegularGridInterpolator((data[DS_KEY], data[GS_KEY], data[LENGTH_KEY], data[SB_KEY]), ydata)
115
+ output = interpolator((vds, vgs, length, vsb))
116
+ else:
117
+ # If VSB isn't considered a dimension in data
118
+ interpolator = RegularGridInterpolator((data[DS_KEY], data[GS_KEY], data[LENGTH_KEY]), ydata)
119
+ output = interpolator((vds, vgs, length))
120
+
121
+
122
+ # Mode 3
123
+ if mode == 3:
124
+ # extracting the invar from kwargs and making it lowercase to avoid case sensitivity
125
+ invar = (next(key for key in kwargs.keys() if '_' in key)).lower()
126
+ invals = kwargs[invar]
127
+ # Interpolating the outvar data across VGS
128
+ # both the outvar and invar are ratios hence making sure to use that ratio by splitting it.
129
+ outvar_numerator, outvar_denominator = outvar.split('_')
130
+ ydata_outvar = data[outvar_numerator] / data[outvar_denominator]
131
+ outvar_interpolator = RegularGridInterpolator((data[DS_KEY], data[GS_KEY], data[LENGTH_KEY], data[SB_KEY]), ydata_outvar)
132
+ invar_numerator, invar_denominator = invar.split('_')
133
+ ydata_invar = data[invar_numerator] / data[invar_denominator]
134
+ invar_interpolator = RegularGridInterpolator((data[DS_KEY], data[GS_KEY], data[LENGTH_KEY], data[SB_KEY]), ydata_invar)
135
+
136
+ vgs_vals = np.linspace(min(data[GS_KEY]), max(data[GS_KEY]), len(data[GS_KEY]))
137
+ outvar_values = outvar_interpolator((vds, vgs_vals, length, vsb))
138
+ invar_values = invar_interpolator((vds, vgs_vals, length, vsb))
139
+
140
+ if invar == "gm_id":
141
+ # If gm_id is passed as the third
142
+ # argument, special care is taken to look only "to the right" of the
143
+ # maximum gm/ID value in the gm/ID vs. VGS plane.
144
+ idx = np.argmax(invar_values)
145
+ invar_values = invar_values[idx:]
146
+ outvar_values = outvar_values[idx:]
147
+
148
+ elif invar in ["gm_cgg", "gm_css"]:
149
+ # If gm_cgg or gm_css is passed as the third
150
+ # argument, special care is taken to look only "to the left" of the
151
+ # maximum gm/cgg or gm/css value in the gm/cgg and gm/css vs. VGS plane.
152
+ idx = np.argmax(invar_values)
153
+ invar_values = invar_values[:idx+1]
154
+ outvar_values = outvar_values[:idx+1]
155
+
156
+ interpolator = PchipInterpolator(invar_values, outvar_values) if method == 'pchip' else interp1d(invar_values, outvar_values, kind=method)
157
+ output = interpolator(invals)
158
+
159
+ # Ensure the output is a column vector if one-dimensional
160
+ if output.ndim == 2 and min(output.shape) == 1:
161
+ output = output.flatten()
162
+ return output