Source code for pybmodes.io.sec_props

# Copyright 2024-2026 Jae Hoon Seo
# Marine Structural Mechanics and Integrity Lab (SMI Lab), Inha University
#
# 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.

"""Parser for beam section-properties files (.dat).

File structure (all units SI)::

    Line 1 : title string
    Line 2 : n_secs  label  description
    Line 3 : blank
    Line 4 : column header
    Line 5 : column units
    Lines 6+: one row per spanwise station (13 space-separated values)
              trailing notes / blank lines after the data are ignored

Column order::

    span_loc  str_tw  tw_iner  mass_den  flp_iner  edge_iner
    flp_stff  edge_stff  tor_stff  axial_stff  cg_offst  sc_offst  tc_offst
"""

from __future__ import annotations

import pathlib
from dataclasses import dataclass

import numpy as np


[docs] @dataclass class SectionProperties: """Spanwise section property table.""" title: str n_secs: int # All arrays have length n_secs. span_loc: np.ndarray # normalised span station (0–1) str_tw: np.ndarray # structural twist (deg) tw_iner: np.ndarray # inertia twist (deg) — set to 0 for towers mass_den: np.ndarray # mass per unit length (kg/m) flp_iner: np.ndarray # flap/f-a mass moment of inertia per unit length (kg·m) edge_iner: np.ndarray # edge/s-s mass moment of inertia per unit length (kg·m) flp_stff: np.ndarray # flap/f-a bending stiffness EI (N·m²) edge_stff: np.ndarray # edge/s-s bending stiffness EI (N·m²) tor_stff: np.ndarray # torsion stiffness GJ (N·m²) axial_stff: np.ndarray # axial stiffness EA (N) cg_offst: np.ndarray # CG offset from reference axis (m) sc_offst: np.ndarray # shear-centre offset from reference axis (m) tc_offst: np.ndarray # tension-centre offset from reference axis (m) source_file: pathlib.Path | None = None
_N_COLS = 13 # expected number of data columns per row
[docs] def read_sec_props(path: str | pathlib.Path) -> SectionProperties: """Parse a section-properties .dat file.""" path = pathlib.Path(path) lines = path.read_text(encoding='latin-1').splitlines() non_empty = [ln.rstrip() for ln in lines if ln.strip()] # The file is expected to start with a title line, an n_secs declaration, # column header, units row, and at least one data row. An empty or # severely-truncated file would otherwise raise a bare IndexError that # buries the path; convert to a clear ValueError. if len(non_empty) < 5: raise ValueError( f"{path}: section-properties file is empty or truncated " f"(found {len(non_empty)} non-blank lines, need >= 5: " f"title, n_secs, header, units, at least one data row)" ) # Line 0 of non_empty: title title = non_empty[0].strip() # Line 1: "n_secs label description" try: n_secs = int(non_empty[1].split()[0]) except (ValueError, IndexError) as exc: raise ValueError( f"{path}: cannot parse n_secs from line 2 (got {non_empty[1]!r})" ) from exc # Lines 2 & 3: column header and units — skip # Line 4 onward: data rows until a line that cannot be parsed as numbers data_rows: list[list[float]] = [] for ln in non_empty[4:]: tokens = ln.split() if len(tokens) < _N_COLS: break # trailing notes / blank separator # Two-stage parse so we can distinguish "row is not numeric" # (trailing notes — break the loop) from "row is numeric but # contains nan/inf" (transcription error — raise loudly). try: raw = [_parse_loose_float(t) for t in tokens[:_N_COLS]] except ValueError: break # trailing note, stop reading for col_idx, v in enumerate(raw): if not _is_finite(v): raise ValueError( f"{path}: non-finite value in section-properties " f"row {len(data_rows) + 1}, column {col_idx + 1}: " f"{tokens[col_idx]!r} parses to {v!r}. Physical " f"quantities must be finite." ) data_rows.append(raw) if len(data_rows) != n_secs: raise ValueError( f"{path}: expected {n_secs} data rows, found {len(data_rows)}" ) arr = np.array(data_rows, dtype=float) # (n_secs, 13) return SectionProperties( title=title, n_secs=n_secs, span_loc = arr[:, 0], str_tw = arr[:, 1], tw_iner = arr[:, 2], mass_den = arr[:, 3], flp_iner = arr[:, 4], edge_iner = arr[:, 5], flp_stff = arr[:, 6], edge_stff = arr[:, 7], tor_stff = arr[:, 8], axial_stff = arr[:, 9], cg_offst = arr[:, 10], sc_offst = arr[:, 11], tc_offst = arr[:, 12], source_file=path, )
def _parse_fortran_float(token: str) -> float: """Strict parse: float literal with D/d exponent notation, must be finite. Used by external callers; the internal data-row parser uses the loose variant + a follow-up :func:`_is_finite` check so it can distinguish "row is not numeric" (trailing note — break the loop) from "row is numeric but non-finite" (transcription error — raise loudly). """ import math value = float(token.replace('d', 'e').replace('D', 'E')) if not math.isfinite(value): raise ValueError( f"Non-finite float in section-properties row: {token!r} " f"parses to {value!r}. Physical quantities must be " f"finite; a stray ``nan`` / ``inf`` is almost certainly " f"a transcription error." ) return value def _parse_loose_float(token: str) -> float: """Loose parse: float literal with D/d exponent notation, allowing NaN / ±Inf. Used inside the data-row loop so the "is this a numeric row?" question is decoupled from "is the value physically valid?". A trailing note like ``# end`` raises ``ValueError`` here (loop breaks); ``nan`` parses cleanly here and the follow-up :func:`_is_finite` check raises with a column-aware error message.""" return float(token.replace('d', 'e').replace('D', 'E')) def _is_finite(value: float) -> bool: """``math.isfinite`` shim used after :func:`_parse_loose_float` so the per-row error message can identify the offending column without re-parsing the whole row.""" import math return math.isfinite(value)