PyIBIS-AMI 9.1.0__tar.gz → 9.3.0__tar.gz

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 (66) hide show
  1. {pyibis_ami-9.1.0/src/pyibis_ami.egg-info → pyibis_ami-9.3.0}/PKG-INFO +17 -14
  2. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/pyproject.toml +24 -14
  3. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0/src/pyibis_ami.egg-info}/PKG-INFO +17 -14
  4. pyibis_ami-9.3.0/src/pyibis_ami.egg-info/requires.txt +15 -0
  5. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/config.py +4 -5
  6. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/model.py +14 -14
  7. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/parameter.py +68 -1
  8. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/parser.py +157 -22
  9. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/reserved_parameter_names.py +1 -1
  10. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ibis/file.py +29 -29
  11. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ibis/model.py +5 -5
  12. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ibis/parser.py +5 -4
  13. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/ami_test_config.py +3 -3
  14. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/ami_tests.py +14 -13
  15. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/ami_tests_helpers.py +2 -2
  16. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/ibis_file_tests.py +3 -3
  17. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/test_defs.py +3 -2
  18. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/test_models.py +0 -0
  19. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/util.py +1 -1
  20. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/tools/run_notebook.py +5 -7
  21. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/util/plot.py +4 -4
  22. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/util/reportlab.py +2 -2
  23. pyibis_ami-9.1.0/src/pyibis_ami.egg-info/requires.txt +0 -13
  24. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/LICENSE +0 -0
  25. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/MANIFEST.in +0 -0
  26. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/README.md +0 -0
  27. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/ami.rst +0 -0
  28. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/conf.py +0 -0
  29. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/ibis.rst +0 -0
  30. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/index.rst +0 -0
  31. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/install.rst +0 -0
  32. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/modules.rst +0 -0
  33. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/docs/source/tools.rst +0 -0
  34. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/all_taps.run +0 -0
  35. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/bandwidth.run +0 -0
  36. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/dummy.em +0 -0
  37. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/examples.md +0 -0
  38. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/freq_resp.em +0 -0
  39. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/impulse.em +0 -0
  40. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/impulse_response_8ma.txt +0 -0
  41. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/mode.run +0 -0
  42. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/stratix4_ami_tx_runs/all_taps.run +0 -0
  43. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/stratix4_ami_tx_runs/test_results.xml +0 -0
  44. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/examples/test_results/test_results.xsl +0 -0
  45. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/setup.cfg +0 -0
  46. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibis_ami.egg-info/SOURCES.txt +0 -0
  47. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibis_ami.egg-info/dependency_links.txt +0 -0
  48. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibis_ami.egg-info/entry_points.txt +0 -0
  49. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibis_ami.egg-info/top_level.txt +0 -0
  50. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/IBIS_AMI_Checker.ipynb +0 -0
  51. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/IBIS_AMI_Tester.ipynb +0 -0
  52. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/__init__.py +0 -0
  53. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/__main__.py +0 -0
  54. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/__init__.py +0 -0
  55. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/generic.ami.em +0 -0
  56. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ami/generic.ibs.em +0 -0
  57. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/common.py +0 -0
  58. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/ibis/__init__.py +0 -0
  59. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/testing/__init__.py +0 -0
  60. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/tools/__init__.py +0 -0
  61. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/tools/run_tests.py +0 -0
  62. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/tools/test_results.xsl +0 -0
  63. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/util/ami.py +0 -0
  64. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/src/pyibisami/util/misc.py +0 -0
  65. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/tests/test_ami_test_config.py +0 -0
  66. {pyibis_ami-9.1.0 → pyibis_ami-9.3.0}/tests/test_run_tests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyibis-ami
3
- Version: 9.1.0
3
+ Version: 9.3.0
4
4
  Summary: Facilitates working directly with IBIS-AMI DLLs from the Python command prompt.
5
5
  Author-email: David Banas <capn.freako@gmail.com>
6
6
  Project-URL: Documentation, https://pyibis-ami.readthedocs.io/en/latest/
@@ -17,25 +17,28 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
22
  Classifier: Topic :: System :: Emulators
22
23
  Classifier: Topic :: Utilities
23
- Requires-Python: <3.13,>=3.10
24
+ Requires-Python: <3.14,>=3.10
24
25
  Description-Content-Type: text/markdown
25
26
  License-File: LICENSE
