Source code for pybmodes.io._elastodyn.writer

# 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.

"""Canonical re-emitters for the three ElastoDyn ``.dat`` flavours.

Each ``write_*`` emits text that parses back to a dataclass equal to
the original. Whitespace, label column position, and comment text are
normalised — the output is **not** byte-identical to any bundled RWT
file, but the test suite checks the parse → emit → re-parse fixed
point under ``np.allclose`` (rtol 1e-12).
"""

from __future__ import annotations

import io
import pathlib

import numpy as np

from pybmodes.io._elastodyn.types import (
    ElastoDynBlade,
    ElastoDynMain,
    ElastoDynTower,
)

# Width constants for the canonical scalar-line format. Values left-
# aligned in a 22-char column followed by a 12-char label column.
_VAL_W = 22
_LBL_W = 12


def _fmt_scalar_line(value: object, label: str, comment: str = "") -> str:
    if isinstance(value, bool):
        sval = "True" if value else "False"
    elif isinstance(value, int):
        sval = str(value)
    elif isinstance(value, float):
        sval = f"{value:.17g}"
    elif isinstance(value, str):
        sval = value
    else:
        sval = str(value)
    line = f"{sval:<{_VAL_W}} {label:<{_LBL_W}}"
    if comment:
        line = f"{line} - {comment}"
    return line


def _scalar_or_default(obj: ElastoDynMain, label: str, fallback: str) -> str:
    """Return the original value string for ``label`` from
    ``obj.scalars`` if captured, otherwise the fallback string."""
    return obj.scalars.get(label, fallback)


