Source code for matcal.dakota.local_calibration_studies

"""
This module contains MatCal's interface to Dakota's local calibration methods.
"""
from numbers import Real

from matcal.core.logger import initialize_matcal_logger
from matcal.core.utilities import (check_item_is_correct_type, 
                                   check_value_is_positive_integer, 
                                   check_value_is_real_between_values)

from matcal.dakota.dakota_studies import DakotaCalibrationStudyBase
from matcal.dakota.input_file_writer import (DakotaCalibrationFile, GeneralGradientMethodType, 
                                             NongradientResponseBlock, DakMethodKeys, 
                                             dakota_response_identifier, GradientResponseBlock, 
                                             LeastSquaresResponseBlock, NumericalGradientBlock, 
                                             DakGradientKeys, GeneralNongradientMethodType, 
                                             method_type_defaults_identifier)

logger = initialize_matcal_logger(__name__)


class _LeastSquaresMethodKeys():
    nl2sol = "nl2sol"
    optpp_g_newton = "optpp_g_newton"
    nlssol_sqp = "nlssol_sqp"


dakota_response_identifier.register(_LeastSquaresMethodKeys.nl2sol, 
                                    LeastSquaresResponseBlock)
dakota_response_identifier.register(_LeastSquaresMethodKeys.optpp_g_newton, 
                                    LeastSquaresResponseBlock)
dakota_response_identifier.register(_LeastSquaresMethodKeys.nlssol_sqp, 
                                    LeastSquaresResponseBlock)


class _GradientMethodKeys():
    npsol_sqp = "npsol_sqp"
    dot_mmfd = "dot_mmfd"
    dot_slp = "dot_slp"
    dot_sqp = "dot_sqp"
    conmin_mfd = "conmin_mfd"
    optpp_q_newton = "optpp_q_newton"
    optpp_g_newton = "optpp_g_newton"
    optpp_fd_newton = "optpp_fd_newton"


dakota_response_identifier.register(_GradientMethodKeys.npsol_sqp, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.dot_mmfd, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.dot_slp, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.dot_sqp, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.conmin_mfd, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.optpp_q_newton, 
                                    GradientResponseBlock)
dakota_response_identifier.register(_GradientMethodKeys.optpp_fd_newton, 
                                    GradientResponseBlock)


class _GradientDakotaFile(DakotaCalibrationFile):
    _method_class = GeneralGradientMethodType

    least_squares_methods = [_LeastSquaresMethodKeys.nl2sol, 
                             _LeastSquaresMethodKeys.optpp_g_newton,
                             _LeastSquaresMethodKeys.nlssol_sqp]
    
    gradient_methods = [_GradientMethodKeys.npsol_sqp,
                        _GradientMethodKeys.dot_mmfd,
                        _GradientMethodKeys.dot_slp, 
                        _GradientMethodKeys.dot_sqp,
                        _GradientMethodKeys.conmin_mfd, 
                        _GradientMethodKeys.optpp_q_newton,
                        _GradientMethodKeys.optpp_fd_newton]

    valid_methods = least_squares_methods+gradient_methods

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_method(_LeastSquaresMethodKeys.nl2sol)
    
    def set_method(self, method_name):
        """
        Sets the Dakota gradient based method to be used by the study. The default method is a least squares method 
        "nl2sol". Other least squares methods include "optpp_g_newton" and "nlssol_sqp". 

        Optionally, gradient based methods that operate on objective functions can be chosen which is recommended 
        for problems with large numbers of residuals such as some full-field interpolation objective. 
        These methods include "npsol_sqp", "dot_sqp" , "optpp_q_newton", and "conmin_mfd". 

        Some gradient methods that use an objective that are available but not recommended included 
        "dot_slp", "dot_mmfd", and  "optpp_fd_newton". These performed poorly on our production level tests, so 
        the other methods are recommended unless you have a specific case where these are required.

        .. note: Some methods treat the method options differently and may not behave the same for different options 
            such as "convergence_tolerance", "max_iterations" and "max_function_evaluations".

        """
        self._set_method(method_name)

    def get_gradient_block(self):
        response_block = self.get_response_block()
        return  response_block.get_subblock(NumericalGradientBlock.type)
    
    def set_step_size(self, value=
                      NumericalGradientBlock.default_values[DakGradientKeys.fd_step_size]):
        """
        Sets the finite difference step sizes for the gradient decent optimizations.
        Default step size is a relative step of 5e-5.
        
        :param step_size: the desired step_size
        :type step_size: float
        """
        check_value_is_real_between_values(value, 1e-9, 1e-1, "step size")
        grad_block = self.get_gradient_block()
        step_size_line  = grad_block.get_line(DakGradientKeys.fd_step_size) 
        step_size_line.set(value)


