diff --git a/src/ansys/aedt/core/hfss.py b/src/ansys/aedt/core/hfss.py index 9ccfc9c65d7..6d06ec27338 100644 --- a/src/ansys/aedt/core/hfss.py +++ b/src/ansys/aedt/core/hfss.py @@ -796,8 +796,8 @@ def create_setup(self, name="MySetupAuto", setup_type=None, **kwargs): """Create an analysis setup for HFSS. Optional arguments are passed along with ``setup_type`` and ``name``. Keyword - names correspond to the ``setup_type`` corresponding to the native AEDT API. - The list of keywords here is not exhaustive. + names correspond to keyword for the ``setup_type`` as defined in + the native AEDT API. .. note:: This method overrides the ``Analysis.setup()`` method for the HFSS app. @@ -842,9 +842,9 @@ def create_setup(self, name="MySetupAuto", setup_type=None, **kwargs): setup.auto_update = False for arg_name, arg_value in kwargs.items(): if setup[arg_name] is not None: - if arg_name == "MultipleAdaptiveFreqsSetup": - setup[arg_name].delete_all() - if isinstance(arg_value, list): + if arg_name == "MultipleAdaptiveFreqsSetup": # A list of frequency values is passed if + setup[arg_name].delete_all() # the default convergence criteria are to be + if isinstance(arg_value, list): # used. for i in arg_value: setup[arg_name][i] = [0.02] else: diff --git a/src/ansys/aedt/core/modules/setup_templates.py b/src/ansys/aedt/core/modules/setup_templates.py index 2bc42ffc1e9..e6dcc0d139c 100644 --- a/src/ansys/aedt/core/modules/setup_templates.py +++ b/src/ansys/aedt/core/modules/setup_templates.py @@ -1587,6 +1587,19 @@ def HFSS3DLayout_AdaptiveFrequencyData(freq): TR = {} +# Default sweep settings for Q3D +SweepQ3D = dict( + { + "IsEnabled": True, + "RangeType": "LinearStep", + "RangeStart": "1GHz", + "RangeEnd": "10GHz", + "Type": "Discrete", + "SaveFields": False, + "SaveRadFields": False, + } +) + SweepHfss3D = dict( { "Type": "Interpolating", diff --git a/src/ansys/aedt/core/modules/solve_setup.py b/src/ansys/aedt/core/modules/solve_setup.py index 7325f8d82c8..1ca23d2663c 100644 --- a/src/ansys/aedt/core/modules/solve_setup.py +++ b/src/ansys/aedt/core/modules/solve_setup.py @@ -427,6 +427,8 @@ def get_solution_data( ] else: setup_sweep_name = [i for i in self._app.existing_analysis_sweeps if self.name == i.split(" : ")[0]] + if report_category == "Fields": + sweep = self._app.nominal_adaptive # Use last adaptive if no sweep is named explicitly. if setup_sweep_name: return self._app.post.get_solution_data( expressions=expressions, @@ -437,7 +439,7 @@ def get_solution_data( context=context, polyline_points=polyline_points, math_formula=math_formula, - setup_sweep_name=sweep, + setup_sweep_name=sweep, # Should this be setup_sweep_name? ) return None @@ -2868,7 +2870,7 @@ def create_single_point_sweep( return sweepdata @pyaedt_function_handler(sweepname="name", sweeptype="sweep_type") - def add_sweep(self, name=None, sweep_type="Interpolating"): + def add_sweep(self, name=None, sweep_type="Interpolating", **props): """Add a sweep to the project. Parameters @@ -2878,6 +2880,8 @@ def add_sweep(self, name=None, sweep_type="Interpolating"): case a name is automatically assigned. sweep_type : str, optional Type of the sweep. The default is ``"Interpolating"``. + **props : Optional context-dependent keyword arguments can be passed + to the sweep setup Returns ------- @@ -2895,14 +2899,18 @@ def add_sweep(self, name=None, sweep_type="Interpolating"): self._app.logger.warning("This method only applies to HFSS and Q3D. Use add_eddy_current_sweep method.") return False if self.setuptype <= 4: - sweep_n = SweepHFSS(self, name=name, sweep_type=sweep_type) + sweep_n = SweepHFSS(self, name=name, sweep_type=sweep_type, props=props) elif self.setuptype in [14, 30, 31]: - sweep_n = SweepMatrix(self, name=name, sweep_type=sweep_type) + sweep_n = SweepMatrix(self, name=name, sweep_type=sweep_type) # TODO: add , props=props) else: self._app.logger.warning("This method only applies to HFSS, Q2D, and Q3D.") return False sweep_n.create() self.sweeps.append(sweep_n) + for setup in self.p_app.setups: + if self.name == setup.name: + setup.sweeps.append(sweep_n) + break return sweep_n @pyaedt_function_handler(sweepname="name") @@ -4013,7 +4021,7 @@ def create_single_point_sweep( return sweepdata @pyaedt_function_handler(sweepname="name", sweeptype="sweep_type") - def add_sweep(self, name=None, sweep_type="Interpolating"): + def add_sweep(self, name=None, sweep_type="Interpolating", **props): """Add a sweep to the project. Parameters @@ -4040,9 +4048,9 @@ def add_sweep(self, name=None, sweep_type="Interpolating"): self._app.logger.warning("This method only applies to HFSS and Q3D. Use add_eddy_current_sweep method.") return False if self.setuptype <= 4: - sweep_n = SweepHFSS(self, name=name, sweep_type=sweep_type) + sweep_n = SweepHFSS(self, name=name, sweep_type=sweep_type, props=props) elif self.setuptype in [14, 30, 31]: - sweep_n = SweepMatrix(self, name=name, sweep_type=sweep_type) + sweep_n = SweepMatrix(self, name=name, sweep_type=sweep_type, props=props) else: self._app.logger.warning("This method only applies to HFSS, Q2D, and Q3D.") return False diff --git a/src/ansys/aedt/core/modules/solve_sweeps.py b/src/ansys/aedt/core/modules/solve_sweeps.py index 3b85a3b29e9..490d88c56c2 100644 --- a/src/ansys/aedt/core/modules/solve_sweeps.py +++ b/src/ansys/aedt/core/modules/solve_sweeps.py @@ -36,6 +36,7 @@ from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.modules.setup_templates import Sweep3DLayout from ansys.aedt.core.modules.setup_templates import SweepHfss3D +from ansys.aedt.core.modules.setup_templates import SweepQ3D from ansys.aedt.core.modules.setup_templates import SweepSiwave open3 = open @@ -108,25 +109,38 @@ class SweepHFSS(object): def __init__(self, setup, name, sweep_type="Interpolating", props=None): self._app = setup self.oanalysis = setup.omodule - self.props = {} self.setup_name = setup.name self.name = name + self.props = copy.deepcopy(SweepHfss3D) if props: - self.props = props + if "RangeStep" in props.keys(): # LinearCount is the default sweep type. Change it if RangeStep is passed. + if "RangeCount" in props.keys(): + self._app.logger.message( + "Inconsistent arguments 'RangeCount' and 'RangeStep' passed to 'SweepHFSS'," + ) + self._app.logger.message("Default remains 'LinearCount' sweep type.") + self._app.logger.message("") # Add a space to the log file. + else: + self.props["RangeType"] = "LinearStep" + for key, value in props.items(): + try: + self.props[key] = value + except ValueError: + error_message = f"Parameter '{key}' is invalid and will be ignored." + self._app.logger.warning(error_message) + + # for t in SweepHfss3D: + # _tuple2dict(t, self.props) + if SequenceMatcher(None, sweep_type.lower(), "interpolating").ratio() > 0.8: + sweep_type = "Interpolating" + elif SequenceMatcher(None, sweep_type.lower(), "discrete").ratio() > 0.8: + sweep_type = "Discrete" + elif SequenceMatcher(None, sweep_type.lower(), "fast").ratio() > 0.8: + sweep_type = "Fast" else: - self.props = copy.deepcopy(SweepHfss3D) - # for t in SweepHfss3D: - # _tuple2dict(t, self.props) - if SequenceMatcher(None, sweep_type.lower(), "interpolating").ratio() > 0.8: - sweep_type = "Interpolating" - elif SequenceMatcher(None, sweep_type.lower(), "discrete").ratio() > 0.8: - sweep_type = "Discrete" - elif SequenceMatcher(None, sweep_type.lower(), "fast").ratio() > 0.8: - sweep_type = "Fast" - else: - warnings.warn("Invalid sweep type. `Interpolating` will be set as the default.") - sweep_type = "Interpolating" - self.props["Type"] = sweep_type + warnings.warn("Invalid sweep type. `Interpolating` will be set as the default.") + sweep_type = "Interpolating" + self.props["Type"] = sweep_type @property def is_solved(self): @@ -612,17 +626,19 @@ class SweepMatrix(object): """ def __init__(self, setup, name, sweep_type="Interpolating", props=None): - self._app = setup + self._app = setup # TODO: Remove sweep_type as an argument as it can be passed in props self.oanalysis = setup.omodule self.setup_name = setup.name self.name = name - self.props = {} + self.props = copy.deepcopy(SweepQ3D) if props: - self.props = props + for key, value in props.items(): + if key in self.props: + self.props[key] = value else: self.props["Type"] = sweep_type if sweep_type == "Discrete": - self.props["isenabled"] = True + self.props["IsEnabled"] = True self.props["RangeType"] = "LinearCount" self.props["RangeStart"] = "2.5GHz" self.props["RangeStep"] = "1GHz" diff --git a/src/ansys/aedt/core/visualization/post/common.py b/src/ansys/aedt/core/visualization/post/common.py index b1057a9e28a..9158e38282d 100644 --- a/src/ansys/aedt/core/visualization/post/common.py +++ b/src/ansys/aedt/core/visualization/post/common.py @@ -494,7 +494,28 @@ def available_report_solutions(self, report_category=None): report_category = self.available_report_types[0] if report_category: return list(self.oreportsetup.GetAvailableSolutions(report_category)) - return None # pragma: no cover + return None + + @pyaedt_function_handler() # pragma: no cover + def _get_setup_from_sweep_name(self, sweep_name): + if ":" not in sweep_name: + sweep_names = [] # Look for sweep name in setups if the setup is not + for s in self._app.setups: # passed explicitly in setup_sweep_name. + for sweep in s.sweeps: + this_name = s.name + " : " + sweep.name if sweep.name == sweep_name else None + if this_name: + sweep_names.append(this_name) + if len(sweep_names) > 1: + warning_str = "More than one sweep with name '{setup_sweep_name}' found. " + warning_str += f"Returning '{sweep_names[0]}'." + self.logger.warning(warning_str) + return sweep_names[0] + elif len(sweep_names) == 1: + return sweep_names[0] + else: + return sweep_name # Nothing found, pass the sweep name through. + else: + return sweep_name @pyaedt_function_handler() def _get_plot_inputs(self): @@ -1211,7 +1232,12 @@ def create_report( >>> m3d.release_desktop(False, False) """ if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep + if report_category == "Fields": + setup_sweep_name = self._app.nominal_adaptive # Field report and no sweep name passed. + else: + setup_sweep_name = self._app.nominal_sweep + elif domain == "Sweep": + setup_sweep_name = self._get_setup_from_sweep_name(setup_sweep_name) if not domain: domain = "Sweep" setup_name = setup_sweep_name.split(":")[0] @@ -1654,6 +1680,8 @@ def create_report_from_configuration( solution_name = self._app.nominal_sweep else: solution_name = self._app.nominal_adaptive + else: + solution_name = self._get_setup_from_sweep_name(solution_name) # If only the sweep name is passed. if props.get("report_category", None) and props["report_category"] in TEMPLATES_BY_NAME: if props.get("context", {"context": {}}).get("domain", "") == "Spectral": report_temp = TEMPLATES_BY_NAME["Spectrum"] @@ -1979,7 +2007,8 @@ def fields(self, expressions=None, setup=None, polyline=None): >>> solutions = report.get_solution_data() """ if not setup: - setup = self._post_app._app.nominal_sweep + # setup = self._post_app._app.nominal_sweep + setup = self._post_app._app.nominal_adaptive rep = None if "Fields" in self._templates: rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "Fields", setup) @@ -2111,7 +2140,7 @@ def rl_fields(self, expressions=None, setup=None, polyline=None): return rep @pyaedt_function_handler(setup_name="setup") - def far_field(self, expressions=None, setup=None, sphere_name=None, source_context=None): + def far_field(self, expressions=None, setup=None, sphere_name=None, source_context=None, **variations): """Create a Far Field Report object. Parameters @@ -2146,10 +2175,17 @@ def far_field(self, expressions=None, setup=None, sphere_name=None, source_conte setup = self._post_app._app.nominal_sweep rep = None if "Far Fields" in self._templates: - rep = ansys.aedt.core.visualization.report.field.FarField(self._post_app, "Far Fields", setup) + rep = ansys.aedt.core.visualization.report.field.FarField(self._post_app, "Far Fields", setup, **variations) rep.far_field_sphere = sphere_name rep.source_context = source_context - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + rep.report_type = "Radiation Pattern" + if expressions: + if type(expressions) == list: + rep.expressions = expressions + else: + rep.expressions = [expressions] + else: + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) return rep @pyaedt_function_handler(setup_name="setup", sphere_name="infinite_sphere") diff --git a/src/ansys/aedt/core/visualization/report/field.py b/src/ansys/aedt/core/visualization/report/field.py index 3440ac55ea0..347bcf2595e 100644 --- a/src/ansys/aedt/core/visualization/report/field.py +++ b/src/ansys/aedt/core/visualization/report/field.py @@ -131,19 +131,19 @@ def near_field(self, value): class FarField(CommonReport): """Provides for managing far field reports.""" - def __init__(self, app, report_category, setup_name, expressions=None): + def __init__(self, app, report_category, setup_name, expressions=None, **variations): CommonReport.__init__(self, app, report_category, setup_name, expressions) + variation_defaults = {"Phi": ["All"], "Theta": ["All"], "Freq": ["Nominal"]} self.domain = "Sweep" self.primary_sweep = "Phi" self.secondary_sweep = "Theta" self.source_context = None self.source_group = None - if "Phi" not in self.variations: - self.variations["Phi"] = ["All"] - if "Theta" not in self.variations: - self.variations["Theta"] = ["All"] - if "Freq" not in self.variations: - self.variations["Freq"] = ["Nominal"] + for key, default_value in variation_defaults.items(): + if key in variations: + self.variations[key] = variations[key] + else: + self.variations[key] = default_value @property def far_field_sphere(self): diff --git a/tests/system/general/example_models/T12/Potter_Horn_242.aedtz b/tests/system/general/example_models/T12/Potter_Horn_242.aedtz new file mode 100644 index 00000000000..cb8d2ae1244 Binary files /dev/null and b/tests/system/general/example_models/T12/Potter_Horn_242.aedtz differ diff --git a/tests/system/general/test_11_Setup.py b/tests/system/general/test_11_Setup.py index ae48203a6ea..300d69f5682 100644 --- a/tests/system/general/test_11_Setup.py +++ b/tests/system/general/test_11_Setup.py @@ -102,6 +102,11 @@ def test_01_create_hfss_setup(self): max_delta_phase=8, custom_entries=[["1", "2", 0.03, 4]], ) + setup2 = self.aedtapp.create_setup( + "MulitFreqSetup", MultipleAdaptiveFreqsSetup=["1GHz", "2GHz"], MaximumPasses=3 + ) + assert setup2.props["SolveType"] == "MultiFrequency" + assert setup2.props["MaximumPasses"] == 3 def test_01b_create_hfss_sweep(self): self.aedtapp.save_project() @@ -121,6 +126,21 @@ def test_01b_create_hfss_sweep(self): assert sweep3.props["Type"] == "Discrete" sweep4 = setup1.create_frequency_sweep("GHz", 23, 25, 401, sweep_type="Fast") assert sweep4.props["Type"] == "Fast" + range_start = "1GHz" + range_end = "2GHz" + range_step = "0.5GHz" + sweep5 = setup1.add_sweep( + "DiscSweep5", + sweep_type="Discrete", + RangeStart=range_start, + RangeEnd=range_end, + RangeStep=range_step, + SaveFields=True, + ) + assert sweep5.props["Type"] == "Discrete" + assert sweep5.props["RangeStart"] == range_start + assert sweep5.props["RangeEnd"] == range_end + assert sweep5.props["RangeStep"] == range_step def test_01c_create_hfss_setup_auto_open(self): self.aedtapp.duplicate_design("auto_open") diff --git a/tests/system/general/test_12_PostProcessing.py b/tests/system/general/test_12_PostProcessing.py index b5f828bcab0..008c34acb2e 100644 --- a/tests/system/general/test_12_PostProcessing.py +++ b/tests/system/general/test_12_PostProcessing.py @@ -43,7 +43,7 @@ from tests.system.general.conftest import config if config["desktopVersion"] > "2022.2": - test_field_name = "Potter_Horn_231" + test_field_name = "Potter_Horn_242" test_project_name = "coax_setup_solved_231" sbr_file = "poc_scat_small_231" q3d_file = "via_gsg_231" @@ -151,7 +151,7 @@ def test_09_manipulate_report(self, field_test): variations["Freq"] = ["30GHz"] field_test.set_source_context(["1"]) context = {"Context": "3D", "SourceContext": "1:1"} - assert field_test.post.create_report( + nominal_report = field_test.post.create_report( "db(GainTotal)", field_test.nominal_adaptive, variations=variations, @@ -161,9 +161,12 @@ def test_09_manipulate_report(self, field_test): plot_type="3D Polar Plot", context=context, ) - plot = field_test.post.create_report( + assert nominal_report + sweep = field_test.setups[0].sweeps[0] + variations["Freq"] = "30.1GHz" + sweep_report = field_test.post.create_report( "db(GainTotal)", - field_test.nominal_adaptive, + sweep.name, variations=variations, primary_sweep_variable="Phi", secondary_sweep_variable="Theta", @@ -171,10 +174,10 @@ def test_09_manipulate_report(self, field_test): plot_type="3D Polar Plot", context="3D", ) - assert plot - assert plot.export_config(os.path.join(self.local_scratch.path, f"{plot.plot_name}.json")) + assert sweep_report + assert sweep_report.export_config(os.path.join(self.local_scratch.path, f"{sweep_report.plot_name}.json")) assert field_test.post.create_report_from_configuration( - os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), solution_name=field_test.nominal_adaptive + os.path.join(self.local_scratch.path, f"{sweep_report.plot_name}.json"), solution_name=sweep.name ) report = AnsysReport() report.create() diff --git a/tests/system/general/test_20_HFSS.py b/tests/system/general/test_20_HFSS.py index dd1f314337e..a1d40e0637e 100644 --- a/tests/system/general/test_20_HFSS.py +++ b/tests/system/general/test_20_HFSS.py @@ -232,9 +232,11 @@ def test_05_create_wave_port_from_sheets(self): assert port3.name in [i.name for i in self.aedtapp.boundaries] def test_06a_create_linear_count_sweep(self): - setup = self.aedtapp.create_setup("MySetup") - setup.props["Frequency"] = "1GHz" - setup.props["BasisOrder"] = 2 + # Newer, simplified notation to pass native API keywords + setup = self.aedtapp.create_setup("MySetup", Frequency="1GHz", BasisOrder=2) + assert setup.props["Frequency"] == "1GHz" + assert setup.props["BasisOrder"] == 2 + # Legacy notation using setup.props followed by setup.update() setup.props["MaximumPasses"] = 1 assert setup.update() assert self.aedtapp.create_linear_count_sweep("MySetup", "GHz", 0.8, 1.2, 401)