def average_spacetime_guess(solver, W, U_pred, dt=None): ''' This method calculates the average space-time guess for the ADERDG predictor step. (This is the default approach for the guess) Inputs: ------- solver: solver object W: spatial polynomial solution [ne, nb, ns] U_pred: space-time polynomial coefficients [ne, nb_st, ns] Outputs: -------- U_pred: Space-time predicted polynomial coefficients [ne, nb_st, ns] ''' # Unpack physics = solver.physics basis = solver.basis elem_helpers = solver.elem_helpers quad_wts = elem_helpers.quad_wts basis_val = elem_helpers.basis_val djac_elems = elem_helpers.djac_elems vol_elems = elem_helpers.vol_elems # Calculate the average state for each element in spatial coordinates Wq = helpers.evaluate_state(W, basis_val, skip_interp=basis.skip_interp) W_bar = helpers.get_element_mean(Wq, quad_wts, djac_elems, vol_elems) U_pred[:] = W_bar return U_pred, W_bar # [ne, nb_st, ns]
def recalculate_jacobian_on(solver, U_pred, dt, Sjac=None): ''' Method to recalculate the jacobian at each subiteration of a nonlinear solver. Note: This has only been useful in very specific applications. Inputs: ------- solver: solver object U_pred: Space-time predicted polynomial coefficients [ne, nb_st, ns] dt: time step size Outputs: -------- Sjac: source term jacobian [nelem, ns, ns] ''' # Unpack physics = solver.physics elem_helpers = solver.elem_helpers elem_helpers_st = solver.elem_helpers_st ns = physics.NUM_STATE_VARS nelem = U_pred.shape[0] quad_wts_st = elem_helpers_st.quad_wts basis_val_st = elem_helpers_st.basis_val nq_t = elem_helpers_st.nq_tile_constant x_elems = elem_helpers.x_elems vol_elems = elem_helpers.vol_elems djac_elems = elem_helpers.djac_elems # Only evaluate Jacobian for stiff sources temp_sources = physics.source_terms.copy() physics.source_terms = physics.implicit_sources.copy() Uq = helpers.evaluate_state(U_pred, basis_val_st) U_bar = helpers.get_element_mean( Uq, quad_wts_st, np.tile(djac_elems, [1, nq_t, 1]) * dt / 2., dt * vol_elems) # Calculate the source term Jacobian using average state Sjac = Sjac.reshape([U_pred.shape[0], 1, ns, ns]) Sjac[:] = 0. # Sjac = np.zeros([U_pred.shape[0], 1, ns, ns]) Sjac = physics.eval_source_term_jacobians(U_bar, x_elems, solver.time, Sjac) Sjac = np.reshape(Sjac, [nelem, ns, ns]) # Set all sources for source_coeffs calculation physics.source_terms = temp_sources.copy()
def get_average_solution(physics, solver, x, basis, var_name): ''' This function evaluates the average numerical solution at a set of points and plots them on the average x location of each element. Inputs: ------- physics: physics object U: state polynomial coefficients [num_elems, num_basis_coeffs, num_state_vars] x: coordinates at which to evaluate solution [num_elems, num_pts, ndims], where num_pts is the number of sample points per element basis: basis object var_name: name of variable to get Outputs: ------- var_numer: values of variable obtained at x [num_elems, num_pts, 1] x_bar: average coordinates for each element [num_elems, 1, 1] ''' mesh = solver.mesh order = solver.order U = solver.state_coeffs if physics.NDIMS > 1: print("Plotting average state not available for 2D") raise NotImplementedError #Additions for Ubar solver.elem_helpers.get_gaussian_quadrature(mesh, physics, basis, 3*order) solver.elem_helpers.get_basis_and_geom_data(mesh, basis, 3*order) elem_helpers = solver.elem_helpers quad_wts = elem_helpers.quad_wts djacs = elem_helpers.djac_elems vols = elem_helpers.vol_elems Uq = helpers.evaluate_state(U, basis.basis_val) Ubar = helpers.get_element_mean(Uq, quad_wts, djacs, vols) var_numer = physics.compute_variable(var_name, Ubar) x_bar = np.sum(x, axis=1).reshape([x.shape[0], 1, 1])/x.shape[1] return var_numer, x_bar
def compute_variable(solver, Uc, var_name): ''' This function computes the desired variable at the element and face quadrature points, as well as the mean of the variable over the element. ''' # Interpolate state at quadrature points over element and on faces limiter = solver.limiters[0] basis = solver.basis physics = solver.physics U_elem_faces = helpers.evaluate_state(Uc, limiter.basis_val_elem_faces, skip_interp=basis.skip_interp) nq_elem = limiter.quad_wts_elem.shape[0] U_elem = U_elem_faces[:, :nq_elem, :] # Average value of state U_bar = helpers.get_element_mean(U_elem, limiter.quad_wts_elem, limiter.djac_elems, limiter.elem_vols) # Compute variable at quadrature points var_elem_faces = physics.compute_variable(var_name, U_elem_faces) # Compute mean var_bar = physics.compute_variable(var_name, U_bar) return var_elem_faces, var_bar
def limit_solution(self, solver, Uc): # Unpack ns = solver.physics.NUM_STATE_VARS physics = solver.physics mesh = solver.mesh elem_helpers = solver.elem_helpers int_face_helpers = solver.int_face_helpers vols = self.elem_vols basis_phys_grads = elem_helpers.basis_phys_grad_elems quad_wts = elem_helpers.quad_wts elemP_IDs = self.elemP_IDs elemM_IDs = self.elemM_IDs djacs = self.djac_elems djacP = self.djac_elems[elemP_IDs] djacM = self.djac_elems[elemM_IDs] # Interpolate state at quadrature points over element and on faces U_elem_faces = helpers.evaluate_state( Uc, self.basis_val_elem_faces, skip_interp=solver.basis.skip_interp) nq_elem = self.quad_wts_elem.shape[0] U_elem = U_elem_faces[:, :nq_elem, :] U_face = U_elem_faces[:, nq_elem:, :] # Average value of states U_bar = helpers.get_element_mean(U_elem, self.quad_wts_elem, djacs, vols) # Calculate the eigenvectors if available (they are pre-allocated in # precompute_helpers) get_eigenvector_function = getattr(physics, "get_conv_eigenvectors", None) if callable(get_eigenvector_function): self.right_eigen, self.left_eigen = \ physics.get_conv_eigenvectors(U_bar) Vc = np.einsum('elij, elj -> eli', self.left_eigen, Uc) # Determine if the elements requires limiting shock_indicated = self.shock_indicator(self, solver, Uc) # Unpack limiter info from shock indicator p0 = np.einsum('ebij, elj -> eli', self.left_eigen, self.Um_elem) p1 = np.einsum('ebij, elj -> eli', self.left_eigen, self.U_elem) p2 = np.einsum('ebij, elj -> eli', self.left_eigen, self.Up_elem) p0_bar = np.einsum('ebij, elj -> eli', self.left_eigen, self.Um_bar) p1_bar = np.einsum('ebij, elj -> eli', self.left_eigen, self.U_bar) p2_bar = np.einsum('ebij, elj -> eli', self.left_eigen, self.Up_bar) # Check basis type and adjust coefficients to maintain element # p1's average value. if solver.basis.MODAL_OR_NODAL == general.ModalOrNodal.Modal: p0_tilde = p0 p2_tilde = p2 p0_tilde[:, 0, :] = p1_bar[:, 0, :] p2_tilde[:, 0, :] = p1_bar[:, 0, :] else: p0_tilde = p0 - p0_bar + p1_bar p2_tilde = p2 - p2_bar + p1_bar # Currently only implemented up to P2 if solver.order > 2: raise NotImplementedError # Allocate weno_wts weno_wts = np.zeros([Uc.shape[0], 3, ns]) # Calculate non-linear weights weno_wts[:, 0, :] = self.get_nonlinearwts(solver.order, p0_tilde, 0.001, basis_phys_grads, quad_wts, vols, djacM) weno_wts[:, 1, :] = self.get_nonlinearwts(solver.order, p1, 0.998, basis_phys_grads, quad_wts, vols, djacs) weno_wts[:, 2, :] = self.get_nonlinearwts(solver.order, p2_tilde, 0.001, basis_phys_grads, quad_wts, vols, djacP) # Normalize the weights normal_wts = weno_wts / np.sum(weno_wts, axis=1).reshape( [Uc.shape[0], 1, ns]) # Update state_coeffs for indicated elements Vc[shock_indicated] = \ np.einsum('ik, ijk -> ijk', normal_wts[shock_indicated, 0], p0_tilde[shock_indicated]) + \ np.einsum('ik, ijk -> ijk', normal_wts[shock_indicated, 1], p1[shock_indicated]) + \ np.einsum('ik, ijk -> ijk', normal_wts[shock_indicated, 2], p2_tilde[shock_indicated]) # Transform characteristic variables back to physical. Uc[shock_indicated] = np.einsum('ebij, elj -> eli', self.right_eigen[shock_indicated], Vc[shock_indicated]) return Uc # [ne, nq, ns]
def limit_solution(self, solver, Uc): # Unpack physics = solver.physics elem_helpers = solver.elem_helpers int_face_helpers = solver.int_face_helpers basis = solver.basis djac = self.djac_elems # Interpolate state at quadrature points over element and on faces U_elem_faces = helpers.evaluate_state(Uc, self.basis_val_elem_faces, skip_interp=basis.skip_interp) nq_elem = self.quad_wts_elem.shape[0] U_elem = U_elem_faces[:, :nq_elem, :] # Average value of state U_bar = helpers.get_element_mean(U_elem, self.quad_wts_elem, djac, self.elem_vols) ne = self.elem_vols.shape[0] # Density and pressure from averaged state rho_bar = physics.compute_variable(self.var_name1, U_bar) p_bar = physics.compute_variable(self.var_name2, U_bar) rhoY_bar = physics.compute_variable(self.var_name3, U_bar) if np.any(rho_bar < 0.) or np.any(p_bar < 0.) or np.any(rhoY_bar < 0.): raise errors.NotPhysicalError # Ignore divide-by-zero np.seterr(divide='ignore') ''' Limit density ''' # Compute density rho_elem_faces = physics.compute_variable(self.var_name1, U_elem_faces) # Check if limiting is needed theta = np.abs((rho_bar - POS_TOL) / (rho_bar - rho_elem_faces)) # Truncate theta1; otherwise, can get noticeably different # results across machines, possibly due to poor conditioning in its # calculation theta1 = trunc(np.minimum(1., np.min(theta, axis=1))) irho = physics.get_state_index(self.var_name1) # Get IDs of elements that need limiting elem_IDs = np.where(theta1 < 1.)[0] # Modify density coefficients if basis.MODAL_OR_NODAL == general.ModalOrNodal.Nodal: Uc[elem_IDs, :, irho] = theta1[elem_IDs]*Uc[elem_IDs, :, irho] \ + (1. - theta1[elem_IDs])*rho_bar[elem_IDs, 0] elif basis.MODAL_OR_NODAL == general.ModalOrNodal.Modal: Uc[elem_IDs, :, irho] *= theta1[elem_IDs] Uc[elem_IDs, 0, irho] += (1. - theta1[elem_IDs, 0]) * rho_bar[elem_IDs, 0, 0] else: raise NotImplementedError if np.any(theta1 < 1.): # Intermediate limited solution U_elem_faces = helpers.evaluate_state( Uc, self.basis_val_elem_faces, skip_interp=basis.skip_interp) ''' Limit mass fraction ''' rhoY_elem_faces = physics.compute_variable(self.var_name3, U_elem_faces) theta = np.abs(rhoY_bar / (rhoY_bar - rhoY_elem_faces + POS_TOL)) # Truncate theta2; otherwise, can get noticeably different # results across machines, possibly due to poor conditioning in its # calculation theta2 = trunc(np.minimum(1., np.amin(theta, axis=1))) irhoY = physics.get_state_index(self.var_name3) # Get IDs of elements that need limiting elem_IDs = np.where(theta2 < 1.)[0] # Modify density coefficients if basis.MODAL_OR_NODAL == general.ModalOrNodal.Nodal: Uc[elem_IDs, :, irhoY] = theta2[elem_IDs] * Uc[elem_IDs, :, irhoY] + ( 1. - theta2[elem_IDs]) * rho_bar[elem_IDs, 0] elif basis.MODAL_OR_NODAL == general.ModalOrNodal.Modal: Uc[elem_IDs, :, irhoY] *= theta2[elem_IDs] Uc[elem_IDs, 0, irhoY] += (1. - theta2[elem_IDs, 0]) * rho_bar[elem_IDs, 0, 0] else: raise NotImplementedError if np.any(theta2 < 1.): U_elem_faces = helpers.evaluate_state( Uc, self.basis_val_elem_faces, skip_interp=basis.skip_interp) ''' Limit pressure ''' # Compute pressure at quadrature points p_elem_faces = physics.compute_variable(self.var_name2, U_elem_faces) theta[:] = 1. # Indices where pressure is negative negative_p_indices = np.where(p_elem_faces < 0.) elem_IDs = negative_p_indices[0] i_neg_p = negative_p_indices[1] theta[elem_IDs, i_neg_p] = p_bar[elem_IDs, :, 0] / ( p_bar[elem_IDs, :, 0] - p_elem_faces[elem_IDs, i_neg_p]) # Truncate theta3; otherwise, can get noticeably different # results across machines, possibly due to poor conditioning in its # calculation theta3 = trunc(np.min(theta, axis=1)) # Get IDs of elements that need limiting elem_IDs = np.where(theta3 < 1.)[0] # Modify coefficients if basis.MODAL_OR_NODAL == general.ModalOrNodal.Nodal: Uc[elem_IDs] = np.einsum( 'im, ijk -> ijk', theta3[elem_IDs], Uc[elem_IDs]) + np.einsum( 'im, ijk -> ijk', 1 - theta3[elem_IDs], U_bar[elem_IDs]) elif basis.MODAL_OR_NODAL == general.ModalOrNodal.Modal: Uc[elem_IDs] *= np.expand_dims(theta3[elem_IDs], axis=2) Uc[elem_IDs, 0] += np.einsum('im, ijk -> ik', 1 - theta3[elem_IDs], U_bar[elem_IDs]) else: raise NotImplementedError np.seterr(divide='warn') return Uc # [ne, nq, ns]
def minmod_shock_indicator(limiter, solver, Uc): ''' TVB modified Minmod calculation used to detect shocks Inputs: ------- limiter: limiter object solver: solver object Uc: state coefficients [ne, nb, ns] Outputs: -------- shock_elems: array with IDs of elements flagged for limiting ''' # Unpack physics = solver.physics mesh = solver.mesh tvb_param = limiter.tvb_param ns = physics.NUM_STATE_VARS elem_helpers = solver.elem_helpers int_face_helpers = solver.int_face_helpers elemP_IDs = limiter.elemP_IDs elemM_IDs = limiter.elemM_IDs djacs = limiter.djac_elems djacP = limiter.djac_elems[elemP_IDs] djacM = limiter.djac_elems[elemM_IDs] UcP = solver.state_coeffs[elemP_IDs] UcM = solver.state_coeffs[elemM_IDs] # Interpolate state at quadrature points over element and on faces U_elem_faces = helpers.evaluate_state(Uc, limiter.basis_val_elem_faces, skip_interp=solver.basis.skip_interp) nq_elem = limiter.quad_wts_elem.shape[0] U_elem = U_elem_faces[:, :nq_elem, :] U_face = U_elem_faces[:, nq_elem:, :] if solver.basis.skip_interp is True: U_face = np.zeros([U_elem.shape[0], 2, ns]) U_face[:, 0, :] = U_elem[:, 0, :] U_face[:, 1, :] = U_elem[:, -1, :] # Average value of states U_bar = helpers.get_element_mean(U_elem, limiter.quad_wts_elem, djacs, limiter.elem_vols) # UcP neighbor evaluated at quadrature points Up_elem = helpers.evaluate_state(UcP, elem_helpers.basis_val, skip_interp=solver.basis.skip_interp) # Average value of state Up_bar = helpers.get_element_mean(Up_elem, limiter.quad_wts_elem, djacP, limiter.elem_vols[elemP_IDs]) # UcM neighbor evaluated at quadrature points Um_elem = helpers.evaluate_state(UcM, elem_helpers.basis_val, skip_interp=solver.basis.skip_interp) # Average value of state Um_bar = helpers.get_element_mean(Um_elem, limiter.quad_wts_elem, djacM, limiter.elem_vols[elemM_IDs]) # Unpack the left and right eigenvector matrices right_eigen = limiter.right_eigen left_eigen = limiter.left_eigen # Store the polynomial coeff values for Up, Um, and U. limiter.U_elem = Uc limiter.Up_elem = UcP limiter.Um_elem = UcM # Store the average values for Up, Um, and U. limiter.U_bar = U_bar limiter.Up_bar = Up_bar limiter.Um_bar = Um_bar U_tilde = (U_face[:, 1, :] - U_bar[:, 0, :]).reshape(U_bar.shape) U_dtilde = (U_bar[:, 0, :] - U_face[:, 0, :]).reshape(U_bar.shape) deltaP_u_bar = Up_bar - U_bar deltaM_u_bar = U_bar - Um_bar aj = np.zeros([U_tilde.shape[0], 3, ns]) aj[:, 0, :] = U_tilde[:, 0, :] aj[:, 1, :] = deltaP_u_bar[:, 0, :] aj[:, 2, :] = deltaM_u_bar[:, 0, :] u_tilde_mod = minmod(aj) tvb = np.where(np.abs(aj[:, 0, :]) <= tvb_param * \ limiter.elem_vols[0]**2)[0] u_tilde_mod[tvb, 0] = aj[tvb, 0, :] aj = np.zeros([U_dtilde.shape[0], 3, ns]) aj[:, 0, :] = U_dtilde[:, 0, :] aj[:, 1, :] = deltaP_u_bar[:, 0, :] aj[:, 2, :] = deltaM_u_bar[:, 0, :] u_dtilde_mod = minmod(aj) tvd = np.where(np.abs(aj[:, 0, :]) <= tvb_param * \ limiter.elem_vols[0]**2)[0] u_dtilde_mod[tvd, 0] = aj[tvd, 0, :] check1 = u_tilde_mod - U_tilde check2 = u_dtilde_mod - U_dtilde shock_elems = np.where((np.abs(check1[:, :, 0]) > 1.e-12) | (np.abs(check2[:, :, 0]) > 1.e-12))[0] # print(shock_elems) return shock_elems
def predictor_elem_stiffimplicit(solver, dt, W, U_pred): ''' Calculates the predicted solution state for the ADER-DG method using a nonlinear solve of the weak form of the DG discretization in time. This function utilizes scipy's root solver (specifically 'hybr' or a modified Powell method) to converge the nonlinear solver. For this method to be effecient, the user should also select the ODEGuess to provide the initial condition to the nonlinear solver. This is suitable for very stiff systems such as those observed in chemically reacting flows. Inputs: ------- solver: solver object dt: time step W: previous time step solution in space only [ne, nb, ns] Outputs: -------- U_pred: predicted solution in space-time [ne, nb_st, ns] ''' # Unpack threshold = solver.params["PredictorThreshold"] physics = solver.physics ns = physics.NUM_STATE_VARS mesh = solver.mesh basis = solver.basis basis_st = solver.basis_st elem_helpers = solver.elem_helpers ader_helpers = solver.ader_helpers order = solver.order quad_wts = elem_helpers.quad_wts basis_val = elem_helpers.basis_val djac_elems = elem_helpers.djac_elems FTR = ader_helpers.FTR MM = ader_helpers.MM SMS_elems = ader_helpers.SMS_elems iK = ader_helpers.iK # Calculate the average state for each element in spatial coordinates vol_elems = elem_helpers.vol_elems Wq = helpers.evaluate_state(W, basis_val, skip_interp=basis.skip_interp) W_bar = helpers.get_element_mean(Wq, quad_wts, djac_elems, vol_elems) # Initialize space-time coefficients U_pred, U_bar = solver.get_spacetime_guess(solver, W, U_pred, dt=dt) # Calculate the source and flux coefficients with initial guess source_coeffs = solver.source_coefficients(dt, order, basis_st, U_pred) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, U_pred) def rhs_weakform(q): ''' Solves the weak form of the DG discretization while doing integration by parts on the temporal term. Inputs: ------- q: Space-time polynomial coffecients [ne x nb_st x ns] Outputs: -------- zero: The rhs of the nonlinear solver should be zero [ne x nb_st x ns] ''' q = q.reshape([U_pred.shape[0], U_pred.shape[1], U_pred.shape[2]]) source_coeffs = solver.source_coefficients(dt, order, basis_st, q) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, q) zero = np.einsum( 'jk, ikm -> ijm', iK, np.einsum('jk, ikl -> ijl', MM, source_coeffs) - np.einsum('ijkl, ikml -> ijm', SMS_elems, flux_coeffs) + np.einsum('jk, ikm -> ijm', FTR, W)) - q q.reshape(-1) # reshape for the nonlinear solver return zero.reshape(-1) # reshape for the nonlinear solver # Iterate using root function sol = root(rhs_weakform, U_pred.reshape(-1), tol=1e-15, jac=None, method='hybr', options={ 'maxfev': 50000, 'xtol': 1e-15 }) U_pred = np.copy(sol.x.reshape([U_pred.shape[0], U_pred.shape[1], ns])) # Note: Other nonlinear solvers could be more efficient. Further work is # needed to determine the most efficient method. Commented code below # is another approach. # sol = newton_krylov(fun, U_pred.reshape(-1), iter=None, # rdiff=None, method='lgmres', maxiter=100) # U_pred = np.copy(sol.reshape([U_pred.shape[0], U_pred.shape[1], ns])) return U_pred # [ne, nb_st, ns]
def predictor_elem_implicit(solver, dt, W, U_pred): ''' Calculates the predicted solution state for the ADER-DG method using a nonlinear solve of the weak form of the DG discretization in time. This function applies the source term implicitly. Appropriate for stiff systems of equations. The implicit solve utilizes the Sylvester equation of the form: AX + XB = C This is a built-in function via the scipy.linalg library. Inputs: ------- solver: solver object dt: time step W: previous time step solution in space only [ne, nb, ns] Outputs: -------- U_pred: predicted solution in space-time [ne, nb_st, ns] ''' # Unpack threshold = solver.params["PredictorThreshold"] physics = solver.physics source_terms = physics.source_terms ns = physics.NUM_STATE_VARS mesh = solver.mesh basis = solver.basis basis_st = solver.basis_st order = solver.order elem_helpers = solver.elem_helpers ader_helpers = solver.ader_helpers quad_wts = elem_helpers.quad_wts basis_val = elem_helpers.basis_val djac_elems = elem_helpers.djac_elems x_elems = elem_helpers.x_elems FTR = ader_helpers.FTR iMM = ader_helpers.iMM SMS_elems = ader_helpers.SMS_elems K = ader_helpers.K # Initialize space-time coefficients U_pred, U_bar = solver.get_spacetime_guess(solver, W, U_pred, dt=dt) # Get physical average for testing purposes vol_elems = elem_helpers.vol_elems Wq = helpers.evaluate_state(W, basis_val, skip_interp=basis.skip_interp) W_bar = helpers.get_element_mean(Wq, quad_wts, djac_elems, vol_elems) # Only evaluate Jacobian for stiff sources temp_sources = physics.source_terms.copy() physics.source_terms = physics.implicit_sources.copy() # Calculate the source term Jacobian using average state Sjac = np.zeros([U_pred.shape[0], 1, ns, ns]) Sjac = physics.eval_source_term_jacobians(W_bar, x_elems, solver.time, Sjac) Sjac = Sjac[:, 0, :, :] # Set all sources for source_coeffs calculation physics.source_terms = temp_sources.copy() # Calculate the source and flux coefficients with initial guess source_coeffs = solver.source_coefficients(dt, order, basis_st, U_pred) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, U_pred) # Iterate using a nonlinear Sylvester solver for the # updated space-time coefficients. Solves for X in the form: # AX + XB = C # Update: We now transform AX+XB=C into KX=C using kronecker # products. niter = 10000 A = np.matmul(iMM, K) U_pred_new = np.zeros_like(U_pred) for i in range(niter): B = -1.0 * dt * Sjac.transpose(0, 2, 1) Q = np.einsum('jk, ikm -> ijm', FTR, W) - np.einsum( 'ijkl, ikml -> ijm', SMS_elems, flux_coeffs) C = source_coeffs - dt*np.matmul(U_pred[:], Sjac[:].transpose(0, 2, 1)) + \ np.einsum('jk, ikl -> ijl', iMM, Q) # Build identity matrices for kronecker procucts I2 = np.eye(A.shape[1]) I1 = np.eye(B.shape[1]) for ie in range(U_pred.shape[0]): # Conduct kronecker products to transfrom Ax+xB=C system to Ax=b kronecker = np.kron(I1, A) + np.kron(B[ie, :, :].transpose(), I2) U_pred_hold = np.linalg.solve(kronecker, C[ie, :, :].transpose().reshape(-1)) U_pred_new[ie, :, :] = U_pred_hold.reshape( U_pred.shape[2], U_pred.shape[1]).transpose() # Note: Previous implementaion used sylvester solve directly. # This still requires further testing to determine which is # more efficient. # U_pred_new[ie, :, :] = solve_sylvester(A, B[ie, :, :], # C[ie, :, :]) # We check when the coefficients are no longer changing. # This can lead to differences between NODAL and MODAL solutions. # This could be resolved by evaluating at the quadrature points # and comparing the error between those values. err = U_pred_new - U_pred if (np.amax(np.abs(err)) < threshold): print("Predictor iterations: ", i) U_pred = np.copy(U_pred_new) break U_pred = np.copy(U_pred_new) source_coeffs = solver.source_coefficients(dt, order, basis_st, U_pred) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, U_pred) # Recalculate jacobian for subiterations (Default is OFF) solver.recalculate_jacobian(solver, U_pred, dt, Sjac) if i == niter - 1: print('Sub-iterations not converging', np.amax(np.abs(err))) return U_pred #_update # [ne, nb_st, ns]
def predictor_elem_explicit(solver, dt, W, U_pred): ''' Calculates the predicted solution state for the ADER-DG method using a nonlinear solve of the weak form of the DG discretization in time. This function treats the source term explicitly. Appropriate for non-stiff systems. Inputs: ------- solver: solver object dt: time step W: previous time step solution in space only [ne, nb, ns] Outputs: -------- U_pred: predicted solution in space-time [ne, nb_st, ns] ''' # Unpack threshold = solver.params["PredictorThreshold"] physics = solver.physics ns = physics.NUM_STATE_VARS mesh = solver.mesh basis = solver.basis basis_st = solver.basis_st elem_helpers = solver.elem_helpers elem_helpers_st = solver.elem_helpers_st ader_helpers = solver.ader_helpers nq_tile_constant = elem_helpers_st.nq_tile_constant basis_ref_grad = elem_helpers.basis_ref_grad basis_ref_grad_st = elem_helpers_st.basis_ref_grad order = solver.order quad_wts = elem_helpers.quad_wts basis_val = elem_helpers.basis_val djac_elems = elem_helpers.djac_elems FTR = ader_helpers.FTR MM = ader_helpers.MM SMS_elems = ader_helpers.SMS_elems iK = ader_helpers.iK # Calculate the average state for each element in spatial coordinates vol_elems = elem_helpers.vol_elems Wq = helpers.evaluate_state(W, basis_val, skip_interp=basis.skip_interp) W_bar = helpers.get_element_mean(Wq, quad_wts, djac_elems, vol_elems) # Initialize space-time coefficients U_pred, U_bar = solver.get_spacetime_guess(solver, W, U_pred, dt=dt) # Calculate the source and flux coefficients with initial guess source_coeffs = solver.source_coefficients(dt, order, basis_st, U_pred) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, U_pred) # Iterate using a discrete Picard nonlinear solve for the # updated space-time coefficients. niter = 100 for i in range(niter): U_pred_new = iK @ ( MM @ source_coeffs - \ smsflux(SMS_elems, flux_coeffs) + FTR @ W ) # We check when the coefficients are no longer changing. # This can lead to differences between NODAL and MODAL solutions. # This could be resolved by evaluating at the quadrature points # and comparing the error between those values. err = U_pred_new - U_pred if np.amax(np.abs(err)) < threshold: U_pred = U_pred_new print("Predictor iterations: ", i) break U_pred = np.copy(U_pred_new) source_coeffs = solver.source_coefficients(dt, order, basis_st, U_pred) flux_coeffs = solver.flux_coefficients(dt, order, basis_st, U_pred) if i == niter - 1: print('Sub-iterations not converging', np.amax(np.abs(err))) raise ValueError('Sub-iterations not converging') return U_pred # [ne, nb_st, ns]
def spacetime_odeguess(solver, W, U_pred, dt=None): ''' This method sets the space-time guess to the predictor step in the ADER-DG scheme by using a built-in ODE solver from scipy. We solve the stationary ODE in time and use the result to construct our guess to the non-linear solver NOTE: Current implementation only supports ODEs Inputs: ------- solver: solver object W: spatial polynomial solution [ne, nb, ns] U_pred: space-time polynomial coefficients [ne, nb_st, ns] Outputs: -------- U_pred: Space-time predicted polynomial coefficients [ne, nb_st, ns] U_bar: Space-time average value [ne, 1, ns] ''' # Unpack physics = solver.physics ns = physics.NUM_STATE_VARS mesh = solver.mesh basis = solver.basis basis_st = solver.basis_st elem_helpers = solver.elem_helpers elem_helpers_st = solver.elem_helpers_st ader_helpers = solver.ader_helpers iMM_elems = ader_helpers.iMM_elems quad_wts = elem_helpers.quad_wts quad_pts = elem_helpers.quad_pts basis_val = elem_helpers.basis_val basis_val_st = elem_helpers_st.basis_val djac_elems = elem_helpers.djac_elems x_elems = elem_helpers.x_elems nelem = W.shape[0] quad_pts_st = elem_helpers_st.quad_pts quad_wts_st = elem_helpers_st.quad_wts nq_st = quad_wts_st.shape[0] nq_t = elem_helpers_st.nq_tile_constant vol_elems = elem_helpers.vol_elems # Evaluate spatial coeffs on spatial quadrature points Wq = helpers.evaluate_state(W, basis_val, skip_interp=basis.skip_interp) # Allocate memory for the guess at the quadrature points Uq_guess = np.zeros([nelem, nq_st, ns]) Uq_guess = np.tile(Wq, [1, Wq.shape[1], 1]) # Build ref temporal array for space-time element t, elem_helpers_st.basis_time = ref_to_phys_time( mesh, solver.time, dt, quad_pts[:, -1:], elem_helpers_st.basis_time) # Build phys time array for space-time element tphys, elem_helpers_st.basis_time = ref_to_phys_time( mesh, solver.time, dt, ader_helpers.x_elems[0, 0:2, :], elem_helpers_st.basis_time) W0, t0 = Wq.reshape(-1), solver.time def func(t, y, x, Sq_exp): ''' Function for the ode solver to calculate the RHS Inputs: ------- t: time y: solution array x: quadrature points Sq_exp: explicit source term evaluated at quadrature points ''' # Keep track of the number of times func is called tvals.append(t) # Evaluate the source term at the quadrature points Sq = np.zeros([U_pred.shape[0], x.shape[1], ns]) y = y.reshape(Sq.shape) Sq = Sq_exp + physics.eval_source_terms(y, x, t, Sq) # NOTE: This function currently does not include the flux evaluation. # It will need to be added for the guess to be correct for more # complicated systems. Current test cases that require this are # only ODE cases. return Sq.reshape(-1) # Evaluate source terms to be taken explicitly temp_sources = physics.source_terms.copy() physics.source_terms = physics.explicit_sources.copy() Sq_exp = np.zeros([U_pred.shape[0], x_elems.shape[1], ns]) Sq_exp = physics.eval_source_terms(Wq, x_elems, t, Sq_exp) # Set implicit sources only for stiff ODE evaluation physics.source_terms = physics.implicit_sources.copy() # Initialize the integrator r = ode(func, jac=None) r.set_integrator('lsoda', nsteps=50000, atol=1e-14, rtol=1e-12) r.set_initial_value(W0, t0).set_f_params(x_elems, Sq_exp) # Set constants for managing data and begin ODE integration loop i = 0 j = 0 # Run the ODEsolver guess while r.successful() and j < t.shape[0]: # Length of tvals represents number of ODE interations per # timestep between two quadrature points in time tvals = [] # Runs the integrator value = r.integrate(r.t + (t[j] - r.t)) # Populate the data into the guess Uq_guess[:,i:t.shape[0]*j+t.shape[0],:] = \ value.reshape([nelem, t.shape[0], ns]) i += t.shape[0] j += 1 tvals = np.unique(tvals) solver.count_evaluations += len(tvals) # Prints the number of ODE iterations print("Steps/quadrature point: ", len(tvals)) physics.source_terms = temp_sources # Get space-time average from initial guess U_bar = helpers.get_element_mean( Uq_guess, quad_wts_st, np.tile(djac_elems, [1, nq_t, 1]) * dt / 2., dt * vol_elems) # Project the guess at the space-time quadrature points to the # state coefficient's initial guess L2_projection(mesh, iMM_elems, solver.basis_st, quad_pts_st, quad_wts_st, np.tile(djac_elems, [1, nq_t, 1]), Uq_guess, U_pred) return U_pred, U_bar