caleb2's picture
initial commit
d68c650
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