[docs] class GradientCalibrationStudy(_GradientDakotaFile, DakotaCalibrationStudyBase): """ The Gradient Calibration algorithm is a local optimization method that requires the objective function be smooth and convex. This method can quickly find a nearby minimum if the objective function is smooth. It is useful if you are calibrating to a single model with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the NL2SOL method from Dakota. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _GradientDakotaFile.__init__(self, )
class _NongradientMethodKeys(): coliny_cobyla = "coliny_cobyla" coliny_pattern_search = "coliny_pattern_search" coliny_solis_wets = "coliny_solis_wets" mesh_adaptive_search = "mesh_adaptive_search" optpp_pds = "optpp_pds" dakota_response_identifier.register(_NongradientMethodKeys.coliny_cobyla, NongradientResponseBlock) dakota_response_identifier.register(_NongradientMethodKeys.coliny_pattern_search, NongradientResponseBlock) dakota_response_identifier.register(_NongradientMethodKeys.coliny_solis_wets, NongradientResponseBlock) dakota_response_identifier.register(_NongradientMethodKeys.mesh_adaptive_search, NongradientResponseBlock) dakota_response_identifier.register(_NongradientMethodKeys.optpp_pds, NongradientResponseBlock) class _NongradientDakotaFile(DakotaCalibrationFile): _method_class = GeneralNongradientMethodType class _NongradientDakotaFileWithVarTol(_NongradientDakotaFile): def set_variable_tolerance(self, value): """ Set the variable convergence tolerance which limits the minimum step length for the algorithm and is a measure of algorithm convergence. A value greater than 0.0 and less than 1.0 is expected. For specifics, see the Dakota user manual. :param value: the desired variable tolerance :type value: float """ check_value_is_real_between_values(value, 0, 1.0, "value") self.set_method_type_block_line(DakMethodKeys.variable_tolerance, value) def get_variable_tolerance(self): """ Returns the variable tolerance size for the study. Returns None if it has not been specified. :rtype: float or None """ method_type_block = self.get_method_type_block() value = None if DakMethodKeys.variable_tolerance in method_type_block.lines: value = method_type_block.get_line_value(DakMethodKeys.variable_tolerance) return value class _MeshAdaptiveDakotaFile(_NongradientDakotaFileWithVarTol): valid_methods = [_NongradientMethodKeys.mesh_adaptive_search] class Keywords(_NongradientDakotaFileWithVarTol.Keywords): variable_neighborhood_search = "variable_neighborhood_search" def set_variable_neighborhood_search(self, value): """ Set the variable neighborhood search parameter for the algorithm. It increases the number of objective function evaluations each iteration to search more of the space in an attempt to escape local minimums. For specifics, see the Dakota user manual. A value of 0 to 1.0 is expected. :param value: the desired variable tolerance :type value: float """ check_value_is_real_between_values(value, 0, 1.0, "value") self.set_method_type_block_line(self.Keywords.variable_neighborhood_search, value) def get_variable_neighborhood_search(self): """ Returns the variable neighborhood search for the study. Returns None if a value is not user specified :rtype: float or None """ method_type_block = self.get_method_type_block() value = None if self.Keywords.variable_neighborhood_search in method_type_block.lines: value = method_type_block.get_line_value(self.Keywords.variable_neighborhood_search) return value class _MeshAdaptiveSearchDefaults: method_specific_default_values = {} default_values = dict(**GeneralNongradientMethodType.default_values, **method_specific_default_values) method_type_defaults_identifier.register(_NongradientMethodKeys.mesh_adaptive_search, _MeshAdaptiveSearchDefaults.default_values)
[docs] class MeshAdaptiveSearchCalibrationStudy(_MeshAdaptiveDakotaFile, DakotaCalibrationStudyBase): """ The Mesh Adaptive Search algorithm is a local optimization method that exhibits some robustness to noisy objective functions. The true global minimum may not be found, but this method can find a nearby minimum even if the objective function is not smooth. This method can require more iterations than a gradient based method but is cheaper than global search methods. It is useful if you are calibrating to a couple models with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the mesh_adaptive_search method from Dakota. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _MeshAdaptiveDakotaFile.__init__(self, ) self._set_method(_NongradientMethodKeys.mesh_adaptive_search)
class _ColinyNongradientDakotaFile(_NongradientDakotaFileWithVarTol): valid_methods = [_NongradientMethodKeys.coliny_cobyla, _NongradientMethodKeys.coliny_solis_wets, _NongradientMethodKeys.coliny_pattern_search] def set_solution_target(self, value): """ Specifies a target value for the objective function. Once the objective function goes below or reaches this value, the calibration will stop. :param value: solution target value :type value: float """ check_item_is_correct_type(value, Real, "value") self.set_method_type_block_line(DakMethodKeys.solution_target, value) def get_solution_target(self): """ Returns the solution target for the study. Returns None if it has not been set. :rtype: float or None """ method_type_block = self.get_method_type_block() value = None if DakMethodKeys.solution_target in method_type_block.lines: value = method_type_block.get_line_value(DakMethodKeys.solution_target) return value class _ColinyCobylaDefaults: method_specific_default_values = { DakMethodKeys.convergence_tol:1e-3 } default_values = dict(**GeneralNongradientMethodType.default_values, **method_specific_default_values) method_type_defaults_identifier.register(_NongradientMethodKeys.coliny_cobyla, _ColinyCobylaDefaults.default_values)
[docs] class CobylaCalibrationStudy(_ColinyNongradientDakotaFile, DakotaCalibrationStudyBase): """ The Cobyla algorithm is a local optimization method that exhibits some robustness to noisy objective functions. The true global minimum may not be found, but this method can find a nearby minimum even if the objective function is not smooth. This method can require more iterations than a gradient based method but is cheaper than global search methods. It is useful if you are calibrating to a couple models with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the coliny_cobyla method from Dakota. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _ColinyNongradientDakotaFile.__init__(self, ) self._set_method(_NongradientMethodKeys.coliny_cobyla)
class _ColinySolisWetsDefaults: method_specific_default_values = { DakMethodKeys.convergence_tol:1e-3, } default_values = dict(**GeneralNongradientMethodType.default_values, **method_specific_default_values) method_type_defaults_identifier.register(_NongradientMethodKeys.coliny_solis_wets, _ColinySolisWetsDefaults.default_values)
[docs] class SolisWetsCalibrationStudy(_ColinyNongradientDakotaFile, DakotaCalibrationStudyBase): """ The Solis Wets algorithm is a local optimization method that exhibits some robustness to noisy objective functions. The true global minimum may not be found, but this method can find a nearby minimum even if the objective function is not smooth. This method can require more iterations than a gradient based method but is cheaper than global search methods. It is useful if you are calibrating to a couple models with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the coliny_solis_wets method from Dakota. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _ColinyNongradientDakotaFile.__init__(self, ) self._set_method(_NongradientMethodKeys.coliny_solis_wets)
class _PatternSearchDakotaFile(_ColinyNongradientDakotaFile): valid_methods = [_NongradientMethodKeys.coliny_pattern_search] class Keywords(_ColinyNongradientDakotaFile.Keywords): exploratory_moves = "exploratory_moves" def set_exploratory_moves(self, value): """ This can be used to set the way the pattern for the search is updated each iteration. Available options are "basic_pattern", "adaptive_pattern", and "multi_step". The default is currently "basic_pattern", which is subject to change. See Dakota manual for specifics. :param value: the exploratory move type to be used :type value: str """ check_item_is_correct_type(value, str, "value") valid_types = ["basic_pattern", "adaptive_pattern", "multi_step"] if value not in valid_types: valid_types_string = '\n'.join(x for x in valid_types) raise ValueError("{} is not a valid \"exploratory moves\" option. The following are valid " "options: {}".format(value, valid_types_string)) self.set_method_type_block_line(self.Keywords.exploratory_moves, value) def get_exploratory_moves(self): """ Returns the exploratory moves pattern for the study. Returns None if it is not user specified. :rtype: str or None """ method_type_block = self.get_method_type_block() value =None if self.Keywords.exploratory_moves in method_type_block.lines: value = method_type_block.get_line_value(self.Keywords.exploratory_moves) return value class _ColinyPatternSearchDefaults: method_specific_default_values = { DakMethodKeys.convergence_tol:1e-3} default_values = dict(**GeneralNongradientMethodType.default_values, **method_specific_default_values) method_type_defaults_identifier.register(_NongradientMethodKeys.coliny_pattern_search, _ColinyPatternSearchDefaults.default_values)
[docs] class PatternSearchCalibrationStudy(_PatternSearchDakotaFile, DakotaCalibrationStudyBase): """ The Pattern Search algorithm is a local optimization method that exhibits some robustness to noisy objective functions. The true global minimum may not be found, but this method can find a nearby minimum even if the objective function is not smooth. This method can require more iterations than a gradient based method but is cheaper than global search methods. It is useful if you are calibrating to a couple models with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the coliny_pattern_search method from Dakota. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _PatternSearchDakotaFile.__init__(self, ) self._set_method(_NongradientMethodKeys.coliny_pattern_search)
class _OptppPdsDakotaFile(DakotaCalibrationFile): _method_class = GeneralNongradientMethodType valid_methods = [_NongradientMethodKeys.optpp_pds] class Keywords(DakotaCalibrationFile.Keywords): search_scheme_size = "search_scheme_size" def set_search_scheme_size(self, value): """ Set the number of samples used for creating the simplex each iteration. Dakota has a default of 32 and MatCal has a default of 10. This should be at least N+1 where N is the number of parameters being calibrated. :param value: the search scheme size :type value: int """ check_value_is_positive_integer(value, "value") self.set_method_type_block_line(self.Keywords.search_scheme_size, value) def get_search_scheme_size(self): """ Returns the search scheme for the study. :rtype: int """ method_type_block = self.get_method_type_block() value = method_type_block.get_line_value(self.Keywords.search_scheme_size) return value class _OptppPdsDefaults: class _Keywords: search_scheme_size = "search_scheme_size" method_specific_default_values = {_Keywords.search_scheme_size:10} default_values = dict(**GeneralNongradientMethodType.default_values, **method_specific_default_values) method_type_defaults_identifier.register(_NongradientMethodKeys.optpp_pds, _OptppPdsDefaults.default_values)
[docs] class ParallelDirectSearchCalibrationStudy(_OptppPdsDakotaFile, DakotaCalibrationStudyBase): """ The Parallel Direct Search algorithm is a local optimization method that exhibits some robustness to noisy objective functions. The true global minimum may not be found, but this method can find a nearby minimum even if the objective function is not smooth. This method can require more iterations than a gradient based method but is cheaper than global search methods. It is useful if you are calibrating to a couple models with only a couple states and you know a decent initial guess for the parameters. This is the MatCal implementation of the optpp_pds method from Dakota. Note that although the algorithm is designed for parallel execution, Dakota has not yet implemented it as parallel so it runs in serial. """ def __init__(self, *parameters): DakotaCalibrationStudyBase.__init__(self, *parameters) _OptppPdsDakotaFile.__init__(self, ) self._set_method(_NongradientMethodKeys.optpp_pds)