Skip to content

Commit 3a82c7b

Browse files
hui-zhou-apre-commit-ci[bot]pyansys-ci-bot
authored
FEAT: Spisim ucie (#6373)
Co-authored-by: ring630 <@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent b2a534d commit 3a82c7b

File tree

6 files changed

+202513
-1
lines changed

6 files changed

+202513
-1
lines changed

doc/changelog.d/6373.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Spisim ucie

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"pyyaml",
4444
"defusedxml>=0.7,<8.0",
4545
"numpy>=1.20.0,<2.3",
46+
"pydantic>=2.6.4,<2.12",
4647
]
4748

4849
[project.optional-dependencies]

src/ansys/aedt/core/visualization/post/spisim.py

Lines changed: 299 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@
2929
import re
3030
import shutil
3131
from struct import unpack
32+
from typing import List
33+
from typing import Optional
34+
from typing import Union
3235

3336
from numpy import float64
3437
from numpy import zeros
38+
from pydantic import BaseModel
39+
from pydantic import Field
3540

3641
from ansys.aedt.core.generic.file_utils import generate_unique_name
3742
from ansys.aedt.core.generic.file_utils import open_file
@@ -40,9 +45,142 @@
4045
from ansys.aedt.core.generic.settings import is_linux
4146
from ansys.aedt.core.generic.settings import settings
4247
from ansys.aedt.core.internal.aedt_versions import aedt_versions
48+
from ansys.aedt.core.internal.errors import AEDTRuntimeError
4349
from ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_parameters import COMParametersVer3p4
4450

4551

52+
class ReportBase(BaseModel):
53+
model_config = {"populate_by_name": True}
54+
55+
56+
class FrequencyFigure(ReportBase):
57+
title: str = Field(..., alias="TITLE")
58+
param: str = Field(..., alias="PARAM")
59+
td_inp_delay: str = Field(..., alias="TDInpDelay")
60+
skew_threshold: str = Field(..., alias="SkewThreshold")
61+
dtcyc: str = Field(..., alias="DTCyc")
62+
xlim: str = Field(..., alias="XLIM")
63+
ylim: str = Field(..., alias="YLIM")
64+
limitline: str = Field(..., alias="LIMITLINE")
65+
gencsv: str = Field(..., alias="GENCSV")
66+
fig_fq_axis_log: str = Field(..., alias="FigFqAxis Log")
67+
fig_fq_unit: str = Field(..., alias="FigFqUnit")
68+
phase: str = Field(..., alias="Phase")
69+
70+
71+
class AdvancedReport(ReportBase):
72+
version: str = Field("1.0", alias="Version")
73+
rpt_name: Optional[str] = Field("", alias="RptName")
74+
touchstone: str = Field(..., alias="Touchstone")
75+
expiration: str = Field(default="12/31/2100", alias="Expiration")
76+
mode: str = Field(..., alias="Mode")
77+
dpextract: Optional[str] = Field("", alias="DPExtract")
78+
port: str = Field(..., alias="Port")
79+
r: int = Field(50, alias="R")
80+
report_dir: str = Field(..., alias="ReportDir")
81+
extrapolate: str = Field(..., alias="Extrapolate")
82+
watermark: Optional[str] = Field("", alias="WaterMark")
83+
td_length: str = Field(..., alias="TDLength")
84+
fq_axis_log: str = Field("F", alias="FqAxis Log")
85+
fq_unit: str = Field("GHz", alias="FqUnit")
86+
smoothing: str = Field("0%", alias="Smoothing")
87+
88+
trace_width: int = Field(4, alias="Trace Width") # Signal traces width in .param plot
89+
title_font_size: int = Field(45, alias="Title FontSize") # Figure title font size
90+
legend_font_size: int = Field(25, alias="Legend FontSize") # Legend font size
91+
axis_font_size: int = Field(35, alias="Axis FontSize") # X-Y axis font size
92+
grid_width: int = Field(0, alias="Grid Width") # Grid line width
93+
94+
var_list: str = Field(..., alias="VARList")
95+
cascade: str = Field(default="", alias="CASCADE") # additional file to be formed via cascading
96+
97+
frequency_domain: Optional[List[FrequencyFigure]] = Field(default=[], alias="[Frequency Domain]")
98+
99+
@classmethod
100+
def from_spisim_cfg(cls, file_path: Union[str, Path]) -> "AdvancedReport": # pragma: no cover
101+
"""Load SPIsim configuration file."""
102+
with open(file_path, "r") as f:
103+
content = f.read()
104+
105+
# Remove everything after % on any line, including full-line %
106+
cleaned = re.sub(r"\s*%.*", "", content)
107+
108+
# Optionally remove empty lines (that were full-line % or left blank after stripping)
109+
cleaned = re.sub(r"^\s*\n", "", cleaned, flags=re.MULTILINE)
110+
111+
# Convert into dict
112+
config = {}
113+
current_section = None
114+
current_figure = None
115+
116+
freq_figures = []
117+
time_figures = []
118+
119+
lines = cleaned.splitlines()
120+
121+
for line in lines:
122+
line = line.strip()
123+
124+
if not line:
125+
continue
126+
127+
# Section header
128+
if line == "[Frequency Domain]":
129+
current_section = "frequency_domain"
130+
current_figure = None # reset on new section
131+
continue
132+
elif current_section == "[Time Domain]":
133+
current_section = "time_domain"
134+
current_figure = None
135+
continue
136+
137+
# Start of a new figure block
138+
if line.startswith("[FIGURE"):
139+
current_figure = {}
140+
if current_section == "frequency_domain":
141+
freq_figures.append(current_figure)
142+
elif current_section == "time_domain":
143+
time_figures.append(current_figure)
144+
continue
145+
146+
# Key-value assignment
147+
if "=" in line:
148+
key, value = map(str.strip, line.split("=", 1))
149+
if current_section == "frequency_domain" and current_figure is not None:
150+
current_figure[key] = value
151+
elif current_section == "time_domain" and current_figure is not None:
152+
current_figure[key] = value
153+
else:
154+
config[key] = value
155+
156+
# Assign section data to top-level keys
157+
if freq_figures:
158+
config["frequency_domain"] = freq_figures
159+
if time_figures:
160+
config["time_domain"] = time_figures
161+
162+
return cls(**config)
163+
164+
def dump_spisim_cfg(self, file_path: Union[str, Path]) -> str:
165+
"""Create a SPIsim configuration file."""
166+
data = self.model_dump(by_alias=True)
167+
168+
lines = []
169+
for k, v in data.items():
170+
if k in ["[Frequency Domain]", "[Time Domain]"]:
171+
lines.append(k + "\n")
172+
figures = v
173+
for idx, fig in enumerate(figures):
174+
lines.append(f"[FIGURE {idx + 1}]\n")
175+
for fig_k, fig_v in fig.items():
176+
lines.append(f"{fig_k}= {fig_v}\n")
177+
else:
178+
lines.append(f"{k}= {v}\n")
179+
with open(file_path, "w") as f:
180+
f.writelines(lines)
181+
return str(file_path)
182+
183+
46184
class SpiSim:
47185
"""Provides support to SpiSim batch mode."""
48186

