Spaces:
Sleeping
Sleeping
File size: 9,745 Bytes
d68c650 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
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
|