pyibis-ami 7.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,635 @@
1
+ """IBIS-AMI parameter parsing and configuration utilities.
2
+
3
+ Original author: David Banas <capn.freako@gmail.com>
4
+
5
+ Original date: December 17, 2016
6
+
7
+ Copyright (c) 2019 David Banas; all rights reserved World wide.
8
+ """
9
+
10
+ from ctypes import c_double
11
+ import re
12
+ from typing import Any, NewType, Optional, TypeAlias
13
+
14
+ from numpy.typing import NDArray
15
+ from parsec import ParseError, generate, many, regex, string
16
+ from traits.api import Bool, Enum, HasTraits, Range, Trait, TraitType
17
+ from traitsui.api import Group, HGroup, Item, VGroup, View
18
+ from traitsui.menu import ModalButtons
19
+
20
+ from pyibisami.ami.model import AMIModelInitializer
21
+ from pyibisami.ami.parameter import AMIParamError, AMIParameter
22
+
23
+ # New types and aliases.
24
+ # Parameters = NewType('Parameters', dict[str, AMIParameter] | dict[str, 'Parameters'])
25
+ # ParamValues = NewType('ParamValues', dict[str, list[Any]] | dict[str, 'ParamValues'])
26
+ # See: https://stackoverflow.com/questions/70894567/using-mypy-newtype-with-type-aliases-or-protocols
27
+ ParamName = NewType("ParamName", str)
28
+ ParamValue: TypeAlias = int | float | str | list["ParamValue"]
29
+ Parameters: TypeAlias = dict[ParamName, AMIParameter] | dict[ParamName, 'Parameters']
30
+ ParamValues: TypeAlias = dict[ParamName, ParamValue] | dict[ParamName, 'ParamValues']
31
+
32
+ #####
33
+ # AMI parameter configurator.
34
+ #####
35
+
36
+
37
+ class AMIParamConfigurator(HasTraits):
38
+ """Customizable IBIS-AMI model parameter configurator.
39
+
40
+ This class can be configured to present a customized GUI to the user
41
+ for configuring a particular IBIS-AMI model.
42
+
43
+ The intended use model is as follows:
44
+
45
+ 1. Instantiate this class only once per IBIS-AMI model invocation.
46
+ When instantiating, provide the unprocessed contents of the AMI
47
+ file, as a single string. This class will take care of getting
48
+ that string parsed properly, and report any errors or warnings
49
+ it encounters, in its ``ami_parsing_errors`` property.
50
+
51
+ 2. When you want to let the user change the AMI parameter
52
+ configuration, call the ``open_gui`` member function.
53
+ (Or, just call the instance as if it were a function.)
54
+ The instance will then present a GUI to the user,
55
+ allowing him to modify the values of any *In* or *InOut* parameters.
56
+ The resultant AMI parameter dictionary, suitable for passing
57
+ into the ``ami_params`` parameter of the ``AMIModelInitializer``
58
+ constructor, can be accessed, via the instance's
59
+ ``input_ami_params`` property. The latest user selections will be
60
+ remembered, as long as the instance remains in scope.
61
+
62
+ The entire AMI parameter definition dictionary, which should *not* be
63
+ passed to the ``AMIModelInitializer`` constructor, is available in the
64
+ instance's ``ami_param_defs`` property.
65
+
66
+ Any errors or warnings encountered while parsing are available, in
67
+ the ``ami_parsing_errors`` property.
68
+ """
69
+
70
+ def __init__(self, ami_file_contents_str: str) -> None:
71
+ """
72
+ Args:
73
+ ami_file_contents_str: The unprocessed contents of the AMI file, as a single string.
74
+ """
75
+
76
+ # Super-class initialization is ABSOLUTELY NECESSARY, in order
77
+ # to get all the Traits/UI machinery setup correctly.
78
+ super().__init__()
79
+
80
+ # Parse the AMI file contents, storing any errors or warnings,
81
+ # and customize the view accordingly.
82
+ err_str, param_dict = parse_ami_param_defs(ami_file_contents_str)
83
+ if not param_dict:
84
+ print("Empty dictionary returned by parse_ami_param_defs()!")
85
+ print(f"Error message:\n{err_str}")
86
+ raise KeyError("Failed to parse AMI file; see console for more detail.")
87
+ top_branch = list(param_dict.items())[0]
88
+ root_name = top_branch[0]
89
+ param_dict = top_branch[1]
90
+ if "Reserved_Parameters" not in param_dict:
91
+ print(f"Error: {err_str}\nParameters: {param_dict}")
92
+ raise KeyError("Unable to get 'Reserved_Parameters' from the parameter set.")
93
+ if "Model_Specific" not in param_dict:
94
+ print(f"Error: {err_str}\nParameters: {param_dict}")
95
+ raise KeyError("Unable to get 'Model_Specific' from the parameter set.")
96
+ pdict = param_dict["Reserved_Parameters"].copy()
97
+ pdict.update(param_dict["Model_Specific"])
98
+ gui_items, new_traits = make_gui(pdict)
99
+ trait_names = []
100
+ for trait in new_traits:
101
+ self.add_trait(trait[0], trait[1])
102
+ trait_names.append(trait[0])
103
+ self._param_trait_names = trait_names
104
+ self._root_name = root_name
105
+ self._ami_parsing_errors = err_str
106
+ self._content = gui_items
107
+ self._param_dict = param_dict
108
+ self._info_dict = {name: p.pvalue for (name, p) in list(param_dict["Reserved_Parameters"].items())}
109
+
110
+ def __call__(self):
111
+ self.open_gui()
112
+
113
+ def open_gui(self):
114
+ """Present a customized GUI to the user, for parameter
115
+ customization."""
116
+ # self.configure_traits(kind='modal') # Waiting for Enthought/Traits PR1841 to be accepted.
117
+ self.configure_traits()
118
+
119
+ def default_traits_view(self):
120
+ "Default Traits/UI view definition."
121
+ view = View(
122
+ resizable=False,
123
+ buttons=ModalButtons,
124
+ title=f"{self._root_name} AMI Parameter Configurator",
125
+ )
126
+ view.set_content(self._content)
127
+ return view
128
+
129
+ def fetch_param(self, branch_names):
130
+ """Returns the parameter found by traversing 'branch_names' or None if
131
+ not found.
132
+
133
+ Note: 'branch_names' should *not* begin with 'root_name'.
134
+ """
135
+ param_dict = self.ami_param_defs
136
+ while branch_names:
137
+ branch_name = branch_names.pop(0)
138
+ if branch_name in param_dict:
139
+ param_dict = param_dict[branch_name]
140
+ else:
141
+ return None
142
+ if isinstance(param_dict, AMIParameter):
143
+ return param_dict
144
+ return None
145
+
146
+ def fetch_param_val(self, branch_names):
147
+ """Returns the value of the parameter found by traversing
148
+ 'branch_names' or None if not found.
149
+
150
+ Note: 'branch_names' should *not* begin with 'root_name'.
151
+ """
152
+ _param = self.fetch_param(branch_names)
153
+ if _param:
154
+ return _param.pvalue
155
+ return None
156
+
157
+ def set_param_val(self, branch_names, new_val):
158
+ """Sets the value of the parameter found by traversing 'branch_names'
159
+ or raises an exception if not found.
160
+
161
+ Note: 'branch_names' should *not* begin with 'root_name'.
162
+ Note: Be careful! There is no checking done here!
163
+ """
164
+
165
+ param_dict = self.ami_param_defs
166
+ while branch_names:
167
+ branch_name = branch_names.pop(0)
168
+ if branch_name in param_dict:
169
+ param_dict = param_dict[branch_name]
170
+ else:
171
+ raise ValueError(
172
+ f"Failed parameter tree search looking for: {branch_name}; available keys: {param_dict.keys()}"
173
+ )
174
+ if isinstance(param_dict, AMIParameter):
175
+ param_dict.pvalue = new_val
176
+ try:
177
+ eval(f"self.set({branch_name}_={new_val})") # pylint: disable=eval-used
178
+ except Exception: # pylint: disable=broad-exception-caught
179
+ eval(f"self.set({branch_name}={new_val})") # pylint: disable=eval-used
180
+ else:
181
+ raise TypeError(f"{param_dict} is not of type: AMIParameter!")
182
+
183
+ @property
184
+ def ami_parsing_errors(self):
185
+ """Any errors or warnings encountered, while parsing the AMI parameter
186
+ definition file contents."""
187
+ return self._ami_parsing_errors
188
+
189
+ @property
190
+ def ami_param_defs(self) -> dict[ParamName, Any]:
191
+ """The entire AMI parameter definition dictionary.
192
+
193
+ Should *not* be passed to ``AMIModelInitializer`` constructor!
194
+ """
195
+ return self._param_dict
196
+
197
+ @property
198
+ def input_ami_params(self) -> ParamValues:
199
+ """
200
+ The dictionary of *Model Specific* AMI parameters of type 'In' or
201
+ 'InOut', along with their user selected values.
202
+
203
+ Should be passed to ``AMIModelInitializer`` constructor.
204
+ """
205
+
206
+ res = {}
207
+ res[ParamName("root_name")] = self._root_name
208
+ params = self.ami_param_defs[ParamName("Model_Specific")]
209
+ for pname in params:
210
+ res.update(self.input_ami_param(params, pname))
211
+ return res
212
+
213
+ def input_ami_param(
214
+ self,
215
+ params: Parameters,
216
+ pname: ParamName,
217
+ prefix: str = ""
218
+ ) -> ParamValues:
219
+ """
220
+ Retrieve one AMI parameter value, or dictionary of subparameter values,
221
+ from the given parameter definition dictionary.
222
+
223
+ Args:
224
+ params: The parameter definition dictionary.
225
+ pname: The simple name of the parameter of interest, used by the IBIS-AMI model.
226
+
227
+ Keyword Args:
228
+ prefix: The current working parameter name prefix.
229
+
230
+ Returns:
231
+ A dictionary of parameter values indexed by non-prefixed parameter names.
232
+
233
+ Notes:
234
+ 1. The "prefix" referred to above refers to a string encoding of the
235
+ hierarchy above a particular trait. We need this hierarchy for the
236
+ sake of the ``Traits/UI`` machinery, which addresses traits by name
237
+ alone. However, the IBIS-AMI model is not expecting it. So, we have
238
+ to strip it off, before sending the result here into ``AMI_Init()``.
239
+ """
240
+
241
+ res = {}
242
+ tname = prefix + pname # This is the fully hierarchical trait name, used by the Traits/UI machinery.
243
+ param = params[pname]
244
+ if isinstance(param, AMIParameter):
245
+ if tname in self._param_trait_names: # If model specific and of type In or InOut...
246
+ # See the docs on the *HasTraits* class, if this is confusing.
247
+ # Querry for a mapped trait, first, by trying to get '<trait_name>_'. (Note the underscore.)
248
+ try:
249
+ res[pname] = self.trait_get(tname + "_")[tname + "_"]
250
+ # If we get an exception, we have an ordinary (i.e. - not mapped) trait.
251
+ except Exception: # pylint: disable=broad-exception-caught
252
+ res[pname] = self.trait_get(tname)[tname]
253
+ elif isinstance(param, dict): # We received a dictionary of subparameters, in 'param'.
254
+ subs: ParamValues = {}
255
+ for sname in param:
256
+ subs.update(self.input_ami_param(param, sname, prefix=pname + "_")) # type: ignore
257
+ res[pname] = subs
258
+ return res
259
+
260
+ @property
261
+ def info_ami_params(self):
262
+ "Dictionary of *Reserved* AMI parameter values."
263
+ return self._info_dict
264
+
265
+ def get_init(
266
+ self,
267
+ bit_time: float,
268
+ sample_interval: float,
269
+ channel_response: NDArray[float],
270
+ ami_params: Optional[dict[str, Any]] = None
271
+ ) -> AMIModelInitializer:
272
+ """
273
+ Get a model initializer, configured by the user if necessary.
274
+ """
275
+
276
+ row_size = len(channel_response)
277
+ if ami_params:
278
+ initializer = AMIModelInitializer(
279
+ ami_params,
280
+ info_params=self.info_ami_params,
281
+ bit_time=c_double(bit_time),
282
+ row_size=row_size,
283
+ sample_interval=c_double(sample_interval)
284
+ )
285
+ else:
286
+ # This call will invoke a GUI applet for the user to interact with,
287
+ # to configure the AMI parameter values.
288
+ self()
289
+ initializer = AMIModelInitializer(
290
+ self.input_ami_params,
291
+ info_params=self.info_ami_params,
292
+ bit_time=c_double(bit_time),
293
+ row_size=row_size,
294
+ sample_interval=c_double(sample_interval)
295
+ )
296
+
297
+ # Don't try to pack this into the parentheses above!
298
+ initializer.channel_response = channel_response
299
+ return initializer
300
+
301
+
302
+ #####
303
+ # AMI file parser.
304
+ #####
305
+
306
+ # ignore cases.
307
+ whitespace = regex(r"\s+", re.MULTILINE)
308
+ comment = regex(r"\|.*")
309
+ ignore = many(whitespace | comment)
310
+
311
+
312
+ def lexeme(p):
313
+ """Lexer for words."""
314
+ return p << ignore # skip all ignored characters.
315
+
316
+
317
+ def int2tap(x):
318
+ """Convert integer to tap position."""
319
+ x = x.strip()
320
+ if x[0] == "-":
321
+ res = "pre" + x[1:]
322
+ else:
323
+ res = "post" + x
324
+ return res
325
+
326
+
327
+ lparen = lexeme(string("("))
328
+ rparen = lexeme(string(")"))
329
+ number = lexeme(regex(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"))
330
+ integ = lexeme(regex(r"[-+]?[0-9]+"))
331
+ nat = lexeme(regex(r"[0-9]+"))
332
+ tap_ix = (integ << whitespace).parsecmap(int2tap)
333
+ symbol = lexeme(regex(r"[0-9a-zA-Z_][^\s()]*"))
334
+ true = lexeme(string("True")).result(True)
335
+ false = lexeme(string("False")).result(False)
336
+ ami_string = lexeme(regex(r'"[^"]*"'))
337
+
338
+ atom = number | symbol | ami_string | (true | false)
339
+ node_name = tap_ix ^ symbol # `tap_ix` is new and gives the tap position; negative positions are allowed.
340
+
341
+
342
+ @generate("AMI node")
343
+ def node():
344
+ "Parse AMI node."
345
+ yield lparen
346
+ label = yield node_name
347
+ values = yield many(expr)
348
+ yield rparen
349
+ return (label, values)
350
+
351
+
352
+ expr = atom | node
353
+ ami_defs = ignore >> node
354
+
355
+
356
+ def proc_branch(branch):
357
+ """Process a branch in a AMI parameter definition tree.
358
+
359
+ That is, build a dictionary from a pair containing:
360
+ - a parameter name, and
361
+ - a list of either:
362
+ - parameter definition tags, or
363
+ - subparameters.
364
+
365
+ We distinguish between the two possible kinds of payloads, by
366
+ peaking at the names of the first two items in the list and noting
367
+ whether they are keys of 'AMIParameter._param_def_tag_procs'.
368
+ We have to do this twice, due to the dual use of the 'Description'
369
+ tag and the fact that we have no guarantee of any particular
370
+ ordering of subparameter branch items.
371
+
372
+ Args:
373
+ p (str, list): A pair, as described above.
374
+
375
+ Returns:
376
+ (str, dict): A pair containing:
377
+ err_str: String containing any errors or warnings encountered,
378
+ while building the parameter dictionary.
379
+ param_dict: Resultant parameter dictionary.
380
+ """
381
+ results = ("", {}) # Empty Results
382
+ if len(branch) != 2:
383
+ if not branch:
384
+ err_str = "ERROR: Empty branch provided to proc_branch()!\n"
385
+ else:
386
+ err_str = f"ERROR: Malformed item: {branch[0]}\n"
387
+ results = (err_str, {})
388
+
389
+ param_name = branch[0]
390
+ param_tags = branch[1]
391
+
392
+ if not param_tags:
393
+ err_str = f"ERROR: No tags/subparameters provided for parameter, '{param_name}'\n"
394
+ results = (err_str, {})
395
+
396
+ try:
397
+ if (
398
+ (len(param_tags) > 1)
399
+ and ( # noqa: W503
400
+ param_tags[0][0] in AMIParameter._param_def_tag_procs # pylint: disable=protected-access # noqa: W503
401
+ )
402
+ and ( # noqa: W503
403
+ param_tags[1][0] in AMIParameter._param_def_tag_procs # pylint: disable=protected-access # noqa: W503
404
+ )
405
+ ):
406
+ try:
407
+ results = ("", {param_name: AMIParameter(param_name, param_tags)})
408
+ except AMIParamError as err:
409
+ results = (str(err), {})
410
+ elif param_name == "Description":
411
+ results = ("", {"description": param_tags[0].strip('"')})
412
+ else:
413
+ err_str = ""
414
+ param_dict = {}
415
+ param_dict[param_name] = {}
416
+ for param_tag in param_tags:
417
+ temp_str, temp_dict = proc_branch(param_tag)
418
+ param_dict[param_name].update(temp_dict)
419
+ if temp_str:
420
+ err_str = (
421
+ f"Error returned by recursive call, while processing parameter, '{param_name}':\n{temp_str}"
422
+ )
423
+ results = (err_str, param_dict)
424
+
425
+ results = (err_str, param_dict)
426
+ except Exception: # pylint: disable=broad-exception-caught
427
+ print(f"Error processing branch:\n{param_tags}")
428
+ return results
429
+
430
+
431
+ def parse_ami_param_defs(param_str): # pylint: disable=too-many-branches
432
+ """Parse the contents of a IBIS-AMI parameter definition file.
433
+
434
+ Args:
435
+ param_str (str): The contents of the file, as a single string.
436
+
437
+ Example:
438
+ ::
439
+
440
+ with open(<ami_file_name>) as ami_file:
441
+ param_str = ami_file.read()
442
+ (err_str, param_dict) = parse_ami_param_defs(param_str)
443
+
444
+ Returns:
445
+ (str, dict): A pair containing:
446
+ err_str:
447
+ - None, if parser succeeds.
448
+ - Helpful message, if it fails.
449
+ param_dict: Dictionary containing parameter definitions.
450
+ (Empty, on failure.)
451
+ It has a single key, at the top level, which is the
452
+ model root name. This key indexes the actual
453
+ parameter dictionary, which has the following
454
+ structure::
455
+
456
+ {
457
+ 'description' : <optional model description string>
458
+ 'Reserved_Parameters' : <dictionary of reserved parameter defintions>
459
+ 'Model_Specific' : <dictionary of model specific parameter definitions>
460
+ }
461
+
462
+ The keys of the 'Reserved_Parameters' dictionary are
463
+ limited to those called out in the IBIS-AMI
464
+ specification.
465
+
466
+ The keys of the 'Model_Specific' dictionary can be
467
+ anything.
468
+
469
+ The values of both are either:
470
+ - instances of class *AMIParameter*, or
471
+ - sub-dictionaries following the same pattern.
472
+ """
473
+ try:
474
+ res = ami_defs.parse(param_str)
475
+ except ParseError as pe:
476
+ err_str = f"Expected {pe.expected} at {pe.loc()} in:\n{pe.text[pe.index:]}"
477
+ return err_str, {}
478
+
479
+ err_str, param_dict = proc_branch(res)
480
+ if err_str:
481
+ return (err_str, {"res": res, "dict": param_dict})
482
+
483
+ reserved_found = False
484
+ init_returns_impulse_found = False
485
+ getwave_exists_found = False
486
+ model_spec_found = False
487
+ params = list(param_dict.items())[0][1]
488
+ for label in list(params.keys()):
489
+ if label == "Reserved_Parameters":
490
+ reserved_found = True
491
+ tmp_params = params[label]
492
+ for param_name in list(tmp_params.keys()):
493
+ if param_name not in AMIParameter.RESERVED_PARAM_NAMES:
494
+ err_str += f"WARNING: Unrecognized reserved parameter name, '{param_name}', found in parameter definition string!\n"
495
+ continue
496
+ param = tmp_params[param_name]
497
+ if param.pname == "AMI_Version":
498
+ if param.pusage != "Info" or param.ptype != "String":
499
+ err_str += "WARNING: Malformed 'AMI_Version' parameter.\n"
500
+ elif param.pname == "Init_Returns_Impulse":
501
+ init_returns_impulse_found = True
502
+ elif param.pname == "GetWave_Exists":
503
+ getwave_exists_found = True
504
+ elif label == "Model_Specific":
505
+ model_spec_found = True
506
+ elif label == "description":
507
+ pass
508
+ else:
509
+ err_str += f"WARNING: Unrecognized group with label, '{label}', found in parameter definition string!\n"
510
+
511
+ if not reserved_found:
512
+ err_str += "ERROR: Reserved parameters section not found! It is required."
513
+
514
+ if not init_returns_impulse_found:
515
+ err_str += "ERROR: Reserved parameter, 'Init_Returns_Impulse', not found! It is required."
516
+
517
+ if not getwave_exists_found:
518
+ err_str += "ERROR: Reserved parameter, 'GetWave_Exists', not found! It is required."
519
+
520
+ if not model_spec_found:
521
+ err_str += "WARNING: Model specific parameters section not found!"
522
+
523
+ return (err_str, param_dict)
524
+
525
+
526
+ def make_gui(params: Parameters) -> tuple[Group, list[TraitType]]:
527
+ """
528
+ Builds top-level ``Group`` and list of ``Trait`` s from AMI parameter dictionary.
529
+
530
+ Args:
531
+ params: Dictionary of AMI parameters to be configured.
532
+
533
+ Returns:
534
+ A pair consisting of:
535
+
536
+ - the top-level ``Group`` for the ``View``, and
537
+ - a list of new ``Trait`` s created.
538
+
539
+ Notes:
540
+ 1. The dictionary passed through ``params`` may have sub-dictionaries.
541
+ The window layout will reflect this nesting.
542
+ """
543
+
544
+ gui_items: list[Item | Group] = []
545
+ new_traits: list[tuple[str, TraitType]] = []
546
+ pnames = list(params.keys())
547
+ pnames.sort()
548
+ for pname in pnames:
549
+ gui_item, new_trait = make_gui_items(pname, params[pname])
550
+ gui_items.extend(gui_item)
551
+ new_traits.extend(new_trait)
552
+
553
+ return (HGroup(*gui_items), new_traits)
554
+
555
+
556
+ def make_gui_items( # pylint: disable=too-many-locals,too-many-branches
557
+ pname: str,
558
+ param: AMIParameter | Parameters
559
+ ) -> tuple[list[Item | Group], list[tuple[str, TraitType]]]:
560
+ """
561
+ Builds list of GUI items and list of traits from AMI parameter or dictionary.
562
+
563
+ Args:
564
+ pname: Parameter or sub-group name.
565
+ param: AMI parameter or dictionary of AMI parameters to be configured.
566
+
567
+ Returns:
568
+ A pair consisting of:
569
+
570
+ - the list of GUI items for the ``View``, and
571
+ - the list of new ``Trait`` s created.
572
+
573
+ Notes:
574
+ 1. A dictionary passed through ``param`` may have sub-dictionaries.
575
+ These will be converted into sub- ``Group`` s in the returned list of GUI items.
576
+ """
577
+
578
+ if isinstance(param, AMIParameter): # pylint: disable=no-else-return
579
+ pusage = param.pusage
580
+ if pusage not in ("In", "InOut"):
581
+ return ([], [])
582
+
583
+ if param.ptype == "Boolean":
584
+ return ([Item(pname, tooltip=param.pdescription)], [(pname, Bool(param.pvalue))])
585
+
586
+ pformat = param.pformat
587
+ match pformat:
588
+ case "Value": # Value
589
+ the_trait = Trait(param.pvalue)
590
+ case "Range":
591
+ the_trait = Range(param.pmin, param.pmax, param.pvalue)
592
+ case "List":
593
+ list_tips = param.plist_tip
594
+ default = param.pdefault
595
+ if list_tips:
596
+ tmp_dict: dict[str, Any] = {}
597
+ tmp_dict.update(list(zip(list_tips, param.pvalue)))
598
+ val = list(tmp_dict.keys())[0]
599
+ if default:
600
+ for tip in tmp_dict.items():
601
+ if tip[1] == default:
602
+ val = tip[0]
603
+ break
604
+ the_trait = Trait(val, tmp_dict)
605
+ else:
606
+ val = default if default else param.pvalue[0]
607
+ the_trait = Enum([val] + param.pvalue)
608
+ case _:
609
+ raise ValueError(f"Unrecognized AMI parameter format: {pformat}!")
610
+ if the_trait.metadata:
611
+ the_trait.metadata.update({"transient": False}) # Required to support modal dialogs.
612
+ else:
613
+ the_trait.metadata = {"transient": False}
614
+ return ([Item(name=pname, label=pname.split("_")[-1], tooltip=param.pdescription)], [(pname, the_trait)])
615
+
616
+ else: # subparameter branch
617
+ gui_items: list[Item | Group] = []
618
+ new_traits: list[tuple[str, TraitType]] = []
619
+ subparam_names = list(param.keys())
620
+ subparam_names.sort()
621
+ group_desc = None
622
+
623
+ # Build GUI items for this branch.
624
+ for subparam_name in subparam_names:
625
+ if subparam_name == "description":
626
+ group_desc = param[subparam_name]
627
+ else:
628
+ tmp_items, tmp_traits = make_gui_items(pname + "_" + subparam_name, param[subparam_name])
629
+ gui_items.extend(tmp_items)
630
+ new_traits.extend(tmp_traits)
631
+
632
+ if group_desc:
633
+ gui_items = [Item(label=group_desc)] + gui_items
634
+
635
+ return ([VGroup(*gui_items, label=pname.split("_")[-1], show_border=True)], new_traits)
pyibisami/common.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ Definitions common to all PyIBIS-AMI modules.
3
+
4
+ Original author: David Banas <capn.freako@gmail.com>
5
+
6
+ Original date: May 15, 2024
7
+
8
+ Copyright (c) 2024 David Banas; all rights reserved World wide.
9
+ """
10
+
11
+ from typing import Any, TypeAlias, TypeVar
12
+ import numpy.typing as npt # type: ignore
13
+ from scipy.linalg import convolution_matrix, lstsq
14
+
15
+ Real = TypeVar("Real", float, float)
16
+ Comp = TypeVar("Comp", complex, complex)
17
+ Rvec: TypeAlias = npt.NDArray[Real]
18
+ Cvec: TypeAlias = npt.NDArray[Comp]
19
+
20
+ PI: float = 3.141592653589793238462643383279502884
21
+ TWOPI: float = 2.0 * PI
22
+
23
+ TestConfig: TypeAlias = tuple[str, tuple[dict[str, Any], dict[str, Any]]]
24
+ TestSweep: TypeAlias = tuple[str, str, list[TestConfig]]
25
+
26
+
27
+ def deconv_same(y: Rvec, x: Rvec) -> Rvec:
28
+ """
29
+ Deconvolve input from output, to recover filter response, for same length I/O.
30
+
31
+ Args:
32
+ y: output signal
33
+ x: input signal
34
+
35
+ Returns:
36
+ h: filter impulse response.
37
+ """
38
+ A = convolution_matrix(x, len(y), "same")
39
+ h, _, _, _ = lstsq(A, y)
40
+ return h
File without changes