Source code for dolphindes.geometry.geometry
"""Geometry type for Dolphindes."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Tuple, cast
import numpy as np
from dolphindes.types import FloatNDArray
[docs]
@dataclass
class GeometryHyperparameters(ABC):
"""Base class for geometry specifications."""
[docs]
@abstractmethod
def get_grid_size(self) -> Tuple[int, int]:
"""
Return (Nx, Ny) or equivalent grid dimensions.
Returns
-------
tuple of int
Grid dimensions (Nx, Ny).
"""
pass
[docs]
@abstractmethod
def get_pixel_areas(self) -> FloatNDArray:
"""
Return the area of each pixel in the grid.
Returns
-------
ndarray of float
Flattened array of pixel areas.
"""
pass
[docs]
@dataclass
class CartesianFDFDGeometry(GeometryHyperparameters):
"""
Cartesian FDFD geometry specification.
Attributes
----------
Nx : int
Number of pixels along the x direction.
Ny : int
Number of pixels along the y direction.
Npmlx : int
Size of the x direction PML in pixels.
Npmly : int
Size of the y direction PML in pixels.
dx : float
Finite difference grid pixel size in x direction, in units of 1.
dy : float
Finite difference grid pixel size in y direction, in units of 1.
bloch_x : float
x-direction phase shift associated with the periodic boundary conditions.
Default: 0.0
bloch_y : float
y-direction phase shift associated with the periodic boundary conditions.
Default: 0.0
"""
Nx: int
Ny: int
Npmlx: int
Npmly: int
dx: float
dy: float
bloch_x: float = 0.0
bloch_y: float = 0.0
def __post_init__(self) -> None:
"""Validate grid dimensions and spacings."""
if self.Nx <= 0 or self.Ny <= 0:
raise ValueError(
f"Nx and Ny must be positive, got Nx={self.Nx}, Ny={self.Ny}."
)
if self.Npmlx < 0 or self.Npmly < 0:
raise ValueError(
f"Npmlx and Npmly must be non-negative, got "
f"Npmlx={self.Npmlx}, Npmly={self.Npmly}."
)
if self.dx <= 0 or self.dy <= 0:
raise ValueError(
f"dx and dy must be positive, got dx={self.dx}, dy={self.dy}."
)
if 2 * self.Npmlx >= self.Nx or 2 * self.Npmly >= self.Ny:
raise ValueError(
"PML regions must fit inside the grid: need 2*Npmlx < Nx and "
f"2*Npmly < Ny, got Npmlx={self.Npmlx}, Nx={self.Nx}, "
f"Npmly={self.Npmly}, Ny={self.Ny}."
)
[docs]
def get_grid_size(self) -> Tuple[int, int]:
"""
Return grid dimensions.
Returns
-------
tuple of int
Grid dimensions (Nx, Ny).
"""
return (self.Nx, self.Ny)
[docs]
def get_pixel_areas(self) -> FloatNDArray:
"""
Return the area of each pixel in the grid.
Returns
-------
ndarray of float
Flattened array of pixel areas (all equal to dx * dy).
"""
return np.full(self.Nx * self.Ny, self.dx * self.dy, dtype=float)
[docs]
@dataclass
class PolarFDFDGeometry(GeometryHyperparameters):
"""
Polar FDFD geometry specification.
Attributes
----------
Nphi : int
Number of azimuthal grid points (in one irreducible region if using symmetry).
Nr : int
Number of radial grid points.
Npml : int
Width of outer radial PML layers in pixels.
dr : float
Radial grid spacing.
Npml_inner : int
Width of inner radial PML layers in pixels.
Default: 0
r_inner : float
Inner radius of computational domain.
Default: 0
mirror : bool
Whether reflection symmetry is assumed with Neumann boundary conditions.
Default: False
n_sectors : int
Number of rotational symmetry sectors (1 for full circle).
Default: 1
bloch_phase : float
Phase shift for Bloch-periodic boundary conditions in azimuthal direction.
Default: 0.0
m : int
PML polynomial grading order.
Default: 3
lnR : float
Logarithm of desired PML reflection coefficient.
Default: -20.0
"""
Nphi: int
Nr: int
Npml: int
dr: float
Npml_inner: int = 0
r_inner: float = 0.0
mirror: bool = False
n_sectors: int = 1
bloch_phase: float = 0.0
m: int = 3
lnR: float = -20.0
def __post_init__(self) -> None:
"""Validate grid dimensions and spacings."""
if self.Nphi <= 0 or self.Nr <= 0:
raise ValueError(
f"Nphi and Nr must be positive, got Nphi={self.Nphi}, Nr={self.Nr}."
)
if self.Npml < 0 or self.Npml_inner < 0:
raise ValueError(
f"Npml and Npml_inner must be non-negative, got "
f"Npml={self.Npml}, Npml_inner={self.Npml_inner}."
)
if self.dr <= 0:
raise ValueError(f"dr must be positive, got dr={self.dr}.")
if self.r_inner < 0:
raise ValueError(
f"r_inner must be non-negative, got r_inner={self.r_inner}."
)
if self.n_sectors <= 0:
raise ValueError(
f"n_sectors must be positive, got n_sectors={self.n_sectors}."
)
if self.Npml + self.Npml_inner >= self.Nr:
raise ValueError(
"PML regions must fit inside the radial grid: need "
f"Npml + Npml_inner < Nr, got Npml={self.Npml}, "
f"Npml_inner={self.Npml_inner}, Nr={self.Nr}."
)
@property
def r_grid(self) -> FloatNDArray:
"""Get the radial grid points."""
return cast(FloatNDArray, self.r_inner + (np.arange(self.Nr) + 0.5) * self.dr)
@property
def nonpmlNr(self) -> int:
"""Get the number of radial grid points excluding PML regions."""
return self.Nr - self.Npml - self.Npml_inner
@property
def dphi(self) -> float:
"""Get the azimuthal grid spacing."""
val = 2 * np.pi / self.n_sectors / self.Nphi
if self.mirror:
val /= 2
return val
@property
def phi_grid(self) -> FloatNDArray:
"""Get the azimuthal grid points."""
return cast(FloatNDArray, np.arange(self.Nphi) * self.dphi)
[docs]
def get_grid_size(self) -> Tuple[int, int]:
"""
Return grid dimensions.
Returns
-------
tuple of int
Grid dimensions (Nr, Nphi).
"""
return (self.Nr, self.Nphi)
[docs]
def get_pixel_areas(self) -> FloatNDArray:
"""
Return the area of each pixel in the grid.
Returns
-------
ndarray of float
Flattened array of pixel areas.
"""
area_r = self.r_grid * self.dr * self.dphi
return cast(FloatNDArray, np.kron(np.ones(self.Nphi), area_r))