[docs] def write_elastodyn_main( obj: ElastoDynMain, path: str | pathlib.Path | None = None ) -> str: """Re-emit a main ElastoDyn file. Returns the text and optionally writes it to ``path``.""" out = io.StringIO() out.write(obj.header + "\n") out.write(obj.title + "\n") # Re-emit every captured scalar with the typed-field value when known, # falling back to the original token when we did not specialise it. typed_overrides = { "NumBl": str(obj.num_bl), "TipRad": f"{obj.tip_rad:.17g}", "HubRad": f"{obj.hub_rad:.17g}", "HubCM": f"{obj.hub_cm:.17g}", "OverHang": f"{obj.overhang:.17g}", "ShftTilt": f"{obj.shft_tilt:.17g}", "Twr2Shft": f"{obj.twr2shft:.17g}", "TowerHt": f"{obj.tower_ht:.17g}", "TowerBsHt": f"{obj.tower_bs_ht:.17g}", "NacCMxn": f"{obj.nac_cm_xn:.17g}", "NacCMyn": f"{obj.nac_cm_yn:.17g}", "NacCMzn": f"{obj.nac_cm_zn:.17g}", "RotSpeed": f"{obj.rot_speed_rpm:.17g}", "HubMass": f"{obj.hub_mass:.17g}", "HubIner": f"{obj.hub_iner:.17g}", "GenIner": f"{obj.gen_iner:.17g}", "NacMass": f"{obj.nac_mass:.17g}", "NacYIner": f"{obj.nac_y_iner:.17g}", "YawBrMass": f"{obj.yaw_br_mass:.17g}", "TwrFile": f'"{obj.twr_file}"', } # Indexed overrides: PreCone(i), TipMass(i), BldFile(i) for i, vf in enumerate(obj.pre_cone): typed_overrides[f"PreCone({i + 1})"] = f"{vf:.17g}" for i, vf in enumerate(obj.tip_mass): typed_overrides[f"TipMass({i + 1})"] = f"{vf:.17g}" for i, vs in enumerate(obj.bld_file): # Match whichever indexing form the original used. if f"BldFile({i + 1})" in obj.scalars: typed_overrides[f"BldFile({i + 1})"] = f'"{vs}"' elif f"BldFile{i + 1}" in obj.scalars: typed_overrides[f"BldFile{i + 1}"] = f'"{vs}"' for label, raw_val in obj.scalars.items(): val = typed_overrides.get(label, raw_val) out.write(f"{val:<{_VAL_W}} {label:<{_LBL_W}}\n") # OutList sections, verbatim. if obj.out_list: out.write("---------------------- OUTPUT --------------------------------------------------\n") # noqa: E501 for ln in obj.out_list: out.write(ln + "\n") if obj.nodal_out_list: out.write("====== Outputs for all blade stations ===========================\n") for ln in obj.nodal_out_list: out.write(ln + "\n") text = out.getvalue() if path is not None: pathlib.Path(path).write_text(text, encoding="latin-1") return text
[docs] def write_elastodyn_tower( obj: ElastoDynTower, path: str | pathlib.Path | None = None ) -> str: out = io.StringIO() out.write(obj.header + "\n") out.write(obj.title + "\n") out.write("---------------------- TOWER PARAMETERS ----------------------------------------\n") out.write(_fmt_scalar_line(obj.n_tw_inp_st, "NTwInpSt") + "\n") out.write(_fmt_scalar_line(obj.twr_fa_dmp[0], "TwrFADmp(1)") + "\n") out.write(_fmt_scalar_line(obj.twr_fa_dmp[1], "TwrFADmp(2)") + "\n") out.write(_fmt_scalar_line(obj.twr_ss_dmp[0], "TwrSSDmp(1)") + "\n") out.write(_fmt_scalar_line(obj.twr_ss_dmp[1], "TwrSSDmp(2)") + "\n") out.write("---------------------- TOWER ADJUSTMUNT FACTORS --------------------------------\n") out.write(_fmt_scalar_line(obj.fa_st_tunr[0], "FAStTunr(1)") + "\n") out.write(_fmt_scalar_line(obj.fa_st_tunr[1], "FAStTunr(2)") + "\n") out.write(_fmt_scalar_line(obj.ss_st_tunr[0], "SSStTunr(1)") + "\n") out.write(_fmt_scalar_line(obj.ss_st_tunr[1], "SSStTunr(2)") + "\n") out.write(_fmt_scalar_line(obj.adj_tw_ma, "AdjTwMa") + "\n") out.write(_fmt_scalar_line(obj.adj_fa_st, "AdjFASt") + "\n") out.write(_fmt_scalar_line(obj.adj_ss_st, "AdjSSSt") + "\n") out.write("---------------------- DISTRIBUTED TOWER PROPERTIES ----------------------------\n") if obj.distr_header_lines: for ln in obj.distr_header_lines: out.write(ln + "\n") else: out.write(" HtFract TMassDen TwFAStif TwSSStif\n") out.write(" (-) (kg/m) (Nm^2) (Nm^2)\n") cols = [obj.ht_fract, obj.t_mass_den, obj.tw_fa_stif, obj.tw_ss_stif] for extra in (obj.tw_fa_iner, obj.tw_ss_iner, obj.tw_fa_cg_of, obj.tw_ss_cg_of): if extra.size: cols.append(extra) for r in range(len(obj.ht_fract)): out.write(" ".join(f"{c[r]:.17e}" for c in cols) + "\n") out.write("---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------\n") for k, v in enumerate(obj.tw_fa_m1_sh): out.write(_fmt_scalar_line(v, f"TwFAM1Sh({k + 2})") + "\n") for k, v in enumerate(obj.tw_fa_m2_sh): out.write(_fmt_scalar_line(v, f"TwFAM2Sh({k + 2})") + "\n") out.write("---------------------- TOWER SIDE-TO-SIDE MODE SHAPES --------------------------\n") for k, v in enumerate(obj.tw_ss_m1_sh): out.write(_fmt_scalar_line(v, f"TwSSM1Sh({k + 2})") + "\n") for k, v in enumerate(obj.tw_ss_m2_sh): out.write(_fmt_scalar_line(v, f"TwSSM2Sh({k + 2})") + "\n") text = out.getvalue() if path is not None: pathlib.Path(path).write_text(text, encoding="latin-1") return text
[docs] def write_elastodyn_blade( obj: ElastoDynBlade, path: str | pathlib.Path | None = None ) -> str: out = io.StringIO() out.write(obj.header + "\n") out.write(obj.title + "\n") out.write("---------------------- BLADE PARAMETERS ----------------------------------------\n") out.write(_fmt_scalar_line(obj.n_bl_inp_st, "NBlInpSt") + "\n") out.write(_fmt_scalar_line(obj.bld_fl_dmp[0], "BldFlDmp(1)") + "\n") out.write(_fmt_scalar_line(obj.bld_fl_dmp[1], "BldFlDmp(2)") + "\n") out.write(_fmt_scalar_line(obj.bld_ed_dmp[0], "BldEdDmp(1)") + "\n") out.write("---------------------- BLADE ADJUSTMENT FACTORS --------------------------------\n") out.write(_fmt_scalar_line(obj.fl_st_tunr[0], "FlStTunr(1)") + "\n") out.write(_fmt_scalar_line(obj.fl_st_tunr[1], "FlStTunr(2)") + "\n") out.write(_fmt_scalar_line(obj.adj_bl_ms, "AdjBlMs") + "\n") out.write(_fmt_scalar_line(obj.adj_fl_st, "AdjFlSt") + "\n") out.write(_fmt_scalar_line(obj.adj_ed_st, "AdjEdSt") + "\n") out.write("---------------------- DISTRIBUTED BLADE PROPERTIES ----------------------------\n") if obj.distr_header_lines: for ln in obj.distr_header_lines: out.write(ln + "\n") else: if obj.pitch_axis is not None: out.write(" BlFract PitchAxis StrcTwst BMassDen FlpStff EdgStff\n") # noqa: E501 out.write(" (-) (-) (deg) (kg/m) (Nm^2) (Nm^2)\n") # noqa: E501 else: out.write(" BlFract StrcTwst BMassDen FlpStff EdgStff\n") # noqa: E501 out.write(" (-) (deg) (kg/m) (Nm^2) (Nm^2)\n") # noqa: E501 cols: list[np.ndarray] = [obj.bl_fract] if obj.pitch_axis is not None: cols.append(obj.pitch_axis) cols.extend([obj.strc_twst, obj.b_mass_den, obj.flp_stff, obj.edg_stff]) for r in range(len(obj.bl_fract)): out.write(" ".join(f"{c[r]:.17e}" for c in cols) + "\n") out.write("---------------------- BLADE MODE SHAPES ---------------------------------------\n") for k, v in enumerate(obj.bld_fl1_sh): out.write(_fmt_scalar_line(v, f"BldFl1Sh({k + 2})") + "\n") for k, v in enumerate(obj.bld_fl2_sh): out.write(_fmt_scalar_line(v, f"BldFl2Sh({k + 2})") + "\n") for k, v in enumerate(obj.bld_edg_sh): out.write(_fmt_scalar_line(v, f"BldEdgSh({k + 2})") + "\n") text = out.getvalue() if path is not None: pathlib.Path(path).write_text(text, encoding="latin-1") return text