"""
The objective module contains the classes related to objectives.
This includes the metric functions, the base objectives, specialized
objectives, objective sets. User facing classes only include metric functions
and objectives.
"""
from matcal.core.objective import Objective
from matcal.core.qoi_extractor import (DataSpecificExtractorWrapper,
StateSpecificExtractorWrapper)
from matcal.core.utilities import check_item_is_correct_type
from matcal.full_field.data_importer import mesh_file_to_skeleton
from matcal.full_field.qoi_extractor import (ExternalVirtualPowerExtractor,
FieldTimeInterpolatorExtractor,
HWDColocatingExperimentSurfaceExtractor,
HWDExperimentSurfaceExtractor,
HWDPolynomialSimulationSurfaceExtractor,
InternalVirtualPowerExtractor,
MeshlessSpaceInterpolatorExtractor)
from matcal.full_field.TwoDimensionalFieldGrid import MeshSkeleton
from matcal.core.logger import initialize_matcal_logger
logger = initialize_matcal_logger(__name__)
# class SimToExpInterpolatedFullFieldObjective(Objective):
# def __init__(self, fem_mesh_file, fem_surface, time_variable, *dependent_fields):
# super().__init__(*dependent_fields)
# self._mesh_file = fem_mesh_file
# self._mesh_surface = fem_surface
# self._time_variable = time_variable
# self.set_experiment_qoi_extractor(FlattenFieldDataExtractor())
# def data_specific_initialization(self, exp_data_collection):
# sim_extractor = DataSpecificExtractorWrapperSimToExpObj()
# for state in exp_data_collection:
# for current_data in exp_data_collection[state]:
# field_extractor = self._create_field_extractor(self._mesh_file, self._mesh_surface, current_data.skeleton, self._time_variable)
# self._number_of_nodes = field_extractor.number_of_nodes_required
# sim_extractor.add(current_data, field_extractor)
# self.set_simulation_qoi_extractor(sim_extractor)
# def _create_field_extractor(self, fem_mesh_file, fem_surface, cloud_skeleton, time_variable):
# fem_skeleton = mesh_file_to_skeleton(fem_mesh_file, fem_surface)
# field_extractor = FieldInterpolatorExtractor(fem_skeleton, cloud_skeleton.spatial_coords, time_variable)
# return field_extractor
# def _initialize_results(self):
# results = ObjectiveResults(
# self._required_fields, self.fields_of_interest)
# results.set_as_large_data_sets_objective()
# return results
[docs]
class InterpolatedFullFieldObjective(Objective):
"""
The InterpolatedFullFieldObjective class handles the calculation
of the residual vector and merit functions when
comparing point data extracted from 2D surfaces for a
:class:`~matcal.core.study_base.StudyBase` evaluation set.
See :meth:`~matcal.core.study_base.StudyBase.add_evaluation_set`.
"""
_class_name = "InterpolatedFullFieldObjective"
def __init__(self, fem_mesh_file, *dependent_fields, time_variable="time", fem_surface=None):
"""
Specify the geometry and fields of interest for the full-filed objective.
For full-field objectives it is expected that each frame of
data is parameterized by an independent time field.
While it does not need to be called time, some field must
characterize the order of each frame of full-field data.
A frame of data consists of the field values
(temperature, displacement, etc.) of all the relevant points for the data set.
This method allows the user to specify these fields for the objective.
This objective will interpolate the experimental
data in space on to the simulation data points, and will interpolate
the simulation data in time to align with the
experimental time stamps.
This method uses GMLS to interpolate in space. There are two
meta-parameters that define this interpolation, polynomial order
and search radius multiplier. By default these values are
both set to 2, but can me modified. Any modifications should be performed
before any objective evaluations occur, because changes to
these parameters will not back propagate.
:param fem_mesh_file: file path pointing to the mesh used for simulation.
:type independent_field: str
:param dependent_fields: the dependent fields for the objective
from as an unpacked list.
:type dependent_fields: str
:param time_variable: The name of the time field used to
parameterize the frames of full-field data
:type time_variable: str
:param fem_surface: the name of the surface to extract the data
points off of from a mesh if simulation
returns more than just the required surface.
:type fem_surface: str
"""
super().__init__(*dependent_fields)
self._mesh_file = fem_mesh_file
self._mesh_surface = fem_surface
self._time_variable = time_variable
self._polynomial_order = 2
self._search_radius_multiplier = 2
mesh_skeleton = mesh_file_to_skeleton(fem_mesh_file, fem_surface)
sim_qoi_extractor = FieldTimeInterpolatorExtractor(mesh_skeleton,
time_variable)
self.set_simulation_qoi_extractor(sim_qoi_extractor)
self.set_as_large_data_sets_objective()
[docs]
def set_interpolation_parameters(self, polynomial_order:int, search_radius_multiplier:float):
"""
Set the interpolation/extrapolation meta-parameters. It is recommend
that if much extrapolation is expected that the polynomial order
used be kept low(<=2).
:param polynomial_order: Value used to indicate the polynomial
order used by GMLS to determine new values.
:type polynomial_order: int
:param search_radius_multiplier: Value used to gather more
points past the minimum required for the determination of a polynomial.
This value should always be greater than 1. Larger values will
tend to smooth out the interpolation while smaller values will allow for
sharper changes in value.
:type search_radius_multiplier: float
"""
self._polynomial_order = polynomial_order
self._search_radius_multiplier = search_radius_multiplier
[docs]
def data_specific_initialization(self, exp_data_collection):
"""
Public method used by this class to correctly incorporate experimental
data into the initialization of the objective.
This is a method meant for use inside of MatCal and is not intended
to be used by users.
:param exp_data_collection: the :class:`~matcal.core.data.DataCollection`
containing the relevant experimental data that this
objective will evaluate.
:type exp_data_collection: :class:`~matcal.core.data.DataCollection`
"""
exp_extractor = DataSpecificExtractorWrapper()
for state in exp_data_collection:
for current_data in exp_data_collection[state]:
field_extractor = self._create_field_extractor(self._mesh_file,
self._mesh_surface,
current_data.skeleton,
self._time_variable)
exp_extractor.add(current_data, field_extractor)
self.set_experiment_qoi_extractor(exp_extractor)
def _create_field_extractor(self, fem_mesh_file, fem_surface, cloud_skeleton, time_variable):
fem_skeleton = mesh_file_to_skeleton(fem_mesh_file, fem_surface)
field_extractor = MeshlessSpaceInterpolatorExtractor(cloud_skeleton.spatial_coords[:,:2],
fem_skeleton.spatial_coords[:,:2],
time_variable, self._polynomial_order,
self._search_radius_multiplier)
return field_extractor
[docs]
class PolynomialHWDObjective(Objective):
"""
The PolynomialHWDObjective class handles the calculation of
the residual vector and merit functions when
comparing point data extracted from 2D surfaces for a study evaluation set.
See :meth:`~matcal.core.study_base.StudyBase.add_evaluation_set`.
PolynomialHWDObjective defines its objective in a latent space.
The latent space represents the raw point data
by weights of different basis modes. These modes represent different
patterns the data can take over
different parts of the domain. This method can support compression of
the full-field data, allowing it to have
a much smaller memory foot print than may be required by other methods.
These modes are formed by performing a QR factorization on a moment matrix.
This factorization provides a basis matrix(Q)
and a change of basis matrix(R). These matrices are used to convert the raw
data tot the latent space and to map different latent spaces to each other.
This method generates its moment matrix by generating polynomials across
different tiers of subdividing the spatial domain.
each tier is a binary split of the last tier, creating a domain decomposition
tree, where depth d of the tree has 2^d subdivisions within it.
The deeper the tree the more local information can be captured by the objective.
Currently the default behavior for this objective involves a colocation
step of the experimental data to the simulation data points.
This step may not be necessary in the future as partitioning
methods for HWD become better.
"""
_class_name = "PolynomialHWDObjective"
def __init__(self, target_coords, *dependent_fields, time_variable:str="time", max_depth:int=6, polynomial_order:int=8):
"""
Specify the geometry and fields of interest for the full-filed objective.
For full-field objectives it is expected that each frame
of data is parameterized by an independent time field.
While it does not need to be called time, some field must
characterize the order of each frame of full-field data.
A frame of data consists of the field values
(temperature, displacement, etc.) of all the
relevant points for the data set.
This method allows the user to specify these fields for the objective.
This objective will use polynomial HWD to compare
experimental data and simulation data in a latent space.
To align the data points in time the simulation
data will interpolated in time to match the experimental time stamps.
There are two meta-parameters that define the latent
space definition, polynomial order and maximum tree depth.
polynomial order changes the polynomial order used to
generate the moment matrix, and the maximum tree depth dictates how
deep of a binary tree can be generated for a given domain.
:param target_coords: two-dimensional array containing
the points to be colocated to to generate a
more consistent set of basis modes. If None is passed
colocation will be skipped. non-colocated HWD
is still in beta at this time an may have unpredictable performance.
:type independent_field: numpy.array or None
:param dependent_fields: the dependent fields for the objective.
:type dependent_fields: list(str)
:param time_variable: The name of the time field
used to parameterize the frames of full-field data
:type time_variable: str
:param max_depth: Specify the maximum depth of binary tree to make.
Value must be 0 or greater.
Note that the number of subdivisions grow geometrically,
and that depths greater than 7 can
take a long time to process.
:type fem_surface: int
:param polynomial_order: Specify the order of polynomial used
to generate the moment matrix. value must be 0 or greater.
:type polynomial_order: int
"""
super().__init__(*dependent_fields)
self._max_depth = max_depth
self._polynomial_order = polynomial_order
self._time_variable = time_variable
self._surface_name = None
self._target_coords = None
self._colocate_exp_data = False
if target_coords is None:
logger.info("WARNING:: Using not co-located HWD, this is pre-beta. "
"Pass a mesh file to use co-location.")
else:
self._set_colocation(target_coords, max_depth=max_depth,
polynomial_order=polynomial_order)
self.set_as_large_data_sets_objective()
def _set_colocation(self, target_coords, max_depth:int=6,
polynomial_order:int=8):
self._target_coords = target_coords
self._polynomial_order = polynomial_order
self._max_depth = max_depth
self._colocate_exp_data = True
[docs]
def data_specific_initialization(self, exp_data_collection):
"""
Public method used by this class to correctly incorporate experimental
data into the initialization of the objective.
This is a method meant for use inside of MatCal and
is not intended to be used by users.
:param exp_data_collection: the :class:`~matcal.core.data.DataCollection`
containing the relevant experimental data that this
objective will evaluate.
:type exp_data_collection: :class:`~matcal.core.data.DataCollection`
"""
sim_extractor = StateSpecificExtractorWrapper()
exp_extractor = StateSpecificExtractorWrapper()
for state in exp_data_collection.states:
first_data = exp_data_collection[state][0]
if self._colocate_exp_data:
state_extractors = self._set_up_colocated(first_data)
else:
state_extractors = self._set_up_mapping(first_data)
state_sim_field_extractor, state_exp_field_extractor = state_extractors
sim_extractor.add(first_data, state_sim_field_extractor)
exp_extractor.add(first_data, state_exp_field_extractor)
self.set_simulation_qoi_extractor(sim_extractor)
self.set_experiment_qoi_extractor(exp_extractor)
def _set_up_mapping(self, first_data):
state_sim_field_extractor = HWDPolynomialSimulationSurfaceExtractor(first_data.skeleton,
self._max_depth,
self._polynomial_order,
self._time_variable)
self._add_surface(state_sim_field_extractor)
state_exp_field_extractor = HWDExperimentSurfaceExtractor(state_sim_field_extractor)
return state_sim_field_extractor,state_exp_field_extractor
def _set_up_colocated(self, first_data):
sim_skeleton = self._get_simulation_points()
state_sim_field_extractor = HWDPolynomialSimulationSurfaceExtractor(sim_skeleton,
self._max_depth,
self._polynomial_order,
self._time_variable)
self._add_surface(state_sim_field_extractor)
state_exp_field_extractor = HWDColocatingExperimentSurfaceExtractor(state_sim_field_extractor,
first_data.skeleton.spatial_coords[:,:2],
sim_skeleton.spatial_coords[:,:2])
return state_sim_field_extractor, state_exp_field_extractor
def _add_surface(self, state_sim_field_extractor):
if self._surface_name is not None:
state_sim_field_extractor.extract_cloud_from_mesh(self._surface_name)
def _get_simulation_points(self):
if not isinstance(self._target_coords, str):
sim_skeleton = MeshSkeleton()
if self._surface_name is not None:
sp = self._target_coords.surfaces[self._surface_name]
sim_skeleton.spatial_coords = self._target_coords.spatial_coords[sp, :]
else:
sim_skeleton.spatial_coords = self._target_coords.spatial_coords
else:
sim_skeleton = mesh_file_to_skeleton(self._target_coords, self._surface_name)
return sim_skeleton
def extract_data_from_mesh_surface(self, surface_name):
self._surface_name = surface_name
[docs]
class MechanicalVFMObjective(Objective):
"""
The MechanicalVFMObjective class handles the calculation of
the residual vector and merit functions when
calibrating a material model using the :ref:`Virtual Fields Method`
and full-field displacement data from
an experiment. It must be combined with one of MatCal's
VFM models which require a :class:`~matcal.full_field.data.FieldData` class
object for the boundary condition data.
The data passed to :meth:`~matcal.core.study_base.StudyBase.add_evaluation_set`
along with this type of objective can be of type :class:`~matcal.full_field.data.FieldData`
or :class:`~matcal.core.data.Data` since the external virtual power
calculation only uses global data.
For the internal virtual power calculation, the virtual field
functions used are
.. math::
\\mathbf{v}^*_X=\\cos\\frac{\\pi\\bar{Y}}{h}
.. math::
\\mathbf{v}^*_Y=\\frac{2\\bar{Y}+h}{2h}
where :math:`Y` is the direction of loading,
:math:`\\bar{Y}` is the centered position
of the current point of interest
in the reference configuration,
and :math:`h` is the total height of
the data.
"""
_class_name = "MechanicalVFMObjective"
def __init__(self, time_field="time", load_field="load"):
"""
Optionally specify the time and load field names
that are required to be in the experiment :class:`~matcal.full_field.data.FieldData`
class for this objective.
These are assumed to be "time" and "load" by default.
:param time_field: the name of the time field in the data.
:type independent_field: str
:param load_field: the name of the load field in the data.
:type load_field: str
:raises Objective.TypeError: If the wrong types are passed into the constructor.
"""
super().__init__('virtual_power')
check_item_is_correct_type(time_field, str, "time_field")
check_item_is_correct_type(load_field, str, "load_field")
self._time_field = time_field
self._load_field = load_field
self._setup_qoi_extractors()
self.set_as_large_data_sets_objective()
def _setup_qoi_extractors(self):
self.set_experiment_qoi_extractor(
ExternalVirtualPowerExtractor(self._time_field,
self._load_field))
self.set_simulation_qoi_extractor(
InternalVirtualPowerExtractor(self._time_field))
def _confirm_simulation_fields(self, simulation_data):
required_fields = ['virtual_power']
self._check_required_fields_are_in_data(
simulation_data, required_fields)
def _confirm_experiment_fields(self, exp_data):
required_fields = ['virtual_power']
self._check_required_fields_are_in_data(exp_data, required_fields)
def virtual_velocity_function(self):
return self._velocity_function
def virtual_velocity_gradient_function(self):
return self._velocity_gradient_function