Source code for ktch.io._SPHARM_PDM

"""SPHARM-PDM file (_para.vtk, _surf.vtk, and .coef) I/O functions"""

# Copyright 2023 Koji Noshita
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pathlib import Path
from typing import List, Tuple, Union

import numpy as np
import numpy.typing as npt


[docs] def read_spharmpdm_coef(path: Union[str, Path]) -> List[npt.NDArray[np.complex128]]: """Read .coef file of SPHARM-PDM. The .coef file is an output of the `ParaToSPHARMMesh` step of `SPHARM-PDM <https://www.nitrc.org/projects/spharm-pdm>`_, and contains SPHARM coefficients. The file format consists of: - First value: total number of coefficients (3*(lmax+1)^2) - Remaining values: real and imaginary parts of coefficients arranged in SPHARM-PDM's specific order Parameters ---------- path : str or pathlib.Path Path to the .coef file. Returns ------- coef : List[npt.NDArray[np.complex128]] List of numpy arrays containing SPHARM coefficients for each coordinate. Raises ------ FileNotFoundError If the file does not exist. ValueError If the file format is invalid or coefficients are malformed. """ path = Path(path) # Validate file existence and readability if not path.exists(): raise FileNotFoundError(f"SPHARM-PDM coefficient file not found: {path}") if not path.is_file(): raise ValueError(f"Path is not a file: {path}") try: with open(path, "r") as f: coef_txt = f.read() except IOError as e: raise IOError(f"Error reading SPHARM-PDM file: {e}") if not coef_txt.strip(): raise ValueError("SPHARM-PDM file is empty") try: cleaned_txt = coef_txt.translate(str.maketrans("", "", "{}\n")) coef = [float(val) for val in cleaned_txt.split(",") if val.strip()] except ValueError as e: raise ValueError(f"Invalid coefficient format in SPHARM-PDM file: {e}") if len(coef) < 1: raise ValueError("SPHARM-PDM file must contain at least one value") # Validate coefficient count # First value is (lmax+1)^2, total coefficients should be 3 * (lmax+1)^2 n_coef_per_coord = int(coef[0]) expected_total = 3 * n_coef_per_coord actual_count = len(coef) - 1 if actual_count != expected_total: raise ValueError( f"Coefficient count mismatch: expected {expected_total} (3 * {n_coef_per_coord}), found {actual_count}" ) # Convert to numpy array and validate dimensions try: coef_array = np.array(coef[1:]).reshape((n_coef_per_coord, 3)) except Exception as e: raise ValueError(f"Failed to convert coefficients to numpy array: {e}") if coef_array.size == 0: raise ValueError("No coefficients found after the count value") lmax_plus_one = np.sqrt(n_coef_per_coord) if not lmax_plus_one.is_integer(): raise ValueError(f"Invalid coefficient count: {n_coef_per_coord} != (lmax+1)^2") coef = cvt_spharm_coef_spharmpdm_to_list(coef_array) return coef
def cvt_spharm_coef_spharmpdm_to_list( coef_spharmpdm: npt.NDArray[np.float64], ) -> List[npt.NDArray[np.complex128]]: """Convert SPHARM-PDM format coefficients to list format. SPHARM-PDM stores coefficients in a specific order: - For m=0: only real part is stored - For m>0: real and imaginary parts are stored separately - Complex conjugate symmetry is used for m<0 Parameters ---------- coef_spharmpdm : np.ndarray of shape ((lmax+1)^2,3) Flattened array of SPHARM coefficients in SPHARM-PDM format. Contains coefficients for x, y, z coordinates. Returns ------- coef_list : list of np.ndarray List where coef_list[l] contains coefficients for degree l. Each element is an array of shape (2*l+1, 3) with complex values. The order is m = -l, -l+1, ..., l-1, l. Raises ------ ValueError If the input array has invalid shape or dimensions. """ lmax = int(np.sqrt(coef_spharmpdm.shape[0]) - 1) if coef_spharmpdm.shape != ((lmax + 1) ** 2, 3): raise ValueError( f"Invalid coefficient array shape: expected {((lmax + 1) ** 2, 3)}, got {coef_spharmpdm.shape}" ) # Convert to list format coef_list = [] for l in range(lmax + 1): coef_l = np.zeros((2 * l + 1, 3), dtype=np.complex128) for idx, m in enumerate(range(-l, l + 1)): if m == 0: # m=0: only real part coef_l[idx] = coef_spharmpdm[l**2] elif m > 0: # m>0: combine real and imaginary parts real_idx = l**2 + 2 * m - 1 imag_idx = l**2 + 2 * m coef_l[idx] = ( coef_spharmpdm[real_idx] - coef_spharmpdm[imag_idx] * 1j ) / 2 else: # m<0: use complex conjugate symmetry abs_m = abs(m) real_idx = l**2 + 2 * abs_m - 1 imag_idx = l**2 + 2 * abs_m coef_l[idx] = ( ((-1) ** m) * (coef_spharmpdm[real_idx] + coef_spharmpdm[imag_idx] * 1j) / 2 ) coef_list.append(coef_l) return coef_list def cvt_spharm_coef_list_to_spharmpdm( coef_list: List[npt.NDArray[np.complex128]], ) -> npt.NDArray[np.float64]: """Convert list format coefficients to SPHARM-PDM format. Converts complex spherical harmonic coefficients from standard list format to SPHARM-PDM's specific storage format. Parameters ---------- coef_list : list of np.ndarray List where coef_list[l] contains coefficients for degree l. Each element should have shape (2*l+1,) with complex values. The order is m = -l, -l+1, ..., l-1, l. Returns ------- coef_spharmpdm : np.ndarray of shape ((lmax+1)^2, 3) Flattened array of coefficients in SPHARM-PDM format. Real and imaginary parts are stored separately. Raises ------ ValueError If the input list has invalid structure or dimensions. Notes ----- The conversion uses the complex conjugate symmetry property: Y_l^{-m} = (-1)^m * conj(Y_l^m) """ if not isinstance(coef_list, list): raise ValueError("coef_list must be a list") if len(coef_list) == 0: raise ValueError("coef_list cannot be empty") lmax = len(coef_list) - 1 # Validate structure of coefficient list for l, coef_l in enumerate(coef_list): expected_len = 2 * l + 1 if len(coef_l) != expected_len: raise ValueError( f"coef_list[{l}] has length {len(coef_l)}, expected {expected_len}" ) # Convert to SPHARM-PDM format coef_spharmpdm = np.zeros(((lmax + 1) ** 2, 3)) for l in range(lmax + 1): l_squared = l**2 # m = 0 coef_spharmpdm[l_squared] = coef_list[l][l].real # m > 0 for m in range(1, l + 1): # Get positive and negative m coefficients coef_pos_m = coef_list[l][m + l] coef_neg_m = coef_list[l][-m + l] sign = (-1) ** m # Real part: sum of positive and negative m coef_spharmpdm[l_squared + 2 * m - 1] = ( coef_pos_m + sign * coef_neg_m ).real # Imaginary part: difference of positive and negative m coef_spharmpdm[l_squared + 2 * m] = ( (coef_pos_m - sign * coef_neg_m) * 1j ).real return coef_spharmpdm