Source code for pyserep.io.mesh_writer

"""
pyserep.io.mesh_writer
==========================
Export SEREP results (master DOFs, mode shapes) to finite element
post-processing formats for visualisation in Ansys, ParaView, etc.

Formats supported
-----------------
csv          — simple CSV: dof_index, node, direction, x, y, z
vtk_points   — VTK legacy format (ParaView)
ansys_node   — Ansys APDL *GET loop list for selected nodes
UFF58        — Universal File Format dataset 58 (SDyPy compatible)
"""

from __future__ import annotations

import os
from typing import Optional

import numpy as np

# ─────────────────────────────────────────────────────────────────────────────
# CSV
# ─────────────────────────────────────────────────────────────────────────────

[docs] def write_master_dofs_csv( master_dofs: np.ndarray, path: str, node_coords: Optional[np.ndarray] = None, verbose: bool = True, ) -> None: """ Write master DOF indices to a CSV file. Columns: dof_index, node_number, direction, [x, y, z] Parameters ---------- master_dofs : np.ndarray of int — 0-based DOF indices path : str — output .csv path node_coords : np.ndarray, shape (N_nodes, 3), optional Node coordinate array (indexed by node_number − 1). If provided, x, y, z columns are written. verbose : bool """ rows = [] for dof in master_dofs: node = int(dof) // 3 + 1 dirn = int(dof) % 3 dir_label = ["UX", "UY", "UZ"][dirn] if node_coords is not None and node - 1 < len(node_coords): x, y, z = node_coords[node - 1] rows.append(f"{dof},{node},{dir_label},{x:.6f},{y:.6f},{z:.6f}") else: rows.append(f"{dof},{node},{dir_label}") header = "dof_index,node_number,direction" if node_coords is not None: header += ",x,y,z" os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w") as f: f.write(header + "\n") f.write("\n".join(rows) + "\n") if verbose: print(f"[mesh_writer] CSV → {path} ({len(master_dofs)} DOFs)")
# ───────────────────────────────────────────────────────────────────────────── # VTK points (ParaView) # ─────────────────────────────────────────────────────────────────────────────
[docs] def write_master_dofs_vtk( master_dofs: np.ndarray, node_coords: np.ndarray, path: str, scalar_data: Optional[np.ndarray] = None, scalar_name: str = "eid_score", verbose: bool = True, ) -> None: """ Write master DOF locations as a VTK unstructured point cloud. Opens in ParaView, MayaVi, VisIt, etc. Parameters ---------- master_dofs : np.ndarray of int node_coords : np.ndarray, shape (N_nodes, 3) path : str — output .vtk path scalar_data : np.ndarray, shape (n_master,), optional Per-DOF scalar field (e.g. EID score, KE contribution). scalar_name : str verbose : bool """ nodes = np.array([int(d) // 3 for d in master_dofs]) # 0-based node indices pts = node_coords[nodes] # (n_master, 3) n = len(master_dofs) lines = [ "# vtk DataFile Version 3.0", "SEREP master DOFs", "ASCII", "DATASET UNSTRUCTURED_GRID", f"POINTS {n} float", ] for x, y, z in pts: lines.append(f"{x:.6f} {y:.6f} {z:.6f}") lines += [ f"CELLS {n} {2*n}", ] for i in range(n): lines.append(f"1 {i}") lines += [ f"CELL_TYPES {n}", ] + ["1"] * n if scalar_data is not None: lines += [ f"POINT_DATA {n}", f"SCALARS {scalar_name} float 1", "LOOKUP_TABLE default", ] for v in scalar_data: lines.append(f"{float(v):.8f}") os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w") as f: f.write("\n".join(lines) + "\n") if verbose: print(f"[mesh_writer] VTK → {path} ({n} points)")
# ───────────────────────────────────────────────────────────────────────────── # Ansys APDL node list # ─────────────────────────────────────────────────────────────────────────────
[docs] def write_ansys_node_list( master_dofs: np.ndarray, path: str, component_name: str = "MASTER_NODES", verbose: bool = True, ) -> None: """ Write an Ansys APDL input file that selects the master DOF nodes. Usage in Ansys: /INPUT, master_nodes.inp Parameters ---------- master_dofs : np.ndarray of int path : str — output .inp path component_name : str — Ansys component name verbose : bool """ nodes = sorted(set(int(d) // 3 + 1 for d in master_dofs)) lines = [ f"! SEREP master DOF node list — {len(nodes)} nodes", "! Generated by pyserep", "NSEL,NONE", ] for node in nodes: lines.append(f"NSEL,A,NODE,,{node}") lines.append(f"CM,{component_name},NODE") lines.append("NSEL,ALL") os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w") as f: f.write("\n".join(lines) + "\n") if verbose: print(f"[mesh_writer] APDL → {path} ({len(nodes)} nodes, " f"component '{component_name}')")
# ───────────────────────────────────────────────────────────────────────────── # UFF58 mode shape export (compatible with SDyPy / pyFRF / ME'scopeVES) # ─────────────────────────────────────────────────────────────────────────────
[docs] def write_uff58_mode_shapes( phi: np.ndarray, freqs_hz: np.ndarray, selected_modes: np.ndarray, master_dofs: np.ndarray, node_coords: Optional[np.ndarray], path: str, zeta: float = 0.001, verbose: bool = True, ) -> None: """ Write mode shapes to a UFF (Universal File Format) dataset 58 file. Compatible with SDyPy, pyFRF, ME'scopeVES, nModal, and other EMA/OMA post-processors. Parameters ---------- phi : np.ndarray, shape (N, n_all_modes) freqs_hz : np.ndarray selected_modes : np.ndarray of int master_dofs : np.ndarray of int — only these DOFs are exported node_coords : np.ndarray, shape (N_nodes, 3), optional path : str — output .uff path zeta : float — damping ratio for all modes verbose : bool """ lines = [] for mode_num, mode_idx in enumerate(selected_modes): fn = freqs_hz[mode_idx] node_ids = [int(d) // 3 + 1 for d in master_dofs] dirs = [int(d) % 3 + 1 for d in master_dofs] lines += [ " -1", " 58", f"SEREP_Mode_{mode_num+1}", "NONE", f"Freq={fn:.4f}Hz", "NONE", "NONE", "NONE", " 3 2 3 8 2 6", # func type = normal mode f" {len(master_dofs)} 1 1", f" {fn:.6e} {zeta:.6e} 0.000000e+00 0.000000e+00 0.000000e+00", ] phi_m = phi[master_dofs, mode_idx] # real amplitudes for node, dirn, val in zip(node_ids, dirs, phi_m): lines.append(f" {node:>8} {dirn} {float(val):>15.8e}") lines.append(" -1") os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w") as f: f.write("\n".join(lines) + "\n") if verbose: print(f"[mesh_writer] UFF58 → {path} " f"({len(selected_modes)} modes × {len(master_dofs)} DOFs)")