26
- Requires-Dist: chaco>=6.0.0
27
- Requires-Dist: click>=8.1.3
28
- Requires-Dist: empy>=3.3.4
29
- Requires-Dist: importlib_resources
30
- Requires-Dist: jupyter
31
- Requires-Dist: lxml-html-clean>=0.4.4
27
+ Requires-Dist: chaco==6.1.1
28
+ Requires-Dist: click==8.4.2
29
+ Requires-Dist: empy==4.2.1
30
+ Requires-Dist: importlib_resources==7.1.0
31
+ Requires-Dist: jupyter==1.1.1
32
+ Requires-Dist: lxml-html-clean==0.4.5
32
33
  Requires-Dist: matplotlib>=3.6.1
33
- Requires-Dist: numpy<1.27,>=1.26
34
- Requires-Dist: papermill
35
- Requires-Dist: parsec>=3.15
36
- Requires-Dist: reportlab>=4.4.10
37
- Requires-Dist: scipy>=1.9
38
- Requires-Dist: types-reportlab>=4.4.10.20260223
34
+ Requires-Dist: numpy>=1.26
35
+ Requires-Dist: papermill==2.7.0
36
+ Requires-Dist: parsec==3.17
37
+ Requires-Dist: reportlab==5.0.0
38
+ Requires-Dist: scipy>=1.15
39
+ Requires-Dist: traits==7.1.0
40
+ Requires-Dist: traitsui==8.0.0
41
+ Requires-Dist: types-reportlab==4.5.1.20260712
39
42
  Dynamic: license-file
40
43
 
