diff --git a/_unittest/example_models/T12/ipk_markers.aedtz b/_unittest/example_models/T12/ipk_markers.aedtz new file mode 100644 index 00000000000..1b32a70e302 Binary files /dev/null and b/_unittest/example_models/T12/ipk_markers.aedtz differ diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 95edad68d13..23c16ddfdf2 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -24,8 +24,10 @@ import os import sys +import tempfile from _unittest.conftest import config +import pandas as pd import pytest from pyaedt import Circuit @@ -45,6 +47,7 @@ sbr_file = "poc_scat_small_231" q3d_file = "via_gsg_231" m2d_file = "m2d_field_lines_test_231" + ipk_markers_proj = "ipk_markers" else: test_field_name = "Potter_Horn" @@ -756,7 +759,7 @@ def test_z99_delete_variations_B(self, field_test): assert field_test.cleanup_solution(vars, entire_solution=False) assert field_test.cleanup_solution(vars, entire_solution=True) - def test_76_ipk_get_scalar_field_value(self, icepak_post): + def test_100_ipk_get_scalar_field_value(self, icepak_post): assert icepak_post.post.get_scalar_field_value( "Heat_Flow_Rate", scalar_function="Integrate", @@ -829,3 +832,45 @@ def test_76_ipk_get_scalar_field_value(self, icepak_post): object_type="point", adjacent_side=False, ) + + @pytest.mark.skipif(config["NonGraphical"], reason="Method does not work in non-graphical mode.") + def test_101_markers(self, add_app): + ipk = add_app(project_name=ipk_markers_proj, application=Icepak, subfolder=test_subfolder) + + f1 = ipk.modeler["Region"].top_face_z + p1 = ipk.post.create_fieldplot_surface(f1.id, "Uz") + f1_c = f1.center + f1_p1 = [f1_c[0] + 0.01, f1_c[1] + 0.01, f1_c[2]] + f1_p2 = [f1_c[0] - 0.01, f1_c[1] - 0.01, f1_c[2]] + d1 = p1.get_points_value([f1_c, f1_p1, f1_p2]) + assert isinstance(d1, pd.DataFrame) + assert d1.index.name == "Name" + assert all(d1.index.values == ["m1", "m2", "m3"]) + assert len(d1["X [mm]"].values) == 3 + assert len(d1.columns) == 4 + + f2 = ipk.modeler["Box1"].top_face_z + p2 = ipk.post.create_fieldplot_surface(f2.id, "Pressure") + d2 = p2.get_points_value({"Center Point": f2.center}) + assert isinstance(d2, pd.DataFrame) + assert d2.index.name == "Name" + assert all(d2.index.values == ["Center Point"]) + assert len(d2.columns) == 4 + assert len(d2["X [mm]"].values) == 1 + + f3 = ipk.modeler["Box1"].bottom_face_y + p3 = ipk.post.create_fieldplot_surface(f3.id, "Temperature") + d3 = p3.get_points_value(f3.center) + assert isinstance(d3, pd.DataFrame) + assert d3.index.name == "Name" + assert all(d3.index.values == ["m1"]) + assert len(d3.columns) == 4 + assert len(d3["X [mm]"].values) == 1 + + f4 = ipk.modeler["Box1"].top_face_x + p4 = ipk.post.create_fieldplot_surface(f4.id, "HeatFlowRate") + temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".csv") + temp_file.close() + d4 = p4.get_points_value(f4.center, filename=temp_file.name) + assert isinstance(d4, pd.DataFrame) + os.path.exists(temp_file.name) diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index 86fa6a3569e..bdd21fdc7d0 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -1105,7 +1105,7 @@ def evaluate_faces_quantity( Dictionary of parameters defined for the specific setup with values. The default is ``{}``. ref_temperature: str, optional Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str + time : str, optional Timestep to get the data from. Default is ``"0s"``. Returns @@ -1168,7 +1168,7 @@ def evaluate_boundary_quantity( Dictionary of parameters defined for the specific setup with values. The default is ``{}``. ref_temperature: str, optional Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str + time : str, optional Timestep to get the data from. Default is ``"0s"``. Returns @@ -1220,7 +1220,7 @@ def evaluate_monitor_quantity( Dictionary of parameters defined for the specific setup with values. The default is ``{}``. ref_temperature: str, optional Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str + time : str, optional Timestep to get the data from. Default is ``"0s"``. Returns @@ -1286,7 +1286,7 @@ def evaluate_object_quantity( Dictionary of parameters defined for the specific setup with values. The default is ``{}``. ref_temperature: str, optional Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str + time : str, optional Timestep to get the data from. Default is ``"0s"``. Returns diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index f283c97c5a8..fc76e421177 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -5711,7 +5711,7 @@ def add_calculation( ref_temperature : str, optional Reference temperature to use in the calculation of the heat transfer coefficient. The default is ``"AmbientTemp"``. - time : str + time : str, optional Timestep to get the data from. Default is ``"0s"``. Returns diff --git a/pyaedt/modules/solutions.py b/pyaedt/modules/solutions.py index 01131fc14e9..ce6ba5ae317 100644 --- a/pyaedt/modules/solutions.py +++ b/pyaedt/modules/solutions.py @@ -21,18 +21,22 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - from collections import OrderedDict +from collections import defaultdict +import csv import itertools import logging import math import os +import shutil import sys +import tempfile from pyaedt.generic.constants import AEDT_UNITS from pyaedt.generic.constants import CSS4_COLORS from pyaedt.generic.constants import db10 from pyaedt.generic.constants import db20 +from pyaedt.generic.general_methods import GrpcApiError from pyaedt.generic.general_methods import check_and_download_file from pyaedt.generic.general_methods import is_ironpython from pyaedt.generic.general_methods import open_file @@ -1797,6 +1801,111 @@ def plotsettings(self): ] return arg + @pyaedt_function_handler() + def get_points_value(self, points, filename=None, visibility=False): # pragma: no cover + """ + Get points data from field plot. + + .. note:: + This method is working only if the associated field plot is currently visible. + + .. note:: + This method does not work in non-graphical mode. + + Parameters + ---------- + points : list, list of lists or dict + List with [x,y,z] coordinates of a point or list of lists of points or + dictionary with keys containing point names and for each key the point + coordinates [x,y,z]. + filename : str, optional + Full path or relative path with filename. + Default is ``None`` in which case no file is exported. + visibility : bool, optional + Whether to keep the markers visible in the UI. Default is ``False``. + + Returns + ------- + dict or pd.DataFrame + Dict containing 5 keys: point names, x,y,z coordinates and the quantity probed. + Each key is associated with a list with the same length of the argument points. + If pandas is installed, the output is a pandas DataFrame with point names as + index and coordinates and quantity as columns. + """ + self.oField.ClearAllMarkers() + + # Clean inputs + if isinstance(points, dict): + points_name, points_value = list(points.keys()), list(points.values()) + elif isinstance(points, list): + points_name = None + if not isinstance(points[0], list): + points_value = [points] + else: + points_value = points + else: + raise AttributeError("``points`` argument is invalid.") + if filename is not None: + if not os.path.isdir(os.path.dirname(filename)): + raise AttributeError("Specified path ({}) does not exist".format(filename)) + + # Create markers + u = self._postprocessor._app.modeler.model_units + added_points_name = [] + for pt_name_idx, pt in enumerate(points_value): + try: + pt = [c if isinstance(c, str) else "{}{}".format(c, u) for c in pt] + self.oField.AddMarkerToPlot(pt, self.name) + if points_name is not None: + added_points_name.append(points_name[pt_name_idx]) + except (GrpcApiError, SystemExit) as e: # pragma: no cover + self._postprocessor.logger.error( + "Point {} not added. Check if it lies inside the plot.".format(str(pt)) + ) + raise e + + # Export data + temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".csv") + temp_file.close() + self.oField.ExportMarkerTable(temp_file.name) + with open_file(temp_file.name, "r") as f: + reader = csv.DictReader(f) + out_dict = defaultdict(list) + for row in reader: + for key in row.keys(): + if key == "Name": + val = row[key] + else: + val = float(row[key].lstrip()) + out_dict[key.lstrip()].append(val) + + # Modify data if needed + if points_name is not None: + out_dict["Name"] = added_points_name + # Export data + if filename is not None: + with open(filename, mode="w") as outfile: + writer = csv.DictWriter(outfile, fieldnames=out_dict.keys()) + writer.writeheader() + for i in range(len(out_dict["Name"])): + row = {field: out_dict[field][i] for field in out_dict} + writer.writerow(row) + elif filename is not None: + # Export data + shutil.copy2(temp_file.name, filename) + os.remove(temp_file.name) + + if not visibility: + self.oField.ClearAllMarkers() + + # Convert to pandas + if pd is not None: + df = pd.DataFrame(out_dict, columns=out_dict.keys()) + df = df.set_index("Name") + return df + else: + return out_dict + @property def surfacePlotInstruction(self): """Surface plot settings.