pyadi-jif 0.1.0__py2.py3-none-any.whl → 0.1.1__py2.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 (43) hide show
  1. adijif/__init__.py +2 -2
  2. adijif/clocks/ad9523_1_bf.py +3 -3
  3. adijif/clocks/ad9528_bf.py +1 -1
  4. adijif/clocks/hmc7044_bf.py +1 -1
  5. adijif/clocks/ltc6952.py +2 -2
  6. adijif/clocks/ltc6952_bf.py +1 -1
  7. adijif/clocks/ltc6953.py +2 -2
  8. adijif/converters/__init__.py +12 -1
  9. adijif/converters/ad9081.py +1 -1
  10. adijif/converters/ad9081_dp.py +2 -0
  11. adijif/converters/ad9084.py +29 -7
  12. adijif/converters/ad9084_draw.py +20 -14
  13. adijif/converters/ad9084_util.py +24 -5
  14. adijif/converters/ad9088_dp.py +111 -0
  15. adijif/converters/converter.py +153 -2
  16. adijif/draw.py +11 -2
  17. adijif/fpgas/xilinx/__init__.py +4 -2
  18. adijif/jesd.py +3 -1
  19. adijif/plls/adf4030.py +6 -1
  20. adijif/plls/adf4382.py +0 -1
  21. adijif/solvers.py +5 -4
  22. adijif/system.py +16 -1
  23. adijif/tools/explorer/cli.py +36 -0
  24. adijif/tools/explorer/main.py +41 -0
  25. adijif/tools/explorer/src/pages/__init__.py +16 -0
  26. adijif/tools/explorer/src/pages/clockconfigurator.py +193 -0
  27. adijif/tools/explorer/src/pages/helpers/datapath.py +85 -0
  28. adijif/tools/explorer/src/pages/helpers/drawers.py +87 -0
  29. adijif/tools/explorer/src/pages/helpers/jesd.py +140 -0
  30. adijif/tools/explorer/src/pages/jesdmodeselector.py +209 -0
  31. adijif/tools/explorer/src/pages/systemconfigurator.py +252 -0
  32. adijif/tools/explorer/src/state.py +141 -0
  33. adijif/tools/explorer/src/utils.py +37 -0
  34. adijif/types.py +23 -4
  35. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/METADATA +36 -12
  36. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/RECORD +41 -30
  37. pyadi_jif-0.1.1.dist-info/entry_points.txt +2 -0
  38. adijif/d2/__init__.py +0 -26
  39. adijif/d2/d2lib.h +0 -81
  40. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/WHEEL +0 -0
  41. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/licenses/AUTHORS.rst +0 -0
  42. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/licenses/LICENSE +0 -0
  43. {pyadi_jif-0.1.0.dist-info → pyadi_jif-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,252 @@
1
+ """System configurator page."""
2
+
3
+ from typing import Optional
4
+
5
+ import pandas as pd
6
+ import streamlit as st
7
+
8
+ import adijif
9
+ from adijif.clocks import supported_parts as csp
10
+ from adijif.converters import supported_parts as xsp
11
+
12
+ # from adijif.utils import get_jesd_mode_from_params
13
+ from ..utils import Page
14
+ from .helpers.datapath import gen_datapath
15
+
16
+ # Clocks
17
+ options_to_skip = ["list_references_available", "d_syspulse"]
18
+ parts_to_ignore = ["ad9545", "ad9523_1"]
19
+ sp = [p for p in csp if p not in parts_to_ignore]
20
+ # Put HMC7044 at the front
21
+ sp = [p for p in sp if p != "hmc7044"]
22
+ sp.insert(0, "hmc7044")
23
+
24
+ # Converters
25
+
26
+
27
+ class SystemConfigurator(Page):
28
+ """System configurator tool page."""
29
+
30
+ def __init__(self, state: Optional[object]) -> None:
31
+ """Initialize system configurator page.
32
+
33
+ Args:
34
+ state: Application state object
35
+ """
36
+ self.state = state
37
+
38
+ def write(self) -> None:
39
+ """Render the system configurator page."""
40
+ st.title("System Configurator")
41
+
42
+ with st.expander("System Settings", expanded=True):
43
+ hsx = st.selectbox(
44
+ label="Select a converter part",
45
+ options=xsp,
46
+ format_func=lambda x: x.upper(),
47
+ key="converter_part_select",
48
+ )
49
+
50
+ clock = st.selectbox(
51
+ label="Select a clock part",
52
+ options=sp,
53
+ format_func=lambda x: x.upper(),
54
+ key="clock_part_select",
55
+ )
56
+
57
+ fpga_kit = st.selectbox(
58
+ label="Select an FPGA development kit",
59
+ options=adijif.xilinx._available_dev_kit_names,
60
+ format_func=lambda x: x.upper(),
61
+ key="fpga_dev_kit_select",
62
+ )
63
+
64
+ reference_rate = st.number_input(
65
+ f"Reference Rate (VCXO) for {clock.upper()} (Hz)",
66
+ value=125000000,
67
+ min_value=int(100e6),
68
+ max_value=int(400e6),
69
+ )
70
+
71
+ sys = adijif.system(hsx.lower(), clock.lower(), "xilinx", reference_rate)
72
+
73
+ # Get Converter clocking requirements
74
+ sys.Debug_Solver = False
75
+
76
+ # Get FPGA clocking requirements
77
+ sys.fpga.setup_by_dev_kit_name(fpga_kit.lower())
78
+
79
+ converter_c, fpga_c = st.columns(2)
80
+
81
+ with converter_c:
82
+ # st.header("Converter Configuration")
83
+ with st.expander("Converter Settings", expanded=True):
84
+
85
+ # Units GHz, MHz, kHz
86
+ units = st.selectbox(
87
+ label="Select units for Converter Clock",
88
+ options=["Hz", "kHz", "MHz", "GHz"],
89
+ index=2,
90
+ )
91
+ if units == "Hz":
92
+ multiplier = 1
93
+ elif units == "kHz":
94
+ multiplier = 1e3
95
+ elif units == "MHz":
96
+ multiplier = 1e6
97
+ elif units == "GHz":
98
+ multiplier = 1e9
99
+
100
+ # Converter Clock
101
+ converter_clock = st.number_input(
102
+ f"Converter Clock ({units})",
103
+ value=1e9 / multiplier,
104
+ format="%f",
105
+ min_value=1e6 / multiplier,
106
+ max_value=20e9 / multiplier,
107
+ )
108
+ # sys.converter.decimation = 1
109
+
110
+ decimation = gen_datapath(sys.converter)
111
+ sys.converter.sample_clock = converter_clock * multiplier / decimation
112
+
113
+ # JESD modes
114
+ qsm = sys.converter.quick_configuration_modes
115
+
116
+ # Flatten dict for display as a list
117
+ qsm_flat = {}
118
+ for jesdclasses in qsm:
119
+ for mode in qsm[jesdclasses]:
120
+ other_settings = (
121
+ f" (M={sys.converter.M}, "
122
+ + f"F={sys.converter.F}, K={sys.converter.K}, "
123
+ + f"Np={sys.converter.Np}, CS={sys.converter.CS}, "
124
+ + f"L={sys.converter.L}, S={sys.converter.S})"
125
+ )
126
+ qsm_flat[
127
+ f"{jesdclasses.upper()} Mode: {mode} {other_settings}"
128
+ ] = {
129
+ "mode": mode,
130
+ "jesdclass": jesdclasses,
131
+ }
132
+
133
+ mode = st.selectbox(
134
+ label="Select JESD Configuration Mode",
135
+ options=list(qsm_flat.keys()),
136
+ # options=list(qsm_flat.keys()),
137
+ # format_func=lambda x: f"{x} : {qsm_flat[x]}",
138
+ )
139
+ sys.converter.set_quick_configuration_mode(
140
+ qsm_flat[mode]["mode"], qsm_flat[mode]["jesdclass"]
141
+ )
142
+
143
+ # with clock_c:
144
+ # st.header("Clock Configuration")
145
+
146
+ # with st.container(border=True):
147
+ # st.markdown(cfg["clock"])
148
+
149
+ with fpga_c:
150
+ with st.expander("FPGA Settings", expanded=True):
151
+
152
+ # sys.fpga.ref_clock_constraint = "Unconstrained"
153
+ ref_clock_constraint = st.selectbox(
154
+ options=adijif.xilinx._ref_clock_constraint_options,
155
+ label="FPGA Reference Clock Constraint",
156
+ index=adijif.xilinx._ref_clock_constraint_options.index(
157
+ "Unconstrained"
158
+ ),
159
+ )
160
+ sys.fpga.ref_clock_constraint = ref_clock_constraint
161
+
162
+ # sys.fpga.sys_clk_select = "XCVR_QPLL0" # Use faster QPLL
163
+ sys_clk_select = st.multiselect(
164
+ options=adijif.xilinx.sys_clk_selections,
165
+ label="XCVR System Clock Source Selection",
166
+ default=adijif.xilinx.sys_clk_selections,
167
+ )
168
+ sys.fpga.sys_clk_select = sys_clk_select
169
+
170
+ # Enable all adijif.xilinx._out_clk_selections for selection by default
171
+ out_clk_select = st.multiselect(
172
+ options=adijif.xilinx._out_clk_selections,
173
+ label="XCVR Output Clock Selection",
174
+ default=adijif.xilinx._out_clk_selections,
175
+ )
176
+ sys.fpga.out_clk_select = out_clk_select
177
+
178
+ # sys.fpga.force_qpll = 1
179
+ force_qpll_options = ["Auto", "Force QPLL", "Force QPLL1", "Force CPLL"]
180
+ force_qpll_selection = st.selectbox(
181
+ options=force_qpll_options,
182
+ label="Transceiver PLL Selection",
183
+ index=0,
184
+ )
185
+ if force_qpll_selection == "Force QPLL":
186
+ sys.fpga.force_qpll = True
187
+ elif force_qpll_selection == "Force QPLL1":
188
+ sys.fpga.force_qpll1 = True
189
+ elif force_qpll_selection == "Force CPLL":
190
+ sys.fpga.force_cpll = True
191
+
192
+ with st.expander("Derived Settings", expanded=True):
193
+
194
+ # Table with lane rate and core clock
195
+ lane_rate = sys.converter.bit_clock
196
+ core_clock = (
197
+ sys.converter.bit_clock / 66
198
+ if sys.converter.jesd_class == "jesd204c"
199
+ else sys.converter.bit_clock / 40
200
+ )
201
+ sample_clock = sys.converter.sample_clock
202
+ converter_clock = sys.converter.converter_clock
203
+
204
+ # Build pandas DataFrame
205
+ df = pd.DataFrame(
206
+ {
207
+ "Setting": [
208
+ "Lane Rate (Gbps)",
209
+ "Needed Core Clock (MHz)",
210
+ "Sample Clock (MHz)",
211
+ "Converter Clock (MHz)",
212
+ ],
213
+ "Value": [
214
+ f"{lane_rate/1e9:.4f}",
215
+ f"{core_clock/1e6:.4f}",
216
+ f"{sample_clock/1e6:.4f}",
217
+ f"{converter_clock/1e6:.4f}",
218
+ ],
219
+ }
220
+ )
221
+ # Remove index
222
+ df.index = df["Setting"]
223
+ df = df.drop(columns=["Setting"])
224
+ st.table(df)
225
+
226
+ with st.expander("System Configuration", expanded=False):
227
+ try:
228
+ cfg = sys.solve()
229
+
230
+ st.subheader("Clock Configuration")
231
+ st.write(cfg["clock"])
232
+
233
+ st.subheader("Converter Configuration")
234
+ st.write(cfg["converter"])
235
+
236
+ st.subheader("FPGA Configuration")
237
+ st.write(cfg["fpga_" + sys.converter.name.upper()])
238
+
239
+ st.subheader("Converter JESD Configuration")
240
+ st.write(cfg["jesd_" + sys.converter.name.upper()])
241
+
242
+ diagram = sys.draw(cfg)
243
+
244
+ except Exception as e:
245
+ diagram = None
246
+ st.error(f"Error solving system configuration: {e}")
247
+
248
+ with st.expander("Diagram", expanded=False):
249
+ if diagram:
250
+ st.image(diagram, width="stretch")
251
+ else:
252
+ st.write("No diagram available.")
@@ -0,0 +1,141 @@
1
+ """Session state management for Streamlit application."""
2
+
3
+ from copy import deepcopy
4
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
5
+
6
+ from streamlit.runtime import get_instance
7
+ from streamlit.runtime.legacy_caching.hashing import _CodeHasher
8
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
9
+
10
+ if TYPE_CHECKING:
11
+ from streamlit.runtime.state.session_state import SessionState as Session
12
+
13
+
14
+ class _SessionState:
15
+ """Internal session state management class."""
16
+
17
+ def __init__(
18
+ self, session: "Session", hash_funcs: Optional[Dict[type, Callable[[Any], str]]]
19
+ ) -> None:
20
+ """Initialize SessionState instance."""
21
+ self.__dict__["_state"] = {
22
+ "data": {},
23
+ "hash": None,
24
+ "hasher": _CodeHasher(hash_funcs),
25
+ "is_rerun": False,
26
+ "session": session,
27
+ }
28
+
29
+ def __call__(self, **kwargs: Dict[str, Any]) -> None: # type: ignore[misc]
30
+ """Initialize state data once."""
31
+ for item, value in kwargs.items():
32
+ if item not in self._state["data"]:
33
+ self._state["data"][item] = value
34
+
35
+ def __getitem__(self, item: str) -> Any: # noqa: ANN401
36
+ """Return a saved state value, None if item is undefined."""
37
+ return self._state["data"].get(item, None)
38
+
39
+ def __getattr__(self, item: str) -> Any: # noqa: ANN401
40
+ """Return a saved state value, None if item is undefined."""
41
+ return self._state["data"].get(item, None)
42
+
43
+ def __setitem__(self, item: str, value: Any) -> None: # noqa: ANN401
44
+ """Set state value."""
45
+ self._state["data"][item] = value
46
+
47
+ def __setattr__(self, item: str, value: Any) -> None: # noqa: ANN401
48
+ """Set state value."""
49
+ self._state["data"][item] = value
50
+
51
+ def clear(self) -> None:
52
+ """Clear session state and request a rerun."""
53
+ self._state["data"].clear()
54
+ self._state["session"].request_rerun()
55
+
56
+ def sync(self) -> None:
57
+ """Rerun the app with all state values up to date.
58
+
59
+ This fixes rollbacks from the beginning.
60
+ """
61
+ # Ensure to rerun only once to avoid infinite loops
62
+ # caused by a constantly changing state value at each run.
63
+ #
64
+ # Example: state.value += 1
65
+ if self._state["is_rerun"]:
66
+ self._state["is_rerun"] = False
67
+
68
+ elif self._state["hash"] is not None:
69
+ if self._state["hash"] != self._state["hasher"].to_bytes(
70
+ self._state["data"], None
71
+ ):
72
+ self._state["is_rerun"] = True
73
+ self._state["session"].request_rerun(None)
74
+
75
+ self._state["hash"] = self._state["hasher"].to_bytes(self._state["data"], None)
76
+
77
+
78
+ def _get_session() -> Any: # noqa: ANN401
79
+ """Get the current Streamlit session."""
80
+ runtime = get_instance()
81
+ session_id = get_script_run_ctx().session_id
82
+ session_info = runtime._session_mgr.get_session_info(session_id)
83
+
84
+ if session_info is None:
85
+ raise RuntimeError("Couldn't get your Streamlit Session object.")
86
+
87
+ return session_info.session
88
+
89
+
90
+ def get_state(
91
+ hash_funcs: Optional[Dict[type, Callable[[Any], str]]] = None,
92
+ ) -> _SessionState:
93
+ """Get or create session state.
94
+
95
+ Args:
96
+ hash_funcs: Optional hash functions for state comparison
97
+
98
+ Returns:
99
+ Session state object
100
+ """
101
+ session = _get_session()
102
+
103
+ if not hasattr(session, "_custom_session_state"):
104
+ session._custom_session_state = _SessionState(session, hash_funcs)
105
+
106
+ return session._custom_session_state
107
+
108
+
109
+ # Only used for separating namespace
110
+ # Everything can be saved at state variable as well.
111
+ CONFIG_DEFAULTS: Dict[str, Any] = {"slider_value": 0}
112
+
113
+
114
+ def provide_state(
115
+ hash_funcs: Optional[Dict[type, Callable[[Any], str]]] = None,
116
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
117
+ """Decorator to provide state to a function.
118
+
119
+ Args:
120
+ hash_funcs: Optional hash functions for state comparison
121
+
122
+ Returns:
123
+ Decorator function
124
+ """
125
+
126
+ def inner(func: Callable[..., Any]) -> Callable[..., Any]: # noqa: ANN401
127
+ """Inner decorator function."""
128
+
129
+ def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
130
+ """Wrapper function that injects state."""
131
+ state = get_state(hash_funcs=hash_funcs)
132
+ if state.client_config is None:
133
+ state.client_config = deepcopy(CONFIG_DEFAULTS)
134
+
135
+ return_value = func(*args, state=state, **kwargs) # noqa: B026
136
+ state.sync()
137
+ return return_value
138
+
139
+ return wrapper
140
+
141
+ return inner
@@ -0,0 +1,37 @@
1
+ """Utility functions and base classes for the explorer."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ import streamlit as st
6
+
7
+ # st.set_page_config(layout="wide")
8
+
9
+
10
+ class Page(ABC):
11
+ """Base class for all page types."""
12
+
13
+ @abstractmethod
14
+ def write(self) -> None:
15
+ """Render the page content."""
16
+ pass
17
+
18
+
19
+ def add_custom_css() -> None:
20
+ """Add custom CSS styling to the Streamlit app."""
21
+ st.html(
22
+ """
23
+ <style>
24
+ .stMainBlockContainer {
25
+ max-width:1200px;
26
+ }
27
+ </style>
28
+ """,
29
+ )
30
+ # st .markdown(
31
+ # """
32
+ # <style>
33
+ # section.main > div {max-width:1000px;}
34
+ # </style>
35
+ # """,
36
+ # unsafe_allow_html=True,
37
+ # )
adijif/types.py CHANGED
@@ -95,16 +95,35 @@ class arb_source:
95
95
 
96
96
  _max_scalar = int(1e11)
97
97
 
98
- def __init__(self, name: str) -> None:
98
+ def __init__(
99
+ self,
100
+ name: str,
101
+ a_min: Union[None, int] = None,
102
+ b_min: Union[None, int] = None,
103
+ a_max: Union[None, int] = None,
104
+ b_max: Union[None, int] = None,
105
+ ) -> None:
99
106
  """Arbitrary source for solver.
100
107
 
101
108
  Args:
102
109
  name (str): Name of source
110
+ a_min (int, optional): Minimum value for numerator. Defaults to None.
111
+ b_min (int, optional): Minimum value for denominator. Defaults to None.
112
+ a_max (int, optional): Maximum value for numerator. Defaults to None.
113
+ b_max (int, optional): Maximum value for denominator. Defaults to None.
103
114
  """
104
115
  self.name = name
105
-
106
- self._a = integer_var(0, self._max_scalar, name=name + "_a")
107
- self._b = integer_var(0, self._max_scalar, name=name + "_b")
116
+ if a_min is None:
117
+ a_min = 0
118
+ if a_max is None:
119
+ a_max = self._max_scalar
120
+ if b_min is None:
121
+ b_min = 0
122
+ if b_max is None:
123
+ b_max = self._max_scalar
124
+
125
+ self._a = integer_var(a_min, a_max, name=name + "_a")
126
+ self._b = integer_var(b_min, b_max, name=name + "_b")
108
127
 
109
128
  def __call__(self, model: Union[GEKKO, CpoModel]) -> Dict:
110
129
  """Generate arbitrary source for solver.
@@ -1,29 +1,39 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyadi-jif
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Python interface and configurator for ADI JESD Interface Framework
5
5
  Author-email: "Travis F. Collins" <travis.collins@analog.com>
6
6
  Maintainer: Analog Devices, Inc.
7
7
  Maintainer-email: Travis Collins <travis.collins@analog.com>
8
8
  License: EPL-2.0
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.9
9
13
  Requires-Python: >=3.9
10
14
  Description-Content-Type: text/markdown
11
15
  License-File: LICENSE
12
16
  License-File: AUTHORS.rst
13
- Requires-Dist: numpy<2
17
+ Requires-Dist: numpy
14
18
  Requires-Dist: openpyxl>=3.1.3
15
- Requires-Dist: pandas[openpyxl]>=1.1.5
19
+ Requires-Dist: pandas>=1.1.5
16
20
  Provides-Extra: cplex
17
21
  Requires-Dist: cplex; extra == "cplex"
18
- Requires-Dist: docplex==2.24.231; extra == "cplex"
22
+ Requires-Dist: docplex; extra == "cplex"
19
23
  Provides-Extra: gekko
20
24
  Requires-Dist: gekko; extra == "gekko"
25
+ Provides-Extra: draw
26
+ Requires-Dist: pyd2lang-native==0.0.3; extra == "draw"
27
+ Provides-Extra: tools
28
+ Requires-Dist: streamlit; extra == "tools"
21
29
  Dynamic: license-file
22
30
 
23
31
  # pyadi-jif: Python Configurator for ADI JESD204 Interface Framework (JIF)
24
32
 
25
33
  A framework to simplify the use of JESD204 with Analog Devices, Inc. data converters and clock chips.
26
34
 
35
+ **New:** Try the interactive web-based [JIF Tools Explorer](#jif-tools-explorer) for a graphical interface!
36
+
27
37
  <p align="center">
28
38
  <img src="doc/source/imgs/PyADI-JIF_logo.png" width="500" alt="PyADI-JIF Logo"> </br>
29
39
  </p>
@@ -33,7 +43,7 @@ A framework to simplify the use of JESD204 with Analog Devices, Inc. data conver
33
43
  <a href="https://opensource.org/licenses/">
34
44
  <img src="https://img.shields.io/badge/License-EPL%20v2-blue.svg" alt="EPL v2.0>
35
45
  </a>
36
-
46
+
37
47
  <a href="https://github.com/analogdevicesinc/pyadi-jif/actions/workflows/tests.yml">
38
48
  <img src="https://github.com/analogdevicesinc/pyadi-jif/actions/workflows/tests.yml/badge.svg" alt="Test Status">
39
49
  </a>
@@ -41,22 +51,36 @@ A framework to simplify the use of JESD204 with Analog Devices, Inc. data conver
41
51
  <a href="https://codecov.io/gh/analogdevicesinc/pyadi-jif">
42
52
  <img src="https://codecov.io/gh/analogdevicesinc/pyadi-jif/branch/main/graph/badge.svg?token=WVSRCSXFWL" alt="Coverage Status">
43
53
  </a>
44
- </p>
45
-
46
- ## Installation
54
+ </p>
55
+
56
+ ## Installation
47
57
 
48
58
  Install JIF with pip
49
59
 
50
- ```bash
51
- pip install --index-url https://test.pypi.org/simple/ pyadi-jif
60
+ ```bash
61
+ pip install 'pyadi-jif[cplex]'
62
+ ```
63
+
64
+ ## JIF Tools Explorer
65
+
66
+ Launch the interactive web-based tools for JESD204 configuration:
67
+
68
+ ```bash
69
+ jiftools
52
70
  ```
53
71
 
72
+ The JIF Tools Explorer provides:
73
+ - **JESD204 Mode Selector** - Find and filter valid JESD204 modes for ADI converters
74
+ - **Clock Configurator** - Configure ADI clock chips (HMC7044, AD9545, etc.)
75
+ - **System Configurator** - Complete end-to-end system design (FPGA + Converter + Clock)
76
+
77
+ See the [Tools Documentation](https://analogdevicesinc.github.io/pyadi-jif/main/tools.html) for detailed usage guide.
54
78
 
55
79
  ## Documentation
56
80
 
57
- [Documentation](https://analogdevicesinc.github.io/pyadi-jif/main/)
81
+ [Documentation](https://analogdevicesinc.github.io/pyadi-jif)
82
+
58
83
 
59
-
60
84
  ## License
61
85
 
62
86
  [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/)