Skip to content

Commit ac0375a

Browse files
authored
Add DataFrame.min and DataFrame.max over 'set_ids' axis or MeshIndex (#333)
* DataFrame.min and DataFrame.max over time-Index and MeshIndex * DataFrame.min and DataFrame.max over time-Index and MeshIndex * WIP mean * Revert "WIP mean" This reverts commit 5bc0d0c. * Improve docstring * Fix step "Set licensing if necessary" in CI and CI_release for retro tests * Fix ANSYS_VERSION for "upload test results" step of retro in ci.yml and ci_release.yml * Update example to remove mention of mean * Update example * Add examples to docstrings * Fix docstring examples * Take comments into account * Fix docstring example for min * Rename example 06-compute-min-max.py
1 parent 49b8eef commit ac0375a

File tree

5 files changed

+321
-5
lines changed

5 files changed

+321
-5
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135
- uses: actions/checkout@v3
136136

137137
- name: "Set licensing if necessary"
138-
if: env.ANSYS_VERSION > 231
138+
if: matrix.ANSYS_VERSION > 231
139139
shell: bash
140140
run: |
141141
echo "ANSYS_DPF_ACCEPT_LA=Y" >> $GITHUB_ENV
@@ -183,7 +183,7 @@ jobs:
183183
- name: "Upload Test Results"
184184
uses: actions/upload-artifact@v3
185185
with:
186-
name: ${{ env.PACKAGE_NAME }}_${{ matrix.python-version }}_${{ matrix.os }}_pytest_${{ env.ANSYS_VERSION }}
186+
name: ${{ env.PACKAGE_NAME }}_${{ matrix.python-version }}_${{ matrix.os }}_pytest_${{ matrix.ANSYS_VERSION }}
187187
path: tests/junit/test-results.xml
188188
if: always()
189189

.github/workflows/ci_release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ jobs:
131131
- uses: actions/checkout@v3
132132

133133
- name: "Set licensing if necessary"
134-
if: env.ANSYS_VERSION > 231
134+
if: matrix.ANSYS_VERSION > 231
135135
shell: bash
136136
run: |
137137
echo "ANSYS_DPF_ACCEPT_LA=Y" >> $GITHUB_ENV
@@ -179,7 +179,7 @@ jobs:
179179
- name: "Upload Test Results"
180180
uses: actions/upload-artifact@v3
181181
with:
182-
name: ${{ env.PACKAGE_NAME }}_${{ matrix.python-version }}_${{ matrix.os }}_pytest_${{ env.ANSYS_VERSION }}
182+
name: ${{ env.PACKAGE_NAME }}_${{ matrix.python-version }}_${{ matrix.os }}_pytest_${{ matrix.ANSYS_VERSION }}
183183
path: tests/junit/test-results.xml
184184
if: always()
185185

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
.. _ref_compute_statistics_example:
3+
4+
Compute minimum and maximum of a DataFrame
5+
==========================================
6+
In this example, transient mechanical displacement data is used
7+
to show how to compute the min or max of a given DataFrame.
8+
"""
9+
10+
###############################################################################
11+
# Perform required imports
12+
# ------------------------
13+
# This example uses a supplied file that you can
14+
# get using the ``examples`` module.
15+
16+
from ansys.dpf import post
17+
from ansys.dpf.post import examples
18+
19+
###############################################################################
20+
# Get ``Simulation`` object
21+
# -------------------------
22+
# Get the ``Simulation`` object that allows access to the result. The ``Simulation``
23+
# object must be instantiated with the path for the result file. For example,
24+
# ``"C:/Users/user/my_result.rst"`` on Windows or ``"/home/user/my_result.rst"``
25+
# on Linux.
26+
27+
example_path = examples.download_crankshaft()
28+
simulation = post.StaticMechanicalSimulation(example_path)
29+
30+
# print the simulation to get an overview of what's available
31+
print(simulation)
32+
33+
###############################################################################
34+
# Extract displacement data
35+
# -------------------------
36+
37+
displacement = simulation.displacement(all_sets=True)
38+
print(displacement)
39+
40+
###############################################################################
41+
# Compute the maximum displacement for each component at each time-step
42+
# ---------------------------------------------------------------------
43+
44+
# The default axis is the MeshIndex
45+
maximum_over_mesh = displacement.max()
46+
print(maximum_over_mesh)
47+
# is equivalent to
48+
maximum_over_mesh = displacement.max(axis="node_ids")
49+
print(maximum_over_mesh)
50+
51+
# Compute the maximum displacement for each node and component across time
52+
# ------------------------------------------------------------------------
53+
maximum_over_time = displacement.max(axis="set_ids")
54+
print(maximum_over_time)
55+
56+
# Compute the maximum displacement overall
57+
# ----------------------------------------
58+
maximum_overall = maximum_over_time.max()
59+
print(maximum_overall)
60+
61+
###############################################################################
62+
# Compute the minimum displacement for each component at each time-step
63+
# ---------------------------------------------------------------------
64+
65+
# The default axis is the MeshIndex
66+
minimum_over_mesh = displacement.min()
67+
print(minimum_over_mesh)
68+
# is equivalent to
69+
minimum_over_mesh = displacement.min(axis="node_ids")
70+
print(minimum_over_mesh)
71+
72+
# Compute the minimum displacement for each node and component across time
73+
# ------------------------------------------------------------------------
74+
minimum_over_time = displacement.min(axis="set_ids")
75+
print(minimum_over_time)
76+
77+
# Compute the minimum displacement overall
78+
# ----------------------------------------
79+
minimum_overall = minimum_over_time.min()
80+
print(minimum_overall)

src/ansys/dpf/post/dataframe.py

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def __init__(
7777
self._last_display_width = display_width
7878
self._last_display_max_colwidth = display_max_colwidth
7979

80+
self._last_minmax: dict = {"axis": None, "min": None, "max": None}
81+
8082
@property
8183
def columns(self) -> MultiIndex:
8284
"""Returns the MultiIndex for the columns of the DataFrame."""
@@ -513,7 +515,7 @@ def treat_elemental_nodal(treat_lines, pos, n_comp, n_ent, n_lines):
513515
else empty
514516
for i in range(len(combination))
515517
]
516-
to_append.append(empty)
518+
to_append.append(empty) # row where row index headers are
517519
# Get data in the FieldsContainer for those positions
518520
# Create label_space from combination
519521
label_space = {}
@@ -833,3 +835,201 @@ def animate(
833835
return fc.animate(
834836
save_as=save_as, deform_by=deform_by, scale_factor=scale_factor, **kwargs
835837
)
838+
839+
def min(self, axis: Union[int, str, None] = 0) -> Union[DataFrame, float]:
840+
"""Return the minimum value over the requested axis.
841+
842+
Parameters
843+
----------
844+
axis:
845+
Axis to perform minimum across.
846+
Defaults to the MeshIndex (0), the row index containing mesh entity IDs.
847+
This computes the minimum across the mesh for each set.
848+
Can also be the SetIndex (1), the column index containing set (time/frequency) IDs.
849+
This computes the minimum across sets (time/frequency) for each mesh entity.
850+
851+
Returns
852+
-------
853+
A scalar if the result of the query is a single number,
854+
or a DataFrame if several numbers along one or several axes.
855+
856+
Examples
857+
--------
858+
>>> from ansys.dpf import post
859+
>>> from ansys.dpf.post import examples
860+
>>> simulation = post.StaticMechanicalSimulation(examples.download_crankshaft())
861+
>>> displacement = simulation.displacement(all_sets=True)
862+
>>> # Compute the maximum displacement value for each component at each time-step
863+
>>> minimum_over_mesh = displacement.min(axis="node_ids")
864+
>>> print(minimum_over_mesh) # doctest: +NORMALIZE_WHITESPACE
865+
results U (m)
866+
set_ids 1 2 3
867+
components
868+
X -7.4732e-04 -1.5081e-03 -2.2755e-03
869+
Y -4.0138e-04 -8.0316e-04 -1.2014e-03
870+
Z -2.1555e-04 -4.3299e-04 -6.5101e-04
871+
>>> # Compute the maximum displacement for each node and component across time
872+
>>> minimum_over_time = displacement.min(axis="set_ids")
873+
>>> print(minimum_over_time) # doctest: +NORMALIZE_WHITESPACE
874+
results U (m)
875+
node_ids components
876+
4872 X -3.4137e-05
877+
Y 5.1667e-04
878+
Z -4.1346e-06
879+
9005 X -5.5625e-05
880+
Y 4.8445e-04
881+
Z -4.9795e-07
882+
...
883+
>>> # Compute the maximum displacement overall
884+
>>> minimum_overall = minimum_over_time.min()
885+
>>> print(minimum_overall) # doctest: +NORMALIZE_WHITESPACE
886+
results U (m)
887+
components
888+
X -2.2755e-03
889+
Y -1.2014e-03
890+
Z -6.5101e-04
891+
"""
892+
self._query_min_max(axis)
893+
return self._last_minmax["min"]
894+
895+
def max(self, axis: Union[int, str, None] = 0) -> Union[DataFrame, float]:
896+
"""Return the maximum value over the requested axis.
897+
898+
Parameters
899+
----------
900+
axis:
901+
Axis to perform maximum across.
902+
Defaults to the MeshIndex (0), the row index containing mesh entity IDs.
903+
This computes the maximum across the mesh for each set.
904+
Can also be the SetIndex (1), the column index containing set (time/frequency) IDs.
905+
This computes the maximum across sets (time/frequency) for each mesh entity.
906+
907+
Returns
908+
-------
909+
A scalar if the result of the query is a single number,
910+
or a DataFrame if several numbers along one or several axes.
911+
912+
Examples
913+
--------
914+
>>> from ansys.dpf import post
915+
>>> from ansys.dpf.post import examples
916+
>>> simulation = post.StaticMechanicalSimulation(examples.download_crankshaft())
917+
>>> displacement = simulation.displacement(all_sets=True)
918+
>>> # Compute the maximum displacement value for each component at each time-step
919+
>>> maximum_over_mesh = displacement.max(axis="node_ids")
920+
>>> print(maximum_over_mesh) # doctest: +NORMALIZE_WHITESPACE
921+
results U (m)
922+
set_ids 1 2 3
923+
components
924+
X 7.3303e-04 1.4495e-03 2.1441e-03
925+
Y 1.3962e-03 2.7884e-03 4.1656e-03
926+
Z 2.1567e-04 4.3321e-04 6.5135e-04
927+
>>> # Compute the maximum displacement for each node and component across time
928+
>>> maximum_over_time = displacement.max(axis="set_ids")
929+
>>> print(maximum_over_time) # doctest: +NORMALIZE_WHITESPACE
930+
results U (m)
931+
node_ids components
932+
4872 X 5.6781e-06
933+
Y 1.5417e-03
934+
Z -2.6398e-06
935+
9005 X -2.6323e-06
936+
Y 1.4448e-03
937+
Z 5.3134e-06
938+
...
939+
>>> # Compute the maximum displacement overall
940+
>>> maximum_overall = maximum_over_time.max()
941+
>>> print(maximum_overall) # doctest: +NORMALIZE_WHITESPACE
942+
results U (m)
943+
components
944+
X 2.1441e-03
945+
Y 4.1656e-03
946+
Z 6.5135e-04
947+
"""
948+
self._query_min_max(axis)
949+
return self._last_minmax["max"]
950+
951+
def _query_min_max(self, axis: Union[int, str, None]) -> None:
952+
"""Create a DPF workflow based on the query arguments for min/max."""
953+
# Translate None query to empty dict
954+
if axis in [None, 0, self.index.mesh_index.name]:
955+
axis = 0
956+
elif axis in [1, ref_labels.set_ids]:
957+
axis = 1
958+
else:
959+
raise ValueError(f"'{axis}' is not an available axis value.")
960+
961+
# print(f"{axis=}")
962+
# If same query as last and last is not None, do not change
963+
if self._last_minmax["axis"] == axis and not self._last_minmax["axis"] is None:
964+
return
965+
# If in need of an update, create the appropriate workflow
966+
wf = dpf.Workflow(server=self._fc._server)
967+
wf.progress_bar = False
968+
969+
# If over mesh
970+
if axis == 0:
971+
min_max_op = dpf.operators.min_max.min_max_over_label_fc(
972+
fields_container=self._fc,
973+
label="time",
974+
server=self._fc._server,
975+
)
976+
# Here the fields are located on the label ("time"), so we have to "transpose" it.
977+
# Extract the data for each time (entity) from the field and create a fields_container
978+
979+
min_fc = dpf.FieldsContainer(server=self._fc._server)
980+
min_fc.add_label(label="time")
981+
min_field = min_max_op.outputs.field_min()
982+
for i, time in enumerate(min_field.scoping.ids):
983+
min_fc.add_field(
984+
label_space={"time": time},
985+
field=dpf.fields_factory.field_from_array(
986+
arr=min_field.get_entity_data(i),
987+
server=self._fc._server,
988+
),
989+
)
990+
991+
max_fc = dpf.FieldsContainer(server=self._fc._server)
992+
max_fc.add_label(label="time")
993+
max_field = min_max_op.outputs.field_max()
994+
for i, time in enumerate(max_field.scoping.ids):
995+
max_fc.add_field(
996+
label_space={"time": time},
997+
field=dpf.fields_factory.field_from_array(
998+
arr=max_field.get_entity_data(i),
999+
server=self._fc._server,
1000+
),
1001+
)
1002+
1003+
index = MultiIndex(
1004+
indexes=[i for i in self.index if i != self.index.mesh_index]
1005+
)
1006+
columns = self.columns
1007+
1008+
# If over time
1009+
else:
1010+
min_max_op = dpf.operators.min_max.min_max_over_time_by_entity(
1011+
fields_container=self._fc,
1012+
server=self._fc._server,
1013+
)
1014+
wf.set_output_name("min", min_max_op.outputs.min)
1015+
wf.set_output_name("max", min_max_op.outputs.max)
1016+
1017+
index = self.index
1018+
columns = MultiIndex(
1019+
indexes=[c for c in self.columns if c != self.columns.set_ids]
1020+
)
1021+
1022+
min_fc = wf.get_output("min", dpf.types.fields_container)
1023+
max_fc = wf.get_output("max", dpf.types.fields_container)
1024+
1025+
self._last_minmax["min"] = DataFrame(
1026+
data=min_fc,
1027+
index=index,
1028+
columns=columns,
1029+
)
1030+
self._last_minmax["max"] = DataFrame(
1031+
data=max_fc,
1032+
index=index,
1033+
columns=columns,
1034+
)
1035+
self._last_minmax["axis"] = axis

tests/test_dataframe.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pytest import fixture
55

66
from ansys.dpf import post
7+
from ansys.dpf.post import examples
78
from ansys.dpf.post.index import (
89
CompIndex,
910
LabelIndex,
@@ -259,3 +260,38 @@ def test_dataframe_array_raise(transient_rst):
259260
ValueError, match="Can only export to array if the DataFrame contains a single"
260261
):
261262
_ = df.array
263+
264+
265+
def test_dataframe_min_max():
266+
simulation = post.TransientMechanicalSimulation(examples.download_crankshaft())
267+
df = simulation.displacement(all_sets=True)
268+
# Over the mesh entities
269+
min_over_mesh = [[-0.00074732, -0.00040138, -0.00021555]]
270+
assert np.all(np.isclose(df.min()._fc[0].data.tolist(), min_over_mesh))
271+
assert np.all(np.isclose(df.min(axis=0)._fc[0].data.tolist(), min_over_mesh))
272+
assert np.all(
273+
np.isclose(df.min(axis="node_ids")._fc[0].data.tolist(), min_over_mesh)
274+
)
275+
276+
max_over_mesh = [[0.00073303, 0.00139618, 0.00021567]]
277+
assert np.all(np.isclose(df.max()._fc[0].data.tolist(), max_over_mesh))
278+
assert np.all(np.isclose(df.max(axis=0)._fc[0].data.tolist(), max_over_mesh))
279+
assert np.all(
280+
np.isclose(df.max(axis="node_ids")._fc[0].data.tolist(), max_over_mesh)
281+
)
282+
283+
# Over the SetIndex
284+
min_over_time = [-3.41368775e-05, 5.16665595e-04, -4.13456506e-06]
285+
assert np.all(np.isclose(df.min(axis=1)._fc[0].data[0].tolist(), min_over_time))
286+
assert np.all(
287+
np.isclose(df.min(axis="set_ids")._fc[0].data[0].tolist(), min_over_time)
288+
)
289+
max_over_time = [5.67807472e-06, 1.54174694e-03, -2.63976203e-06]
290+
assert np.all(np.isclose(df.max(axis=1)._fc[0].data[0].tolist(), max_over_time))
291+
assert np.all(
292+
np.isclose(df.max(axis="set_ids")._fc[0].data[0].tolist(), max_over_time)
293+
)
294+
295+
# Raise unrecognized axis
296+
with pytest.raises(ValueError, match="is not an available axis value"):
297+
df.max(axis="raises")

0 commit comments

Comments
 (0)