pyvale 2025.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyvale might be problematic. Click here for more details.
- pyvale/__init__.py +75 -0
- pyvale/core/__init__.py +7 -0
- pyvale/core/analyticmeshgen.py +59 -0
- pyvale/core/analyticsimdatafactory.py +63 -0
- pyvale/core/analyticsimdatagenerator.py +160 -0
- pyvale/core/camera.py +146 -0
- pyvale/core/cameradata.py +64 -0
- pyvale/core/cameradata2d.py +82 -0
- pyvale/core/cameratools.py +328 -0
- pyvale/core/cython/rastercyth.c +32267 -0
- pyvale/core/cython/rastercyth.py +636 -0
- pyvale/core/dataset.py +250 -0
- pyvale/core/errorcalculator.py +112 -0
- pyvale/core/errordriftcalc.py +146 -0
- pyvale/core/errorintegrator.py +339 -0
- pyvale/core/errorrand.py +614 -0
- pyvale/core/errorsysdep.py +331 -0
- pyvale/core/errorsysfield.py +407 -0
- pyvale/core/errorsysindep.py +905 -0
- pyvale/core/experimentsimulator.py +99 -0
- pyvale/core/field.py +136 -0
- pyvale/core/fieldconverter.py +154 -0
- pyvale/core/fieldsampler.py +112 -0
- pyvale/core/fieldscalar.py +167 -0
- pyvale/core/fieldtensor.py +221 -0
- pyvale/core/fieldtransform.py +384 -0
- pyvale/core/fieldvector.py +215 -0
- pyvale/core/generatorsrandom.py +528 -0
- pyvale/core/imagedef2d.py +566 -0
- pyvale/core/integratorfactory.py +241 -0
- pyvale/core/integratorquadrature.py +192 -0
- pyvale/core/integratorrectangle.py +88 -0
- pyvale/core/integratorspatial.py +90 -0
- pyvale/core/integratortype.py +44 -0
- pyvale/core/optimcheckfuncs.py +153 -0
- pyvale/core/raster.py +31 -0
- pyvale/core/rastercy.py +76 -0
- pyvale/core/rasternp.py +604 -0
- pyvale/core/rendermesh.py +156 -0
- pyvale/core/sensorarray.py +179 -0
- pyvale/core/sensorarrayfactory.py +210 -0
- pyvale/core/sensorarraypoint.py +280 -0
- pyvale/core/sensordata.py +72 -0
- pyvale/core/sensordescriptor.py +101 -0
- pyvale/core/sensortools.py +143 -0
- pyvale/core/visualexpplotter.py +151 -0
- pyvale/core/visualimagedef.py +71 -0
- pyvale/core/visualimages.py +75 -0
- pyvale/core/visualopts.py +180 -0
- pyvale/core/visualsimanimator.py +83 -0
- pyvale/core/visualsimplotter.py +182 -0
- pyvale/core/visualtools.py +81 -0
- pyvale/core/visualtraceplotter.py +256 -0
- pyvale/data/__init__.py +7 -0
- pyvale/data/case13_out.e +0 -0
- pyvale/data/case16_out.e +0 -0
- pyvale/data/case17_out.e +0 -0
- pyvale/data/case18_1_out.e +0 -0
- pyvale/data/case18_2_out.e +0 -0
- pyvale/data/case18_3_out.e +0 -0
- pyvale/data/case25_out.e +0 -0
- pyvale/data/case26_out.e +0 -0
- pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
- pyvale/examples/__init__.py +7 -0
- pyvale/examples/analyticdatagen/__init__.py +7 -0
- pyvale/examples/analyticdatagen/ex1_1_scalarvisualisation.py +38 -0
- pyvale/examples/analyticdatagen/ex1_2_scalarcasebuild.py +46 -0
- pyvale/examples/analyticdatagen/ex2_1_analyticsensors.py +83 -0
- pyvale/examples/ex1_1_thermal2d.py +89 -0
- pyvale/examples/ex1_2_thermal2d.py +111 -0
- pyvale/examples/ex1_3_thermal2d.py +113 -0
- pyvale/examples/ex1_4_thermal2d.py +89 -0
- pyvale/examples/ex1_5_thermal2d.py +105 -0
- pyvale/examples/ex2_1_thermal3d .py +87 -0
- pyvale/examples/ex2_2_thermal3d.py +51 -0
- pyvale/examples/ex2_3_thermal3d.py +109 -0
- pyvale/examples/ex3_1_displacement2d.py +47 -0
- pyvale/examples/ex3_2_displacement2d.py +79 -0
- pyvale/examples/ex3_3_displacement2d.py +104 -0
- pyvale/examples/ex3_4_displacement2d.py +105 -0
- pyvale/examples/ex4_1_strain2d.py +57 -0
- pyvale/examples/ex4_2_strain2d.py +79 -0
- pyvale/examples/ex4_3_strain2d.py +100 -0
- pyvale/examples/ex5_1_multiphysics2d.py +78 -0
- pyvale/examples/ex6_1_multiphysics2d_expsim.py +118 -0
- pyvale/examples/ex6_2_multiphysics3d_expsim.py +158 -0
- pyvale/examples/features/__init__.py +7 -0
- pyvale/examples/features/ex_animation_tools_3dmonoblock.py +83 -0
- pyvale/examples/features/ex_area_avg.py +89 -0
- pyvale/examples/features/ex_calibration_error.py +108 -0
- pyvale/examples/features/ex_chain_field_errs.py +141 -0
- pyvale/examples/features/ex_field_errs.py +78 -0
- pyvale/examples/features/ex_sensor_single_angle_batch.py +110 -0
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +86 -0
- pyvale/examples/rasterisation/ex_rastenp.py +154 -0
- pyvale/examples/rasterisation/ex_rastercyth_oneframe.py +220 -0
- pyvale/examples/rasterisation/ex_rastercyth_static_cypara.py +194 -0
- pyvale/examples/rasterisation/ex_rastercyth_static_pypara.py +193 -0
- pyvale/simcases/case00_HEX20.i +242 -0
- pyvale/simcases/case00_HEX27.i +242 -0
- pyvale/simcases/case00_TET10.i +242 -0
- pyvale/simcases/case00_TET14.i +242 -0
- pyvale/simcases/case01.i +101 -0
- pyvale/simcases/case02.i +156 -0
- pyvale/simcases/case03.i +136 -0
- pyvale/simcases/case04.i +181 -0
- pyvale/simcases/case05.i +234 -0
- pyvale/simcases/case06.i +305 -0
- pyvale/simcases/case07.geo +135 -0
- pyvale/simcases/case07.i +87 -0
- pyvale/simcases/case08.geo +144 -0
- pyvale/simcases/case08.i +153 -0
- pyvale/simcases/case09.geo +204 -0
- pyvale/simcases/case09.i +87 -0
- pyvale/simcases/case10.geo +204 -0
- pyvale/simcases/case10.i +257 -0
- pyvale/simcases/case11.geo +337 -0
- pyvale/simcases/case11.i +147 -0
- pyvale/simcases/case12.geo +388 -0
- pyvale/simcases/case12.i +329 -0
- pyvale/simcases/case13.i +140 -0
- pyvale/simcases/case14.i +159 -0
- pyvale/simcases/case15.geo +337 -0
- pyvale/simcases/case15.i +150 -0
- pyvale/simcases/case16.geo +391 -0
- pyvale/simcases/case16.i +357 -0
- pyvale/simcases/case17.geo +135 -0
- pyvale/simcases/case17.i +144 -0
- pyvale/simcases/case18.i +254 -0
- pyvale/simcases/case18_1.i +254 -0
- pyvale/simcases/case18_2.i +254 -0
- pyvale/simcases/case18_3.i +254 -0
- pyvale/simcases/case19.geo +252 -0
- pyvale/simcases/case19.i +99 -0
- pyvale/simcases/case20.geo +252 -0
- pyvale/simcases/case20.i +250 -0
- pyvale/simcases/case21.geo +74 -0
- pyvale/simcases/case21.i +155 -0
- pyvale/simcases/case22.geo +82 -0
- pyvale/simcases/case22.i +140 -0
- pyvale/simcases/case23.geo +164 -0
- pyvale/simcases/case23.i +140 -0
- pyvale/simcases/case24.geo +79 -0
- pyvale/simcases/case24.i +123 -0
- pyvale/simcases/case25.geo +82 -0
- pyvale/simcases/case25.i +140 -0
- pyvale/simcases/case26.geo +166 -0
- pyvale/simcases/case26.i +140 -0
- pyvale/simcases/run_1case.py +61 -0
- pyvale/simcases/run_all_cases.py +69 -0
- pyvale/simcases/run_build_case.py +64 -0
- pyvale/simcases/run_example_cases.py +69 -0
- pyvale-2025.4.0.dist-info/METADATA +140 -0
- pyvale-2025.4.0.dist-info/RECORD +157 -0
- pyvale-2025.4.0.dist-info/WHEEL +5 -0
- pyvale-2025.4.0.dist-info/licenses/LICENSE +21 -0
- pyvale-2025.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================================================
|
|
3
|
+
pyvale: the python validation engine
|
|
4
|
+
License: MIT
|
|
5
|
+
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
+
================================================================================
|
|
7
|
+
"""
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import numpy as np
|
|
10
|
+
from pyvale.core.sensorarray import ISensorArray
|
|
11
|
+
import mooseherder as mh
|
|
12
|
+
|
|
13
|
+
# NOTE: This module is a feature under developement.
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ExperimentStats:
|
|
17
|
+
"""Dataclass holding summary statistics for a series of simulated
|
|
18
|
+
experiments.
|
|
19
|
+
"""
|
|
20
|
+
mean: np.ndarray | None = None
|
|
21
|
+
std: np.ndarray | None = None
|
|
22
|
+
cov: np.ndarray | None = None
|
|
23
|
+
max: np.ndarray | None = None
|
|
24
|
+
min: np.ndarray | None = None
|
|
25
|
+
med: np.ndarray | None = None
|
|
26
|
+
q25: np.ndarray | None = None
|
|
27
|
+
q75: np.ndarray | None = None
|
|
28
|
+
mad: np.ndarray | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExperimentSimulator:
|
|
32
|
+
"""An experiment simulator for running monte-carlo analysis by applying a
|
|
33
|
+
list of sensor arrays to a list of simulations over a given number of user
|
|
34
|
+
defined experiments. Calculates summary statistics for each sensor array
|
|
35
|
+
applied to each simulation.
|
|
36
|
+
"""
|
|
37
|
+
__slots__ = ("sim_list","sensor_arrays","num_exp_per_sim","_exp_data",
|
|
38
|
+
"_exp_stats")
|
|
39
|
+
|
|
40
|
+
def __init__(self,
|
|
41
|
+
sim_list: list[mh.SimData],
|
|
42
|
+
sensor_arrays: list[ISensorArray],
|
|
43
|
+
num_exp_per_sim: int
|
|
44
|
+
) -> None:
|
|
45
|
+
|
|
46
|
+
self.sim_list = sim_list
|
|
47
|
+
self.sensor_arrays = sensor_arrays
|
|
48
|
+
self.num_exp_per_sim = num_exp_per_sim
|
|
49
|
+
self._exp_data = [None]*len(self.sensor_arrays)
|
|
50
|
+
self._exp_stats = [None]*len(self.sensor_arrays)
|
|
51
|
+
|
|
52
|
+
def get_data(self) -> list[np.ndarray | None]:
|
|
53
|
+
return self._exp_data
|
|
54
|
+
|
|
55
|
+
def get_stats(self) -> list[np.ndarray | None]:
|
|
56
|
+
return self._exp_stats
|
|
57
|
+
|
|
58
|
+
def run_experiments(self) -> list[np.ndarray]:
|
|
59
|
+
n_sims = len(self.sim_list)
|
|
60
|
+
# shape=list[n_arrays](n_sims,n_exps,n_sens,n_comps,n_time_steps)
|
|
61
|
+
self._exp_data = [None]*len(self.sensor_arrays)
|
|
62
|
+
|
|
63
|
+
for ii,aa in enumerate(self.sensor_arrays):
|
|
64
|
+
meas_array = np.zeros((n_sims,self.num_exp_per_sim)+
|
|
65
|
+
aa.get_measurement_shape())
|
|
66
|
+
|
|
67
|
+
for jj,ss in enumerate(self.sim_list):
|
|
68
|
+
aa.get_field().set_sim_data(ss)
|
|
69
|
+
|
|
70
|
+
for ee in range(self.num_exp_per_sim):
|
|
71
|
+
meas_array[jj,ee,:,:,:] = aa.calc_measurements()
|
|
72
|
+
|
|
73
|
+
self._exp_data[ii] = meas_array
|
|
74
|
+
|
|
75
|
+
return self._exp_data
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def calc_stats(self) -> list[ExperimentStats]:
|
|
79
|
+
# shape=list[n_arrays](n_sims,n_exps,n_sens,n_comps,n_time_steps)
|
|
80
|
+
self._exp_stats = [None]*len(self.sensor_arrays)
|
|
81
|
+
for ii,_ in enumerate(self.sensor_arrays):
|
|
82
|
+
array_stats = ExperimentStats()
|
|
83
|
+
array_stats.max = np.max(self._exp_data[ii],axis=1)
|
|
84
|
+
array_stats.min = np.min(self._exp_data[ii],axis=1)
|
|
85
|
+
array_stats.mean = np.mean(self._exp_data[ii],axis=1)
|
|
86
|
+
array_stats.std = np.std(self._exp_data[ii],axis=1)
|
|
87
|
+
array_stats.med = np.median(self._exp_data[ii],axis=1)
|
|
88
|
+
array_stats.q25 = np.quantile(self._exp_data[ii],0.25,axis=1)
|
|
89
|
+
array_stats.q75 = np.quantile(self._exp_data[ii],0.75,axis=1)
|
|
90
|
+
array_stats.mad = np.median(np.abs(self._exp_data[ii] -
|
|
91
|
+
np.median(self._exp_data[ii],axis=1,keepdims=True)),axis=1)
|
|
92
|
+
self._exp_stats[ii] = array_stats
|
|
93
|
+
|
|
94
|
+
return self._exp_stats
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
pyvale/core/field.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================================================
|
|
3
|
+
pyvale: the python validation engine
|
|
4
|
+
License: MIT
|
|
5
|
+
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
+
================================================================================
|
|
7
|
+
"""
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.spatial.transform import Rotation
|
|
11
|
+
import pyvista as pv
|
|
12
|
+
import mooseherder as mh
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IField(ABC):
|
|
16
|
+
"""Interface (abstract base class) for sampling (interpolating) physical
|
|
17
|
+
fields from simulations to provide sensor values at specified locations and
|
|
18
|
+
times.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def set_sim_data(self,sim_data: mh.SimData) -> None:
|
|
23
|
+
"""Abstract method. Sets the SimData object that will be interpolated to
|
|
24
|
+
obtain sensor values. The purpose of this is to be able to apply the
|
|
25
|
+
same sensor array to an array of different simulations.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
sim_data : mh.SimData
|
|
30
|
+
Mooseherder SimData object. Contains a mesh and a simulated
|
|
31
|
+
physical field.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_sim_data(self) -> mh.SimData:
|
|
37
|
+
"""Abstract method. Gets the simulation data object associated with this
|
|
38
|
+
field. Used by pyvale visualisation tools to display simulation data
|
|
39
|
+
with simulated sensor values.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
mh.SimData
|
|
44
|
+
Mooseherder SimData object. Contains a mesh and a simulated
|
|
45
|
+
physical field.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get_time_steps(self) -> np.ndarray:
|
|
51
|
+
"""Abstract method. Gets a 1D array of time steps from the simulation
|
|
52
|
+
data.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
np.ndarray
|
|
57
|
+
1D array of simulation time steps. shape=(num_time_steps,)
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def get_visualiser(self) -> pv.UnstructuredGrid:
|
|
63
|
+
"""Abstract method. Gets a pyvista unstructured grid object for
|
|
64
|
+
visualisation purposes.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
pv.UnstructuredGrid
|
|
69
|
+
Pyvista unstructured grid object containing only a mesh without any
|
|
70
|
+
physical field data attached.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_all_components(self) -> tuple[str,...]:
|
|
76
|
+
"""Gets the string keys for the component of the physical field. For
|
|
77
|
+
example: a scalar field might just have ('temperature',) whereas a
|
|
78
|
+
vector field might have ('disp_x','disp_y','disp_z').
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
tuple[str,...]
|
|
83
|
+
Tuple containing the string keys for all components of the physical
|
|
84
|
+
field.
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def get_component_index(self,component: str) -> int:
|
|
90
|
+
"""Gets the index for a component of the physical field. Used for
|
|
91
|
+
getting the index of a component in the sensor measurement array.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
component : str
|
|
96
|
+
String key for the field component (e.g. 'temperature' or 'disp_x').
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
int
|
|
101
|
+
Index for the selected field component
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def sample_field(self,
|
|
107
|
+
points: np.ndarray,
|
|
108
|
+
times: np.ndarray | None = None,
|
|
109
|
+
angles: tuple[Rotation,...] | None = None,
|
|
110
|
+
) -> np.ndarray:
|
|
111
|
+
"""Samples (interpolates) the simulation field at the specified
|
|
112
|
+
positions, times, and angles.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
points : np.ndarray
|
|
117
|
+
Spatial points to be sampled with the rows indicating the point
|
|
118
|
+
number of the columns indicating the X,Y and Z coordinates.
|
|
119
|
+
times : np.ndarray | None, optional
|
|
120
|
+
Times to sample the underlying simulation. If None then the
|
|
121
|
+
simulation time steps are used and no temporal interpolation is
|
|
122
|
+
performed, by default None.
|
|
123
|
+
angles : tuple[Rotation,...] | None, optional
|
|
124
|
+
Angles to rotate the sampled values into with rotations specified
|
|
125
|
+
with respect to the simulation world coordinates. If a single
|
|
126
|
+
rotation is specified then all points are assumed to have the same
|
|
127
|
+
angle and are batch processed for speed. If None then no rotation is
|
|
128
|
+
performed, by default None.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
np.ndarray
|
|
133
|
+
An array of sampled (interpolated) values with the following
|
|
134
|
+
dimensions: shape=(num_points,num_components,num_time_steps).
|
|
135
|
+
"""
|
|
136
|
+
pass
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================================================
|
|
3
|
+
pyvale: the python validation engine
|
|
4
|
+
License: MIT
|
|
5
|
+
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
+
================================================================================
|
|
7
|
+
"""
|
|
8
|
+
import warnings
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pyvista as pv
|
|
11
|
+
from pyvista import CellType
|
|
12
|
+
import mooseherder as mh
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def simdata_to_pyvista(sim_data: mh.SimData,
|
|
16
|
+
components: tuple[str,...] | None,
|
|
17
|
+
spat_dim: int
|
|
18
|
+
) -> tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]:
|
|
19
|
+
"""Converts the mesh and field data in a `SimData` object into a pyvista
|
|
20
|
+
UnstructuredGrid for sampling (interpolating) the data and visualisation.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
sim_data : mh.SimData
|
|
25
|
+
Object containing a mesh and associated field data from a simulation.
|
|
26
|
+
components : tuple[str,...] | None
|
|
27
|
+
String keys for the components of the field to extract from the
|
|
28
|
+
simulation data.
|
|
29
|
+
spat_dim : int
|
|
30
|
+
Number of spatial dimensions (2 or 3) used to determine the element
|
|
31
|
+
types in the mesh from the number of nodes per element.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]
|
|
36
|
+
The first UnstructuredGrid has the field components attached as dataset
|
|
37
|
+
arrays. The second has no field data attached for visualisation.
|
|
38
|
+
"""
|
|
39
|
+
flat_connect = np.array([],dtype=np.int64)
|
|
40
|
+
cell_types = np.array([],dtype=np.int64)
|
|
41
|
+
|
|
42
|
+
for cc in sim_data.connect:
|
|
43
|
+
# NOTE: need the -1 here to make element numbers 0 indexed!
|
|
44
|
+
this_connect = np.copy(sim_data.connect[cc])-1
|
|
45
|
+
(nodes_per_elem,n_elems) = this_connect.shape
|
|
46
|
+
|
|
47
|
+
this_cell_type = _get_pyvista_cell_type(nodes_per_elem,spat_dim)
|
|
48
|
+
|
|
49
|
+
# VTK and exodus have different winding for 3D higher order quads
|
|
50
|
+
this_connect = _exodus_to_pyvista_connect(this_cell_type,this_connect)
|
|
51
|
+
|
|
52
|
+
this_connect = this_connect.T.flatten()
|
|
53
|
+
idxs = np.arange(0,n_elems*nodes_per_elem,nodes_per_elem,dtype=np.int64)
|
|
54
|
+
|
|
55
|
+
this_connect = np.insert(this_connect,idxs,nodes_per_elem)
|
|
56
|
+
|
|
57
|
+
cell_types = np.hstack((cell_types,np.full(n_elems,this_cell_type)))
|
|
58
|
+
flat_connect = np.hstack((flat_connect,this_connect),dtype=np.int64)
|
|
59
|
+
|
|
60
|
+
cells = flat_connect
|
|
61
|
+
|
|
62
|
+
points = sim_data.coords
|
|
63
|
+
pv_grid = pv.UnstructuredGrid(cells, cell_types, points)
|
|
64
|
+
pv_grid_vis = pv.UnstructuredGrid(cells, cell_types, points)
|
|
65
|
+
|
|
66
|
+
if components is not None and sim_data.node_vars is not None:
|
|
67
|
+
for cc in components:
|
|
68
|
+
pv_grid[cc] = sim_data.node_vars[cc]
|
|
69
|
+
|
|
70
|
+
return (pv_grid,pv_grid_vis)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_pyvista_cell_type(nodes_per_elem: int, spat_dim: int) -> CellType:
|
|
74
|
+
"""Helper function to identify the pyvista element type in the mesh.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
nodes_per_elem : int
|
|
79
|
+
Number of nodes per element.
|
|
80
|
+
spat_dim : int
|
|
81
|
+
Number of spatial dimensions in the mesh (2 or 3).
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
CellType
|
|
86
|
+
Enumeration describing the element type in pyvista.
|
|
87
|
+
"""
|
|
88
|
+
cell_type = 0
|
|
89
|
+
|
|
90
|
+
if spat_dim == 2:
|
|
91
|
+
if nodes_per_elem == 4:
|
|
92
|
+
cell_type = CellType.QUAD
|
|
93
|
+
elif nodes_per_elem == 3:
|
|
94
|
+
cell_type = CellType.TRIANGLE
|
|
95
|
+
elif nodes_per_elem == 6:
|
|
96
|
+
cell_type = CellType.QUADRATIC_TRIANGLE
|
|
97
|
+
elif nodes_per_elem == 7:
|
|
98
|
+
cell_type = CellType.BIQUADRATIC_TRIANGLE
|
|
99
|
+
elif nodes_per_elem == 8:
|
|
100
|
+
cell_type = CellType.QUADRATIC_QUAD
|
|
101
|
+
elif nodes_per_elem == 9:
|
|
102
|
+
cell_type = CellType.BIQUADRATIC_QUAD
|
|
103
|
+
else:
|
|
104
|
+
warnings.warn(f"Cell type 2D with {nodes_per_elem} "
|
|
105
|
+
+ "nodes not recognised. Defaulting to 4 node QUAD")
|
|
106
|
+
cell_type = CellType.QUAD
|
|
107
|
+
else:
|
|
108
|
+
if nodes_per_elem == 8:
|
|
109
|
+
cell_type = CellType.HEXAHEDRON
|
|
110
|
+
elif nodes_per_elem == 4:
|
|
111
|
+
cell_type = CellType.TETRA
|
|
112
|
+
elif nodes_per_elem == 10:
|
|
113
|
+
cell_type = CellType.QUADRATIC_TETRA
|
|
114
|
+
elif nodes_per_elem == 20:
|
|
115
|
+
cell_type = CellType.QUADRATIC_HEXAHEDRON
|
|
116
|
+
elif nodes_per_elem == 27:
|
|
117
|
+
cell_type = CellType.TRIQUADRATIC_HEXAHEDRON
|
|
118
|
+
else:
|
|
119
|
+
warnings.warn(f"Cell type 3D with {nodes_per_elem} "
|
|
120
|
+
+ "nodes not recognised. Defaulting to 8 node HEX")
|
|
121
|
+
cell_type = CellType.HEXAHEDRON
|
|
122
|
+
|
|
123
|
+
return cell_type
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _exodus_to_pyvista_connect(cell_type: CellType, connect: np.ndarray) -> np.ndarray:
|
|
127
|
+
copy_connect = np.copy(connect)
|
|
128
|
+
|
|
129
|
+
# NOTE: it looks like VTK does not support TET14
|
|
130
|
+
# VTK and exodus have different winding for 3D higher order quads
|
|
131
|
+
if cell_type == CellType.QUADRATIC_HEXAHEDRON:
|
|
132
|
+
connect[12:16,:] = copy_connect[16:20,:]
|
|
133
|
+
connect[16:20,:] = copy_connect[12:16,:]
|
|
134
|
+
elif cell_type == CellType.TRIQUADRATIC_HEXAHEDRON:
|
|
135
|
+
connect[12:16,:] = copy_connect[16:20,:]
|
|
136
|
+
connect[16:20,:] = copy_connect[12:16,:]
|
|
137
|
+
connect[20:24,:] = copy_connect[23:27,:]
|
|
138
|
+
connect[24,:] = copy_connect[21,:]
|
|
139
|
+
connect[25,:] = copy_connect[22,:]
|
|
140
|
+
connect[26,:] = copy_connect[20,:]
|
|
141
|
+
|
|
142
|
+
return connect
|
|
143
|
+
|
|
144
|
+
def scale_length_units(sim_data: mh.SimData,
|
|
145
|
+
disp_comps: tuple[str,...],
|
|
146
|
+
scale: float) -> mh.SimData:
|
|
147
|
+
|
|
148
|
+
sim_data.coords = sim_data.coords*scale
|
|
149
|
+
for cc in disp_comps:
|
|
150
|
+
sim_data.node_vars[cc] = sim_data.node_vars[cc]*scale
|
|
151
|
+
|
|
152
|
+
return sim_data
|
|
153
|
+
|
|
154
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================================================
|
|
3
|
+
pyvale: the python validation engine
|
|
4
|
+
License: MIT
|
|
5
|
+
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
+
================================================================================
|
|
7
|
+
"""
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pyvista as pv
|
|
10
|
+
from pyvale.core.field import IField
|
|
11
|
+
from pyvale.core.sensordata import SensorData
|
|
12
|
+
from pyvale.core.integratorfactory import build_spatial_averager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sample_field_with_sensor_data(field: IField, sensor_data: SensorData
|
|
16
|
+
) -> np.ndarray:
|
|
17
|
+
"""Samples (interpolates) an `IField` object using the parameters specified
|
|
18
|
+
in the `SensorData` object.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
field : IField
|
|
23
|
+
The simulated physical field that the sensors will samples from. This is
|
|
24
|
+
normally a `FieldScalar`, `FieldVector` or `FieldTensor`.
|
|
25
|
+
sensor_data : SensorData
|
|
26
|
+
Contains sensor array parameters including: number of sensors, positions
|
|
27
|
+
and sample times. See the `SensorData` class for more information.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
np.ndarray
|
|
32
|
+
Array of sampled sensor measurements with shape=(num_sensors,
|
|
33
|
+
num_field_components,num_time_steps).
|
|
34
|
+
"""
|
|
35
|
+
if sensor_data.spatial_averager is None:
|
|
36
|
+
return field.sample_field(sensor_data.positions,
|
|
37
|
+
sensor_data.sample_times,
|
|
38
|
+
sensor_data.angles)
|
|
39
|
+
|
|
40
|
+
spatial_integrator = build_spatial_averager(field,sensor_data)
|
|
41
|
+
return spatial_integrator.calc_averages()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# NOTE: sampling outside the bounds of the sample returns a value of 0
|
|
45
|
+
def sample_pyvista_grid(components: tuple[str,...],
|
|
46
|
+
pyvista_grid: pv.UnstructuredGrid,
|
|
47
|
+
sim_time_steps: np.ndarray,
|
|
48
|
+
points: np.ndarray,
|
|
49
|
+
sample_times: np.ndarray | None = None
|
|
50
|
+
) -> np.ndarray:
|
|
51
|
+
"""Function for sampling (interpolating) a pyvista grid object containing
|
|
52
|
+
simulated field data. The pyvista sample method uses VTK to perform the
|
|
53
|
+
spatial interpolation using the element shape functions. If the sampling
|
|
54
|
+
time steps are not the same as the simulation time then a linear
|
|
55
|
+
interpolation over time is performed using numpy.
|
|
56
|
+
|
|
57
|
+
NOTE: sampling outside the mesh bounds of the sample returns a value of 0.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
components : tuple[str,...]
|
|
62
|
+
String keys for the components to be sampled in the pyvista grid object.
|
|
63
|
+
Useful for only interpolating the field components of interest for speed
|
|
64
|
+
and memory reduction.
|
|
65
|
+
pyvista_grid : pv.UnstructuredGrid
|
|
66
|
+
Pyvista grid object containing the simulation mesh and the components of
|
|
67
|
+
the physical field that will be sampled.
|
|
68
|
+
sim_time_steps : np.ndarray
|
|
69
|
+
Simulation time steps corresponding to the fields in the pyvista grid
|
|
70
|
+
object.
|
|
71
|
+
points : np.ndarray
|
|
72
|
+
Coordinates of the points at which to sample the pyvista grid object.
|
|
73
|
+
shape=(num_points,3) where the columns are the X, Y and Z coordinates of
|
|
74
|
+
the sample points in simulation world coordintes.
|
|
75
|
+
sample_times : np.ndarray | None, optional
|
|
76
|
+
Array of time steps at which to sample the pyvista grid. If None then no
|
|
77
|
+
temporal interpolation is performed and the sample times are assumed to
|
|
78
|
+
be the simulation time steps.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
np.ndarray
|
|
83
|
+
Array of sampled sensor measurements with shape=(num_sensors,
|
|
84
|
+
num_field_components,num_time_steps).
|
|
85
|
+
"""
|
|
86
|
+
pv_points = pv.PolyData(points)
|
|
87
|
+
sample_data = pv_points.sample(pyvista_grid)
|
|
88
|
+
|
|
89
|
+
n_comps = len(components)
|
|
90
|
+
(n_sensors,n_time_steps) = np.array(sample_data[components[0]]).shape
|
|
91
|
+
sample_at_sim_time = np.empty((n_sensors,n_comps,n_time_steps))
|
|
92
|
+
|
|
93
|
+
for ii,cc in enumerate(components):
|
|
94
|
+
sample_at_sim_time[:,ii,:] = np.array(sample_data[cc])
|
|
95
|
+
|
|
96
|
+
if sample_times is None:
|
|
97
|
+
return sample_at_sim_time
|
|
98
|
+
|
|
99
|
+
def sample_time_interp(x):
|
|
100
|
+
return np.interp(sample_times, sim_time_steps, x)
|
|
101
|
+
|
|
102
|
+
n_time_steps = sample_times.shape[0]
|
|
103
|
+
sample_at_spec_time = np.empty((n_sensors,n_comps,n_time_steps))
|
|
104
|
+
|
|
105
|
+
for ii,cc in enumerate(components):
|
|
106
|
+
sample_at_spec_time[:,ii,:] = np.apply_along_axis(sample_time_interp,-1,
|
|
107
|
+
sample_at_sim_time[:,ii,:])
|
|
108
|
+
|
|
109
|
+
return sample_at_spec_time
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================================================
|
|
3
|
+
pyvale: the python validation engine
|
|
4
|
+
License: MIT
|
|
5
|
+
Copyright (C) 2025 The Computer Aided Validation Team
|
|
6
|
+
================================================================================
|
|
7
|
+
"""
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pyvista as pv
|
|
10
|
+
from scipy.spatial.transform import Rotation
|
|
11
|
+
import mooseherder as mh
|
|
12
|
+
|
|
13
|
+
from pyvale.core.field import IField
|
|
14
|
+
from pyvale.core.fieldconverter import simdata_to_pyvista
|
|
15
|
+
from pyvale.core.fieldsampler import sample_pyvista_grid
|
|
16
|
+
|
|
17
|
+
class FieldScalar(IField):
|
|
18
|
+
"""Class for sampling (interpolating) scalar fields from simulations to
|
|
19
|
+
provide sensor values at specified locations and times.
|
|
20
|
+
|
|
21
|
+
Implements the `IField` interface.
|
|
22
|
+
"""
|
|
23
|
+
__slots__ = ("_field_key","_spat_dims","_sim_data","_pyvista_grid",
|
|
24
|
+
"_pyvista_vis")
|
|
25
|
+
|
|
26
|
+
def __init__(self,
|
|
27
|
+
sim_data: mh.SimData,
|
|
28
|
+
field_key: str,
|
|
29
|
+
spat_dims: int) -> None:
|
|
30
|
+
"""Initialiser for the `FieldScalar` class.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
sim_data : mh.SimData
|
|
35
|
+
Simulation data object containing the mesh and field to interpolate.
|
|
36
|
+
field_key : str
|
|
37
|
+
String key for the scalar field component in the `SimData` object.
|
|
38
|
+
spat_dims : int
|
|
39
|
+
Number of spatial dimensions (2 or 3) used for identifying element
|
|
40
|
+
types.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self._field_key = field_key
|
|
44
|
+
self._spat_dims = spat_dims
|
|
45
|
+
|
|
46
|
+
self._sim_data = sim_data
|
|
47
|
+
(self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
|
|
48
|
+
self._sim_data,
|
|
49
|
+
(self._field_key,),
|
|
50
|
+
self._spat_dims
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def set_sim_data(self, sim_data: mh.SimData) -> None:
|
|
54
|
+
"""Sets the `SimData` object that will be interpolated to obtain sensor
|
|
55
|
+
values. The purpose of this is to be able to apply the same sensor array
|
|
56
|
+
to an array of different simulations by setting a different `SimData`.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
sim_data : mh.SimData
|
|
61
|
+
Mooseherder SimData object. Contains a mesh and a simulated
|
|
62
|
+
physical field.
|
|
63
|
+
"""
|
|
64
|
+
self._sim_data = sim_data
|
|
65
|
+
(self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
|
|
66
|
+
sim_data,
|
|
67
|
+
(self._field_key,),
|
|
68
|
+
self._spat_dims
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_sim_data(self) -> mh.SimData:
|
|
72
|
+
"""Gets the simulation data object associated with this field. Used by
|
|
73
|
+
pyvale visualisation tools to display simulation data with simulated
|
|
74
|
+
sensor values.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
mh.SimData
|
|
79
|
+
Mooseherder SimData object. Contains a mesh and a simulated
|
|
80
|
+
physical field.
|
|
81
|
+
"""
|
|
82
|
+
return self._sim_data
|
|
83
|
+
|
|
84
|
+
def get_time_steps(self) -> np.ndarray:
|
|
85
|
+
"""Gets a 1D array of time steps from the simulation data.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
np.ndarray
|
|
90
|
+
1D array of simulation time steps. shape=(num_time_steps,)
|
|
91
|
+
"""
|
|
92
|
+
return self._sim_data.time
|
|
93
|
+
|
|
94
|
+
def get_visualiser(self) -> pv.UnstructuredGrid:
|
|
95
|
+
"""Gets a pyvista unstructured grid object for visualisation purposes.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
pv.UnstructuredGrid
|
|
100
|
+
Pyvista unstructured grid object containing only a mesh without any
|
|
101
|
+
physical field data attached.
|
|
102
|
+
"""
|
|
103
|
+
return self._pyvista_vis
|
|
104
|
+
|
|
105
|
+
def get_all_components(self) -> tuple[str, ...]:
|
|
106
|
+
"""Gets the string key for the component of the physical field. A scalar
|
|
107
|
+
field only has a single component so a tuple of length 1 is returned.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
tuple[str,...]
|
|
112
|
+
Tuple containing the string key for the physical field.
|
|
113
|
+
"""
|
|
114
|
+
return (self._field_key,)
|
|
115
|
+
|
|
116
|
+
def get_component_index(self, comp: str) -> int:
|
|
117
|
+
"""Gets the index for a component of the physical field. Used for
|
|
118
|
+
getting the index of a component in the sensor measurement array.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
component : str
|
|
123
|
+
String key for the field component (e.g. 'temperature' or 'disp_x').
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
int
|
|
128
|
+
Index for the selected field component
|
|
129
|
+
"""
|
|
130
|
+
return 0 # scalar fields only have one component!
|
|
131
|
+
|
|
132
|
+
def sample_field(self,
|
|
133
|
+
points: np.ndarray,
|
|
134
|
+
times: np.ndarray | None = None,
|
|
135
|
+
angles: tuple[Rotation,...] | None = None,
|
|
136
|
+
) -> np.ndarray:
|
|
137
|
+
"""Samples (interpolates) the simulation field at the specified
|
|
138
|
+
positions, times, and angles.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
points : np.ndarray
|
|
143
|
+
Spatial points to be sampled with the rows indicating the point
|
|
144
|
+
number of the columns indicating the X,Y and Z coordinates.
|
|
145
|
+
times : np.ndarray | None, optional
|
|
146
|
+
Times to sample the underlying simulation. If None then the
|
|
147
|
+
simulation time steps are used and no temporal interpolation is
|
|
148
|
+
performed, by default None.
|
|
149
|
+
angles : tuple[Rotation,...] | None, optional
|
|
150
|
+
Angles to rotate the sampled values into with rotations specified
|
|
151
|
+
with respect to the simulation world coordinates. If a single
|
|
152
|
+
rotation is specified then all points are assumed to have the same
|
|
153
|
+
angle and are batch processed for speed. If None then no rotation is
|
|
154
|
+
performed, by default None.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
np.ndarray
|
|
159
|
+
An array of sampled (interpolated) values with the following
|
|
160
|
+
dimensions: shape=(num_points,num_components,num_time_steps).
|
|
161
|
+
"""
|
|
162
|
+
return sample_pyvista_grid((self._field_key,),
|
|
163
|
+
self._pyvista_grid,
|
|
164
|
+
self._sim_data.time,
|
|
165
|
+
points,
|
|
166
|
+
times)
|
|
167
|
+
|