Source code for pybmodes.workflows.report

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

"""``pybmodes report`` workflow as a typed library function.

Runs the full modal-analysis pipeline on one ElastoDyn deck — tower +
blade FEM solves, polynomial fits, optional coefficient validation,
optional Campbell sweep — and writes a single combined Markdown / HTML
/ CSV report. The report module itself
(:func:`pybmodes.report.generate_report`) writes the file; this workflow
is responsible for orchestrating the inputs.
"""
from __future__ import annotations

import pathlib
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Literal

from pybmodes.workflows._base import WorkflowResult

if TYPE_CHECKING:
    from pybmodes.campbell import CampbellResult
    from pybmodes.checks import ModelWarning
    from pybmodes.elastodyn.params import (
        BladeElastoDynParams,
        TowerElastoDynParams,
    )
    from pybmodes.elastodyn.validate import ValidationResult
    from pybmodes.models.result import ModalResult

ReportFormat = Literal["md", "html", "csv"]


[docs] @dataclass class ReportResult(WorkflowResult): """Result of :func:`run_report`. Attributes ---------- out_path : pathlib.Path | None Absolute path of the written report file. tower_modal, blade_modal Modal-analysis results for the tower and blade sides. The report itself is keyed off ``tower_modal`` (one combined report per deck); both are exposed here so callers can inspect the underlying frequencies / shapes without re-solving. tower_params, blade_params Fitted polynomial coefficient blocks. validation : ValidationResult | None Coefficient validation result; ``None`` when ``validate=False``. campbell : CampbellResult | None Campbell sweep result; ``None`` when ``campbell=False``. check_warnings : list[ModelWarning] Pre-solve check findings (tower + blade), captured for the report's ``check_model`` section. Surfaces both sides — the blade-side findings were missing in 0.x and are restored here. """ out_path: pathlib.Path | None = None tower_modal: ModalResult | None = None blade_modal: ModalResult | None = None tower_params: TowerElastoDynParams | None = None blade_params: BladeElastoDynParams | None = None validation: ValidationResult | None = None campbell: CampbellResult | None = None check_warnings: list[ModelWarning] = field(default_factory=list)
[docs] def run_report( dat_path: str | pathlib.Path, out_path: str | pathlib.Path, *, n_modes: int = 10, format: ReportFormat = "md", validate: bool = True, campbell: bool = False, max_rpm: float = 15.0, n_steps: int = 16, n_blade_modes: int = 4, n_tower_modes: int = 4, ) -> ReportResult: """Run modal analysis + (optional) validation + (optional) Campbell on one ElastoDyn deck and write a combined report. Library entry point for :command:`pybmodes report`. Parameters ---------- dat_path : str or pathlib.Path ElastoDyn main ``.dat`` file. out_path : str or pathlib.Path Destination report file. Parent directory is created if missing. n_modes : int, default 10 Number of FEM modes to solve per side. format : {"md", "html", "csv"}, default "md" Report format. validate : bool, default True Run :func:`~pybmodes.elastodyn.validate_dat_coefficients` and attach the result to the report's validation section. campbell : bool, default False Run a rotor-speed sweep from 0 to ``max_rpm`` in ``n_steps`` points and attach the result to the report's Campbell section. max_rpm, n_steps, n_blade_modes, n_tower_modes Campbell-sweep parameters. Ignored when ``campbell=False``. Returns ------- ReportResult Carries the rendered file path plus every intermediate artefact (modal results, fitted params, validation, Campbell sweep, pre-solve warnings) so callers can introspect without re-running the workflow. Raises ------ FileNotFoundError When ``dat_path`` does not exist. """ import numpy as np from pybmodes.checks import check_model as _check_model from pybmodes.elastodyn import ( compute_blade_params, compute_tower_params, validate_dat_coefficients, ) from pybmodes.models import RotatingBlade, Tower from pybmodes.report import generate_report main_dat = pathlib.Path(dat_path).resolve() if not main_dat.is_file(): raise FileNotFoundError(f"file not found: {main_dat}") out = pathlib.Path(out_path).resolve() out.parent.mkdir(parents=True, exist_ok=True) messages: list[str] = [] messages.append( f"report: building tower + blade models from {main_dat.name}" ) tower_model = Tower.from_elastodyn(main_dat) blade_model = RotatingBlade.from_elastodyn(main_dat) tower_modal = tower_model.run(n_modes=n_modes, check_model=False) blade_modal = blade_model.run(n_modes=n_modes, check_model=False) tower_params = compute_tower_params(tower_modal) blade_params = compute_blade_params(blade_modal) # Pre-solve warnings (captured, not raised — surfaced via the # report's check_model section). Includes BOTH tower-side and # blade-side findings. check_warnings = list(_check_model(tower_model, n_modes=n_modes)) check_warnings.extend(_check_model(blade_model, n_modes=n_modes)) validation = ( validate_dat_coefficients(main_dat) if validate else None ) campbell_result = None if campbell: from pybmodes.campbell import campbell_sweep rpm_grid = np.linspace(0.0, max_rpm, n_steps) campbell_result = campbell_sweep( main_dat, rpm_grid, n_blade_modes=n_blade_modes, n_tower_modes=n_tower_modes, ) generate_report( tower_modal, out, format=format, model=tower_model, validation=validation, check_warnings=check_warnings, tower_params=tower_params, blade_params=blade_params, campbell=campbell_result, source_file=main_dat, ) messages.append(f"wrote {out}") return ReportResult( exit_code=0, messages=messages, out_path=out, tower_modal=tower_modal, blade_modal=blade_modal, tower_params=tower_params, blade_params=blade_params, validation=validation, campbell=campbell_result, check_warnings=check_warnings, )