Spaces:
Sleeping
Sleeping
import numpy as np | |
from stnn.data.function_generators import generate_random_functions | |
from .circle import get_system_circle, get_boundary_quantities_circle, u_dot_thetahat | |
from .ellipse import (get_system_ellipse, get_boundary_quantities_ellipse, u_dot_etavec) | |
class PDESystem: | |
""" | |
Constructs the PDE system given input parameters. The finite-difference matrices, grids, and other relevant | |
quantities are available as attributes. | |
Constructor Args: | |
params (dict): Configuration dictionary containing the parameters that define the PDE system. | |
Attributes: | |
ib_slice (numpy.ndarray): Boolean array defining nodes adjacent to the inner boundary | |
ob_slice (numpy.ndarray): Boolean array defining nodes adjacent to the outer boundary | |
x2_ib (numpy.ndarray): The x2 coordinate values at the inner boundary. | |
x2_ob (numpy.ndarray): The x2 coordinate values at the outer boundary. | |
x3_ib (numpy.ndarray): The x3 coordinate values at the inner boundary. | |
x3_ob (numpy.ndarray): The x3 coordinate values at the outer boundary. | |
Dx1_coeff (numpy.ndarray): Coefficients for the advection operator in the radial direction. Used for converting | |
boundary conditions to the r.h.s. of the linear system defining a | |
boundary-value problem. | |
dx1a (numpy.ndarray): Grid spacing adjacent to the inner boundary | |
dx1b (numpy.ndarray): Grid spacing adjacent to the outer boundary | |
L (numpy.ndarray): Finite-difference representation of the linear operator defining the PDE. | |
x1 (numpy.ndarray): The grid values in radial coordinate (r or mu) | |
x2 (numpy.ndarray): The grid values in the angular coordinate (theta or eta). | |
x3 (numpy.ndarray): The grid values in the w coordinate | |
a1 (float): Minor axis of the inner boundary | |
a2 (float): Minor axis of the outer boundary | |
b1 (float): Major axis of the inner boundary | |
b2 (float): Major axis of the outer boundary | |
_coords (str): The type of coordinate system used ('ellipse' or 'circle'). This affects how the grids and | |
other geometric properties are calculated. | |
params (dict): The configuration dictionary containing the PDE parameters. | |
""" | |
def __init__(self, params): | |
self._required_keys = ['nx1', 'nx2', 'nx3', 'ell', 'a2', 'eccentricity'] | |
self._optional_keys = [] | |
self.ib_slice = None | |
self.ob_slice = None | |
self.x2_ib = None | |
self.x2_ob = None | |
self.x3_ib = None | |
self.x3_ob = None | |
self.Dx1_coeff = None | |
self.dx1b = None | |
self.dx1a = None | |
self.L = None | |
self.x1 = None | |
self.x2 = None | |
self.x3 = None | |
self.a1 = None | |
self.a2 = None | |
self.b1 = None | |
self.b2 = None | |
self._coords = None | |
self.params = params | |
self.initialize() | |
def initialize(self): | |
""" | |
Constructs the system matrices and vectors based on the stored configuration. | |
Depending on the 'eccentricity' parameter in `self.params`, the coordinate system is set | |
to either 'circle' or 'ellipse'; the domain parametrization and finite-difference grid are defined | |
accordingly. | |
""" | |
params = self.params | |
missing_keys = [key for key in self._required_keys if key not in params] | |
if missing_keys: | |
raise KeyError(f"Missing keys in config: {', '.join(missing_keys)}") | |
# The functions 'get_system_circle' and 'get_system_ellipse' have a fair amount | |
# of overlap and probably should be combined, but for now they are kept separate | |
# for simplicity and readability. | |
if params['eccentricity'] < 1e-7: | |
self._coords = 'circle' | |
L, x1, x2, x3, dx1a, dx1b, Dx1_coeff = get_system_circle(params) | |
x2_ib, x2_ob, x3_ib, x3_ob, ib_slice, ob_slice = get_boundary_quantities_circle(x2, x3) | |
self.b2 = params['a2'] | |
else: | |
self._coords = 'ellipse' | |
(L, x1, x2, x3, dx1a, dx1b, Dx1_coeff, major_axis_outer) = get_system_ellipse(params) | |
x2_ib, x2_ob, x3_ib, x3_ob, ib_slice, ob_slice = get_boundary_quantities_ellipse(x1, x2, x3) | |
self.b2 = major_axis_outer | |
self.a1 = 1.0 - params['eccentricity'] | |
self.a2 = params['a2'] | |
self.b1 = 1.0 | |
self.x1 = x1 | |
self.x2 = x2 | |
self.x3 = x3 | |
self.L = L | |
self.dx1a = dx1a | |
self.dx1b = dx1b | |
self.Dx1_coeff = Dx1_coeff | |
self.x2_ib = x2_ib | |
self.x2_ob = x2_ob | |
self.x3_ib = x3_ib | |
self.x3_ob = x3_ob | |
self.ib_slice = ib_slice | |
self.ob_slice = ob_slice | |
def generate_random_bc(self, func_gen_id): | |
""" | |
Generates random boundary conditions for the PDE system. | |
Args: | |
func_gen_id (int): Integer representing the type of 'function generator' used to construct the | |
boundary conditions. | |
Returns: | |
tuple: A tuple containing: | |
- ibf_data: Inner boundary data | |
- obf_data: Outer boundary data | |
- b: 3D array for passing to the GMRES solver. 'b' contains the boundary data but is defined | |
on the full 3D grid. | |
- bf: Flattened boundary data before it is permuted | |
The boundary conditions are defined on the inner and outer boundaries of the domain and are denoted | |
by 'ibf_data' and 'obf_data'. The function passes 'ibf_data' and 'obf_data' through 'convert_boundary_data', | |
which converts them into formats suitable for passing into the GMRES solver and STNN model (e.g., by | |
reshaping and permutation operations). | |
""" | |
# Note the change of variable (x2, x3) -> (x2, x2 - x3). | |
ibf_data = generate_random_functions(1, self.x2_ib, self.x2_ib - self.x3_ib, | |
max_freq=self.params['nx3'], func_gen_id = func_gen_id)[0, self.ib_slice] | |
obf_data = generate_random_functions(1, self.x2_ob, self.x2_ob - self.x3_ob, | |
max_freq=self.params['nx3'], func_gen_id = func_gen_id)[0, self.ob_slice] | |
# Combine boundary data in single vector | |
bf = np.concatenate([ibf_data, obf_data], axis = -1).flatten() | |
# Permutes 'ibf_data' and 'obf_data' and construct 'b' | |
ibf_data, obf_data, b = self.convert_boundary_data(ibf_data, obf_data) | |
return ibf_data, obf_data, b, bf | |
def convert_boundary_data(self, ibf_data, obf_data): | |
""" | |
Converts boundary data into formats suitable for passing into the GMRES solver and STNN model. | |
Args: | |
ibf_data: Inner boundary data | |
obf_data: Outer boundary data | |
Returns: | |
tuple: A tuple containing: | |
- ibf_data: Inner boundary data, permuted to match the input structure of the EinsumTTL layer. | |
- obf_data: Outer boundary data, permuted to match the input structure of the EinsumTTL layer | |
- b: 3D array for passing to the GMRES solver. 'b' contains the boundary data but is defined | |
on the full 3D grid. | |
""" | |
nx1, nx2, nx3 = self.params['nx1'], self.params['nx2'], self.params['nx3'] | |
b = np.zeros((nx1, nx2, nx3), dtype=np.float64) | |
b[0, self.ib_slice] = self.Dx1_coeff[0, self.ib_slice] * (ibf_data / self.dx1a) | |
b[-1, self.ob_slice] = -self.Dx1_coeff[-1, self.ob_slice] * (obf_data / self.dx1b) | |
if self._coords == 'ellipse': | |
sin_angle = u_dot_etavec(self.x1, self.x2, self.x3) | |
elif self._coords == 'circle': | |
sin_angle = u_dot_thetahat(self.x2, self.x3) | |
else: | |
raise ValueError(f'"_coords" attribute should be either "ellipse" or "circle"; instead received {self._coords}') | |
# reshape and permute 'ibf_data' and 'obf_data' | |
sin_angle_i = sin_angle[0, self.ib_slice].reshape(nx2, nx3 // 2) | |
sin_angle_o = sin_angle[-1, self.ob_slice].reshape(nx2, nx3 // 2) | |
W_I = np.argsort(sin_angle_i, axis=1) | |
W_O = np.argsort(sin_angle_o, axis=1) | |
ibf_data = ibf_data.reshape((nx2, nx3 // 2)) | |
obf_data = obf_data.reshape((nx2, nx3 // 2)) | |
for n in range(nx2): | |
ibf_data[n, :] = ibf_data[n, W_I[n, :]] | |
obf_data[n, :] = obf_data[n, W_O[n, :]] | |
return ibf_data, obf_data, b | |
def get_xy_grids(self): | |
""" | |
Converts the native grids of the PDE system to xy coordinates (no interpolation). | |
The function also applies a wrap-around in the x2 domain for plotting purposes, ensuring | |
continuity across the (periodic) domain. | |
Returns: | |
tuple of numpy.ndarray: A tuple containing two 2D numpy arrays: | |
- x_grid: The x-coordinates grid. | |
- y_grid: The y-coordinates grid. | |
""" | |
x1_1D = self.x1[:, 0, 0] | |
x2_1D = self.x2[0, :, 0] | |
x2_1D = np.append(x2_1D, np.array([np.pi - 1e-3])) # wrap around for plotting | |
x1_2D, x2_2D = np.meshgrid(x1_1D, x2_1D, indexing='ij') | |
if self._coords == 'ellipse': | |
focal_distance = np.sqrt(self.b1**2 - self.a1**2) | |
x_grid = focal_distance * np.sinh(x1_2D) * np.cos(x2_2D) | |
y_grid = focal_distance * np.cosh(x1_2D) * np.sin(x2_2D) | |
elif self._coords == 'circle': | |
x_grid = x1_2D * np.cos(x2_2D) | |
y_grid = x1_2D * np.sin(x2_2D) | |
else: | |
raise ValueError(f'"_coords" should be either "ellipse" or "circle"; instead received {self._coords}') | |
return x_grid, y_grid | |