@@ -85,15 +223,44 @@ def _copy_to_relative_path(self, file_name):
85223
self.logger.warning(f"Failed to copy {file_name}")
86224
return str(pathlib.Path(file_name).name)
87225

226+
@staticmethod
227+
def __parser_spisim_cfg(file_path):
228+
"""Load a SPIsim configuration file.
229+
230+
Parameters
231+
----------
232+
file_path : str
233+
Path of the configuration file.
234+
235+
Returns
236+
-------
237+
bool
238+
``True`` when successful, ``False`` when failed.
239+
"""
240+
temp = {}
241+
with open(file_path, "r") as fp:
242+
lines = fp.readlines()
243+
for line in lines:
244+
if not line.startswith("#") and "=" in line:
245+
split_line = [i.strip() for i in line.split("=")]
246+
kw, value = split_line
247+
temp[kw] = value
248+
return temp
249+
88250
@pyaedt_function_handler()
89-
def __compute_spisim(self, parameter, config_file, out_file=""):
251+
def __compute_spisim(self, parameter, config_file, out_file="", in_file=""):
90252
import subprocess # nosec
91253

92254
exec_name = "SPISimJNI_LX64.exe" if is_linux else "SPISimJNI_WIN64.exe"
93255
spisim_exe = os.path.join(self.desktop_install_dir, "spisim", "SPISim", "modules", "ext", exec_name)
94256
command = [spisim_exe, parameter]
257+
258+
if in_file != "":
259+
command += ["-i", str(in_file)]
260+
95261
config_folder = os.path.dirname(config_file)
96262
cfg_file_only = os.path.split(config_file)[-1]
263+
97264
if config_file != "":
98265
command += ["-v", f"CFGFILE={cfg_file_only}"]
99266
if out_file:
@@ -111,6 +278,7 @@ def __compute_spisim(self, parameter, config_file, out_file=""):
111278
my_env["SPISIM_OUTPUT_LOG"] = os.path.join(out_file, generate_unique_name("spsim_out") + ".log")
112279

113280
with open_file(out_processing, "w") as outfile:
281+
settings.logger.info(f"Execute : {' '.join(command)}")
114282
subprocess.run(command, env=my_env, cwd=config_folder, stdout=outfile, stderr=outfile, check=True) # nosec
115283
return out_processing
116284