41
44
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/capn-freako/PyAMI/master.svg)](https://results.pre-commit.ci/latest/github/capn-freako/PyAMI/master)
@@ -5,25 +5,27 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pyibis-ami"
7
7
  description = "Facilitates working directly with IBIS-AMI DLLs from the Python command prompt."
8
- version = "9.1.0"
8
+ version = "9.3.0"
9
9
  authors = [ {name = "David Banas", email = "capn.freako@gmail.com"}
10
10
  ]
11
11
  readme = "README.md"
12
- requires-python = ">=3.10,<3.13"
12
+ requires-python = ">=3.10,<3.14"
13
13
  dependencies = [
14
- "chaco>=6.0.0",
15
- "click>=8.1.3",
16
- "empy>=3.3.4",
17
- "importlib_resources",
18
- "jupyter",
19
- "lxml-html-clean>=0.4.4",
14
+ "chaco==6.1.1",
15
+ "click==8.4.2",
16
+ "empy==4.2.1",
17
+ "importlib_resources==7.1.0",
18
+ "jupyter==1.1.1",
19
+ "lxml-html-clean==0.4.5",
20
20
  "matplotlib>=3.6.1",
21
- "numpy>=1.26,<1.27",
22
- "papermill",
23
- "parsec>=3.15",
24
- "reportlab>=4.4.10",
25
- "scipy>=1.9",
26
- "types-reportlab>=4.4.10.20260223",
21
+ "numpy>=1.26",
22
+ "papermill==2.7.0",
23
+ "parsec==3.17",
24
+ "reportlab==5.0.0",
25
+ "scipy>=1.15",
26
+ "traits==7.1.0",
27
+ "traitsui==8.0.0",
28
+ "types-reportlab==4.5.1.20260712",
27
29
  ]
28
30
  keywords=["ibis-ami"]
29
31
  classifiers=[
@@ -36,6 +38,7 @@ classifiers=[
36
38
  "Programming Language :: Python :: 3.10",
37
39
  "Programming Language :: Python :: 3.11",
38
40
  "Programming Language :: Python :: 3.12",
41
+ "Programming Language :: Python :: 3.13",
39
42
  "Programming Language :: Python :: Implementation :: CPython",
40
43
  "Topic :: System :: Emulators",
41
44
  "Topic :: Utilities",
@@ -93,6 +96,13 @@ ignore = [
93
96
  exclude = [
94
97
  "*.ipynb"
95
98
  ]
99
+ lint.ignore = [
100
+ "UP045", # "Optional[X] -> "X | None"
101
+ "I001", # import formatting
102
+ "BLE001", # blind exceptions
103
+ "UP031", # format specifiers instead of '%'
104
+ "RUF022", # unsorted all
105
+ ]
96
106
 
97
107
  [[tool.mypy.overrides]]
98
108
  module = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyibis-ami
3
- Version: 9.1.0
3
+ Version: 9.3.0
4
4
  Summary: Facilitates working directly with IBIS-AMI DLLs from the Python command prompt.
5
5
  Author-email: David Banas <capn.freako@gmail.com>
6
6
  Project-URL: Documentation, https://pyibis-ami.readthedocs.io/en/latest/
@@ -17,25 +17,28 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
22
  Classifier: Topic :: System :: Emulators
22
23
  Classifier: Topic :: Utilities
23
- Requires-Python: <3.13,>=3.10
24
+ Requires-Python: <3.14,>=3.10
24
25
  Description-Content-Type: text/markdown
25
26
  License-File: LICENSE
26
- Requires-Dist: chaco>=6.0.0
27
- Requires-Dist: click>=8.1.3
28
- Requires-Dist: empy>=3.3.4
29
- Requires-Dist: importlib_resources
30
- Requires-Dist: jupyter
31
- Requires-Dist: lxml-html-clean>=0.4.4
27
+ Requires-Dist: chaco==6.1.1
28
+ Requires-Dist: click==8.4.2
29
+ Requires-Dist: empy==4.2.1
30
+ Requires-Dist: importlib_resources==7.1.0
31
+ Requires-Dist: jupyter==1.1.1
32
+ Requires-Dist: lxml-html-clean==0.4.5
32
33
  Requires-Dist: matplotlib>=3.6.1
33
- Requires-Dist: numpy<1.27,>=1.26
34
- Requires-Dist: papermill
35
- Requires-Dist: parsec>=3.15
36
- Requires-Dist: reportlab>=4.4.10
37
- Requires-Dist: scipy>=1.9
38
- Requires-Dist: types-reportlab>=4.4.10.20260223
34
+ Requires-Dist: numpy>=1.26
35
+ Requires-Dist: papermill==2.7.0
36
+ Requires-Dist: parsec==3.17
37
+ Requires-Dist: reportlab==5.0.0
38
+ Requires-Dist: scipy>=1.15
39
+ Requires-Dist: traits==7.1.0
40
+ Requires-Dist: traitsui==8.0.0
41
+ Requires-Dist: types-reportlab==4.5.1.20260712
39
42
  Dynamic: license-file
40
43
 
41
44
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/capn-freako/PyAMI/master.svg)](https://results.pre-commit.ci/latest/github/capn-freako/PyAMI/master)
@@ -0,0 +1,15 @@
1
+ chaco==6.1.1
2
+ click==8.4.2
3
+ empy==4.2.1
4
+ importlib_resources==7.1.0
5
+ jupyter==1.1.1
6
+ lxml-html-clean==0.4.5
7
+ matplotlib>=3.6.1
8
+ numpy>=1.26
9
+ papermill==2.7.0
10
+ parsec==3.17
11
+ reportlab==5.0.0
12
+ scipy>=1.15
13
+ traits==7.1.0
14
+ traitsui==8.0.0
15
+ types-reportlab==4.5.1.20260712
@@ -171,7 +171,7 @@ def mk_model(
171
171
  "param_types": param_types,
172
172
  "model_name": model_name,
173
173
  "description": description,
174
- "date": str(date.today()),
174
+ "date": str(date.today()), # noqa: DTZ011
175
175
  },
176
176
  )
177
177
  try:
@@ -246,8 +246,8 @@ def mk_tests( # pylint: disable=too-many-locals
246
246
  pname.mkdir(exist_ok=True)
247
247
  pname = (pname / file_base_name).resolve()
248
248
  pname.mkdir(exist_ok=True)
249
- for fname in test_defs.keys():
250
- desc, ami_defs, sim_defs, ref_fstr = test_defs[fname]
249
+ for fname, value in test_defs.items():
250
+ desc, ami_defs, sim_defs, ref_fstr = value
251
251
  ami_str, ami_dict = ami_defs
252
252
  sim_str, sim_dict = sim_defs
253
253
  with open((pname / fname).with_suffix(".run"), "w", encoding="utf-8") as f:
@@ -258,8 +258,7 @@ def mk_tests( # pylint: disable=too-many-locals
258
258
  pdict.update(dict(sim_comb))
259
259
  f.write(f"\n('{ami_str.format(pdict=pdict)}_{sim_str.format(pdict=pdict)}', \\\n")
260
260
  f.write(f" ({{'root_name': '{file_base_name}', \\\n")
261
- for k, v in ami_comb:
262
- f.write(f" '{k}': {v}, \\\n")
261
+ f.writelines(f" '{k}': {v}, \\\n" for k, v in ami_comb)
263
262
  f.write(" }, \\\n")
264
263
  if sim_comb:
265
264
  head, *tail = sim_comb
@@ -355,11 +355,12 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
355
355
  self._info_params = init_object.info_params # pylint: disable=attribute-defined-outside-init
356
356
 
357
357
  # Check GetWave() consistency if possible.
358
- if init_object.info_params and init_object.info_params["GetWave_Exists"] and init_object.info_params["GetWave_Exists"].pvalue:
359
- if not self._amiGetWave:
360
- raise RuntimeError(
361
- "Reserved parameter `GetWave_Exists` is True, but I can't bind to `AMI_GetWave()`!"
362
- )
358
+ if (init_object.info_params and init_object.info_params["GetWave_Exists"] and
359
+ init_object.info_params["GetWave_Exists"].pvalue and
360
+ not self._amiGetWave): # noqa: E129
361
+ raise RuntimeError(
362
+ "Reserved parameter `GetWave_Exists` is True, but I can't bind to `AMI_GetWave()`!"
363
+ )
363
364
 
364
365
  # Construct the AMI parameters string.
365
366
  def sexpr(pname: str, pval: Any) -> str:
@@ -387,7 +388,7 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
387
388
  self._root_name = root_name # pylint: disable=attribute-defined-outside-init
388
389
  ami_params_in = f"({root_name} "
389
390
  for item in list(init_object.ami_params.items()):
390
- if not item[0] == "root_name":
391
+ if item[0] != "root_name":
391
392
  ami_params_in += sexpr(item[0], item[1])
392
393
  ami_params_in += ")"
393
394
  self._ami_params_in = ami_params_in.encode("utf-8") # pylint: disable=attribute-defined-outside-init
@@ -424,7 +425,7 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
424
425
  print(f"&ami_params_out = {byref(self._ami_params_out)}")
425
426
  print(f"&ami_mem_handle = {byref(self._ami_mem_handle)}") # type: ignore
426
427
  print(f"&msg = {byref(self._msg)}")
427
- raise err
428
+ raise
428
429
 
429
430
  # Initialize attributes used by getWave().
430
431
  bit_time = init_object.bit_time
@@ -441,7 +442,7 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
441
442
  )
442
443
  self._getwave_step_response_out_params = None
443
444
 
444
- def getWave(self, wave: Rvec, bits_per_call: int = 0) -> tuple[Rvec, Rvec, list[str]]: # noqa: F405
445
+ def getWave(self, wave: Rvec, bits_per_call: int = 0) -> tuple[Rvec, Rvec, list[str]]:
445
446
  """
446
447
  Performs time domain processing of input waveform, using the ``AMI_GetWave()`` function.
447
448
 
@@ -562,13 +563,12 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
562
563
  ts = self.sample_interval
563
564
  info_params = self._info_params
564
565
  ignore_bits = 0
565
- if info_params:
566
- if "Ignore_Bits" in info_params:
567
- ignore_bits = info_params["Ignore_Bits"].pvalue
566
+ if info_params and "Ignore_Bits" in info_params:
567
+ ignore_bits = info_params["Ignore_Bits"].pvalue
568
568
 
569
569
  # Capture/convert instance variables.
570
- chnl_imp = np.array(self.channel_response) * ts # input (a.k.a. - "channel") impulse response (V/sample)
571
- out_imp = np.array(self.initOut) * ts # output impulse response (V/sample)
570
+ chnl_imp = np.array(self.channel_response[:self.row_size]) * ts # input (a.k.a. - "channel") impulse response (V/sample)
571
+ out_imp = np.array(self.initOut[:self.row_size]) * ts # output impulse response (V/sample)
572
572
 
573
573
  # Calculate some needed intermediate values.
574
574
  nspui = int(ui / ts) # samps per UI
@@ -584,7 +584,7 @@ class AMIModel: # pylint: disable=too-many-instance-attributes
584
584
  ):
585
585
  pass
586
586
  else:
587
- h_model = deconv_same(out_imp, chnl_imp) # noqa: F405
587
+ h_model = deconv_same(out_imp, chnl_imp)
588
588
  rslt[IMP_RESP_INIT] = np.roll(h_model, -len(h_model) // 2 + 3 * nspui)
589
589
 
590
590
  h_init = np.roll(out_imp, pad_samps)
@@ -7,6 +7,10 @@ Original date: December 24, 2016
7
7
  Copyright (c) 2019 David Banas; all rights reserved World wide.
8
8
  """
9
9
 
10
+ from typing import ClassVar
11
+
12
+ from traits.api import Bool, Float, HasTraits, List, Str
13
+
10
14
  #####
11
15
  # AMI parameter
12
16
  #####
@@ -179,7 +183,7 @@ class AMIParameter: # pylint: disable=too-many-instance-attributes,too-few-publ
179
183
  # The idea is to allow this class to grow along with the IBIS
180
184
  # standard, without having to change any of its boilerplate.
181
185
 
182
- _param_def_tag_procs = {
186
+ _param_def_tag_procs: ClassVar = {
183
187
  "Usage": _set_usage,
184
188
  "Type": _set_type,
185
189
  "Format": _set_format,
@@ -326,3 +330,66 @@ class AMIParameter: # pylint: disable=too-many-instance-attributes,too-few-publ
326
330
 
327
331
  def __str__(self):
328
332
  return f"{self.pvalue}"
333
+
334
+
335
+ #####
336
+ # AMI parameter tuner
337
+ #####
338
+
339
+
340
+ # pylint: disable=too-many-instance-attributes,too-few-public-methods
341
+ class AmiParamTuner(HasTraits):
342
+ """Object used to populate the rows of a Tx/Rx AMI parameter tuning table.
343
+
344
+ Represents one *Model_Specific* 'In'/'InOut' AMI parameter, of 'Range'
345
+ format, offered to the user for inclusion in EQ optimization.
346
+ """
347
+
348
+ name = Str("(noname)") # Display name (fully hierarchical, e.g. "tx_preset_coeffs_main").
349
+ branch_names = List(Str) # Hierarchical path for `fetch_param_val()`/`set_param_val()`.
350
+ enabled = Bool(False) # Will participate in EQ optimization when *True*.
351
+ min_val = Float(0.0) # Minimum allowed value during optimization.
352
+ max_val = Float(0.0) # Maximum allowed value during optimization.
353
+ step = Float(0.0) # Increment used during optimization.
354
+ value = Float(0.0) # Current value.
355
+ is_int = Bool(False) # Round swept values to the nearest integer when *True*.
356
+
357
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
358
+ def __init__(
359
+ self, name: str = "(noname)", branch_names: "list[str] | None" = None, enabled: bool = False,
360
+ min_val: float = 0.0, max_val: float = 0.0, step: float = 0.0, value: float = 0.0, is_int: bool = False
361
+ ):
362
+ """
363
+ Allows user to define properties, at instantiation.
364
+
365
+ Keyword Args:
366
+ name: Parameter name/label.
367
+ Default: "(noname)"
368
+ branch_names: Hierarchical path to the parameter, rooted at "Model_Specific".
369
+ Default: []
370
+ enabled: Will participate in EQ optimization when *True*.
371
+ Default: *False*
372
+ min_val: Minimum allowed value during optimization.
373
+ Default: 0.0
374
+ max_val: Maximum allowed value during optimization.
375
+ Default: 0.0
376
+ step: Increment used during optimization.
377
+ Default: 0.0
378
+ value: Current value.
379
+ Default: 0.0
380
+ is_int: Round swept values to the nearest integer when *True*.
381
+ Default: *False*
382
+ """
383
+
384
+ # Super-class initialization is ABSOLUTELY NECESSARY, in order
385
+ # to get all the Traits/UI machinery setup correctly.
386
+ super().__init__()
387
+
388
+ self.name = name
389
+ self.branch_names = branch_names or []
390
+ self.enabled = enabled
391
+ self.min_val = min_val
392
+ self.max_val = max_val
393
+ self.step = step
394
+ self.value = value
395
+ self.is_int = is_int
@@ -11,7 +11,8 @@ from __future__ import annotations
11
11
  from ctypes import c_double
12
12
  from dataclasses import dataclass
13
13
  import re
14
- from typing import Any, Callable, NewType, Optional, TypeAlias
14
+ from typing import Any, NewType, Optional, TypeAlias
15
+ from collections.abc import Callable
15
16
 
16
17
  import numpy as np
17
18
  from numpy.typing import NDArray
@@ -21,7 +22,7 @@ from traitsui.api import Group, HGroup, Item, VGroup, View
21
22
  from traitsui.menu import ModalButtons
22
23
 
23
24
  from .model import AMIModelInitializer
24
- from .parameter import AMIParamError, AMIParameter
25
+ from .parameter import AmiParamTuner, AMIParamError, AMIParameter
25
26
  from .reserved_parameter_names import AmiReservedParameterName, RESERVED_PARAM_NAMES
26
27
 
27
28
  # New types and aliases.
@@ -47,7 +48,7 @@ class AmiLeaf:
47
48
  @dataclass(frozen=True)
48
49
  class AmiBranch:
49
50
  name: AmiName
50
- children: list["AmiNode"]
51
+ children: list[AmiNode]
51
52
 
52
53
 
53
54
  AmiNode: TypeAlias = AmiLeaf | AmiBranch
@@ -60,7 +61,7 @@ AmiRootName = NewType("AmiRootName", str)
60
61
  ReservedParamDict: TypeAlias = dict[AmiReservedParameterName, AMIParameter]
61
62
  ModelSpecificDict: TypeAlias = dict[ParamName, "'AMIParameter' | 'ModelSpecificDict'"]
62
63
 
63
- __all__ = [
64
+ __all__ = [ # ruff: ignore=RUF022
64
65
  "ParamName", "ParamValue", "Parameters", "ParamValues",
65
66
  "AmiName", "AmiAtom", "AmiNode", "AmiNodeParser", "AmiParser",
66
67
  "ami_parse", "AMIParamConfigurator"]
@@ -186,32 +187,125 @@ class AMIParamConfigurator(HasTraits):
186
187
  return _param.pvalue
187
188
  return None
188
189
 
189
- def set_param_val(self, branch_names, new_val):
190
- """Sets the value of the parameter found by traversing 'branch_names'
190
+ def set_param_val(
191
+ self: AMIParamConfigurator,
192
+ branch_names: list[str],
193
+ new_val: Any
194
+ ) -> None:
195
+ """
196
+ Sets the value of the parameter found by traversing 'branch_names'
191
197
  or raises an exception if not found.
192
198
 
193
- Note: 'branch_names' should *not* begin with 'root_name'.
194
- Note: Be careful! There is no checking done here!
199
+ Args:
200
+ branch_names: A sequence of node names, used to traverse the parameter tree.
201
+ new_val: The value to assign to the target parameter.
202
+
203
+ Notes:
204
+ 1. ``branch_names`` should *not* begin with <root_name>.
205
+ 2. Be careful! There is no checking done here!
195
206
  """
196
207
 
197
208
  param_dict = self.ami_param_defs
209
+ tname_parts: list[str] = [] # Fully hierarchical trait name, minus the root key.
210
+ root_key: Optional[str] = None
198
211
  while branch_names:
199
212
  branch_name = branch_names.pop(0)
213
+ if root_key is None:
214
+ root_key = branch_name # "Reserved_Parameters" or "Model_Specific"; not part of the trait name.
215
+ else:
216
+ tname_parts.append(branch_name)
200
217
  if branch_name in param_dict:
201
- param_dict = param_dict[branch_name]
218
+ param_dict = param_dict[branch_name] # type: ignore
202
219
  else:
203
220
  raise ValueError(
204
221
  f"Failed parameter tree search looking for: {branch_name}; available keys: {param_dict.keys()}"
205
222
  )
206
223
  if isinstance(param_dict, AMIParameter):
207
- param_dict.pvalue = new_val
208
- try:
209
- eval(f"self.set({branch_name}_={new_val})") # pylint: disable=eval-used
210
- except Exception: # pylint: disable=broad-exception-caught
211
- eval(f"self.set({branch_name}={new_val})") # pylint: disable=eval-used
224
+ # Coerce to the parameter's declared type. Notably: a `Bool` trait rejects a
225
+ # float outright, and a vendor's AMI_Init() parser may choke on "1.0" where an
226
+ # Integer-typed value (e.g. a List-format mode selector) is expected.
227
+ if param_dict.ptype == "Boolean":
228
+ if not isinstance(new_val, bool):
229
+ if isinstance(new_val, str):
230
+ match new_val:
231
+ case "FALSE" | "False" | "false":
232
+ new_val = False
233
+ case _:
234
+ new_val = bool(new_val)
235
+ else:
236
+ new_val = bool(new_val)
237
+ elif param_dict.ptype in ("Integer", "Tap"):
238
+ new_val = int(new_val)
239
+ # `pvalue`, for 'List' format, holds the list of *legal* values, not the
240
+ # current one (that lives on the Trait) -- leave it alone, to avoid corrupting it.
241
+ if param_dict.pformat != "List":
242
+ param_dict.pvalue = new_val
243
+ tname = "_".join(tname_parts)
244
+ if tname:
245
+ try:
246
+ self.trait_set(**{f"{tname}_": new_val})
247
+ except Exception: # pylint: disable=broad-exception-caught
248
+ self.trait_set(**{tname: new_val})
212
249
  else:
213
250
  raise TypeError(f"{param_dict} is not of type: AMIParameter!")
214
251
 
252
+ @property
253
+ def tunable_params(self) -> list[tuple[list[str], AMIParameter]]:
254
+ """Every *Model Specific* parameter of type 'In' or 'InOut' that is a
255
+ candidate for automated sweeping/optimization, paired with its fully
256
+ hierarchical branch name path. This includes:
257
+
258
+ - numeric 'Range'-format parameters (real min/max bounds),
259
+ - 'Boolean' parameters (True/False), and
260
+ - 'List'-format 'Integer' parameters whose legal values form a
261
+ contiguous range (e.g. a mode selector like `(List 0 1)`) --
262
+ this is deliberately conservative: a non-contiguous legal set
263
+ (e.g. `(List 6 12 18)`) can't be swept with a uniform step
264
+ without risking illegal intermediate values, so it's excluded.
265
+
266
+ The returned branch name paths are ready to pass directly into
267
+ ``fetch_param_val()``/``set_param_val()`` (i.e. - they are rooted at
268
+ ``"Model_Specific"``).
269
+ """
270
+ return _walk_tunable_params(self._model_specific_dict, ["Model_Specific"])
271
+
272
+ def mk_tap_tuners(self) -> list[AmiParamTuner]:
273
+ """Build a fresh list of `AmiParamTuner`s from this AMI model's tunable
274
+ Model_Specific parameters (see `tunable_params` for exactly which ones
275
+ qualify: Range-format, Boolean, and contiguous Integer/List "mode
276
+ selector" parameters)."""
277
+ tuners = []
278
+ for branch_names, param in self.tunable_params:
279
+ tname = "_".join(branch_names[1:]) # Drop the "Model_Specific" root.
280
+ if param.pformat == "Range":
281
+ pmin, pmax = float(param.pmin), float(param.pmax)
282
+ step = (pmax - pmin) / 10 or 1.0
283
+ is_int = param.ptype == "Integer"
284
+ value = float(param.pvalue)
285
+ elif param.pformat == "Value" and param.ptype == "Boolean":
286
+ pmin, pmax, step, is_int = 0.0, 1.0, 1.0, True
287
+ value = float(param.pvalue)
288
+ else: # List-format Integer "mode selector" (contiguous legal values).
289
+ vals = [float(v) for v in param.pvalue]
290
+ pmin, pmax, step, is_int = min(vals), max(vals), 1.0, True
291
+ # `pvalue`, for 'List' format, is the list of *legal* values, not the
292
+ # current one -- that lives on the Trait `make_gui_items()` registered.
293
+ try:
294
+ value = float(self.trait_get(tname + "_")[tname + "_"])
295
+ except Exception: # pylint: disable=broad-exception-caught
296
+ value = float(self.trait_get(tname)[tname])
297
+ tuners.append(AmiParamTuner(
298
+ name=tname,
299
+ branch_names=branch_names,
300
+ enabled=False,
301
+ min_val=pmin,
302
+ max_val=pmax,
303
+ step=step,
304
+ value=value,
305
+ is_int=is_int,
306
+ ))
307
+ return tuners
308
+
215
309
  @property
216
310
  def ami_parsing_errors(self) -> list[str]:
217
311
  """Any errors or warnings encountered, while parsing the AMI parameter
@@ -286,7 +380,7 @@ class AMIParamConfigurator(HasTraits):
286
380
  elif isinstance(param, dict): # We received a dictionary of subparameters, in 'param'.
287
381
  subs: ParamValues = {}
288
382
  for sname in param:
289
- subs.update(self.input_ami_param(param, sname, prefix=pname + "_")) # type: ignore
383
+ subs.update(self.input_ami_param(param, sname, prefix=tname + "_")) # type: ignore
290
384
  res[pname] = subs
291
385
  return res
292
386
 
@@ -332,6 +426,47 @@ class AMIParamConfigurator(HasTraits):
332
426
  return initializer
333
427
 
334
428
 
429
+ def _is_sweepable(param: AMIParameter) -> bool:
430
+ "See `AMIParamConfigurator.tunable_params` for the exact criteria."
431
+ if param.pusage not in ("In", "InOut"):
432
+ return False
433
+ if param.pformat == "Range":
434
+ return True
435
+ if param.pformat == "Value" and param.ptype == "Boolean":
436
+ return True
437
+ if param.pformat == "List" and param.ptype == "Integer":
438
+ vals = sorted(int(v) for v in param.pvalue)
439
+ return vals == list(range(vals[0], vals[-1] + 1)) # Contiguous?
440
+ return False
441
+
442
+
443
+ def _walk_tunable_params(
444
+ params: Parameters, prefix: list[str]
445
+ ) -> list[tuple[list[str], AMIParameter]]:
446
+ """Recursively collect every sweepable (see `_is_sweepable`) leaf of a
447
+ *Model Specific* parameter (sub)tree.
448
+
449
+ Args:
450
+ params: The (sub)dictionary of AMI parameters to walk.
451
+ prefix: The branch name path leading to ``params``.
452
+
453
+ Returns:
454
+ A list of (branch name path, parameter) pairs, one per sweepable leaf.
455
+ """
456
+
457
+ results: list[tuple[list[str], AMIParameter]] = []
458
+ for pname, param in params.items():
459
+ if pname == "description": # A branch's optional descriptive text, not a subparameter.
460
+ continue
461
+ path = prefix + [pname]
462
+ if isinstance(param, AMIParameter):
463
+ if _is_sweepable(param):
464
+ results.append((path, param))
465
+ else: # Subparameter branch.
466
+ results.extend(_walk_tunable_params(param, path))
467
+ return results
468
+
469
+
335
470
  #####
336
471
  # AMI file parser.
337
472
  #####
@@ -367,7 +502,8 @@ integ = lexeme(regex(r"[-+]?[0-9]+"))
367
502
  nat = lexeme(regex(r"[0-9]+"))
368
503
  # tap_ix = (integ << whitespace).parsecmap(int2tap) # Doesn't work!
369
504
  # tap_ix = integ.parsecmap(int2tap) # Breaks parsing of node names that begin with a numeral!
370
- tap_ix = integ.parsecmap(int2tap) << whitespace
505
+ # tap_ix = integ.parsecmap(int2tap) << whitespace # This fails; I don't know why.
506
+ tap_ix = lexeme(integ.parsecmap(int2tap)) # This works, as an alternative to the line above.
371
507
  symbol = lexeme(regex(r"[0-9a-zA-Z_][^\s()]*"))
372
508
  true = lexeme(string("True")).result(True)
373
509
  false = lexeme(string("False")).result(False)
@@ -557,7 +693,7 @@ def parse_ami_file_contents( # pylint: disable=too-many-locals,too-many-branche
557
693
  init_returns_impulse_found = False
558
694
  getwave_exists_found = False
559
695
  model_spec_found = False
560
- root_name, params = list(param_dict.items())[0]
696
+ root_name, params = next(iter(param_dict.items()))
561
697
  description = ""
562
698
  reserved_params_dict: dict[AmiReservedParameterName, AMIParameter] = {}
563
699
  model_specific_dict: ModelSpecificDict = {}
@@ -605,8 +741,7 @@ def parse_ami_file_contents( # pylint: disable=too-many-locals,too-many-branche
605
741
  # Legacy client code support:
606
742
  def parse_ami_param_defs(file_contents: str) -> tuple[ParseErrMsg, dict[str, Any]]:
607
743
  "The legacy version of ``parse_ami_file_contents()``."
608
- # err_msg, root_name, description, reserved_params_dict, model_specific_dict = parse_ami_file_contents(file_contents)
609
- errors, warnings, root_name, description, reserved_params_dict, model_specific_dict = parse_ami_file_contents(file_contents)
744
+ errors, _warnings, root_name, description, reserved_params_dict, model_specific_dict = parse_ami_file_contents(file_contents)
610
745
  if errors:
611
746
  err_msg = errors[0]
612
747
  else:
@@ -649,8 +784,8 @@ def make_gui(params: ModelSpecificDict) -> tuple[Group, list[tuple[str, TraitTyp
649
784
  # but no pre-existing enclosing group, in a ``VGroup``.
650
785
  # - Generate grouped parameter names and list of unique group names.
651
786
  pnames = list(params.keys())
652
- pnames_split = list(map(lambda n: n.split('_'), pnames))
653
- group_names = list(map(lambda xs: xs[0], pnames_split))
787
+ pnames_split = [n.split('_') for n in pnames]
788
+ group_names = [xs[0] for xs in pnames_split]
654
789
  grouped_pnames = list(zip(pnames, group_names))
655
790
  # The following doesn't preserve the parameter ordering of the AMI file!
656
791
  # for group_name in set(group_names): # Iterate over unique group names.
@@ -728,7 +863,7 @@ def make_gui_items( # pylint: disable=too-many-locals,too-many-branches
728
863
  if list_tips:
729
864
  tmp_dict: dict[str, Any] = {}
730
865
  tmp_dict.update(list(zip(list_tips, param.pvalue)))
731
- val = list(tmp_dict.keys())[0]
866
+ val = next(iter(tmp_dict.keys()))
732
867
  if default:
733
868
  for tip in tmp_dict.items():
734
869
  if tip[1] == default:
@@ -12,7 +12,7 @@ from dataclasses import dataclass
12
12
 
13
13
 
14
14
  @dataclass
15
- class AmiReservedParameterName():
15
+ class AmiReservedParameterName:
16
16
  "IBIS-AMI Reserved Parameter Name"
17
17
 
18
18
  pname: str