2929import re
3030import shutil
3131from struct import unpack
32+ from typing import List
33+ from typing import Optional
34+ from typing import Union
3235
3336from numpy import float64
3437from numpy import zeros
38+ from pydantic import BaseModel
39+ from pydantic import Field
3540
3641from ansys .aedt .core .generic .file_utils import generate_unique_name
3742from ansys .aedt .core .generic .file_utils import open_file
4045from ansys .aedt .core .generic .settings import is_linux
4146from ansys .aedt .core .generic .settings import settings
4247from ansys .aedt .core .internal .aedt_versions import aedt_versions
48+ from ansys .aedt .core .internal .errors import AEDTRuntimeError
4349from 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+
46184class 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
419717def detect_encoding (file_path , expected_pattern = "" , re_flags = 0 ):
420718 """Check encoding of a file."""
0 commit comments