diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ca4529e4ec..74280703f34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,3 +12,37 @@ questions or submissions to this repository. For contributing to this project, please refer to the [PyAnsys Developer's Guide]. [PyAnsys Developer's Guide]: https://dev.docs.pyansys.com/index.html + +## Directing Issues and Features Requests + +For reporting bugs and proposing new features, please use the Issues tab instead of the Discussions tab. This will help us track and prioritize work in a more organized manner. + +## Purpose of Discussions + +The Discussions tab should primarily be used for general questions and discussions about the project. This could include discussions about potential improvements, the future direction of the project, brainstorming ideas, help with using the software, and other topics that don't exactly fit as an Issue. + +Remember, maintaining distinct places for different types of interactions helps keep our project organized and moving forward in a coordinated manner. + +## Modify the Code + +Everyone can contribute to this project, irrespective of their level of expertise. Your diverse skills, perspectives, and experiences are valuable and we welcome them. + +Here's a simple overview of how you can start making contributions: + +**Fork the Repository:** "Forking" means creating a personal copy of this repository on your GitHub account. + +**Clone the Repository:** After forking, you need to download the repository to your local machine. This can be done using the `git clone` command. + +**Create a New Branch:** A branch is used to isolate your changes from the main project. You can create a new branch using the `git branch` command. Remember to switch to your new branch with the `git checkout` command. + +**Commit Your Changes:** After making your changes, you need to "commit" them. A commit is a packaged set of alterations. Use `git add` to add your files to staging, and then `git commit -m "your message"` to commit them. + +**Push Your Changes:** After committing your changes, "push" them to your forked repository on GitHub with `git push origin `. + +**Create a Pull Request:** A Pull Request (PR) lets us know you have changes you think should be included in the main project. Go to your forked repository on GitHub and click on the 'Pull request' button. + +Following these steps ensures that your contributions will be easily reviewed and potentially included in the project much faster. + +Please don't get discouraged if you find these steps complex, we are here to help you throughout the process. + +We hope these rules will make the Discussions section a better place for every contributor. diff --git a/_unittest/example_models/T20/test_cad.nas b/_unittest/example_models/T20/test_cad.nas index 948c6b48f5a..640b41ee14f 100644 --- a/_unittest/example_models/T20/test_cad.nas +++ b/_unittest/example_models/T20/test_cad.nas @@ -23,7 +23,7 @@ GRID 17 -142.085-81.8469969.8416 GRID 18 338.1222.4335859436.2341 GRID 19 328.0788-91.8056 418.809 GRID 20 260.6554-89.1117 459.994 -GRID 21 1574.546 -4.1-111484.805 +GRID 21 1574.546 -34.1-11484.805 GRID 22 4411.665-98.6627 505.296 GRID 23 4410.392-95.7236504.7545 GRID 24 4412.102-93.0528505.4793 @@ -38,18 +38,18 @@ GRID 32 4410.434-95.7239504.6637 GRID 33 4414.683-98.9433506.4686 GRID 34 4416.393-96.2763507.1935 GRID 35 4413.413-96.0004505.9284 -GRID 40 0.0 0.0 0.0 -GRID 41 1.0 0.0 0.0 -GRID 42 1.0 1.0 0.0 -GRID 43 0.0 1.0 0.0 -GRID 44 0.0 0.0 1.0 -GRID 45 1.0 0.0 1.0 -GRID 46 1.0 1.0 1.0 -GRID 47 0.0 1.0 1.0 -GRID 50 0.0 0.0 0.0 -GRID 51 2.0 0.0 0.0 -GRID 52 0.0 2.0 0.0 -GRID 53 0.0 0.0 2.0 +GRID 40 0.0 0.0 0.0 +GRID 41 1.0 0.0 0.0 +GRID 42 1.0 1.0 0.0 +GRID 43 0.0 1.0 0.0 +GRID 44 0.0 0.0 1.0 +GRID 45 1.0 0.0 1.0 +GRID 46 1.0 1.0 1.0 +GRID 47 0.0 1.0 1.0 +GRID 50 0.0 0.0 0.0 +GRID 51 2.0 0.0 0.0 +GRID 52 0.0 2.0 0.0 +GRID 53 0.0 0.0 2.0 CTRIA3 92455 31 4 5 6 CTRIA3 92456 31 5 10 11 CTRIA3 92457 31 21 20 16 @@ -64,7 +64,8 @@ CPENTA 25229 9 27 22 28 33 29 35 CPENTA 25279 9 25 28 24 30 35 31 CPENTA 25284 9 26 27 28 34 33 35 CPENTA 25328 9 26 28 25 34 35 30 -CHEXA 1 105 40 41 42 43 44 45 46 47 +CHEXA 1 105 40 41 42 43 44 45 +* 46 47 CTETRA 1 115 50 51 52 53 ENDDATA diff --git a/_unittest/example_models/T20/test_cad_2.nas b/_unittest/example_models/T20/test_cad_2.nas new file mode 100644 index 00000000000..f41ccef4bea --- /dev/null +++ b/_unittest/example_models/T20/test_cad_2.nas @@ -0,0 +1,24 @@ +$ +$ Settings : +$ +$ Output format : MSC Nastran +$ +$ Output : Visible +$ +$ +$ +$ +BEGIN BULK +GRID* 4 627.87512 568.96751* +* 1942.1985 +GRID* 5 812.95973 486.74968* +* 1495.9564 +GRID* 6 812.99916 484.24676* +* 1470.1764 +CTRIA3* 75986 19 4 5 +* 6 + + +ENDDATA +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ diff --git a/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup.json b/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup.json new file mode 100644 index 00000000000..7d9b5448660 --- /dev/null +++ b/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup.json @@ -0,0 +1,148 @@ +{ + "filename": null, + "open_edb_after_build": true, + "dc_settings": { + "dc_compute_inductance": false, + "dc_contact_radius": "100um", + "dc_slide_position": 1, + "dc_use_dc_custom_settings": false, + "dc_plot_jv": true, + "dc_min_plane_area_to_mesh": "8mil2", + "dc_min_void_area_to_mesh": "0.734mil2", + "dc_error_energy": 0.02, + "dc_max_init_mesh_edge_length": "5.0mm", + "dc_max_num_pass": 5, + "dc_min_num_pass": 1, + "dc_mesh_bondwires": true, + "dc_num_bondwire_sides": 8, + "dc_mesh_vias": true, + "dc_num_via_sides": 8, + "dc_percent_local_refinement": 0.2, + "dc_perform_adaptive_refinement": true, + "dc_refine_bondwires": true, + "dc_refine_vias": true, + "dc_report_config_file": "", + "dc_report_show_Active_devices": true, + "dc_export_thermal_data": true, + "dc_full_report_path": "", + "dc_icepak_temp_file": "", + "dc_import_thermal_data": false, + "dc_per_pin_res_path": "", + "dc_per_pin_use_pin_format": true, + "dc_use_loop_res_for_per_pin": true, + "dc_via_report_path": "", + "dc_source_terms_to_ground": {} + }, + "ac_settings": { + "sweep_interpolating": true, + "use_q3d_for_dc": false, + "relative_error": 0.005, + "use_error_z0": false, + "percentage_error_z0": 1, + "enforce_causality": true, + "enforce_passivity": false, + "passivity_tolerance": 0.0001, + "sweep_name": "Sweep1", + "radiation_box": 2, + "start_freq": "0.0GHz", + "stop_freq": "20GHz", + "sweep_type": 0, + "step_freq": "0GHz", + "decade_count": 100, + "mesh_freq": "33GHz", + "max_num_passes": 30, + "max_mag_delta_s": 0.03, + "min_num_passes": 1, + "basis_order": 0, + "do_lambda_refinement": true, + "arc_angle": "10deg", + "start_azimuth": 5, + "max_arc_points": 24, + "use_arc_to_chord_error": true, + "arc_to_chord_error": "1um", + "defeature_abs_length": "1um", + "defeature_layout": true, + "minimum_void_surface": 0, + "max_suf_dev": 0.001, + "process_padstack_definitions": false, + "return_current_distribution": true, + "ignore_non_functional_pads": true, + "include_inter_plane_coupling": true, + "xtalk_threshold": -50, + "min_void_area": "0.01mm2", + "min_pad_area_to_mesh": "0.01mm2", + "snap_length_threshold": "2.5um", + "min_plane_area_to_mesh": "4mil2", + "mesh_sizefactor": 0.0 + }, + "batch_solve_settings": { + "signal_nets": [ + "PCIe_Gen4_RX0_N", + "PCIe_Gen4_RX0_P", + "PCIe_Gen4_RX1_N", + "PCIe_Gen4_RX1_P", + "PCIe_Gen4_RX2_N", + "PCIe_Gen4_RX2_P", + "PCIe_Gen4_RX3_N", + "PCIe_Gen4_RX3_P", + "PCIe_Gen4_TX0_N", + "PCIe_Gen4_TX0_CAP_N", + "PCIe_Gen4_TX0_p", + "PCIe_Gen4_TX0_CAP_P", + "PCIe_Gen4_TX1_N", + "PCIe_Gen4_TX1_CAP_N", + "PCIe_Gen4_TX1_P", + "PCIe_Gen4_TX1_CAP_P", + "PCIe_Gen4_TX2_N", + "PCIe_Gen4_TX2_CAP_N", + "PCIe_Gen4_TX2_P", + "PCIe_Gen4_TX2_CAP_P", + "PCIe_Gen4_TX3_N", + "PCIe_Gen4_TX3_CAP_N", + "PCIe_Gen4_TX3_P", + "PCIe_Gen4_TX3_CAP_P" + ], + "power_nets": [ + "1V0", + "2V5", + "5V", + "GND" + ], + "components": [ + "X1", + "U1" + ], + "cutout_subdesign_type": 1, + "cutout_subdesign_expansion": 0.001, + "cutout_subdesign_round_corner": true, + "use_default_cutout": false, + "generate_excitations": true, + "add_frequency_sweep": true, + "include_only_selected_nets": false, + "generate_solder_balls": true, + "coax_solder_ball_diameter": [], + "use_default_coax_port_radial_extension": true, + "trim_reference_size": false, + "output_aedb": null, + "signal_layers_properties": {}, + "coplanar_instances": [], + "signal_layer_etching_instances": [], + "etching_factor_instances": [], + "use_dielectric_extent_multiple": true, + "dielectric_extent": 0.001, + "use_airbox_horizontal_multiple": true, + "airbox_horizontal_extent": 0.1, + "use_airbox_negative_vertical_extent_multiple": true, + "airbox_negative_vertical_extent": 0.1, + "use_airbox_positive_vertical_extent_multiple": true, + "airbox_positive_vertical_extent": 0.1, + "honor_user_dielectric": false, + "truncate_airbox_at_ground": false, + "use_radiation_boundary": true, + "do_cutout_subdesign": true, + "do_pin_group": true, + "sources": [] + }, + "setup_name": "Pyaedt_setup", + "solver_type": 6 +} \ No newline at end of file diff --git a/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup_custom_sballs.json b/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup_custom_sballs.json new file mode 100644 index 00000000000..ba5ccd3273b --- /dev/null +++ b/_unittest/example_models/TEDB/ANSYS-HSD_V1.aedb/simsetup_custom_sballs.json @@ -0,0 +1,156 @@ +{ + "filename": null, + "open_edb_after_build": true, + "dc_settings": { + "dc_compute_inductance": false, + "dc_contact_radius": "100um", + "dc_slide_position": 1, + "dc_use_dc_custom_settings": false, + "dc_plot_jv": true, + "dc_min_plane_area_to_mesh": "8mil2", + "dc_min_void_area_to_mesh": "0.734mil2", + "dc_error_energy": 0.02, + "dc_max_init_mesh_edge_length": "5.0mm", + "dc_max_num_pass": 5, + "dc_min_num_pass": 1, + "dc_mesh_bondwires": true, + "dc_num_bondwire_sides": 8, + "dc_mesh_vias": true, + "dc_num_via_sides": 8, + "dc_percent_local_refinement": 0.2, + "dc_perform_adaptive_refinement": true, + "dc_refine_bondwires": true, + "dc_refine_vias": true, + "dc_report_config_file": "", + "dc_report_show_Active_devices": true, + "dc_export_thermal_data": true, + "dc_full_report_path": "", + "dc_icepak_temp_file": "", + "dc_import_thermal_data": false, + "dc_per_pin_res_path": "", + "dc_per_pin_use_pin_format": true, + "dc_use_loop_res_for_per_pin": true, + "dc_via_report_path": "", + "dc_source_terms_to_ground": {} + }, + "ac_settings": { + "sweep_interpolating": true, + "use_q3d_for_dc": false, + "relative_error": 0.005, + "use_error_z0": false, + "percentage_error_z0": 1, + "enforce_causality": true, + "enforce_passivity": false, + "passivity_tolerance": 0.0001, + "sweep_name": "Sweep1", + "radiation_box": 2, + "start_freq": "0.0GHz", + "stop_freq": "20GHz", + "sweep_type": 0, + "step_freq": "0GHz", + "decade_count": 100, + "mesh_freq": "33GHz", + "max_num_passes": 30, + "max_mag_delta_s": 0.03, + "min_num_passes": 1, + "basis_order": 0, + "do_lambda_refinement": true, + "arc_angle": "10deg", + "start_azimuth": 5, + "max_arc_points": 24, + "use_arc_to_chord_error": true, + "arc_to_chord_error": "1um", + "defeature_abs_length": "1um", + "defeature_layout": true, + "minimum_void_surface": 0, + "max_suf_dev": 0.001, + "process_padstack_definitions": false, + "return_current_distribution": true, + "ignore_non_functional_pads": true, + "include_inter_plane_coupling": true, + "xtalk_threshold": -50, + "min_void_area": "0.01mm2", + "min_pad_area_to_mesh": "0.01mm2", + "snap_length_threshold": "2.5um", + "min_plane_area_to_mesh": "4mil2", + "mesh_sizefactor": 0.0 + }, + "batch_solve_settings": { + "signal_nets": [ + "PCIe_Gen4_RX0_N", + "PCIe_Gen4_RX0_P", + "PCIe_Gen4_RX1_N", + "PCIe_Gen4_RX1_P", + "PCIe_Gen4_RX2_N", + "PCIe_Gen4_RX2_P", + "PCIe_Gen4_RX3_N", + "PCIe_Gen4_RX3_P", + "PCIe_Gen4_TX0_N", + "PCIe_Gen4_TX0_CAP_N", + "PCIe_Gen4_TX0_p", + "PCIe_Gen4_TX0_CAP_P", + "PCIe_Gen4_TX1_N", + "PCIe_Gen4_TX1_CAP_N", + "PCIe_Gen4_TX1_P", + "PCIe_Gen4_TX1_CAP_P", + "PCIe_Gen4_TX2_N", + "PCIe_Gen4_TX2_CAP_N", + "PCIe_Gen4_TX2_P", + "PCIe_Gen4_TX2_CAP_P", + "PCIe_Gen4_TX3_N", + "PCIe_Gen4_TX3_CAP_N", + "PCIe_Gen4_TX3_P", + "PCIe_Gen4_TX3_CAP_P" + ], + "power_nets": [ + "1V0", + "2V5", + "5V", + "GND" + ], + "components": [ + { + "refdes": "X1", + "solder_balls_height": 0.00025, + "solder_balls_size": 0.0001, + "solder_balls_mid_size": 0.00015 + }, + { + "refdes": "U1", + "solder_balls_height": 0.00035 + } + ], + "cutout_subdesign_type": 1, + "cutout_subdesign_expansion": 0.001, + "cutout_subdesign_round_corner": true, + "use_default_cutout": false, + "generate_excitations": true, + "add_frequency_sweep": true, + "include_only_selected_nets": false, + "generate_solder_balls": true, + "coax_solder_ball_diameter": [], + "use_default_coax_port_radial_extension": true, + "trim_reference_size": false, + "output_aedb": null, + "signal_layers_properties": {}, + "coplanar_instances": [], + "signal_layer_etching_instances": [], + "etching_factor_instances": [], + "use_dielectric_extent_multiple": true, + "dielectric_extent": 0.001, + "use_airbox_horizontal_multiple": true, + "airbox_horizontal_extent": 0.1, + "use_airbox_negative_vertical_extent_multiple": true, + "airbox_negative_vertical_extent": 0.1, + "use_airbox_positive_vertical_extent_multiple": true, + "airbox_positive_vertical_extent": 0.1, + "honor_user_dielectric": false, + "truncate_airbox_at_ground": false, + "use_radiation_boundary": true, + "do_cutout_subdesign": true, + "do_pin_group": true, + "sources": [] + }, + "setup_name": "Pyaedt_setup", + "solver_type": 6 +} \ No newline at end of file diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index b926ebd088d..32b3a9f6949 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -264,12 +264,10 @@ def test_009_vias_creation(self): assert self.edbapp.padstacks.definitions["myVia"].hole_range == "through" self.edbapp.padstacks.create(padstackname="myVia_bullet", antipad_shape="Bullet") assert "myVia_bullet" in list(self.edbapp.padstacks.definitions.keys()) - self.edbapp.add_design_variable("via_x", 5e-3) self.edbapp["via_y"] = "1mm" assert self.edbapp["via_y"].value == 1e-3 assert self.edbapp["via_y"].value_string == "1mm" - assert self.edbapp.padstacks.place(["via_x", "via_x+via_y"], "myVia", via_name="via_test1") assert self.edbapp.padstacks.place(["via_x", "via_x+via_y*2"], "myVia_bullet") self.edbapp.padstacks["via_test1"].net_name = "GND" @@ -2921,9 +2919,8 @@ def test_147_find_dc_shorts(self): edbapp.layout_validation.illegal_rlc_values(True) # assert len(dc_shorts) == 20 - assert ["LVDS_CH09_N", "GND"] in dc_shorts - assert ["LVDS_CH09_N", "DDR4_DM3"] in dc_shorts - assert ["DDR4_DM3", "LVDS_CH07_N"] in dc_shorts + assert ["SFPA_Tx_Fault", "PCIe_Gen4_CLKREQ_L"] in dc_shorts + assert ["VDD_DDR", "GND"] in dc_shorts assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) > 0 edbapp.nets["DDR4_DM3"].find_dc_short(True) assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) == 0 @@ -2983,3 +2980,16 @@ def test_151_materials_read_materials(self): assert mats[key]["mass_density"] == 8055 assert mats[key]["specific_heat"] == 480 assert mats[key]["thermal_expansion_coeffcient"] == 1.08e-005 + + def test_152_simconfig_built_custom_sballs_height(self): + source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + target_path = os.path.join(self.local_scratch.path, "test_custom_sball_height", "ANSYS-HSD_V1.aedb") + self.local_scratch.copyfolder(source_path, target_path) + json_file = os.path.join(target_path, "simsetup_custom_sballs.json") + edbapp = Edb(target_path, edbversion=desktop_version) + simconfig = edbapp.new_simulation_configuration() + simconfig.import_json(json_file) + edbapp.build_simulation_project(simconfig) + assert round(edbapp.components["X1"].solder_ball_height, 6) == 0.00025 + assert round(edbapp.components["U1"].solder_ball_height, 6) == 0.00035 + edbapp.close_edb() diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 560ee092fd9..16e15f4df48 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -82,7 +82,7 @@ def test_02_create_material(self): assert mat1.set_electrical_steel_coreloss(1, 2, 3, 4, 0.002) assert mat1.get_curve_coreloss_type() == "Electrical Steel" - assert mat1.get_curve_coreloss_values()["core_loss_equiv_cut_depth"] == "0.002meter" + assert mat1.get_curve_coreloss_values()["core_loss_equiv_cut_depth"] == 0.002 assert mat1.set_hysteresis_coreloss(1, 2, 3, 4, 0.002) assert mat1.get_curve_coreloss_type() == "Hysteresis Model" assert mat1.set_bp_curve_coreloss([[0, 0], [10, 10], [20, 20]]) @@ -210,7 +210,7 @@ def test_08B_import_materials_from_excel(self): assert len(mats) == 2 def test_09_non_linear_materials(self, add_app): - app = add_app(application=Maxwell3d) + app = add_app(application=Maxwell3d, solution_type="Transient") mat1 = app.materials.add_material("myMat") mat1.permeability = [[0, 0], [1, 12], [10, 30]] mat1.permittivity = [[0, 0], [2, 12], [10, 30]] @@ -274,3 +274,108 @@ def test_13_get_materials_in_project(self): assert isinstance(used_materials, list) for m in [mat for mat in self.aedtapp.materials if mat.is_used]: assert m.name in used_materials + + def test_14_get_coreloss_coefficients(self): + mat = self.aedtapp.materials.add_material("mat_test") + # Test points_list_at_freq + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]} + ) + assert isinstance(coeff, list) + assert len(coeff) == 3 + assert all(isinstance(c, float) for c in coeff) + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={"60Hz": [[0, 0], [1, 3.5], [2, 7.4]]} + ) + assert isinstance(coeff, list) + assert len(coeff) == 3 + assert all(isinstance(c, float) for c in coeff) + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={"0.06kHz": [[0, 0], [1, 3.5], [2, 7.4]]} + ) + assert isinstance(coeff, list) + assert len(coeff) == 3 + assert all(isinstance(c, float) for c in coeff) + try: + self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq=[[0, 0], [1, 3.5], [2, 7.4]] + ) + assert False + except TypeError: + assert True + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={ + 60: [[0, 0], [1, 3.5], [2, 7.4]], + 100: [[0, 0], [1, 8], [2, 9]], + 150: [[0, 0], [1, 10], [2, 19]], + } + ) + assert isinstance(coeff, list) + assert len(coeff) == 3 + assert all(isinstance(c, float) for c in coeff) + # Test thickness + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="0.6mm" + ) + assert isinstance(coeff, list) + assert len(coeff) == 3 + assert all(isinstance(c, float) for c in coeff) + try: + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="invalid" + ) + assert False + except TypeError: + assert True + try: + coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness=50 + ) + assert False + except TypeError: + assert True + + def test_14_set_core_loss(self): + mat = self.aedtapp.materials["mat_test"] + # Test points_list_at_freq + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]} + ) + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={"60Hz": [[0, 0], [1, 3.5], [2, 7.4]]} + ) + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={"0.06kHz": [[0, 0], [1, 3.5], [2, 7.4]]} + ) + try: + self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq=[[0, 0], [1, 3.5], [2, 7.4]] + ) + assert False + except TypeError: + assert True + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={ + 60: [[0, 0], [1, 3.5], [2, 7.4]], + 100: [[0, 0], [1, 8], [2, 9]], + 150: [[0, 0], [1, 10], [2, 19]], + } + ) + # Test thickness + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="0.6mm" + ) + try: + coeff = self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="invalid" + ) + assert False + except TypeError: + assert True + try: + coeff = self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness=50 + ) + assert False + except TypeError: + assert True diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 5b5db41e60a..1457c97ba9b 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -209,14 +209,9 @@ def test_09_manipulate_report_D(self, field_test): def test_09_manipulate_report_E(self, field_test): field_test.modeler.create_polyline([[0, 0, 0], [0, 5, 30]], name="Poly1", non_model=True) variations2 = field_test.available_variations.nominal_w_values_dict - variations2["Theta"] = ["All"] - variations2["Phi"] = ["All"] - variations2["Freq"] = ["30GHz"] - variations2["Distance"] = ["All"] - assert field_test.post.create_report( + + assert field_test.setups[0].create_report( "Mag_E", - field_test.nominal_adaptive, - variations=variations2, primary_sweep_variable="Distance", context="Poly1", report_category="Fields", @@ -226,11 +221,10 @@ def test_09_manipulate_report_E(self, field_test): new_report.polyline = "Poly1" assert new_report.create() new_report = field_test.post.reports_by_category.modal_solution("S(1,1)") - new_report.plot_type = "Smith Chart" + new_report.report_type = "Smith Chart" assert new_report.create() - data = field_test.post.get_solution_data( + data = field_test.setups[0].get_solution_data( "Mag_E", - field_test.nominal_adaptive, variations=variations2, primary_sweep_variable="Theta", context="Poly1", @@ -269,7 +263,7 @@ def test_17_circuit(self, circuit_test): circuit_test.analyze_setup("LNA") circuit_test.analyze_setup("Transient") assert circuit_test.setups[0].is_solved - assert circuit_test.post.create_report(["dB(S(Port1, Port1))", "dB(S(Port1, Port2))"], "LNA") + assert circuit_test.setups[0].create_report(["dB(S(Port1, Port1))", "dB(S(Port1, Port2))"]) new_report = circuit_test.post.reports_by_category.standard( ["dB(S(Port1, Port1))", "dB(S(Port1, Port2))"], "LNA" ) @@ -382,7 +376,7 @@ def test_55_time_plot(self, sbr_test): assert sbr_test.setups[0].is_solved solution_data = sbr_test.post.get_solution_data( expressions=["NearEX", "NearEY", "NearEZ"], - variations={"_u": ["All"], "_v": ["All"], "Freq": ["All"]}, + # variations={"_u": ["All"], "_v": ["All"], "Freq": ["All"]}, context="Near_Field", report_category="Near Fields", ) diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index 72f828818a7..4ded45f5f93 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -1421,9 +1421,11 @@ def test_58_create_near_field_line(self): def test_59_test_nastran(self): self.aedtapp.insert_design("Nas_teest") example_project = os.path.join(local_path, "../_unittest/example_models", test_subfolder, "test_cad.nas") + example_project2 = os.path.join(local_path, "../_unittest/example_models", test_subfolder, "test_cad_2.nas") cads = self.aedtapp.modeler.import_nastran(example_project) assert len(cads) > 0 + assert self.aedtapp.modeler.import_nastran(example_project2) def test_60_set_variable(self): self.aedtapp.variable_manager.set_variable("var_test", expression="123") @@ -1481,8 +1483,13 @@ def test_63_set_phase_center_per_port(self): name="Wave2", renormalize=False, ) - assert self.aedtapp.set_phase_center_per_port() - assert self.aedtapp.set_phase_center_per_port(["Global", "Global"]) + if self.aedtapp.desktop_class.is_grpc_api: + assert self.aedtapp.set_phase_center_per_port() + assert self.aedtapp.set_phase_center_per_port(["Global", "Global"]) + else: + assert not self.aedtapp.set_phase_center_per_port() + assert not self.aedtapp.set_phase_center_per_port(["Global", "Global"]) + assert not self.aedtapp.set_phase_center_per_port(["Global"]) assert not self.aedtapp.set_phase_center_per_port("Global") diff --git a/_unittest/test_44_TouchstoneParser.py b/_unittest/test_44_TouchstoneParser.py index 5852b3fb7dc..726a4a74c99 100644 --- a/_unittest/test_44_TouchstoneParser.py +++ b/_unittest/test_44_TouchstoneParser.py @@ -38,7 +38,7 @@ def test_02_read_ts_file(self): ts1 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1234.s8p")) assert ts1.get_mixed_mode_touchstone_data() ts2 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1324.s8p")) - assert ts1.get_mixed_mode_touchstone_data(port_ordering="1324") + assert ts2.get_mixed_mode_touchstone_data(port_ordering="1324") assert ts1.plot_insertion_losses(plot=False) assert ts1.get_worst_curve(curve_list=ts1.get_return_loss_index(), plot=False) diff --git a/_unittest/test_98_Icepak.py b/_unittest/test_98_Icepak.py index b9e5ab38d95..07dc3477583 100644 --- a/_unittest/test_98_Icepak.py +++ b/_unittest/test_98_Icepak.py @@ -200,7 +200,7 @@ def test_12a_AssignMeshOperation(self): @pytest.mark.skipif(config["use_grpc"], reason="GRPC usage leads to SystemExit.") def test_12b_failing_AssignMeshOperation(self): - assert not self.aedtapp.mesh.assign_mesh_region("N0C0MP", 1, is_submodel=True) + assert self.aedtapp.mesh.assign_mesh_region("N0C0MP", 1, is_submodel=True) test = self.aedtapp.mesh.assign_mesh_region(["USB_ID"], 1) b = self.aedtapp.modeler.create_box([0, 0, 0], [1, 1, 1]) b.model = False diff --git a/_unittest_solvers/test_26_emit.py b/_unittest_solvers/test_26_emit.py index c6a6ea6b6d9..31a24239686 100644 --- a/_unittest_solvers/test_26_emit.py +++ b/_unittest_solvers/test_26_emit.py @@ -206,6 +206,45 @@ def test_radio_component(self, add_app): assert start_freq == 0.1 start_freq = radio.band_start_frequency(band, "THz") assert start_freq == 0.0001 + # test band.set_band_start_frequency + start_freq = 10 + units = 'MHz' + radio.set_band_start_frequency(band, start_freq, units=units) + assert radio.band_start_frequency(band, units=units) == start_freq + start_freq = 20000000 + radio.set_band_start_frequency(band, start_freq) + assert radio.band_start_frequency(band, units='Hz') == start_freq + # test band.set_band_stop_frequency + stop_freq = 30 + units = 'MHz' + radio.set_band_stop_frequency(band, stop_freq, units=units) + assert radio.band_stop_frequency(band, units=units) == stop_freq + stop_freq = 40000000 + radio.set_band_stop_frequency(band, stop_freq) + assert radio.band_stop_frequency(band, units='Hz') == stop_freq + # test corner cases for band start and stop frequencies + start_freq = 10 + stop_freq = 9 + units = 'Hz' + radio.set_band_start_frequency(band, start_freq, units=units) + radio.set_band_stop_frequency(band, stop_freq, units=units) + assert radio.band_start_frequency(band, units="Hz") == 8 + radio.set_band_start_frequency(band, 10, units=units) + assert radio.band_stop_frequency(band, units="Hz") == 11 + units = 'wrong' + radio.set_band_stop_frequency(band, 10, units=units) + assert radio.band_stop_frequency(band, units='Hz') == 10 + radio.set_band_start_frequency(band, 10, units=units) + assert radio.band_start_frequency(band, units='Hz') == 10 + with pytest.raises(ValueError) as e: + start_freq = 101 + units = 'GHz' + radio.set_band_start_frequency(band, start_freq, units=units) + assert "Frequency should be within 1Hz to 100 GHz." in str(e) + stop_freq = 102 + radio.set_band_stop_frequency(band, stop_freq, units=units) + assert "Frequency should be within 1Hz to 100 GHz." in str(e) + # test power unit conversions band_power = radio.band_tx_power(band) assert band_power == 40.0 diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 7c490d965fa..07e8cdebe20 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -1195,7 +1195,7 @@ def _create_setup(self, setupname="MySetupAuto", setuptype=None, props=None): setup = SetupHFSSAuto(self, setuptype, name) elif setuptype == 4: setup = SetupSBR(self, setuptype, name) - elif setuptype in [5, 6, 7, 8, 9, 10]: + elif setuptype in [5, 6, 7, 8, 9, 10, 56]: setup = SetupMaxwell(self, setuptype, name) elif setuptype in [14]: setup = SetupQ3D(self, setuptype, name) diff --git a/pyaedt/application/design_solutions.py b/pyaedt/application/design_solutions.py index 7d6207c5d73..e497c10fb1a 100644 --- a/pyaedt/application/design_solutions.py +++ b/pyaedt/application/design_solutions.py @@ -110,7 +110,7 @@ "name": None, "options": None, "report_type": None, - "default_setup": None, + "default_setup": 8, "default_adaptive": "LastAdaptive", "intrinsics": ["Freq", "Phase"], }, @@ -118,7 +118,7 @@ "name": None, "options": None, "report_type": None, - "default_setup": None, + "default_setup": 8, "default_adaptive": "LastAdaptive", }, "ElectroDCConduction": { @@ -138,13 +138,21 @@ }, }, "Twin Builder": { - "TR": {"name": None, "options": None, "report_type": "Standard", "default_setup": 35, "default_adaptive": None}, + "TR": { + "name": None, + "options": None, + "report_type": "Standard", + "default_setup": 35, + "default_adaptive": None, + "intrinsics": ["Time"], + }, "AC": { "name": None, "options": None, "report_type": "Standard", "default_setup": None, "default_adaptive": None, + "intrinsics": ["Freq"], }, "DC": { "name": None, @@ -161,6 +169,7 @@ "report_type": "Standard", "default_setup": 15, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximDC": { "name": None, @@ -175,6 +184,7 @@ "report_type": "Standard", "default_setup": 17, "default_adaptive": None, + "intrinsics": ["Time"], }, "NexximVerifEye": { "name": None, @@ -182,6 +192,7 @@ "report_type": "Standard", "default_setup": 19, "default_adaptive": None, + "intrinsics": ["Time"], }, "NexximQuickEye": { "name": None, @@ -189,6 +200,7 @@ "report_type": "Standard", "default_setup": 18, "default_adaptive": None, + "intrinsics": ["Time"], }, "NexximAMI": { "name": None, @@ -196,6 +208,7 @@ "report_type": "Standard", "default_setup": 20, "default_adaptive": None, + "intrinsics": ["Time"], }, "NexximOscillatorRSF": { "name": None, @@ -203,6 +216,7 @@ "report_type": "Standard", "default_setup": 21, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximOscillator1T": { "name": None, @@ -210,6 +224,7 @@ "report_type": "Standard", "default_setup": 22, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximOscillatorNT": { "name": None, @@ -217,6 +232,7 @@ "report_type": "Standard", "default_setup": 23, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximHarmonicBalance1T": { "name": None, @@ -224,6 +240,7 @@ "report_type": "Standard", "default_setup": 24, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximHarmonicBalanceNT": { "name": None, @@ -231,6 +248,7 @@ "report_type": "Standard", "default_setup": 25, "default_adaptive": None, + "intrinsics": ["Freq"], }, "NexximSystem": { "name": None, @@ -238,6 +256,7 @@ "report_type": "Standard", "default_setup": 26, "default_adaptive": None, + "intrinsics": ["Time"], }, "NexximTVNoise": { "name": None, @@ -245,6 +264,7 @@ "report_type": "Standard", "default_setup": 27, "default_adaptive": None, + "intrinsics": ["Freq"], }, "HSPICE": { "name": None, @@ -252,8 +272,16 @@ "report_type": "Standard", "default_setup": 28, "default_adaptive": None, + "intrinsics": ["Time"], + }, + "TR": { + "name": None, + "options": None, + "report_type": "Standard", + "default_setup": 17, + "default_adaptive": None, + "intrinsics": ["Time"], }, - "TR": {"name": None, "options": None, "report_type": "Standard", "default_setup": 17, "default_adaptive": None}, }, "2D Extractor": { "Open": { @@ -353,6 +381,7 @@ "report_type": "Modal Solution Data", "default_setup": 4, "default_adaptive": "Sweep", + "intrinsics": ["Freq", "Phase"], }, }, "Icepak": { @@ -439,6 +468,7 @@ "report_type": "Standard", "default_setup": 41, "default_adaptive": None, + "intrinsics": ["Freq"], }, "LNA3DLayout": { "name": None, @@ -446,6 +476,7 @@ "report_type": "Standard", "default_setup": 42, "default_adaptive": None, + "intrinsics": ["Freq"], }, }, "Mechanical": { diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 65a79301e7d..b33e7417acd 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -833,6 +833,8 @@ def _init_dotnet( ): # pragma: no cover import pythoncom + pythoncom.CoInitialize() + if is_linux: raise Exception( "PyAEDT supports COM initialization in Windows only. To use in Linux, upgrade to AEDT 2022 R2 or later." diff --git a/pyaedt/edb.py b/pyaedt/edb.py index 7dc1d8b4e67..c33c6f86bee 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -1412,6 +1412,7 @@ def _create_extent( smart_cut=False, reference_list=[], include_pingroups=True, + pins_to_preserve=None, ): if extent_type in ["Conforming", self.edb_api.geometry.extent_type.Conforming, 1]: if use_pyaedt_extent: @@ -1423,7 +1424,7 @@ def _create_extent( expansion_size, smart_cut, reference_list, - include_pingroups, + pins_to_preserve, ) else: _poly = self.layout.expanded_extent( @@ -1448,7 +1449,7 @@ def _create_extent( expansion_size, smart_cut, reference_list, - include_pingroups, + pins_to_preserve, ) else: _poly = self.layout.expanded_extent( @@ -1473,65 +1474,73 @@ def _create_conformal( round_extension, smart_cutout=False, reference_list=[], - include_pingroups=True, + pins_to_preserve=None, ): names = [] _polys = [] for net in net_signals: names.append(net.GetName()) + if pins_to_preserve: + insts = self.padstacks.instances + for i in pins_to_preserve: + p = insts[i].position + pos_1 = [i - expansion_size for i in p] + pos_2 = [i + expansion_size for i in p] + plane = self.modeler.Shape("rectangle", pointA=pos_1, pointB=pos_2) + rectangle_data = self.modeler.shape_to_polygon_data(plane) + _polys.append(rectangle_data) + for prim in self.modeler.primitives: if prim is not None and prim.net_name in names: - obj_data = prim.primitive_object.GetPolygonData().Expand( - expansion_size, tolerance, round_corner, round_extension - ) - if obj_data: - _polys.extend(list(obj_data)) + _polys.append(prim.primitive_object.GetPolygonData()) if smart_cutout: - _polys.extend(self._smart_cut(net_signals, reference_list, include_pingroups)) - _poly_unite = self.edb_api.geometry.polygon_data.unite(_polys) + objs_data = self._smart_cut(reference_list, expansion_size) + _polys.extend(objs_data) + k = 0 + delta = expansion_size / 5 + while k < 10: + unite_polys = [] + for i in _polys: + obj_data = i.Expand(expansion_size, tolerance, round_corner, round_extension) + if obj_data: + unite_polys.extend(list(obj_data)) + _poly_unite = self.edb_api.geometry.polygon_data.unite(unite_polys) + if len(_poly_unite) == 1: + self.logger.info("Correctly computed Extension at first iteration.") + return _poly_unite[0] + k += 1 + expansion_size += delta if len(_poly_unite) == 1: + self.logger.info("Correctly computed Extension in {} iterations.".format(k)) return _poly_unite[0] else: + self.logger.info("Failed to Correctly computed Extension.") areas = [i.Area() for i in _poly_unite] return _poly_unite[areas.index(max(areas))] @pyaedt_function_handler() - def _smart_cut(self, net_signals, reference_list=[], include_pingroups=True): + def _smart_cut(self, reference_list=[], expansion_size=1e-12): + from pyaedt.generic.clr_module import Tuple + _polys = [] terms = [term for term in self.layout.terminals if int(term.GetBoundaryType()) in [0, 3, 4, 7, 8]] locations = [] for term in terms: - if term.GetTerminalType().ToString() == "PadstackInstanceTerminal": - if term.GetParameters()[1].GetNet().GetName() in reference_list: - locations.append(self.padstacks.instances[term.GetParameters()[1].GetId()].position) - elif term.GetTerminalType().ToString() == "PointTerminal" and term.GetNet().GetName() in reference_list: + if term.GetTerminalType().ToString() == "PointTerminal" and term.GetNet().GetName() in reference_list: pd = term.GetParameters()[1] locations.append([pd.X.ToDouble(), pd.Y.ToDouble()]) - if include_pingroups: - for reference in reference_list: - for pin in self.nets.nets[reference].padstack_instances: - if pin.pingroups: - locations.append(pin.position) for point in locations: pointA = self.edb_api.geometry.point_data( - self.edb_value(point[0] - 1e-12), self.edb_value(point[1] - 1e-12) + self.edb_value(point[0] - expansion_size), self.edb_value(point[1] - expansion_size) ) pointB = self.edb_api.geometry.point_data( - self.edb_value(point[0] + 1e-12), self.edb_value(point[1] + 1e-12) + self.edb_value(point[0] + expansion_size), self.edb_value(point[1] + expansion_size) ) points = Tuple[self.edb_api.geometry.geometry.PointData, self.edb_api.geometry.geometry.PointData]( pointA, pointB ) _polys.append(self.edb_api.geometry.polygon_data.create_from_bbox(points)) - for cname, c in self.components.instances.items(): - if ( - set(net_signals).intersection(c.nets) - and c.is_enabled - and c.model_type in ["SParameterModel", "SpiceModel", "NetlistModel"] - ): - for pin in c.pins: - locations.append(pin.position) return _polys @pyaedt_function_handler() @@ -1544,17 +1553,28 @@ def _create_convex_hull( round_extension, smart_cut=False, reference_list=[], - include_pingroups=True, + pins_to_preserve=None, ): names = [] _polys = [] for net in net_signals: names.append(net.GetName()) + if pins_to_preserve: + insts = self.padstacks.instances + for i in pins_to_preserve: + p = insts[i].position + pos_1 = [i - 1e-12 for i in p] + pos_2 = [i + 1e-12 for i in p] + plane = self.modeler.Shape("rectangle", pointA=pos_1, pointB=pos_2) + rectangle_data = self.modeler.shape_to_polygon_data(plane) + _polys.append(rectangle_data) for prim in self.modeler.primitives: if prim is not None and prim.net_name in names: _polys.append(prim.primitive_object.GetPolygonData()) if smart_cut: - _polys.extend(self._smart_cut(net_signals, reference_list, include_pingroups)) + objs_data = self._smart_cut(reference_list, expansion_size) + _polys.extend(objs_data) + _poly = self.edb_api.geometry.polygon_data.get_convex_hull_of_polygons(convert_py_list_to_net_list(_polys)) _poly = _poly.Expand(expansion_size, tolerance, round_corner, round_extension)[0] return _poly @@ -2027,6 +2047,17 @@ def _create_cutout_multithread( ): pins_to_preserve.extend([i.id for i in el.pins.values()]) nets_to_preserve.extend(el.nets) + if include_pingroups: + for reference in reference_list: + for pin in self.nets.nets[reference].padstack_instances: + if pin.pingroups: + pins_to_preserve.append(pin.id) + if check_terminals: + terms = [term for term in self.layout.terminals if int(term.GetBoundaryType()) in [0, 3, 4, 7, 8]] + for term in terms: + if term.GetTerminalType().ToString() == "PadstackInstanceTerminal": + if term.GetParameters()[1].GetNet().GetName() in reference_list: + pins_to_preserve.append(term.GetParameters()[1].GetId()) for i in self.nets.nets.values(): name = i.name @@ -2077,6 +2108,7 @@ def _create_cutout_multithread( smart_cut=check_terminals, reference_list=reference_list, include_pingroups=include_pingroups, + pins_to_preserve=pins_to_preserve, ) if extent_type in ["Conforming", self.edb_api.geometry.extent_type.Conforming, 1] and extent_defeature > 0: _poly = _poly.Defeature(extent_defeature) @@ -3066,10 +3098,9 @@ def build_simulation_project(self, simulation_setup): idx = simulation_setup.signal_layer_etching_instances.index(layer) if len(simulation_setup.etching_factor_instances) > idx: self.stackup[layer].etch_factor = float(simulation_setup.etching_factor_instances[idx]) - if not simulation_setup.signal_nets and simulation_setup.components: nets_to_include = [] - pnets = list(self.nets.power_nets.keys())[:] + pnets = list(self.nets.power.keys())[:] for el in simulation_setup.components: nets_to_include.append([i for i in self.components[el].nets if i not in pnets]) simulation_setup.signal_nets = [ @@ -3132,13 +3163,33 @@ def build_simulation_project(self, simulation_setup): if not simulation_setup.generate_solder_balls: source_type = SourceType.CircPort for cmp in simulation_setup.components: - self.components.create_port_on_component( - cmp, - net_list=simulation_setup.signal_nets, - do_pingroup=False, - reference_net=simulation_setup.power_nets, - port_type=source_type, - ) + if isinstance(cmp, str): # keep legacy component + self.components.create_port_on_component( + cmp, + net_list=simulation_setup.signal_nets, + do_pingroup=False, + reference_net=simulation_setup.power_nets, + port_type=source_type, + ) + elif isinstance(cmp, dict): + if "refdes" in cmp: + if not "solder_balls_height" in cmp: # pragma no cover + cmp["solder_balls_height"] = None + if not "solder_balls_size" in cmp: # pragma no cover + cmp["solder_balls_size"] = None + cmp["solder_balls_mid_size"] = None + if not "solder_balls_mid_size" in cmp: # pragma no cover + cmp["solder_balls_mid_size"] = None + self.components.create_port_on_component( + cmp["refdes"], + net_list=simulation_setup.signal_nets, + do_pingroup=False, + reference_net=simulation_setup.power_nets, + port_type=source_type, + solder_balls_height=cmp["solder_balls_height"], + solder_balls_size=cmp["solder_balls_size"], + solder_balls_mid_size=cmp["solder_balls_mid_size"], + ) if simulation_setup.generate_solder_balls and not self.hfss.set_coax_port_attributes( simulation_setup ): # pragma: no cover @@ -3160,17 +3211,26 @@ def build_simulation_project(self, simulation_setup): if simulation_setup.solver_type == SolverType.SiwaveSYZ: if simulation_setup.generate_excitations: for cmp in simulation_setup.components: - self.components.create_port_on_component( - cmp, - net_list=simulation_setup.signal_nets, - do_pingroup=simulation_setup.do_pingroup, - reference_net=simulation_setup.power_nets, - port_type=SourceType.CircPort, - ) + if isinstance(cmp, str): # keep legacy + self.components.create_port_on_component( + cmp, + net_list=simulation_setup.signal_nets, + do_pingroup=simulation_setup.do_pingroup, + reference_net=simulation_setup.power_nets, + port_type=SourceType.CircPort, + ) + elif isinstance(cmp, dict): + if "refdes" in cmp: # pragma no cover + self.components.create_port_on_component( + cmp["refdes"], + net_list=simulation_setup.signal_nets, + do_pingroup=simulation_setup.do_pingroup, + reference_net=simulation_setup.power_nets, + port_type=SourceType.CircPort, + ) self.logger.info("Configuring analysis setup.") if not self.siwave.configure_siw_analysis_setup(simulation_setup): # pragma: no cover self.logger.error("Failed to configure Siwave simulation setup.") - if simulation_setup.solver_type == SolverType.SiwaveDC: if simulation_setup.generate_excitations: self.components.create_source_on_component(simulation_setup.sources) diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index 3d492a36528..92fb5db470e 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -805,7 +805,16 @@ def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0, port @pyaedt_function_handler() def create_port_on_component( - self, component, net_list, port_type=SourceType.CoaxPort, do_pingroup=True, reference_net="gnd", port_name=None + self, + component, + net_list, + port_type=SourceType.CoaxPort, + do_pingroup=True, + reference_net="gnd", + port_name=None, + solder_balls_height=None, + solder_balls_size=None, + solder_balls_mid_size=None, ): """Create ports on a component. @@ -830,7 +839,14 @@ def create_port_on_component( If a port with the specified name already exists, the default naming convention is used so that port creation does not fail. - + solder_balls_height : float, optional + Solder balls height used for the component. When provided default value is overwritten and must be + provided in meter. + solder_balls_size : float, optional + Solder balls diameter. When provided auto evaluation based on padstack size will be disabled. + solder_balls_mid_size : float, optional + Solder balls mid diameter. When provided if value is different than solder balls size, spheroid shape will + be switched. Returns ------- double, bool @@ -875,13 +891,36 @@ def create_port_on_component( if port_type == SourceType.CoaxPort: pad_params = self._padstack.get_pad_parameters(pin=cmp_pins[0], layername=pin_layers[0], pad_type=0) if not pad_params[0] == 7: - sball_diam = min([self._pedb.edb_value(val).ToDouble() for val in pad_params[1]]) - solder_ball_height = 2 * sball_diam / 3 - else: - bbox = pad_params[1] - sball_diam = min([abs(bbox[2] - bbox[0]), abs(bbox[3] - bbox[1])]) * 0.8 - solder_ball_height = 2 * sball_diam / 3 - self.set_solder_ball(component, solder_ball_height, sball_diam) + if not solder_balls_size: # pragma no cover + sball_diam = min([self._pedb.edb_value(val).ToDouble() for val in pad_params[1]]) + sball_mid_diam = sball_diam + else: # pragma no cover + sball_diam = solder_balls_size + if solder_balls_mid_size: + sball_mid_diam = solder_balls_mid_size + else: + sball_mid_diam = solder_balls_size + if not solder_balls_height: # pragma no cover + solder_balls_height = 2 * sball_diam / 3 + else: # pragma no cover + if not solder_balls_size: + bbox = pad_params[1] + sball_diam = min([abs(bbox[2] - bbox[0]), abs(bbox[3] - bbox[1])]) * 0.8 + else: + if not solder_balls_mid_size: + sball_mid_diam = solder_balls_size + if not solder_balls_height: + solder_balls_height = 2 * sball_diam / 3 + sball_shape = "Cylinder" + if not sball_diam == sball_mid_diam: + sball_shape = "Spheroid" + self.set_solder_ball( + component=component, + sball_height=solder_balls_height, + sball_diam=sball_diam, + sball_mid_diam=sball_mid_diam, + shape=sball_shape, + ) for pin in cmp_pins: self._padstack.create_coax_port(padstackinstance=pin, name=port_name) @@ -1661,6 +1700,7 @@ def create_pingroup_from_pins(self, pins, group_name=None): def delete_single_pin_rlc(self, deactivate_only=False): # type: (bool) -> list """Delete all RLC components with a single pin. + Single pin component model type will be reverted to ``"RLC"``. Parameters ---------- @@ -1688,6 +1728,7 @@ def delete_single_pin_rlc(self, deactivate_only=False): if val.numpins < 2 and val.type in ["Resistor", "Capacitor", "Inductor"]: if deactivate_only: val.is_enabled = False + val.model_type = "RLC" else: val.edbcomponent.Delete() deleted_comps.append(comp) diff --git a/pyaedt/edb_core/edb_data/obj_base.py b/pyaedt/edb_core/edb_data/obj_base.py index 45945326703..9f3ac6cb6d6 100644 --- a/pyaedt/edb_core/edb_data/obj_base.py +++ b/pyaedt/edb_core/edb_data/obj_base.py @@ -13,4 +13,7 @@ def is_null(self): @property def type(self): """Get type.""" - return self._edb_object.GetType() + try: + return self._edb_object.GetType() + except AttributeError: # pragma: no cover + return None diff --git a/pyaedt/edb_core/edb_data/padstacks_data.py b/pyaedt/edb_core/edb_data/padstacks_data.py index 4abd7768f42..9bc4897a57c 100644 --- a/pyaedt/edb_core/edb_data/padstacks_data.py +++ b/pyaedt/edb_core/edb_data/padstacks_data.py @@ -1669,7 +1669,10 @@ def lower_elevation(self): float Lower elavation of the placement layer. """ - return self._edb_padstackinstance.GetGroup().GetPlacementLayer().Clone().GetLowerElevation() + try: + return self._edb_padstackinstance.GetGroup().GetPlacementLayer().Clone().GetLowerElevation() + except AttributeError: # pragma: no cover + return None @property def upper_elevation(self): @@ -1680,7 +1683,10 @@ def upper_elevation(self): float Upper elevation of the placement layer. """ - return self._edb_padstackinstance.GetGroup().GetPlacementLayer().Clone().GetUpperElevation() + try: + return self._edb_padstackinstance.GetGroup().GetPlacementLayer().Clone().GetUpperElevation() + except AttributeError: # pragma: no cover + return None @property def top_bottom_association(self): diff --git a/pyaedt/edb_core/edb_data/primitives_data.py b/pyaedt/edb_core/edb_data/primitives_data.py index 8896d96cb99..259f8f31b33 100644 --- a/pyaedt/edb_core/edb_data/primitives_data.py +++ b/pyaedt/edb_core/edb_data/primitives_data.py @@ -1,5 +1,6 @@ import math +from pyaedt.edb_core.dotnet.database import NetDotNet from pyaedt.edb_core.dotnet.primitive import BondwireDotNet from pyaedt.edb_core.dotnet.primitive import CircleDotNet from pyaedt.edb_core.dotnet.primitive import PathDotNet @@ -82,7 +83,10 @@ def type(self): ------- str """ - return self._edb_object.GetPrimitiveType().ToString() + try: + return self._edb_object.GetPrimitiveType().ToString() + except AttributeError: # pragma: no cover + return "" @property def net_name(self): @@ -101,14 +105,20 @@ def net_name(self, name): self.primitive_object.SetNet(net) else: try: - self.net = name - except: + if isinstance(name, str): + self.net = name + elif isinstance(name, NetDotNet): + self.net = name.name + except: # pragma: no cover self._app.logger.error("Failed to set net name.") @property def layer(self): """Get the primitive edb layer object.""" - return self.primitive_object.GetLayer() + try: + return self.primitive_object.GetLayer() + except AttributeError: # pragma: no cover + return None @property def layer_name(self): @@ -118,7 +128,10 @@ def layer_name(self): ------- str """ - return self.layer.GetName() + try: + return self.layer.GetName() + except AttributeError: # pragma: no cover + return None @layer_name.setter def layer_name(self, val): @@ -144,7 +157,10 @@ def is_void(self): ------- bool """ - return self._edb_object.IsVoid() + try: + return self._edb_object.IsVoid() + except AttributeError: # pragma: no cover + return None def get_connected_objects(self): """Get connected objects. diff --git a/pyaedt/edb_core/hfss.py b/pyaedt/edb_core/hfss.py index d202d05db19..81cbf6242b2 100644 --- a/pyaedt/edb_core/hfss.py +++ b/pyaedt/edb_core/hfss.py @@ -1427,9 +1427,20 @@ def set_coax_port_attributes(self, simulation_setup=None): ) return False net_names = [net.name for net in self._layout.nets if not net.IsPowerGround()] - cmp_names = ( - simulation_setup.components if simulation_setup.components else [gg.GetName() for gg in self._layout.groups] - ) + if simulation_setup.components and isinstance(simulation_setup.components[0], str): + cmp_names = ( + simulation_setup.components + if simulation_setup.components + else [gg.GetName() for gg in self._layout.groups] + ) + elif ( + simulation_setup.components + and isinstance(simulation_setup.components[0], dict) + and "refdes" in simulation_setup.components[0] + ): + cmp_names = [cmp["refdes"] for cmp in simulation_setup.components] + else: + cmp_names = [] ii = 0 for cc in cmp_names: cmp = self._pedb.edb_api.cell.hierarchy.component.FindByName(self._active_layout, cc) diff --git a/pyaedt/edb_core/layout_validation.py b/pyaedt/edb_core/layout_validation.py index 1f5446ab8d8..ccbaa8f0033 100644 --- a/pyaedt/edb_core/layout_validation.py +++ b/pyaedt/edb_core/layout_validation.py @@ -55,7 +55,10 @@ def dc_shorts(self, net_list=None, fix=False): else: _padstacks_list[n_name] = [pad] dc_shorts = [] + all_shorted_nets = [] for net in net_list: + if net in all_shorted_nets: + continue objs = [] for i in _objects_list.get(net, []): objs.append(i) @@ -68,11 +71,13 @@ def dc_shorts(self, net_list=None, fix=False): connected_objs = objs[0].get_connected_objects() connected_objs.append(objs[0]) net_dc_shorts = [obj for obj in connected_objs] + all_shorted_nets.append(net) if net_dc_shorts: dc_nets = list(set([obj.net.name for obj in net_dc_shorts if not obj.net.name == net])) for dc in dc_nets: if dc: dc_shorts.append([net, dc]) + all_shorted_nets.append(dc) if fix: temp = [] for i in net_dc_shorts: @@ -95,7 +100,12 @@ def dc_shorts(self, net_list=None, fix=False): @pyaedt_function_handler() def disjoint_nets( - self, net_list=None, keep_only_main_net=False, clean_disjoints_less_than=0.0, order_by_area=False + self, + net_list=None, + keep_only_main_net=False, + clean_disjoints_less_than=0.0, + order_by_area=False, + keep_disjoint_pins=False, ): """Find and fix disjoint nets from a given netlist. @@ -110,6 +120,8 @@ def disjoint_nets( order_by_area : bool, optional Whether if the naming order has to be by number of objects (fastest) or area (slowest but more accurate). Default is ``False``. + keep_disjoint_pins : bool, optional + Whether if delete disjoints pins not connected to any other primitive or not. Default is ``False``. Returns ------- List @@ -190,14 +202,24 @@ def area_calc(elem): except KeyError: pass elif len(disjoints) == 1 and ( - isinstance(obj_dict[disjoints[0]], EDBPadstackInstance) - or clean_disjoints_less_than + clean_disjoints_less_than + and "area" in dir(obj_dict[disjoints[0]]) and obj_dict[disjoints[0]].area() < clean_disjoints_less_than ): try: obj_dict[disjoints[0]].delete() except KeyError: pass + elif ( + len(disjoints) == 1 + and not keep_disjoint_pins + and isinstance(obj_dict[disjoints[0]], EDBPadstackInstance) + ): + try: + obj_dict[disjoints[0]].delete() + except KeyError: + pass + else: new_net_name = generate_unique_name(net, n=6) net_obj = self._pedb.nets.find_or_create_net(new_net_name) diff --git a/pyaedt/generic/configurations.py b/pyaedt/generic/configurations.py index 91403b5f4c3..1b0163bade4 100644 --- a/pyaedt/generic/configurations.py +++ b/pyaedt/generic/configurations.py @@ -1143,7 +1143,7 @@ def _export_general(self, dict_out): dict_out["general"]["date"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") dict_out["general"]["object_mapping"] = {} dict_out["general"]["output_variables"] = {} - if self._app.output_variables: + if list(self._app.output_variables): oo_out = os.path.join(tempfile.gettempdir(), generate_unique_name("oo") + ".txt") self._app.ooutput_variable.ExportOutputVariables(oo_out) with open(oo_out, "r") as f: diff --git a/pyaedt/generic/touchstone_parser.py b/pyaedt/generic/touchstone_parser.py index 75a506b89fc..b373f6997e1 100644 --- a/pyaedt/generic/touchstone_parser.py +++ b/pyaedt/generic/touchstone_parser.py @@ -1,4 +1,4 @@ -from copy import deepcopy as copy +from copy import copy import itertools import os import re diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index 53d96d99303..9029d85043d 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -5947,7 +5947,7 @@ def set_phase_center_per_port(self, coordinate_system=None): """ if not self.desktop_class.is_grpc_api: # pragma: no cover - self.hfss.logger.warning("Set phase center is not supported by AEDT COM API. Set phase center manually") + self.logger.warning("Set phase center is not supported by AEDT COM API. Set phase center manually") return False port_names = [] diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index c20bee56ea5..ac637ba8c63 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -34,6 +34,7 @@ from pyaedt.modules.Boundary import BoundaryObject from pyaedt.modules.Boundary import NativeComponentObject from pyaedt.modules.Boundary import NetworkObject +from pyaedt.modules.Boundary import _create_boundary from pyaedt.modules.monitor_icepak import Monitor @@ -630,14 +631,7 @@ def create_conduting_plate( ) props["Shell Conduction"] = shell_conduction bound = BoundaryObject(self, bc_name, props, "Conducting Plate") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler() def create_source_power( @@ -727,14 +721,7 @@ def create_source_power( props["Temperature"] = temperature props["Radiation"] = OrderedDict({"Radiate": radiate}) bound = BoundaryObject(self, source_name, props, "SourceIcepak") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler() def create_network_block( @@ -3486,14 +3473,7 @@ def assign_stationary_wall( props["External Radiation Reference Temperature"] = ext_surf_rad_ref_temp props["External Radiation View Factor"] = ext_surf_rad_view_factor bound = BoundaryObject(self, name, props, "Stationary Wall") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler() def assign_stationary_wall_with_heat_flux( @@ -4200,14 +4180,7 @@ def assign_solid_block( boundary_name = generate_unique_name("Block") bound = BoundaryObject(self, boundary_name, props, "Block") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler def assign_hollow_block( @@ -4335,14 +4308,7 @@ def assign_hollow_block( boundary_name = generate_unique_name("Block") bound = BoundaryObject(self, boundary_name, props, "Block") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler() def get_fans_operating_point(self, export_file=None, setup_name=None, timestep=None, design_variation=None): @@ -4827,11 +4793,7 @@ def assign_mass_flow_free_opening( ) @pyaedt_function_handler() - def assign_symmetry_wall( - self, - geometry, - boundary_name=None, - ): + def assign_symmetry_wall(self, geometry, boundary_name=None): """Assign symmetry wall boundary condition. Parameters @@ -4864,14 +4826,7 @@ def assign_symmetry_wall( props["Objects"] = geometry bound = BoundaryObject(self, boundary_name, props, "Symmetry Wall") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): - return None + return _create_boundary(bound) @pyaedt_function_handler() def assign_adiabatic_plate(self, assignment, high_radiation_dict=None, low_radiation_dict=None, boundary_name=None): @@ -5044,11 +4999,11 @@ def assign_recirculation_opening(self, face_list, extract_face, thermal_specific if not len(face_list) == 2: self.logger.error("Recirculation boundary condition must be assigned to two faces.") return False - if conductance_external_temperature is not None and thermal_specification is not "Conductance": + if conductance_external_temperature is not None and thermal_specification != "Conductance": self.logger.warning( '``conductance_external_temperature`` does not have any effect unless the ``thermal_specification`` ' 'is ``"Conductance"``.') - if conductance_external_temperature is not None and thermal_specification is not "Conductance": + if conductance_external_temperature is not None and thermal_specification != "Conductance": self.logger.warning( '``conductance_external_temperature`` must be specified when ``thermal_specification`` ' 'is ``"Conductance"``. Setting ``conductance_external_temperature`` to ``"AmbientTemp"``.') @@ -5122,14 +5077,7 @@ def assign_recirculation_opening(self, face_list, extract_face, thermal_specific boundary_name = generate_unique_name("Recirculating") bound = BoundaryObject(self, boundary_name, props, "Recirculating") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma: no cover - raise SystemExit - except (GrpcApiError, SystemExit): # pragma : no cover - return None + return _create_boundary(bound) @pyaedt_function_handler() def assign_blower_type1(self, faces, inlet_face, fan_curve_pressure, fan_curve_flow, blower_power="0W", blade_rpm=0, @@ -5281,11 +5229,4 @@ def _assign_blower(self, props, faces, inlet_face, fan_curve_flow_unit, fan_curv if not boundary_name: boundary_name = generate_unique_name("Blower") bound = BoundaryObject(self, boundary_name, props, "Blower") - try: - if bound.create(): - self._boundaries[bound.name] = bound - return bound - else: # pragma : no cover - raise SystemExit - except (GrpcApiError, SystemExit): # pragma: no cover - return None + return _create_boundary(bound) diff --git a/pyaedt/modeler/circuits/PrimitivesEmit.py b/pyaedt/modeler/circuits/PrimitivesEmit.py index a9843eb5426..98b1c68c137 100644 --- a/pyaedt/modeler/circuits/PrimitivesEmit.py +++ b/pyaedt/modeler/circuits/PrimitivesEmit.py @@ -744,6 +744,144 @@ def band_stop_frequency(self, band_node, units=""): units = self.units["Frequency"] return consts.unit_converter(float(band_node.props["StopFrequency"]), "Freq", "Hz", units) + def set_band_start_frequency(self, band_node, band_start_freq, units=""): + """Set start frequency of the band. + + Parameters + ---------- + band_node : pyaedt.modeler.circuits.PrimitivesEmit.EmitComponentPropNode object + Instance of the band node + band_start_freq : float + Start frequency of the band. + units : str, optional + Units of the start frequency. If no units are specified or the units + specified are invalid, then `"Hz"`` is assumed. + + Returns + ------ + None + + Examples + -------- + >>> from pyaedt import Emit + >>> aedtapp = Emit(new_desktop_session=False) + >>> radio = aedtapp.modeler.components.create_component("New Radio") + >>> band = radio.bands()[0] + >>> start_freq = 10 + >>> units = 'MHz' + >>> radio.set_band_start_frequency(band, start_freq, units=units) + """ + + # if "Band" not in band_node.props["Type"]: + # raise TypeError("{} must be a band.".format(band_node.node_name)) + + if not units or units not in emit_consts.EMIT_VALID_UNITS["Frequency"]: + units = "Hz" + + # convert to Hz + freq_float_in_Hz = consts.unit_converter(band_start_freq, "Freq", units, "Hz") + freq_string = "{}".format(freq_float_in_Hz) + if not (1 <= freq_float_in_Hz <= 100000000000): # pragma: no cover + raise ValueError("Frequency should be within 1Hz to 100 GHz.") + if float(band_node.props["StopFrequency"]) < freq_float_in_Hz: # pragma: no cover + stop_freq = freq_float_in_Hz + 1 + band_node._set_prop_value({"StopFrequency": str(stop_freq)}) + else: + prop_list = {"StartFrequency": freq_string} + band_node._set_prop_value(prop_list) + + def set_band_stop_frequency(self, band_node, band_stop_freq, units=""): + """Set stop frequency of the band. + + Parameters + ---------- + band_node : pyaedt.modeler.circuits.PrimitivesEmit.EmitComponentPropNode object + Instance of the band node + band_stop_freq : float + Stop frequency of the band. + units : str, optional + Units of the stop frequency. If no units are specified or the units + specified are invalid, then `"Hz"`` is assumed. + + Returns + ------ + None + + Examples + -------- + >>> from pyaedt import Emit + >>> aedtapp = Emit(new_desktop_session=False) + >>> radio = aedtapp.modeler.components.create_component("New Radio") + >>> band = radio.bands()[0] + >>> stop_freq = 10 + >>> units = 'MHz' + >>> radio.set_band_stop_frequency(band, stop_freq, units=units) + """ + # if "Band" not in band_node.props["Type"]: + # raise TypeError("{} must be a band.".format(band_node.node_name)) + if not units or units not in emit_consts.EMIT_VALID_UNITS["Frequency"]: + units = "Hz" + # convert to Hz + freq_float_in_Hz = consts.unit_converter(band_stop_freq, "Freq", units, "Hz") + if not (1 <= freq_float_in_Hz <= 100000000000): # pragma: no cover + raise ValueError("Frequency should be within 1Hz to 100 GHz.") + if float(band_node.props["StartFrequency"]) > freq_float_in_Hz: # pragma: no cover + if freq_float_in_Hz > 1: + band_node._set_prop_value({"StartFrequency": str(freq_float_in_Hz - 1)}) + else: # pragma: no cover + raise ValueError("Band stop frequency is less than start frequency.") + freq_string = "{}".format(freq_float_in_Hz) + prop_list = {"StopFrequency": freq_string} + band_node._set_prop_value(prop_list) + + # def duplicate_band(self, band_node_to_duplicate): + # """ + # [Incomplete 10/19/2023] + # Parameters + # ---------- + # band_node_to_duplicate + # + # Returns + # ------- + # + # """ + # # append number to the name of the band to duplicate. + # print('Duplicating...') + # + # + # # return band node + # def convert_channels_to_multi_bands(self, band_node): + # """ + # [Incomplete 10/19/2023] + # Parameters + # ---------- + # band_node + # + # Returns + # ------- + # + # """ + # # get the channels. Say returns 10 channels in the band_node + # # Name = r.bands()[0].children[0].props['Name'] + # # band_node.props['Name'] + # # Start = r.bands()[0].props['StartFrequency'] + # band_start_frequency = float(band_node.props['StartFrequency']) + # # Stop = r.bands()[0].props['StopFrequency'] + # band_stop_frequency = float(band_node.props['StopFrequency']) + # # Spacing = r.bands()[0].props['ChannelSpacing'] + # channel_spacing = float(band_node.props['ChannelSpacing']) + # # for each channel + # # 1) create a band (duplicate original one) + # # 2) set band start and stop frequencies + # for channel in list(range(int(band_start_frequency), int(band_stop_frequency), int(channel_spacing))): + # baby_band_start = channel + # baby_band_stop = channel+channel_spacing + # baby_band_node = self.duplicate_band(band_node) # return band name or some handle to it + # self.set_band_start_frequency(baby_band_node, baby_band_start) + # self.set_band_stop_frequency(baby_band_node, baby_band_stop) + # # set start and stop freq for that band name + # # to be + def band_channel_bandwidth(self, band_node, units=""): """Get the channel bandwidth of the band node. diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index 7ebb7fccb7a..b2f2fdb9b32 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -889,130 +889,112 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import lines = f.read().splitlines() id = 0 for line in lines: - line_split = [line[i : i + 8] for i in range(0, len(line), 8)] - if len(line_split) < 5: + line_type = line[:8].strip() + if line.startswith("$") or line.startswith("*"): continue - if line_split[0].startswith("GRID"): - try: - import re - - out = re.findall("^.{24}(.{8})(.{8})(.{8})", line)[0] - n1 = out[0].replace(".-", ".e-").strip() - n2 = out[1].replace(".-", ".e-").strip() - n3 = out[2].replace(".-", ".e-").strip() - - if "-" in n1[1:]: - n1 = n1[0] + n1[1:].replace("-", "e-") - n1 = float(n1) - if "-" in n2[1:]: - n2 = n2[0] + n2[1:].replace("-", "e-") - n2 = float(n2) - if "-" in n3[1:]: - n3 = n3[0] + n3[1:].replace("-", "e-") - n3 = float(n3) - - nas_to_dict["Points"][int(line_split[1])] = [n1, n2, n3] - nas_to_dict["PointsId"][int(line_split[1])] = id + elif line_type in ["GRID", "CTRIA3"]: + grid_id = int(line[8:16]) + if line_type == "CTRIA3": + tria_id = int(line[16:24]) + n1 = line[24:32].strip() + if "-" in n1[1:]: + n1 = n1[0] + n1[1:].replace("-", "e-") + n2 = line[32:40].strip() + if "-" in n2[1:]: + n2 = n2[0] + n2[1:].replace("-", "e-") + n3 = line[40:48].strip() + if "-" in n3[1:]: + n3 = n3[0] + n3[1:].replace("-", "e-") + if line_type == "GRID": + nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)] + nas_to_dict["PointsId"][grid_id] = grid_id id += 1 - except: - pass - elif line_split[0].startswith("CTRIA3"): - if int(line_split[2]) in nas_to_dict["Triangles"]: - nas_to_dict["Triangles"][int(line_split[2])].append( - [ - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - ] - ) - else: - nas_to_dict["Triangles"][int(line_split[2])] = [ - [ - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - ] - ] - elif line_split[0].startswith("CPENTA"): - if int(line_split[2]) in nas_to_dict["Solids"]: - nas_to_dict["Solids"][int(line_split[2])].append( - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), - int(line_split[7]), - int(line_split[8]), - ] - ) else: - nas_to_dict["Solids"][int(line_split[2])] = [ - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), - int(line_split[7]), - int(line_split[8]), - ] - ] - elif line_split[0].startswith("CHEXA"): - if int(line_split[2]) in nas_to_dict["Solids"]: - nas_to_dict["Solids"][int(line_split[2])].append( - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), - int(line_split[7]), - int(line_split[8]), - int(line_split[9]), - int(line_split[10]), + if tria_id in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"][tria_id].append( + [ + int(n1), + int(n2), + int(n3), + ] + ) + else: + nas_to_dict["Triangles"][tria_id] = [ + [ + int(n1), + int(n2), + int(n3), + ] ] - ) + elif line_type in ["GRID*", "CTRIA3*"]: + grid_id = int(line[8:24]) + if line_type == "CTRIA3*": + tria_id = int(line[24:40]) + n1 = line[40:56].strip() + if "-" in n1[1:]: + n1 = n1[0] + n1[1:].replace("-", "e-") + n2 = line[56:72].strip() + if "-" in n2[1:]: + n2 = n2[0] + n2[1:].replace("-", "e-") + + n3 = line[72:88].strip() + if not n3 or n3 == "*": + n3 = lines[lines.index(line) + 1][8:24].strip() + if "-" in n3[1:]: + n3 = n3[0] + n3[1:].replace("-", "e-") + if line_type == "GRID*": + nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)] + nas_to_dict["PointsId"][grid_id] = id + id += 1 else: - nas_to_dict["Solids"][int(line_split[2])] = [ - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), - int(line_split[7]), - int(line_split[8]), - int(line_split[9]), - int(line_split[10]), - ] - ] - elif line_split[0].startswith("CTETRA"): - if int(line_split[2]) in nas_to_dict["Solids"]: - nas_to_dict["Solids"][int(line_split[2])].append( - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), + if tria_id in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"][tria_id].append( + [ + int(n1), + int(n2), + int(n3), + ] + ) + else: + nas_to_dict["Triangles"][tria_id] = [ + [ + int(n1), + int(n2), + int(n3), + ] ] - ) + elif line_type in ["CPENTA", "CHEXA", "CTETRA"]: + obj_id = int(line[16:24]) + n1 = int(line[24:32]) + n2 = int(line[32:40]) + n3 = int(line[40:48]) + n4 = int(line[48:56]) + obj_list = [line_type, n1, n2, n3, n4] + if line_type == "CPENTA": + n5 = int(line[56:64]) + n6 = int(line[64:72]) + obj_list.extend([n5, n6]) + + if line_type == "CHEXA": + n5 = int(line[56:64]) + n6 = int(line[64:72]) + n7 = int(lines[lines.index(line) + 1][8:16].strip()) + n8 = int(lines[lines.index(line) + 1][16:24].strip()) + + obj_list.extend([n5, n6, n7, n8]) + if obj_id in nas_to_dict["Solids"]: + nas_to_dict["Solids"][obj_id].append(obj_list) else: - nas_to_dict["Solids"][int(line_split[2])] = [ - [ - line_split[0].strip(), - int(line_split[3]), - int(line_split[4]), - int(line_split[5]), - int(line_split[6]), - ] - ] - elif line_split[0].startswith("CROD") or line_split[0].startswith("CBEAM"): - if int(line_split[2]) in nas_to_dict["Lines"]: - nas_to_dict["Lines"][int(line_split[2])].append([int(line_split[3]), int(line_split[4])]) + nas_to_dict["Solids"][obj_id] = [[i for i in obj_list]] + elif line_type in ["CROD", "CBEAM"]: + obj_id = int(line[16:24]) + n1 = int(line[24:32]) + n2 = int(line[32:40]) + if obj_id in nas_to_dict["Lines"]: + nas_to_dict["Lines"][obj_id].append([n1, n2]) else: - nas_to_dict["Lines"][int(line_split[2])] = [[int(line_split[3]), int(line_split[4])]] + nas_to_dict["Lines"][obj_id] = [[n1, n2]] + self.logger.info_timer("File loaded") objs_before = [i for i in self.object_names] if nas_to_dict["Triangles"]: diff --git a/pyaedt/modules/Boundary.py b/pyaedt/modules/Boundary.py index 92ea792ce45..8061aaa131f 100644 --- a/pyaedt/modules/Boundary.py +++ b/pyaedt/modules/Boundary.py @@ -4506,3 +4506,14 @@ def _create_node_dict(self, default_dict): node_args[k] = val return node_args + + +def _create_boundary(bound): + try: + if bound.create(): + bound._app._boundaries[bound.name] = bound + return bound + else: # pragma : no cover + raise Exception + except Exception: # pragma: no cover + return None diff --git a/pyaedt/modules/Material.py b/pyaedt/modules/Material.py index a768baacb9a..14940bb3e18 100644 --- a/pyaedt/modules/Material.py +++ b/pyaedt/modules/Material.py @@ -16,8 +16,11 @@ import copy import warnings +from pyaedt.application.Variables import decompose_variable_value from pyaedt.generic.DataHandlers import _dict2arg from pyaedt.generic.constants import CSS4_COLORS +from pyaedt.generic.constants import unit_converter +from pyaedt.generic.general_methods import is_number from pyaedt.generic.general_methods import pyaedt_function_handler @@ -1157,7 +1160,7 @@ def _get_args(self, props=None): Parameters ---------- - prop : str, optional + props : str, optional Name of the property. The default is ``None``. """ if not props: @@ -1992,16 +1995,277 @@ def set_magnetic_coercivity(self, value=0, x=1, y=0, z=0): return self.update() @pyaedt_function_handler() - def set_electrical_steel_coreloss(self, kh=0, kc=0, ke=0, kdc=0, cut_depth=0.0001): - """Set Electrical Steel Type Core Loss. + def get_core_loss_coefficients( + self, + points_list_at_freq, + core_loss_model_type="Electrical Steel", + thickness="0.5mm", + conductivity=0, + coefficient_setup="w_per_cubic_meter", + ): + """Get electrical steel or power ferrite core loss coefficients at a given frequency. + + Parameters + ---------- + points_list_at_freq : dict + Dictionary where keys are the frequencies (in Hz) and values are lists of points (BP curve). + If the core loss model is calculated at one frequency, this parameter must be provided as a + dictionary with one key (single frequency in Hz) and values are lists of points at + that specific frequency (BP curve). + core_loss_model_type : str, optional + Core loss model type. The default value is ``"Electrical Steel"``. + Options are ``"Electrical Steel"`` and ``"Power Ferrite"``. + thickness : str, optional + Thickness provided as the value plus the unit. + The default is ``0.5mm``. + conductivity : float, optional + Material conductivity. + The default is ``0``. + coefficient_setup : str, optional + Core loss unit. The default is ``"w_per_cubic_meter"``. + Options are ``"kw_per_cubic_meter"``, ``"w_per_cubic_meter"``, ``"w_per_kg"``, + and ``"w_per_lb"``. + + + Returns + ------- + list + List of core loss coefficients. + Returns Kh, Kc, and Ke coefficients if the core loss model is ``"Electrical Steel"``. + Returns Cm, X, and Y if the core loss model is ``"Power Ferrite"``. + + Examples + -------- + This example shows how to get core loss coefficients for Electrical Steel core loss model. + + >>> from pyaedt import Maxwell3d + >>> m3d = Maxwell3d() + >>> box = m3d.modeler.create_box([-10, -10, 0], [20, 20, 20], "box_to_split") + >>> box.material = "magnesium" + >>> coefficients = m3d.materials["magnesium"].get_core_loss_coefficients( + ... points_list_at_freq={60 : [[0, 0], [1, 3], [2, 7]]}, + ... thickness="0.5mm", + ... conductivity=0) + >>> print(coefficients) + >>> m3d.release_desktop(True, True) + """ + if not isinstance(points_list_at_freq, dict): + raise TypeError("Points list at frequency must be provided as a dictionary.") + if not isinstance(thickness, str): + raise TypeError("Thickness must be provided as a string with value and unit.") + else: + value, unit = decompose_variable_value(thickness) + if not is_number(value) and not unit: + raise TypeError("Thickness must be provided as a string with value and unit.") + props = OrderedDict({}) + freq_keys = list(points_list_at_freq.keys()) + for i in range(0, len(freq_keys)): + if isinstance(freq_keys[i], str): + value, unit = decompose_variable_value(freq_keys[i]) + if unit != "Hz": + value = unit_converter(values=value, unit_system="Freq", input_units=unit, output_units="Hz") + points_list_at_freq[value] = points_list_at_freq[freq_keys[i]] + del points_list_at_freq[freq_keys[i]] + + if len(points_list_at_freq) == 1: + props["CoefficientSetupData"] = OrderedDict({}) + props["CoefficientSetupData"]["property_data"] = "coreloss_data" + props["CoefficientSetupData"]["coefficient_setup"] = coefficient_setup + frequency = list(points_list_at_freq.keys())[0] + props["CoefficientSetupData"]["Frequency"] = "{}Hz".format(frequency) + props["CoefficientSetupData"]["Thickness"] = thickness + props["CoefficientSetupData"]["Conductivity"] = str(conductivity) + points = [i for p in points_list_at_freq[frequency] for i in p] + props["CoefficientSetupData"]["Coordinates"] = OrderedDict({"DimUnits": ["", ""], "Points": points}) + elif len(points_list_at_freq) > 1: + props["CoreLossMultiCurveData"] = OrderedDict({}) + props["CoreLossMultiCurveData"]["property_data"] = "coreloss_multi_curve_data" + props["CoreLossMultiCurveData"]["coreloss_unit"] = "w_per_cubic_meter" + + props["CoreLossMultiCurveData"]["AllCurves"] = OrderedDict({}) + props["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"] = [] + for freq in points_list_at_freq.keys(): + points = [i for p in points_list_at_freq[freq] for i in p] + one_curve = OrderedDict( + { + "Frequency": "{}Hz".format(freq), + "Coordinates": OrderedDict({"DimUnits": ["", ""], "Points": points}), + } + ) + props["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"].append(one_curve) + + props = self._get_args(props) + props.pop(0) + props[0][-1][2] = "NAME:Points" + points = props[0][-1].pop(2) + props[0][-1][2].insert(0, points) + coefficients = self.odefinition_manager.ComputeCoreLossCoefficients( + core_loss_model_type, self.mass_density.evaluated_value, props[0] + ) + return list(coefficients) + + @pyaedt_function_handler() + def set_coreloss_at_frequency( + self, + points_list_at_freq, + kdc=0, + cut_depth="1mm", + thickness="0.5mm", + conductivity=0, + coefficient_setup="w_per_cubic_meter", + core_loss_model_type="Electrical Steel", + ): + """Set electrical steel or power ferrite core loss model at one single frequency or at multiple frequencies. Parameters ---------- - kh : float - kc : float - ke : float + points_list_at_freq : dict + Dictionary where keys are the frequencies (in Hz) and values are lists of points (BP curve). + If the core loss model is calculated at one frequency, this parameter must be provided as a + dictionary with one key (single frequency in Hz) and values are lists of points at + that specific frequency (BP curve). kdc : float - cut_depth : float + Coefficient considering the DC flux bias effects + cut_depth : str, optional + Equivalent cut depth. + You use this parameter to consider the manufacturing effects on core loss computation. + The default value is ``"1mm"``. + thickness : str, optional + Thickness specified in terms of the value plus the unit. + The default is ``"0.5mm"``. + conductivity : float, optional + Conductivity. The unit is S/m. + The default is ``"0 S/m"``. + coefficient_setup : str, optional + Core loss unit. The default is ``"w_per_cubic_meter"``. + Options are ``"kw_per_cubic_meter"``, ``"w_per_cubic_meter"``, ``"w_per_kg"``, + and ``"w_per_lb"``. + core_loss_model_type : str, optional + Core loss model type. The default value is ``"Electrical Steel"``. + Options are ``"Electrical Steel"`` and ``"Power Ferrite"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oDefinitionManager.EditMaterial + + Examples + -------- + This example shows how to set a core loss model for a material in case material properties are calculated for + core losses at one frequency or core losses versus frequencies (core losses multicurve data). + The first case shows how to set properties for core losses at one frequency: + + >>> from pyaedt import Maxwell3d + >>> m3d = Maxwell3d() + >>> box = m3d.modeler.create_box([-10, -10, 0], [20, 20, 20], "box_to_split") + >>> box.material = "magnesium" + >>> m3d.materials["magnesium"].set_electrical_steel_coreloss( + ... points_list_at_freq={60 : [[0,0], [1,3.5], [2,7.4]]} + ... ) + >>> m3d.release_desktop(True, True) + + The second case shows how to set properties for core losses versus frequencies: + + >>> from pyaedt import Maxwell3d + >>> m3d = Maxwell3d() + >>> box = m3d.modeler.create_box([-10, -10, 0], [20, 20, 20], "box_to_split") + >>> box.material = "magnesium" + >>> m3d.materials["magnesium"].set_electrical_steel_coreloss( + ... points_list_at_freq={60 : [[0,0], [1,3.5], [2,7.4]], + ... 100 : [[0,0], [1,8], [2,9]], + ... 150 : [[0,0], [1,10], [2,19]]} + ... ) + >>> m3d.release_desktop(True, True) + + """ + if not isinstance(points_list_at_freq, dict): + raise TypeError("Points list at frequency must be provided as a dictionary.") + freq_keys = list(points_list_at_freq.keys()) + for i in range(0, len(freq_keys)): + if isinstance(freq_keys[i], str): + value, unit = decompose_variable_value(freq_keys[i]) + if unit != "Hz": + value = unit_converter(values=value, unit_system="Freq", input_units=unit, output_units="Hz") + points_list_at_freq[value] = points_list_at_freq[freq_keys[i]] + del points_list_at_freq[freq_keys[i]] + if "core_loss_type" not in self._props: + self._props["core_loss_type"] = OrderedDict( + {"property_type": "ChoiceProperty", "Choice": "Electrical Steel"} + ) + else: + self._props.pop("core_loss_cm", None) + self._props.pop("core_loss_x", None) + self._props.pop("core_loss_y", None) + self._props.pop("core_loss_hci", None) + self._props.pop("core_loss_br", None) + self._props.pop("core_loss_hkc", None) + self._props.pop("core_loss_curves", None) + self._props["core_loss_type"]["Choice"] = core_loss_model_type + if len(points_list_at_freq) == 1: + self._props["AttachedData"]["CoefficientSetupData"] = OrderedDict({}) + self._props["AttachedData"]["CoefficientSetupData"]["property_data"] = "coreloss_data" + self._props["AttachedData"]["CoefficientSetupData"]["coefficient_setup"] = coefficient_setup + frequency = list(points_list_at_freq.keys())[0] + self._props["AttachedData"]["CoefficientSetupData"]["Frequency"] = "{}Hz".format(frequency) + self._props["AttachedData"]["CoefficientSetupData"]["Thickness"] = thickness + self._props["AttachedData"]["CoefficientSetupData"]["Conductivity"] = str(conductivity) + points = [i for p in points_list_at_freq[frequency] for i in p] + self._props["AttachedData"]["CoefficientSetupData"]["Coordinates"] = OrderedDict( + {"DimUnits": ["", ""], "Points": points} + ) + elif len(points_list_at_freq) > 1: + self._props["AttachedData"]["CoreLossMultiCurveData"] = OrderedDict({}) + self._props["AttachedData"]["CoreLossMultiCurveData"]["property_data"] = "coreloss_multi_curve_data" + self._props["AttachedData"]["CoreLossMultiCurveData"]["coreloss_unit"] = "w_per_cubic_meter" + + self._props["AttachedData"]["CoreLossMultiCurveData"]["AllCurves"] = OrderedDict({}) + self._props["AttachedData"]["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"] = [] + for freq in points_list_at_freq.keys(): + points = [i for p in points_list_at_freq[freq] for i in p] + one_curve = OrderedDict( + { + "Frequency": "{}Hz".format(freq), + "Coordinates": OrderedDict({"DimUnits": ["", ""], "Points": points}), + } + ) + self._props["AttachedData"]["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"].append(one_curve) + + coefficients = self.get_core_loss_coefficients( + points_list_at_freq, thickness=thickness, conductivity=conductivity + ) + self._props["core_loss_kh"] = str(coefficients[0]) + self._props["core_loss_kc"] = str(coefficients[1]) + self._props["core_loss_ke"] = str(coefficients[2]) + self._props["core_loss_kdc"] = str(kdc) + self._props["core_loss_equiv_cut_depth"] = cut_depth + return self.update() + + @pyaedt_function_handler() + def set_electrical_steel_coreloss(self, kh=0, kc=0, ke=0, kdc=0, cut_depth="1mm"): + """Set electrical steel core loss. + + Parameters + ---------- + kh : float, optional + Hysteresis core loss coefficient. + The default is ``0``. + kc : float, optional + Eddy-current core loss coefficient. + The default is ``0``. + ke : float, optional + Excess core loss coefficient. + The default is ``0``. + kdc : float, optional + Coefficient considering the DC flux bias effects. + The default is ``0``. + cut_depth : str, optional + Equivalent cut depth considering manufacturing effects on core loss computation. + The default value is ``"1mm"``. Returns ------- @@ -2024,7 +2288,7 @@ def set_electrical_steel_coreloss(self, kh=0, kc=0, ke=0, kdc=0, cut_depth=0.000 self._props["core_loss_kc"] = str(kc) self._props["core_loss_ke"] = str(ke) self._props["core_loss_kdc"] = str(kdc) - self._props["core_loss_equiv_cut_depth"] = "{}meter".format(cut_depth) + self._props["core_loss_equiv_cut_depth"] = cut_depth return self.update() @pyaedt_function_handler() @@ -2306,9 +2570,9 @@ def set_djordjevic_sarkar_model( i_freq : int, float, optional. Input frequency in Hz. sigma_dc : int, float, optional - Conductivity at DC. + Conductivity at DC. The default is ``1e-12``. freq_hi : int, float, optional - High Frequency corner in Hz. + High-frequency corner in Hz. The default is ``159.15494e9``. Returns ------- @@ -2320,7 +2584,6 @@ def set_djordjevic_sarkar_model( K = "({} * {} - {} / (2 * pi * {} * e0)) / atan({} / {})".format(dk, df, sigma_dc, i_freq, freq_hi, i_freq) epsilon_inf = "({} - {} / 2 * ln({}**2 / {}**2 + 1))".format(dk, K, freq_hi, i_freq) freq_low = "({} / exp(10 * {} * {} / ({})))".format(freq_hi, df, epsilon_inf, K) - ds_er = "{} + {} / 2 * ln(({}**2 + Freq**2) / ({}**2 + Freq**2))".format(epsilon_inf, K, freq_hi, freq_low) cond = "{} + 2 * pi * Freq * e0 * ({}) * (atan(Freq / ({})) - atan(Freq / {}))".format( sigma_dc, K, freq_low, freq_hi diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index 7a79917c1ce..e8f51209f24 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -1585,6 +1585,15 @@ def create_report( ... "InputCurrent(PHA)", domain="Time", primary_sweep_variable="Time", plotname="Winding Plot 1" ... ) """ + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_sweep + if not domain: + domain = "Sweep" + setup_name = setup_sweep_name.split(":")[0] + if setup_name: + for setup in self._app.setups: + if setup.name == setup_name and "Time" in setup.default_intrinsics: + domain = "Time" if domain in ["Spectral", "Spectrum"]: report_category = "Spectrum" elif not report_category and not self._app.design_solutions.report_type: @@ -1598,8 +1607,7 @@ def create_report( report_class = TEMPLATES_BY_NAME["Fields"] else: report_class = TEMPLATES_BY_NAME["Standard"] - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep + report = report_class(self, report_category, setup_sweep_name) if not expressions: expressions = [ @@ -1607,6 +1615,8 @@ def create_report( ] report.expressions = expressions report.domain = domain + if not variations: + variations = self._app.available_variations.nominal_w_values_dict if primary_sweep_variable: report.primary_sweep = primary_sweep_variable elif domain == "DCIR": # pragma: no cover @@ -1617,8 +1627,8 @@ def create_report( variations = {"Index": "All"} if secondary_sweep_variable: report.secondary_sweep = secondary_sweep_variable - if variations: - report.variations = variations + + report.variations = variations report.report_type = plot_type report.sub_design_id = subdesign_id report.point_number = polyline_points @@ -1669,7 +1679,7 @@ def get_solution_data( self, expressions=None, setup_sweep_name=None, - domain="Sweep", + domain=None, variations=None, primary_sweep_variable=None, report_category=None, @@ -1696,7 +1706,7 @@ def get_solution_data( Plot Domain. Options are "Sweep" for frequency domain related results and "Time" for transient related data. variations : dict, optional Dictionary of all families including the primary sweep. - The default is ``None`` which will use the nominal variations of the design. + The default is ``None`` which will use the nominal variations of the setup. primary_sweep_variable : str, optional Name of the primary sweep. The default is ``"None"`` which, depending on the context, will internally assign the primary sweep to: @@ -1784,6 +1794,15 @@ def get_solution_data( ...) """ expressions = [expressions] if isinstance(expressions, str) else expressions + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_sweep + if not domain: + domain = "Sweep" + setup_name = setup_sweep_name.split(":")[0] + if setup_name: + for setup in self._app.setups: + if setup.name == setup_name and "Time" in setup.default_intrinsics: + domain = "Time" if domain in ["Spectral", "Spectrum"]: report_category = "Spectrum" if not report_category and not self._app.design_solutions.report_type: @@ -1797,8 +1816,7 @@ def get_solution_data( report_class = TEMPLATES_BY_NAME["Fields"] else: report_class = TEMPLATES_BY_NAME["Standard"] - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep + report = report_class(self, report_category, setup_sweep_name) if not expressions: expressions = [ @@ -1812,6 +1830,8 @@ def get_solution_data( report.primary_sweep = primary_sweep_variable if variations: report.variations = variations + else: + report.variations = self._app.available_variations.nominal_w_values_dict report.sub_design_id = subdesign_id report.point_number = polyline_points if context == "Differential Pairs": diff --git a/pyaedt/modules/SolveSetup.py b/pyaedt/modules/SolveSetup.py index 91ff27b461a..f7eae27897c 100644 --- a/pyaedt/modules/SolveSetup.py +++ b/pyaedt/modules/SolveSetup.py @@ -220,6 +220,241 @@ def name(self, name): self._setupname = name self.props["Name"] = name + @pyaedt_function_handler() + def get_solution_data( + self, + expressions=None, + domain=None, + variations=None, + primary_sweep_variable=None, + report_category=None, + context=None, + polyline_points=1001, + math_formula=None, + sweep_name=None, + ): + """Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. + Data to be retrieved from Electronics Desktop are any simulation results available in that + specific simulation context. + Most of the argument have some defaults which works for most of the ``Standard`` report quantities. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value ``"dB(S(1,1))"`` or a list of values. + Default is `None` which will return all traces. + domain : str, optional + Plot Domain. Options are "Sweep" for frequency domain related results and "Time" for transient related data. + variations : dict, optional + Dictionary of all families including the primary sweep. + The default is ``None`` which will use the nominal variations of the setup. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"None"`` which, depending on the context, + will internally assign the primary sweep to: + 1. ``Freq`` for frequency domain results, + 2. ``Time`` for transient results, + 3. ``Theta`` for radiation patterns, + 4. ``distance`` for field plot over a polyline. + report_category : str, optional + Category of the Report to be created. If `None` default data Report will be used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category will be in this case "Far Fields". + Depending on the setup different categories are available. + If `None` default category will be used (the first item in the Results drop down menu in AEDT). + To get the list of available categories user can use method ``available_report_types``. + context : str, dict, optional + This is the context of the report. + The default is ``None``. It can be: + 1. `None` + 2. Infinite Sphere name for Far Fields Plot. + 3. Dictionary. If dictionary is passed, key is the report property name and value is property value. + polyline_points : int, optional + Number of points on which to create the report for plots on polylines. + This parameter is valid for ``Fields`` plot only. + math_formula : str, optional + One of the available AEDT mathematical formulas to apply. For example, ``abs, dB``. + sweep_name : str, optional + Name of the sweep adaptive setup from which get solutions. Default is ``LastAdaptive``. + + Returns + ------- + :class:`pyaedt.modules.solutions.SolutionData` + Solution Data object. + + References + ---------- + + >>> oModule.GetSolutionDataPerVariation + + Examples + -------- + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> aedtapp.post.create_report("dB(S(1,1))") + + >>> variations = aedtapp.available_variations.nominal_w_values_dict + >>> variations["Theta"] = ["All"] + >>> variations["Phi"] = ["All"] + >>> variations["Freq"] = ["30GHz"] + >>> data1 = aedtapp.post.get_solution_data( + ... "GainTotal", + ... aedtapp.nominal_adaptive, + ... variations=variations, + ... primary_sweep_variable="Phi", + ... secondary_sweep_variable="Theta", + ... context="3D", + ... report_category="Far Fields", + ...) + + >>> data2 =aedtapp.post.get_solution_data( + ... "S(1,1)", + ... aedtapp.nominal_sweep, + ... variations=variations, + ...) + >>> data2.plot() + + >>> from pyaedt import Maxwell2d + >>> maxwell_2d = Maxwell2d() + >>> data3 = maxwell_2d.post.get_solution_data( + ... "InputCurrent(PHA)", domain="Time", primary_sweep_variable="Time", + ... ) + >>> data3.plot("InputCurrent(PHA)") + + >>> from pyaedt import Circuit + >>> circuit = Circuit() + >>> context = {"algorithm": "FFT", "max_frequency": "100MHz", "time_stop": "2.5us", "time_start": "0ps"} + >>> spectralPlotData = circuit.post.get_solution_data( + ... expressions="V(Vprobe1)", primary_sweep_variable="Spectrum", domain="Spectral", + ... context=context + ...) + """ + if sweep_name: + setup_sweep_name = [ + i for i in self._app.existing_analysis_sweeps if self.name == i.split(" : ")[0] and sweep_name in i + ] + else: + setup_sweep_name = [i for i in self._app.existing_analysis_sweeps if self.name == i.split(" : ")[0]] + if setup_sweep_name: + return self._app.post.get_solution_data( + expressions=expressions, + domain=domain, + variations=variations, + primary_sweep_variable=primary_sweep_variable, + report_category=report_category, + context=context, + polyline_points=polyline_points, + math_formula=math_formula, + setup_sweep_name=setup_sweep_name[0], + ) + return None + + @pyaedt_function_handler() + def create_report( + self, + expressions=None, + domain="Sweep", + variations=None, + primary_sweep_variable=None, + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=1001, + plotname=None, + sweep_name=None, + ): + """Create a report in AEDT. It can be a 2D plot, 3D plot, polar plots or data tables. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value = ``"dB(S(1,1))"``. + domain : str, optional + Plot Domain. Options are "Sweep", "Time", "DCIR". + variations : dict, optional + Dictionary of all families including the primary sweep. The default is ``{"Freq": ["All"]}``. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"Freq"``. + secondary_sweep_variable : str, optional + Name of the secondary sweep variable in 3D Plots. + report_category : str, optional + Category of the Report to be created. If `None` default data Report will be used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category will be in this case "Far Fields". + Depending on the setup different categories are available. + If `None` default category will be used (the first item in the Results drop down menu in AEDT). + plot_type : str, optional + The format of Data Visualization. Default is ``Rectangular Plot``. + context : str, optional + The default is ``None``. It can be `None`, `"Differential Pairs"`,`"RL"`, + `"Sources"`, `"Vias"`,`"Bondwires"`, `"Probes"` for Hfss3dLayout or + Reduce Matrix Name for Q2d/Q3d solution or Infinite Sphere name for Far Fields Plot. + plotname : str, optional + Name of the plot. The default is ``None``. + polyline_points : int, optional, + Number of points on which create the report for plots on polylines. + subdesign_id : int, optional + Specify a subdesign ID to export a Touchstone file of this subdesign. Valid for Circuit Only. + The default value is ``None``. + context : str, optional + sweep_name : str, optional + Name of the sweep adaptive setup from which get solutions. Default is ``LastAdaptive``. + + Returns + ------- + :class:`pyaedt.modules.report_templates.Standard` + ``True`` when successful, ``False`` when failed. + + + References + ---------- + + >>> oModule.CreateReport + + Examples + -------- + >>> from pyaedt import Circuit + >>> aedtapp = Circuit() + >>> aedtapp.post.create_report("dB(S(1,1))") + + >>> variations = aedtapp.available_variations.nominal_w_values_dict + >>> aedtapp.post.setups[0].create_report( + ... "dB(S(1,1))", + ... variations=variations, + ... primary_sweep_variable="Freq", + ...) + + >>> aedtapp.post.create_report( + ... "S(1,1)", + ... variations=variations, + ... plot_type="Smith Chart", + ...) + """ + if sweep_name: + setup_sweep_name = [ + i for i in self._app.existing_analysis_sweeps if self.name == i.split(" : ")[0] and sweep_name in i + ] + else: + setup_sweep_name = [i for i in self._app.existing_analysis_sweeps if self.name == i.split(" : ")[0]] + if setup_sweep_name: + return self._app.post.create_report( + expressions=expressions, + domain=domain, + variations=variations, + primary_sweep_variable=primary_sweep_variable, + secondary_sweep_variable=secondary_sweep_variable, + report_category=report_category, + plot_type=plot_type, + context=context, + polyline_points=polyline_points, + plotname=plotname, + setup_sweep_name=setup_sweep_name[0], + ) + return None + class Setup(CommonSetup): """Initializes, creates, and updates a 3D setup. @@ -1181,6 +1416,168 @@ def disable(self, setup_name=None): self._odesign.EnableSolutionSetup(setup_name, False) return True + @pyaedt_function_handler() + def get_solution_data( + self, + expressions=None, + domain=None, + variations=None, + primary_sweep_variable=None, + report_category=None, + context=None, + subdesign_id=None, + polyline_points=1001, + math_formula=None, + ): + """Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. + Data to be retrieved from Electronics Desktop are any simulation results available in that + specific simulation context. + Most of the argument have some defaults which works for most of the ``Standard`` report quantities. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value ``"dB(S(1,1))"`` or a list of values. + Default is `None` which will return all traces. + domain : str, optional + Plot Domain. Options are "Sweep" for frequency domain related results and "Time" for transient related data. + variations : dict, optional + Dictionary of all families including the primary sweep. + The default is ``None`` which will use the nominal variations of the setup. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"None"`` which, depending on the context, + will internally assign the primary sweep to: + 1. ``Freq`` for frequency domain results, + 2. ``Time`` for transient results, + 3. ``Theta`` for radiation patterns, + 4. ``distance`` for field plot over a polyline. + report_category : str, optional + Category of the Report to be created. If `None` default data Report will be used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category will be in this case "Far Fields". + Depending on the setup different categories are available. + If `None` default category will be used (the first item in the Results drop down menu in AEDT). + To get the list of available categories user can use method ``available_report_types``. + context : str, dict, optional + This is the context of the report. + The default is ``None``. It can be: + 1. `None` + 2. ``"Differential Pairs"`` + 3. Reduce Matrix Name for Q2d/Q3d solution + 4. Infinite Sphere name for Far Fields Plot. + 5. Dictionary. If dictionary is passed, key is the report property name and value is property value. + subdesign_id : int, optional + Subdesign ID for exporting a Touchstone file of this subdesign. + This parameter is valid for ``Circuit`` only. + The default value is ``None``. + polyline_points : int, optional + Number of points on which to create the report for plots on polylines. + This parameter is valid for ``Fields`` plot only. + math_formula : str, optional + One of the available AEDT mathematical formulas to apply. For example, ``abs, dB``. + + + Returns + ------- + :class:`pyaedt.modules.solutions.SolutionData` + Solution Data object. + + References + ---------- + + >>> oModule.GetSolutionDataPerVariation + """ + return self._app.post.get_solution_data( + expressions=expressions, + domain=domain, + variations=variations, + primary_sweep_variable=primary_sweep_variable, + report_category=report_category, + context=context, + subdesign_id=subdesign_id, + polyline_points=polyline_points, + math_formula=math_formula, + setup_sweep_name=self.name, + ) + + @pyaedt_function_handler() + def create_report( + self, + expressions=None, + domain="Sweep", + variations=None, + primary_sweep_variable=None, + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=1001, + plotname=None, + ): + """Create a report in AEDT. It can be a 2D plot, 3D plot, polar plots or data tables. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value = ``"dB(S(1,1))"``. + domain : str, optional + Plot Domain. Options are "Sweep", "Time", "DCIR". + variations : dict, optional + Dictionary of all families including the primary sweep. The default is ``{"Freq": ["All"]}``. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"Freq"``. + secondary_sweep_variable : str, optional + Name of the secondary sweep variable in 3D Plots. + report_category : str, optional + Category of the Report to be created. If `None` default data Report will be used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category will be in this case "Far Fields". + Depending on the setup different categories are available. + If `None` default category will be used (the first item in the Results drop down menu in AEDT). + plot_type : str, optional + The format of Data Visualization. Default is ``Rectangular Plot``. + context : str, optional + The default is ``None``. It can be `None`, `"Differential Pairs"`,`"RL"`, + `"Sources"`, `"Vias"`,`"Bondwires"`, `"Probes"` for Hfss3dLayout or + Reduce Matrix Name for Q2d/Q3d solution or Infinite Sphere name for Far Fields Plot. + plotname : str, optional + Name of the plot. The default is ``None``. + polyline_points : int, optional, + Number of points on which create the report for plots on polylines. + subdesign_id : int, optional + Specify a subdesign ID to export a Touchstone file of this subdesign. Valid for Circuit Only. + The default value is ``None``. + context : str, optional + + Returns + ------- + :class:`pyaedt.modules.report_templates.Standard` + ``True`` when successful, ``False`` when failed. + + + References + ---------- + + >>> oModule.CreateReport + """ + return self._app.post.create_report( + expressions=expressions, + domain=domain, + variations=variations, + primary_sweep_variable=primary_sweep_variable, + secondary_sweep_variable=secondary_sweep_variable, + report_category=report_category, + plot_type=plot_type, + context=context, + polyline_points=polyline_points, + plotname=plotname, + subdesign_id=subdesign_id, + setup_sweep_name=self.name, + ) + class Setup3DLayout(CommonSetup): """Initializes, creates, and updates a 3D Layout setup. @@ -1232,13 +1629,20 @@ def is_solved(self): `True` if solutions are available. """ if self.props.get("SolveSetupType", "HFSS") == "HFSS": - sol = self._app.post.reports_by_category.standard(setup_name="{} : Last Adaptive".format(self.name)) + combined_name = "{} : Last Adaptive".format(self.name) + expressions = [i for i in self.p_app.post.available_report_quantities(solution=combined_name)] + sol = self._app.post.reports_by_category.standard(setup_name=combined_name, expressions=expressions[0]) elif self.props.get("SolveSetupType", "HFSS") == "SIwave": + combined_name = "{} : {}".format(self.name, self.sweeps[0].name) + expressions = [i for i in self.p_app.post.available_report_quantities(solution=combined_name)] sol = self._app.post.reports_by_category.standard( - setup_name="{} : {}".format(self.name, self.sweeps[0].name) + setup_name=combined_name, + expressions=expressions[0], ) else: - sol = self._app.post.reports_by_category.standard(setup_name=self.name) + expressions = [i for i in self.p_app.post.available_report_quantities(solution=self.name)] + + sol = self._app.post.reports_by_category.standard(setup_name=self.name, expressions=expressions[0]) if identify_setup(self.props): sol.domain = "Time" return True if sol.get_solution_data() else False