From bb3830c4119908e50198191187363dddfb6dd20e Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:16:47 +0200 Subject: [PATCH 1/2] Support case files (#3799) * Added support to Case Files, minore fix improvement in the plot field and in animate field with pyvista. Added support to Q3D named expression field plot. * Improved Q3D DC IR Example * Improved Q3D DC IR Example * Add UT and pragma no cover --------- Co-authored-by: maxcapodi78 Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Co-authored-by: Samuel Lopez --- _unittest/test_12_1_PostProcessing.py | 4 + examples/05-Q3D/Q3D_DC_IR.py | 54 ++++++- .../06-Multiphysics/Hfss_Icepak_Coupling.py | 4 +- pyaedt/generic/plot.py | 48 ++++--- pyaedt/modules/AdvancedPostProcessing.py | 30 +++- pyaedt/modules/PostProcessor.py | 77 ++++++++-- pyaedt/modules/solutions.py | 133 ++++++++++-------- 7 files changed, 247 insertions(+), 103 deletions(-) diff --git a/_unittest/test_12_1_PostProcessing.py b/_unittest/test_12_1_PostProcessing.py index 120b5e46be3..2349af9db82 100644 --- a/_unittest/test_12_1_PostProcessing.py +++ b/_unittest/test_12_1_PostProcessing.py @@ -556,6 +556,10 @@ def test_15_export_plot(self): show=False, export_path=os.path.join(self.local_scratch.path, "image.jpg") ) assert os.path.exists(obj.image_file) + obj2 = self.aedtapp.post.plot_model_obj( + show=False, export_path=os.path.join(self.local_scratch.path, "image2.jpg"), plot_as_separate_objects=False + ) + assert os.path.exists(obj2.image_file) @pytest.mark.skipif(is_linux or sys.version_info < (3, 8), reason="Not running in ironpython") def test_16_create_field_plot(self): diff --git a/examples/05-Q3D/Q3D_DC_IR.py b/examples/05-Q3D/Q3D_DC_IR.py index cabbe6c6ea3..91a05e5440b 100644 --- a/examples/05-Q3D/Q3D_DC_IR.py +++ b/examples/05-Q3D/Q3D_DC_IR.py @@ -158,11 +158,11 @@ # Use previously calculated positions to identify faces, # select the net "1_Top" and # assign sources and sinks on nets. - sink_f = q3d.modeler.create_circle(q3d.PLANE.XY, location_u11_scl, 0.1) source_f1 = q3d.modeler.create_circle(q3d.PLANE.XY, location_u9_1_scl, 0.1) source_f2 = q3d.modeler.create_circle(q3d.PLANE.XY, location_u9_2_scl, 0.1) source_f3= q3d.modeler.create_circle(q3d.PLANE.XY, location_u11_r106, 0.1) +sources_objs = [source_f1, source_f2, source_f3] q3d.auto_identify_nets() identified_net = q3d.nets[0] @@ -173,9 +173,11 @@ source2 = q3d.source(source_f2, net_name=identified_net) source3 = q3d.source(source_f3, net_name=identified_net) +sources_bounds = [source1, source2, source3] -q3d.edit_sources(dcrl={"{}:{}".format(source1.props["Net"], source1.name): "1.0A", - "{}:{}".format(source2.props["Net"], source2.name): "1.0A"}) +q3d.edit_sources(dcrl={"{}:{}".format(source1.props["Net"], source1.name): "-1.0A", + "{}:{}".format(source2.props["Net"], source2.name): "-1.0A", + "{}:{}".format(source2.props["Net"], source3.name): "-1.0A"}) ############################################################################### # Create setup @@ -191,13 +193,27 @@ setup.props["DC"]["Cond"]["MaxPass"]=3 setup.analyze() +############################################################################### +# Field Calculator +# ~~~~~~~~~~~~~~~~ +# We will create a named expression using field calculator. + +drop_name = "Vdrop3_3" +fields = q3d.ofieldsreporter +q3d.ofieldsreporter.CalcStack("clear") +q3d.ofieldsreporter.EnterQty("Phidc") +q3d.ofieldsreporter.EnterScalar(3.3) +q3d.ofieldsreporter.CalcOp("+") +q3d.ofieldsreporter.AddNamedExpression(drop_name, "DC R/L Fields") + ############################################################################### # Phi plot # ~~~~~~~~ # Compute ACL solutions and plot them. -plot1 = q3d.post.create_fieldplot_surface(q3d.modeler.get_objects_by_material("copper"), "Phidc", +plot1 = q3d.post.create_fieldplot_surface(q3d.modeler.get_objects_by_material("copper"), quantityName=drop_name, intrinsincDict={"Freq": "1GHz"}) + q3d.post.plot_field_from_fieldplot( plot1.name, project_path=q3d.working_directory, @@ -209,6 +225,36 @@ log_scale=False, ) + +############################################################################### +# Computing Voltage on Source Circles +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Using Field Calculator we can compute the voltage on source circles and get the value +# using get_solution_data method. + +curves = [] +for source_circle, source_bound in zip(sources_objs, sources_bounds): + source_sheet_name = source_circle.name + + curves.append("V{}".format(source_bound.name)) + + q3d.ofieldsreporter.CalcStack("clear") + q3d.ofieldsreporter.CopyNamedExprToStack(drop_name) + q3d.ofieldsreporter.EnterSurf(source_sheet_name) + q3d.ofieldsreporter.CalcOp("Maximum") + q3d.ofieldsreporter.AddNamedExpression("V{}".format(source_bound.name), "DC R/L Fields") + + +data = q3d.post.get_solution_data( + curves, + q3d.nominal_adaptive, + variations={"Freq": "1GHz"}, + report_category="DC R/L Fields", + ) +for curve in curves: + print(data.data_real(curve)) + + ############################################################################### # Close AEDT # ~~~~~~~~~~ diff --git a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py index 393d46ca912..658b0e6186f 100644 --- a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py +++ b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py @@ -287,8 +287,8 @@ log_scale=True, ) animated.gif_file = os.path.join(aedtapp.working_directory, "animate.gif") -animated.camera_position = [0, 0, 300] -animated.focal_point = [0, 0, 0] +# animated.camera_position = [0, 0, 300] +# animated.focal_point = [0, 0, 0] # Set off_screen to False to visualize the animation. # animated.off_screen = False animated.animate() diff --git a/pyaedt/generic/plot.py b/pyaedt/generic/plot.py index 0d68f27f88e..872ac5ade79 100644 --- a/pyaedt/generic/plot.py +++ b/pyaedt/generic/plot.py @@ -1320,7 +1320,12 @@ def _read_mesh_files(self, read_frames=False): obj_to_iterate.append(i) for field in obj_to_iterate: if field.path and not field._cached_polydata: - if ".aedtplt" in field.path: + if ".case" in field.path: + reader = pv.get_reader(os.path.abspath(field.path)).read() + field._cached_polydata = reader[reader.keys()[0]].extract_surface() + field.label = field._cached_polydata.point_data.active_scalars_name + + elif ".aedtplt" in field.path: vertices, faces, scalars, log1 = _parse_aedtplt(field.path) if self.convert_fields_in_db: scalars = [np.multiply(np.log10(i), self.log_multiplier) for i in scalars] @@ -1574,6 +1579,8 @@ def plot(self, export_image_path=None): cmap=field.color_map, opacity=field.opacity, show_edges=field.show_edge, + smooth_shading=True, + split_sharp_edges=True, ) self.pv.set_scale(self.x_scale, self.y_scale, self.z_scale) @@ -1587,9 +1594,9 @@ def plot(self, export_image_path=None): self.pv.show_grid(color=tuple(axes_color), grid=self.show_grid, fmt="%.2e") if self.bounding_box: self.pv.add_bounding_box(color=tuple(axes_color)) - self.pv.set_focus(self.pv.mesh.center) if not self.isometric_view: + self.pv.set_focus(self.pv.mesh.center) if isinstance(self.camera_position, (tuple, list)): self.pv.camera.position = self.camera_position self.pv.camera.focal_point = self.focal_point @@ -1695,19 +1702,7 @@ def animate(self): for m in self.fields: labels.append([m.name, "red"]) self.pv.add_legend(labels=labels, bcolor=None, face="circle", size=[0.15, 0.15]) - if not self.isometric_view: - if isinstance(self.camera_position, (tuple, list)): - self.pv.camera.position = self.camera_position - self.pv.camera.focal_point = self.focal_point - self.pv.camera.up = self.view_up - else: - self.pv.camera_position = self.camera_position - self.pv.camera.azimuth += self.azimuth_angle - self.pv.camera.roll += self.roll_angle - self.pv.camera.elevation += self.elevation_angle - else: - self.pv.isometric_view() - self.pv.zoom = self.zoom + self._animating = True if self.gif_file: @@ -1754,11 +1749,6 @@ def p_callback(): cmap=field.color_map, opacity=field.opacity, ) - # run until q is pressed - if self.pv.mesh: - self.pv.set_focus(self.pv.mesh.center) - - cpos = self.pv.show(interactive=False, auto_close=False, interactive_update=not self.off_screen) if self.range_min is not None and self.range_max is not None: mins = self.range_min @@ -1785,6 +1775,24 @@ def p_callback(): name="FieldPlot", opacity=self.frames[0].opacity, ) + # run until q is pressed + if self.pv.mesh: + self.pv.set_focus(self.pv.mesh.center) + if not self.isometric_view: + if isinstance(self.camera_position, (tuple, list)): + self.pv.camera.position = self.camera_position + self.pv.camera.focal_point = self.focal_point + self.pv.camera.up = self.view_up + else: + self.pv.camera_position = self.camera_position + self.pv.camera.azimuth += self.azimuth_angle + self.pv.camera.roll += self.roll_angle + self.pv.camera.elevation += self.elevation_angle + else: + self.pv.isometric_view() + self.pv.camera.zoom(self.zoom) + cpos = self.pv.show(interactive=False, auto_close=False, interactive_update=not self.off_screen) + start = time.time() try: self.pv.update(1, force_redraw=True) diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index b5373a42e15..de3dd31a747 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -322,6 +322,7 @@ def plot_field_from_fieldplot( show_grid=False, show_bounding=False, show_legend=True, + plot_as_separate_objects=True, ): """Export a field plot to an image file (JPG or PNG) using Python PyVista. @@ -367,6 +368,8 @@ def plot_field_from_fieldplot( Whether to display the axes bounding box or not. The default is ``False``. show_legend : bool, optional Whether to display the legend or not. The default is ``True``. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. Returns ------- @@ -381,8 +384,12 @@ def plot_field_from_fieldplot( else: self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - file_to_add = self.export_field_plot(plotname, self._app.working_directory) - model = self.get_model_plotter_geometries(generate_mesh=False, get_objects_from_aedt=plot_cad_objs) + file_to_add = self.export_field_plot(plotname, self._app.working_directory, file_format="case") + model = self.get_model_plotter_geometries( + generate_mesh=False, + get_objects_from_aedt=plot_cad_objs, + plot_as_separate_objects=plot_as_separate_objects, + ) model.show_legend = show_legend model.off_screen = not show if dark_mode: @@ -391,7 +398,10 @@ def plot_field_from_fieldplot( model.show_grid = show_grid if file_to_add: model.add_field_from_file( - file_to_add, coordinate_units=self.modeler.model_units, show_edges=meshplot, log_scale=log_scale + file_to_add, + coordinate_units=self.modeler.model_units, + show_edges=meshplot, + log_scale=log_scale, ) if plot_label: model.fields[0].label = plot_label @@ -427,7 +437,7 @@ def plot_field( scale_min=None, scale_max=None, plot_cad_objs=True, - log_scale=True, + log_scale=False, export_path="", imageformat="jpg", keep_plot_after_generation=False, @@ -436,6 +446,7 @@ def plot_field( show_grid=False, show_legend=True, filter_objects=[], + plot_as_separate_objects=True, ): """Create a field plot using Python PyVista and export to an image file (JPG or PNG). @@ -471,7 +482,7 @@ def plot_field( plot_cad_objs : bool, optional Whether to include objects in the plot. The default is ``True``. log_scale : bool, optional - Whether to plot fields in log scale. The default is ``True``. + Whether to plot fields in log scale. The default is ``False``. export_path : str, optional Image export path. Default is ``None`` to not export the image. imageformat : str, optional @@ -490,6 +501,8 @@ def plot_field( Whether to display the legend or not. The default is ``True``. filter_objects : list, optional Objects list for filtering the ``CutPlane`` plots. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. Returns ------- @@ -534,6 +547,7 @@ def plot_field( show_grid=show_grid, show_bounding=show_bounding, show_legend=show_legend, + plot_as_separate_objects=plot_as_separate_objects, ) if not keep_plot_after_generation: plotf.delete() @@ -652,7 +666,7 @@ def plot_animated_field( object_list, quantity, setup_name, intrinsics, filter_objects=filter_objects ) if plotf: - file_to_add = self.export_field_plot(plotf.name, export_path, plotf.name + str(v)) + file_to_add = self.export_field_plot(plotf.name, export_path, plotf.name + str(v), file_format="case") if file_to_add: fields_to_add.append(file_to_add) plotf.delete() @@ -761,7 +775,9 @@ def animate_fields_from_aedtplt( ] ) fields_to_add.append( - self.export_field_plot(plotname, project_path, plotname + variation_variable + str(el)) + self.export_field_plot( + plotname, project_path, plotname + variation_variable + str(el), file_format="case" + ) ) model = self.get_model_plotter_geometries(generate_mesh=False) diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index 286207b85f1..7a79917c1ce 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -2636,7 +2636,15 @@ def export_field_plot(self, plotname, filepath, filename="", file_format="aedtpl if not filename: filename = plotname file_path = os.path.join(filepath, filename + "." + file_format) - self.ofieldsreporter.ExportFieldPlot(plotname, False, file_path) + if ".case" in file_path: + try: + self.ofieldsreporter.ExportFieldPlot(plotname, False, file_path) + except: # pragma: no cover + self.logger.warning("case file is not supported for this plot. Switching to aedtplt") + file_path = os.path.join(filepath, filename + ".aedtplt") + self.ofieldsreporter.ExportFieldPlot(plotname, False, file_path) + else: # pragma: no cover + self.ofieldsreporter.ExportFieldPlot(plotname, False, file_path) if settings.remote_rpc_session_temp_folder: local_path = os.path.join(settings.remote_rpc_session_temp_folder, filename + "." + file_format) file_path = check_and_download_file(local_path, file_path) @@ -2703,7 +2711,7 @@ def change_field_plot_scale(self, plot_name, minimum_value, maximum_value, is_lo @pyaedt_function_handler() def _create_fieldplot( - self, objlist, quantityName, setup_name, intrinsics, listtype, plot_name=None, filter_boxes=[] + self, objlist, quantityName, setup_name, intrinsics, listtype, plot_name=None, filter_boxes=[], field_type=None ): if not listtype.startswith("Layer") and self._app.design_type != "HFSS 3D Layout Design": objlist = self._app.modeler.convert_to_selections(objlist, True) @@ -2765,6 +2773,8 @@ def _create_fieldplot( intrinsincList=intrinsics, layers_plot_type=listtype, ) + if self._app.design_type == "Q3D Extractor": # pragma: no cover + plot.field_type = field_type plot.name = plot_name plot.plotFolder = plot_name plot.filter_boxes = filter_boxes @@ -2787,6 +2797,7 @@ def _create_fieldplot_line_traces( setup_name, intrinsics, plot_name=None, + field_type="", ): if not setup_name: setup_name = self._app.existing_analysis_sweeps[0] @@ -2814,6 +2825,8 @@ def _create_fieldplot_line_traces( intrinsincList=intrinsics, seedingFaces=seeding_faces_ids, ) + if field_type: + plot.field_type = field_type plot.name = plot_name plot.plotFolder = plot_name @@ -2827,7 +2840,9 @@ def _create_fieldplot_line_traces( return False @pyaedt_function_handler() - def create_fieldplot_line(self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None): + def create_fieldplot_line( + self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None, field_type="DC R/L Fields" + ): """Create a field plot of the line. Parameters @@ -2845,6 +2860,8 @@ def create_fieldplot_line(self, objlist, quantityName, setup_name=None, intrinsi is ``{}``. plot_name : str, optional Name of the fieldplot to create. + field_type : str, optional + Field type to plot. Valid only for Q3D Field plots. Returns ------- @@ -2861,7 +2878,9 @@ def create_fieldplot_line(self, objlist, quantityName, setup_name=None, intrinsi if plot_name and plot_name in list(self.field_plots.keys()): self.logger.info("Plot {} exists. returning the object.".format(plot_name)) return self.field_plots[plot_name] - return self._create_fieldplot(objlist, quantityName, setup_name, intrinsincDict, "Line", plot_name) + return self._create_fieldplot( + objlist, quantityName, setup_name, intrinsincDict, "Line", plot_name, field_type=field_type + ) @pyaedt_function_handler() def create_fieldplot_line_traces( @@ -2872,6 +2891,7 @@ def create_fieldplot_line_traces( setup_name=None, intrinsinc_dict=None, plot_name=None, + field_type="DC R/L Fields", ): """ Create a field plot of the line. @@ -2892,6 +2912,8 @@ def create_fieldplot_line_traces( is ``{}``. plot_name : str, optional Name of the field plot to create. The default is ``None``. + field_type : str, optional + Field type to plot. Valid only for Q3D Field plots. Returns ------- @@ -2965,6 +2987,7 @@ def create_fieldplot_line_traces( setup_name, intrinsinc_dict, plot_name, + field_type=field_type, ) @pyaedt_function_handler() @@ -3033,7 +3056,9 @@ def create_fieldplot_layers_nets( return self._create_fieldplot(layers_nets, quantity_name, setup_name, intrinsics, plot_type, plot_name) @pyaedt_function_handler() - def create_fieldplot_surface(self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None): + def create_fieldplot_surface( + self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None, field_type="DC R/L Fields" + ): """Create a field plot of surfaces. Parameters @@ -3051,6 +3076,8 @@ def create_fieldplot_surface(self, objlist, quantityName, setup_name=None, intri is ``{}``. plot_name : str, optional Name of the fieldplot to create. + field_type : str, optional + Field type to plot. Valid only for Q3D Field plots. Returns ------- @@ -3075,11 +3102,20 @@ def create_fieldplot_surface(self, objlist, quantityName, setup_name=None, intri new_obj_list.extend([i.id for i in self._app.modeler[objs].faces]) else: new_obj_list.append(objs) - return self._create_fieldplot(new_obj_list, quantityName, setup_name, intrinsincDict, "FacesList", plot_name) + return self._create_fieldplot( + new_obj_list, quantityName, setup_name, intrinsincDict, "FacesList", plot_name, field_type=field_type + ) @pyaedt_function_handler() def create_fieldplot_cutplane( - self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None, filter_objects=[] + self, + objlist, + quantityName, + setup_name=None, + intrinsincDict=None, + plot_name=None, + filter_objects=[], + field_type="DC R/L Fields", ): """Create a field plot of cut planes. @@ -3100,6 +3136,8 @@ def create_fieldplot_cutplane( Name of the fieldplot to create. filter_objects : list, optional Objects list on which filter the plot. + field_type : str, optional + Field type to plot. Valid only for Q3D Field plots. Returns ------- @@ -3119,11 +3157,26 @@ def create_fieldplot_cutplane( if filter_objects: filter_objects = self._app.modeler.convert_to_selections(filter_objects, True) return self._create_fieldplot( - objlist, quantityName, setup_name, intrinsincDict, "CutPlane", plot_name, filter_boxes=filter_objects + objlist, + quantityName, + setup_name, + intrinsincDict, + "CutPlane", + plot_name, + filter_boxes=filter_objects, + field_type=field_type, ) @pyaedt_function_handler() - def create_fieldplot_volume(self, objlist, quantityName, setup_name=None, intrinsincDict=None, plot_name=None): + def create_fieldplot_volume( + self, + objlist, + quantityName, + setup_name=None, + intrinsincDict=None, + plot_name=None, + field_type="DC R/L Fields", + ): """Create a field plot of volumes. Parameters @@ -3157,7 +3210,9 @@ def create_fieldplot_volume(self, objlist, quantityName, setup_name=None, intrin if plot_name and plot_name in list(self.field_plots.keys()): self.logger.info("Plot {} exists. returning the object.".format(plot_name)) return self.field_plots[plot_name] - return self._create_fieldplot(objlist, quantityName, setup_name, intrinsincDict, "ObjList", plot_name) + return self._create_fieldplot( + objlist, quantityName, setup_name, intrinsincDict, "ObjList", plot_name, field_type=field_type + ) @pyaedt_function_handler() def export_field_jpg( @@ -3514,7 +3569,7 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj else: fname = os.path.join(export_path, "Model_AllObjs_AllMats.obj") self._app.modeler.oeditor.ExportModelMeshToFile(fname, obj_list) - return [[fname, "grey", 0.6]] + return [[fname, "aquamarine", 0.3]] @pyaedt_function_handler() def export_mesh_obj(self, setup_name=None, intrinsic_dict=None): diff --git a/pyaedt/modules/solutions.py b/pyaedt/modules/solutions.py index 561c5f61e69..9249f813c92 100644 --- a/pyaedt/modules/solutions.py +++ b/pyaedt/modules/solutions.py @@ -2566,6 +2566,7 @@ def __init__( self.SeedingPointsNumber = 15 self.FractionOfMaximum = 0.8 self._filter_boxes = [] + self.field_type = None @property def filter_boxes(self): @@ -2776,7 +2777,7 @@ def surfacePlotInstruction(self): List of surface plot settings. """ - return [ + out = [ "NAME:" + self.name, "SolutionName:=", self.solutionName, @@ -2784,26 +2785,33 @@ def surfacePlotInstruction(self): self.quantityName, "PlotFolder:=", self.plotFolder, - "UserSpecifyName:=", - 1, - "UserSpecifyFolder:=", - 1, - "StreamlinePlot:=", - False, - "AdjacentSidePlot:=", - False, - "FullModelPlot:=", - False, - "IntrinsicVar:=", - self.intrinsicVar, - "PlotGeomInfo:=", - self.plotGeomInfo, - "FilterBoxes:=", - [len(self.filter_boxes)] + self.filter_boxes, - self.plotsettings, - "EnableGaussianSmoothing:=", - False, ] + if self.field_type: + out.extend(["FieldType:=", self.field_type]) + out.extend( + [ + "UserSpecifyName:=", + 1, + "UserSpecifyFolder:=", + 1, + "StreamlinePlot:=", + False, + "AdjacentSidePlot:=", + False, + "FullModelPlot:=", + False, + "IntrinsicVar:=", + self.intrinsicVar, + "PlotGeomInfo:=", + self.plotGeomInfo, + "FilterBoxes:=", + [len(self.filter_boxes)] + self.filter_boxes, + self.plotsettings, + "EnableGaussianSmoothing:=", + False, + ] + ) + return out @property def surfacePlotInstructionLineTraces(self): @@ -2818,7 +2826,7 @@ def surfacePlotInstructionLineTraces(self): List of plot settings for line traces. """ - return [ + out = [ "NAME:" + self.name, "SolutionName:=", self.solutionName, @@ -2830,45 +2838,52 @@ def surfacePlotInstructionLineTraces(self): "QuantityName_FieldLineTrace", "PlotFolder:=", self.plotFolder, - "IntrinsicVar:=", - self.intrinsicVar, - "Trace Step Length:=", - self.TraceStepLength, - "Use Adaptive Step:=", - self.UseAdaptiveStep, - "Seeding Faces:=", - self.seeding_faces, - "Seeding Markers:=", - [0], - "Surface Tracing Objects:=", - self.surfaces_indexes, - "Volume Tracing Objects:=", - self.volume_indexes, - "Seeding Sampling Option:=", - self.SeedingSamplingOption, - "Seeding Points Number:=", - self.SeedingPointsNumber, - "Fractional of Maximal:=", - self.FractionOfMaximum, - "Discrete Seeds Option:=", - "Marker Point", - [ - "NAME:InceptionEvaluationSettings", - "Gas Type:=", - 0, - "Gas Pressure:=", - 1, - "Use Inception:=", - True, - "Potential U0:=", - 0, - "Potential K:=", - 0, - "Potential A:=", - 1, - ], - self.field_line_trace_plot_settings, ] + if self.field_type: + out.extend(["FieldType:=", self.field_type]) + out.extend( + [ + "IntrinsicVar:=", + self.intrinsicVar, + "Trace Step Length:=", + self.TraceStepLength, + "Use Adaptive Step:=", + self.UseAdaptiveStep, + "Seeding Faces:=", + self.seeding_faces, + "Seeding Markers:=", + [0], + "Surface Tracing Objects:=", + self.surfaces_indexes, + "Volume Tracing Objects:=", + self.volume_indexes, + "Seeding Sampling Option:=", + self.SeedingSamplingOption, + "Seeding Points Number:=", + self.SeedingPointsNumber, + "Fractional of Maximal:=", + self.FractionOfMaximum, + "Discrete Seeds Option:=", + "Marker Point", + [ + "NAME:InceptionEvaluationSettings", + "Gas Type:=", + 0, + "Gas Pressure:=", + 1, + "Use Inception:=", + True, + "Potential U0:=", + 0, + "Potential K:=", + 0, + "Potential A:=", + 1, + ], + self.field_line_trace_plot_settings, + ] + ) + return out @property def field_plot_settings(self): From 7eae896a1e6dd2514f6dfdf4138a3e83423bc287 Mon Sep 17 00:00:00 2001 From: Hui Zhou Date: Thu, 26 Oct 2023 14:56:03 +0200 Subject: [PATCH 2/2] terminal enhancement (#3795) * fix * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update pyaedt/edb_core/edb_data/terminals.py Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> * Update pyaedt/edb_core/edb_data/terminals.py Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> * fix --------- 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: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> --- _unittest/test_00_EDB.py | 11 +++ pyaedt/edb.py | 13 ++- pyaedt/edb_core/edb_data/obj_base.py | 4 +- pyaedt/edb_core/edb_data/ports.py | 24 ------ pyaedt/edb_core/edb_data/terminals.py | 118 ++++++++++++++++++++++++-- pyaedt/edb_core/siwave.py | 44 +++++++++- 6 files changed, 177 insertions(+), 37 deletions(-) diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index c47d1346d6b..e79ebe9cbce 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -456,6 +456,15 @@ def test_042_create_current_source(self): self.edbapp.siwave.create_pin_group(reference_designator="U1", pin_numbers=["A14", "A15"], group_name="vp_neg") assert self.edbapp.siwave.create_voltage_probe_on_pin_group("vprobe", "vp_pos", "vp_neg") assert self.edbapp.probes["vprobe"] + self.edbapp.siwave.place_voltage_probe( + "vprobe_2", "1V0", ["112mm", "24mm"], "1_Top", "GND", ["112mm", "27mm"], "Inner1(GND1)" + ) + vprobe_2 = self.edbapp.probes["vprobe_2"] + ref_term = vprobe_2.ref_terminal + assert isinstance(ref_term.location, list) + ref_term.location = [0, 0] + assert ref_term.layer + ref_term.layer = "1_Top" def test_043_create_dc_terminal(self): assert self.edbapp.siwave.create_dc_terminal("U1", "DDR4_DQ40", "dc_terminal1") == "dc_terminal1" @@ -1602,6 +1611,8 @@ def test_120_edb_create_port(self): port_ver = edb.ports["port_ver"] assert not port_ver.is_null assert port_ver.hfss_type == "Gap" + port_hori = edb.ports["port_hori"] + assert port_hori.ref_terminal args = { "layer_name": "1_Top", diff --git a/pyaedt/edb.py b/pyaedt/edb.py index 667b2ea3104..508fab190cc 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -24,7 +24,6 @@ from pyaedt.edb_core.edb_data.hfss_simulation_setup_data import HfssSimulationSetup from pyaedt.edb_core.edb_data.ports import BundleWavePort from pyaedt.edb_core.edb_data.ports import CoaxPort -from pyaedt.edb_core.edb_data.ports import ExcitationProbes from pyaedt.edb_core.edb_data.ports import ExcitationSources from pyaedt.edb_core.edb_data.ports import GapPort from pyaedt.edb_core.edb_data.ports import WavePort @@ -35,8 +34,10 @@ from pyaedt.edb_core.edb_data.terminals import BundleTerminal from pyaedt.edb_core.edb_data.terminals import EdgeTerminal from pyaedt.edb_core.edb_data.terminals import PadstackInstanceTerminal +from pyaedt.edb_core.edb_data.terminals import PinGroupTerminal from pyaedt.edb_core.edb_data.terminals import Terminal from pyaedt.edb_core.edb_data.variables import Variable +from pyaedt.edb_core.general import BoundaryType from pyaedt.edb_core.general import LayoutObjType from pyaedt.edb_core.general import Primitives from pyaedt.edb_core.general import TerminalType @@ -364,6 +365,8 @@ def terminals(self): ter = BundleTerminal(self, i) elif terminal_type == TerminalType.PadstackInstanceTerminal.name: ter = PadstackInstanceTerminal(self, i) + elif terminal_type == TerminalType.PinGroupTerminal.name: + ter = PinGroupTerminal(self, i) else: ter = Terminal(self, i) temp[ter.name] = ter @@ -424,8 +427,12 @@ def sources(self): @property def probes(self): """Get all layout sources.""" - terms = [term for term in self.layout.terminals if int(term.GetBoundaryType()) in [8]] - return {ter.GetName(): ExcitationProbes(self, ter) for ter in terms} + temp = {} + for name, val in self.terminals.items(): + if val.boundary_type == BoundaryType.kVoltageProbe.name: + if not val.is_reference_terminal: + temp[name] = val + return temp @pyaedt_function_handler() def open_edb(self): diff --git a/pyaedt/edb_core/edb_data/obj_base.py b/pyaedt/edb_core/edb_data/obj_base.py index 4d43360e47a..45945326703 100644 --- a/pyaedt/edb_core/edb_data/obj_base.py +++ b/pyaedt/edb_core/edb_data/obj_base.py @@ -1,9 +1,9 @@ class ObjBase(object): """Manages EDB functionalities for a base object.""" - def __init__(self, pedb, model): + def __init__(self, pedb, edb_object): self._pedb = pedb - self._edb_object = model + self._edb_object = edb_object @property def is_null(self): diff --git a/pyaedt/edb_core/edb_data/ports.py b/pyaedt/edb_core/edb_data/ports.py index 26e5d1271c7..a26d240be6f 100644 --- a/pyaedt/edb_core/edb_data/ports.py +++ b/pyaedt/edb_core/edb_data/ports.py @@ -176,30 +176,6 @@ def phase(self, value): self._edb_object.SetSourcePhase(self._edb.utility.value(value)) -class ExcitationProbes(Terminal): - """Manage probes properties. - - Parameters - ---------- - pedb : pyaedt.edb.Edb - Edb object from Edblib. - edb_terminal : Ansys.Ansoft.Edb.Cell.Terminal.EdgeTerminal - Edge terminal instance from Edb. - - - Examples - -------- - This example shows how to access this class. - >>> from pyaedt import Edb - >>> edb = Edb("myaedb.aedb") - >>> probes = edb.probes - >>> print(probes["Probe1"].name) - """ - - def __init__(self, pedb, edb_terminal): - Terminal.__init__(self, pedb, edb_terminal) - - class BundleWavePort(BundleTerminal): """Manages bundle wave port properties. diff --git a/pyaedt/edb_core/edb_data/terminals.py b/pyaedt/edb_core/edb_data/terminals.py index 2a996f08df1..39245883611 100644 --- a/pyaedt/edb_core/edb_data/terminals.py +++ b/pyaedt/edb_core/edb_data/terminals.py @@ -4,6 +4,7 @@ from pyaedt.edb_core.edb_data.connectable import Connectable from pyaedt.edb_core.edb_data.padstacks_data import EDBPadstackInstance from pyaedt.edb_core.edb_data.primitives_data import cast +from pyaedt.edb_core.general import BoundaryType from pyaedt.edb_core.general import TerminalType from pyaedt.edb_core.general import convert_py_list_to_net_list from pyaedt.generic.general_methods import generate_unique_name @@ -135,7 +136,17 @@ def boundary_type(self): ------- int """ - return self._edb_object.GetBoundaryType() + return self._edb_object.GetBoundaryType().ToString() + + @boundary_type.setter + def boundary_type(self, value): + if not value in [i.name for i in BoundaryType]: # pragma : no cover + self._pedb.logger.warning("Invalid Boundary Type={}".format(value)) + if value == self._pedb.edb_api.cell.terminal.BoundaryType.kVoltageProbe.ToString(): + temp = self._pedb.edb_api.cell.terminal.BoundaryType.kVoltageProbe + else: # pragma : no cover + temp = self._pedb.edb_api.cell.terminal.BoundaryType.InvalidBoundary + self._edb_object.SetBoundaryType(temp) @property def impedance(self): @@ -146,6 +157,28 @@ def impedance(self): def impedance(self, value): self._edb_object.SetImpedance(self._pedb.edb_value(value)) + @property + def is_reference_terminal(self): + """Whether it is a reference terminal.""" + return self._edb_object.IsReferenceTerminal() + + @property + def ref_terminal(self): + """Get reference terminal.""" + + terminal = Terminal(self._pedb, self._edb_object.GetReferenceTerminal()) + if not terminal.is_null: + if terminal.terminal_type == TerminalType.PointTerminal.name: + return PointTerminal(self._pedb, terminal._edb_object) + elif terminal.terminal_type == TerminalType.EdgeTerminal.name: + return EdgeTerminal(self._pedb, terminal._edb_object) + elif terminal.terminal_type == TerminalType.InvalidTerminal.name: # pragma : no cover + return None + + @ref_terminal.setter + def ref_terminal(self, value): + self._edb_object.SetReferenceTerminal(value._edb_object) + @property def reference_object(self): # pragma : no cover """This returns the object assigned as reference. It can be a primitive or a padstack instance. @@ -244,7 +277,7 @@ def get_pin_group_terminal_reference_pin(self, gnd_net_name_preference=None): # return EDBPadstackInstance(refTermPSI, self._pedb) except AttributeError: return None - return None # pragma: no cover + return None @pyaedt_function_handler() def get_edge_terminal_reference_primitive(self): # pragma : no cover @@ -268,7 +301,7 @@ def get_edge_terminal_reference_primitive(self): # pragma : no cover prim_shape_data = primitive.GetPolygonData() if prim_shape_data.PointInPolygon(shape_pd): return cast(primitive, self._pedb) - return None # pragma: no cover + return None @pyaedt_function_handler() def get_point_terminal_reference_primitive(self): # pragma : no cover @@ -331,11 +364,11 @@ def _get_closest_pin(self, ref_pin, pin_list, gnd_net=None): else: power_ground_net_names = [net for net in self._pedb.nets.power_nets.keys()] comp_ref_pins = [i for i in pin_list if i.GetNet().GetName() in power_ground_net_names] - if len(comp_ref_pins) == 0: + if len(comp_ref_pins) == 0: # pragma: no cover self._pedb.logger.error( "Terminal with PadStack Instance Name {} component has no reference pins.".format(ref_pin.GetName()) - ) # pragma: no cover - return None # pragma: no cover + ) + return None closest_pin_distance = None pin_obj = None for pin in comp_ref_pins: # find the distance to all the pins to the terminal pin @@ -456,3 +489,76 @@ def create(self, padstack_instance, name=None, layer=None, is_ref=False): terminal = PadstackInstanceTerminal(self._pedb, terminal) return terminal if not terminal.is_null else False + + +class PointTerminal(Terminal): + """Manages point terminal properties.""" + + def __init__(self, pedb, edb_object=None): + super().__init__(pedb, edb_object) + + @pyaedt_function_handler + def create(self, name, net, location, layer, is_ref=False): + """Create a point terminal. + + Parameters + ---------- + name : str + Name of the terminal. + net : str + Name of the net. + location : list + Location of the terminal. + layer : str + Name of the layer. + is_ref : bool, optional + Whether it is a reference terminal. + + Returns + ------- + + """ + terminal = self._pedb.edb_api.cell.terminal.PointTerminal.Create( + self._pedb.active_layout, + self._pedb.nets[net].net_object, + name, + self._pedb.point_data(*location), + self._pedb.stackup[layer]._edb_layer, + is_ref, + ) + terminal = PointTerminal(self._pedb, terminal) + return terminal if not terminal.is_null else False + + @property + def location(self): + """Get location of the terminal.""" + point_data = self._pedb.point_data(0, 0) + layer = list(self._pedb.stackup.layers.values())[0]._edb_layer + if self._edb_object.GetParameters(point_data, layer): + return [point_data.X.ToDouble(), point_data.Y.ToDouble()] + + @location.setter + def location(self, value): + layer = self.layer + self._edb_object.SetParameters(self._pedb.point_data(*value), layer) + + @property + def layer(self): + """Get layer of the terminal.""" + point_data = self._pedb.point_data(0, 0) + layer = list(self._pedb.stackup.layers.values())[0]._edb_layer + if self._edb_object.GetParameters(point_data, layer): + return layer + + @layer.setter + def layer(self, value): + layer = self._pedb.stackup.layers[value]._edb_layer + point_data = self._pedb.point_data(*self.location) + self._edb_object.SetParameters(point_data, layer) + + +class PinGroupTerminal(Terminal): + """Manages pin group terminal properties.""" + + def __init__(self, pedb, edb_object=None): + super().__init__(pedb, edb_object) diff --git a/pyaedt/edb_core/siwave.py b/pyaedt/edb_core/siwave.py index 647f8afa3ef..8d6b65c0ced 100644 --- a/pyaedt/edb_core/siwave.py +++ b/pyaedt/edb_core/siwave.py @@ -7,14 +7,13 @@ from pyaedt.edb_core.edb_data.simulation_configuration import SimulationConfiguration from pyaedt.edb_core.edb_data.simulation_configuration import SourceType - -# from pyaedt.edb_core.edb_data.sources import SourceType from pyaedt.edb_core.edb_data.sources import CircuitPort from pyaedt.edb_core.edb_data.sources import CurrentSource from pyaedt.edb_core.edb_data.sources import DCTerminal from pyaedt.edb_core.edb_data.sources import PinGroup from pyaedt.edb_core.edb_data.sources import ResistorSource from pyaedt.edb_core.edb_data.sources import VoltageSource +from pyaedt.edb_core.general import BoundaryType from pyaedt.edb_core.general import convert_py_list_to_net_list from pyaedt.generic.constants import SolverType from pyaedt.generic.constants import SweepType @@ -1388,3 +1387,44 @@ def create_circuit_port_on_pin_group(self, pos_pin_group_name, neg_pin_group_nam neg_terminal.SetName(name + "_ref") pos_terminal.SetReferenceTerminal(neg_terminal) return True + + @pyaedt_function_handler + def place_voltage_probe( + self, + name, + positive_net_name, + positive_location, + positive_layer, + negative_net_name, + negative_location, + negative_layer, + ): + """Place a voltage probe between two points. + + Parameters + ---------- + name : str, + Name of the probe. + positive_net_name : str + Name of the positive net. + positive_location : list + Location of the positive terminal. + positive_layer : str, + Layer of the positive terminal. + negative_net_name : str, + Name of the negative net. + negative_location : list + Location of the negative terminal. + negative_layer : str + Layer of the negative terminal. + """ + from pyaedt.edb_core.edb_data.terminals import PointTerminal + + point_terminal = PointTerminal(self._pedb) + p_terminal = point_terminal.create(name, positive_net_name, positive_location, positive_layer) + p_terminal.boundary_type = BoundaryType.kVoltageProbe.name + + n_terminal = point_terminal.create(name + "_ref", negative_net_name, negative_location, negative_layer) + n_terminal.boundary_type = BoundaryType.kVoltageProbe.name + p_terminal.ref_terminal = n_terminal + return self._pedb.probes[name]