def _load_content(self, N, directory, filename): affine_expansion_N = OnlineAffineExpansionStorage(self._len) loaded = affine_expansion_N.load(directory, filename + "_N=" + str(N)) assert loaded is True return affine_expansion_N
class TimeDependentRBReducedProblem_Class( ParametrizedReducedDifferentialProblem_DerivedClass): # Default initialization of members. def __init__(self, truth_problem, **kwargs): # Call to parent ParametrizedReducedDifferentialProblem_DerivedClass.__init__( self, truth_problem, **kwargs) # Storage related to error estimation for initial condition # initial_condition_product: AffineExpansionStorage (for problems with one component) # or dict of AffineExpansionStorage (for problem with several components) self.initial_condition_product = None def _init_error_estimation_operators(self, current_stage="online"): ParametrizedReducedDifferentialProblem_DerivedClass._init_error_estimation_operators( self, current_stage) # Also initialize data structures related to initial condition error estimation if len(self.components) > 1: initial_condition_product = dict() for component in self.components: if self.initial_condition[ component] and not self.initial_condition_is_homogeneous[ component]: initial_condition_product[ component, component] = OnlineAffineExpansionStorage( self.Q_ic[component], self.Q_ic[component]) assert current_stage in ("online", "offline") if current_stage == "online": self.assemble_error_estimation_operators( ("initial_condition_" + component, "initial_condition_" + component), "online") elif current_stage == "offline": pass # Nothing else to be done else: raise ValueError( "Invalid stage in _init_error_estimation_operators()." ) if len(initial_condition_product) > 0: self.initial_condition_product = initial_condition_product else: if self.initial_condition and not self.initial_condition_is_homogeneous: self.initial_condition_product = OnlineAffineExpansionStorage( self.Q_ic, self.Q_ic) assert current_stage in ("online", "offline") if current_stage == "online": self.assemble_error_estimation_operators( ("initial_condition", "initial_condition"), "online") elif current_stage == "offline": pass # Nothing else to be done else: raise ValueError( "Invalid stage in _init_error_estimation_operators()." ) # Build operators for error estimation def build_error_estimation_operators(self, current_stage="offline"): ParametrizedReducedDifferentialProblem_DerivedClass.build_error_estimation_operators( self, current_stage) # Initial condition self._build_reduced_initial_condition_error_estimation( current_stage) def _build_reduced_initial_condition_error_estimation( self, current_stage="offline"): # Assemble initial condition product error estimation operator if len(self.components) > 1: for component in self.components: if self.initial_condition[ component] and not self.initial_condition_is_homogeneous[ component]: self.assemble_error_estimation_operators( ("initial_condition_" + component, "initial_condition_" + component), current_stage) else: if self.initial_condition and not self.initial_condition_is_homogeneous: self.assemble_error_estimation_operators( ("initial_condition", "initial_condition"), current_stage) # Assemble operators for error estimation def assemble_error_estimation_operators(self, term, current_stage="online"): if term[0].startswith("initial_condition") and term[1].startswith( "initial_condition"): component0 = term[0].replace("initial_condition", "").replace("_", "") component1 = term[1].replace("initial_condition", "").replace("_", "") assert current_stage in ("online", "offline") if current_stage == "online": # load from file assert (component0 != "") == (component1 != "") if component0 != "": assert component0 in self.components assert component1 in self.components self.initial_condition_product[ component0, component1].load( self.folder["error_estimation"], "initial_condition_product_" + component0 + "_" + component1) return self.initial_condition_product[component0, component1] else: assert len(self.components) == 1 self.initial_condition_product.load( self.folder["error_estimation"], "initial_condition_product") return self.initial_condition_product elif current_stage == "offline": inner_product = self.truth_problem._combined_projection_inner_product assert (component0 != "") == (component1 != "") if component0 != "": for q0 in range(self.Q_ic[component0]): for q1 in range(self.Q_ic[component1]): self.initial_condition_product[ component0, component1][q0, q1] = ( transpose( self.truth_problem. initial_condition[component0][q0]) * inner_product * self.truth_problem. initial_condition[component1][q1]) self.initial_condition_product[ component0, component1].save( self.folder["error_estimation"], "initial_condition_product_" + component0 + "_" + component1) return self.initial_condition_product[component0, component1] else: assert len(self.components) == 1 for q0 in range(self.Q_ic): for q1 in range(self.Q_ic): self.initial_condition_product[q0, q1] = ( transpose(self.truth_problem. initial_condition[q0]) * inner_product * self.truth_problem.initial_condition[q1]) self.initial_condition_product.save( self.folder["error_estimation"], "initial_condition_product") return self.initial_condition_product else: raise ValueError( "Invalid stage in assemble_error_estimation_operators()." ) else: return ParametrizedReducedDifferentialProblem_DerivedClass.assemble_error_estimation_operators( self, term, current_stage) # Return the error bound for the initial condition def get_initial_error_estimate_squared(self): self._solution = self._solution_over_time[0] N = self._solution.N at_least_one_non_homogeneous_initial_condition = False addend_0 = 0. addend_1_right = OnlineFunction(N) if len(self.components) > 1: for component in self.components: if self.initial_condition[ component] and not self.initial_condition_is_homogeneous[ component]: at_least_one_non_homogeneous_initial_condition = True theta_ic_component = self.compute_theta( "initial_condition_" + component) addend_0 += sum( product( theta_ic_component, self.initial_condition_product[component, component], theta_ic_component)) addend_1_right += sum( product(theta_ic_component, self.initial_condition[:N])) else: if self.initial_condition and not self.initial_condition_is_homogeneous: at_least_one_non_homogeneous_initial_condition = True theta_ic = self.compute_theta("initial_condition") addend_0 += sum( product(theta_ic, self.initial_condition_product, theta_ic)) addend_1_right += sum( product(theta_ic, self.initial_condition[:N])) if at_least_one_non_homogeneous_initial_condition: inner_product_N = self._combined_projection_inner_product[:N, : N] addend_1_left = self._solution addend_2 = transpose( self._solution) * inner_product_N * self._solution return addend_0 - 2.0 * (transpose(addend_1_left) * addend_1_right) + addend_2 else: return 0.
class ParametrizedReducedDifferentialProblem(ParametrizedProblem, metaclass=ABCMeta): """ Base class containing the interface of a projection based ROM for elliptic coercive problems. Initialization of dimension of reduced problem N, boundary conditions, terms and their order, number of terms in the affine expansion Q, reduced operators and inner products, reduced solution, reduced basis functions matrix. :param truth_problem: class of the truth problem to be solved. """ @sync_setters("truth_problem", "set_mu", "mu") @sync_setters("truth_problem", "set_mu_range", "mu_range") def __init__(self, truth_problem, **kwargs): # Call to parent ParametrizedProblem.__init__(self, truth_problem.name()) # $$ ONLINE DATA STRUCTURES $$ # # Online reduced space dimension self.N = None # integer (for problems with one component) or dict of integers (for problem with several components) self.N_bc = None # integer (for problems with one component) or dict of integers (for problem with several components) self.dirichlet_bc = None # bool (for problems with one component) or dict of bools (for problem with several components) self.dirichlet_bc_are_homogeneous = None # bool (for problems with one component) or dict of bools (for problem with several components) self._combined_and_homogenized_dirichlet_bc = None # Form names and order self.terms = truth_problem.terms self.terms_order = truth_problem.terms_order self.components = truth_problem.components # Number of terms in the affine expansion self.Q = dict() # from string to integer # Reduced order operators self.OperatorExpansionStorage = OnlineAffineExpansionStorage self.operator = dict() # from string to OperatorExpansionStorage self.inner_product = None # AffineExpansionStorage (for problems with one component) or dict of AffineExpansionStorage (for problem with several components), even though it will contain only one matrix self._combined_inner_product = None self.projection_inner_product = None # AffineExpansionStorage (for problems with one component) or dict of AffineExpansionStorage (for problem with several components), even though it will contain only one matrix self._combined_projection_inner_product = None # Solution self._solution = None # OnlineFunction self._solution_cache = dict() # of Functions self._output = 0 self._output_cache = dict() # of Numbers self._output_cache__current_cache_key = None # $$ OFFLINE DATA STRUCTURES $$ # # High fidelity problem self.truth_problem = truth_problem # Basis functions matrix self.basis_functions = None # BasisFunctionsMatrix # I/O self.folder["basis"] = os.path.join(self.folder_prefix, "basis") self.folder["reduced_operators"] = os.path.join( self.folder_prefix, "reduced_operators") self.cache_config = config.get("reduced problems", "cache") def init(self, current_stage="online"): """ Initialize data structures required during the online phase. """ self._init_operators(current_stage) self._init_inner_products(current_stage) self._init_basis_functions(current_stage) def _init_operators(self, current_stage="online"): """ Initialize data structures required for the online phase. Internal method. """ for term in self.terms: if term not in self.operator: # init was not called already self.Q[term] = self.truth_problem.Q[term] self.operator[term] = self.OperatorExpansionStorage( self.Q[term]) assert current_stage in ("online", "offline") if current_stage == "online": for term in self.terms: self.assemble_operator(term, "online") elif current_stage == "offline": pass # Nothing else to be done else: raise ValueError("Invalid stage in _init_operators().") def _init_inner_products(self, current_stage="online"): """ Initialize data structures required for the online phase. Internal method. """ assert current_stage in ("online", "offline") if current_stage == "online": n_components = len(self.components) # Inner products if self.inner_product is None: # init was not called already if n_components > 1: inner_product_string = "inner_product_{c}" self.inner_product = dict() for component in self.components: self.inner_product[ component] = OnlineAffineExpansionStorage(1) self.assemble_operator( inner_product_string.format(c=component), "online") else: self.inner_product = OnlineAffineExpansionStorage(1) self.assemble_operator("inner_product", "online") self._combined_inner_product = self._combine_all_inner_products( ) # Projection inner product if self.projection_inner_product is None: # init was not called already if n_components > 1: projection_inner_product_string = "projection_inner_product_{c}" self.projection_inner_product = dict() for component in self.components: self.projection_inner_product[ component] = OnlineAffineExpansionStorage(1) self.assemble_operator( projection_inner_product_string.format( c=component), "online") else: self.projection_inner_product = OnlineAffineExpansionStorage( 1) self.assemble_operator("projection_inner_product", "online") self._combined_projection_inner_product = self._combine_all_projection_inner_products( ) elif current_stage == "offline": n_components = len(self.components) # Inner products if self.inner_product is None: # init was not called already if n_components > 1: self.inner_product = dict() for component in self.components: self.inner_product[ component] = OnlineAffineExpansionStorage(1) else: self.inner_product = OnlineAffineExpansionStorage(1) # Projection inner product if self.projection_inner_product is None: # init was not called already if n_components > 1: self.projection_inner_product = dict() for component in self.components: self.projection_inner_product[ component] = OnlineAffineExpansionStorage(1) else: self.projection_inner_product = OnlineAffineExpansionStorage( 1) else: raise ValueError("Invalid stage in _init_inner_products().") def _combine_all_inner_products(self): if len(self.components) > 1: all_inner_products = list() for component in self.components: assert len( self.inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix all_inner_products.append(self.inner_product[component][0]) all_inner_products = tuple(all_inner_products) else: assert len( self.inner_product ) == 1 # the affine expansion storage contains only the inner product matrix all_inner_products = (self.inner_product[0], ) all_inner_products = OnlineAffineExpansionStorage(all_inner_products) all_inner_products_thetas = (1., ) * len(all_inner_products) return sum(product(all_inner_products_thetas, all_inner_products)) def _combine_all_projection_inner_products(self): if len(self.components) > 1: all_projection_inner_products = list() for component in self.components: assert len( self.projection_inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix all_projection_inner_products.append( self.projection_inner_product[component][0]) all_projection_inner_products = tuple( all_projection_inner_products) else: assert len( self.projection_inner_product ) == 1 # the affine expansion storage contains only the inner product matrix all_projection_inner_products = ( self.projection_inner_product[0], ) all_projection_inner_products = OnlineAffineExpansionStorage( all_projection_inner_products) all_projection_inner_products_thetas = ( 1., ) * len(all_projection_inner_products) return sum( product(all_projection_inner_products_thetas, all_projection_inner_products)) def _init_basis_functions(self, current_stage="online"): """ Basis functions are initialized. Internal method. """ assert current_stage in ("online", "offline") # Initialize basis functions mappings if self.basis_functions is None: # avoid re-initializing basis functions matrix multiple times self.basis_functions = BasisFunctionsMatrix(self.truth_problem.V) self.basis_functions.init(self.components) # Get number of components n_components = len(self.components) # Get helper strings depending on the number of basis components if n_components > 1: dirichlet_bc_string = "dirichlet_bc_{c}" def has_non_homogeneous_dirichlet_bc(component): return self.dirichlet_bc[ component] and not self.dirichlet_bc_are_homogeneous[ component] def get_basis_functions(component): return self.basis_functions[component] else: dirichlet_bc_string = "dirichlet_bc" def has_non_homogeneous_dirichlet_bc(component): return self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous def get_basis_functions(component): return self.basis_functions # Detect how many theta terms are related to boundary conditions assert (self.dirichlet_bc is None) == (self.dirichlet_bc_are_homogeneous is None) if self.dirichlet_bc is None: # init was not called already dirichlet_bc = dict() for component in self.components: try: theta_bc = self.compute_theta( dirichlet_bc_string.format(c=component)) except ValueError: # there were no Dirichlet BCs to be imposed by lifting dirichlet_bc[component] = False else: dirichlet_bc[component] = True if n_components == 1: self.dirichlet_bc = dirichlet_bc[self.components[0]] else: self.dirichlet_bc = dirichlet_bc self.dirichlet_bc_are_homogeneous = self.truth_problem.dirichlet_bc_are_homogeneous assert self._combined_and_homogenized_dirichlet_bc is None self._combined_and_homogenized_dirichlet_bc = self._combine_and_homogenize_all_dirichlet_bcs( ) # Load basis functions if current_stage == "online": basis_functions_loaded = self.basis_functions.load( self.folder["basis"], "basis") # To properly initialize N and N_bc, detect how many theta terms # are related to boundary conditions if basis_functions_loaded: N = OnlineSizeDict() N_bc = OnlineSizeDict() for component in self.components: if has_non_homogeneous_dirichlet_bc(component): theta_bc = self.compute_theta( dirichlet_bc_string.format(c=component)) N[component] = len( get_basis_functions(component)) - len(theta_bc) N_bc[component] = len(theta_bc) else: N[component] = len(get_basis_functions(component)) N_bc[component] = 0 assert len(N) == len(N_bc) assert len(N) > 0 if len(N) == 1: self.N = N[self.components[0]] self.N_bc = N_bc[self.components[0]] else: self.N = N self.N_bc = N_bc elif current_stage == "offline": # Store the lifting functions in self.basis_functions for component in self.components: self.assemble_operator( dirichlet_bc_string.format(c=component), "offline" ) # no return value from assemble_operator in this case # Save basis functions matrix, that contains up to now only lifting functions self.basis_functions.save(self.folder["basis"], "basis") # Properly fill in self.N_bc if n_components == 1: self.N = 0 self.N_bc = len(self.basis_functions) else: N = OnlineSizeDict() N_bc = OnlineSizeDict() for component in self.components: N[component] = 0 N_bc[component] = len(self.basis_functions[component]) self.N = N self.N_bc = N_bc # Note that, however, self.N is not increased, so it will actually contain the number # of basis functions without the lifting ones. else: raise ValueError("Invalid stage in _init_basis_functions().") def _combine_and_homogenize_all_dirichlet_bcs(self): if len(self.components) > 1: all_dirichlet_bcs_thetas = dict() for component in self.components: if self.dirichlet_bc[ component] and not self.dirichlet_bc_are_homogeneous[ component]: all_dirichlet_bcs_thetas[component] = (0, ) * len( self.compute_theta("dirichlet_bc_" + component)) if len(all_dirichlet_bcs_thetas) == 0: all_dirichlet_bcs_thetas = None else: if self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous: all_dirichlet_bcs_thetas = (0, ) * len( self.compute_theta("dirichlet_bc")) else: all_dirichlet_bcs_thetas = None return all_dirichlet_bcs_thetas def solve(self, N=None, **kwargs): """ Perform an online solve. self.N will be used as matrix dimension if the default value is provided for N. :param N : Dimension of the reduced problem :type N : integer :return: reduced solution """ N, kwargs = self._online_size_from_kwargs(N, **kwargs) N += self.N_bc cache_key = self._cache_key_from_N_and_kwargs(N, **kwargs) self._solution = OnlineFunction(N) if "RAM" in self.cache_config and cache_key in self._solution_cache: log(PROGRESS, "Loading reduced solution from cache") assign(self._solution, self._solution_cache[cache_key]) else: log(PROGRESS, "Solving reduced problem") assert not hasattr(self, "_is_solving") self._is_solving = True self._solve(N, **kwargs) delattr(self, "_is_solving") if "RAM" in self.cache_config: self._solution_cache[cache_key] = copy(self._solution) return self._solution class ProblemSolver(object, metaclass=ABCMeta): def __init__(self, problem, N): self.problem = problem self.N = N def bc_eval(self): problem = self.problem if len(problem.components) > 1: all_dirichlet_bcs_thetas = dict() for component in problem.components: if problem.dirichlet_bc[ component] and not problem.dirichlet_bc_are_homogeneous[ component]: all_dirichlet_bcs_thetas[ component] = problem.compute_theta( "dirichlet_bc_" + component) if len(all_dirichlet_bcs_thetas) == 0: all_dirichlet_bcs_thetas = None else: if problem.dirichlet_bc and not problem.dirichlet_bc_are_homogeneous: all_dirichlet_bcs_thetas = problem.compute_theta( "dirichlet_bc") else: all_dirichlet_bcs_thetas = None return all_dirichlet_bcs_thetas @abstractmethod def solve(self): pass # Perform an online solve (internal) def _solve(self, N, **kwargs): problem_solver = self.ProblemSolver(self, N) problem_solver.solve() def project(self, snapshot, on_dirichlet_bc=True, N=None, **kwargs): N, kwargs = self._online_size_from_kwargs(N, **kwargs) N += self.N_bc # Get truth and reduced inner product matrices for projection inner_product = self.truth_problem._combined_projection_inner_product inner_product_N = self._combined_projection_inner_product[:N, :N] # Get basis basis_functions = self.basis_functions[:N] # Define storage for projected solution projected_snapshot_N = OnlineFunction(N) # Project on reduced basis if on_dirichlet_bc: solver = OnlineLinearSolver( inner_product_N, projected_snapshot_N, transpose(basis_functions) * inner_product * snapshot) else: solver = OnlineLinearSolver( inner_product_N, projected_snapshot_N, transpose(basis_functions) * inner_product * snapshot, self._combined_and_homogenized_dirichlet_bc) solver.set_parameters(self._linear_solver_parameters) solver.solve() return projected_snapshot_N def compute_output(self): """ :return: reduced output """ cache_key = self._output_cache__current_cache_key if "RAM" in self.cache_config and cache_key in self._output_cache: log(PROGRESS, "Loading reduced output from cache") self._output = self._output_cache[cache_key] else: log(PROGRESS, "Computing reduced output") N = self._solution.N try: self._compute_output(N) except ValueError: # raised by compute_theta if output computation is optional self._output = NotImplemented if "RAM" in self.cache_config: self._output_cache[cache_key] = self._output return self._output def _compute_output(self, N): """ Perform an online evaluation of the output. """ self._output = NotImplemented def _online_size_from_kwargs(self, N, **kwargs): return OnlineSizeDict.generate_from_N_and_kwargs( self.components, self.N, N, **kwargs) def _cache_key_from_N_and_kwargs(self, N, **kwargs): """ Internal method. :param N: dimension of reduced problem. """ for blacklist in ("components", "inner_product"): if blacklist in kwargs: del kwargs[blacklist] if isinstance(N, dict): cache_key = (self.mu, tuple(sorted(N.items())), tuple(sorted(kwargs.items()))) else: assert isinstance(N, int) cache_key = (self.mu, N, tuple(sorted(kwargs.items()))) # Store current cache_key to be used when computing output self._output_cache__current_cache_key = cache_key # Return return cache_key def build_reduced_operators(self, current_stage="offline"): """ It asssembles the reduced order affine expansion. """ # Inner products and projection inner products self._build_reduced_inner_products(current_stage) # Terms self._build_reduced_operators(current_stage) def _build_reduced_operators(self, current_stage="offline"): for term in self.terms: self.operator[term] = self.assemble_operator(term, current_stage) def _build_reduced_inner_products(self, current_stage="offline"): n_components = len(self.components) # Inner products if n_components > 1: inner_product_string = "inner_product_{c}" for component in self.components: self.inner_product[component] = self.assemble_operator( inner_product_string.format(c=component), current_stage) else: self.inner_product = self.assemble_operator( "inner_product", current_stage) self._combined_inner_product = self._combine_all_inner_products() # Projection inner product if n_components > 1: projection_inner_product_string = "projection_inner_product_{c}" for component in self.components: self.projection_inner_product[ component] = self.assemble_operator( projection_inner_product_string.format(c=component), current_stage) else: self.projection_inner_product = self.assemble_operator( "projection_inner_product", current_stage) self._combined_projection_inner_product = self._combine_all_projection_inner_products( ) def compute_error(self, **kwargs): """ Returns the function _compute_error() evaluated for the desired parameter. :return: error between online and offline solutions. """ self.truth_problem.solve(**kwargs) return self._compute_error(**kwargs) def _compute_error(self, **kwargs): """ It computes the error of the reduced order approximation with respect to the full order one for the current value of mu. """ (components, inner_product ) = self._preprocess_compute_error_and_relative_error_kwargs(**kwargs) # Storage error = dict() # Compute the error on the solution if len(components) > 0: N = self._solution.N reduced_solution = self.basis_functions[:N] * self._solution truth_solution = self.truth_problem._solution error_function = truth_solution - reduced_solution for component in components: error_norm_squared_component = transpose( error_function) * inner_product[component] * error_function assert error_norm_squared_component >= 0. or isclose( error_norm_squared_component, 0.) error[component] = sqrt(error_norm_squared_component) # Simplify trivial case if len(components) == 1: error = error[components[0]] # return error def compute_relative_error(self, **kwargs): """ It returns the function _compute_relative_error() evaluated for the desired parameter. :return: relative error. """ absolute_error = self.compute_error(**kwargs) return self._compute_relative_error(absolute_error, **kwargs) def _compute_relative_error(self, absolute_error, **kwargs): """ It computes the relative error of the reduced order approximation with respect to the full order one for the current value of mu. """ (components, inner_product ) = self._preprocess_compute_error_and_relative_error_kwargs(**kwargs) # Handle trivial case from compute_error if len(components) == 1: absolute_error_ = dict() absolute_error_[components[0]] = absolute_error absolute_error = absolute_error_ # Storage relative_error = dict() # Compute the relative error on the solution if len(components) > 0: truth_solution = self.truth_problem._solution for component in components: truth_solution_norm_squared_component = transpose( truth_solution) * inner_product[component] * truth_solution assert truth_solution_norm_squared_component >= 0. or isclose( truth_solution_norm_squared_component, 0.) if truth_solution_norm_squared_component != 0.: relative_error[ component] = absolute_error[component] / sqrt( abs(truth_solution_norm_squared_component)) else: if absolute_error[component] == 0.: relative_error[component] = 0. else: relative_error[component] = float("NaN") # Simplify trivial case if len(components) == 1: relative_error = relative_error[components[0]] # return relative_error def _preprocess_compute_error_and_relative_error_kwargs(self, **kwargs): """ This function returns the components and the inner products, picking them up from the kwargs or choosing default ones in case they are not defined yet. Internal method. :return: components and inner_product. """ # Set default components, if needed if "components" not in kwargs: kwargs["components"] = self.components # Set inner product for components, if needed if "inner_product" not in kwargs: inner_product = dict() if len(kwargs["components"]) > 1: for component in kwargs["components"]: assert len( self.truth_problem.inner_product[component]) == 1 inner_product[ component] = self.truth_problem.inner_product[ component][0] else: assert len(self.truth_problem.inner_product) == 1 inner_product[kwargs["components"] [0]] = self.truth_problem.inner_product[0] kwargs["inner_product"] = inner_product else: assert isinstance(kwargs["inner_product"], dict) assert set(kwargs["inner_product"].keys()) == set( kwargs["components"]) # return (kwargs["components"], kwargs["inner_product"]) # Compute the error of the reduced order output with respect to the full order one # for the current value of mu def compute_error_output(self, **kwargs): """ It returns the function _compute_error_output() evaluated for the desired parameter. :return: output error. """ self.truth_problem.solve(**kwargs) self.truth_problem.compute_output() return self._compute_error_output(**kwargs) # Internal method for output error computation def _compute_error_output(self, **kwargs): """ It computes the output error of the reduced order approximation with respect to the full order one for the current value of mu. """ # Skip if no output defined if self._output is NotImplemented: assert self.truth_problem._output is NotImplemented return NotImplemented else: # Compute the error on the output reduced_output = self._output truth_output = self.truth_problem._output error_output = abs(truth_output - reduced_output) return error_output # Compute the relative error of the reduced order approximation with respect to the full order one # for the current value of mu def compute_relative_error_output(self, **kwargs): """ It returns the function _compute_relative_error_output() evaluated for the desired parameter. :return: relative output error. """ absolute_error_output = self.compute_error_output(**kwargs) return self._compute_relative_error_output(absolute_error_output, **kwargs) # Internal method for output error computation def _compute_relative_error_output(self, absolute_error_output, **kwargs): """ It computes the realtive output error of the reduced order approximation with respect to the full order one for the current value of mu. """ # Skip if no output defined if self._output is NotImplemented: assert self.truth_problem._output is NotImplemented assert absolute_error_output is NotImplemented return NotImplemented else: # Compute the relative error on the output truth_output = abs(self.truth_problem._output) if truth_output != 0.: return absolute_error_output / truth_output else: if absolute_error_output == 0.: return 0. else: return float("NaN") def export_solution(self, folder=None, filename=None, solution=None, component=None, suffix=None): """ It exports reduced solution to file. :param folder: the folder into which we want to save the solution. :param filename: the name of the file to be saved. :param solution: the solution to be saved. :param component: the component of the of the solution to be saved. :param suffix: suffix to add to the name. """ if solution is None: solution = self._solution N = solution.N self.truth_problem.export_solution(folder, filename, self.basis_functions[:N] * solution, component, suffix) def export_error(self, folder=None, filename=None, component=None, suffix=None, **kwargs): self.truth_problem.solve(**kwargs) reduced_solution = self.basis_functions[:self._solution. N] * self._solution truth_solution = self.truth_problem._solution error_function = truth_solution - reduced_solution self.truth_problem.export_solution(folder, filename, error_function, component, suffix) def compute_theta(self, term): """ Return theta multiplicative terms of the affine expansion of the problem. :param term: the forms of the class of the problem. :return: computed thetas. """ return self.truth_problem.compute_theta(term) # Assemble the reduced order affine expansion def assemble_operator(self, term, current_stage="online"): """ Terms and respective thetas are assembled. :param term: the forms of the class of the problem. :param current_stage: online or offline stage. """ assert current_stage in ("online", "offline") if current_stage == "online": # load from file # Note that it would not be needed to return the loaded operator in # init(), since it has been already modified in-place. We do this, however, # because we want this interface to be compatible with the one in # EllipticCoerciveProblem, i.e. we would like to be able to use a reduced # problem also as a truth problem for a nested reduction if term in self.terms: self.operator[term].load(self.folder["reduced_operators"], "operator_" + term) return self.operator[term] elif term.startswith("inner_product"): component = term.replace("inner_product", "").replace("_", "") if component != "": assert component in self.components self.inner_product[component].load( self.folder["reduced_operators"], term) return self.inner_product[component] else: assert len(self.components) == 1 self.inner_product.load(self.folder["reduced_operators"], term) return self.inner_product elif term.startswith("projection_inner_product"): component = term.replace("projection_inner_product", "").replace("_", "") if component != "": assert component in self.components self.projection_inner_product[component].load( self.folder["reduced_operators"], term) return self.projection_inner_product[component] else: assert len(self.components) == 1 self.projection_inner_product.load( self.folder["reduced_operators"], term) return self.projection_inner_product elif term.startswith("dirichlet_bc"): raise ValueError( "There should be no need to assemble Dirichlet BCs when querying online reduced problems." ) else: raise ValueError("Invalid term for assemble_operator().") elif current_stage == "offline": # As in the previous case, there is no need to return anything because # we are still training the reduced order model, so the previous remark # (on the usage of a reduced problem as a truth one) cannot hold here. # However, in order to have a consistent interface we return the assembled # operator if term in self.terms: assert self.Q[term] == self.truth_problem.Q[term] for q in range(self.Q[term]): assert self.terms_order[term] in (0, 1, 2) if self.terms_order[term] == 2: self.operator[term][q] = transpose( self.basis_functions ) * self.truth_problem.operator[term][ q] * self.basis_functions elif self.terms_order[term] == 1: self.operator[term][q] = transpose( self.basis_functions ) * self.truth_problem.operator[term][q] elif self.terms_order[term] == 0: self.operator[term][q] = self.truth_problem.operator[ term][q] else: raise ValueError("Invalid value for order of term " + term) self.operator[term].save(self.folder["reduced_operators"], "operator_" + term) return self.operator[term] elif term.startswith("inner_product"): component = term.replace("inner_product", "").replace("_", "") if component != "": assert component in self.components assert len( self.inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix assert len( self.truth_problem.inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix self.inner_product[component][0] = transpose( self.basis_functions ) * self.truth_problem.inner_product[component][ 0] * self.basis_functions self.inner_product[component].save( self.folder["reduced_operators"], term) return self.inner_product[component] else: assert len(self.components) == 1 # single component case assert len( self.inner_product ) == 1 # the affine expansion storage contains only the inner product matrix assert len( self.truth_problem.inner_product ) == 1 # the affine expansion storage contains only the inner product matrix self.inner_product[0] = transpose( self.basis_functions ) * self.truth_problem.inner_product[ 0] * self.basis_functions self.inner_product.save(self.folder["reduced_operators"], term) return self.inner_product elif term.startswith("projection_inner_product"): component = term.replace("projection_inner_product", "").replace("_", "") if component != "": assert component in self.components assert len( self.projection_inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix assert len( self.truth_problem.projection_inner_product[component] ) == 1 # the affine expansion storage contains only the inner product matrix self.projection_inner_product[component][0] = transpose( self.basis_functions ) * self.truth_problem.projection_inner_product[component][ 0] * self.basis_functions self.projection_inner_product[component].save( self.folder["reduced_operators"], term) return self.projection_inner_product[component] else: assert len(self.components) == 1 # single component case assert len( self.projection_inner_product ) == 1 # the affine expansion storage contains only the inner product matrix assert len( self.truth_problem.projection_inner_product ) == 1 # the affine expansion storage contains only the inner product matrix self.projection_inner_product[0] = transpose( self.basis_functions ) * self.truth_problem.projection_inner_product[ 0] * self.basis_functions self.projection_inner_product.save( self.folder["reduced_operators"], term) return self.projection_inner_product elif term.startswith("dirichlet_bc"): component = term.replace("dirichlet_bc", "").replace("_", "") if component != "": assert component in self.components has_non_homogeneous_dirichlet_bc = self.dirichlet_bc[ component] and not self.dirichlet_bc_are_homogeneous[ component] else: assert len(self.components) == 1 component = None has_non_homogeneous_dirichlet_bc = self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous if has_non_homogeneous_dirichlet_bc: # Compute lifting functions for the value of mu possibly provided by the user Q_dirichlet_bcs = len(self.compute_theta(term)) # Temporarily override compute_theta method to return only one nonzero # theta term related to boundary conditions standard_compute_theta = self.truth_problem.compute_theta for i in range(Q_dirichlet_bcs): def modified_compute_theta(self, term_): if term_ == term: theta_bc = standard_compute_theta(term_) modified_theta_bc = list() for j in range(Q_dirichlet_bcs): if j != i: modified_theta_bc.append(0.) else: modified_theta_bc.append(theta_bc[i]) return tuple(modified_theta_bc) else: return standard_compute_theta(term_) PatchInstanceMethod(self.truth_problem, "compute_theta", modified_compute_theta).patch() # ... and store the solution of the truth problem corresponding to that boundary condition # as lifting function solve_message = "Computing and storing lifting function n. " + str( i) if component is not None: solve_message += " for component " + component solve_message += " (obtained for mu = " + str( self.mu) + ") in the basis matrix" print(solve_message) lifting = self._lifting_truth_solve(term, i) self.basis_functions.enrich(lifting, component=component) # Restore the standard compute_theta method self.truth_problem.compute_theta = standard_compute_theta else: raise ValueError("Invalid term for assemble_operator().") else: raise ValueError("Invalid stage in assemble_operator().") def _lifting_truth_solve(self, term, i): # Since lifting solves for different values of i are associated to the same parameter # but with a patched call to compute_theta(), which returns the i-th component, we set # a custom cache_key so that they are properly differentiated when reading from cache. lifting = self.truth_problem.solve(cache_key="lifting_" + str(i)) lifting /= self.compute_theta(term)[i] return lifting def get_stability_factor(self): """ Return a lower bound for the coercivity constant. """ return self.truth_problem.get_stability_factor()
class EIMApproximation(ParametrizedProblem): # Default initialization of members @sync_setters("truth_problem", "set_mu", "mu") @sync_setters("truth_problem", "set_mu_range", "mu_range") def __init__(self, truth_problem, parametrized_expression, folder_prefix, basis_generation): # Call the parent initialization ParametrizedProblem.__init__(self, folder_prefix) # Store the parametrized expression self.parametrized_expression = parametrized_expression self.truth_problem = truth_problem assert basis_generation in ("Greedy", "POD") self.basis_generation = basis_generation # $$ ONLINE DATA STRUCTURES $$ # # Online reduced space dimension self.N = 0 # Define additional storage for EIM self.interpolation_locations = parametrized_expression.create_interpolation_locations_container( ) # interpolation locations selected by the greedy (either a ReducedVertices or ReducedMesh) self.interpolation_matrix = OnlineAffineExpansionStorage( 1) # interpolation matrix # Solution self._interpolation_coefficients = None # OnlineFunction # $$ OFFLINE DATA STRUCTURES $$ # self.snapshot = None # will be filled in by Function, Vector or Matrix as appropriate in the EIM preprocessing self.snapshot_cache = dict() # of Function, Vector or Matrix # Basis functions container self.basis_functions = parametrized_expression.create_basis_container() # I/O self.folder["basis"] = os.path.join(self.folder_prefix, "basis") self.folder["cache"] = os.path.join(self.folder_prefix, "cache") self.folder["reduced_operators"] = os.path.join( self.folder_prefix, "reduced_operators") self.cache_config = config.get("EIM", "cache") # Initialize data structures required for the online phase def init(self, current_stage="online"): assert current_stage in ("online", "offline") # Read/Initialize reduced order data structures if current_stage == "online": self.interpolation_locations.load(self.folder["reduced_operators"], "interpolation_locations") self.interpolation_matrix.load(self.folder["reduced_operators"], "interpolation_matrix") self.basis_functions.load(self.folder["basis"], "basis") self.N = len(self.basis_functions) elif current_stage == "offline": # Nothing to be done pass else: raise ValueError("Invalid stage in init().") def evaluate_parametrized_expression(self): (cache_key, cache_file) = self._cache_key_and_file() if "RAM" in self.cache_config and cache_key in self.snapshot_cache: self.snapshot = self.snapshot_cache[cache_key] elif "Disk" in self.cache_config and self.import_solution( self.folder["cache"], cache_file): if "RAM" in self.cache_config: self.snapshot_cache[cache_key] = copy(self.snapshot) else: self.snapshot = evaluate(self.parametrized_expression) if "RAM" in self.cache_config: self.snapshot_cache[cache_key] = copy(self.snapshot) self.export_solution( self.folder["cache"], cache_file ) # Note that we export to file regardless of config options, because they may change across different runs def _cache_key_and_file(self): cache_key = self.mu cache_file = hashlib.sha1(str(cache_key).encode("utf-8")).hexdigest() return (cache_key, cache_file) # Perform an online solve. def solve(self, N=None): if N is None: N = self.N self._solve(self.parametrized_expression, N) return self._interpolation_coefficients def _solve(self, rhs_, N=None): if N is None: N = self.N if N > 0: self._interpolation_coefficients = OnlineFunction(N) # Evaluate the parametrized expression at interpolation locations rhs = evaluate(rhs_, self.interpolation_locations[:N]) (max_abs_rhs, _) = max(abs(rhs)) if max_abs_rhs == 0.: # If the rhs is zero, then we are interpolating the zero function # and the default zero coefficients are enough. pass else: # Extract the interpolation matrix lhs = self.interpolation_matrix[0][:N, :N] # Solve the interpolation problem solver = OnlineLinearSolver(lhs, self._interpolation_coefficients, rhs) solver.solve() else: self._interpolation_coefficients = None # OnlineFunction # Call online_solve and then convert the result of online solve from OnlineVector to a tuple def compute_interpolated_theta(self, N=None): interpolated_theta = self.solve(N) interpolated_theta_list = list() for theta in interpolated_theta: interpolated_theta_list.append(theta) if N is not None: # Make sure to append a 0 coefficient for each basis function # which has not been requested for n in range(N, self.N): interpolated_theta_list.append(0.0) return tuple(interpolated_theta_list) # Compute the interpolation error and/or its maximum location def compute_maximum_interpolation_error(self, N=None): if N is None: N = self.N # Compute the error (difference with the eim approximation) if N > 0: error = self.snapshot - self.basis_functions[:N] * self._interpolation_coefficients else: error = copy( self.snapshot) # need a copy because it will be rescaled # Get the location of the maximum error (maximum_error, maximum_location) = max(abs(error)) # Return return (error, maximum_error, maximum_location) def compute_maximum_interpolation_relative_error(self, N=None): (absolute_error, maximum_absolute_error, maximum_location) = self.compute_maximum_interpolation_error(N) (maximum_snapshot_value, _) = max(abs(self.snapshot)) return (absolute_error / maximum_snapshot_value, maximum_absolute_error / maximum_snapshot_value, maximum_location) # Export solution to file def export_solution(self, folder, filename, solution=None): if solution is None: solution = self.snapshot export(solution, folder, filename) # Import solution from file def import_solution(self, folder, filename, solution=None): if solution is None: if self.snapshot is None: self.snapshot = self.parametrized_expression.create_empty_snapshot( ) solution = self.snapshot return import_(solution, folder, filename)
class EIMApproximation(ParametrizedProblem): # Default initialization of members @sync_setters("truth_problem", "set_mu", "mu") @sync_setters("truth_problem", "set_mu_range", "mu_range") def __init__(self, truth_problem, parametrized_expression, folder_prefix, basis_generation): # Call the parent initialization ParametrizedProblem.__init__(self, folder_prefix) # Store the parametrized expression self.parametrized_expression = parametrized_expression self.truth_problem = truth_problem assert basis_generation in ("Greedy", "POD") self.basis_generation = basis_generation # $$ ONLINE DATA STRUCTURES $$ # # Online reduced space dimension self.N = 0 # Define additional storage for EIM self.interpolation_locations = parametrized_expression.create_interpolation_locations_container() # interpolation locations selected by the greedy (either a ReducedVertices or ReducedMesh) self.interpolation_matrix = OnlineAffineExpansionStorage(1) # interpolation matrix # Solution self._interpolation_coefficients = None # OnlineFunction # $$ OFFLINE DATA STRUCTURES $$ # self.snapshot = parametrized_expression.create_empty_snapshot() # Basis functions container self.basis_functions = parametrized_expression.create_basis_container() # I/O self.folder["basis"] = os.path.join(self.folder_prefix, "basis") self.folder["cache"] = os.path.join(self.folder_prefix, "cache") self.folder["reduced_operators"] = os.path.join(self.folder_prefix, "reduced_operators") def _snapshot_cache_key_generator(*args, **kwargs): assert args == self.mu assert len(kwargs) == 0 return self._cache_key() def _snapshot_cache_import(filename): snapshot = copy(self.snapshot) self.import_solution(self.folder["cache"], filename, snapshot) return snapshot def _snapshot_cache_export(filename): self.export_solution(self.folder["cache"], filename) def _snapshot_cache_filename_generator(*args, **kwargs): assert args == self.mu assert len(kwargs) == 0 return self._cache_file() self._snapshot_cache = Cache( "EIM", key_generator=_snapshot_cache_key_generator, import_=_snapshot_cache_import, export=_snapshot_cache_export, filename_generator=_snapshot_cache_filename_generator ) # Initialize data structures required for the online phase def init(self, current_stage="online"): assert current_stage in ("online", "offline") # Read/Initialize reduced order data structures if current_stage == "online": self.interpolation_locations.load(self.folder["reduced_operators"], "interpolation_locations") self.interpolation_matrix.load(self.folder["reduced_operators"], "interpolation_matrix") self.basis_functions.load(self.folder["basis"], "basis") self.N = len(self.basis_functions) elif current_stage == "offline": # Nothing to be done pass else: raise ValueError("Invalid stage in init().") def evaluate_parametrized_expression(self): try: assign(self.snapshot, self._snapshot_cache[self.mu]) except KeyError: self.snapshot = evaluate(self.parametrized_expression) self._snapshot_cache[self.mu] = copy(self.snapshot) def _cache_key(self): return self.mu def _cache_file(self): return hashlib.sha1(str(self._cache_key()).encode("utf-8")).hexdigest() # Perform an online solve. def solve(self, N=None): if N is None: N = self.N self._solve(self.parametrized_expression, N) return self._interpolation_coefficients def _solve(self, rhs_, N=None): if N is None: N = self.N if N > 0: self._interpolation_coefficients = OnlineFunction(N) # Evaluate the parametrized expression at interpolation locations rhs = evaluate(rhs_, self.interpolation_locations[:N]) (max_abs_rhs, _) = max(abs(rhs)) if max_abs_rhs == 0.: # If the rhs is zero, then we are interpolating the zero function # and the default zero coefficients are enough. pass else: # Extract the interpolation matrix lhs = self.interpolation_matrix[0][:N, :N] # Solve the interpolation problem solver = OnlineLinearSolver(lhs, self._interpolation_coefficients, rhs) solver.solve() else: self._interpolation_coefficients = None # OnlineFunction # Call online_solve and then convert the result of online solve from OnlineVector to a tuple def compute_interpolated_theta(self, N=None): interpolated_theta = self.solve(N) interpolated_theta_list = list() for theta in interpolated_theta: interpolated_theta_list.append(theta) if N is not None: # Make sure to append a 0 coefficient for each basis function # which has not been requested for n in range(N, self.N): interpolated_theta_list.append(0.0) return tuple(interpolated_theta_list) # Compute the interpolation error and/or its maximum location def compute_maximum_interpolation_error(self, N=None): if N is None: N = self.N # Compute the error (difference with the eim approximation) if N > 0: error = self.snapshot - self.basis_functions[:N]*self._interpolation_coefficients else: error = copy(self.snapshot) # need a copy because it will be rescaled # Get the location of the maximum error (maximum_error, maximum_location) = max(abs(error)) # Return return (error, maximum_error, maximum_location) def compute_maximum_interpolation_relative_error(self, N=None): (absolute_error, maximum_absolute_error, maximum_location) = self.compute_maximum_interpolation_error(N) (maximum_snapshot_value, _) = max(abs(self.snapshot)) if maximum_snapshot_value != 0.: return (absolute_error/maximum_snapshot_value, maximum_absolute_error/maximum_snapshot_value, maximum_location) else: if maximum_absolute_error == 0.: return (absolute_error, maximum_absolute_error, maximum_location) # the first two arguments are a zero expression and zero scalar else: return (None, float("NaN"), maximum_location) # the first argument should be a NaN expression # Export solution to file def export_solution(self, folder=None, filename=None, solution=None): if folder is None: folder = self.folder_prefix if filename is None: filename = "snapshot" if solution is None: solution = self.snapshot export(solution, folder, filename) # Import solution from file def import_solution(self, folder=None, filename=None, solution=None): if folder is None: folder = self.folder_prefix if filename is None: filename = "snapshot" if solution is None: solution = self.snapshot import_(solution, folder, filename)