from .common import * def u_dot_muvec(mu, eta, w): """ Dot product of u = (cos(w), sin(w)) with the coordinate vector for mu. Args: mu (float or array-like): The mu coordinate(s). eta (float or array-like): The eta coordinate(s). w (float or array-like): The w coordinate(s). Returns: numpy.ndarray: The calculated dot product for each point. """ return (0.5 * np.cosh(mu) * np.cos(eta - w) + 0.5 * np.cosh(mu) * np.cos(eta + w) + 0.5 * np.sinh(mu) * np.cos(eta - w) - 0.5 * np.sinh(mu) * np.cos(eta + w)) def u_dot_etavec(mu, eta, w): """ Dot product of u = (cos(w), sin(w)) with the coordinate vector for eta. Args: mu (float or array-like): The mu coordinate(s). eta (float or array-like): The eta coordinate(s). w (float or array-like): The w coordinate(s). Returns: numpy.ndarray: The calculated dot product for each point. """ return (-0.5 * np.sinh(mu) * np.sin(eta + w) - 0.5 * np.sinh(mu) * np.sin(eta - w) + 0.5 * np.cosh(mu) * np.sin(eta + w) - 0.5 * np.cosh(mu) * np.sin(eta - w)) def get_system_ellipse(config): """ For an elliptical geometry, constructs the matrices, grids, and other quantities corresponding to the PDE system specified by "config"". Args: config (dict): Configuration dictionary containing the system parameters. Returns: tuple: A tuple containing matrices, grids, etc. for the PDE system """ required_keys = ['nx1', 'nx2', 'nx3', 'ell', 'a2', 'eccentricity'] optional_keys = [] missing_keys = [key for key in required_keys if key not in config] if missing_keys: raise KeyError(f"Missing keys in config: {', '.join(missing_keys)}") unused_keys = [key for key in config if key not in required_keys + optional_keys] if unused_keys: warnings.warn(f"Unused keys in config: {', '.join(unused_keys)}") for key in ['nx1', 'nx2', 'nx3']: if not isinstance(config[key], int): raise TypeError(f"{key} must be an integer.") for key in ['nx1', 'nx2', 'nx3', 'ell']: if config[key] <= 0: raise ValueError(f"{key} must be positive.") if not (0 <= config['eccentricity'] < 1.0): raise ValueError('eccentricity must be >= 0 and < 1.') if config['a2'] <= config['eccentricity']: raise ValueError(f'a2 must be greater than the eccentricity.') nmu, neta, nw = config['nx1'], config['nx2'], config['nx3'] minor_axis_outer = config['a2'] ell = config['ell'] minor_axis = 1.0 - config['eccentricity'] major_axis = 1.0 focal_distance = np.sqrt(major_axis**2 - minor_axis**2) mu1 = np.arccosh(major_axis / focal_distance) major_axis_outer = np.sqrt(focal_distance**2 + minor_axis_outer**2) mu2 = np.arccosh(major_axis_outer / focal_distance) # 1D grids eta, w = get_angular_grids(neta, nw) # mu grid: non-uniform spacing and Dirichlet boundary conditions y = np.linspace(-np.pi / 2, np.pi / 2, nmu + 2, dtype = np.float64) mu_ = np.log((np.exp(mu2) - np.exp(mu1)) * (np.sin(y) / 2 + 0.5) + np.exp(mu1)) dmu1 = mu_[1] - mu_[0] dmu2 = mu_[-1] - mu_[-2] mu = mu_[1:-1] # 1D finite-difference operators Deta_minus, Deta_plus = d_dx_upwind(eta, neta) D2w = d2_dx2_fourth_order(w, nw) Dmu_minus, Dmu_plus = d_dx_upwind_nonuniform(mu_, nmu) # 3D quantities. Kronecker products are used to build the 3D difference operators mu_3D, eta_3D, w_3D = np.meshgrid(mu, eta, w, indexing = 'ij') I_mu = sp.eye(nmu) I_eta = sp.eye(neta) I_w = sp.eye(nw) Deta_3D_minus = sp.kron(sp.kron(I_mu, Deta_minus), I_w) Deta_3D_plus = sp.kron(sp.kron(I_mu, Deta_plus), I_w) D2w_3D = sp.kron(sp.kron(I_mu, I_eta), D2w) Dmu_3D_minus = sp.kron(sp.kron(Dmu_minus, I_eta), I_w) Dmu_3D_plus = sp.kron(sp.kron(Dmu_plus, I_eta), I_w) # Metric tensor. Note that g_12 = g_21 = 0 and g_11 = g_22. g_11 = focal_distance * (np.cosh(mu_3D) * np.cosh(mu_3D) * np.cos(eta_3D) * np.cos(eta_3D) + np.sinh(mu_3D) * np.sinh(mu_3D) * np.sin(eta_3D) * np.sin(eta_3D)) # Dot products dp_mu = u_dot_muvec(mu_3D, eta_3D, w_3D) dp_eta = u_dot_etavec(mu_3D, eta_3D, w_3D) # Coefficient of d / dmu Dmu_3D_coeff_meshgrid = dp_mu / g_11 test_ill_conditioned(Dmu_3D_coeff_meshgrid) Dmu_3D_coeff = sp.diags(Dmu_3D_coeff_meshgrid.ravel()) # Coefficient of d / deta Deta_3D_coeff_meshgrid = dp_eta / g_11 Deta_3D_coeff = sp.diags(Deta_3D_coeff_meshgrid.ravel()) # Upwind differencing Dmu_3D_upwind = upwind_operator(Dmu_3D_minus, Dmu_3D_plus, Dmu_3D_coeff_meshgrid) Deta_3D_upwind = upwind_operator(Deta_3D_minus, Deta_3D_plus, Deta_3D_coeff_meshgrid) # Full operator L = Dmu_3D_coeff @ Dmu_3D_upwind + Deta_3D_coeff @ Deta_3D_upwind - (1 / ell) * D2w_3D return L, mu_3D, eta_3D, w_3D, dmu1, dmu2, Dmu_3D_coeff_meshgrid, major_axis_outer def get_boundary_quantities_ellipse(mu_3D, eta_3D, w_3D): """ Gets grid coordinates on the boundaries, as well as slice arrays for positive/negative angles with respect to the boundary angle. Args: mu_3D: 3D array of mu values on the grid. eta_3D: 3D array of eta values on the grid. w_3D (numpy.ndarray): 3D array of w values on the grid. Returns: tuple: Tuple of the grid coordinates and slice arrays """ eta_2D_ib = eta_3D[0, ...] eta_2D_ob = eta_3D[-1, ...] w_2D_ib = w_3D[0, ...] w_2D_ob = w_3D[-1, ...] ib_slice = u_dot_muvec(mu_3D, eta_3D, w_3D)[0, ...] > 0 ob_slice = u_dot_muvec(mu_3D, eta_3D, w_3D)[-1, ...] < 0 return eta_2D_ib, eta_2D_ob, w_2D_ib, w_2D_ob, ib_slice, ob_slice