@@ -415,6 +583,136 @@ def export_com_configure_file(self, file_path, standard=1):
415583
"""
416584
return COMParametersVer3p4(standard).export(file_path)
417585

586+
@pyaedt_function_handler()
587+
def compute_ucie(
588+
self,
589+
tx_ports: list[int],
590+
rx_ports: list[int],
591+
victim_ports: list[int],
592+
tx_resistance: Union[int, float, str] = 30,
593+
tx_capacitance: str = "0.2p",
594+
rx_resistance: Union[int, float, str] = 50,
595+
rx_capacitance: str = "0.2p",
596+
packaging_type="standard",
597+
data_rate="GTS04",
598+
report_directory: str = None,
599+
):
600+
"""Universal Chiplet Interface Express (UCIe) Compliance support.
601+
602+
Parameters
603+
----------
604+
tx_ports : list
605+
Transmitter port indexes.
606+
rx_ports : list
607+
Receiver port indexes.
608+
victim_ports : list
609+
Victim port indexes.
610+
tx_resistance : float, str, optional
611+
Transmitter termination resistance parameter.
612+
tx_capacitance : str, optional
613+
Transmitter termination capacitance parameter.
614+
rx_resistance : float, str, optional
615+
Receiver termination resistance parameter.
616+
rx_capacitance : str, optional
617+
Receiver termination capacitance parameter.
618+
packaging_type : str, optional
619+
Type of packaging. Available options are ``standard`` and ``advanced``.
620+
data_rate : str, optional
621+
Data rate. Available options are ``GTS04``, ``GTS08``.,``GTS12``.``GTS16``.``GTS24``. and ``GTS32``.
622+
report_directory : str, optional
623+
Directory to save report files.
624+
"""
625+
626+
class Ucie(BaseModel):
627+
TxR: Union[str, int]
628+
TxC: str
629+
RxR: Union[str, int]
630+
RxC: str
631+
TxIdx: str
632+
RxIdx: str
633+
RxCal: str
634+
PkgType: str
635+
DatRate: str
636+
637+
def to_var_list(self):
638+
string = "(Spec 'UCIE1P1_CHANNEL')"
639+
for k, v in self.model_dump().items():
640+
string = string + f"({k} {v})"
641+
return string
642+
643+
cfg_ucie = Ucie(
644+
PkgType=packaging_type.upper(),
645+
TxR=tx_resistance,
646+
TxC=tx_capacitance,
647+
RxR=rx_resistance,
648+
RxC=rx_capacitance,
649+
TxIdx="/".join([str(i) for i in tx_ports]),
650+
RxIdx="/".join([str(i) for i in rx_ports]),
651+
RxCal="/".join([str(i) for i in victim_ports]),
652+
DatRate=data_rate,
653+
)
654+
655+
if report_directory:
656+
report_directory_ = Path(report_directory)
657+
if not report_directory_.exists():
658+
report_directory_.mkdir()
659+
else:
660+
report_directory_ = Path(self.working_directory)
661+
662+
cfg = AdvancedReport(
663+
touchstone=Path(self.touchstone_file).suffix.lstrip("."),
664+
mode="SINGLE",
665+
port="INCREMENTAL",
666+
report_dir=str(report_directory_),
667+
var_list=cfg_ucie.to_var_list(),
668+
extrapolate="100G",
669+
td_length="200n",
670+
frequency_domain=[
671+
FrequencyFigure(
672+
TITLE="Voltage Transfer Function: Loss",
673+
PARAM="VTFLOSS",
674+
TDInpDelay="0.1n",
675+
SkewThreshold="0.2",
676+
DTCyc="0.5",
677+
XLIM="(1 32G)",
678+
YLIM="(0 -50)",
679+
LIMITLINE="LimitLine = VTF_Loss {Upper [1 -5], [24G -5]}",
680+
GENCSV="DB",
681+
fig_fq_axis_log="F",
682+
FigFqUnit="GHz",
683+
Phase="OFF",
684+
),
685+
FrequencyFigure(
686+
TITLE="Voltage Transfer Function: Crosstalk",
687+
PARAM="VTFXTKS",
688+
TDInpDelay="0.1n",
689+
SkewThreshold="0.2",
690+
DTCyc="0.5",
691+
XLIM="(1 32G)",
692+
YLIM="(0 -80)",
693+
LIMITLINE="LimitLine = VTF_Xtks {Lower [1 -24],[24G -24]}",
694+
GENCSV="DB",
695+
fig_fq_axis_log="F",
696+
FigFqUnit="GHz",
697+
Phase="OFF",
698+
),
699+
],
700+
)
701+
fpath_cfg = cfg.dump_spisim_cfg(report_directory_ / "ucie.cfg")
702+
log_file = self.__compute_spisim(parameter="REPORT", config_file=fpath_cfg, in_file=self.touchstone_file)
703+
with open(log_file, "r") as f:
704+
log = f.read()
705+
for i in log.split("\n"):
706+
settings.logger.info(i)
707+
match = re.search(r"Execution status: .* status \b(FAILED|OK)\b", log)
708+
try:
709+
if match.groups()[0] == "OK":
710+
return True
711+
else: # pragma: no cover
712+
return False
713+
except Exception: # pragma: no cover
714+
raise AEDTRuntimeError("SPIsim Failed")
715+
418716

419717
def detect_encoding(file_path, expected_pattern="", re_flags=0):
420718
"""Check encoding of a file."""

0 commit comments

Comments
 (0)