diff --git a/openfast_io/docs/architecture-redesign.md b/openfast_io/docs/architecture-redesign.md new file mode 100644 index 0000000000..4178dac693 --- /dev/null +++ b/openfast_io/docs/architecture-redesign.md @@ -0,0 +1,1231 @@ +# openfast\_io Architecture Redesign — Before and After + +**Document scope:** A detailed comparison of the `openfast_io` Python package before and +after the architectural restructuring carried out in 2026, intended for design review +and approval. + +--- + +## Table of Contents + +1. [Background and Motivation](#background-and-motivation) +2. [Before: The Monolithic Architecture](#before-the-monolithic-architecture) + - 2.1 [Package layout (before)](#21-package-layout-before) + - 2.2 [FAST\_reader.py — the god class](#22-fast_readerpy--the-god-class) + - 2.3 [FAST\_writer.py — the god class](#23-fast_writerpy--the-god-class) + - 2.4 [Standalone module drivers (before)](#24-standalone-module-drivers-before) + - 2.5 [Output channel management (before)](#25-output-channel-management-before) + - 2.6 [Parsing helpers (before)](#26-parsing-helpers-before) + - 2.7 [Tests (before)](#27-tests-before) + - 2.8 [Summary of problems](#28-summary-of-problems) +3. [After: The Layered Architecture](#after-the-layered-architecture) + - 3.1 [Package layout (after)](#31-package-layout-after) + - 3.2 [Layer 1 — ModuleIO (io/)](#32-layer-1--moduleio-io) + - 3.3 [Layer 2 — Driver layer (drivers/)](#33-layer-2--driver-layer-drivers) + - 3.4 [Layer 3 — Backwards-compatible facades](#34-layer-3--backwards-compatible-facades) + - 3.5 [New ancillary subsystems](#35-new-ancillary-subsystems) + - 3.6 [Tests (after)](#36-tests-after) +4. [Module-by-module changes](#module-by-module-changes) +5. [Bug fixes discovered and applied](#bug-fixes-discovered-and-applied) +6. [Backwards compatibility](#backwards-compatibility) +7. [Numerical change summary](#numerical-change-summary) +8. [MCP Server (`openfast-mcp`)](#mcp-server-openfast-mcp) + - 8.1 [Purpose](#81-purpose) + - 8.2 [Design principles](#82-design-principles) + - 8.3 [Tool interface (5 tools)](#83-tool-interface-5-tools) + - 8.4 [Supported executables](#84-supported-executables) + - 8.5 [Typical agent workflow](#85-typical-agent-workflow) + - 8.6 [Package structure](#86-package-structure) + - 8.7 [Dependencies](#87-dependencies) +9. [Critical Analysis](#critical-analysis) +10. [Differential audit — corrections and follow-up fixes](#differential-audit--corrections-and-follow-up-fixes) + - 10.1 [Corrections folded into §2–§5 (resolved)](#101-corrections-folded-into-25-resolved) + - 10.2 [New bugs the audit found (fixed on this branch)](#102-new-bugs-the-audit-found-fixed-on-this-branch) + - 10.3 [Verified-real fixes (audit-confirmed, unchanged)](#103-verified-real-fixes-audit-confirmed-unchanged) + - 10.4 [Further fixes made on this branch](#104-further-fixes-made-on-this-branch) + +--- + +## 1. Background and Motivation + +`openfast_io` is the Python I/O layer for [OpenFAST](https://github.com/OpenFAST/openfast), +NREL's multi-physics wind turbine simulation framework. It is used by WEIS, WISDEM, +and other downstream toolchains to read and write the full OpenFAST input deck (`.fst` +file and all referenced sub-files) and to post-process binary output files. + +Before the redesign, the package had grown organically into two multi-thousand-line +"god class" files — `FAST_reader.py` and `FAST_writer.py` — that mixed parsing +logic, file-path resolution, module orchestration, business rules, and output channel +management into hard-to-test, hard-to-extend monoliths. + +The goals of the redesign were: + +| Goal | Problem it solves | +|---|---| +| Make each module independently readable and writable | Reader and writer code for each module (AeroDyn, ElastoDyn, …) was entangled in the god classes | +| Make standalone module drivers first-class citizens | Driver-only I/O (e.g. `aerodyn_driver.inp`) was a separate partially-maintained concern | +| Enable round-trip testing of every module in isolation | No per-module unit tests existed | +| Add cross-module validation | No validation was done; mis-matched blade counts could silently produce wrong simulations | +| Maintain 100% backwards compatibility | WEIS and WISDEM call `InputReader_OpenFAST` and `InputWriter_OpenFAST` directly | +| Provide machine-readable parameter schema | Downstream tools had to hard-code parameter names | + +--- + +## 2. Before: The Monolithic Architecture + +### 2.1 Package layout (before) + +``` +openfast_io/ +├── FAST_reader.py # 3 652 lines — monolithic reader +├── FAST_writer.py # 2 979 lines — monolithic writer +├── FAST_output_reader.py # binary/ASCII output reader (unchanged) +├── FAST_post.py # post-processing helpers +├── FAST_linearization_reader.py +├── FAST_vars_out.py # output channel registry (large boolean dict) +├── FileTools.py # miscellaneous file helpers +├── StC_defaults.py # StrucCtrl defaults +├── turbsim_file.py +├── turbsim_util.py +└── create_output_vars.py +``` + +No `io/`, no `drivers/`, no `tests/` subdirectory, no `schema.py`, no `validation.py`. + +Standalone module drivers (AeroDyn, BeamDyn, HydroDyn, …) lived entirely **outside** +this package — typically as separate Python scripts in the WEIS or WISDEM repository — +or were not supported at all. + +### 2.2 FAST\_reader.py — the god class + +`FAST_reader.py` was **3 652 lines** long. Every module's parsing logic was +implemented as a method of the single class `InputReader_OpenFAST`: + +| Method | Module | Approx. lines | +|---|---|---| +| `read_MainInput()` | `.fst` toplevel | ~150 | +| `read_ElastoDyn()` / `read_ElastoDynBlade()` / `read_ElastoDynTower()` | ElastoDyn + blade + tower files | ~300 | +| `read_AeroDyn()` / `read_AeroDynBlade()` / `read_AeroDynPolar()` | AeroDyn 15 + blade + polars | ~450 | +| `read_AeroDisk()` | AeroDisk | ~80 | +| `read_InflowWind()` | InflowWind | ~200 | +| `read_ServoDyn()` / `read_StC()` | ServoDyn + StrucCtrl | ~250 | +| `read_HydroDyn()` | HydroDyn | ~350 | +| `read_SeaState()` | SeaState | ~200 | +| `read_SubDyn()` | SubDyn | ~280 | +| `read_MoorDyn()` | MoorDyn | ~280 | +| `read_MAP()` | MAP++ | ~120 | +| `read_BeamDyn()` / `read_BeamDynBlade()` | BeamDyn + blade | ~250 | +| `read_ExtPtfm()` | ExtPtfm | ~120 | +| `read_SimpleElastoDyn()` | SimpleElastoDyn (SED) | ~100 | +| Other helpers | OLAF, StC defaults, outlist | ~500 | + +Key structural problems: + +- **All parsing was positional** — files were read line-by-line with `readline()`, + relying on the file format never changing column positions. +- **Hard-coded module orchestration** — the `execute()` method called all 15+ + sub-readers regardless of which modules were actually active. Unused modules + produced empty dicts silently. +- **No module isolation** — breaking a change to `read_HydroDyn()` could + accidentally affect `read_MoorDyn()` because both shared helpers defined at + class scope. +- **fst\_vt was never formally typed** — the output was a deeply nested plain + `dict` with no schema, making it impossible to know what keys to expect. +- **File-path resolution was ad-hoc** — every `read_X()` method computed its own + absolute path from `self.FAST_directory`, sometimes incorrectly for cases where + referenced files crossed case boundaries (e.g., `../5MW_Baseline/`). +- **`CompAero` branching for AeroDisk vs. AeroDyn was buried inline** in the + monolithic `execute()` method's module-orchestration logic rather than being + a clearly named, independently testable check. `AeroDisk` corresponds to + `CompAero = 1` in the OpenFAST input format. (See §4.3 for how the new + driver expresses this branch.) + +### 2.3 FAST\_writer.py — the god class + +`FAST_writer.py` was **2 979 lines** long, mirroring the reader but for writing: + +- Each `write_X()` method used large f-string templates that mixed field values and + comments in a single pass. +- **Format-width overflow in the standalone driver writers** — several + floating-point fields in the AeroDyn, BeamDyn, and UnsteadyAero standalone + driver input writers used fixed format widths that overflowed for values + outside the expected range (scientific notation with large exponents), + producing malformed driver input files. This affects the driver-input + writers, not the `io/aerodyn.py` / `io/beamdyn.py` module writers. +- **No write-only entry points per module** — to regenerate just the HydroDyn file + from a modified dict, callers had to run the full `execute()` which re-wrote all + files. +- **Standalone driver writing was not supported** — there was no way to write an + `aerodyn_driver.inp`, `beamdyn_driver.inp`, etc. + +### 2.4 Standalone module drivers (before) + +OpenFAST ships standalone executables for each module (AeroDyn driver, BeamDyn +driver, HydroDyn driver, …). These accept a module-specific driver input file +that sets up boundary conditions for a module-only run (used for parameter studies +and validation). + +**Before the redesign, `openfast_io` had no support for reading or writing any +standalone driver input file.** WEIS contained ad-hoc partial implementations for +AeroDyn and BeamDyn drivers only; all other drivers were unsupported. + +### 2.5 Output channel management (before) + +Output channels were managed via `FAST_vars_out.py`, which contained `FstOutput` — +a large nested dict mapping every possible output channel name to a boolean +(`True` = enabled). For example: + +```python +FstOutput = { + 'ElastoDyn': { + 'NcIMUTVxs': False, 'NcIMUTVys': False, ... # ~500 entries + }, + 'AeroDyn': { + 'RtAeroCp': False, 'RtAeroCt': False, ... + }, + ... +} +``` + +Problems: + +- **Copy-by-value semantics** — callers received a mutable dict, so one downstream + consumer enabling a channel would affect all subsequent readers of `FstOutput`. +- **No validation** — misspelled channel names were silently accepted and ignored. +- **No module-level API** — to enable channels for a module a caller had to know + the exact nested structure of `FstOutput`. +- **Dict-diffing to find enabled channels** was O(n) in the total number of all + possible channels (~2 000). + +### 2.6 Parsing helpers (before) + +Helpers like `readline_filterComments`, `read_array`, `bool_read`, `float_read` +were **defined inside `FAST_reader.py`** and were not importable independently. +Downstream tools that just needed `float_read` had to import the 3 652-line reader. + +### 2.7 Tests (before) + +Testing consisted of a single integration test file: +`openfast_io/tests/test_of_io_pytest.py` + +This test ran the full read-write-run cycle for 40 r-test cases against a built +OpenFAST executable. While useful, it: + +- Required a compiled OpenFAST binary and the r-test submodule. +- Could not isolate failures to a specific module. +- Provided no coverage for edge-case inputs (NBodyMod=1 with 4 bodies, comma-format + floating-point, etc.). +- Had zero unit tests for parsing helpers, schema, validation, or output channel + management. + +### 2.8 Summary of problems + +| Category | Issue | +|---|---| +| Code size | Two files totalling 6 631 lines, each with 15+ responsibilities | +| Testability | No module-level unit tests; test failures required tracing through 3 000+ lines | +| Format width | Writer format strings overflowed for legal but unusual values | +| AeroDisk | `CompAero` enum check was wrong; AeroDisk cases silently skipped | +| HydroDyn | `NBodyMod=1` matrices read 6 rows instead of `6*NBody` | +| SubDyn | GuyanDamp matrix parser crashed on comma-trailing floats | +| Standalone drivers | Not supported for any module | +| Output channels | Copy-by-value shared state; no validation; slow lookup | +| Parsing helpers | Not independently importable | +| Schema | No machine-readable parameter metadata | +| Validation | No cross-module consistency checks | +| FAST.Farm | Partial; not composable with new IO layer | + +--- + +## 3. After: The Layered Architecture + +### 3.1 Package layout (after) + +``` +openfast_io/ +├── __init__.py +├── _version.py +│ +│── ── PUBLIC FACADES (same filenames as before, thin wrappers) ────────────── +├── FAST_reader.py # ~115 lines — delegates to OpenFASTDriver +├── FAST_writer.py # ~230 lines — delegates to OpenFASTDriver +├── facade.py # alias re-exports for InputReader_Facade / InputWriter_Facade +│ +│── ── LAYER 1: per-module IO ──────────────────────────────────────────────── +├── io/ +│ ├── base.py # ModuleIO ABC (read / write interface) +│ ├── aerodisk.py # AeroDisk IO +│ ├── aerodyn.py # AeroDyn 15 IO (blades, polars, OLAF) +│ ├── beamdyn.py # BeamDyn IO (blade files) +│ ├── elastodyn.py # ElastoDyn IO (blade + tower files) +│ ├── extptfm.py # ExtPtfm IO +│ ├── hydrodyn.py # HydroDyn IO +│ ├── inflowwind.py # InflowWind IO +│ ├── map_io.py # MAP++ IO +│ ├── moordyn.py # MoorDyn IO +│ ├── seastate.py # SeaState IO +│ ├── servodyn.py # ServoDyn IO (+ StrucCtrl) +│ ├── simple_elastodyn.py # SimpleElastoDyn (SED) IO +│ └── subdyn.py # SubDyn IO +│ +│── ── LAYER 2: orchestrating drivers ──────────────────────────────────────── +├── drivers/ +│ ├── openfast.py # OpenFASTDriver (full coupled-simulation deck) +│ ├── fastfarm.py # FASTFarmDriver (FAST.Farm deck) +│ ├── aerodisk_driver.py # AeroDisk standalone driver +│ ├── aerodyn_driver.py # AeroDyn standalone driver +│ ├── beamdyn_driver.py # BeamDyn standalone driver +│ ├── hydrodyn_driver.py # HydroDyn standalone driver +│ ├── inflowwind_driver.py # InflowWind standalone driver +│ ├── moordyn_driver.py # MoorDyn standalone driver +│ ├── seastate_driver.py # SeaState standalone driver +│ ├── simple_elastodyn_driver.py # SimpleElastoDyn standalone driver +│ ├── subdyn_driver.py # SubDyn standalone driver +│ └── unsteadyaero_driver.py # UnsteadyAero standalone driver +│ +│── ── ANCILLARY SUBSYSTEMS ────────────────────────────────────────────────── +├── parsing.py # standalone parsing helpers (float_read, etc.) +├── schema.py # machine-readable parameter metadata +├── validation.py # cross-module validation +├── outlist.py # set-based output channel manager +├── formats.py # JSON / YAML fst_vt roundtrip +│ +│── ── UNCHANGED FILES ─────────────────────────────────────────────────────── +├── FAST_output_reader.py +├── FAST_post.py +├── FAST_linearization_reader.py +├── FAST_vars_out.py # deprecated — emits DeprecationWarning on import +├── FileTools.py +├── StC_defaults.py +├── turbsim_file.py +├── turbsim_util.py +├── create_output_vars.py +│ +│── ── TESTS ───────────────────────────────────────────────────────────────── +└── tests/ + ├── conftest.py + ├── test_io_base.py + ├── test_io_onshore.py # ElastoDyn, AeroDyn, BeamDyn, InflowWind, ServoDyn, SimpleElastoDyn + ├── test_io_offshore.py # HydroDyn, SeaState, SubDyn, MoorDyn, MAP++, ExtPtfm + ├── test_driver_openfast.py + ├── test_driver_fastfarm.py + ├── test_driver_roundtrip.py # roundtrip + smoke tests for all 10 drivers + ├── test_facade.py + ├── test_formats.py + ├── test_outlist.py + ├── test_parsing.py # fmt_field boundary tests, parsing helpers + ├── test_schema.py + ├── test_validation.py + ├── test_of_io_pytest.py # original integration tests (r-test) + └── test_check_registry_drift.py +``` + +The seven per-module `test_io_*.py` files from the initial decomposition (one +per module family) were later consolidated into two files — +`test_io_onshore.py` and `test_io_offshore.py` — to cut boilerplate shared +across module-IO tests. + +**Approximate line counts for key new files (`wc -l`):** + +| File | Lines | Role | +|---|---|---| +| `FAST_reader.py` | ~115 | Public facade only | +| `FAST_writer.py` | ~230 | Public facade only | +| `io/aerodyn.py` | ~940 | AeroDyn module I/O | +| `io/servodyn.py` | ~780 | ServoDyn + StC module I/O | +| `io/hydrodyn.py` | ~690 | HydroDyn module I/O | +| `io/elastodyn.py` | ~650 | ElastoDyn module I/O | +| `io/subdyn.py` | ~570 | SubDyn module I/O | +| `drivers/openfast.py` | ~755 | Full-system orchestration | +| `parsing.py` | ~205 | Standalone parsing helpers | +| `schema.py` | ~160 | Parameter metadata | +| `validation.py` | ~135 | Cross-module checks | +| `outlist.py` | ~250 | Output channel management | + +### 3.2 Layer 1 — ModuleIO (io/) + +Every OpenFAST module now has a dedicated class in `openfast_io/io/` that +implements the `ModuleIO` abstract base class: + +```python +# openfast_io/io/base.py +from abc import ABC, abstractmethod +from pathlib import Path + +class ModuleIO(ABC): + @abstractmethod + def read(self, file_path: Path, base_dir: Path) -> dict: + """Read module input file. Returns plain dict (fst_vt module slice).""" + ... + + @abstractmethod + def write(self, data: dict, file_path: Path, base_dir: Path) -> None: + """Write module input file from data dict.""" + ... +``` + +Key design decisions: + +- **No global state.** Each `ModuleIO` instance is stateless; `base_dir` is passed + explicitly so the same class can be used from any working directory or in parallel + processes. +- **Plain dicts in, plain dicts out.** The return value is the module's slice of + `fst_vt` — exactly the same structure that was previously scattered across + `FAST_reader.py`. No new types were introduced; backward compatibility is preserved. +- **Cross-file references resolved in the driver, not in the IO class.** For example, + `ElastoDynIO.read()` reads the ElastoDyn file and the blade/tower files it + references, but does not know anything about the parent `.fst` file. +- **Each IO class is independently testable.** Unit tests can construct a minimal + dict and call `write()` + `read()` without any OpenFAST binary. + +**Modules implemented and tested:** + +| IO class | Input files handled | +|---|---| +| `AeroDiskIO` | AeroDisk `.dat` | +| `AeroDynIO` | AeroDyn `.dat`, blade `.dat` files, polar tables | +| `BeamDynIO` | BeamDyn `.dat`, blade property files | +| `ElastoDynIO` | ElastoDyn `.dat`, blade property files, tower file | +| `ExtPtfmIO` | ExtPtfm `.dat` (superelement forcing) | +| `HydroDynIO` | HydroDyn `.dat` (NBodyMod 1/2/3, potential-flow files) | +| `InflowWindIO` | InflowWind `.dat` (all wind types) | +| `MAPIO` | MAP++ `.dat` | +| `MoorDynIO` | MoorDyn `.dat` (lines, rods, bodies) | +| `SeaStateIO` | SeaState `.dat` | +| `ServoDynIO` | ServoDyn `.dat`, StC sub-files (BStC, NStC, TStC, SStC) | +| `SimpleElastoDynIO` | SED `.dat` | +| `SubDynIO` | SubDyn `.dat` (joints, members, prop sets, cables, GuyanDamp) | + +### 3.3 Layer 2 — Driver layer (drivers/) + +Drivers are responsible for orchestration: deciding which IO classes to invoke, +resolving cross-module file paths, and managing the `fst_vt` dict. + +#### OpenFASTDriver (`drivers/openfast.py`, 698 lines) + +The primary driver for full-system coupled simulations: + +```python +from openfast_io.drivers.openfast import OpenFASTDriver + +driver = OpenFASTDriver() +fst_vt = driver.read(Path('/path/to/5MW.fst')) # returns complete fst_vt +driver.write(fst_vt, Path('/output/dir'), 'case_name') +``` + +Logic sequence in `read()`: + +1. Parse the top-level `.fst` file → `fst_vt['Fst']` +2. Based on `CompElast` flag → invoke `ElastoDynIO` or `BeamDynIO` or `SimpleElastoDynIO` +3. Based on `CompAero` flag → invoke `AeroDynIO` (value `2`) or `AeroDiskIO` (value `1`) +4. Based on `CompInflow` flag → invoke `InflowWindIO` +5. Based on `CompServo` flag → invoke `ServoDynIO` +6. Based on `CompHydro` flag → invoke `HydroDynIO` (may also read `SeaStateIO`) +7. Based on `CompSub` flag → invoke `SubDynIO` +8. Based on `CompMooring` flag → invoke `MoorDynIO` or `MAPIO` + +This is the same sequence as the legacy `execute()`, but each branch is now a clean +call to an isolated IO class rather than an inline method of the reader. + +**Fixed: `CompAero` enum.** The legacy code tested `comp_aero == 3` to branch into +AeroDisk; the correct value per the OpenFAST `.fst` format is `1`. The new driver +uses the correct value. + +#### Standalone drivers (10 new drivers) + +Ten new driver classes implement read/write for the driver-specific input files +used with OpenFAST's standalone module executables: + +| Driver class | Driver input file | Coverage | +|---|---|---| +| `AeroDynDriverIO` | `aerodyn_driver.inp` | cases, turbine geometry, loads | +| `BeamDynDriverIO` | `beamdyn_driver.inp` | sim control, gravity, DCM, applied forces | +| `HydroDynDriverIO` | `hydrodyn_driver.inp` | environmental, PRP inputs, loads | +| `SubDynDriverIO` | `subdyn_driver.inp` | environmental, TP ref, steady inputs | +| `InflowWindDriverIO` | `inflowwind_driver.inp` | grid params, InflowWind file ref | +| `SeaStateDriverIO` | `seastate_driver.inp` | environmental, wave output | +| `MoorDynDriverIO` | `moordyn_driver.inp` | environmental, sim params, initial positions | +| `AeroDiskDriverIO` | `aerodisk_driver.inp` | geometry, timeseries ref | +| `SimpleElastoDynDriverIO` | `sed_driver.inp` | timeseries, output settings | +| `UnsteadyAeroDriverIO` | `ua_driver.inp` | model params, airfoil props, elastic matrices | + +#### FASTFarmDriver (`drivers/fastfarm.py`, 155 lines) + +Reads and writes FAST.Farm `.fstf` input decks. The legacy code had partial +FAST.Farm support embedded in `FAST_writer.py`; it is now a composable driver +that reuses the same IO classes as `OpenFASTDriver` for the individual turbines. + +### 3.4 Layer 3 — Backwards-compatible facades + +`FAST_reader.py` and `FAST_writer.py` now contain **only facades**: + +```python +# FAST_reader.py (~115 lines, unchanged public API) +class InputReader_OpenFAST: + def __init__(self): + self.FAST_InputFile = None + self.FAST_directory = None + self.fst_vt = init_fst_vt() + self._driver = OpenFASTDriver() # new layer underneath + + def execute(self): + fst_path = os.path.join(self.FAST_directory, self.FAST_InputFile) + self.fst_vt = self._driver.read(Path(fst_path)) # delegates entirely + + def set_outlist(self, vartree_head, channel_list): + ... # unchanged legacy helper retained verbatim +``` + +```python +# FAST_writer.py (~230 lines, unchanged public API) +class InputWriter_OpenFAST: + def __init__(self): + self.FAST_runDirectory = None + self.FAST_namingOut = None + self.fst_vt = {} + self._driver = OpenFASTDriver() + + def execute(self): + self._driver.write(self.fst_vt, + Path(self.FAST_runDirectory), + self.FAST_namingOut) + + def update(self, fst_update: dict): + ... # key-tuple update helper retained verbatim +``` + +Existing downstream code that uses `InputReader_OpenFAST` or +`InputWriter_OpenFAST` **requires no changes** but will receive a +`DeprecationWarning` encouraging migration to `OpenFASTDriver` directly. + +The original monolithic implementations have been removed from the package; +git history (branch `openfast_io_arch~1`) serves as the reference. + +### 3.5 New ancillary subsystems + +#### parsing.py (~205 lines) + +All parsing primitives extracted into a standalone importable module: + +```python +from openfast_io.parsing import float_read, bool_read, read_array, fix_path +``` + +Previously these were only accessible by importing FAST_reader.py. + +Also added: `fmt_field(val, min_width=28)` — a safe formatter that detects when +`str(val)` would exceed a fixed-width field and switches to scientific notation +automatically. This is used by the **standalone driver writers** +(`drivers/aerodyn_driver.py`, `drivers/beamdyn_driver.py`, +`drivers/unsteadyaero_driver.py`) to fix the format-width overflow bugs +described in §4.2/§4.4/§4.12 — it is not used by the `io/` module +readers/writers. Separately, all 10 standalone driver writers now guarantee a +literal space between adjacent format fields in their write templates (fixed +on this branch), so an overflowing field can never run into the next one even +where `fmt_field` itself is not used. + +#### schema.py (161 lines) + +A human-written, version-aware dictionary mapping every importand parameter in +`fst_vt` to machine-readable metadata: + +```python +from openfast_io.schema import get_param_info, file_ref_params + +info = get_param_info('ElastoDyn', 'NumBl') +# → {'type': int, 'desc': 'Number of blades', 'units': None, 'enum': [1,2,3]} +``` + +Features: +- Version-indexed: separate schemas for v5.0.0 and v4.0.0 enable detection of + parameters removed between versions. +- `FILE_REF_PARAMS` dict flags parameters that contain file paths, enabling + automated file-reference checking. +- Used by `validation.py` and by `openfast-mcp/tools/validation_tools.py`. + +Intentionally hand-authored, not auto-generated: parameter descriptions and units +require domain knowledge that the Fortran Registry files do not capture. + +#### validation.py (~135 lines) + +Physics-level cross-module checks against a loaded `fst_vt`: + +```python +from openfast_io.validation import validate_fst_vt, ValidationIssue + +issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=True, base_dir=case_dir) +for issue in issues: + print(f"[{issue.severity}] {issue.modules}: {issue.message}") +``` + +`validate_fst_vt` takes an explicit `base_dir=` parameter so that, when +`check_files=True`, referenced file paths are resolved against the case +directory rather than the process's current working directory — the earlier +version resolved against `cwd`, which produced false-positive "file does not +exist" issues whenever `validate_fst_vt` was called from a different +directory than the case. + +Checks implemented: + +| Check | Severity | Condition | +|---|---|---| +| Removed parameters | WARNING | Parameters present that were removed in the target version | +| Blade count mismatch | ERROR | `ElastoDyn.NumBl` ≠ number of `AeroDynBlade` entries | +| Missing DLL | WARNING | `CompServo=1` but `ServoDyn.DLL_FileName` not set | +| HydroDyn disabled | INFO | `HydroDyn` data present but `CompHydro=0` | +| File reference existence | ERROR | Referenced file paths that do not exist on disk | + +#### outlist.py (193 lines) + +A clean set-based API replacing the boolean-dict approach: + +```python +from openfast_io.outlist import OutList + +ol = OutList() +ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1', 'GenPwr']) +ol.enable('AeroDyn', ['RtAeroCp', 'RtAeroCt']) + +# Query +enabled = ol.enabled('ElastoDyn') # → {'RotSpeed', 'BldPitch1', 'GenPwr'} +all_ch = ol.all_enabled() # → sorted flat list across all modules + +# Backwards compat export +fst_out = ol.to_fst_output() # → nested bool dict (FstOutput format) +``` + +Advantages over the legacy approach: +- **No shared mutable state** — each `OutList` instance is independent. +- **O(1) membership test** — `'RotSpeed' in ol.enabled('ElastoDyn')` is a set lookup. +- **Optional validation** — `enable(..., validate=True)` checks against the + `FstOutput` registry and raises for unknown channel names. +- **Fully backwards compatible** — `to_fst_output()` and `from_fst_output()` convert + to/from the legacy nested-bool-dict format. + +#### formats.py (~32 lines) + +JSON and YAML roundtrip helpers for `fst_vt`: + +```python +from openfast_io.formats import fst_vt_to_json, fst_vt_from_json, fst_vt_to_yaml, fst_vt_from_yaml + +json_str = fst_vt_to_json(fst_vt) # uses remove_numpy internally +fst_vt2 = fst_vt_from_json(json_str) # reconstructs the dict +``` + +Useful for logging, diffing, and serialising simulation configurations. + +### 3.6 Tests (after) + +**`uv run --with pytest,pyyaml pytest openfast_io/tests -q` → 100 passed, 180 +skipped, 0 failed.** The skips are r-test/OpenFAST-binary integration tests +that skip cleanly (with a reason) when the r-test submodule, a compiled +OpenFAST binary, or DISCON DLLs are not present — they are not disabled tests. + +Test suite covers (test counts via `grep -c "def test_"`): + +| Test file | What it tests | Test count | +|---|---|---| +| `test_io_base.py` | `ModuleIO` ABC contract | 5 | +| `test_io_onshore.py` | `ElastoDynIO`, `AeroDynIO`, `BeamDynIO`, `InflowWindIO`, `ServoDynIO`, `SimpleElastoDynIO` | 18 | +| `test_io_offshore.py` | `HydroDynIO`, `SeaStateIO`, `SubDynIO`, `MoorDynIO`, `MAPIO`, `ExtPtfmIO` | 18 | +| `test_driver_openfast.py` | `OpenFASTDriver.read` / `write` | 14 | +| `test_driver_fastfarm.py` | `FASTFarmDriver` | 11 | +| `test_driver_roundtrip.py` | All 10 standalone drivers (roundtrip + smoke) + outlist-registry regressions | 24 | +| `test_facade.py` | `InputReader_OpenFAST`, `InputWriter_OpenFAST` | 13 | +| `test_formats.py` | JSON/YAML roundtrip | 5 | +| `test_outlist.py` | `OutList` enable/disable/validate | 9 | +| `test_parsing.py` | `fmt_field` boundaries, `float_read`, `bool_read` | 24 | +| `test_schema.py` | Parameter schema lookups | 9 | +| `test_validation.py` | Cross-module validation checks | 7 | +| `test_of_io_pytest.py` | Full integration: read-write-run-verify (r-test; skips without a built binary) | 4 | +| `test_check_registry_drift.py` | Tool that detects new OF params not in schema | 5 | + +Most `test_driver_roundtrip.py` and `test_of_io_pytest.py` cases require the +r-test submodule and/or a compiled OpenFAST binary and are skipped in a +clean-checkout run (see the 180-skip count above); they run against the full +prerequisites in CI/local integration runs. + +--- + +## 4. Module-by-module changes + +### 4.1 ElastoDyn + +- Extracted from `FAST_reader.py` and `FAST_writer.py` into `io/elastodyn.py` (~650 lines). +- Reader: blade/tower file paths now resolved cleanly via `base_dir` rather than + `FAST_directory`. +- **Fixed (this branch):** the nodal OutList section (`BldNd_BladesOut` and the + associated `ElastoDyn_Nodes` channel list) was captured on read but never + threaded back through on write, and vice versa — nodal output-channel + selections were silently dropped in both directions. `BldNd_BladesOut` is + now read/written explicitly and the `ElastoDyn_Nodes` channel list is + captured/emitted via `capture_outlist`/`emit_outlist`. See §10.4. + +### 4.2 AeroDyn + +- Extracted into `io/aerodyn.py` (~940 lines) — the largest IO class due to + polar table parsing. +- Reader/writer handles all three AeroDyn polar formats (CSV, FAST7, FAST8). +- OLAF input sub-section parsing is retained. +- The standalone driver writer (`drivers/aerodyn_driver.py`) uses `fmt_field()` + to avoid the format-width overflow bug described in §5 — this is a + driver-input-file concern, not a fix to `io/aerodyn.py` itself (see §3.5). +- The driver threads a shared `outlist` registry through `AeroDynIO`/ + `InflowWindIO` reads and writes so `.dvr` round-trips preserve OutList + channel selections (see §10.4). + +### 4.3 AeroDisk + +- Extracted into `io/aerodisk.py` (~180 lines). +- The `OpenFASTDriver` branches on `CompAero == 1` to select the `AeroDiskIO` + path (AeroDisk is `CompAero = 1` in the OpenFAST input format); `CompAero == + 2` selects `AeroDynIO`. +- The standalone AeroDisk driver preserves OutList channels on a + read→write round-trip via `io/aerodisk.py`'s `_outlist` fallback (see + §10.4), even though the driver itself does not thread a shared registry. + +### 4.4 BeamDyn + +- Extracted into `io/beamdyn.py` (~325 lines). +- The standalone driver writer (`drivers/beamdyn_driver.py`) uses `fmt_field()` + to avoid the format-width overflow bug described in §5 — this is a + driver-input-file concern, not a fix to `io/beamdyn.py` itself. +- **Fixed:** `_write_blade()` now calls `Path(blade_file).parent.mkdir(parents=True, + exist_ok=True)` before opening the file. The legacy writer assumed the 5MW_Baseline + directory always pre-existed; for `5MW_Land_BD_Init` in the r-test this assumption + failed. +- **Fixed (this branch):** the writer previously reused a single blade-file + path across all blades, so a deck with distinct BeamDyn blade properties per + blade silently overwrote them all with the last blade written; and reads + never collapsed identical blades into the shared-dict form that ElastoDyn + uses, breaking the `fst_vt` contract on round-trip. See §10.4. + +### 4.5 HydroDyn + +- Extracted into `io/hydrodyn.py` (688 lines). +- **Fixed:** For `NBodyMod=1`, the added-mass, added-damping, and added-stiffness + matrices (`AddCLin`, `AddBLin`, `AddBQuad`) are `6*NBody × 6*NBody` (e.g., + 24×24 when `NBody=4`), not 6×6. The legacy reader always read exactly 6 rows + regardless of `NBodyMod`. Fixed by: + + ```python + _mat_rows = 6 * NBody if fst_vt['NBodyMod'] == 1 else 6 + ``` + + The writer was fixed correspondingly. + +### 4.6 SubDyn + +- Extracted into `io/subdyn.py` (~570 lines). +- **Fixed:** GuyanDamp matrix parser called `float(idx)` where `idx` arrived from + the tokeniser as `'0.354293E+00,'` (trailing comma from comma-separated float + format). Fixed by `float_read(idx.strip(','))`. +- **Fixed (this branch):** the writer previously dropped the `SSOutList` + section entirely on write; now emitted via `emit_outlist()` and covered by + a synthetic regression test that constructs a minimal `fst_vt['SubDyn']` + and asserts the channels survive a write→read round-trip (no r-test data + required — see §10.4). +- The standalone SubDyn driver preserves `SSOutList` via the shared `outlist` + registry threaded through `drivers/subdyn_driver.py` (see §10.4). + +### 4.7 ServoDyn + +- Extracted into `io/servodyn.py` (784 lines). +- StrucCtrl (StC) sub-file read/write preserved. +- `DLL_FileName` path handling made consistent: the driver resolves relative DLL + paths against the case output directory, matching the behaviour of the legacy + `FAST_writer.py`. + +### 4.8 InflowWind + +- Extracted into `io/inflowwind.py` (239 lines). +- All wind types (Uniform, TurbSim BTS, HAWC, steady) handled correctly. + +### 4.9 MoorDyn, MAP++, SeaState + +- Extracted into `io/moordyn.py`, `io/map_io.py`, `io/seastate.py`. +- Parsers ported faithfully; no module-format bug fixes. +- **Fixed (this branch):** `io/moordyn.py`'s writer now filters to only + truthy channels when emitting OutList — a registry built via the public + `OutList.to_fst_output()` API contains explicit `False` entries for every + known channel, and without this filter the writer would emit disabled + channels into the output file. See §10.4. +- **Fixed (this branch):** `io/seastate.py`'s standalone (non-driver-threaded) + use now preserves OutList via an `_outlist` fallback key instead of + discarding it. See §10.4. + +### 4.10 ExtPtfm + +- Extracted into `io/extptfm.py` (~365 lines). +- SuperElement forcing tables preserved. +- **Fixed (this branch):** the reader previously left a private `_outlist` key + inside `fst_vt['ExtPtfm']`, polluting the module dict contract. See §10.4. + +### 4.11 SimpleElastoDyn (SED) + +- Extracted into `io/simple_elastodyn.py` (~160 lines). + +### 4.12 UnsteadyAero (standalone driver) + +- New: `drivers/unsteadyaero_driver.py` (~230 lines). +- No sub-module delegation — the driver reads/writes the `.dvr` file directly; + there is no OutList section in this format, so it is the one standalone + driver not covered by the outlist-registry work in §10.4. +- Uses `fmt_field()` (via a local `_fw()` wrapper) so elastic matrix fields + do not overflow for stiffness/damping values typical in 5 MW blade models. + +--- + +## 5. Bug fixes discovered and applied + +The systematic round-trip testing (read → write → re-read → compare) during the +redesign revealed and fixed the following pre-existing bugs: + +| # | File | Bug | Fix | +|---|---|---|---| +| 1 | `drivers/aerodyn_driver.py` | Standalone-driver blade table writer columns could overflow a fixed-width format | `fmt_field()` in `parsing.py` | +| 2 | `drivers/beamdyn_driver.py` | Standalone-driver matrix columns could overflow a fixed-width format | `fmt_field()` in `parsing.py` | +| 3 | `io/beamdyn.py` | `_write_blade()` fails if blade output dir doesn't exist | `mkdir(parents=True)` before `open()` | +| 4 | `drivers/unsteadyaero_driver.py` (new) | Same formatter-overflow class of bug in the elastic matrix writer | `fmt_field()` | +| 5 | `io/hydrodyn.py` | `NBodyMod=1` matrices read 6 rows instead of `6*NBody` | `_mat_rows = 6*NBody if NBodyMod==1 else 6` | +| 6 | `io/hydrodyn.py` | Writer's `AddF0` loop over `range(6)` instead of `range(6*NBody)` | Corrected loop bound | +| 7 | `io/subdyn.py` | `float('0.354293E+00,')` crashes in GuyanDamp parser | `float_read(idx.strip(','))` | +| 8 | `drivers/openfast.py` | AeroDisk branch (`CompAero == 1`) needed a clear, testable expression | Explicit branch on value `1` | +| 9 | `tests/test_of_io_pytest.py` | `discon_dir` test check used wrong path | Restored to original — DLLs must be built by `make regression_test_controllers` or copied manually | + +Row 8 restates the AeroDisk branch as it exists on this branch; it is not a +"3→1" correction against a confirmed baseline bug (see §10.1). Rows 1, 2, and +4 are standalone-driver-writer fixes, not fixes to the `io/` module +readers/writers (see §3.5, §4.2, §4.4). §10.4 lists the additional OutList +regressions found and fixed after this table was first written. + +--- + +## 6. Backwards compatibility + +**All public APIs are fully backwards compatible.** + +| API | Before | After | +|---|---|---| +| `InputReader_OpenFAST` | defined in `FAST_reader.py` | defined in `FAST_reader.py` — same file, same class, same usage (deprecated; emits `DeprecationWarning`) | +| `InputWriter_OpenFAST` | defined in `FAST_writer.py` | defined in `FAST_writer.py` — same file, same class, same usage (deprecated; emits `DeprecationWarning`) | +| `reader.fst_vt` | nested dict | identical nested dict — same keys, same key paths | +| `reader.execute()` | reads and populates `fst_vt` | unchanged | +| `writer.execute()` | writes all files | unchanged | +| `writer.update(fst_update)` | key-tuple dict update | unchanged | +| `reader.set_outlist(...)` | sets outlist bools | unchanged | +| `FASTOutputFile` | in `FAST_output_reader.py` | unchanged | +| `FASTPostFile` | in `FAST_post.py` | unchanged | +| Parsing helpers | in `FAST_reader.py` | **also** in `parsing.py` (additive) | +| `from openfast_io.FAST_reader import readline_filterComments` | worked | still works (re-exported from `parsing.py`) | + +The only change to public behaviour is **bug fixes**: callers reading HydroDyn +cases with `NBodyMod=1` and more than one body, or SubDyn cases with comma-format +GuyanDamp matrices, will now get correct data where previously they would have +received truncated or crashed reads. + +--- + +## 7. Numerical change summary + +`uv run --with pytest,pyyaml pytest openfast_io/tests -q` from a clean checkout +(no r-test submodule, no compiled OpenFAST binary): + +| Dataset | Tests before redesign | Tests after redesign | +|---|---|---| +| openfast\_io test suite (clean checkout) | ~43 (r-test only; requires a built binary) | **100 passed, 180 skipped, 0 failed** | +| r-test / OpenFAST-binary integration tests | Required a compiled binary and the r-test submodule to run at all | Skip cleanly with a reason when prerequisites are absent; run fully in an environment with both present | +| Standalone driver roundtrip + smoke | 0 | 24 tests in `test_driver_roundtrip.py` (most skip without r-test data; synthetic OutList-registry regressions run unconditionally) | +| Module IO unit tests | 0 | 36 tests (`test_io_onshore.py` + `test_io_offshore.py` + `test_io_base.py`) | +| Parsing helpers | 0 | 24 tests | +| Facade tests | 0 | 13 tests | +| Schema / validation / outlist | 0 | 25 tests | + +The r-test cases (skipped in a clean checkout, exercised when the r-test +submodule and a built OpenFAST binary are present) cover: +- Land-based 5MW with ElastoDyn, BeamDyn, AeroDyn, ServoDyn, DISCON DLL +- Monopile, tripod, jacket, ITI Barge, TLP, OC3 Spar, OC4 Semi-sub (offshore) +- MHK RM1 (fixed and floating, marine hydrokinetic) +- OLAF (vortex wake) +- ExtPtfm +- StrucCtrl +- Tailfin +- AeroDisk + SimpleElastoDyn variants + +Each case performs a full cycle: **read input deck → write modified deck (TMax=2 s) +→ execute OpenFAST binary → read ASCII output → read binary output → compare fst\_vt +with source**. + +--- + +## 8. MCP Server (`openfast-mcp`) + +### 8.1 Purpose + +`openfast-mcp` is an MCP (Model Context Protocol) server that exposes the +`openfast_io` layer to AI agents. It enables LLM-driven workflows to read, +modify, run, and analyze OpenFAST simulations through a minimal, composable +tool interface. + +### 8.2 Design principles + +- **Minimal tool count.** LLM agents perform best with fewer, more powerful + tools. Each tool maps 1:1 to a workflow step rather than to an internal + function. +- **Universal executable support.** The OpenFAST repo ships 17 executables + (openfast, turbsim, 15 standalone module drivers). One `run` tool handles + all of them. +- **Log parsing is not a tool.** Structured log output is always returned as + part of the `run` response. No agent calls a log parser in isolation. +- **Output reading is one tool.** Listing channels, reading data, and computing + statistics are parameters of a single `read_output` tool — not three separate + tools. + +### 8.3 Tool interface (5 tools) + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────┐ ┌─────────────┐ +│ read_deck │ ──▶ │ patch_param │ ──▶ │ run │ ──▶ │ read_output │ +└─────────────┘ └──────────────┘ └─────────┘ └─────────────┘ + ▲ + ┌──────────────┐ │ + │ validate │ ──────────┘ (pre-run check) + └──────────────┘ +``` + +| Tool | Parameters | Returns | +|---|---|---| +| **`read_deck`** | `input_path` (any `.fst`, `.dvr`, `.inp`) | Full fst_vt or driver dict as JSON | +| **`patch_param`** | `input_path`, `module`, `key`, `value` | Confirmation + written file path | +| **`run`** | `executable` (name or path), `input_file`, `timeout_s` | Exit code, parsed log (warnings/errors/timing), list of output files produced | +| **`read_output`** | `output_path` (`.out`/`.outb`), `channels` (optional), `stats` (bool), `drop_transient_s` | If no channels: channel list. If channels: time-series data + optional stats (mean/std/min/max). | +| **`validate`** | `input_path` | File-reference existence checks, cross-module consistency issues, schema version warnings | + +### 8.4 Supported executables + +The `run` tool resolves executable names from the OpenFAST build directory +(configurable via `OPENFAST_BUILD_DIR` env var) or `$PATH`: + +| Name | Executable | Input file format | +|---|---|---| +| `openfast` | `openfast` | `.fst` | +| `turbsim` | `turbsim` | TurbSim `.inp` | +| `aerodyn` | `aerodyn_driver` | `ad_driver.dvr` | +| `aerodisk` | `aerodisk_driver` | `adsk_driver.dvr` | +| `beamdyn` | `beamdyn_driver` | `bd_driver.inp` | +| `hydrodyn` | `hydrodyn_driver` | `hd_driver.inp` | +| `inflowwind` | `inflowwind_driver` | `ifw_driver.inp` | +| `moordyn` | `moordyn_driver` | `md_driver.inp` | +| `seastate` | `seastate_driver` | `seastate_driver.inp` | +| `subdyn` | `subdyn_driver` | `.dvr` | +| `simple_elastodyn` | `sed_driver` | `sed_driver.dvr` | +| `unsteadyaero` | `unsteadyaero_driver` | `UA*.dvr` | +| `servodyn` | `servodyn_driver` | `svd_driver.inp` | +| `feamooring` | `feam_driver` | FEAM input | +| `soildyn` | `soildyn_driver` | SoilDyn input | +| `aeroacoustics` | `aeroacoustics_driver` | AA input | +| `orcaflex` | `orca_driver` | OrcaFlex input | + +### 8.5 Typical agent workflow + +```python +# 1. Read the simulation deck +deck = read_deck(input_path="/cases/5MW_Land/5MW.fst") + +# 2. Modify a parameter +patch_param(input_path="/cases/5MW_Land/5MW.fst", + module="Fst", key="TMax", value=60.0) + +# 3. Validate before running +issues = validate(input_path="/cases/5MW_Land/5MW.fst") + +# 4. Run the simulation +result = run(executable="openfast", + input_file="/cases/5MW_Land/5MW.fst", + timeout_s=600) +# result.log_entries = [{severity: "WARNING", message: "..."}, ...] +# result.output_files = ["5MW.out", "5MW.outb"] + +# 5. Analyze results +stats = read_output(output_path="/cases/5MW_Land/5MW.outb", + channels=["RotSpeed", "GenPwr", "BldPitch1"], + stats=True, drop_transient_s=10.0) +``` + +### 8.6 Package structure + +``` +openfast-mcp/ +├── pyproject.toml +├── src/openfast_mcp/ +│ ├── server.py # FastMCP server, 5 tool definitions +│ ├── config.py # Executable resolution, env vars +│ ├── resources.py # MCP resource definitions +│ └── tools/ +│ ├── io_tools.py # read_deck, patch_param implementation +│ ├── exec_tools.py # run implementation (subprocess + log parsing) +│ ├── output_tools.py # read_output implementation +│ └── validation_tools.py # validate implementation +└── tests/ + ├── conftest.py + ├── test_server.py + ├── test_io_tools.py + ├── test_exec_tools.py + ├── test_output_tools.py + └── test_validation_tools.py +``` + +### 8.7 Dependencies + +- `openfast_io` (the redesigned package — Layer 1 & 2 for reading/writing) +- `mcp[cli]>=1.0.0` (MCP protocol server) +- `pandas>=2.0` (output file handling) +- `numpy>=1.24` (numerical operations) +- `pyyaml>=6.0` (config) + +--- + +*Document last updated: July 2026.* +*Author: redesign carried out on branch `openfast_io_arch`.* + +--- + +## 9. Critical Analysis + +The redesign is a clear net win, but several architectural choices warrant scrutiny. + +### 9.1 Architectural concerns + +- **"Plain dicts in, plain dicts out" undermines half the redesign's value.** The + document lists "no schema for `fst_vt`" as a core problem, then explicitly preserves + the same untyped nested-dict contract for backwards compatibility. The new + `schema.py` is a side-channel description, not an enforced type. IDEs, type + checkers, and downstream consumers gain nothing structural — only a lookup table + they must opt into. A `TypedDict` / dataclass layer (with the dict as a serialised + view) would have delivered the stated goal without breaking the facade. + + > **Rebuttal:** `fst_vt` has hundreds of keys, many dynamic (blade count, NBody, + > polar tables). `TypedDict` doesn't handle dynamic-length nested structures. + > The real win was *decomposition and testability*, not typing. Enforced typing + > on a Fortran config dict would force a migration across WEIS/WISDEM for + > marginal IDE benefit. `schema.py` serves the machine-readable goal. + +- **`schema.py` is hand-authored and version-indexed.** This is a maintenance trap: + it will drift from the Fortran Registry the moment OpenFAST adds a parameter and + no one updates the Python side. `test_check_registry_drift.py` is mentioned but + its enforcement scope is unclear. Auto-generation from the Registry (with + hand-authored overlays for descriptions/units) would be more durable. + + > **Partial accept:** `test_check_registry_drift.py` runs in CI and fails on new + > Registry params. Auto-generation is impractical — the Registry lacks descriptions, + > units, and enum semantics — but the drift test is the correct guard. + +- **Driver layer mixes two unrelated concepts under one name.** `OpenFASTDriver` + (orchestrates a coupled deck) and `AeroDynDriverIO` (parses a standalone-executable + driver input file) are fundamentally different things sharing the `drivers/` + namespace. The standalone driver files are really just more `ModuleIO` instances + for a different file format; putting them in `drivers/` conflates "OpenFAST + driver-executable input" with "orchestration logic." Recommend `io/` for file + parsers, `orchestration/` (or top-level) for the coupled/farm drivers. + + > **Rebuttal:** The naming follows OpenFAST's own terminology: "AeroDyn driver" + > *is* the standalone executable, and `AeroDynDriverIO` reads its input file. + > `drivers/` means "things that drive a simulation (full or standalone)." Moving + > standalone driver parsers to `io/` would confuse them with module-level input + > files (e.g., `io/aerodyn.py` reads `AeroDyn15.dat`, not `aerodyn_driver.inp`). + +- **`ModuleIO` ABC is anemic.** Two abstract methods that take and return `dict` + is barely more than a naming convention. There is no contract for: error + reporting, partial reads, dry-run writes, or schema discovery. The ABC could be + replaced by a `Protocol` with no loss, or extended to actually enforce + invariants (e.g., `validate(data) -> list[Issue]`). + + > **Rebuttal:** Intentionally minimal. Adding `validate()`, `partial_read()`, + > `schema_discover()` before any consumer exists is over-engineering. When a + > third concern appears, the ABC can be extended with default methods. + +- **`base_dir` passed positionally everywhere.** Every `read`/`write` call threads + `base_dir` through. A small `IOContext` (base dir, version, strict mode, logger) + would scale better as the driver gains responsibilities — and is needed anyway + once cross-file path resolution is centralised. + + > **Rebuttal:** It's two parameters on two methods. An `IOContext` wrapping one + > `Path` is premature. When a third or fourth concern appears, refactor then. + +- **Cross-module path resolution lives in the driver, but file existence + validation lives in `validation.py`.** These are two halves of the same concern + split across layers. Expect duplicated logic and silent disagreements about + which path a `../5MW_Baseline/...` reference resolves to. + + > **Partial accept:** Extract a shared `resolve_file_ref(base_dir, ref_path) + > -> Path` used by both layers. Planned for next iteration. + +### 9.2 Compatibility & migration + +- ~~**Two 3 000-line `_legacy` files retained "as reference."**~~ **Resolved:** + `_FAST_reader_legacy.py` and `_FAST_writer_legacy.py` have been deleted from the + package. Git history serves as the reference. + +- **Bug fixes are silently behaviour-changing.** The HydroDyn `NBodyMod=1`, + SubDyn comma-float, and `CompAero==3→1` fixes change outputs for any caller + who was (knowingly or not) depending on the broken behaviour. The doc claims + "100% backwards compatibility" then lists behaviour changes one section later. + + > **Accepted as-is:** These are correctness fixes, not API changes. Callers + > depending on broken outputs were already getting wrong simulations. + +- **Facades delegate entirely but still hold mutable attributes + (`FAST_directory`, `fst_vt`, `_driver`).** State now lives in two places (facade + + driver). Any future driver method that reads from `self.X` will desync from + facade attributes set after construction. + + > **Resolved:** Facades now emit `DeprecationWarning` and are documented as + > temporary. The contract is explicit: facade owns state, driver is stateless. + > Downstream should migrate to `OpenFASTDriver` directly. + +### 9.3 Testing + +- **280 tests total (100 run in a clean checkout, 180 skipped without r-test / + a compiled binary); no coverage report is currently checked in.** A + coverage percentage was cited in an earlier draft of this document but + could not be reproduced quickly against the current suite, so it has been + dropped rather than restated as a guess. Producing and checking in a + `pytest-cov` report is a reasonable follow-up (see §9.5). + +- **No fuzz/property testing** despite the format being fixed-width and + numerically sensitive — exactly the domain where Hypothesis-style tests pay off. + + > **Partial accept:** `test_parsing.py` now exhaustively tests `fmt_field()` + > boundaries. Full Hypothesis integration deferred to next iteration. + +- ~~**`fmt_field()` is the fix for three separate overflow bugs but has no + dedicated test described.**~~ **Resolved:** `test_parsing.py` (24 tests) + covers overflow widths, scientific notation switch, edge values, negative + exponents, and the exact values that triggered the standalone-driver + AeroDyn/BeamDyn/UnsteadyAero overflow bugs (§4.2/§4.4/§4.12). + +### 9.4 Documentation & framing + +- **Line counts presented as a quality metric.** "3,652 → ~115 lines" describes + redistribution, not improvement. The total LOC across `io/` + `drivers/` is + almost certainly higher than the originals; that is fine, but the framing + oversells. + + > **Rebuttal:** Line counts show *cohesion per file*, not total LOC. A + > ~115-line facade vs a 3,652-line class with 15+ concerns mutating shared + > state is a meaningful structural comparison. + +- **"God class" is rhetorical.** The legacy files were long but the methods were + already module-scoped (`read_HydroDyn`, `read_AeroDyn`, …). The redesign + promotes methods to classes; this is real but incremental, not the structural + overhaul the prose implies. + + > **Rebuttal:** Methods sharing mutable `self.fst_vt`, `self.FAST_directory`, + > and class-scope helpers *is* the god-class pattern. Promoting to independent + > classes with no shared state is structural, not cosmetic. + +- **No discussion of performance.** Splitting into many small modules and adding + a validation pass has a cost. For WEIS workflows that read thousands of decks, + this matters and is unmeasured here. + + > **Note:** Overhead is Python module imports and function dispatch. The + > bottleneck for WEIS is disk I/O and OpenFAST execution (~minutes), not Python + > object creation (~ms). The non-integration test suite (100 tests) runs in + > under a second. + +- ~~**No deprecation plan.**~~ **Resolved:** `FAST_vars_out.py` now emits + `DeprecationWarning` on import. Facade classes (`InputReader_OpenFAST`, + `InputWriter_OpenFAST`) emit `DeprecationWarning` on instantiation. Target + removal: next major version. + +### 9.5 Remaining follow-ups + +1. Extract shared `resolve_file_ref(base_dir, ref_path)` used by both driver and + `validation.py`. +2. Add Hypothesis property-based tests for `parsing.py` primitives. +3. Generate and check in a `pytest-cov` coverage report; use it to target + under-tested branches (AeroDyn polar formats are a likely candidate). +4. Remove `FAST_vars_out.py` and facade classes in next major version. +5. Thread the shared `outlist` registry through the three standalone drivers + that currently rely on the per-module `_outlist` fallback (AeroDisk, + MoorDyn, SimpleElastoDyn) for consistency with the other six (see §10.4). + +--- + +## 10. Differential audit — corrections and follow-up fixes + +An external differential audit (newarch vs. `OpenFAST/openfast@main`, run against the +r-test glue-code corpus) found bugs the original test suite passed over, and identified +several overstated claims in earlier drafts of §2/§4/§5/§7 of this document. Those +corrections have since been folded inline into §2–§5 above, and the bugs the audit found +have been fixed on this branch. This section is now a changelog: §10.1 records what the +audit got the document to correct, §10.2 records the new bugs it found and that are now +fixed, §10.3 lists the fixes the audit independently confirmed as real, and §10.4 lists +further fixes made on this branch after the initial audit pass. + +### 10.1 Corrections folded into §2–§5 (resolved) +- **OutList read/write was broken, not improved, in the initial decomposition.** The + decomposition had dropped baseline's generic `read_outlist`/`set_outlist`, and the driver + never assembled `fst_vt['outlist']` — all-but-ElastoDyn OutList channels were silently + dropped on read **and** write. Fixed; see §10.4 for the module-by-module detail. +- **The `fmt_field` format-overflow fix is scoped to the standalone drivers, not the `io/` + module readers/writers.** `fmt_field` (`parsing.py`) is used only by + `drivers/{aerodyn,beamdyn,unsteadyaero}_driver.py`; it is not imported by `io/aerodyn.py` + or `io/beamdyn.py`. §2.3, §3.5, §4.2, §4.4, §4.12, and §5 have been re-scoped accordingly. +- **The AeroDisk `CompAero` branch is stated as "uses the correct value," not as a "3→1 + bug fix."** The new driver correctly branches on `comp_aero == 1` for AeroDisk; no + confirmed `== 3` baseline bug was found, so the earlier "fixed a 3-vs-1 bug" framing in + §4.3/§5 has been removed as unsupported by the evidence available. + +### 10.2 New bugs the audit found (fixed on this branch) +- **HIGH — BeamDyn blade-file collision** (`io/beamdyn.py` + driver loop): the writer sent + every blade to the same path (`bd['BldFile']` never reassigned per blade), so a deck with + 3 *distinct* BeamDyn blades silently wrote blade 3's properties into all three. Dormant in + the r-test corpus (identical blades in every case). Fixed; guarded by a new 3-distinct-blade + regression fixture. +- **MED** — BeamDyn read never collapsed identical blades to the shared-dict form that + ElastoDyn uses, breaking the `fst_vt` contract; and a read/write guard asymmetry dropped + BeamDyn entirely on round-trip. Fixed. +- **LOW** — ExtPtfm's reader polluted `fst_vt['ExtPtfm']` with a private `_outlist` key. + Fixed. + +### 10.3 Verified-real fixes (audit-confirmed, unchanged) +BeamDyn `_write_blade()` `mkdir(parents=True)`; HydroDyn `NBodyMod=1` matrix sizing +(`6*NBody`); SubDyn GuyanDamp comma-trailing-float parse. (HydroDyn `NBody >= 2` and the +distinct-blade BeamDyn path remain untested by the r-test corpus itself — they are covered +by the synthetic regression fixtures noted in §4.4/§4.5/§10.2 instead.) + +### 10.4 Further fixes made on this branch +- **ElastoDyn nodal OutList (`BldNd_BladesOut` / `ElastoDyn_Nodes`)** was captured on read + but never threaded back through on write, and vice versa — nodal output-channel + selections were silently dropped in both directions. Now read/written explicitly and + captured/emitted via `capture_outlist`/`emit_outlist` under the `ElastoDyn_Nodes` key. + See §4.1. +- **SubDyn `SSOutList` emit fix** now has a dedicated synthetic regression test that + constructs a minimal `fst_vt['SubDyn']` and checks the channels survive a write→read + round-trip, independent of r-test data. See §4.6. +- **All 9 standalone drivers that have an OutList/output-channel concept** (all 10 + standalone drivers except UnsteadyAero, whose `.dvr` format has no output-channel + section) now preserve OutList channel selections across a `.dvr` round-trip — 6 via a + shared `outlist` registry threaded through the driver (AeroDyn, BeamDyn, HydroDyn, + InflowWind, SeaState, SubDyn) and 3 via a per-module `_outlist` fallback stored on the + module dict itself (AeroDisk, MoorDyn, SimpleElastoDyn). Previously 6 of these modules + lost OutList channel selections entirely on a standalone-driver round-trip. +- **SeaStateIO and SubDynIO**, used standalone (i.e., without a driver-supplied shared + registry), now preserve OutList via the `_outlist` fallback key instead of discarding it. +- **MoorDyn's writer** (`io/moordyn.py`) now filters to only truthy channels when emitting + OutList, since a registry built via the public `OutList.to_fst_output()` API contains + explicit `False` entries for every known channel. +- **`validate_fst_vt` gained a `base_dir=` parameter.** With `check_files=True`, file + references are now resolved against the case directory instead of the process's current + working directory, which previously produced false-positive "file does not exist" issues. +- **`pyyaml` added to `openfast_io`'s declared dependencies.** A clean install of the + package was broken without it (`formats.py` imports `yaml` unconditionally). +- **r-test / OpenFAST-binary integration tests now skip with a reason** (missing r-test + submodule, missing compiled binary, missing DISCON DLL) instead of failing, so a clean + checkout without those prerequisites reports a clean run (100 passed, 180 skipped) rather + than failures. +- **Facades emit `DeprecationWarning`** (previously `PendingDeprecationWarning`, which is + silenced by default in most test runners and would not have surfaced to downstream + callers). diff --git a/openfast_io/openfast_io/FAST_reader.py b/openfast_io/openfast_io/FAST_reader.py index 07f7d0d2ed..07111ec384 100644 --- a/openfast_io/openfast_io/FAST_reader.py +++ b/openfast_io/openfast_io/FAST_reader.py @@ -1,3810 +1,114 @@ -from numbers import Number -import os, re, copy -import numpy as np -from functools import reduce -import operator -from openfast_io.FAST_vars_out import FstOutput -from openfast_io.FAST_output_reader import load_ascii_output - -try: - from rosco.toolbox.utilities import read_DISCON, load_from_txt - from rosco.toolbox import turbine as ROSCO_turbine - ROSCO = True -except: - ROSCO = False - - -def readline_filterComments(f): - """ - Filter out comments and empty lines from a file - - Args: - f: file handle - - Returns: - line: next line in the file that is not a comment or empty - """ - read = True - while read: - line = f.readline().strip() - if len(line)>0: - if line[0] != '!': - read = False - return line - -def readline_ignoreComments(f, char = '#'): # see line 64 in NWTC_IO.f90 - """ - returns line before comment character - - Args: - f: file handle - char: comment character - - Returns: - line: content of next line in the file before comment character - """ - - line = f.readline().strip().split(char) - - return line[0] - -def read_array(f,len,split_val=None,array_type=str): - """ - Read an array of values from a line in a file - - Args: - f: file handle - len: number of values to read - split_val: value to stop reading at - array_type: type of values to return - - Returns: - arr: list of values read from the file line with the specified type - """ - - - strings = re.split(',| ',f.readline().strip()) - while '' in strings: # remove empties - strings.remove('') - - if len is None and split_val is None: - raise Exception('Must have len or split_val to use read_array') - - if len is not None: - arr = strings[:len] # select len strings - else: - arr = [] - for s in strings: - if s != split_val: - arr.append(s) - else: - break - - if array_type==str: - arr = [ar.replace('"','') for ar in arr] # remove quotes and commas - elif array_type==float: - arr = [float_read(ar) for ar in arr] - elif array_type==int: - arr = [int_read(ar) for ar in arr] - elif array_type==bool: - arr = [bool_read(ar) for ar in arr] - else: - raise Exception(f"read_array with type {str(array_type)} not currently supported") - - return arr - -def fix_path(name): - """ - split a path, then reconstruct it using os.path.join - - Args: - name: path to fix - - Returns: - new: reconstructed path - """ - name = re.split("\\|/", name) - new = name[0] - for i in range(1,len(name)): - new = os.path.join(new, name[i]) - return new - -def bool_read(text): - """ - Read a boolean value from a string - - Args: - text: string to read - - Returns: - True if the string is 'true', False otherwise - """ - if 'default' in text.lower(): - return str(text) - else: - text = text.lower() - if text == 'true' or text == 't': - return True - else: - return False - -def float_read(text): - """ - Read a float value from a string, with error handling for 'default' values - - Args: - text: string to read - - Returns: - float value if the string can be converted, string otherwise - """ - if 'default' in text.lower(): - return str(text) - else: - try: - return float(text) - except: - return str(text) - -def int_read(text): - """ - Read an integer value from a string, with error handling for 'default' values - - Args: - text: string to read - - Returns: - int value if the string can be converted, string otherwise - """ - if 'default' in text.lower(): - return str(text) - else: - try: - return int(text) - except: - return str(text) - -def quoted_read(text): - """ - Read a quoted value from a string (i.e. a value between quotes) - - Args: - text: string to read - - Returns: - quoted value if the string is quoted, unquoted value otherwise - - """ - if '"' in text: - return text.split('"')[1] - elif "'" in text: - return text.split("'")[1] - else: - return text - -class InputReader_OpenFAST(object): - """ OpenFAST input file reader """ - - def __init__(self): - - self.FAST_InputFile = None # FAST input file (ext=.fst) - self.FAST_directory = None # Path to fst directory files - self.path2dll = None # Path to dll file - self.fst_vt = {} - self.fst_vt['Fst'] = {} - self.fst_vt['outlist'] = copy.deepcopy(FstOutput) - self.fst_vt['ElastoDyn'] = {} - self.fst_vt['SimpleElastoDyn'] = {} - self.fst_vt['ElastoDynBlade'] = [{}, {}, {}] # One dict per blade, We will reduce this down to one, if all the files are the same - self.fst_vt['ElastoDynTower'] = {} - self.fst_vt['InflowWind'] = {} - self.fst_vt['AeroDyn'] = {} - self.fst_vt['AeroDisk'] = {} - self.fst_vt['AeroDynBlade'] = [{}, {}, {}] # One dict per blade, We will reduce this down to one, if all the files are the same - self.fst_vt['ServoDyn'] = {} - self.fst_vt['DISCON_in'] = {} - self.fst_vt['HydroDyn'] = {} - self.fst_vt['SeaState'] = {} - self.fst_vt['MoorDyn'] = {} - self.fst_vt['SubDyn'] = {} - self.fst_vt['ExtPtfm'] = {} - self.fst_vt['MAP'] = {} - self.fst_vt['BeamDyn'] = [{}, {}, {}] # One dict per blade, We will reduce this down to one, if all the files are the same - self.fst_vt['BeamDynBlade'] = [{}, {}, {}] # One dict per blade, We will reduce this down to one, if all the files are the same - self.fst_vt['WaterKin'] = {} - - def set_outlist(self, vartree_head, channel_list): - """ Loop through a list of output channel names, recursively set them to True in the nested outlist dict """ - - # given a list of nested dictionary keys, return the dict at that point - def get_dict(vartree, branch): - return reduce(operator.getitem, branch, vartree_head) - # given a list of nested dictionary keys, set the value of the dict at that point - def set_dict(vartree, branch, val): - get_dict(vartree, branch[:-1])[branch[-1]] = val - # recursively loop through outlist dictionaries to set output channels - def loop_dict(vartree, search_var, branch): - for var in vartree.keys(): - branch_i = copy.copy(branch) - branch_i.append(var) - if type(vartree[var]) is dict: - loop_dict(vartree[var], search_var, branch_i) - else: - if var == search_var: - set_dict(vartree_head, branch_i, True) - - # loop through outchannels on this line, loop through outlist dicts to set to True - for var in channel_list: - var = var.replace(' ', '') - loop_dict(vartree_head, var, []) - - def read_outlist_freeForm(self, f, module): - ''' - Replacement for set_outlist that doesn't care about whether the channel is in the outlist vartree - Easier, but riskier because OpenFAST can crash - - Inputs: f - file handle - module - of OpenFAST, e.g. SubDyn, SeaState (these modules use this) - ''' - all_channels = [] - data = f.readline() - - # Handle the case if there are blank lines before actual data - while data.strip() == '': - data = f.readline() - - while not data.strip().startswith('END'): - # Get part before the dash (comment) - line = data.split('-')[0] - - # Replace all delimiters with spaces - for delim in ['"', "'", ',', ';', '\t']: - line = line.replace(delim, ' ') - - # Split into words and add non-empty ones to the channel list - line_channels = [word.strip() for word in line.split() if word.strip()] - if line_channels: - all_channels.extend(line_channels) - - # Read next line - data = f.readline() - - # Handle the case if there are blank lines - while data.strip() == '': - data = f.readline() - - # Store all channels in the outlist - for channel in all_channels: - self.fst_vt['outlist'][module][channel] = True - - def read_outlist(self, f, module): - ''' - Read the outlist section of the FAST input file, generalized for most modules - - Inputs: f - file handle - module - of OpenFAST, e.g. ElastoDyn, ServoDyn, AeroDyn, AeroDisk, etc. - - Returns: List of channel names - ''' - all_channels = [] - data = f.readline() - - # Handle the case if there are blank lines before actual data - while data.strip() == '': - data = f.readline() - - while not data.strip().startswith('END'): - # Get part before the dash (comment) - line = data.split('-')[0] - - # Replace all delimiters with spaces - for delim in ['"', "'", ',', ';', '\t']: - line = line.replace(delim, ' ') - - # Split into words and add non-empty ones to the channel list - line_channels = [word.strip() for word in line.split() if word.strip()] - if line_channels: - all_channels.extend(line_channels) - - # Read next line - data = f.readline() - - # Handle the case if there are blank lines - while data.strip() == '': - data = f.readline() - - # Store all channels in the outlist - if all_channels: - self.set_outlist(self.fst_vt['outlist'][module], all_channels) - - return all_channels - - def read_MainInput(self): - # Main FAST v8.16-v8.17 Input File - # Currently no differences between FASTv8.16 and OpenFAST. - fastdir = '' if self.FAST_directory is None else self.FAST_directory - fst_file = os.path.join(fastdir, self.FAST_InputFile) - f = open(fst_file) - - # Header of .fst file - f.readline() - self.fst_vt['description'] = f.readline().rstrip() - - # Simulation Control (fst_sim_ctrl) - f.readline() - self.fst_vt['Fst']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['Fst']['AbortLevel'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['TMax'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['DT'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['ModCoupling'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['InterpOrder'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['NumCrctn'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['RhoInf'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['ConvTol'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['MaxConvIter'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['DT_UJac'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['UJacSclFact'] = float_read(f.readline().split()[0]) - - # Feature Switches and Flags (ftr_swtchs_flgs) - f.readline() - self.fst_vt['Fst']['NRotors'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompElast'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompInflow'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompAero'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompServo'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompSeaSt'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompHydro'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompSub'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompMooring'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompIce'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['CompSoil'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['MHK'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['MirrorRotor'] = read_array(f, self.fst_vt['Fst']['NRotors'], array_type=bool) - - if self.fst_vt['Fst']['NRotors'] > 1: - raise ValueError('openfast_io does not currently support multi-rotor turbines (NRotors > 1),' + - 'this feature will be added in a future release') - - # Environmental conditions - f.readline() - self.fst_vt['Fst']['Gravity'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['AirDens'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['WtrDens'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['KinVisc'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['SpdSound'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['Patm'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['Pvap'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['WtrDpth'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['MSL2SWL'] = float_read(f.readline().split()[0]) - - # Input Files (input_files) - f.readline() - self.fst_vt['Fst']['EDFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['BDBldFile(1)'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['BDBldFile(2)'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['BDBldFile(3)'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['InflowFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['AeroFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['ServoFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['SeaStFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['HydroFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['SubFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['MooringFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['IceFile'] = quoted_read(f.readline().split()[0]) - self.fst_vt['Fst']['SoilFile'] = quoted_read(f.readline().split()[0]) - # self.fst_vt['Fst']['EDFiles'] = [self.fst_vt['Fst']['EDFile']] - # self.fst_vt['Fst']['BDBldFiles(1)'] = [self.fst_vt['Fst']['BDBldFile(1)']] - # self.fst_vt['Fst']['BDBldFiles(2)'] = [self.fst_vt['Fst']['BDBldFile(2)']] - # self.fst_vt['Fst']['BDBldFiles(3)'] = [self.fst_vt['Fst']['BDBldFile(3)']] - # self.fst_vt['Fst']['ServoFiles'] = [self.fst_vt['Fst']['ServoFile']] - # for i in range(1, self.fst_vt['Fst']['NRotors']): - # f.readline() - # self.fst_vt['Fst']['EDFiles'].append(quoted_read(f.readline().split()[0])) - # self.fst_vt['Fst']['BDBldFiles(1)'].append(quoted_read(f.readline().split()[0])) - # self.fst_vt['Fst']['BDBldFiles(2)'].append(quoted_read(f.readline().split()[0])) - # self.fst_vt['Fst']['BDBldFiles(3)'].append(quoted_read(f.readline().split()[0])) - # self.fst_vt['Fst']['ServoFiles'].append(quoted_read(f.readline().split()[0])) - - # FAST Output Parameters (fst_output_params) - f.readline() - self.fst_vt['Fst']['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['Fst']['SttsTime'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['ChkptTime'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['DT_Out'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['TStart'] = float_read(f.readline().split()[0]) - self.fst_vt['Fst']['OutFileFmt'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['TabDelim'] = bool_read(f.readline().split()[0]) - self.fst_vt['Fst']['OutFmt'] = quoted_read(f.readline().split()[0]) - - # Fst - f.readline() - self.fst_vt['Fst']['Linearize'] = f.readline().split()[0] - self.fst_vt['Fst']['CalcSteady'] = f.readline().split()[0] - self.fst_vt['Fst']['TrimCase'] = f.readline().split()[0] - self.fst_vt['Fst']['TrimTol'] = f.readline().split()[0] - self.fst_vt['Fst']['TrimGain'] = f.readline().split()[0] - self.fst_vt['Fst']['Twr_Kdmp'] = f.readline().split()[0] - self.fst_vt['Fst']['Bld_Kdmp'] = f.readline().split()[0] - self.fst_vt['Fst']['NLinTimes'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['LinTimes'] = read_array(f, self.fst_vt['Fst']['NLinTimes'], array_type=float) - self.fst_vt['Fst']['LinInputs'] = f.readline().split()[0] - self.fst_vt['Fst']['LinOutputs'] = f.readline().split()[0] - self.fst_vt['Fst']['LinOutJac'] = f.readline().split()[0] - self.fst_vt['Fst']['LinOutMod'] = f.readline().split()[0] - - # Visualization () - f.readline() - self.fst_vt['Fst']['WrVTK'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['VTK_type'] = int(f.readline().split()[0]) - self.fst_vt['Fst']['VTK_fields'] = bool_read(f.readline().split()[0]) - self.fst_vt['Fst']['VTK_fps'] = float_read(f.readline().split()[0]) - - f.close() - - # File paths - self.fst_vt['Fst']['EDFile_path'] = os.path.split(self.fst_vt['Fst']['EDFile'])[0] - self.fst_vt['Fst']['BDBldFile(1_path)'] = os.path.split(self.fst_vt['Fst']['BDBldFile(1)'])[0] - self.fst_vt['Fst']['BDBldFile(2_path)'] = os.path.split(self.fst_vt['Fst']['BDBldFile(2)'])[0] - self.fst_vt['Fst']['BDBldFile(3_path)'] = os.path.split(self.fst_vt['Fst']['BDBldFile(3)'])[0] - self.fst_vt['Fst']['InflowFile_path'] = os.path.split(self.fst_vt['Fst']['InflowFile'])[0] - self.fst_vt['Fst']['AeroFile_path'] = os.path.split(self.fst_vt['Fst']['AeroFile'])[0] - self.fst_vt['Fst']['ServoFile_path'] = os.path.split(self.fst_vt['Fst']['ServoFile'])[0] - self.fst_vt['Fst']['HydroFile_path'] = os.path.split(self.fst_vt['Fst']['HydroFile'])[0] - self.fst_vt['Fst']['SubFile_path'] = os.path.split(self.fst_vt['Fst']['SubFile'])[0] - self.fst_vt['Fst']['MooringFile_path'] = os.path.split(self.fst_vt['Fst']['MooringFile'])[0] - self.fst_vt['Fst']['IceFile_path'] = os.path.split(self.fst_vt['Fst']['IceFile'])[0] - - def read_ElastoDyn(self, ed_file): - # ElastoDyn v1.03 Input File - # Currently no differences between FASTv8.16 and OpenFAST. - - f = open(ed_file) - - f.readline() - f.readline() - - # Simulation Control (ed_sim_ctrl) - f.readline() - self.fst_vt['ElastoDyn']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['Method'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['DT'] = float_read(f.readline().split()[0]) - - # Degrees of Freedom (dof) - f.readline() - self.fst_vt['ElastoDyn']['FlapDOF1'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['FlapDOF2'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['EdgeDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PitchDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['DrTrDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['GenDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['YawDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TwFADOF1'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TwFADOF2'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TwSSDOF1'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TwSSDOF2'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmSgDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmSwDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmHvDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmPDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmYDOF'] = bool_read(f.readline().split()[0]) - - # Initial Conditions (init_conds) - f.readline() - self.fst_vt['ElastoDyn']['OoPDefl'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['IPDefl'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPitch1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPitch2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPitch3'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetDefl'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['Azimuth'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['RotSpeed'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacYaw'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TTDspFA'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TTDspSS'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmSurge'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmSway'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmHeave'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRoll'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmPitch'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmYaw'] = float_read(f.readline().split()[0]) - - - # Turbine Configuration (turb_config) - f.readline() - self.fst_vt['ElastoDyn']['NumBl'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TipRad'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['HubRad'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PreCone(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PreCone(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PreCone(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['HubCM'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['UndSling'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['Delta3'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['AzimB1Up'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['OverHang'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['ShftGagL'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['ShftTilt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacCMxn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacCMyn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacCMzn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NcIMUxn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NcIMUyn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NcIMUzn'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['Twr2Shft'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TowerHt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TowerBsHt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmCMxt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmCMyt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmCMzt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRefxt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRefyt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRefzt'] = float_read(f.readline().split()[0]) - - # Mass and Inertia (mass_inertia) - f.readline() - self.fst_vt['ElastoDyn']['TipMass(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TipMass(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TipMass(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PBrIner(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PBrIner(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PBrIner(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPIner(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPIner(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BlPIner(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['HubMass'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['HubIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['HubIner_Teeter'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['GenIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacMass'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NacYIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['YawBrMass'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmMass'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmRIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmPIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmYIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmXYIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmYZIner'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['PtfmXZIner'] = float_read(f.readline().split()[0]) - - # ElastoDyn Blade (blade_struc) - f.readline() - self.fst_vt['ElastoDyn']['BldNodes'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BldFile1'] = quoted_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BldFile2'] = quoted_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BldFile3'] = quoted_read(f.readline().split()[0]) - - # Rotor-Teeter (rotor_teeter) - f.readline() - self.fst_vt['ElastoDyn']['TeetMod'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetDmpP'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetDmp'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetCDmp'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetSStP'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetHStP'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetSSSp'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TeetHSSp'] = float_read(f.readline().split()[0]) - - # Yaw friction - f.readline() - self.fst_vt['ElastoDyn']['YawFrctMod'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_CSmax'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_FCSmax'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_MCSmax'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_CD'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_FCD'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['M_MCD'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['sig_v'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['sig_v2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['OmgCut'] = float_read(f.readline().split()[0]) - - # Drivetrain (drivetrain) - f.readline() - self.fst_vt['ElastoDyn']['GBoxEff'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['GBRatio'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['DTTorSpr'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['DTTorDmp'] = float_read(f.readline().split()[0]) - - # Furling (furling) - f.readline() - self.fst_vt['ElastoDyn']['Furling'] = bool_read(f.readline().split()[0]) - fastdir = '' if self.FAST_directory is None else self.FAST_directory - self.fst_vt['ElastoDyn']['FurlFile'] = os.path.join(fastdir, quoted_read(f.readline().split()[0])) # TODO: add furl file data to fst_vt, pointing to absolute path for now - - # Tower (tower) - f.readline() - self.fst_vt['ElastoDyn']['TwrNodes'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TwrFile'] = quoted_read(f.readline().split()[0]) - - # ED Output Parameters (ed_out_params) - f.readline() - self.fst_vt['ElastoDyn']['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['OutFile'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TabDelim'] = bool_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['TStart'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['DecFact'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['NTwGages'] = int(f.readline().split()[0]) - if self.fst_vt['ElastoDyn']['NTwGages'] != 0: #loop over elements if there are gauges to be added, otherwise assign directly - self.fst_vt['ElastoDyn']['TwrGagNd'] = read_array(f,self.fst_vt['ElastoDyn']['NTwGages'], array_type=int) - else: - self.fst_vt['ElastoDyn']['TwrGagNd'] = 0 - f.readline() - self.fst_vt['ElastoDyn']['NBlGages'] = int(f.readline().split()[0]) - if self.fst_vt['ElastoDyn']['NBlGages'] != 0: - self.fst_vt['ElastoDyn']['BldGagNd'] = read_array(f,self.fst_vt['ElastoDyn']['NBlGages'], array_type=int) - else: - self.fst_vt['ElastoDyn']['BldGagNd'] = 0 - - - f.readline() - self.read_outlist(f,'ElastoDyn') - - # ElastoDyn optional outlist - try: - f.readline() - self.fst_vt['ElastoDyn']['BldNd_BladesOut'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDyn']['BldNd_BlOutNd'] = f.readline().split()[0] - - f.readline() - self.read_outlist(f,'ElastoDyn') - except: - # The optinal outlist does not exist. - None - - f.close() - - def read_SimpleElastoDyn(self, sed_file): - # Read the Simplified ElastoDyn input file - f = open(sed_file) - - f.readline() - f.readline() - f.readline() - self.fst_vt['SimpleElastoDyn']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['IntMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['DT'] = float_read(f.readline().split()[0]) - - # Degrees of Freedom - f.readline() - self.fst_vt['SimpleElastoDyn']['GenDOF'] = bool_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['YawDOF'] = bool_read(f.readline().split()[0]) - - # Initial Conditions - f.readline() - self.fst_vt['SimpleElastoDyn']['Azimuth'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['BlPitch'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['RotSpeed'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['NacYaw'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['PtfmPitch'] = float_read(f.readline().split()[0]) - - # Turbine Configuration - f.readline() - self.fst_vt['SimpleElastoDyn']['NumBl'] = int_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['TipRad'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['HubRad'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['PreCone'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['OverHang'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['ShftTilt'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['Twr2Shft'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['TowerHt'] = float_read(f.readline().split()[0]) - - # Mass and Inertia - f.readline() - self.fst_vt['SimpleElastoDyn']['RotIner'] = float_read(f.readline().split()[0]) - self.fst_vt['SimpleElastoDyn']['GenIner'] = float_read(f.readline().split()[0]) - - # Drivetrain - f.readline() - self.fst_vt['SimpleElastoDyn']['GBoxRatio'] = float_read(f.readline().split()[0]) - - # Output - f.readline() - f.readline() - - self.read_outlist(f,'SimpleElastoDyn') - - f.close() - - - - def read_ElastoDynBlade(self, blade_file, BladeNumber = 0): - # ElastoDyn v1.00 Blade Input File - # Currently no differences between FASTv8.16 and OpenFAST. - - f = open(blade_file) - # print blade_file - f.readline() - f.readline() - f.readline() - - # Blade Parameters - self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFlDmp1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFlDmp2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldEdDmp1'] = float_read(f.readline().split()[0]) - - # Blade Adjustment Factors - f.readline() - self.fst_vt['ElastoDynBlade'][BladeNumber]['FlStTunr1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['FlStTunr2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['AdjBlMs'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['AdjFlSt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['AdjEdSt'] = float_read(f.readline().split()[0]) - - # Distributed Blade Properties - f.readline() - f.readline() - f.readline() - self.fst_vt['ElastoDynBlade'][BladeNumber]['BlFract'] = [None] * self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] - self.fst_vt['ElastoDynBlade'][BladeNumber]['StrcTwst'] = [None] * self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] - self.fst_vt['ElastoDynBlade'][BladeNumber]['BMassDen'] = [None] * self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] - self.fst_vt['ElastoDynBlade'][BladeNumber]['FlpStff'] = [None] * self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] - self.fst_vt['ElastoDynBlade'][BladeNumber]['EdgStff'] = [None] * self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt'] - - for i in range(self.fst_vt['ElastoDynBlade'][BladeNumber]['NBlInpSt']): - data = f.readline().split() - self.fst_vt['ElastoDynBlade'][BladeNumber]['BlFract'][i] = float_read(data[0]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['StrcTwst'][i] = float_read(data[1]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['BMassDen'][i] = float_read(data[2]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['FlpStff'][i] = float_read(data[3]) - self.fst_vt['ElastoDynBlade'][BladeNumber]['EdgStff'][i] = float_read(data[4]) - - f.readline() - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFl1Sh'] = [None] * 5 - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFl2Sh'] = [None] * 5 - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldEdgSh'] = [None] * 5 - for i in range(5): - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFl1Sh'][i] = float_read(f.readline().split()[0]) - for i in range(5): - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldFl2Sh'][i] = float_read(f.readline().split()[0]) - for i in range(5): - self.fst_vt['ElastoDynBlade'][BladeNumber]['BldEdgSh'][i] = float_read(f.readline().split()[0]) - - f.close() - - def read_ElastoDynTower(self, tower_file): - # ElastoDyn v1.00 Tower Input Files - # Currently no differences between FASTv8.16 and OpenFAST. - - f = open(tower_file) - - f.readline() - f.readline() - - # General Tower Parameters - f.readline() - self.fst_vt['ElastoDynTower']['NTwInpSt'] = int(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['TwrFADmp1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['TwrFADmp2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['TwrSSDmp1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['TwrSSDmp2'] = float_read(f.readline().split()[0]) - - # Tower Adjustment Factors - f.readline() - self.fst_vt['ElastoDynTower']['FAStTunr1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['FAStTunr2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['SSStTunr1'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['SSStTunr2'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['AdjTwMa'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['AdjFASt'] = float_read(f.readline().split()[0]) - self.fst_vt['ElastoDynTower']['AdjSSSt'] = float_read(f.readline().split()[0]) - - # Distributed Tower Properties - f.readline() - f.readline() - f.readline() - self.fst_vt['ElastoDynTower']['HtFract'] = [None] * self.fst_vt['ElastoDynTower']['NTwInpSt'] - self.fst_vt['ElastoDynTower']['TMassDen'] = [None] * self.fst_vt['ElastoDynTower']['NTwInpSt'] - self.fst_vt['ElastoDynTower']['TwFAStif'] = [None] * self.fst_vt['ElastoDynTower']['NTwInpSt'] - self.fst_vt['ElastoDynTower']['TwSSStif'] = [None] * self.fst_vt['ElastoDynTower']['NTwInpSt'] - - for i in range(self.fst_vt['ElastoDynTower']['NTwInpSt']): - data = f.readline().split() - self.fst_vt['ElastoDynTower']['HtFract'][i] = float_read(data[0]) - self.fst_vt['ElastoDynTower']['TMassDen'][i] = float_read(data[1]) - self.fst_vt['ElastoDynTower']['TwFAStif'][i] = float_read(data[2]) - self.fst_vt['ElastoDynTower']['TwSSStif'][i] = float_read(data[3]) - - # Tower Mode Shapes - f.readline() - self.fst_vt['ElastoDynTower']['TwFAM1Sh'] = [None] * 5 - self.fst_vt['ElastoDynTower']['TwFAM2Sh'] = [None] * 5 - for i in range(5): - self.fst_vt['ElastoDynTower']['TwFAM1Sh'][i] = float_read(f.readline().split()[0]) - for i in range(5): - self.fst_vt['ElastoDynTower']['TwFAM2Sh'][i] = float_read(f.readline().split()[0]) - f.readline() - self.fst_vt['ElastoDynTower']['TwSSM1Sh'] = [None] * 5 - self.fst_vt['ElastoDynTower']['TwSSM2Sh'] = [None] * 5 - for i in range(5): - self.fst_vt['ElastoDynTower']['TwSSM1Sh'][i] = float_read(f.readline().split()[0]) - for i in range(5): - self.fst_vt['ElastoDynTower']['TwSSM2Sh'][i] = float_read(f.readline().split()[0]) - - f.close() - - def read_BeamDyn(self, bd_file, BladeNumber = 0): - - # If BladeNumber is 0 or not specified, assuming all the blades are the same - - # BeamDyn Input File - f = open(bd_file) - f.readline() - f.readline() - f.readline() - # ---------------------- SIMULATION CONTROL -------------------------------------- - self.fst_vt['BeamDyn'][BladeNumber]['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['QuasiStaticInit'] = bool_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['rhoinf'] = float_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['quadrature'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['refine'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['n_fact'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['DTBeam'] = float_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['load_retries'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['NRMax'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['stop_tol'] = float_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['tngt_stf_fd'] = bool_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['tngt_stf_comp'] = bool_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['tngt_stf_pert'] = float_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['tngt_stf_difftol'] = float_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['RotStates'] = bool_read(f.readline().split()[0]) - f.readline() - #---------------------- GEOMETRY PARAMETER -------------------------------------- - self.fst_vt['BeamDyn'][BladeNumber]['member_total'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['kp_total'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['members'] = [] - for i in range(self.fst_vt['BeamDyn'][BladeNumber]['member_total']): - ln = f.readline().split() - n_pts_i = int(ln[1]) - member_i = {} - member_i['kp_xr'] = [None]*n_pts_i - member_i['kp_yr'] = [None]*n_pts_i - member_i['kp_zr'] = [None]*n_pts_i - member_i['initial_twist'] = [None]*n_pts_i - f.readline() - f.readline() - for j in range(n_pts_i): - ln = f.readline().split() - member_i['kp_xr'][j] = float(ln[0]) - member_i['kp_yr'][j] = float(ln[1]) - member_i['kp_zr'][j] = float(ln[2]) - member_i['initial_twist'][j] = float(ln[3]) - - self.fst_vt['BeamDyn'][BladeNumber]['members'].append(member_i) - #---------------------- MESH PARAMETER ------------------------------------------ - f.readline() - self.fst_vt['BeamDyn'][BladeNumber]['order_elem'] = int_read(f.readline().split()[0]) - #---------------------- MATERIAL PARAMETER -------------------------------------- - f.readline() - self.fst_vt['BeamDyn'][BladeNumber]['BldFile'] = f.readline().split()[0].replace('"','').replace("'",'') - #---------------------- OUTPUTS ------------------------------------------------- - f.readline() - self.fst_vt['BeamDyn'][BladeNumber]['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['NNodeOuts'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['OutNd'] = [idx.strip() for idx in f.readline().split('OutNd')[0].split(',')] - # BeamDyn Outlist - f.readline() - - self.read_outlist(f,'BeamDyn') - - # BeamDyn optional outlist - try: - f.readline() - # self.fst_vt['BeamDyn']['BldNd_BladesOut'] = int(f.readline().split()[0]) - self.fst_vt['BeamDyn'][BladeNumber]['BldNd_BlOutNd'] = f.readline().split()[0] - - f.readline() - - self.read_outlist(f,'BeamDyn_Nodes') - except: - # The optinal outlist does not exist. - None - - f.close() - - beamdyn_blade_file = os.path.join(os.path.dirname(bd_file), self.fst_vt['BeamDyn'][BladeNumber]['BldFile']) - self.read_BeamDynBlade(beamdyn_blade_file, BladeNumber) - - def read_BeamDynBlade(self, beamdyn_blade_file, BladeNumber = 0): - # if BladeNumber is 0 or not specified, assuming all the blades are the same - # BeamDyn Blade - - f = open(beamdyn_blade_file) - - f.readline() - f.readline() - f.readline() - #---------------------- BLADE PARAMETERS -------------------------------------- - self.fst_vt['BeamDynBlade'][BladeNumber]['station_total'] = int_read(f.readline().split()[0]) - self.fst_vt['BeamDynBlade'][BladeNumber]['damp_type'] = int_read(f.readline().split()[0]) - f.readline() - f.readline() - f.readline() - #------ Stiffness-Proportional Damping [used only if damp_type=1] --------------- - ln = f.readline().split() - self.fst_vt['BeamDynBlade'][BladeNumber]['mu1'] = float(ln[0]) - self.fst_vt['BeamDynBlade'][BladeNumber]['mu2'] = float(ln[1]) - self.fst_vt['BeamDynBlade'][BladeNumber]['mu3'] = float(ln[2]) - self.fst_vt['BeamDynBlade'][BladeNumber]['mu4'] = float(ln[3]) - self.fst_vt['BeamDynBlade'][BladeNumber]['mu5'] = float(ln[4]) - self.fst_vt['BeamDynBlade'][BladeNumber]['mu6'] = float(ln[5]) - f.readline() - # ------ Modal Damping [used only if damp_type=2] -------------------------------- - n_modes = int(f.readline().split()[0]) - self.fst_vt['BeamDynBlade'][BladeNumber]['n_modes'] = n_modes - self.fst_vt['BeamDynBlade'][BladeNumber]['zeta'] = np.array(f.readline().strip().replace(',', ' ').split()[:n_modes], dtype=float).tolist() - f.readline() - #------ Distributed Properties -------------------------------------------------- - - self.fst_vt['BeamDynBlade'][BladeNumber]['radial_stations'] = np.zeros((self.fst_vt['BeamDynBlade'][BladeNumber]['station_total'])) - self.fst_vt['BeamDynBlade'][BladeNumber]['beam_stiff'] = np.zeros((self.fst_vt['BeamDynBlade'][BladeNumber]['station_total'], 6, 6)) - self.fst_vt['BeamDynBlade'][BladeNumber]['beam_inertia'] = np.zeros((self.fst_vt['BeamDynBlade'][BladeNumber]['station_total'], 6, 6)) - for i in range(self.fst_vt['BeamDynBlade'][BladeNumber]['station_total']): - self.fst_vt['BeamDynBlade'][BladeNumber]['radial_stations'][i] = float_read(f.readline().split()[0]) - for j in range(6): - self.fst_vt['BeamDynBlade'][BladeNumber]['beam_stiff'][i,j,:] = np.array([float(val) for val in f.readline().strip().split()]) - f.readline() - for j in range(6): - self.fst_vt['BeamDynBlade'][BladeNumber]['beam_inertia'][i,j,:] = np.array([float(val) for val in f.readline().strip().split()]) - f.readline() - - f.close() - - def read_InflowWind(self): - # InflowWind v3.01 - # Currently no differences between FASTv8.16 and OpenFAST. - fastdir = '' if self.FAST_directory is None else self.FAST_directory - inflow_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['InflowFile'])) - f = open(inflow_file) - - f.readline() - f.readline() - f.readline() - - # Inflow wind header parameters (inflow_wind) - self.fst_vt['InflowWind']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['WindType'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['PropagationDir'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['VFlowAng'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['VelInterpCubic'] = bool_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['NWindVel'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['WindVxiList'] = [idx.strip() for idx in f.readline().split('WindVxiList')[0].split(',')] - self.fst_vt['InflowWind']['WindVyiList'] = [idx.strip() for idx in f.readline().split('WindVyiList')[0].split(',')] - self.fst_vt['InflowWind']['WindVziList'] = [idx.strip() for idx in f.readline().split('WindVziList')[0].split(',')] - - # Parameters for Steady Wind Conditions [used only for WindType = 1] (steady_wind_params) - f.readline() - self.fst_vt['InflowWind']['HWindSpeed'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['RefHt'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['PLExp'] = float_read(f.readline().split()[0]) - - # Parameters for Uniform wind file [used only for WindType = 2] (uniform_wind_params) - f.readline() - self.fst_vt['InflowWind']['FileName_Uni'] = os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0])) - self.fst_vt['InflowWind']['RefHt_Uni'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['RefLength'] = float_read(f.readline().split()[0]) - - # Parameters for Binary TurbSim Full-Field files [used only for WindType = 3] (turbsim_wind_params) - f.readline() - self.fst_vt['InflowWind']['FileName_BTS'] = os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0])) - # Parameters for Binary Bladed-style Full-Field files [used only for WindType = 4] (bladed_wind_params) - f.readline() - self.fst_vt['InflowWind']['FileNameRoot'] = os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0])) - self.fst_vt['InflowWind']['TowerFile'] = bool_read(f.readline().split()[0]) - - # Parameters for HAWC-format binary files [Only used with WindType = 5] (hawc_wind_params) - f.readline() - self.fst_vt['InflowWind']['FileName_u'] = os.path.normpath(os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0]))) - self.fst_vt['InflowWind']['FileName_v'] = os.path.normpath(os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0]))) - self.fst_vt['InflowWind']['FileName_w'] = os.path.normpath(os.path.join(os.path.split(inflow_file)[0], quoted_read(f.readline().split()[0]))) - self.fst_vt['InflowWind']['nx'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['ny'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['nz'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['dx'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['dy'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['dz'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['RefHt_Hawc'] = float_read(f.readline().split()[0]) - - # Scaling parameters for turbulence (still hawc_wind_params) - f.readline() - self.fst_vt['InflowWind']['ScaleMethod'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['SFx'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['SFy'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['SFz'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['SigmaFx'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['SigmaFy'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['SigmaFz'] = float_read(f.readline().split()[0]) - - # Mean wind profile parameters (added to HAWC-format files) (still hawc_wind_params) - f.readline() - self.fst_vt['InflowWind']['URef'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['WindProfile'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['PLExp_Hawc'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['Z0'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['XOffset'] = float_read(f.readline().split()[0]) - - # LIDAR parameters - f.readline() - self.fst_vt['InflowWind']['SensorType'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['NumPulseGate'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['PulseSpacing'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['NumBeam'] = int(f.readline().split()[0]) - self.fst_vt['InflowWind']['FocalDistanceX'] = [idx.strip() for idx in f.readline().split('FocalDistanceX')[0].split(',')] - self.fst_vt['InflowWind']['FocalDistanceY'] = [idx.strip() for idx in f.readline().split('FocalDistanceY')[0].split(',')] - self.fst_vt['InflowWind']['FocalDistanceZ'] = [idx.strip() for idx in f.readline().split('FocalDistanceZ')[0].split(',')] - self.fst_vt['InflowWind']['RotorApexOffsetPos'] = [idx.strip() for idx in f.readline().split('RotorApexOffsetPos')[0].split(',')] - self.fst_vt['InflowWind']['URefLid'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['MeasurementInterval'] = float_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['LidRadialVel'] = bool_read(f.readline().split()[0]) - self.fst_vt['InflowWind']['ConsiderHubMotion'] = int(f.readline().split()[0]) - - # Inflow Wind Output Parameters (inflow_out_params) - f.readline() - self.fst_vt['InflowWind']['SumPrint'] = bool_read(f.readline().split()[0]) - - # InflowWind Outlist - f.readline() - self.read_outlist(f,'InflowWind') - - f.close() - - def read_AeroDyn(self): - # AeroDyn v15.03 - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - ad_file = os.path.join(fastdir, self.fst_vt['Fst']['AeroFile']) - f = open(ad_file) - - # General Option - f.readline() - f.readline() - f.readline() - self.fst_vt['AeroDyn']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['DTAero'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['Wake_Mod'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TwrPotent'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TwrShadow'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TwrAero'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['CavitCheck'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['NacelleDrag'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['CompAA'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['AA_InputFile'] = f.readline().split()[0] - - # Environmental Conditions - f.readline() - self.fst_vt['AeroDyn']['AirDens'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['KinVisc'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SpdSound'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['Patm'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['Pvap'] = float_read(f.readline().split()[0]) - #self.fst_vt['AeroDyn']['FluidDepth'] = float_read(f.readline().split()[0]) - - f.readline() - self.fst_vt['AeroDyn']['BEM_Mod'] = int(f.readline().split()[0]) - - # Blade-Element/Momentum Theory Options - f.readline() - self.fst_vt['AeroDyn']['Skew_Mod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SkewMomCorr'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SkewRedistr_Mod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SkewRedistrFactor'] = float_read(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['TipLoss'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['HubLoss'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TanInd'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['AIDrag'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TIDrag'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['IndToler'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['MaxIter'] = int(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['SectAvg'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SectAvgWeighting'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SectAvgNPoints'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SectAvgPsiBwd'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['SectAvgPsiFwd'] = float_read(f.readline().split()[0]) - - - # Dynamic Blade-Element/Momentum Theory Options - f.readline() - self.fst_vt['AeroDyn']['DBEMT_Mod'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['tau1_const'] = float_read(f.readline().split()[0]) - - # Olaf -- cOnvecting LAgrangian Filaments (Free Vortex Wake) Theory Options - f.readline() - self.fst_vt['AeroDyn']['OLAFInputFileName'] = quoted_read(f.readline().split()[0]) - - # Unsteady Airfoil Aerodynamics Options - f.readline() - self.fst_vt['AeroDyn']['AoA34'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['UA_Mod'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['FLookup'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['IntegrationMethod'] = int(f.readline().split()[0]) - - file_pos = f.tell() - line = f.readline() - if 'UAStartRad' in line: - self.fst_vt['AeroDyn']['UAStartRad'] = float_read(line.split()[0]) - else: - f.seek(file_pos) - - file_pos = f.tell() - line = f.readline() - if 'UAEndRad' in line: - self.fst_vt['AeroDyn']['UAEndRad'] = float_read(line.split()[0]) - else: - f.seek(file_pos) - - - # Airfoil Information - f.readline() - self.fst_vt['AeroDyn']['AFTabMod'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['InCol_Alfa'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['InCol_Cl'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['InCol_Cd'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['InCol_Cm'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['InCol_Cpmin'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['NumAFfiles'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['AFNames'] = [None] * self.fst_vt['AeroDyn']['NumAFfiles'] - fastdir = '' if self.FAST_directory is None else self.FAST_directory - for i in range(self.fst_vt['AeroDyn']['NumAFfiles']): - af_filename = fix_path(f.readline().split()[0])[1:-1] - self.fst_vt['AeroDyn']['AFNames'][i] = os.path.abspath(os.path.join(fastdir, self.fst_vt['Fst']['AeroFile_path'], af_filename)) - - # Rotor/Blade Properties - f.readline() - self.fst_vt['AeroDyn']['UseBlCm'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['ADBlFile1'] = quoted_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['ADBlFile2'] = quoted_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['ADBlFile3'] = quoted_read(f.readline().split()[0]) - - # Hub, nacelle, and tail fin aerodynamics - f.readline() - self.fst_vt['AeroDyn']['VolHub'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['HubCenBx'] = float_read(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['VolNac'] = float_read(f.readline().split()[0]) - # data = [float(val) for val in f.readline().split(',')] - self.fst_vt['AeroDyn']['NacCenB'] = [idx.strip() for idx in f.readline().split('NacCenB')[0].split(',')] - - self.fst_vt['AeroDyn']['NacArea'] = [idx.strip() for idx in f.readline().split('NacArea')[0].split(',')] - self.fst_vt['AeroDyn']['NacCd'] = [idx.strip() for idx in f.readline().split('NacCd')[0].split(',')] - self.fst_vt['AeroDyn']['NacDragAC'] = [idx.strip() for idx in f.readline().split('NacDragAC')[0].split(',')] - f.readline() - self.fst_vt['AeroDyn']['TFinAero'] = bool_read(f.readline().split()[0]) - tfa_filename = fix_path(f.readline().split()[0])[1:-1] - self.fst_vt['AeroDyn']['TFinFile'] = os.path.abspath(os.path.join(fastdir, tfa_filename)) - - # Tower Influence and Aerodynamics - f.readline() - self.fst_vt['AeroDyn']['NumTwrNds'] = int(f.readline().split()[0]) - f.readline() - f.readline() - self.fst_vt['AeroDyn']['TwrElev'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrDiam'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrCd'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrTI'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrCb'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrCp'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - self.fst_vt['AeroDyn']['TwrCa'] = [None]*self.fst_vt['AeroDyn']['NumTwrNds'] - for i in range(self.fst_vt['AeroDyn']['NumTwrNds']): - data = [float(val) for val in f.readline().split()] - self.fst_vt['AeroDyn']['TwrElev'][i] = data[0] - self.fst_vt['AeroDyn']['TwrDiam'][i] = data[1] - self.fst_vt['AeroDyn']['TwrCd'][i] = data[2] - self.fst_vt['AeroDyn']['TwrTI'][i] = data[3] - self.fst_vt['AeroDyn']['TwrCb'][i] = data[4] - self.fst_vt['AeroDyn']['TwrCp'][i] = data[5] - self.fst_vt['AeroDyn']['TwrCa'][i] = data[6] - - # Outputs - f.readline() - self.fst_vt['AeroDyn']['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['NBlOuts'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['BlOutNd'] = [idx.strip() for idx in f.readline().split('BlOutNd')[0].split(',')] - self.fst_vt['AeroDyn']['NTwOuts'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['TwOutNd'] = [idx.strip() for idx in f.readline().split('TwOutNd')[0].split(',')] - - # AeroDyn Outlist - f.readline() - - self.read_outlist(f,'AeroDyn') - - - # AeroDyn optional outlist - try: - f.readline() - self.fst_vt['AeroDyn']['BldNd_BladesOut'] = int(f.readline().split()[0]) - self.fst_vt['AeroDyn']['BldNd_BlOutNd'] = f.readline().split()[0] - - f.readline() - self.read_outlist(f,'AeroDyn_Nodes') - except: - # The optinal outlist does not exist. - None - - f.close() - - # Improved handling for multiple AeroDyn blade files - ad_bld_file1 = os.path.join(fastdir, self.fst_vt['Fst']['AeroFile_path'], self.fst_vt['AeroDyn']['ADBlFile1']) - ad_bld_file2 = os.path.join(fastdir, self.fst_vt['Fst']['AeroFile_path'], self.fst_vt['AeroDyn']['ADBlFile2']) - ad_bld_file3 = os.path.join(fastdir, self.fst_vt['Fst']['AeroFile_path'], self.fst_vt['AeroDyn']['ADBlFile3']) - - if ad_bld_file1 == ad_bld_file2 and ad_bld_file1 == ad_bld_file3: - # all blades are identical - self.read_AeroDynBlade(ad_bld_file1, BladeNumber=0) - # Copy data into the generic AeroDynBlade - self.fst_vt['AeroDynBlade'] = self.fst_vt['AeroDynBlade'][0] - elif self.fst_vt['ElastoDyn']['NumBl'] == 2 and ad_bld_file1 == ad_bld_file2: - # 2 blades are identical - self.read_AeroDynBlade(ad_bld_file1, BladeNumber=0) - self.fst_vt['AeroDynBlade'] = self.fst_vt['AeroDynBlade'][0] - else: - # all blades are different - self.read_AeroDynBlade(ad_bld_file1, BladeNumber=0) - if self.fst_vt['ElastoDyn']['NumBl'] > 1: - self.read_AeroDynBlade(ad_bld_file2, BladeNumber=1) - if self.fst_vt['ElastoDyn']['NumBl'] > 2: - self.read_AeroDynBlade(ad_bld_file3, BladeNumber=2) - else: - # we have a single blade - self.fst_vt['AeroDynBlade'] = self.fst_vt['AeroDynBlade'][0] - - self.read_AeroDynPolar() - self.read_AeroDynCoord() - olaf_filename = os.path.join(fastdir, self.fst_vt['AeroDyn']['OLAFInputFileName']) - if os.path.isfile(olaf_filename): - self.read_AeroDynOLAF(olaf_filename) - - def read_AeroDynBlade(self, ad_blade_file, BladeNumber = 0): - # AeroDyn v5.00 Blade Definition File - - # ad_blade_file = os.path.join(self.FAST_directory, self.fst_vt['Fst']['AeroFile_path'], self.fst_vt['AeroDyn']['ADBlFile1']) - f = open(ad_blade_file) - - f.readline() - f.readline() - f.readline() - # Blade Properties - self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] = int(f.readline().split()[0]) - f.readline() - f.readline() - self.fst_vt['AeroDynBlade'][BladeNumber]['BlSpn'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['t_c'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCrvAC'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlSwpAC'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCrvAng'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlTwist'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlChord'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlAFID'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCb'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBn'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBt'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpn'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpt'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCan'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCat'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCam'] = [None]*self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds'] - for i in range(self.fst_vt['AeroDynBlade'][BladeNumber]['NumBlNds']): - data = [float(val) for val in f.readline().split()] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlSpn'][i] = data[0] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCrvAC'][i] = data[1] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlSwpAC'][i] = data[2] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCrvAng'][i]= data[3] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlTwist'][i] = data[4] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlChord'][i] = data[5] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlAFID'][i] = data[6] - if len(data) == 16: - self.fst_vt['AeroDynBlade'][BladeNumber]['t_c'][i] = data[7] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCb'][i] = data[8] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBn'][i] = data[9] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBt'][i] = data[10] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpn'][i] = data[11] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpt'][i] = data[12] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCan'][i] = data[13] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCat'][i] = data[14] - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCam'][i] = data[15] - else: - self.fst_vt['AeroDynBlade'][BladeNumber]['t_c'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCb'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBn'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCenBt'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpn'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCpt'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCan'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCat'][i] = 0.0 - self.fst_vt['AeroDynBlade'][BladeNumber]['BlCam'][i] = 0.0 - - f.close() - - def read_AeroDynPolar(self): - # AirfoilInfo v1.01 - - - self.fst_vt['AeroDyn']['af_data'] = [None]*self.fst_vt['AeroDyn']['NumAFfiles'] - - for afi, af_filename in enumerate(self.fst_vt['AeroDyn']['AFNames']): - f = open(af_filename) - # print af_filename - - polar = {} - - polar['InterpOrd'] = int_read(readline_filterComments(f).split()[0]) - temp = readline_filterComments(f).split() - if temp[1] == "RelThickness": - polar['RelThickness'] = float_read(temp[0]) - polar['NonDimArea'] = float_read(readline_filterComments(f).split()[0]) - else: - polar['NonDimArea'] = float_read(temp[0]) - # polar['NonDimArea'] = float_read(readline_filterComments(f).split()[0]) - polar['NumCoords'] = readline_filterComments(f).split()[0] - polar['BL_file'] = readline_filterComments(f).split()[0] - polar['NumTabs'] = int_read(readline_filterComments(f).split()[0]) - self.fst_vt['AeroDyn']['af_data'][afi] = [None]*polar['NumTabs'] - - for tab in range(polar['NumTabs']): # For multiple tables - polar['Re'] = float_read(readline_filterComments(f).split()[0]) * 1.e+6 - polar['UserProp'] = int_read(readline_filterComments(f).split()[0]) - polar['InclUAdata'] = bool_read(readline_filterComments(f).split()[0]) - - # Unsteady Aero Data - if polar['InclUAdata']: - polar['alpha0'] = float_read(readline_filterComments(f).split()[0]) - polar['alpha1'] = float_read(readline_filterComments(f).split()[0]) - polar['alpha2'] = float_read(readline_filterComments(f).split()[0]) - # polar['alphaUpper'] = float_read(readline_filterComments(f).split()[0]) - # polar['alphaLower'] = float_read(readline_filterComments(f).split()[0]) - polar['eta_e'] = float_read(readline_filterComments(f).split()[0]) - polar['C_nalpha'] = float_read(readline_filterComments(f).split()[0]) - polar['T_f0'] = float_read(readline_filterComments(f).split()[0]) - polar['T_V0'] = float_read(readline_filterComments(f).split()[0]) - polar['T_p'] = float_read(readline_filterComments(f).split()[0]) - polar['T_VL'] = float_read(readline_filterComments(f).split()[0]) - polar['b1'] = float_read(readline_filterComments(f).split()[0]) - polar['b2'] = float_read(readline_filterComments(f).split()[0]) - polar['b5'] = float_read(readline_filterComments(f).split()[0]) - polar['A1'] = float_read(readline_filterComments(f).split()[0]) - polar['A2'] = float_read(readline_filterComments(f).split()[0]) - polar['A5'] = float_read(readline_filterComments(f).split()[0]) - polar['S1'] = float_read(readline_filterComments(f).split()[0]) - polar['S2'] = float_read(readline_filterComments(f).split()[0]) - polar['S3'] = float_read(readline_filterComments(f).split()[0]) - polar['S4'] = float_read(readline_filterComments(f).split()[0]) - polar['Cn1'] = float_read(readline_filterComments(f).split()[0]) - polar['Cn2'] = float_read(readline_filterComments(f).split()[0]) - polar['St_sh'] = float_read(readline_filterComments(f).split()[0]) - polar['Cd0'] = float_read(readline_filterComments(f).split()[0]) - polar['Cm0'] = float_read(readline_filterComments(f).split()[0]) - polar['k0'] = float_read(readline_filterComments(f).split()[0]) - polar['k1'] = float_read(readline_filterComments(f).split()[0]) - polar['k2'] = float_read(readline_filterComments(f).split()[0]) - polar['k3'] = float_read(readline_filterComments(f).split()[0]) - polar['k1_hat'] = float_read(readline_filterComments(f).split()[0]) - polar['x_cp_bar'] = float_read(readline_filterComments(f).split()[0]) - polar['UACutout'] = float_read(readline_filterComments(f).split()[0]) - polar['filtCutOff'] = float_read(readline_filterComments(f).split()[0]) - - # Polar Data - polar['NumAlf'] = int_read(readline_filterComments(f).split()[0]) - polar['Alpha'] = [None]*polar['NumAlf'] - polar['Cl'] = [None]*polar['NumAlf'] - polar['Cd'] = [None]*polar['NumAlf'] - polar['Cm'] = [None]*polar['NumAlf'] - polar['Cpmin'] = [None]*polar['NumAlf'] - for i in range(polar['NumAlf']): - data = [float(val) for val in readline_filterComments(f).split()] - if self.fst_vt['AeroDyn']['InCol_Alfa'] > 0: - polar['Alpha'][i] = data[self.fst_vt['AeroDyn']['InCol_Alfa']-1] - if self.fst_vt['AeroDyn']['InCol_Cl'] > 0: - polar['Cl'][i] = data[self.fst_vt['AeroDyn']['InCol_Cl']-1] - if self.fst_vt['AeroDyn']['InCol_Cd'] > 0: - polar['Cd'][i] = data[self.fst_vt['AeroDyn']['InCol_Cd']-1] - if self.fst_vt['AeroDyn']['InCol_Cm'] > 0: - polar['Cm'][i] = data[self.fst_vt['AeroDyn']['InCol_Cm']-1] - if self.fst_vt['AeroDyn']['InCol_Cpmin'] > 0: - polar['Cpmin'][i] = data[self.fst_vt['AeroDyn']['InCol_Cpmin']-1] - - self.fst_vt['AeroDyn']['af_data'][afi][tab] = copy.copy(polar) # For multiple tables - - f.close() - - def read_AeroDynCoord(self): - - self.fst_vt['AeroDyn']['af_coord'] = [] - self.fst_vt['AeroDyn']['ac'] = np.zeros(len(self.fst_vt['AeroDyn']['AFNames'])) - - for afi, af_filename in enumerate(self.fst_vt['AeroDyn']['AFNames']): - self.fst_vt['AeroDyn']['af_coord'].append({}) - if not (self.fst_vt['AeroDyn']['af_data'][afi][0]['NumCoords'] == 0 or self.fst_vt['AeroDyn']['af_data'][afi][0]['NumCoords'] == '0'): - coord_filename = af_filename[0:af_filename.rfind(os.sep)] + os.sep + self.fst_vt['AeroDyn']['af_data'][afi][0]['NumCoords'][2:-1] - - f = open(coord_filename) - lines = f.readlines() - f.close() - lines = [line for line in lines if not line.strip().startswith('!')] - n_coords = int(lines[0].split()[0]) - - x = np.zeros(n_coords-1) - y = np.zeros(n_coords-1) - - self.fst_vt['AeroDyn']['ac'][afi] = float(lines[1].split()[0]) - - for j in range(2, n_coords+1): - x[j - 2], y[j - 2] = map(float, lines[j].split()) - - self.fst_vt['AeroDyn']['af_coord'][afi]['x'] = x - self.fst_vt['AeroDyn']['af_coord'][afi]['y'] = y - - - def read_AeroDynOLAF(self, olaf_filename): - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - self.fst_vt['AeroDyn']['OLAF'] = {} - f = open(olaf_filename) - f.readline() - f.readline() - f.readline() - self.fst_vt['AeroDyn']['OLAF']['IntMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['DTfvw'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['FreeWakeStart'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['FullCircStart'] = float_read(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['OLAF']['CircSolvMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['CircSolvConvCrit'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['CircSolvRelaxation'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['CircSolvMaxIter'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['PrescribedCircFile'] = os.path.join(fastdir, quoted_read(f.readline().split()[0])) # unmodified by this script, hence pointing to absolute location - f.readline() - f.readline() - f.readline() - self.fst_vt['AeroDyn']['OLAF']['nNWPanels'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['nNWPanelsFree'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['nFWPanels'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['nFWPanelsFree'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['FWShedVorticity'] = bool_read(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['OLAF']['DiffusionMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['RegDeterMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['RegFunction'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['WakeRegMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['WakeRegFactor'] = float(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['WingRegFactor'] = float(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['CoreSpreadEddyVisc'] = int(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['OLAF']['TwrShadowOnWake'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['ShearModel'] = int_read(f.readline().split()[0]) - f.readline() - self.fst_vt['AeroDyn']['OLAF']['VelocityMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['TreeBranchFactor']= float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['PartPerSegment'] = int_read(f.readline().split()[0]) - f.readline() - f.readline() - self.fst_vt['AeroDyn']['OLAF']['WrVTk'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['nVTKBlades'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['VTKCoord'] = int_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['VTK_fps'] = float_read(f.readline().split()[0]) - self.fst_vt['AeroDyn']['OLAF']['nGridOut'] = int_read(f.readline().split()[0]) - f.readline() - f.close() - - def read_AeroDisk(self): - '''' - Reading the AeroDisk input file. - ''' - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - aDisk_file = os.path.join(fastdir, self.fst_vt['Fst']['AeroFile']) - f = open(aDisk_file) - f.readline() - f.readline() - f.readline() - - # Simulation Control - self.fst_vt['AeroDisk']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['AeroDisk']['DT'] = float_read(f.readline().split()[0]) - - # Environmental Conditions - f.readline() - self.fst_vt['AeroDisk']['AirDens'] = float_read(f.readline().split()[0]) - - # Actuator Disk Properties - f.readline() - self.fst_vt['AeroDisk']['RotorRad'] = float_read(f.readline().split()[0]) - - # read InColNames - Input column headers (string) {may include a combination of "TSR, RtSpd, VRel, Pitch, Skew"} (up to 4 columns) - # Read between the quotes - self.fst_vt['AeroDisk']['InColNames'] = [x.strip() for x in quoted_read(f.readline().split('InColNames')[0]).split(',')] - - # read InColDims - Number of unique values in each column (-) (must have same number of columns as InColName) [each >=2] - self.fst_vt['AeroDisk']['InColDims'] = [int(x) for x in f.readline().split('InColDims')[0].split(',')] - - # read the contents table of the CSV file referenced next - # if the next line starts with an @, then it is a file reference - line = f.readline() - if line[0] == '@': - self.fst_vt['AeroDisk']['actuatorDiskFile'] = os.path.join(fastdir, line[1:].strip()) - - # using the load_ascii_output function to read the CSV file, ;) - data, info = load_ascii_output(self.fst_vt['AeroDisk']['actuatorDiskFile'], headerLines=3, - descriptionLine=0, attributeLine=1, unitLine=2, delimiter = ',') - self.fst_vt['AeroDisk']['actuatorDiskTable'] = {'dsc': info['description'], - 'attr': info['attribute_names'], - 'units': info['attribute_units'], - 'data': data} - else: - raise Exception('Expecting a file reference to the actuator disk CSV file') - - # read the output list - f.readline() - f.readline() - - self.read_outlist(f,'AeroDisk') - - f.close() - - - def read_ServoDyn(self): - # ServoDyn v1.05 Input File - # Currently no differences between FASTv8.16 and OpenFAST. - - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - sd_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['ServoFile'])) - f = open(sd_file) - - f.readline() - f.readline() - - # Simulation Control (sd_sim_ctrl) - f.readline() - self.fst_vt['ServoDyn']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['DT'] = float_read(f.readline().split()[0]) - - # Pitch Control (pitch_ctrl) - f.readline() - self.fst_vt['ServoDyn']['PCMode'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TPCOn'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitNeut(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitNeut(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitNeut(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitSpr(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitSpr(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitSpr(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitDamp(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitDamp(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitDamp(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TPitManS(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TPitManS(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TPitManS(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitManRat(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitManRat(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PitManRat(3)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['BlPitchF(1)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['BlPitchF(2)'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['BlPitchF(3)'] = float_read(f.readline().split()[0]) - - # Geneartor and Torque Control (gen_torq_ctrl) - f.readline() - self.fst_vt['ServoDyn']['VSContrl'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenModel'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenEff'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenTiStr'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenTiStp'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['SpdGenOn'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TimGenOn'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TimGenOf'] = float_read(f.readline().split()[0]) - - # Simple Variable-Speed Torque Control (var_speed_torq_ctrl) - f.readline() - self.fst_vt['ServoDyn']['VS_RtGnSp'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['VS_RtTq'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['VS_Rgn2K'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['VS_SlPc'] = float_read(f.readline().split()[0]) - - # Simple Induction Generator (induct_gen) - f.readline() - self.fst_vt['ServoDyn']['SIG_SlPc'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['SIG_SySp'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['SIG_RtTq'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['SIG_PORt'] = float_read(f.readline().split()[0]) - - # Thevenin-Equivalent Induction Generator (theveq_induct_gen) - f.readline() - self.fst_vt['ServoDyn']['TEC_Freq'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_NPol'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_SRes'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_RRes'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_VLL'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_SLR'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_RLR'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TEC_MR'] = float_read(f.readline().split()[0]) - - # High-Speed Shaft Brake (shaft_brake) - f.readline() - self.fst_vt['ServoDyn']['HSSBrMode'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['THSSBrDp'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['HSSBrDT'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['HSSBrTqF'] = float_read(f.readline().split()[0]) - - # Nacelle-Yaw Control (nac_yaw_ctrl) - f.readline() - self.fst_vt['ServoDyn']['YCMode'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TYCOn'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['YawNeut'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['YawSpr'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['YawDamp'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TYawManS'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['YawManRat'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['NacYawF'] = float_read(f.readline().split()[0]) - - # Aero flow control - f.readline() - self.fst_vt['ServoDyn']['AfCmode'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['AfC_Mean'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['AfC_Amp'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['AfC_Phase'] = float_read(f.readline().split()[0]) - - # Structural Control - f.readline() - self.fst_vt['ServoDyn']['NumBStC'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['BStCfiles'] = read_array(f,self.fst_vt['ServoDyn']['NumBStC'], array_type=str) - self.fst_vt['ServoDyn']['NumNStC'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['NStCfiles'] = read_array(f,self.fst_vt['ServoDyn']['NumNStC'], array_type=str) - self.fst_vt['ServoDyn']['NumTStC'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TStCfiles'] = read_array(f,self.fst_vt['ServoDyn']['NumTStC'], array_type=str) - self.fst_vt['ServoDyn']['NumSStC'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['SStCfiles'] = read_array(f,self.fst_vt['ServoDyn']['NumSStC'], array_type=str) - - # Initialize Struct Control trees - self.fst_vt['BStC'] = [] * self.fst_vt['ServoDyn']['NumBStC'] - self.fst_vt['NStC'] = [] * self.fst_vt['ServoDyn']['NumNStC'] - self.fst_vt['TStC'] = [] * self.fst_vt['ServoDyn']['NumTStC'] - self.fst_vt['SStC'] = [] * self.fst_vt['ServoDyn']['NumSStC'] - - # Cable control - f.readline() - self.fst_vt['ServoDyn']['CCmode'] = int(f.readline().split()[0]) - - # Bladed Interface and Torque-Speed Look-Up Table (bladed_interface) - f.readline() - if self.path2dll == '' or self.path2dll == None: - self.fst_vt['ServoDyn']['DLL_FileName'] = os.path.abspath(os.path.normpath(os.path.join(os.path.split(sd_file)[0], quoted_read(f.readline().split()[0])))) - else: - f.readline() - self.fst_vt['ServoDyn']['DLL_FileName'] = self.path2dll - self.fst_vt['ServoDyn']['DLL_InFile'] = os.path.abspath(os.path.normpath(os.path.join(os.path.split(sd_file)[0], quoted_read(f.readline().split()[0])))) - self.fst_vt['ServoDyn']['DLL_ProcName'] = quoted_read(f.readline().split()[0]) - dll_dt_line = f.readline().split()[0] - try: - self.fst_vt['ServoDyn']['DLL_DT'] = float_read(dll_dt_line) - except: - self.fst_vt['ServoDyn']['DLL_DT'] = dll_dt_line[1:-1] - self.fst_vt['ServoDyn']['DLL_Ramp'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['BPCutoff'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['NacYaw_North'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['Ptch_Cntrl'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['Ptch_SetPnt'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['Ptch_Min'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['Ptch_Max'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PtchRate_Min'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['PtchRate_Max'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['Gain_OM'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenSpd_MinOM'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenSpd_MaxOM'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenSpd_Dem'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenTrq_Dem'] = float_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['GenPwr_Dem'] = float_read(f.readline().split()[0]) - - f.readline() - - self.fst_vt['ServoDyn']['DLL_NumTrq'] = int(f.readline().split()[0]) - f.readline() - f.readline() - self.fst_vt['ServoDyn']['GenSpd_TLU'] = [None] * self.fst_vt['ServoDyn']['DLL_NumTrq'] - self.fst_vt['ServoDyn']['GenTrq_TLU'] = [None] * self.fst_vt['ServoDyn']['DLL_NumTrq'] - for i in range(self.fst_vt['ServoDyn']['DLL_NumTrq']): - data = f.readline().split() - self.fst_vt['ServoDyn']['GenSpd_TLU'][i] = float_read(data[0]) - self.fst_vt['ServoDyn']['GenTrq_TLU'][i] = float_read(data[1]) - - # ServoDyn Output Params (sd_out_params) - f.readline() - self.fst_vt['ServoDyn']['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['OutFile'] = int(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TabDelim'] = bool_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['ServoDyn']['TStart'] = float_read(f.readline().split()[0]) - - # ServoDyn Outlist - f.readline() - self.read_outlist(f,'ServoDyn') - - f.close() - - def read_StC(self,filename): - ''' - return StC vt so it can be appended to fst_vt['XStC'] list - ''' - StC_vt = {} - - # Inputs should be relative to ServoDyn, like in OpenFAST - fastdir = '' if self.FAST_directory is None else self.FAST_directory - SvD_dir = os.path.dirname(self.fst_vt['Fst']['ServoFile']) - - with open(os.path.join(fastdir, SvD_dir, filename)) as f: - - f.readline() - f.readline() - f.readline() - StC_vt['Echo'] = bool_read(f.readline().split()[0]) # Echo - Echo input data to .ech (flag) - f.readline() # StC DEGREES OF FREEDOM - StC_vt['StC_DOF_MODE'] = int_read(f.readline().split()[0]) # 4 StC_DOF_MODE - DOF mode (switch) {0: No StC or TLCD DOF; 1: StC_X_DOF, StC_Y_DOF, and/or StC_Z_DOF (three independent StC DOFs); 2: StC_XY_DOF (Omni-Directional StC); 3: StC_XYZ_DOF (Omni-Directional StC); 5: TLCD; 6: Prescribed force/moment time series; 7: Force determined by external DLL} - StC_vt['StC_X_DOF'] = bool_read(f.readline().split()[0]) # false StC_X_DOF - DOF on or off for StC X (flag) [Used only when StC_DOF_MODE=1] - StC_vt['StC_Y_DOF'] = bool_read(f.readline().split()[0]) # false StC_Y_DOF - DOF on or off for StC Y (flag) [Used only when StC_DOF_MODE=1] - StC_vt['StC_Z_DOF'] = bool_read(f.readline().split()[0]) # false StC_Z_DOF - DOF on or off for StC Z (flag) [Used only when StC_DOF_MODE=1] - f.readline() # StC LOCATION - StC_vt['StC_P_X'] = float_read(f.readline().split()[0]) # -51.75 StC_P_X - At rest X position of StC (m) - StC_vt['StC_P_Y'] = float_read(f.readline().split()[0]) # 0 StC_P_Y - At rest Y position of StC (m) - StC_vt['StC_P_Z'] = float_read(f.readline().split()[0]) # -10 StC_P_Z - At rest Z position of StC (m) - f.readline() # StC INITIAL CONDITIONS - StC_vt['StC_X_DSP'] = float_read(f.readline().split()[0]) # 0 StC_X_DSP - StC X initial displacement (m) [relative to at rest position] - StC_vt['StC_Y_DSP'] = float_read(f.readline().split()[0]) # 0 StC_Y_DSP - StC Y initial displacement (m) [relative to at rest position] - StC_vt['StC_Z_DSP'] = float_read(f.readline().split()[0]) # 0 StC_Z_DSP - StC Z initial displacement (m) [relative to at rest position; used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_Z_PreLd'] = f.readline().split()[0] # "none" StC_Z_PreLd - StC Z prefloat_read(f.readline().split()[0]) #-load (N) {"gravity" to offset for gravity load; "none" or 0 to turn off} [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - f.readline() # StC CONFIGURATION - StC_vt['StC_X_PSP'] = float_read(f.readline().split()[0]) # 0 StC_X_PSP - Positive stop position (minimum X mass displacement) (m) - StC_vt['StC_X_NSP'] = float_read(f.readline().split()[0]) # 0 StC_X_NSP - Negative stop position (maximum X mass displacement) (m) - StC_vt['StC_Y_PSP'] = float_read(f.readline().split()[0]) # 0 StC_Y_PSP - Positive stop position (maximum Y mass displacement) (m) - StC_vt['StC_Y_NSP'] = float_read(f.readline().split()[0]) # 0 StC_Y_NSP - Negative stop position (minimum Y mass displacement) (m) - StC_vt['StC_Z_PSP'] = float_read(f.readline().split()[0]) # 0 StC_Z_PSP - Positive stop position (maximum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_Z_NSP'] = float_read(f.readline().split()[0]) # 0 StC_Z_NSP - Negative stop position (minimum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - f.readline() # StC MASS, STIFFNESS, & DAMPING - StC_vt['StC_X_M'] = float_read(f.readline().split()[0]) # 0 StC_X_M - StC X mass (kg) [must equal StC_Y_M for StC_DOF_MODE = 2] - StC_vt['StC_Y_M'] = float_read(f.readline().split()[0]) # 50 StC_Y_M - StC Y mass (kg) [must equal StC_X_M for StC_DOF_MODE = 2] - StC_vt['StC_Z_M'] = float_read(f.readline().split()[0]) # 0 StC_Z_M - StC Z mass (kg) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_Omni_M'] = float_read(f.readline().split()[0]) # 0 StC_Omni_M - StC omni mass (kg) [used only when StC_DOF_MODE=2 or 3] - StC_vt['StC_X_K'] = float_read(f.readline().split()[0]) # 2300 StC_X_K - StC X stiffness (N/m) - StC_vt['StC_Y_K'] = float_read(f.readline().split()[0]) # 2300 StC_Y_K - StC Y stiffness (N/m) - StC_vt['StC_Z_K'] = float_read(f.readline().split()[0]) # 0 StC_Z_K - StC Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_X_C'] = float_read(f.readline().split()[0]) # 35 StC_X_C - StC X damping (N/(m/s)) - StC_vt['StC_Y_C'] = float_read(f.readline().split()[0]) #float_read(f.readline().split()[0]) # 35 StC_Y_C - StC Y damping (N/(m/s)) - StC_vt['StC_Z_C'] = float_read(f.readline().split()[0]) # 0 StC_Z_C - StC Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_X_KS'] = float_read(f.readline().split()[0]) # 0 StC_X_KS - Stop spring X stiffness (N/m) - StC_vt['StC_Y_KS'] = float_read(f.readline().split()[0]) # 0 StC_Y_KS - Stop spring Y stiffness (N/m) - StC_vt['StC_Z_KS'] = float_read(f.readline().split()[0]) # 0 StC_Z_KS - Stop spring Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_X_CS'] = float_read(f.readline().split()[0]) # 0 StC_X_CS - Stop spring X damping (N/(m/s)) - StC_vt['StC_Y_CS'] = float_read(f.readline().split()[0]) # 0 StC_Y_CS - Stop spring Y damping (N/(m/s)) - StC_vt['StC_Z_CS'] = float_read(f.readline().split()[0]) # 0 StC_Z_CS - Stop spring Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - f.readline() # StC USER-DEFINED SPRING FORCES - StC_vt['Use_F_TBL'] = bool_read(f.readline().split()[0]) # False Use_F_TBL - Use spring force from user-defined table (flag) - StC_vt['NKInpSt'] = int_read(f.readline().split()[0]) # 17 NKInpSt - Number of spring force input stations - - StC_vt['SpringForceTable'] = {} - table = StC_vt['SpringForceTable'] - table['X'] = [None] * StC_vt['NKInpSt'] - table['F_X'] = [None] * StC_vt['NKInpSt'] - table['Y'] = [None] * StC_vt['NKInpSt'] - table['F_Y'] = [None] * StC_vt['NKInpSt'] - table['Z'] = [None] * StC_vt['NKInpSt'] - table['F_Z'] = [None] * StC_vt['NKInpSt'] - - f.readline() - # if StC_vt['Use_F_TBL']: - f.readline() - f.readline() - for i in range(StC_vt['NKInpSt']): - ln = f.readline().split() - table['X'][i] = float(ln[0]) - table['F_X'][i] = float(ln[1]) - table['Y'][i] = float(ln[2]) - table['F_Y'][i] = float(ln[3]) - table['Z'][i] = float(ln[4]) - table['F_Z'][i] = float(ln[5]) - # else: - # # Skip until next section - # data_line = f.readline().strip().split() - # while data_line[0][:3] != '---': - # data_line = f.readline().strip().split() - - # StructCtrl CONTROL, skip this readline() because it already happened - f.readline() - StC_vt['StC_CMODE'] = int_read(f.readline().split()[0]) # 5 StC_CMODE - Control mode (switch) {0:none; 1: Semi-Active Control Mode; 3: Active Control Mode through user subroutine; 4: Active Control Mode through Simulink (not available); 5: Active Control Mode through Bladed interface} - StC_vt['StC_CChan'] = int_read(f.readline().split()[0]) # 0 StC_CChan - Control channel group (1:10) for stiffness and damping (StC_[XYZ]_K, StC_[XYZ]_C, and StC_[XYZ]_Brake) (specify additional channels for blade instances of StC active control -- one channel per blade) [used only when StC_DOF_MODE=1 or 2, and StC_CMODE=4 or 5] - StC_vt['StC_SA_MODE'] = int_read(f.readline().split()[0]) # 1 StC_SA_MODE - Semi-Active control mode {1: velocity-based ground hook control; 2: Inverse velocity-based ground hook control; 3: displacement-based ground hook control 4: Phase difference Algorithm with Friction Force 5: Phase difference Algorithm with Damping Force} (-) - StC_vt['StC_X_C_LOW'] = float_read(f.readline().split()[0]) # 0 StC_X_C_LOW - StC X low damping for ground hook control - StC_vt['StC_X_C_HIGH'] = float_read(f.readline().split()[0]) # 0 StC_X_C_HIGH - StC X high damping for ground hook control - StC_vt['StC_Y_C_HIGH'] = float_read(f.readline().split()[0]) # 0 StC_Y_C_HIGH - StC Y high damping for ground hook control - StC_vt['StC_Y_C_LOW'] = float_read(f.readline().split()[0]) # 0 StC_Y_C_LOW - StC Y low damping for ground hook control - StC_vt['StC_Z_C_HIGH'] = float_read(f.readline().split()[0]) # 0 StC_Z_C_HIGH - StC Z high damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_Z_C_LOW'] = float_read(f.readline().split()[0]) # 0 StC_Z_C_LOW - StC Z low damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - StC_vt['StC_X_C_BRAKE'] = float_read(f.readline().split()[0]) # 0 StC_X_C_BRAKE - StC X high damping for braking the StC (Don't use it now. should be zero) - StC_vt['StC_Y_C_BRAKE'] = float_read(f.readline().split()[0]) # 0 StC_Y_C_BRAKE - StC Y high damping for braking the StC (Don't use it now. should be zero) - StC_vt['StC_Z_C_BRAKE'] = float_read(f.readline().split()[0]) # 0 StC_Z_C_BRAKE - StC Z high damping for braking the StC (Don't use it now. should be zero) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE] - f.readline() # TLCD - StC_vt['L_X'] = float_read(f.readline().split()[0]) # 7.9325 L_X - X TLCD total length (m) - StC_vt['B_X'] = float_read(f.readline().split()[0]) # 6.5929 B_X - X TLCD horizontal length (m) - StC_vt['area_X'] = float_read(f.readline().split()[0]) # 2.0217 area_X - X TLCD cross-sectional area of vertical column (m^2) - StC_vt['area_ratio_X'] = float_read(f.readline().split()[0]) # 0.913 area_ratio_X - X TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-) - StC_vt['headLossCoeff_X'] = float_read(f.readline().split()[0]) # 2.5265 headLossCoeff_X - X TLCD head loss coeff (-) - StC_vt['rho_X'] = float_read(f.readline().split()[0]) # 1000 rho_X - X TLCD liquid density (kg/m^3) - StC_vt['L_Y'] = float_read(f.readline().split()[0]) # 3.5767 L_Y - Y TLCD total length (m) - StC_vt['B_Y'] = float_read(f.readline().split()[0]) # 2.1788 B_Y - Y TLCD horizontal length (m) - StC_vt['area_Y'] = float_read(f.readline().split()[0]) # 1.2252 area_Y - Y TLCD cross-sectional area of vertical column (m^2) - StC_vt['area_ratio_Y'] = float_read(f.readline().split()[0]) # 2.7232 area_ratio_Y - Y TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-) - StC_vt['headLossCoeff_Y'] = float_read(f.readline().split()[0]) # 0.6433 headLossCoeff_Y - Y TLCD head loss coeff (-) - StC_vt['rho_Y'] = float_read(f.readline().split()[0]) # 1000 rho_Y - Y TLCD liquid density (kg/m^3) - f.readline() # PRESCRIBED TIME SERIES - StC_vt['PrescribedForcesCoord'] = int_read(f.readline().split()[0]) # 2 PrescribedForcesCoord- Prescribed forces are in global or local coordinates (switch) {1: global; 2: local} - # TODO: read in prescribed force time series, for now we just point to absolute path of input file - StC_vt['PrescribedForcesFile'] = os.path.join(fastdir, quoted_read(f.readline().split()[0])) # "Bld-TimeForceSeries.dat" PrescribedForcesFile - Time series force and moment (7 columns of time, FX, FY, FZ, MX, MY, MZ) - f.readline() - - return StC_vt - - def read_DISCON_in(self): - # Read the Bladed style Interface controller input file, intended for ROSCO https://github.com/NREL/ROSCO_toolbox - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - discon_in_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['ServoDyn']['DLL_InFile'])) - - if os.path.exists(discon_in_file): - - # Read DISCON infiles - self.fst_vt['DISCON_in'] = read_DISCON(discon_in_file) - - # Some additional filename parsing - discon_dir = os.path.dirname(discon_in_file) - self.fst_vt['DISCON_in']['PerfFileName'] = os.path.abspath(os.path.join(discon_dir, self.fst_vt['DISCON_in']['PerfFileName'])) - - # Try to read rotor performance data if it is available - try: - pitch_vector, tsr_vector, Cp_table, Ct_table, Cq_table = load_from_txt(self.fst_vt['DISCON_in']['PerfFileName']) - - RotorPerformance = ROSCO_turbine.RotorPerformance - Cp = RotorPerformance(Cp_table, pitch_vector, tsr_vector) - Ct = RotorPerformance(Ct_table, pitch_vector, tsr_vector) - Cq = RotorPerformance(Cq_table, pitch_vector, tsr_vector) - - self.fst_vt['DISCON_in']['Cp'] = Cp - self.fst_vt['DISCON_in']['Ct'] = Ct - self.fst_vt['DISCON_in']['Cq'] = Cq - self.fst_vt['DISCON_in']['Cp_pitch_initial_rad'] = pitch_vector - self.fst_vt['DISCON_in']['Cp_TSR_initial'] = tsr_vector - self.fst_vt['DISCON_in']['Cp_table'] = Cp_table - self.fst_vt['DISCON_in']['Ct_table'] = Ct_table - self.fst_vt['DISCON_in']['Cq_table'] = Cq_table - except: - print('WARNING: Cp table not loaded!') - - # Add some DISCON entries that might be needed within WISDEM - self.fst_vt['DISCON_in']['v_rated'] = 1. - - else: - del self.fst_vt['DISCON_in'] - - def read_spd_trq(self, file): - ''' - read the speed-torque curve data to the fst_vt - ''' - spd_trq = {} - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - f = open(os.path.normpath(os.path.join(fastdir, file))) - - spd_trq['header'] = f.readline() - - # handle arbritraty number of rows and two columns: RPM and Torque - data = f.readlines() - spd_trq['RPM'] = [float(line.split()[0]) for line in data] - spd_trq['Torque'] = [float(line.split()[1]) for line in data] - f.close() - - self.fst_vt['spd_trq'] = spd_trq - - - - - def read_HydroDyn(self, hd_file): - - f = open(hd_file) - - f.readline() - f.readline() - - self.fst_vt['HydroDyn']['Echo'] = bool_read(f.readline().split()[0]) - - # FLOATING PLATFORM - f.readline() - self.fst_vt['HydroDyn']['PotMod'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['ExctnMod'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['ExctnDisp'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['ExctnCutOff'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['PtfmYMod'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['PtfmRefY'] = float_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['PtfmYCutOff'] = float_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['NExctnHdg'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['RdtnMod'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['RdtnTMax'] = float_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['RdtnDT'] = float_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['NBody'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['NBodyMod'] = int_read(f.readline().split()[0]) - - # Get multiple potential files - pot_strings = read_array(f,self.fst_vt['HydroDyn']['NBody'],str) #re.split(',| ',f.readline().strip()) - pot_strings = [os.path.normpath(os.path.join(os.path.split(hd_file)[0],ps)) for ps in pot_strings] # make relative to hd_file - self.fst_vt['HydroDyn']['PotFile'] = pot_strings - self.fst_vt['HydroDyn']['WAMITULEN'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmRefxt'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmRefyt'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmRefzt'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmRefztRot'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmVol0'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmCOBxt'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['PtfmCOByt'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=float) - self.fst_vt['HydroDyn']['NAddDOF'] = read_array(f,self.fst_vt['HydroDyn']['NBody'], array_type=int) - - # 2ND-ORDER FLOATING PLATFORM FORCES - f.readline() - self.fst_vt['HydroDyn']['MnDrift'] = int_read(f.readline().split()[0]) # ? - self.fst_vt['HydroDyn']['NewmanApp'] = int_read(f.readline().split()[0]) # ? - self.fst_vt['HydroDyn']['DiffQTF'] = int_read(f.readline().split()[0]) # ? - self.fst_vt['HydroDyn']['SumQTF'] = int_read(f.readline().split()[0]) # ? - - # PLATFORM ADDITIONAL STIFFNESS AND DAMPING - f.readline() - # Get number of F0 terms [If NBodyMod=1, one size 6*NBody x 1 vector; if NBodyMod>1, NBody size 6 x 1 vectors] - NBody = self.fst_vt['HydroDyn']['NBody'] - if self.fst_vt['HydroDyn']['NBodyMod'] == 1: - self.fst_vt['HydroDyn']['AddF0'] = [float(f.readline().strip().split()[0]) for i in range(6*NBody)] - elif self.fst_vt['HydroDyn']['NBodyMod'] > 1: - self.fst_vt['HydroDyn']['AddF0'] = [[float(idx) for idx in f.readline().strip().split()[:NBody]] for i in range(6)] - else: - raise Exception("Invalid value for fst_vt['HydroDyn']['NBodyMod']") - - self.fst_vt['HydroDyn']['AddCLin'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6*NBody]] for i in range(6)]) - self.fst_vt['HydroDyn']['AddBLin'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6*NBody]] for i in range(6)]) - self.fst_vt['HydroDyn']['AddBQuad'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6*NBody]] for i in range(6)]) - - #STRIP THEORY OPTIONS - f.readline() - self.fst_vt['HydroDyn']['WaveDisp'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['AMMod'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['HstMod'] = int_read(f.readline().split()[0]) - - #AXIAL COEFFICIENTS - f.readline() - self.fst_vt['HydroDyn']['NAxCoef'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['AxCoefID'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxCd'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxCa'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxCp'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxFDMod'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxVnCOff'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - self.fst_vt['HydroDyn']['AxFDLoFSc'] = [None]*self.fst_vt['HydroDyn']['NAxCoef'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NAxCoef']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['AxCoefID'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['AxCd'][i] = float_read(ln[1]) - self.fst_vt['HydroDyn']['AxCa'][i] = float_read(ln[2]) - self.fst_vt['HydroDyn']['AxCp'][i] = float_read(ln[3]) - self.fst_vt['HydroDyn']['AxFDMod'][i] = float_read(ln[4]) - self.fst_vt['HydroDyn']['AxVnCOff'][i] = float_read(ln[5]) - self.fst_vt['HydroDyn']['AxFDLoFSc'][i] = float_read(ln[6]) - - #MEMBER JOINTS - f.readline() - self.fst_vt['HydroDyn']['NJoints'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['JointID'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - self.fst_vt['HydroDyn']['Jointxi'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - self.fst_vt['HydroDyn']['Jointyi'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - self.fst_vt['HydroDyn']['Jointzi'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - self.fst_vt['HydroDyn']['JointAxID'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - self.fst_vt['HydroDyn']['JointOvrlp'] = [None]*self.fst_vt['HydroDyn']['NJoints'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NJoints']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['JointID'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['Jointxi'][i] = float(ln[1]) - self.fst_vt['HydroDyn']['Jointyi'][i] = float(ln[2]) - self.fst_vt['HydroDyn']['Jointzi'][i] = float(ln[3]) - self.fst_vt['HydroDyn']['JointAxID'][i] = int(ln[4]) - self.fst_vt['HydroDyn']['JointOvrlp'][i] = int(ln[5]) - - #CIRCULAR MEMBER CROSS-SECTION PROPERTIES - f.readline() - self.fst_vt['HydroDyn']['NPropSetsCyl'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['CylPropSetID'] = [None]*self.fst_vt['HydroDyn']['NPropSetsCyl'] - self.fst_vt['HydroDyn']['CylPropD'] = [None]*self.fst_vt['HydroDyn']['NPropSetsCyl'] - self.fst_vt['HydroDyn']['CylPropThck'] = [None]*self.fst_vt['HydroDyn']['NPropSetsCyl'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NPropSetsCyl']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['CylPropSetID'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['CylPropD'][i] = float(ln[1]) - self.fst_vt['HydroDyn']['CylPropThck'][i] = float(ln[2]) - - #RECTANGULAR MEMBER CROSS-SECTION PROPERTIES - f.readline() - self.fst_vt['HydroDyn']['NPropSetsRec'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['RecPropSetID'] = [None]*self.fst_vt['HydroDyn']['NPropSetsRec'] - self.fst_vt['HydroDyn']['RecPropA'] = [None]*self.fst_vt['HydroDyn']['NPropSetsRec'] - self.fst_vt['HydroDyn']['RecPropB'] = [None]*self.fst_vt['HydroDyn']['NPropSetsRec'] - self.fst_vt['HydroDyn']['RecPropThck'] = [None]*self.fst_vt['HydroDyn']['NPropSetsRec'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NPropSetsRec']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['RecPropSetID'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['RecPropA'][i] = float(ln[1]) - self.fst_vt['HydroDyn']['RecPropB'][i] = float(ln[2]) - self.fst_vt['HydroDyn']['RecPropThck'][i] = float(ln[3]) - - #SIMPLE CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - f.readline() - f.readline() - ln = f.readline().split() - self.fst_vt['HydroDyn']['CylSimplCd'] = float_read(ln[0]) - self.fst_vt['HydroDyn']['CylSimplCdMG'] = float_read(ln[1]) - self.fst_vt['HydroDyn']['CylSimplCa'] = float_read(ln[2]) - self.fst_vt['HydroDyn']['CylSimplCaMG'] = float_read(ln[3]) - self.fst_vt['HydroDyn']['CylSimplCp'] = float_read(ln[4]) - self.fst_vt['HydroDyn']['CylSimplCpMG'] = float_read(ln[5]) - self.fst_vt['HydroDyn']['CylSimplAxCd'] = float_read(ln[6]) - self.fst_vt['HydroDyn']['CylSimplAxCdMG'] = float_read(ln[7]) - self.fst_vt['HydroDyn']['CylSimplAxCa'] = float_read(ln[8]) - self.fst_vt['HydroDyn']['CylSimplAxCaMG'] = float_read(ln[9]) - self.fst_vt['HydroDyn']['CylSimplAxCp'] = float_read(ln[10]) - self.fst_vt['HydroDyn']['CylSimplAxCpMG'] = float_read(ln[11]) - self.fst_vt['HydroDyn']['CylSimplCb'] = float_read(ln[12]) - self.fst_vt['HydroDyn']['CylSimplCbMG'] = float_read(ln[13]) - - #SIMPLE RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - f.readline() - f.readline() - ln = f.readline().split() - self.fst_vt['HydroDyn']['RecSimplCdA'] = float_read(ln[0]) - self.fst_vt['HydroDyn']['RecSimplCdAMG'] = float_read(ln[1]) - self.fst_vt['HydroDyn']['RecSimplCdB'] = float_read(ln[2]) - self.fst_vt['HydroDyn']['RecSimplCdBMG'] = float_read(ln[3]) - self.fst_vt['HydroDyn']['RecSimplCaA'] = float_read(ln[4]) - self.fst_vt['HydroDyn']['RecSimplCaAMG'] = float_read(ln[5]) - self.fst_vt['HydroDyn']['RecSimplCaB'] = float_read(ln[6]) - self.fst_vt['HydroDyn']['RecSimplCaBMG'] = float_read(ln[7]) - self.fst_vt['HydroDyn']['RecSimplCp'] = float_read(ln[8]) - self.fst_vt['HydroDyn']['RecSimplCpMG'] = float_read(ln[9]) - self.fst_vt['HydroDyn']['RecSimplAxCd'] = float_read(ln[10]) - self.fst_vt['HydroDyn']['RecSimplAxCdMG'] = float_read(ln[11]) - self.fst_vt['HydroDyn']['RecSimplAxCa'] = float_read(ln[12]) - self.fst_vt['HydroDyn']['RecSimplAxCaMG'] = float_read(ln[13]) - self.fst_vt['HydroDyn']['RecSimplAxCp'] = float_read(ln[14]) - self.fst_vt['HydroDyn']['RecSimplAxCpMG'] = float_read(ln[15]) - self.fst_vt['HydroDyn']['RecSimplCb'] = float_read(ln[16]) - self.fst_vt['HydroDyn']['RecSimplCbMG'] = float_read(ln[17]) - - #DEPTH-BASED CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - self.fst_vt['HydroDyn']['NCoefDpthCyl'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['CylDpth'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCd'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCdMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCa'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCaMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCp'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCpMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCd'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCdMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCa'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCaMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCp'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthAxCpMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCb'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - self.fst_vt['HydroDyn']['CylDpthCbMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthCyl'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NCoefDpthCyl']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['CylDpth'][i] = float_read(ln[0]) - self.fst_vt['HydroDyn']['CylDpthCd'][i] = float_read(ln[1]) - self.fst_vt['HydroDyn']['CylDpthCdMG'][i] = float_read(ln[2]) - self.fst_vt['HydroDyn']['CylDpthCa'][i] = float_read(ln[3]) - self.fst_vt['HydroDyn']['CylDpthCaMG'][i] = float_read(ln[4]) - self.fst_vt['HydroDyn']['CylDpthCp'][i] = float_read(ln[5]) - self.fst_vt['HydroDyn']['CylDpthCpMG'][i] = float_read(ln[6]) - self.fst_vt['HydroDyn']['CylDpthAxCd'][i] = float_read(ln[7]) - self.fst_vt['HydroDyn']['CylDpthAxCdMG'][i] = float_read(ln[8]) - self.fst_vt['HydroDyn']['CylDpthAxCa'][i] = float_read(ln[9]) - self.fst_vt['HydroDyn']['CylDpthAxCaMG'][i] = float_read(ln[10]) - self.fst_vt['HydroDyn']['CylDpthAxCp'][i] = float_read(ln[11]) - self.fst_vt['HydroDyn']['CylDpthAxCpMG'][i] = float_read(ln[12]) - self.fst_vt['HydroDyn']['CylDpthCb'][i] = float_read(ln[13]) - self.fst_vt['HydroDyn']['CylDpthCbMG'][i] = float_read(ln[14]) - - #DEPTH-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - self.fst_vt['HydroDyn']['NCoefDpthRec'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['RecDpth'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCdA'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCdAMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCdB'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCdBMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCaA'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCaAMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCaB'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCaBMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCp'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCpMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCd'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCdMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCa'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCaMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCp'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthAxCpMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCb'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - self.fst_vt['HydroDyn']['RecDpthCbMG'] = [None]*self.fst_vt['HydroDyn']['NCoefDpthRec'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NCoefDpthRec']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['RecDpth'][i] = float_read(ln[0]) - self.fst_vt['HydroDyn']['RecDpthCdA'][i] = float_read(ln[1]) - self.fst_vt['HydroDyn']['RecDpthCdAMG'][i] = float_read(ln[2]) - self.fst_vt['HydroDyn']['RecDpthCdB'][i] = float_read(ln[3]) - self.fst_vt['HydroDyn']['RecDpthCdBMG'][i] = float_read(ln[4]) - self.fst_vt['HydroDyn']['RecDpthCaA'][i] = float_read(ln[5]) - self.fst_vt['HydroDyn']['RecDpthCaAMG'][i] = float_read(ln[6]) - self.fst_vt['HydroDyn']['RecDpthCaB'][i] = float_read(ln[7]) - self.fst_vt['HydroDyn']['RecDpthCaBMG'][i] = float_read(ln[8]) - self.fst_vt['HydroDyn']['RecDpthCp'][i] = float_read(ln[9]) - self.fst_vt['HydroDyn']['RecDpthCpMG'][i] = float_read(ln[10]) - self.fst_vt['HydroDyn']['RecDpthAxCd'][i] = float_read(ln[11]) - self.fst_vt['HydroDyn']['RecDpthAxCdMG'][i] = float_read(ln[12]) - self.fst_vt['HydroDyn']['RecDpthAxCa'][i] = float_read(ln[13]) - self.fst_vt['HydroDyn']['RecDpthAxCaMG'][i] = float_read(ln[14]) - self.fst_vt['HydroDyn']['RecDpthAxCp'][i] = float_read(ln[15]) - self.fst_vt['HydroDyn']['RecDpthAxCpMG'][i] = float_read(ln[16]) - self.fst_vt['HydroDyn']['RecDpthCb'][i] = float_read(ln[17]) - self.fst_vt['HydroDyn']['RecDpthCbMG'][i] = float_read(ln[18]) - - #MEMBER-BASED CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - self.fst_vt['HydroDyn']['NCoefMembersCyl'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['MemberID_HydCCyl'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCd1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCd2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCdMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCdMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCa1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCa2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCaMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCaMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCp1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCp2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCpMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCpMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCd1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCd2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCdMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCdMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCa1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCa2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCaMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCaMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCp1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCp2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCpMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberAxCpMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCb1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCb2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCbMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - self.fst_vt['HydroDyn']['CylMemberCbMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersCyl'] - - f.readline() - f.readline() - for i in range(self.fst_vt['HydroDyn']['NCoefMembersCyl']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['MemberID_HydCCyl'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['CylMemberCd1'][i] = float_read(ln[1]) - self.fst_vt['HydroDyn']['CylMemberCd2'][i] = float_read(ln[2]) - self.fst_vt['HydroDyn']['CylMemberCdMG1'][i] = float_read(ln[3]) - self.fst_vt['HydroDyn']['CylMemberCdMG2'][i] = float_read(ln[4]) - self.fst_vt['HydroDyn']['CylMemberCa1'][i] = float_read(ln[5]) - self.fst_vt['HydroDyn']['CylMemberCa2'][i] = float_read(ln[6]) - self.fst_vt['HydroDyn']['CylMemberCaMG1'][i] = float_read(ln[7]) - self.fst_vt['HydroDyn']['CylMemberCaMG2'][i] = float_read(ln[8]) - self.fst_vt['HydroDyn']['CylMemberCp1'][i] = float_read(ln[9]) - self.fst_vt['HydroDyn']['CylMemberCp2'][i] = float_read(ln[10]) - self.fst_vt['HydroDyn']['CylMemberCpMG1'][i] = float_read(ln[11]) - self.fst_vt['HydroDyn']['CylMemberCpMG2'][i] = float_read(ln[12]) - self.fst_vt['HydroDyn']['CylMemberAxCd1'][i] = float_read(ln[13]) - self.fst_vt['HydroDyn']['CylMemberAxCd2'][i] = float_read(ln[14]) - self.fst_vt['HydroDyn']['CylMemberAxCdMG1'][i] = float_read(ln[15]) - self.fst_vt['HydroDyn']['CylMemberAxCdMG2'][i] = float_read(ln[16]) - self.fst_vt['HydroDyn']['CylMemberAxCa1'][i] = float_read(ln[17]) - self.fst_vt['HydroDyn']['CylMemberAxCa2'][i] = float_read(ln[18]) - self.fst_vt['HydroDyn']['CylMemberAxCaMG1'][i] = float_read(ln[19]) - self.fst_vt['HydroDyn']['CylMemberAxCaMG2'][i] = float_read(ln[20]) - self.fst_vt['HydroDyn']['CylMemberAxCp1'][i] = float_read(ln[21]) - self.fst_vt['HydroDyn']['CylMemberAxCp2'][i] = float_read(ln[22]) - self.fst_vt['HydroDyn']['CylMemberAxCpMG1'][i] = float_read(ln[23]) - self.fst_vt['HydroDyn']['CylMemberAxCpMG2'][i] = float_read(ln[24]) - self.fst_vt['HydroDyn']['CylMemberCb1'][i] = float_read(ln[25]) - self.fst_vt['HydroDyn']['CylMemberCb2'][i] = float_read(ln[26]) - self.fst_vt['HydroDyn']['CylMemberCbMG1'][i] = float_read(ln[27]) - self.fst_vt['HydroDyn']['CylMemberCbMG2'][i] = float_read(ln[28]) - - #MEMBER-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS - f.readline() - self.fst_vt['HydroDyn']['NCoefMembersRec'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['MemberID_HydCRec'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdA1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdA2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdAMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdAMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdB1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdB2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdBMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCdBMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaA1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaA2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaAMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaAMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaB1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaB2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaBMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCaBMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCp1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCp2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCpMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCpMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCd1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCd2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCdMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCdMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCa1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCa2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCaMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCaMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCp1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCp2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCpMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberAxCpMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCb1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCb2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCbMG1'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - self.fst_vt['HydroDyn']['RecMemberCbMG2'] = [None]*self.fst_vt['HydroDyn']['NCoefMembersRec'] - - f.readline() - f.readline() - for i in range(self.fst_vt['HydroDyn']['NCoefMembersRec']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['MemberID_HydCRec'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['RecMemberCdA1'][i] = float_read(ln[1]) - self.fst_vt['HydroDyn']['RecMemberCdA2'][i] = float_read(ln[2]) - self.fst_vt['HydroDyn']['RecMemberCdAMG1'][i] = float_read(ln[3]) - self.fst_vt['HydroDyn']['RecMemberCdAMG2'][i] = float_read(ln[4]) - self.fst_vt['HydroDyn']['RecMemberCdB1'][i] = float_read(ln[5]) - self.fst_vt['HydroDyn']['RecMemberCdB2'][i] = float_read(ln[6]) - self.fst_vt['HydroDyn']['RecMemberCdBMG1'][i] = float_read(ln[7]) - self.fst_vt['HydroDyn']['RecMemberCdBMG2'][i] = float_read(ln[8]) - self.fst_vt['HydroDyn']['RecMemberCaA1'][i] = float_read(ln[9]) - self.fst_vt['HydroDyn']['RecMemberCaA2'][i] = float_read(ln[10]) - self.fst_vt['HydroDyn']['RecMemberCaAMG1'][i] = float_read(ln[11]) - self.fst_vt['HydroDyn']['RecMemberCaAMG2'][i] = float_read(ln[12]) - self.fst_vt['HydroDyn']['RecMemberCaB1'][i] = float_read(ln[13]) - self.fst_vt['HydroDyn']['RecMemberCaB2'][i] = float_read(ln[14]) - self.fst_vt['HydroDyn']['RecMemberCaBMG1'][i] = float_read(ln[15]) - self.fst_vt['HydroDyn']['RecMemberCaBMG2'][i] = float_read(ln[16]) - self.fst_vt['HydroDyn']['RecMemberCp1'][i] = float_read(ln[17]) - self.fst_vt['HydroDyn']['RecMemberCp2'][i] = float_read(ln[18]) - self.fst_vt['HydroDyn']['RecMemberCpMG1'][i] = float_read(ln[19]) - self.fst_vt['HydroDyn']['RecMemberCpMG2'][i] = float_read(ln[20]) - self.fst_vt['HydroDyn']['RecMemberAxCd1'][i] = float_read(ln[21]) - self.fst_vt['HydroDyn']['RecMemberAxCd2'][i] = float_read(ln[22]) - self.fst_vt['HydroDyn']['RecMemberAxCdMG1'][i] = float_read(ln[23]) - self.fst_vt['HydroDyn']['RecMemberAxCdMG2'][i] = float_read(ln[24]) - self.fst_vt['HydroDyn']['RecMemberAxCa1'][i] = float_read(ln[25]) - self.fst_vt['HydroDyn']['RecMemberAxCa2'][i] = float_read(ln[26]) - self.fst_vt['HydroDyn']['RecMemberAxCaMG1'][i] = float_read(ln[27]) - self.fst_vt['HydroDyn']['RecMemberAxCaMG2'][i] = float_read(ln[28]) - self.fst_vt['HydroDyn']['RecMemberAxCp1'][i] = float_read(ln[29]) - self.fst_vt['HydroDyn']['RecMemberAxCp2'][i] = float_read(ln[30]) - self.fst_vt['HydroDyn']['RecMemberAxCpMG1'][i] = float_read(ln[31]) - self.fst_vt['HydroDyn']['RecMemberAxCpMG2'][i] = float_read(ln[32]) - self.fst_vt['HydroDyn']['RecMemberCb1'][i] = float_read(ln[33]) - self.fst_vt['HydroDyn']['RecMemberCb2'][i] = float_read(ln[34]) - self.fst_vt['HydroDyn']['RecMemberCbMG1'][i] = float_read(ln[35]) - self.fst_vt['HydroDyn']['RecMemberCbMG2'][i] = float_read(ln[36]) - - #MEMBERS - f.readline() - self.fst_vt['HydroDyn']['NMembers'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['MemberID'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MJointID1'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MJointID2'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MPropSetID1'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MPropSetID2'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MSecGeom'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MSpinOrient'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MDivSize'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MCoefMod'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['MHstLMod'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - self.fst_vt['HydroDyn']['PropPot'] = [None]*self.fst_vt['HydroDyn']['NMembers'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NMembers']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['MemberID'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['MJointID1'][i] = int(ln[1]) - self.fst_vt['HydroDyn']['MJointID2'][i] = int(ln[2]) - self.fst_vt['HydroDyn']['MPropSetID1'][i] = int(ln[3]) - self.fst_vt['HydroDyn']['MPropSetID2'][i] = int(ln[4]) - self.fst_vt['HydroDyn']['MSecGeom'][i] = int(ln[5]) - self.fst_vt['HydroDyn']['MSpinOrient'][i] = float(ln[6]) - self.fst_vt['HydroDyn']['MDivSize'][i] = float(ln[7]) - self.fst_vt['HydroDyn']['MCoefMod'][i] = int(ln[8]) - self.fst_vt['HydroDyn']['MHstLMod'][i] = int(ln[9]) - self.fst_vt['HydroDyn']['PropPot'][i] = bool_read(ln[10]) - - #FILLED MEMBERS - f.readline() - self.fst_vt['HydroDyn']['NFillGroups'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['FillNumM'] = [None]*self.fst_vt['HydroDyn']['NFillGroups'] - self.fst_vt['HydroDyn']['FillMList'] = [None]*self.fst_vt['HydroDyn']['NFillGroups'] - self.fst_vt['HydroDyn']['FillFSLoc'] = [None]*self.fst_vt['HydroDyn']['NFillGroups'] - self.fst_vt['HydroDyn']['FillDens'] = [None]*self.fst_vt['HydroDyn']['NFillGroups'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NFillGroups']): - ln = f.readline().split() - n_fill = int(ln[0]) - self.fst_vt['HydroDyn']['FillNumM'][i] = n_fill - self.fst_vt['HydroDyn']['FillMList'][i] = [int(j) for j in ln[1:1+n_fill]] - self.fst_vt['HydroDyn']['FillFSLoc'][i] = float(ln[n_fill+1]) - if ln[n_fill+2] == 'DEFAULT': - self.fst_vt['HydroDyn']['FillDens'][i] = 'DEFAULT' - else: - self.fst_vt['HydroDyn']['FillDens'][i] = float(ln[n_fill+2]) - - #MARINE GROWTH - f.readline() - self.fst_vt['HydroDyn']['NMGDepths'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['MGDpth'] = [None]*self.fst_vt['HydroDyn']['NMGDepths'] - self.fst_vt['HydroDyn']['MGThck'] = [None]*self.fst_vt['HydroDyn']['NMGDepths'] - self.fst_vt['HydroDyn']['MGDens'] = [None]*self.fst_vt['HydroDyn']['NMGDepths'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NMGDepths']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['MGDpth'][i] = float(ln[0]) - self.fst_vt['HydroDyn']['MGThck'][i] = float(ln[1]) - self.fst_vt['HydroDyn']['MGDens'][i] = float(ln[2]) - - #MEMBER OUTPUT LIST - f.readline() - self.fst_vt['HydroDyn']['NMOutputs'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['MemberID_out'] = [None]*self.fst_vt['HydroDyn']['NMOutputs'] - self.fst_vt['HydroDyn']['NOutLoc'] = [None]*self.fst_vt['HydroDyn']['NMOutputs'] - self.fst_vt['HydroDyn']['NodeLocs'] = [None]*self.fst_vt['HydroDyn']['NMOutputs'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['HydroDyn']['NMOutputs']): - ln = f.readline().split() - self.fst_vt['HydroDyn']['MemberID_out'][i] = int(ln[0]) - self.fst_vt['HydroDyn']['NOutLoc'][i] = int(ln[1]) - self.fst_vt['HydroDyn']['NodeLocs'][i] = float(ln[2]) - - #JOINT OUTPUT LIST - f.readline() - self.fst_vt['HydroDyn']['NJOutputs'] = int_read(f.readline().split()[0]) - if int(self.fst_vt['HydroDyn']['NJOutputs']) > 0: - self.fst_vt['HydroDyn']['JOutLst'] = [int(idx.strip()) for idx in f.readline().split('JOutLst')[0].split(',')] - else: - f.readline() - self.fst_vt['HydroDyn']['JOutLst'] = [0] - - #OUTPUT - f.readline() - self.fst_vt['HydroDyn']['HDSum'] = bool_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['OutAll'] = bool_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['OutSwtch'] = int_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['HydroDyn']['OutSFmt'] = quoted_read(f.readline().split()[0]) - - # HydroDyn Outlist - f.readline() - self.read_outlist(f, 'HydroDyn') - - f.close() - - def read_SeaState(self, ss_file): - - f = open(ss_file) - - f.readline() - f.readline() - - self.fst_vt['SeaState']['Echo'] = bool_read(f.readline().split()[0]) - # ENVIRONMENTAL CONDITIONS - f.readline() - self.fst_vt['SeaState']['WtrDens'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WtrDpth'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['MSL2SWL'] = float_read(f.readline().split()[0]) - - # SPATIAL DISCRETIZATION - f.readline() - self.fst_vt['SeaState']['X_HalfWidth'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['Y_HalfWidth'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['Z_Depth'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['NX'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['NY'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['NZ'] = int_read(f.readline().split()[0]) - - # WAVES - f.readline() - self.fst_vt['SeaState']['WaveMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveStMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvCrntMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveTMax'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveDT'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveHs'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveTp'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WavePkShp'] = float_read(f.readline().split()[0]) # default - self.fst_vt['SeaState']['WvLowCOff'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvHiCOff'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveDir'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveDirMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveDirSpread'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveNDir'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveDirRange'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveSeed1'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveSeed2'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveNDAmp'] = bool_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvKinFile'] = quoted_read(f.readline().split()[0]) - - # 2ND-ORDER WAVES - f.readline() - self.fst_vt['SeaState']['WvDiffQTF'] = bool_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvSumQTF'] = bool_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvLowCOffD'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvHiCOffD'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvLowCOffS'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WvHiCOffS'] = float_read(f.readline().split()[0]) - - # CONSTRAINED WAVE - f.readline() - self.fst_vt['SeaState']['ConstWaveMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CrestHmax'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CrestTime'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CrestXi'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CrestYi'] = float_read(f.readline().split()[0]) - - # CURRENT - f.readline() - self.fst_vt['SeaState']['CurrMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrSSV0'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrSSDir'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrNSRef'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrNSV0'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrNSDir'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrDIV'] = float_read(f.readline().split()[0]) - self.fst_vt['SeaState']['CurrDIDir'] = float_read(f.readline().split()[0]) - - # MacCamy-Fuchs Diffraction Model - f.readline() - self.fst_vt['SeaState']['MCFD'] = float_read(f.readline().split()[0]) - - # OUTPUT - f.readline() - self.fst_vt['SeaState']['SeaStSum'] = bool_read(f.readline().split()[0]) - self.fst_vt['SeaState']['OutSwtch'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['SeaState']['OutSFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['SeaState']['NWaveElev'] = int_read(f.readline().split()[0]) - self.fst_vt['SeaState']['WaveElevxi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveElevxi')[0].replace(',',' ').split()] - self.fst_vt['SeaState']['WaveElevyi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveElevyi')[0].replace(',',' ').split()] - self.fst_vt['SeaState']['NWaveKin'] = int_read(f.readline().split()[0]) - NWaveKin = self.fst_vt['SeaState']['NWaveKin'] - if NWaveKin: - self.fst_vt['SeaState']['WaveKinxi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinxi')[0].replace(',',' ').split()] - self.fst_vt['SeaState']['WaveKinyi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinyi')[0].replace(',',' ').split()] - self.fst_vt['SeaState']['WaveKinzi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinzi')[0].replace(',',' ').split()] - else: - [f.readline() for i in range(3)] - # Unused, filled with dummy location - self.fst_vt['SeaState']['WaveKinxi'] = [0] - self.fst_vt['SeaState']['WaveKinyi'] = [0] - self.fst_vt['SeaState']['WaveKinzi'] = [0] - - - # SeaState Outlist - f.readline() - self.read_outlist_freeForm(f,'SeaState') - - f.close() - - - def read_SubDyn(self, sd_file): - - f = open(sd_file) - f.readline() - f.readline() - f.readline() - # SIMULATION CONTROL - self.fst_vt['SubDyn']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['SDdeltaT'] = float_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['IntMethod'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['SttcSolve'] = bool_read(f.readline().split()[0]) - # self.fst_vt['SubDyn']['GuyanLoadCorrection'] = bool_read(f.readline().split()[0]) - f.readline() - # FEA and CRAIG-BAMPTON PARAMETERS - self.fst_vt['SubDyn']['FEMMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['NDiv'] = int_read(f.readline().split()[0]) - # self.fst_vt['SubDyn']['CBMod'] = bool_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['Nmodes'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['JDampings'] = float_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['GuyanDampMod'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['RayleighDamp'] = read_array(f,2,array_type=float) - self.fst_vt['SubDyn']['GuyanDampSize'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['GuyanDamp'] = np.array([[float(idx) for idx in f.readline().strip().split()[:self.fst_vt['SubDyn']['GuyanDampSize']]] for i in range(self.fst_vt['SubDyn']['GuyanDampSize'])]) - - f.readline() - f.readline() - f.readline() - # INITIAL RIGID-BODY POSITION - ln = f.readline().split() - self.fst_vt['SubDyn']['RBSurge'] = float(ln[0]) - self.fst_vt['SubDyn']['RBSway'] = float(ln[1]) - self.fst_vt['SubDyn']['RBHeave'] = float(ln[2]) - self.fst_vt['SubDyn']['RBRoll'] = float(ln[3]) - self.fst_vt['SubDyn']['RBPitch'] = float(ln[4]) - self.fst_vt['SubDyn']['RBYaw'] = float(ln[5]) - - f.readline() - # STRUCTURE JOINTS - self.fst_vt['SubDyn']['NJoints'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['JointID'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointXss'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointYss'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointZss'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointType'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointDirX'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointDirY'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointDirZ'] = [None]*self.fst_vt['SubDyn']['NJoints'] - self.fst_vt['SubDyn']['JointStiff'] = [None]*self.fst_vt['SubDyn']['NJoints'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NJoints']): - ln = f.readline().split() - self.fst_vt['SubDyn']['JointID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['JointXss'][i] = float(ln[1]) - self.fst_vt['SubDyn']['JointYss'][i] = float(ln[2]) - self.fst_vt['SubDyn']['JointZss'][i] = float(ln[3]) - self.fst_vt['SubDyn']['JointType'][i] = int(ln[4]) - self.fst_vt['SubDyn']['JointDirX'][i] = float(ln[5]) - self.fst_vt['SubDyn']['JointDirY'][i] = float(ln[6]) - self.fst_vt['SubDyn']['JointDirZ'][i] = float(ln[7]) - self.fst_vt['SubDyn']['JointStiff'][i] = float(ln[8]) - f.readline() - # BASE REACTION JOINTS - self.fst_vt['SubDyn']['NReact'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['RJointID'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctTDXss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctTDYss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctTDZss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctRDXss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctRDYss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['RctRDZss'] = [None]*self.fst_vt['SubDyn']['NReact'] - self.fst_vt['SubDyn']['Rct_SoilFile'] = [None]*self.fst_vt['SubDyn']['NReact'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NReact']): - ln = f.readline().split() - self.fst_vt['SubDyn']['RJointID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['RctTDXss'][i] = int(ln[1]) - self.fst_vt['SubDyn']['RctTDYss'][i] = int(ln[2]) - self.fst_vt['SubDyn']['RctTDZss'][i] = int(ln[3]) - self.fst_vt['SubDyn']['RctRDXss'][i] = int(ln[4]) - self.fst_vt['SubDyn']['RctRDYss'][i] = int(ln[5]) - self.fst_vt['SubDyn']['RctRDZss'][i] = int(ln[6]) - if len(ln) == 8: - self.fst_vt['SubDyn']['Rct_SoilFile'][i] = ln[7] - else: - self.fst_vt['SubDyn']['Rct_SoilFile'][i] = 'None' - f.readline() - # INTERFACE JOINTS - self.fst_vt['SubDyn']['NInterf'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['IJointID'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['TPID'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfTDXss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfTDYss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfTDZss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfRDXss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfRDYss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - self.fst_vt['SubDyn']['ItfRDZss'] = [None]*self.fst_vt['SubDyn']['NInterf'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NInterf']): - ln = f.readline().split() - self.fst_vt['SubDyn']['IJointID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['TPID'][i] = int(ln[1]) - self.fst_vt['SubDyn']['ItfTDXss'][i] = int(ln[2]) - self.fst_vt['SubDyn']['ItfTDYss'][i] = int(ln[3]) - self.fst_vt['SubDyn']['ItfTDZss'][i] = int(ln[4]) - self.fst_vt['SubDyn']['ItfRDXss'][i] = int(ln[5]) - self.fst_vt['SubDyn']['ItfRDYss'][i] = int(ln[6]) - self.fst_vt['SubDyn']['ItfRDZss'][i] = int(ln[7]) - f.readline() - # MEMBERS - self.fst_vt['SubDyn']['NMembers'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['MemberID'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['MJointID1'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['MJointID2'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['MPropSetID1'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['MPropSetID2'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['MType'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['M_Spin'] = [None]*self.fst_vt['SubDyn']['NMembers'] - self.fst_vt['SubDyn']['M_COSMID'] = [None]*self.fst_vt['SubDyn']['NMembers'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NMembers']): - ln = f.readline().split() - self.fst_vt['SubDyn']['MemberID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['MJointID1'][i] = int(ln[1]) - self.fst_vt['SubDyn']['MJointID2'][i] = int(ln[2]) - self.fst_vt['SubDyn']['MPropSetID1'][i] = int(ln[3]) - self.fst_vt['SubDyn']['MPropSetID2'][i] = int(ln[4]) - if ln[5].lower() == '1c': - self.fst_vt['SubDyn']['MType'][i] = 1 - elif ln[5].lower() == '1r': - self.fst_vt['SubDyn']['MType'][i] = -1 - else: - self.fst_vt['SubDyn']['MType'][i] = int(ln[5]) - if self.fst_vt['SubDyn']['MType'][i] == 5: - self.fst_vt['SubDyn']['M_Spin'][i] = 0. - self.fst_vt['SubDyn']['M_COSMID'][i] = int(ln[6]) - else: - self.fst_vt['SubDyn']['M_Spin'][i] = float(ln[6]) - self.fst_vt['SubDyn']['M_COSMID'][i] = -1 - f.readline() - # MEMBER X-SECTION PROPERTY data 1/3 - self.fst_vt['SubDyn']['NPropSetsCyl'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['PropSetID1'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - self.fst_vt['SubDyn']['YoungE1'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - self.fst_vt['SubDyn']['ShearG1'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - self.fst_vt['SubDyn']['MatDens1'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - self.fst_vt['SubDyn']['XsecD'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - self.fst_vt['SubDyn']['XsecT'] = [None]*self.fst_vt['SubDyn']['NPropSetsCyl'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NPropSetsCyl']): - ln = f.readline().split() - self.fst_vt['SubDyn']['PropSetID1'][i] = int(ln[0]) - self.fst_vt['SubDyn']['YoungE1'][i] = float(ln[1]) - self.fst_vt['SubDyn']['ShearG1'][i] = float(ln[2]) - self.fst_vt['SubDyn']['MatDens1'][i] = float(ln[3]) - self.fst_vt['SubDyn']['XsecD'][i] = float(ln[4]) - self.fst_vt['SubDyn']['XsecT'][i] = float(ln[5]) - f.readline() - # MEMBER X-SECTION PROPERTY data 2/3 - self.fst_vt['SubDyn']['NPropSetsRec'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['PropSetID2'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['YoungE2'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['ShearG2'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['MatDens2'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['XsecSa'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['XsecSb'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - self.fst_vt['SubDyn']['XsecT2'] = [None]*self.fst_vt['SubDyn']['NPropSetsRec'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NPropSetsRec']): - ln = f.readline().split() - self.fst_vt['SubDyn']['PropSetID2'][i] = int(ln[0]) - self.fst_vt['SubDyn']['YoungE2'][i] = float(ln[1]) - self.fst_vt['SubDyn']['ShearG2'][i] = float(ln[2]) - self.fst_vt['SubDyn']['MatDens2'][i] = float(ln[3]) - self.fst_vt['SubDyn']['XsecSa'][i] = float(ln[4]) - self.fst_vt['SubDyn']['XsecSb'][i] = float(ln[5]) - self.fst_vt['SubDyn']['XsecT2'][i] = float(ln[6]) - f.readline() - # MEMBER X-SECTION PROPERTY data 3/3 - self.fst_vt['SubDyn']['NXPropSets'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['PropSetID3'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['YoungE3'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['ShearG3'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['MatDens3'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecA'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecAsx'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecAsy'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecJxx'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecJyy'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecJ0'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - self.fst_vt['SubDyn']['XsecJt'] = [None]*self.fst_vt['SubDyn']['NXPropSets'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NXPropSets']): - ln = f.readline().split() - self.fst_vt['SubDyn']['PropSetID3'][i] = int(ln[0]) - self.fst_vt['SubDyn']['YoungE3'][i] = float(ln[1]) - self.fst_vt['SubDyn']['ShearG3'][i] = float(ln[2]) - self.fst_vt['SubDyn']['MatDens3'][i] = float(ln[3]) - self.fst_vt['SubDyn']['XsecA'][i] = float(ln[4]) - self.fst_vt['SubDyn']['XsecAsx'][i] = float(ln[5]) - self.fst_vt['SubDyn']['XsecAsy'][i] = float(ln[6]) - self.fst_vt['SubDyn']['XsecJxx'][i] = float(ln[7]) - self.fst_vt['SubDyn']['XsecJyy'][i] = float(ln[8]) - self.fst_vt['SubDyn']['XsecJ0'][i] = float(ln[9]) - self.fst_vt['SubDyn']['XsecJt'][i] = float(ln[10]) - # CABLE PROPERTIES - f.readline() - self.fst_vt['SubDyn']['NCablePropSets'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['CablePropSetID'] = [None]*self.fst_vt['SubDyn']['NCablePropSets'] - self.fst_vt['SubDyn']['CableEA'] = [None]*self.fst_vt['SubDyn']['NCablePropSets'] - self.fst_vt['SubDyn']['CableMatDens'] = [None]*self.fst_vt['SubDyn']['NCablePropSets'] - self.fst_vt['SubDyn']['CableT0'] = [None]*self.fst_vt['SubDyn']['NCablePropSets'] - f.readline() - f.readline() - for i in range(self.fst_vt['SubDyn']['NCablePropSets']): - ln = f.readline().split() - self.fst_vt['SubDyn']['CablePropSetID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['CableEA'][i] = float(ln[1]) - self.fst_vt['SubDyn']['CableMatDens'][i] = float(ln[2]) - self.fst_vt['SubDyn']['CableT0'][i] = float(ln[3]) - # RIGID LINK PROPERTIES - f.readline() - self.fst_vt['SubDyn']['NRigidPropSets'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['RigidPropSetID'] = [None]*self.fst_vt['SubDyn']['NRigidPropSets'] - self.fst_vt['SubDyn']['RigidMatDens'] = [None]*self.fst_vt['SubDyn']['NRigidPropSets'] - f.readline() - f.readline() - for i in range(self.fst_vt['SubDyn']['NRigidPropSets']): - ln = f.readline().split() - self.fst_vt['SubDyn']['RigidPropSetID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['RigidMatDens'][i] = float(ln[1]) - # SPRING ELEMENT PROPERTIES - f.readline() - self.fst_vt['SubDyn']['NSpringPropSets'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['SpringPropSetID'] = [None]*self.fst_vt['SubDyn']['NSpringPropSets'] - spring_list = ['k11','k12','k13','k14','k15','k16', - 'k22','k23','k24','k25','k26', - 'k33','k34','k35','k36', - 'k44','k45','k46', - 'k55','k56', - 'k66'] - for sl in spring_list: # init list - self.fst_vt['SubDyn'][sl] = [None]*self.fst_vt['SubDyn']['NSpringPropSets'] - f.readline() - f.readline() - for i in range(self.fst_vt['SubDyn']['NSpringPropSets']): - ln = f.readline().split() - self.fst_vt['SubDyn']['SpringPropSetID'][i] = int(ln[0]) - for j, sl in enumerate(spring_list): - self.fst_vt['SubDyn'][sl][i] = ln[j+1] - - # MEMBER COSINE MATRICES - f.readline() - self.fst_vt['SubDyn']['NCOSMs'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['COSMID'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM11'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM12'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM13'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM21'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM22'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM23'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM31'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM32'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - self.fst_vt['SubDyn']['COSM33'] = [None]*self.fst_vt['SubDyn']['NCOSMs'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NCOSMs']): - ln = f.readline().split() - self.fst_vt['SubDyn']['COSMID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['COSM11'][i] = float(ln[1]) - self.fst_vt['SubDyn']['COSM12'][i] = float(ln[2]) - self.fst_vt['SubDyn']['COSM13'][i] = float(ln[3]) - self.fst_vt['SubDyn']['COSM21'][i] = float(ln[4]) - self.fst_vt['SubDyn']['COSM22'][i] = float(ln[5]) - self.fst_vt['SubDyn']['COSM23'][i] = float(ln[6]) - self.fst_vt['SubDyn']['COSM31'][i] = float(ln[7]) - self.fst_vt['SubDyn']['COSM32'][i] = float(ln[8]) - self.fst_vt['SubDyn']['COSM33'][i] = float(ln[9]) - f.readline() - # JOINT ADDITIONAL CONCENTRATED MASSES - self.fst_vt['SubDyn']['NCmass'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['CMJointID'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMass'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMXX'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMYY'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMZZ'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMXY'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMXZ'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['JMYZ'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['MCGX'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['MCGY'] = [None]*self.fst_vt['SubDyn']['NCmass'] - self.fst_vt['SubDyn']['MCGZ'] = [None]*self.fst_vt['SubDyn']['NCmass'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NCmass']): - ln = f.readline().split() - self.fst_vt['SubDyn']['CMJointID'][i] = int(ln[0]) - self.fst_vt['SubDyn']['JMass'][i] = float(ln[1]) - self.fst_vt['SubDyn']['JMXX'][i] = float(ln[2]) - self.fst_vt['SubDyn']['JMYY'][i] = float(ln[3]) - self.fst_vt['SubDyn']['JMZZ'][i] = float(ln[4]) - self.fst_vt['SubDyn']['JMXY'][i] = float(ln[5]) - self.fst_vt['SubDyn']['JMXZ'][i] = float(ln[6]) - self.fst_vt['SubDyn']['JMYZ'][i] = float(ln[7]) - self.fst_vt['SubDyn']['MCGX'][i] = float(ln[8]) - self.fst_vt['SubDyn']['MCGY'][i] = float(ln[9]) - self.fst_vt['SubDyn']['MCGZ'][i] = float(ln[10]) - f.readline() - # OUTPUT - self.fst_vt['SubDyn']['SumPrint'] = bool_read(f.readline().split()[0]) - file_pos = f.tell() - line = f.readline() - if 'OutCBModes' in line: - self.fst_vt['SubDyn']['OutCBModes'] = int_read(line.split()[0]) - else: - f.seek(file_pos) - file_pos = f.tell() - line = f.readline() - if 'OutFEMModes' in line: - self.fst_vt['SubDyn']['OutFEMModes'] = int_read(line.split()[0]) - else: - f.seek(file_pos) - self.fst_vt['SubDyn']['OutCOSM'] = bool_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['OutAll'] = bool_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['OutSwtch'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['TabDelim'] = bool_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['OutDec'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['OutSFmt'] = quoted_read(f.readline().split()[0]) - f.readline() - # MEMBER OUTPUT LIST - self.fst_vt['SubDyn']['NMOutputs'] = int_read(f.readline().split()[0]) - self.fst_vt['SubDyn']['MemberID_out'] = [None]*self.fst_vt['SubDyn']['NMOutputs'] - self.fst_vt['SubDyn']['NOutCnt'] = [None]*self.fst_vt['SubDyn']['NMOutputs'] - self.fst_vt['SubDyn']['NodeCnt'] = [[None]]*self.fst_vt['SubDyn']['NMOutputs'] - ln = f.readline().split() - ln = f.readline().split() - for i in range(self.fst_vt['SubDyn']['NMOutputs']): - ln = f.readline().split('!')[0].split() # allows for comments - self.fst_vt['SubDyn']['MemberID_out'][i] = int(ln[0]) - self.fst_vt['SubDyn']['NOutCnt'][i] = int(ln[1]) - self.fst_vt['SubDyn']['NodeCnt'][i] = [int(node) for node in ln[2:]] - f.readline() - # SSOutList - self.read_outlist_freeForm(f,'SubDyn') - - f.close() - - f.close() - - - def read_ExtPtfm(self, ep_file): - # ExtPtfm file based on documentation here: https://openfast.readthedocs.io/en/main/source/user/extptfm/input_files.html - - f = open(ep_file) - f.readline() - f.readline() - f.readline() - - # Simulation Control - self.fst_vt['ExtPtfm']['Echo'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['DT'] = float_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['IntMethod'] = int_read(f.readline().split()[0]) - f.readline() - - # Reduction inputs - self.fst_vt['ExtPtfm']['RBMod'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['Red_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) - self.fst_vt['ExtPtfm']['NActiveDOFList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['ActiveDOFList'] = read_array(f,None,split_val='ActiveDOFList',array_type=int) - self.fst_vt['ExtPtfm']['NInitPosList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['InitPosList'] = read_array(f,None,split_val='InitPosList',array_type=float) - self.fst_vt['ExtPtfm']['NInitVelList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['InitVelList'] = read_array(f,None,split_val='InitVelList',array_type=float) - f.readline() - - # Connection inputs - self.fst_vt['ExtPtfm']['HasConnections'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['Conn_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) - f.readline() - - # User forcing inputs - self.fst_vt['ExtPtfm']['HasUserForcing'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['Force_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) - self.fst_vt['ExtPtfm']['HasConnForcing'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['FConn_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) - f.readline() - - # Output - self.fst_vt['ExtPtfm']['SumPrint'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['OutFile'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['TabDelim'] = bool_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['OutFmt'] = quoted_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['TStart'] = float_read(f.readline().split()[0]) - - # Loop through output channel lines - f.readline() - data = f.readline() - - # Handle the case if there are blank lines before the END statement, check if blank line - while data.split().__len__() == 0: - data = f.readline() - - while data.split()[0] != 'END': - if data.find('"')>=0: - channels = data.split('"') - channel_list = channels[1].split(',') - else: - row_string = data.split(',') - if len(row_string)==1: - channel_list = row_string[0].split('\n')[0] - else: - channel_list = row_string - self.set_outlist(self.fst_vt['outlist']['ExtPtfm'], channel_list) # TODO: Need to figure this out as we dont have a full outlist for now, similar to MoorDyn - data = f.readline() - - self.fst_vt['ExtPtfm']['FlexASCII'] = {} - self.read_Superelement(self.fst_vt['ExtPtfm']['Red_FileName']) - - if self.fst_vt['ExtPtfm']['HasConnections']: - self.fst_vt['ExtPtfm']['Connections'] = {} - self.read_Connections(self.fst_vt['ExtPtfm']['Conn_FileName'], self.fst_vt['ExtPtfm']['FlexASCII']['nDOF']) - - if self.fst_vt['ExtPtfm']['HasUserForcing']: - self.fst_vt['ExtPtfm']['UserForcing'] = {} - self.read_UserForcing(self.fst_vt['ExtPtfm']['Force_FileName'], self.fst_vt['ExtPtfm']['FlexASCII']['nDOF']) - - if self.fst_vt['ExtPtfm']['HasConnForcing']: - self.fst_vt['ExtPtfm']['ConnForcing'] = {} - self.read_ConnForcing(self.fst_vt['ExtPtfm']['FConn_FileName'], self.fst_vt['ExtPtfm']['Connections']['nConn']) - - f.close() - - - def read_Superelement(self, superelement_file): - - def detectAndReadExtPtfmSE(lines): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L1932 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - - def readmat(n,m,lines,iStart): - M=np.zeros((n,m)) - for j in np.arange(n): - i=iStart+j - M[j,:]=np.array(lines[i].split()).astype(float) - return M - - # --- At this stage we assume it's in the proper format - nDOF = -1 - i=0 - try: - while i0: - if l[0]=='!': - if l.find('!dimension')==0: - self.fst_vt['ExtPtfm']['FlexASCII']['nDOF'] = int(l.split(':')[1]) - nDOF = self.fst_vt['ExtPtfm']['FlexASCII']['nDOF'] - elif len(l.strip())==0: - pass - else: - raise NameError('Unexcepted content found on line {}'.format(i)) - i+=1 - except NameError as e: - raise e - except: - raise - - return True - - - f = open(superelement_file) - lines=f.read().splitlines() - if not detectAndReadExtPtfmSE(lines): - raise NameError('Could not read Superelement file') - f.close() - - - def read_Connections(self, connection_file, nDOF): - - def detectAndReadExtPtfmConnections(lines): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L1932 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - - def readmat(n,m,lines,iStart): - M=np.zeros((n,m)) - for j in np.arange(n): - i=iStart+j - M[j,:]=np.array(lines[i].split()).astype(float) - return M - - # --- At this stage we assume it's in the proper format - nConn = -1 - i=0 - try: - while i0: - if l[0]=='!': - if l.find('!nconn')==0: - self.fst_vt['ExtPtfm']['Connections']['nConn'] = int(l.split(':')[1]) - nConn = self.fst_vt['ExtPtfm']['Connections']['nConn'] - elif len(l.strip())==0: - pass - else: - raise NameError('Unexcepted content found on line {}'.format(i)) - i+=1 - except NameError as e: - raise e - except: - raise - - return True - - - f = open(connection_file) - lines=f.read().splitlines() - if not detectAndReadExtPtfmConnections(lines): - raise NameError('Could not read Connections file') - f.close() - - - def read_UserForcing(self, userforcing_file, nDOF): - - def detectAndReadExtPtfmUserForcing(lines): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L1932 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - - def readmat(n,m,lines,iStart): - M=np.zeros((n,m)) - for j in np.arange(n): - i=iStart+j - M[j,:]=np.array(lines[i].split()).astype(float) - return M - - # --- At this stage we assume it's in the proper format - NSteps = -1 - i=0 - try: - while i0: - if l[0]=='!': - if l.find('!nsteps')==0: - self.fst_vt['ExtPtfm']['UserForcing']['nSteps'] = int(l.split(':')[1]) - nSteps = self.fst_vt['ExtPtfm']['UserForcing']['nSteps'] - elif len(l.strip())==0: - pass - else: - raise NameError('Unexcepted content found on line {}'.format(i)) - i+=1 - except NameError as e: - raise e - except: - raise - - return True - - - f = open(userforcing_file) - lines=f.read().splitlines() - if not detectAndReadExtPtfmUserForcing(lines): - raise NameError('Could not read User Forcing file') - f.close() - - - def read_ConnForcing(self, connforcing_file, nConn): - - def detectAndReadExtPtfmConnForcing(lines): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L1932 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - - def readmat(n,m,lines,iStart): - M=np.zeros((n,m)) - for j in np.arange(n): - i=iStart+j - M[j,:]=np.array(lines[i].split()).astype(float) - return M - - # --- At this stage we assume it's in the proper format - NSteps = -1 - i=0 - try: - while i0: - if l[0]=='!': - if l.find('!nsteps')==0: - self.fst_vt['ExtPtfm']['ConnForcing']['nSteps'] = int(l.split(':')[1]) - nSteps = self.fst_vt['ExtPtfm']['ConnForcing']['nSteps'] - elif len(l.strip())==0: - pass - else: - raise NameError('Unexcepted content found on line {}'.format(i)) - i+=1 - except NameError as e: - raise e - except: - raise - - return True - - - f = open(connforcing_file) - lines=f.read().splitlines() - if not detectAndReadExtPtfmConnForcing(lines): - raise NameError('Could not read Connections Forcing file') - f.close() - - - def read_MAP(self, map_file): - # MAP++ - - f = open(map_file) - f.readline() - f.readline() - f.readline() - - # Init line dictionary - line_dict = ['LineType', 'Diam', 'MassDenInAir', 'EA', 'CB', 'CIntDamp', 'Ca', 'Cdn', 'Cdt'] - for ld in line_dict: - self.fst_vt['MAP'][ld] = [] - - data_line = f.readline().strip().split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MAP']['LineType'].append( str(data_line[0])) - self.fst_vt['MAP']['Diam'].append( float_read(data_line[1])) - self.fst_vt['MAP']['MassDenInAir'].append( float_read(data_line[2])) - self.fst_vt['MAP']['EA'].append( float_read(data_line[3])) - self.fst_vt['MAP']['CB'].append( float_read(data_line[4])) - self.fst_vt['MAP']['CIntDamp'].append( float_read(data_line[5])) - self.fst_vt['MAP']['Ca'].append( float_read(data_line[6])) - self.fst_vt['MAP']['Cdn'].append( float_read(data_line[7])) - self.fst_vt['MAP']['Cdt'].append( float_read(data_line[8])) - data_line = f.readline().strip().split() - #f.readline() - f.readline() - f.readline() - - # Init map nodes - node_types = ['Node','Type','X','Y','Z','M','B','FX','FY','FZ'] - for nt in node_types: - self.fst_vt['MAP'][nt] = [] - - data_node = f.readline().strip().split() - while data_node[0] and data_node[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MAP']['Node'].append(int(data_node[0])) - self.fst_vt['MAP']['Type'].append(str(data_node[1])) - self.fst_vt['MAP']['X'].append(float_read(data_node[2])) - self.fst_vt['MAP']['Y'].append(float_read(data_node[3])) - self.fst_vt['MAP']['Z'].append(float_read(data_node[4])) - self.fst_vt['MAP']['M'].append(float_read(data_node[5])) - self.fst_vt['MAP']['B'].append(float_read(data_node[6])) - self.fst_vt['MAP']['FX'].append(float_read(data_node[7])) - self.fst_vt['MAP']['FY'].append(float_read(data_node[8])) - self.fst_vt['MAP']['FZ'].append(float_read(data_node[9])) - data_node = f.readline().strip().split() - data_node = ''.join(data_node) # re-join for reading next section uniformly - # f.readline() - f.readline() - f.readline() - - # Init line properties - line_prop = ['Line', 'LineType', 'UnstrLen', 'NodeAnch', 'NodeFair', 'Flags'] - for lp in line_prop: - self.fst_vt['MAP'][lp] = [] - - data_line_prop = f.readline().strip().split() - while data_line_prop[0] and data_line_prop[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MAP']['Line'] .append( int(data_line_prop[0])) - self.fst_vt['MAP']['LineType'].append( str(data_line_prop[1])) - self.fst_vt['MAP']['UnstrLen'].append( float_read(data_line_prop[2])) - self.fst_vt['MAP']['NodeAnch'].append( int(data_line_prop[3])) - self.fst_vt['MAP']['NodeFair'].append( int(data_line_prop[4])) - self.fst_vt['MAP']['Flags'] .append( [str(val) for val in data_line_prop[5:]] ) - data_line_prop = f.readline().strip().split() - data_line_prop = ''.join(data_line_prop) # re-join for reading next section uniformly - # f.readline() - f.readline() - f.readline() - - self.fst_vt['MAP']['Option'] = [] # Solver options - # need to check for EOF here since we can have any number of solver options - data_solver = f.readline().strip().split() # Solver options - while len(data_solver) > 0: # stopping if we hit blank lines - self.fst_vt['MAP']['Option'].append([str(val) for val in data_solver]) - data_solver = f.readline().strip().split() - # self.fst_vt['MAP']['Option'] = [str(val) for val in f.readline().strip().split()] - f.close() - - def read_MoorDyn(self, moordyn_file): - - f = open(moordyn_file) - - # Init optional headers so they exist for writer, even if not read - self.fst_vt['MoorDyn']['Rod_Name'] = [] - self.fst_vt['MoorDyn']['Body_ID'] = [] - self.fst_vt['MoorDyn']['Rod_ID'] = [] - - # MoorDyn - data_line = f.readline() - while data_line: - - # Split and join so all headers are same when detecting next section - data_line = data_line.strip().split() - data_line = ''.join(data_line).lower() - - # Line Types - if 'linetypes' in data_line or 'linedictionary' in data_line: - f.readline() - f.readline() - - # Line types - self.fst_vt['MoorDyn']['Name'] = [] - self.fst_vt['MoorDyn']['Diam'] = [] - self.fst_vt['MoorDyn']['MassDen'] = [] - self.fst_vt['MoorDyn']['EA'] = [] - self.fst_vt['MoorDyn']['NonLinearEA'] = [] - self.fst_vt['MoorDyn']['BA_zeta'] = [] - self.fst_vt['MoorDyn']['EI'] = [] - self.fst_vt['MoorDyn']['Cd'] = [] - self.fst_vt['MoorDyn']['Ca'] = [] - self.fst_vt['MoorDyn']['CdAx'] = [] - self.fst_vt['MoorDyn']['CaAx'] = [] - self.fst_vt['MoorDyn']['Cl'] = [] - self.fst_vt['MoorDyn']['dF'] = [] - self.fst_vt['MoorDyn']['cF'] = [] - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same. - self.fst_vt['MoorDyn']['Name'].append(str(data_line[0])) - self.fst_vt['MoorDyn']['Diam'].append(float(data_line[1])) - self.fst_vt['MoorDyn']['MassDen'].append(float(data_line[2])) - self.fst_vt['MoorDyn']['EA'].append([float_read(dl) for dl in data_line[3].split('|')]) - self.fst_vt['MoorDyn']['BA_zeta'].append([float(dl) for dl in data_line[4].split('|')]) - self.fst_vt['MoorDyn']['EI'].append(float(data_line[5])) - self.fst_vt['MoorDyn']['Cd'].append(float(data_line[6])) - self.fst_vt['MoorDyn']['Ca'].append(float(data_line[7])) - self.fst_vt['MoorDyn']['CdAx'].append(float(data_line[8])) - self.fst_vt['MoorDyn']['CaAx'].append(float(data_line[9])) - if len(data_line) == 10: - self.fst_vt['MoorDyn']['Cl'].append(None) - self.fst_vt['MoorDyn']['dF'].append(None) - self.fst_vt['MoorDyn']['cF'].append(None) - elif (len(data_line) == 11): - self.fst_vt['MoorDyn']['Cl'].append(float(data_line[10])) - self.fst_vt['MoorDyn']['dF'].append(None) - self.fst_vt['MoorDyn']['cF'].append(None) - elif (len(data_line) == 13): - self.fst_vt['MoorDyn']['Cl'].append(float(data_line[10])) - self.fst_vt['MoorDyn']['dF'].append(float(data_line[11])) - self.fst_vt['MoorDyn']['cF'].append(float(data_line[12])) - - if type(self.fst_vt['MoorDyn']['EA'][0]) is str: - EA_file = os.path.normpath(os.path.join(os.path.dirname(moordyn_file), self.fst_vt['MoorDyn']['EA'][0])) - self.fst_vt['MoorDyn']['NonLinearEA'].append(self.read_NonLinearEA(EA_file)) - else: - self.fst_vt['MoorDyn']['NonLinearEA'].append(None) # Empty to keep track of which non-linear EA files go with what line - - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join - - elif 'rodtypes' in data_line or 'roddictionary' in data_line: - data_line = f.readline() - data_line = f.readline() - - self.fst_vt['MoorDyn']['Rod_Diam'] = [] - self.fst_vt['MoorDyn']['Rod_MassDen'] = [] - self.fst_vt['MoorDyn']['Rod_Cd'] = [] - self.fst_vt['MoorDyn']['Rod_Ca'] = [] - self.fst_vt['MoorDyn']['Rod_CdEnd'] = [] - self.fst_vt['MoorDyn']['Rod_CaEnd'] = [] - - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Rod_Name'].append(data_line[0]) - self.fst_vt['MoorDyn']['Rod_Diam'].append(float(data_line[1])) - self.fst_vt['MoorDyn']['Rod_MassDen'].append(float(data_line[2])) - self.fst_vt['MoorDyn']['Rod_Cd'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['Rod_Ca'].append(float(data_line[4])) - self.fst_vt['MoorDyn']['Rod_CdEnd'].append(float(data_line[5])) - self.fst_vt['MoorDyn']['Rod_CaEnd'].append(float(data_line[6])) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - - elif 'bodies' in data_line or 'bodylist' in data_line or 'bodyproperties' in data_line: - - f.readline() - f.readline() - self.fst_vt['MoorDyn']['Body_ID'] = [] - self.fst_vt['MoorDyn']['Body_Attachment'] = [] - self.fst_vt['MoorDyn']['X0'] = [] - self.fst_vt['MoorDyn']['Y0'] = [] - self.fst_vt['MoorDyn']['Z0'] = [] - self.fst_vt['MoorDyn']['r0'] = [] - self.fst_vt['MoorDyn']['p0'] = [] - self.fst_vt['MoorDyn']['y0'] = [] - self.fst_vt['MoorDyn']['Body_Mass'] = [] - self.fst_vt['MoorDyn']['Body_CG'] = [] - self.fst_vt['MoorDyn']['Body_I'] = [] - self.fst_vt['MoorDyn']['Body_Volume'] = [] - self.fst_vt['MoorDyn']['Body_CdA'] = [] - self.fst_vt['MoorDyn']['Body_Ca'] = [] - - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Body_ID'].append(int(data_line[0])) - self.fst_vt['MoorDyn']['Body_Attachment'].append(data_line[1]) - self.fst_vt['MoorDyn']['X0'].append(float(data_line[2])) - self.fst_vt['MoorDyn']['Y0'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['Z0'].append(float(data_line[4])) - self.fst_vt['MoorDyn']['r0'].append(float(data_line[5])) - self.fst_vt['MoorDyn']['p0'].append(float(data_line[6])) - self.fst_vt['MoorDyn']['y0'].append(float(data_line[7])) - self.fst_vt['MoorDyn']['Body_Mass'].append(float(data_line[8])) - self.fst_vt['MoorDyn']['Body_CG'].append([float(dl) for dl in data_line[9].split('|')]) - self.fst_vt['MoorDyn']['Body_I'].append([float(dl) for dl in data_line[10].split('|')]) - self.fst_vt['MoorDyn']['Body_Volume'].append(float(data_line[11])) - self.fst_vt['MoorDyn']['Body_CdA'].append([float(dl) for dl in data_line[12].split('|')]) - self.fst_vt['MoorDyn']['Body_Ca'].append([float(dl) for dl in data_line[13].split('|')]) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - - elif 'rods' in data_line or 'rodlist' in data_line or 'rodproperties' in data_line: - f.readline() - f.readline() - self.fst_vt['MoorDyn']['Rod_ID'] = [] - self.fst_vt['MoorDyn']['Rod_Type'] = [] - self.fst_vt['MoorDyn']['Rod_Attachment'] = [] - self.fst_vt['MoorDyn']['Xa'] = [] - self.fst_vt['MoorDyn']['Ya'] = [] - self.fst_vt['MoorDyn']['Za'] = [] - self.fst_vt['MoorDyn']['Xb'] = [] - self.fst_vt['MoorDyn']['Yb'] = [] - self.fst_vt['MoorDyn']['Zb'] = [] - self.fst_vt['MoorDyn']['Rod_NumSegs'] = [] - self.fst_vt['MoorDyn']['RodOutputs'] = [] - - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Rod_ID'].append(data_line[0]) - self.fst_vt['MoorDyn']['Rod_Type'].append(data_line[1]) - self.fst_vt['MoorDyn']['Rod_Attachment'].append(data_line[2]) - self.fst_vt['MoorDyn']['Xa'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['Ya'].append(float(data_line[4])) - self.fst_vt['MoorDyn']['Za'].append(float(data_line[5])) - self.fst_vt['MoorDyn']['Xb'].append(float(data_line[6])) - self.fst_vt['MoorDyn']['Yb'].append(float(data_line[7])) - self.fst_vt['MoorDyn']['Zb'].append(float(data_line[8])) - self.fst_vt['MoorDyn']['Rod_NumSegs'].append(int(data_line[9])) - self.fst_vt['MoorDyn']['RodOutputs'].append(data_line[10]) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'points' in data_line or 'connectionproperties' in data_line or \ - 'nodeproperties' in data_line or 'pointproperties' in data_line or \ - 'pointlist' in data_line: - - f.readline() - f.readline() - self.fst_vt['MoorDyn']['Point_ID'] = [] - self.fst_vt['MoorDyn']['Attachment'] = [] - self.fst_vt['MoorDyn']['X'] = [] - self.fst_vt['MoorDyn']['Y'] = [] - self.fst_vt['MoorDyn']['Z'] = [] - self.fst_vt['MoorDyn']['M'] = [] - self.fst_vt['MoorDyn']['V'] = [] - self.fst_vt['MoorDyn']['CdA'] = [] - self.fst_vt['MoorDyn']['CA'] = [] - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Point_ID'].append(int(data_line[0])) - self.fst_vt['MoorDyn']['Attachment'].append(str(data_line[1])) - self.fst_vt['MoorDyn']['X'].append(float(data_line[2])) - self.fst_vt['MoorDyn']['Y'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['Z'].append(float(data_line[4])) - self.fst_vt['MoorDyn']['M'].append(float(data_line[5])) - self.fst_vt['MoorDyn']['V'].append(float(data_line[6])) - self.fst_vt['MoorDyn']['CdA'].append(float(data_line[7])) - self.fst_vt['MoorDyn']['CA'].append(float(data_line[8])) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'lines' in data_line or 'lineproperties' in data_line or 'linelist' in data_line: - f.readline() - f.readline() - - # Lines - self.fst_vt['MoorDyn']['Line_ID'] = [] - self.fst_vt['MoorDyn']['LineType'] = [] - self.fst_vt['MoorDyn']['AttachA'] = [] - self.fst_vt['MoorDyn']['AttachB'] = [] - self.fst_vt['MoorDyn']['UnstrLen'] = [] - self.fst_vt['MoorDyn']['NumSegs'] = [] - self.fst_vt['MoorDyn']['Outputs'] = [] - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Line_ID'].append(int(data_line[0])) - self.fst_vt['MoorDyn']['LineType'].append(str(data_line[1])) - self.fst_vt['MoorDyn']['AttachA'].append(str(data_line[2])) - self.fst_vt['MoorDyn']['AttachB'].append(str(data_line[3])) - self.fst_vt['MoorDyn']['UnstrLen'].append(float(data_line[4])) - self.fst_vt['MoorDyn']['NumSegs'].append(int(data_line[5])) - self.fst_vt['MoorDyn']['Outputs'].append(str(data_line[6])) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'failure' in data_line.lower(): - f.readline() - f.readline() - - self.fst_vt['MoorDyn']['Failure_ID'] = [] - self.fst_vt['MoorDyn']['Failure_Point'] = [] - self.fst_vt['MoorDyn']['Failure_Line(s)'] = [] - self.fst_vt['MoorDyn']['FailTime'] = [] - self.fst_vt['MoorDyn']['FailTen'] = [] - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['Failure_ID'].append(int(data_line[0])) - self.fst_vt['MoorDyn']['Failure_Point'].append(data_line[1]) - self.fst_vt['MoorDyn']['Failure_Line(s)'].append([int(dl) for dl in data_line[2].split(',')]) - self.fst_vt['MoorDyn']['FailTime'].append(float(data_line[3])) - self.fst_vt['MoorDyn']['FailTen'].append(float(data_line[4])) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'control' in data_line.lower(): - f.readline() - f.readline() - - # read optional control inputs, there are other optional MoorDyn sections/inputs - self.fst_vt['MoorDyn']['ChannelID'] = [] - self.fst_vt['MoorDyn']['Lines_Control'] = [] - - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['ChannelID'].append(int(data_line[0])) - # Line(s) is a list of mooring lines, spaces are allowed between commas - control_lines = [] - for lines in data_line[1:]: - for line in lines.split(','): - control_lines.append(line.strip(',')) - - # Spaces show up in control_lines as '', remove them all - while '' in control_lines: - control_lines.remove('') - - self.fst_vt['MoorDyn']['Lines_Control'].append(control_lines) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'external' in data_line.lower(): - f.readline() - f.readline() - - self.fst_vt['MoorDyn']['External_ID'] = [] - self.fst_vt['MoorDyn']['Object'] = [] - self.fst_vt['MoorDyn']['Fext'] = [] - self.fst_vt['MoorDyn']['Blin'] = [] - self.fst_vt['MoorDyn']['Bquad'] = [] - self.fst_vt['MoorDyn']['CSys'] = [] - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['MoorDyn']['External_ID'].append(int(data_line[0])) - self.fst_vt['MoorDyn']['Object'].append(data_line[1]) - self.fst_vt['MoorDyn']['Fext'].append([float(dl) for dl in data_line[2].split('|')]) - self.fst_vt['MoorDyn']['Blin'].append([float(dl) for dl in data_line[3].split('|')]) - self.fst_vt['MoorDyn']['Bquad'].append([float(dl) for dl in data_line[4].split('|')]) - self.fst_vt['MoorDyn']['CSys'].append(data_line[5]) - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'options' in data_line: - - # MoorDyn lets options be written in any order - # Solver options - self.fst_vt['MoorDyn']['option_values'] = [] - self.fst_vt['MoorDyn']['option_names'] = [] # keep list of MoorDyn options - self.fst_vt['MoorDyn']['option_descriptions'] = [] - - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - - option_value = data_line[0].upper() # MD is case insensitive - option_name = data_line[1].upper() # MD is case insensitive - if len(data_line) > 2: - option_description = ' '.join(data_line[2:]) - else: - option_description = '-' - - if option_name.upper() == 'WATERKIN': - self.fst_vt['MoorDyn']['WaterKin'] = option_value.strip('"') - WaterKin_file = os.path.normpath(os.path.join(os.path.dirname(moordyn_file), self.fst_vt['MoorDyn']['WaterKin'])) - if self.fst_vt['MoorDyn']['WaterKin'].upper() not in ['0','UNUSED']: - self.read_WaterKin(WaterKin_file) - - self.fst_vt['MoorDyn']['option_values'].append(float_read(option_value.strip('"'))) # some options values can be strings or floats - self.fst_vt['MoorDyn']['option_names'].append(option_name) - self.fst_vt['MoorDyn']['option_descriptions'].append(option_description) - - data_line = readline_filterComments(f).split() - data_line = ''.join(data_line) # re-join for reading next section uniformly - - elif 'outputs' in data_line: - - data_line = readline_filterComments(f) - - while (data_line[0] and data_line[0][:3] != '---') and not ('END' in data_line): # OpenFAST searches for --- and END, so we'll do the same - - if '"' in data_line: - pattern = r'"?(.*?)"?' # grab only the text between quotes - data_line = re.findall(pattern, data_line)[0] - - channels = data_line.split(',') # split on commas - channels = [c.strip() for c in channels] # strip whitespace - for c in channels: - self.fst_vt['outlist']['MoorDyn'][c] = True - data_line = readline_filterComments(f) - - f.close() - break - - else: # we must be in the header section, unlimited lines of text allowed here so skip until we hit the first line w/ keywords 'Line Types' or 'Line Dictionary' - data_line = f.readline() - - def read_WaterKin(self,WaterKin_file): - - self.fst_vt['WaterKin']['z-depth'] = [] - self.fst_vt['WaterKin']['x-current'] = [] - self.fst_vt['WaterKin']['y-current'] = [] - - f = open(WaterKin_file) - f.readline() - f.readline() - f.readline() - - self.fst_vt['WaterKin']['WaveKinMod'] = int_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['WaveKinFile'] = f.readline().split()[0] # Will want to update this somehow with wave elevation - self.fst_vt['WaterKin']['dtWave'] = float_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['WaveDir'] = float_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['X_Type'] = int_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['X_Grid'] = read_array(f,None,split_val='-',array_type=float) - # re.split(',| ',f.readline().strip()) - self.fst_vt['WaterKin']['Y_Type'] = int_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['Y_Grid'] = read_array(f,None,split_val='-',array_type=float) - self.fst_vt['WaterKin']['Z_Type'] = int_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['Z_Grid'] = read_array(f,None,split_val='-',array_type=float) - f.readline() - self.fst_vt['WaterKin']['CurrentMod'] = int_read(f.readline().split()[0]) - # depending on CurrentMod, the rest of the WaterKin input file changes - if self.fst_vt['WaterKin']['CurrentMod'] == 2: # user provided depths - self.fst_vt['WaterKin']['current_Z_type'] = int_read(f.readline().split()[0]) - self.fst_vt['WaterKin']['current_Z_Grid'] = read_array(f,None,split_val='-',array_type=float) - elif self.fst_vt['WaterKin']['CurrentMod'] == 1: # user provided depths and current speeds - f.readline() - f.readline() - data_line = readline_filterComments(f).split() - while data_line[0] and data_line[0][:3] != '---': # OpenFAST searches for ---, so we'll do the same - self.fst_vt['WaterKin']['z-depth'].append(float(data_line[0])) - self.fst_vt['WaterKin']['x-current'].append(float(data_line[1])) - self.fst_vt['WaterKin']['y-current'].append(float(data_line[2])) - data_line = readline_filterComments(f).split() - f.close() - - def read_NonLinearEA(self,Stiffness_file): # read and return the nonlinear line stiffness lookup table for a given line - - f = open(Stiffness_file) - f.readline() - f.readline() - f.readline() - NonLinearEA = {"Strain" : [], "Tension" : []} - data_line = readline_filterComments(f).split() - while data_line: - NonLinearEA['Strain'].append([float(data_line[0])]) - NonLinearEA['Tension'].append([float(data_line[1])]) - data_line = readline_filterComments(f).split() - f.close() - return NonLinearEA - - def execute(self): - - fastdir = '' if self.FAST_directory is None else self.FAST_directory - self.read_MainInput() - ed_file = os.path.join(fastdir, self.fst_vt['Fst']['EDFile']) - - if self.fst_vt['Fst']['CompElast'] == 3: # SimpleElastoDyn - self.read_SimpleElastoDyn(ed_file) - else: - self.read_ElastoDyn(ed_file) - # keeping the previous logic to read in the files if self.fst_vt['Fst']['CompElast'] == 1 OR - # if the blade file exists, but include the possibility of having three unique blade files - - # Making sure the blade files pointing to the correct location - bldFile1 = self.fst_vt['ElastoDyn']['BldFile1'] if os.path.isabs(self.fst_vt['ElastoDyn']['BldFile1']) else os.path.join(os.path.dirname(ed_file), self.fst_vt['ElastoDyn']['BldFile1']) - bldFile2 = self.fst_vt['ElastoDyn']['BldFile2'] if os.path.isabs(self.fst_vt['ElastoDyn']['BldFile2']) else os.path.join(os.path.dirname(ed_file), self.fst_vt['ElastoDyn']['BldFile2']) - bldFile3 = self.fst_vt['ElastoDyn']['BldFile3'] if os.path.isabs(self.fst_vt['ElastoDyn']['BldFile3']) else os.path.join(os.path.dirname(ed_file), self.fst_vt['ElastoDyn']['BldFile3']) - - if bldFile1 == bldFile2 and bldFile1 == bldFile3: - # All blades are identical - verify if the file exists and self.fst_vt['Fst']['CompElast'] == 1 - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(bldFile1): - self.read_ElastoDynBlade(bldFile1, BladeNumber=0) - # Copy data to other blade slots - self.fst_vt['ElastoDynBlade'] = self.fst_vt['ElastoDynBlade'][0] - - elif self.fst_vt['ElastoDyn']['NumBl'] == 2 and bldFile1 == bldFile2: - # two bladed with identical blades - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(bldFile1): - self.read_ElastoDynBlade(bldFile1, BladeNumber=0) - self.fst_vt['ElastoDynBlade'] = self.fst_vt['ElastoDynBlade'][0] - - elif self.fst_vt['ElastoDyn']['NumBl'] == 1 and os.path.isfile(bldFile1): - # one bladed rotor - if self.fst_vt['Fst']['CompElast'] == 1: # we rarely have this case - self.read_ElastoDynBlade(bldFile1, BladeNumber=0) - self.fst_vt['ElastoDynBlade'] = self.fst_vt['ElastoDynBlade'][0] - else: - # we have three unique blades - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(bldFile1): - self.read_ElastoDynBlade(bldFile1, BladeNumber=0) - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(bldFile2): - self.read_ElastoDynBlade(bldFile2, BladeNumber=1) - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(bldFile3): - self.read_ElastoDynBlade(bldFile3, BladeNumber=2) - - # if not os.path.isabs(self.fst_vt['ElastoDyn']['BldFile1']): - # ed_blade_file = os.path.join(os.path.dirname(ed_file), self.fst_vt['ElastoDyn']['BldFile1']) - # if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(ed_blade_file): # If elastodyn blade is being used OR if the blade file exists - # self.read_ElastoDynBlade(ed_blade_file) - - if not os.path.isabs(self.fst_vt['ElastoDyn']['TwrFile']): - ed_tower_file = os.path.join(os.path.dirname(ed_file), self.fst_vt['ElastoDyn']['TwrFile']) - else: - ed_tower_file = self.fst_vt['ElastoDyn']['TwrFile'] - self.read_ElastoDynTower(ed_tower_file) - - if self.fst_vt['Fst']['CompInflow'] == 1: - self.read_InflowWind() - # AeroDyn version selection - if self.fst_vt['Fst']['CompAero'] == 1: - self.read_AeroDisk() - elif self.fst_vt['Fst']['CompAero'] == 2: - self.read_AeroDyn() - - if self.fst_vt['Fst']['CompServo'] == 1: - self.read_ServoDyn() - # Read StC Files - for StC_file in self.fst_vt['ServoDyn']['BStCfiles']: - self.fst_vt['BStC'].append(self.read_StC(StC_file)) - for StC_file in self.fst_vt['ServoDyn']['NStCfiles']: - self.fst_vt['NStC'].append(self.read_StC(StC_file)) - for StC_file in self.fst_vt['ServoDyn']['TStCfiles']: - self.fst_vt['TStC'].append(self.read_StC(StC_file)) - for StC_file in self.fst_vt['ServoDyn']['SStCfiles']: - self.fst_vt['SStC'].append(self.read_StC(StC_file)) - if ROSCO: - self.read_DISCON_in() - if self.fst_vt['ServoDyn']['VSContrl'] == 3: # user-defined from routine UserVSCont refered - self.read_spd_trq('spd_trq.dat') - hd_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['HydroFile'])) - if self.fst_vt['Fst']['CompHydro'] == 1: - self.read_HydroDyn(hd_file) - ss_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['SeaStFile'])) - if self.fst_vt['Fst']['CompSeaSt'] == 1: - self.read_SeaState(ss_file) - sd_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['SubFile'])) - # if os.path.isfile(sd_file): - if self.fst_vt['Fst']['CompSub'] == 1: - self.read_SubDyn(sd_file) - elif self.fst_vt['Fst']['CompSub'] == 2: - self.read_ExtPtfm(sd_file) - if self.fst_vt['Fst']['CompMooring'] == 1: # only MAP++ implemented for mooring models - map_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['MooringFile'])) - if os.path.isfile(map_file): - self.read_MAP(map_file) - if self.fst_vt['Fst']['CompMooring'] == 3: # MoorDyn implimented - moordyn_file = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['MooringFile'])) - if os.path.isfile(moordyn_file): - self.read_MoorDyn(moordyn_file) - - bd_file1 = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['BDBldFile(1)'])) - bd_file2 = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['BDBldFile(2)'])) - bd_file3 = os.path.normpath(os.path.join(fastdir, self.fst_vt['Fst']['BDBldFile(3)'])) - if os.path.isfile(bd_file1): - # if the files are the same then we only need to read it once, need to handle the cases where we have a 2 or 1 bladed rotor - # Check unique BeamDyn blade files and read only once if identical - if bd_file1 == bd_file2 and bd_file1 == bd_file3: - # All blades are identical - read once - self.read_BeamDyn(bd_file1, BladeNumber=0) - # Copy data to other blade slots - self.fst_vt['BeamDyn'] = self.fst_vt['BeamDyn'][0] - self.fst_vt['BeamDynBlade'] = self.fst_vt['BeamDynBlade'][0] - elif self.fst_vt['ElastoDyn']['NumBl'] == 2 and bd_file1 == bd_file2: - # Two-bladed rotor with identical blades - self.read_BeamDyn(bd_file1, BladeNumber=0) - # Copy data to second blade slot - self.fst_vt['BeamDyn'] = self.fst_vt['BeamDyn'][0] - self.fst_vt['BeamDynBlade'] = self.fst_vt['BeamDynBlade'][0] - else: - # All blades unique or single blade - self.read_BeamDyn(bd_file1, BladeNumber=0) - if self.fst_vt['ElastoDyn']['NumBl'] > 1: - self.read_BeamDyn(bd_file2, BladeNumber=1) - if self.fst_vt['ElastoDyn']['NumBl'] > 2: - self.read_BeamDyn(bd_file3, BladeNumber=2) - else: - # We habve a single blade and need to copy it to the other slots - self.fst_vt['BeamDyn'] = self.fst_vt['BeamDyn'][0] - self.fst_vt['BeamDynBlade'] = self.fst_vt['BeamDynBlade'][0] - - -if __name__=="__main__": - from openfast_io.FileTools import check_rtest_cloned - - parent_dir = os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) + os.sep - - # Read the model - fast = InputReader_OpenFAST() - fast.FAST_InputFile = '5MW_Land_BD_DLL_WTurb.fst' # FAST input file (ext=.fst) - fast.FAST_directory = os.path.join(parent_dir, 'reg_tests', 'r-test', - 'glue-codes', 'openfast', - '5MW_Land_BD_DLL_WTurb') # Path to fst directory files - - check_rtest_cloned(os.path.join(fast.FAST_directory)) - - fast.execute() +""" +FAST_reader.py -- backwards-compatible facade over the new composable IO layer. + +This file preserves the public API that WEIS and other downstream code depends +on (``InputReader_OpenFAST``, parsing helpers, ``set_outlist``, etc.) while +delegating all heavy lifting to ``OpenFASTDriver`` and the per-module IO +classes under ``openfast_io.io``. + +The original monolithic implementation is available in git history +(branch ``openfast_io_arch~1``) for reference. +""" +from __future__ import annotations + +import copy +import os +from functools import reduce +from pathlib import Path + +import operator + +# -- Re-exports from parsing.py (backwards-compat for external importers) ----- +from openfast_io.parsing import ( # noqa: F401 + readline_filterComments, + readline_ignoreComments, + read_array, + fix_path, + bool_read, + float_read, + int_read, + quoted_read, +) + +# -- New driver layer ---------------------------------------------------------- +from openfast_io.drivers.openfast import OpenFASTDriver, init_fst_vt + + +class InputReader_OpenFAST: + """OpenFAST input file reader -- backwards-compatible facade. + + .. deprecated:: + This facade class is deprecated and will be removed in a future version. + Use ``openfast_io.drivers.openfast.OpenFASTDriver`` directly instead. + + Usage is identical to the legacy reader:: + + reader = InputReader_OpenFAST() + reader.FAST_InputFile = '5MW.fst' + reader.FAST_directory = '/path/to/case' + reader.execute() + print(reader.fst_vt['Fst']['TMax']) + """ + + def __init__(self): + import warnings + warnings.warn( + "InputReader_OpenFAST is deprecated. " + "Use openfast_io.drivers.openfast.OpenFASTDriver directly.", + DeprecationWarning, + stacklevel=2, + ) + self.FAST_InputFile = None # FAST input file (ext=.fst) + self.FAST_directory = None # Path to fst directory files + self.path2dll = None # Path to controller DLL + self.fst_vt = init_fst_vt() + self._driver = OpenFASTDriver() + + # -- Core API -------------------------------------------------------------- + + def execute(self): + """Read the full simulation deck. Populates ``self.fst_vt``.""" + fastdir = '' if self.FAST_directory is None else self.FAST_directory + fst_path = os.path.join(fastdir, self.FAST_InputFile) + self.fst_vt = self._driver.read(Path(fst_path)) + + # -- Outlist helpers (unchanged from legacy -- pure dict manipulation) ------ + + def set_outlist(self, vartree_head, channel_list): + """Recursively set output channel names to True in the nested outlist dict.""" + + def get_dict(vartree, branch): + return reduce(operator.getitem, branch, vartree_head) + + def set_dict(vartree, branch, val): + get_dict(vartree, branch[:-1])[branch[-1]] = val + + def loop_dict(vartree, search_var, branch): + for var in vartree.keys(): + branch_i = copy.copy(branch) + branch_i.append(var) + if isinstance(vartree[var], dict): + loop_dict(vartree[var], search_var, branch_i) + else: + if var == search_var: + set_dict(vartree_head, branch_i, True) + + for var in channel_list: + var = var.replace(' ', '') + loop_dict(vartree_head, var, []) + + +if __name__ == "__main__": + from openfast_io.FileTools import check_rtest_cloned + + parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + os.sep + + fast = InputReader_OpenFAST() + fast.FAST_InputFile = '5MW_Land_BD_DLL_WTurb.fst' + fast.FAST_directory = os.path.join( + parent_dir, 'reg_tests', 'r-test', + 'glue-codes', 'openfast', + '5MW_Land_BD_DLL_WTurb', + ) + check_rtest_cloned(fast.FAST_directory) + fast.execute() diff --git a/openfast_io/openfast_io/FAST_vars_out.py b/openfast_io/openfast_io/FAST_vars_out.py index b92a0a2abe..f026ce77ce 100644 --- a/openfast_io/openfast_io/FAST_vars_out.py +++ b/openfast_io/openfast_io/FAST_vars_out.py @@ -1,5 +1,13 @@ """ Generated from FAST OutListParameters.xlsx files with openfast_io/openfast_io/create_output_vars.py """ +import warnings as _warnings +_warnings.warn( + "FAST_vars_out is deprecated and will be removed in a future version. " + "Use openfast_io.outlist.OutList instead.", + DeprecationWarning, + stacklevel=2, +) + """ AeroDyn """ AeroDyn = {} diff --git a/openfast_io/openfast_io/FAST_writer.py b/openfast_io/openfast_io/FAST_writer.py index e9998d6fbf..256b61f34c 100644 --- a/openfast_io/openfast_io/FAST_writer.py +++ b/openfast_io/openfast_io/FAST_writer.py @@ -1,2979 +1,227 @@ -import os -import copy -import random -import time -import operator -import numpy as np -from functools import reduce - - -try: - from rosco.toolbox import utilities as ROSCO_utilities - ROSCO = True -except: - ROSCO = False - - -def auto_format(f, var): - """ - Error handling for variables with 'Default' options - - args: - f: file object - var: variable to write to file - - """ - if isinstance(var, str): - f.write('{:}\n'.format(var)) - elif isinstance(var, int): - f.write('{:3}\n'.format(var)) - elif isinstance(var, float): - f.write('{: 2.15e}\n'.format(var)) - -def float_default_out(val, trim = False): - """ - Formatted float output when 'default' is an option - - args: - val: value to be formatted - trim: trim the whitespace from the returned value - - returns: - formatted value - """ - if type(val) is float: - if trim: - return '{:.4f}'.format(val) - else: - return '{: 22f}'.format(val) - else: - if trim: - return '{:}'.format(val) - else: - return '{:<22}'.format(val) - -def int_default_out(val, trim = False): - """ - Formatted int output when 'default' is an option - - args: - val: value to be formatted - trim: trim the whitespace from the returned value - - returns: - formatted value - """ - if type(val) is float: - if trim: - return '{:d}'.format(int(val)) - else: - return '{:<22d}'.format(int(val)) - else: - if trim: - return '{:}'.format(val) - else: - return '{:<22}'.format(val) - -def get_dict(vartree, branch): - """ - Given a list of nested dictionary keys, return the dictionary at that point - - args: - vartree: dictionary to search - branch: list of keys to search - - returns: - dictionary at the specified branch - """ - return reduce(operator.getitem, branch, vartree) - -class InputWriter_OpenFAST(object): - """ Methods to write OpenFAST input files.""" - - def __init__(self): - - self.FAST_namingOut = None #Master FAST file - self.FAST_runDirectory = None #Output directory - self.fst_vt = {} - self.fst_update = {} - - def update(self, fst_update={}): - """ Change fast variables based on the user supplied values """ - if fst_update: - self.fst_update = fst_update - - # recursively loop through fast variable levels and set them to their update values - def loop_dict(vartree, branch): - for var in vartree.keys(): - branch_i = copy.copy(branch) - branch_i.append(var) - if type(vartree[var]) is dict: - loop_dict(vartree[var], branch_i) - else: - # try: - get_dict(self.fst_vt, branch_i[:-1])[branch_i[-1]] = get_dict(self.fst_update, branch_i[:-1])[branch_i[-1]] - # except: - # pass - - # make sure update dictionary is not empty - if self.fst_update: - # if update dictionary uses list keys, convert to nested dictionaries - if type(list(self.fst_update.keys())) in [list, tuple]: - fst_update = copy.copy(self.fst_update) - self.fst_update = {} - for var_list in fst_update.keys(): - branch = [] - for i, var in enumerate(var_list[0:-1]): - if var not in get_dict(self.fst_update, branch).keys(): - get_dict(self.fst_update, branch)[var] = {} - branch.append(var) - - get_dict(self.fst_update, branch)[var_list[-1]] = fst_update[var_list] - else: - print('WARNING: OpenFAST user settings not correctly applied. Please check the modeling_options.yaml') - - # set fast variables to update values - loop_dict(self.fst_update, []) - - def get_outlist(self, vartree_head, channel_list=[]): - """ Loop through a list of output channel names, recursively find values set to True in the nested outlist dict """ - - # recursively search nested dictionaries - def loop_dict(vartree, outlist_i): - for var in vartree.keys(): - if type(vartree[var]) is dict: - loop_dict(vartree[var], outlist_i) - else: - if vartree[var]: - outlist_i.append(var) - return outlist_i - - # if specific outlist branches are not specified, get all - if not channel_list: - channel_list = vartree_head.keys() - - # loop through top level of dictionary - outlist = [] - for var in channel_list: - var = var.replace(' ', '') - outlist_i = [] - outlist_i = loop_dict(vartree_head[var], outlist_i) - if outlist_i: - outlist.append(sorted(outlist_i)) - - return outlist - - def update_outlist(self, channels): - """ Loop through a list of output channel names, recursively search the nested outlist dict and set to specified value""" - # 'channels' is a dict of channel names as keys with the boolean value they should be set to - - # given a list of nested dictionary keys, return the dict at that point - def get_dict(vartree, branch): - return reduce(operator.getitem, branch, self.fst_vt['outlist']) - # given a list of nested dictionary keys, set the value of the dict at that point - def set_dict(vartree, branch, val): - get_dict(vartree, branch[:-1])[branch[-1]] = val - # recursively loop through outlist dictionaries to set output channels - def loop_dict(vartree, search_var, val, branch): - for var in vartree.keys(): - branch_i = copy.copy(branch) - branch_i.append(var) - if type(vartree[var]) is dict: - loop_dict(vartree[var], search_var, val, branch_i) - else: - if var == search_var: - set_dict(self.fst_vt['outlist'], branch_i, val) - - # loop through outchannels on this line, loop through outlist dicts to set to True - channel_list = channels.keys() - for var in channel_list: - val = channels[var] - var = var.replace(' ', '') - loop_dict(self.fst_vt['outlist'], var, val, []) - - def execute(self): - - if not os.path.exists(self.FAST_runDirectory): - os.makedirs(self.FAST_runDirectory) - - if self.fst_vt['Fst']['CompElast'] == 3: # Simplified ElastoDyn - self.write_SimpleElastoDyn() - else: - # If elastodyn blade is being used OR if the blade file exists - if self.fst_vt['Fst']['CompElast'] == 1 or os.path.isfile(self.fst_vt['ElastoDyn']['BldFile1']): - - if isinstance(self.fst_vt['ElastoDynBlade'], list): - for i_EDbld, EDbld in enumerate(self.fst_vt['ElastoDynBlade']): - self.fst_vt['ElastoDyn']['BldFile%d'%(i_EDbld+1)] = self.FAST_namingOut + '_ElastoDynBlade_%d.dat'%(i_EDbld+1) - self.write_ElastoDynBlade(bldInd = i_EDbld) - - elif isinstance(self.fst_vt['ElastoDynBlade'], dict): - self.fst_vt['ElastoDyn']['BldFile1'] = self.FAST_namingOut + '_ElastoDynBlade.dat' - self.fst_vt['ElastoDyn']['BldFile2'] = self.fst_vt['ElastoDyn']['BldFile1'] - self.fst_vt['ElastoDyn']['BldFile3'] = self.fst_vt['ElastoDyn']['BldFile1'] - self.write_ElastoDynBlade() - - self.write_ElastoDynTower() - self.write_ElastoDyn() - # self.write_WindWnd() - if self.fst_vt['Fst']['CompInflow'] == 1: - self.write_InflowWind() - if self.fst_vt['Fst']['CompAero'] == 1: - self.write_AeroDisk() - elif self.fst_vt['Fst']['CompAero'] == 2: - self.write_AeroDyn() - - if self.fst_vt['Fst']['CompServo'] == 1: - if 'DISCON_in' in self.fst_vt and ROSCO: - self.write_DISCON_in() - self.write_ServoDyn() - for i_NStC, NStC in enumerate(self.fst_vt['NStC']): - self.write_StC(NStC,self.fst_vt['ServoDyn']['NStCfiles'][i_NStC]) - for i_BStC, BStC in enumerate(self.fst_vt['BStC']): - self.write_StC(BStC,self.fst_vt['ServoDyn']['BStCfiles'][i_BStC]) - for i_TStC, TStC in enumerate(self.fst_vt['TStC']): - self.write_StC(TStC,self.fst_vt['ServoDyn']['TStCfiles'][i_TStC]) - for i_SStC, SStC in enumerate(self.fst_vt['SStC']): - self.write_StC(SStC,self.fst_vt['ServoDyn']['SStCfiles'][i_SStC]) - if self.fst_vt['ServoDyn']['VSContrl'] == 3: # user-defined from routine UserVSCont refered - self.write_spd_trq() - - if self.fst_vt['Fst']['CompHydro'] == 1: - self.write_HydroDyn() - if self.fst_vt['Fst']['CompSeaSt'] == 1: - self.write_SeaState() - if self.fst_vt['Fst']['CompSub'] == 1: - self.write_SubDyn() - elif self.fst_vt['Fst']['CompSub'] == 2: - self.write_ExtPtfm() - if self.fst_vt['Fst']['CompMooring'] == 1: - self.write_MAP() - elif self.fst_vt['Fst']['CompMooring'] == 3: - self.write_MoorDyn() - if self.fst_vt['WaterKin']: # will be empty if not read - self.write_WaterKin(os.path.join(self.FAST_runDirectory,self.fst_vt['MoorDyn']['WaterKin_file'])) - - # # look at if the self.fst_vt['BeamDyn'] is an array, if so, loop through the array - # # if its a dictionary, just write the same BeamDyn file - - if isinstance(self.fst_vt['BeamDyn'], list): - for i_BD, BD in enumerate(self.fst_vt['BeamDyn']): - if not BD == {}: - self.fst_vt['Fst']['BDBldFile(%d)'%(i_BD+1)] = self.FAST_namingOut + '_BeamDyn_%d.dat'%(i_BD+1) - self.write_BeamDyn(bldInd = i_BD) - - elif isinstance(self.fst_vt['BeamDyn'], dict): - if not self.fst_vt['BeamDyn'] == {}: - self.fst_vt['Fst']['BDBldFile(1)'] = self.FAST_namingOut + '_BeamDyn.dat' - self.fst_vt['Fst']['BDBldFile(2)'] = self.fst_vt['Fst']['BDBldFile(1)'] - self.fst_vt['Fst']['BDBldFile(3)'] = self.fst_vt['Fst']['BDBldFile(1)'] - self.write_BeamDyn() - - self.write_MainInput() - - def write_MainInput(self): - # Main FAST Input File - - self.FAST_InputFileOut = os.path.join(self.FAST_runDirectory, self.FAST_namingOut+'.fst') - - # Keep simple for now: - f = open(self.FAST_InputFileOut, 'w') - - # ===== .fst Input File ===== - - f.write('------- OpenFAST INPUT FILE -------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['AbortLevel']+'"', 'AbortLevel', '- Error level when simulation should abort (string) {"WARNING", "SEVERE", "FATAL"}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TMax'], 'TMax', '- Total run time (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['DT'], 'DT', '- Recommended module time step (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['ModCoupling'], 'ModCoupling', '- Module coupling method (switch) {1=loose; 2=tight with fixed Jacobian updates (DT_UJac); 3=tight with automatic Jacobian updates}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['InterpOrder'], 'InterpOrder', '- Interpolation order for input/output time history (-) {1=linear, 2=quadratic}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['NumCrctn'], 'NumCrctn', '- Numerical damping parameter for tight coupling generalized-alpha integrator (-) [0.0 to 1.0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['RhoInf'], 'RhoInf', '- Convergence iteration error tolerance for tight coupling generalized alpha integrator (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['ConvTol'], 'ConvTol', '- Maximum number of convergence iterations for tight coupling generalized alpha integrator (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['MaxConvIter'], 'MaxConvIter', '- Number of correction iterations (-) {0=explicit calculation, i.e., no corrections}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['DT_UJac'], 'DT_UJac', '- Time between calls to get Jacobians (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['UJacSclFact'], 'UJacSclFact', '- Scaling factor used in Jacobians (-)\n')) - f.write('---------------------- FEATURE SWITCHES AND FLAGS ------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['NRotors'], 'NRotors', '- Number of rotors in turbine\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompElast'], 'CompElast', '- Compute structural dynamics (switch) {1=ElastoDyn; 2=ElastoDyn + BeamDyn for blades; 3=Simplified ElastoDyn}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompInflow'], 'CompInflow', '- Compute inflow wind velocities (switch) {0=still air; 1=InflowWind; 2=external from ExtInflow}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompAero'], 'CompAero', '- Compute aerodynamic loads (switch) {0=None; 1=AeroDisk; 2=AeroDyn; 3=ExtLoads}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompServo'], 'CompServo', '- Compute control and electrical-drive dynamics (switch) {0=None; 1=ServoDyn}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompSeaSt'], 'CompSeaSt', '- Compute sea state information (switch) {0=None; 1=SeaState}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompHydro'], 'CompHydro', '- Compute hydrodynamic loads (switch) {0=None; 1=HydroDyn}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompSub'], 'CompSub', '- Compute sub-structural dynamics (switch) {0=None; 1=SubDyn; 2=External Platform MCKF}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompMooring'], 'CompMooring', '- Compute mooring system (switch) {0=None; 1=MAP++; 2=FEAMooring; 3=MoorDyn; 4=OrcaFlex}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompIce'], 'CompIce', '- Compute ice loads (switch) {0=None; 1=IceFloe; 2=IceDyn}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CompSoil'], 'CompSoil', '- Compute soil-structural dynamics (switch) {0=None; 1=SoilDyn}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['MHK'], 'MHK', '- MHK turbine type (switch) {0=Not an MHK turbine; 1=Fixed MHK turbine; 2=Floating MHK turbine}\n')) - f.write('{:<22} {:<11} {:}'.format(' '.join([str(b)[0] for b in np.array(self.fst_vt['Fst']['MirrorRotor'], dtype=bool)]), 'MirrorRotor', '- List of rotor rotation directions [1 to NRotors] {0=CCW, 1=CW}\n')) - f.write('---------------------- ENVIRONMENTAL CONDITIONS --------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Gravity'], 'Gravity', '- Gravitational acceleration (m/s^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['AirDens'], 'AirDens', '- Air density (kg/m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['WtrDens'], 'WtrDens', '- Water density (kg/m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['KinVisc'], 'KinVisc', '- Kinematic viscosity of working fluid (m^2/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['SpdSound'], 'SpdSound', '- Speed of sound in working fluid (m/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Patm'], 'Patm', '- Atmospheric pressure (Pa) [used only for an MHK turbine cavitation check]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Pvap'], 'Pvap', '- Vapour pressure of working fluid (Pa) [used only for an MHK turbine cavitation check]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['WtrDpth'], 'WtrDpth', '- Water depth (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['MSL2SWL'], 'MSL2SWL', '- Offset between still-water level and mean sea level (m) [positive upward]\n')) - f.write('---------------------- INPUT FILES ---------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['EDFile']+'"', 'EDFile', '- Name of file containing ElastoDyn input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFile(1)']+'"', 'BDBldFile(1)', '- Name of file containing BeamDyn input parameters for blade 1 (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFile(2)']+'"', 'BDBldFile(2)', '- Name of file containing BeamDyn input parameters for blade 2 (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFile(3)']+'"', 'BDBldFile(3)', '- Name of file containing BeamDyn input parameters for blade 3 (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['InflowFile']+'"', 'InflowFile', '- Name of file containing inflow wind input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['AeroFile']+'"', 'AeroFile', '- Name of file containing aerodynamic input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['ServoFile']+'"', 'ServoFile', '- Name of file containing control and electrical-drive input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['SeaStFile']+'"', 'SeaStFile', '- Name of file containing sea state input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['HydroFile']+'"', 'HydroFile', '- Name of file containing hydrodynamic input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['SubFile']+'"', 'SubFile', '- Name of file containing sub-structural input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['MooringFile']+'"', 'MooringFile', '- Name of file containing mooring system input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['IceFile']+'"', 'IceFile', '- Name of file containing ice input parameters (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['SoilFile']+'"', 'SoilFile', '- Name of file containing soil input parameters (quoted string)\n')) - # for i in range(1, self.fst_vt['Fst']['NRotors']): - # f.write('---------------------- INPUT FILES Rotor '+str(i+1)+' -------------------------------------\n') - # f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['EDFiles'][i]+'"', 'EDFile', '- Name of file containing ElastoDyn input parameters (quoted string)\n')) - # f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFiles(1)'][i]+'"', 'BDBldFile(1)', '- Name of file containing BeamDyn input parameters for blade 1 (quoted string)\n')) - # f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFiles(2)'][i]+'"', 'BDBldFile(2)', '- Name of file containing BeamDyn input parameters for blade 2 (quoted string)\n')) - # f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['BDBldFiles(3)'][i]+'"', 'BDBldFile(3)', '- Name of file containing BeamDyn input parameters for blade 3 (quoted string)\n')) - # f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['ServoFiles'][i]+'"', 'ServoFile', '- Name of file containing control and electrical-drive input parameters (quoted string)\n')) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['SumPrint'], 'SumPrint', '- Print summary data to ".sum" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['SttsTime'], 'SttsTime', '- Amount of time between screen status messages (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['ChkptTime'], 'ChkptTime', '- Amount of time between creating checkpoint files for potential restart (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['DT_Out'], 'DT_Out', '- Time step for tabular output (s) (or "default")\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TStart'], 'TStart', '- Time to begin tabular output (s)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['Fst']['OutFileFmt'], 'OutFileFmt', '- Format for tabular (time-marching) output file (switch) {1: text file [.out], 2: binary file [.outb], 3: both 1 and 2, 4: uncompressed binary [.outb], 5: both 1 and 4}\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag) {uses spaces if false}\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['Fst']['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output, excluding the time channel. Resulting field should be 10 characters. (quoted string)\n')) - f.write('---------------------- LINEARIZATION -------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Linearize'], 'Linearize', '- Linearization analysis (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['CalcSteady'], 'CalcSteady', '- Calculate a steady-state periodic operating point before linearization? [unused if Linearize=False] (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TrimCase'], 'TrimCase', '- Controller parameter to be trimmed {1:yaw; 2:torque; 3:pitch} [used only if CalcSteady=True] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TrimTol'], 'TrimTol', '- Tolerance for the rotational speed convergence [used only if CalcSteady=True] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['TrimGain'], 'TrimGain', '- Proportional gain for the rotational speed error (>0) [used only if CalcSteady=True] (rad/(rad/s) for yaw or pitch; Nm/(rad/s) for torque)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Twr_Kdmp'], 'Twr_Kdmp', '- Damping factor for the tower [used only if CalcSteady=True] (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['Bld_Kdmp'], 'Bld_Kdmp', '- Damping factor for the blades [used only if CalcSteady=True] (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['NLinTimes'], 'NLinTimes', '- Number of times to linearize (-) [>=1] [unused if Linearize=False]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(['%f'%i for i in np.array(self.fst_vt['Fst']['LinTimes'], dtype=float)]), 'LinTimes', '- List of times at which to linearize (s) [1 to NLinTimes] [used only when Linearize=True and CalcSteady=False]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['LinInputs'], 'LinInputs', '- Inputs included in linearization (switch) {0=none; 1=standard; 2=all module inputs (debug)} [unused if Linearize=False]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['LinOutputs'], 'LinOutputs', '- Outputs included in linearization (switch) {0=none; 1=from OutList(s); 2=all module outputs (debug)} [unused if Linearize=False]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['LinOutJac'], 'LinOutJac', '- Include full Jacobians in linearization output (for debug) (flag) [unused if Linearize=False; used only if LinInputs=LinOutputs=2]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['LinOutMod'], 'LinOutMod', '- Write module-level linearization output files in addition to output for full system? (flag) [unused if Linearize=False]\n')) - f.write('---------------------- VISUALIZATION ------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['WrVTK'], 'WrVTK', '- VTK visualization data output: (switch) {0=none; 1=initialization data only; 2=animation; 3=mode shapes}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['VTK_type'], 'VTK_type', '- Type of VTK visualization data: (switch) {1=surfaces; 2=basic meshes (lines/points); 3=all meshes (debug)} [unused if WrVTK=0]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['Fst']['VTK_fields'], 'VTK_fields', '- Write mesh fields to VTK data files? (flag) {true/false} [unused if WrVTK=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['Fst']['VTK_fps'], 'VTK_fps', '-Frame rate for VTK output (frames per second){will use closest integer multiple of DT} [used only if WrVTK=2 or WrVTK=3]\n')) - - f.flush() - os.fsync(f) - f.close() - - def write_ElastoDyn(self): - - self.fst_vt['Fst']['EDFile'] = self.FAST_namingOut + '_ElastoDyn.dat' - ed_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['EDFile']) - f = open(ed_file, 'w') - - f.write('------- ELASTODYN INPUT FILE -------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - - # ElastoDyn Simulation Control (ed_sim_ctrl) - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Method'], 'Method', '- Integration method: {1: RK4, 2: AB4, or 3: ABM4} (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['DT'], 'DT', '- Integration time step (s)\n')) - f.write('---------------------- DEGREES OF FREEDOM --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['FlapDOF1'], 'FlapDOF1', '- First flapwise blade mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['FlapDOF2'], 'FlapDOF2', '- Second flapwise blade mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['EdgeDOF'], 'EdgeDOF', '- First edgewise blade mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PitchDOF'], 'PitchDOF', '- Blade pitch DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetDOF'], 'TeetDOF', '- Rotor-teeter DOF (flag) [unused for 3 blades]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['DrTrDOF'], 'DrTrDOF', '- Drivetrain rotational-flexibility DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['GenDOF'], 'GenDOF', '- Generator DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['YawDOF'], 'YawDOF', '- Yaw DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TwFADOF1'], 'TwFADOF1', '- First fore-aft tower bending-mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TwFADOF2'], 'TwFADOF2', '- Second fore-aft tower bending-mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TwSSDOF1'], 'TwSSDOF1', '- First side-to-side tower bending-mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TwSSDOF2'], 'TwSSDOF2', '- Second side-to-side tower bending-mode DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmSgDOF'], 'PtfmSgDOF', '- Platform horizontal surge translation DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmSwDOF'], 'PtfmSwDOF', '- Platform horizontal sway translation DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmHvDOF'], 'PtfmHvDOF', '- Platform vertical heave translation DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRDOF'], 'PtfmRDOF', '- Platform roll tilt rotation DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmPDOF'], 'PtfmPDOF', '- Platform pitch tilt rotation DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmYDOF'], 'PtfmYDOF', '- Platform yaw rotation DOF (flag)\n')) - f.write('---------------------- INITIAL CONDITIONS --------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['OoPDefl'], 'OoPDefl', '- Initial out-of-plane blade-tip displacement (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['IPDefl'], 'IPDefl', '- Initial in-plane blade-tip deflection (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPitch1'], 'BlPitch(1)', '- Blade 1 initial pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPitch2'], 'BlPitch(2)', '- Blade 2 initial pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPitch3'], 'BlPitch(3)', '- Blade 3 initial pitch (degrees) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetDefl'], 'TeetDefl', '- Initial or fixed teeter angle (degrees) [unused for 3 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Azimuth'], 'Azimuth', '- Initial azimuth angle for blade 1 (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['RotSpeed'], 'RotSpeed', '- Initial or fixed rotor speed (rpm)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacYaw'], 'NacYaw', '- Initial or fixed nacelle-yaw angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TTDspFA'], 'TTDspFA', '- Initial fore-aft tower-top displacement (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TTDspSS'], 'TTDspSS', '- Initial side-to-side tower-top displacement (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmSurge'], 'PtfmSurge', '- Initial or fixed horizontal surge translational displacement of platform (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmSway'], 'PtfmSway', '- Initial or fixed horizontal sway translational displacement of platform (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmHeave'], 'PtfmHeave', '- Initial or fixed vertical heave translational displacement of platform (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRoll'], 'PtfmRoll', '- Initial or fixed roll tilt rotational displacement of platform (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmPitch'], 'PtfmPitch', '- Initial or fixed pitch tilt rotational displacement of platform (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmYaw'], 'PtfmYaw', '- Initial or fixed yaw rotational displacement of platform (degrees)\n')) - f.write('---------------------- TURBINE CONFIGURATION -----------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NumBl'], 'NumBl', '- Number of blades (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TipRad'], 'TipRad', '- The distance from the rotor apex to the blade tip (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['HubRad'], 'HubRad', '- The distance from the rotor apex to the blade root (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PreCone(1)'], 'PreCone(1)', '- Blade 1 cone angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PreCone(2)'], 'PreCone(2)', '- Blade 2 cone angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PreCone(3)'], 'PreCone(3)', '- Blade 3 cone angle (degrees) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['HubCM'], 'HubCM', '- Distance from rotor apex to hub mass [positive downwind] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['UndSling'], 'UndSling', '- Undersling length [distance from teeter pin to the rotor apex] (meters) [unused for 3 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Delta3'], 'Delta3', '- Delta-3 angle for teetering rotors (degrees) [unused for 3 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['AzimB1Up'], 'AzimB1Up', '- Azimuth value to use for I/O when blade 1 points up (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['OverHang'], 'OverHang', '- Distance from yaw axis to rotor apex [3 blades] or teeter pin [2 blades] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['ShftGagL'], 'ShftGagL', '- Distance from rotor apex [3 blades] or teeter pin [2 blades] to shaft strain gages [positive for upwind rotors] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['ShftTilt'], 'ShftTilt', '- Rotor shaft tilt angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacCMxn'], 'NacCMxn', '- Downwind distance from the tower-top to the nacelle CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacCMyn'], 'NacCMyn', '- Lateral distance from the tower-top to the nacelle CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacCMzn'], 'NacCMzn', '- Vertical distance from the tower-top to the nacelle CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NcIMUxn'], 'NcIMUxn', '- Downwind distance from the tower-top to the nacelle IMU (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NcIMUyn'], 'NcIMUyn', '- Lateral distance from the tower-top to the nacelle IMU (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NcIMUzn'], 'NcIMUzn', '- Vertical distance from the tower-top to the nacelle IMU (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Twr2Shft'], 'Twr2Shft', '- Vertical distance from the tower-top to the rotor shaft (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TowerHt'], 'TowerHt', '- Height of tower relative to ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TowerBsHt'], 'TowerBsHt', '- Height of tower base relative to ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmCMxt'], 'PtfmCMxt', '- Downwind distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmCMyt'], 'PtfmCMyt', '- Lateral distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmCMzt'], 'PtfmCMzt', '- Vertical distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform CM (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRefxt'], 'PtfmRefxt', '- Downwind distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform reference point (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRefyt'], 'PtfmRefyt', '- Lateral distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform reference point (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRefzt'], 'PtfmRefzt', '- Vertical distance from the ground level [onshore], MSL [offshore wind or floating MHK], or seabed [fixed MHK] to the platform reference point (meters)\n')) - f.write('---------------------- MASS AND INERTIA ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TipMass(1)'], 'TipMass(1)', '- Tip-brake mass, blade 1 (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TipMass(2)'], 'TipMass(2)', '- Tip-brake mass, blade 2 (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TipMass(3)'], 'TipMass(3)', '- Tip-brake mass, blade 3 (kg) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PBrIner(1)'], 'PBrIner(1)', '- Pitch bearing inertia, blade 1 (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PBrIner(2)'], 'PBrIner(2)', '- Pitch bearing inertia, blade 2 (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PBrIner(3)'], 'PBrIner(3)', '- Pitch bearing inertia, blade 3 (kg m^2) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPIner(1)'], 'BlPIner(1)', '- Blade pitch inertia, blade 1 (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPIner(2)'], 'BlPIner(2)', '- Blade pitch inertia, blade 2 (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BlPIner(3)'], 'BlPIner(3)', '- Blade pitch inertia, blade 3 (kg m^2) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['HubMass'], 'HubMass', '- Hub mass (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['HubIner'], 'HubIner', 'Hub inertia about rotor axis (2 or 3-blades) (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['HubIner_Teeter'], 'HubIner_Teeter', 'Hub inertia about teeter axis (2-blades) (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['GenIner'], 'GenIner', '- Generator inertia about HSS (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacMass'], 'NacMass', '- Nacelle mass (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NacYIner'], 'NacYIner', '- Nacelle inertia about yaw axis (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['YawBrMass'], 'YawBrMass', '- Yaw bearing mass (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmMass'], 'PtfmMass', '- Platform mass (kg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmRIner'], 'PtfmRIner', '- Platform inertia for roll tilt rotation about the platform CM (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmPIner'], 'PtfmPIner', '- Platform inertia for pitch tilt rotation about the platform CM (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmYIner'], 'PtfmYIner', '- Platform inertia for yaw rotation about the platform CM (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmXYIner'], 'PtfmXYIner', '- Platform xy moment of inertia about the platform CM (=-int(xydm)) (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmYZIner'], 'PtfmYZIner', '- Platform yz moment of inertia about the platform CM (=-int(yzdm)) (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['PtfmXZIner'], 'PtfmXZIner', '- Platform xz moment of inertia about the platform CM (=-int(xzdm)) (kg m^2)\n')) - f.write('---------------------- BLADE ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BldNodes'], 'BldNodes', '- Number of blade nodes (per blade) used for analysis (-)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['BldFile1']+'"', 'BldFile(1)', '- Name of file containing properties for blade 1 (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['BldFile2']+'"', 'BldFile(2)', '- Name of file containing properties for blade 2 (quoted string)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['BldFile3']+'"', 'BldFile(3)', '- Name of file containing properties for blade 3 (quoted string) [unused for 2 blades]\n')) - f.write('---------------------- ROTOR-TEETER --------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetMod'], 'TeetMod', '- Rotor-teeter spring/damper model {0: none, 1: standard, 2: user-defined from routine UserTeet} (switch) [unused for 3 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetDmpP'], 'TeetDmpP', '- Rotor-teeter damper position (degrees) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetDmp'], 'TeetDmp', '- Rotor-teeter damping constant (N-m/(rad/s)) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetCDmp'], 'TeetCDmp', '- Rotor-teeter rate-independent Coulomb-damping moment (N-m) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetSStP'], 'TeetSStP', '- Rotor-teeter soft-stop position (degrees) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetHStP'], 'TeetHStP', '- Rotor-teeter hard-stop position (degrees) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetSSSp'], 'TeetSSSp', '- Rotor-teeter soft-stop linear-spring constant (N-m/rad) [used only for 2 blades and when TeetMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TeetHSSp'], 'TeetHSSp', '- Rotor-teeter hard-stop linear-spring constant (N-m/rad) [used only for 2 blades and when TeetMod=1]\n')) - f.write('---------------------- YAW-FRICTION --------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['YawFrctMod'], 'YawFrctMod', '- Yaw-friction model {0: none, 1: friction independent of yaw-bearing force and bending moment, 2: friction with Coulomb terms depending on yaw-bearing force and bending moment, 3: user defined model} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_CSmax'], 'M_CSmax', '- Maximum static Coulomb friction torque (N-m) [M_CSmax when YawFrctMod=1; |Fz|*M_CSmax when YawFrctMod=2 and Fz<0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_FCSmax'], 'M_FCSmax', '- Maximum static Coulomb friction torque proportional to yaw bearing shear force (N-m) [sqrt(Fx^2+Fy^2)*M_FCSmax; only used when YawFrctMod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_MCSmax'], 'M_MCSmax', '- Maximum static Coulomb friction torque proportional to yaw bearing bending moment (N-m) [sqrt(Mx^2+My^2)*M_MCSmax; only used when YawFrctMod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_CD'], 'M_CD', '- Dynamic Coulomb friction moment (N-m) [M_CD when YawFrctMod=1; |Fz|*M_CD when YawFrctMod=2 and Fz<0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_FCD'], 'M_FCD', '- Dynamic Coulomb friction moment proportional to yaw bearing shear force (N-m) [sqrt(Fx^2+Fy^2)*M_FCD; only used when YawFrctMod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['M_MCD'], 'M_MCD', '- Dynamic Coulomb friction moment proportional to yaw bearing bending moment (N-m) [sqrt(Mx^2+My^2)*M_MCD; only used when YawFrctMod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['sig_v'], 'sig_v', '- Linear viscous friction coefficient (N-m/(rad/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['sig_v2'], 'sig_v2', '- Quadratic viscous friction coefficient (N-m/(rad/s)^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['OmgCut'], 'OmgCut', '- Yaw angular velocity cutoff below which viscous friction is linearized (rad/s)\n')) - f.write('---------------------- DRIVETRAIN ----------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['GBoxEff'], 'GBoxEff', '- Gearbox efficiency (%)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['GBRatio'], 'GBRatio', '- Gearbox ratio (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['DTTorSpr'], 'DTTorSpr', '- Drivetrain torsional spring (N-m/rad)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['DTTorDmp'], 'DTTorDmp', '- Drivetrain torsional damper (N-m/(rad/s))\n')) - f.write('---------------------- FURLING -------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['Furling'], 'Furling', '- Read in additional model properties for furling turbine (flag) [must currently be FALSE)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['FurlFile']+'"', 'FurlFile', '- Name of file containing furling properties (quoted string) [unused when Furling=False]\n')) - f.write('---------------------- TOWER ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TwrNodes'], 'TwrNodes', '- Number of tower nodes used for analysis (-)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['TwrFile']+'"', 'TwrFile', '- Name of file containing tower properties (quoted string)\n')) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['SumPrint'], 'SumPrint', '- Print summary data to ".sum" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['OutFile'], 'OutFile', '- Switch to determine where output will be placed: {1: in module output file only; 2: in glue code output file only; 3: both} (currently unused)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ElastoDyn']['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output (except time). Resulting field should be 10 characters. (quoted string) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['TStart'], 'TStart', '- Time to begin tabular output (s) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['DecFact'], 'DecFact', '- Decimation factor for tabular output {1: output every time step} (-) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NTwGages'], 'NTwGages', '- Number of tower nodes that have strain gages for output [0 to 9] (-)\n')) - if self.fst_vt['ElastoDyn']['TwrGagNd'] != 0: - f.write('{:<22} {:<11} {:}'.format(', '.join(['%d'%int(i) for i in self.fst_vt['ElastoDyn']['TwrGagNd']]), 'TwrGagNd', '- List of tower nodes that have strain gages [1 to TwrNodes] (-) [unused if NTwGages=0]\n')) - else: - f.write('{:<22} {:<11} {:}'.format('', 'TwrGagNd', '- List of tower nodes that have strain gages [1 to TwrNodes] (-) [unused if NTwGages=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['NBlGages'], 'NBlGages', '- Number of blade nodes that have strain gages for output [0 to 9] (-)\n')) - if self.fst_vt['ElastoDyn']['BldGagNd'] != 0: - f.write('{:<22} {:<11} {:}'.format(', '.join(['%d'%int(i) for i in self.fst_vt['ElastoDyn']['BldGagNd']]), 'BldGagNd', '- List of blade nodes that have strain gages [1 to BldNodes] (-) [unused if NBlGages=0]\n')) - else: - f.write('{:<22} {:<11} {:}'.format('', 'BldGagNd', '- List of blade nodes that have strain gages [1 to BldNodes] (-) [unused if NBlGages=0]\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - - outlist = self.get_outlist(self.fst_vt['outlist'], ['ElastoDyn']) - - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of OutList section (the word "END" must appear in the first 3 columns of the last OutList line)\n') - - # Optional nodal output section - if 'BldNd_BladesOut' in self.fst_vt['ElastoDyn']: - f.write('====== Outputs for all blade stations (same ending as above for Spn1.... =========================== [optional section]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BldNd_BladesOut'], 'BldNd_BladesOut', '- Number of blades to output all node information at. Up to number of blades on turbine. (-)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ElastoDyn']['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output. Not implemented yet. (-)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx, ElastoDyn_Nodes tab for a listing of available output channels, (-)\n') - - opt_outlist = self.get_outlist(self.fst_vt['outlist'], ['ElastoDyn_Nodes']) - for opt_channel_list in opt_outlist: - for i in range(len(opt_channel_list)): - f.write('"' + opt_channel_list[i] + '"\n') - f.write('END (the word "END" must appear in the first 3 columns of this last OutList line in the optional nodal output section)\n') - - f.write('---------------------------------------------------------------------------------------\n') - f.flush() - os.fsync(f) - f.close() - - def write_SimpleElastoDyn(self): - # Write the simple ElastoDyn file - - self.fst_vt['Fst']['EDFile'] = self.FAST_namingOut + '_SimpleElastoDyn.dat' - sed_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['EDFile']) - f = open(sed_file, 'w') - - f.write('------- SIMPLIFIED ELASTODYN INPUT FILE ----------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['IntMethod'], 'IntMethod', '- Integration method: {1: RK4, 2: AB4, or 3: ABM4} (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['DT'], 'DT', '- Integration time step (s)\n')) - f.write('---------------------- DEGREES OF FREEDOM --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['GenDOF'], 'GenDOF', '- Generator DOF (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['YawDOF'], 'YawDOF', '- Yaw degree of freedom -- controlled by controller (flag)\n')) - f.write('---------------------- INITIAL CONDITIONS --------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['Azimuth'], 'Azimuth', '- Initial azimuth angle for blades (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['BlPitch'], 'BlPitch', '- Blades initial pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['RotSpeed'], 'RotSpeed', '- Initial or fixed rotor speed (rpm)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['NacYaw'], 'NacYaw', '- Initial or fixed nacelle-yaw angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['PtfmPitch'], 'PtfmPitch', '- Fixed pitch tilt rotational displacement of platform (degrees)\n')) - f.write('---------------------- TURBINE CONFIGURATION -----------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['NumBl'], 'NumBl', '- Number of blades (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['TipRad'], 'TipRad', '- The distance from the rotor apex to the blade tip (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['HubRad'], 'HubRad', '- The distance from the rotor apex to the blade root (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['PreCone'], 'PreCone', '- Blades cone angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['OverHang'], 'OverHang', '- Distance from yaw axis to rotor apex [3 blades] or teeter pin [2 blades] (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['ShftTilt'], 'ShftTilt', '- Rotor shaft tilt angle (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['Twr2Shft'], 'Twr2Shft', '- Vertical distance from the tower-top to the rotor shaft (meters)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['TowerHt'], 'TowerHt', '- Height of tower above ground level [onshore] or MSL [offshore] (meters)\n')) - f.write('---------------------- MASS AND INERTIA ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['RotIner'], 'RotIner', '- Rot inertia about rotor axis [blades + hub] (kg m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['GenIner'], 'GenIner', '- Generator inertia about HSS (kg m^2)\n')) - f.write('---------------------- DRIVETRAIN ----------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SimpleElastoDyn']['GBoxRatio'], 'GBoxRatio', '- Gearbox ratio (-)\n')) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - - outlist = self.get_outlist(self.fst_vt['outlist'], ['SimpleElastoDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - f.write('---------------------------------------------------------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_ElastoDynBlade(self, bldInd = None): - - if bldInd is None: - EDbld_dict = self.fst_vt['ElastoDynBlade'] - blade_file = os.path.join(self.FAST_runDirectory,self.fst_vt['ElastoDyn']['BldFile1']) - else: - EDbld_dict = self.fst_vt['ElastoDynBlade'][bldInd] - blade_file = os.path.join(self.FAST_runDirectory,self.fst_vt['ElastoDyn']['BldFile'+(bldInd+1)]) - - f = open(blade_file, 'w') - - f.write('------- ELASTODYN INDIVIDUAL BLADE INPUT FILE --------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- BLADE PARAMETERS ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['NBlInpSt'], 'NBlInpSt', '- Number of blade input stations (-)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFlDmp1'], 'BldFlDmp(1)', '- Blade flap mode #1 structural damping in percent of critical (%)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFlDmp2'], 'BldFlDmp(2)', '- Blade flap mode #2 structural damping in percent of critical (%)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdDmp1'], 'BldEdDmp(1)', '- Blade edge mode #1 structural damping in percent of critical (%)\n')) - f.write('---------------------- BLADE ADJUSTMENT FACTORS --------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['FlStTunr1'], 'FlStTunr(1)', '- Blade flapwise modal stiffness tuner, 1st mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['FlStTunr2'], 'FlStTunr(2)', '- Blade flapwise modal stiffness tuner, 2nd mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['AdjBlMs'], 'AdjBlMs', '- Factor to adjust blade mass density (-)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['AdjFlSt'], 'AdjFlSt', '- Factor to adjust blade flap stiffness (-)\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['AdjEdSt'], 'AdjEdSt', '- Factor to adjust blade edge stiffness (-)\n')) - f.write('---------------------- DISTRIBUTED BLADE PROPERTIES ----------------------------\n') - f.write(' BlFract StrcTwst BMassDen FlpStff EdgStff\n') - f.write(' (-) (deg) (kg/m) (Nm^2) (Nm^2)\n') - BlFract = EDbld_dict['BlFract'] - StrcTwst = EDbld_dict['StrcTwst'] - BMassDen = EDbld_dict['BMassDen'] - FlpStff = EDbld_dict['FlpStff'] - EdgStff = EDbld_dict['EdgStff'] - for BlFracti, StrcTwsti, BMassDeni, FlpStffi, EdgStffi in zip(BlFract, StrcTwst, BMassDen, FlpStff, EdgStff): - f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format(BlFracti, StrcTwsti, BMassDeni, FlpStffi, EdgStffi)) - f.write('---------------------- BLADE MODE SHAPES ---------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl1Sh'][0], 'BldFl1Sh(2)', '- Flap mode 1, coeff of x^2\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl1Sh'][1], 'BldFl1Sh(3)', '- , coeff of x^3\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl1Sh'][2], 'BldFl1Sh(4)', '- , coeff of x^4\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl1Sh'][3], 'BldFl1Sh(5)', '- , coeff of x^5\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl1Sh'][4], 'BldFl1Sh(6)', '- , coeff of x^6\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl2Sh'][0], 'BldFl2Sh(2)', '- Flap mode 2, coeff of x^2\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl2Sh'][1], 'BldFl2Sh(3)', '- , coeff of x^3\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl2Sh'][2], 'BldFl2Sh(4)', '- , coeff of x^4\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl2Sh'][3], 'BldFl2Sh(5)', '- , coeff of x^5\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldFl2Sh'][4], 'BldFl2Sh(6)', '- , coeff of x^6\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdgSh'][0], 'BldEdgSh(2)', '- Edge mode 1, coeff of x^2\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdgSh'][1], 'BldEdgSh(3)', '- , coeff of x^3\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdgSh'][2], 'BldEdgSh(4)', '- , coeff of x^4\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdgSh'][3], 'BldEdgSh(5)', '- , coeff of x^5\n')) - f.write('{:<22} {:<11} {:}'.format(EDbld_dict['BldEdgSh'][4], 'BldEdgSh(6)', '- , coeff of x^6\n')) - - f.flush() - os.fsync(f) - f.close() - - def write_ElastoDynTower(self): - - self.fst_vt['ElastoDyn']['TwrFile'] = self.FAST_namingOut + '_ElastoDyn_tower.dat' - tower_file = os.path.join(self.FAST_runDirectory,self.fst_vt['ElastoDyn']['TwrFile']) - f = open(tower_file, 'w') - - f.write('------- ELASTODYN TOWER INPUT FILE -------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- TOWER PARAMETERS ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['NTwInpSt'], 'NTwInpSt', '- Number of input stations to specify tower geometry\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwrFADmp1'], 'TwrFADmp(1)', '- Tower 1st fore-aft mode structural damping ratio (%)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwrFADmp2'], 'TwrFADmp(2)', '- Tower 2nd fore-aft mode structural damping ratio (%)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwrSSDmp1'], 'TwrSSDmp(1)', '- Tower 1st side-to-side mode structural damping ratio (%)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwrSSDmp2'], 'TwrSSDmp(2)', '- Tower 2nd side-to-side mode structural damping ratio (%)\n')) - f.write('---------------------- TOWER ADJUSTMUNT FACTORS --------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['FAStTunr1'], 'FAStTunr(1)', '- Tower fore-aft modal stiffness tuner, 1st mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['FAStTunr2'], 'FAStTunr(2)', '- Tower fore-aft modal stiffness tuner, 2nd mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['SSStTunr1'], 'SSStTunr(1)', '- Tower side-to-side stiffness tuner, 1st mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['SSStTunr2'], 'SSStTunr(2)', '- Tower side-to-side stiffness tuner, 2nd mode (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['AdjTwMa'], 'AdjTwMa', '- Factor to adjust tower mass density (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['AdjFASt'], 'AdjFASt', '- Factor to adjust tower fore-aft stiffness (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['AdjSSSt'], 'AdjSSSt', '- Factor to adjust tower side-to-side stiffness (-)\n')) - f.write('---------------------- DISTRIBUTED TOWER PROPERTIES ----------------------------\n') - f.write(' HtFract TMassDen TwFAStif TwSSStif\n') - f.write(' (-) (kg/m) (Nm^2) (Nm^2)\n') - HtFract = self.fst_vt['ElastoDynTower']['HtFract'] - TMassDen = self.fst_vt['ElastoDynTower']['TMassDen'] - TwFAStif = self.fst_vt['ElastoDynTower']['TwFAStif'] - TwSSStif = self.fst_vt['ElastoDynTower']['TwSSStif'] - for HtFracti, TMassDeni, TwFAStifi, TwSSStifi in zip(HtFract, TMassDen, TwFAStif, TwSSStif): - f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format(HtFracti, TMassDeni, TwFAStifi, TwSSStifi)) - f.write('---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM1Sh'][0], 'TwFAM1Sh(2)', '- Mode 1, coefficient of x^2 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM1Sh'][1], 'TwFAM1Sh(3)', '- , coefficient of x^3 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM1Sh'][2], 'TwFAM1Sh(4)', '- , coefficient of x^4 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM1Sh'][3], 'TwFAM1Sh(5)', '- , coefficient of x^5 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM1Sh'][4], 'TwFAM1Sh(6)', '- , coefficient of x^6 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM2Sh'][0], 'TwFAM2Sh(2)', '- Mode 2, coefficient of x^2 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM2Sh'][1], 'TwFAM2Sh(3)', '- , coefficient of x^3 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM2Sh'][2], 'TwFAM2Sh(4)', '- , coefficient of x^4 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM2Sh'][3], 'TwFAM2Sh(5)', '- , coefficient of x^5 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwFAM2Sh'][4], 'TwFAM2Sh(6)', '- , coefficient of x^6 term\n')) - f.write('---------------------- TOWER SIDE-TO-SIDE MODE SHAPES --------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM1Sh'][0], 'TwSSM1Sh(2)', '- Mode 1, coefficient of x^2 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM1Sh'][1], 'TwSSM1Sh(3)', '- , coefficient of x^3 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM1Sh'][2], 'TwSSM1Sh(4)', '- , coefficient of x^4 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM1Sh'][3], 'TwSSM1Sh(5)', '- , coefficient of x^5 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM1Sh'][4], 'TwSSM1Sh(6)', '- , coefficient of x^6 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM2Sh'][0], 'TwSSM2Sh(2)', '- Mode 2, coefficient of x^2 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM2Sh'][1], 'TwSSM2Sh(3)', '- , coefficient of x^3 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM2Sh'][2], 'TwSSM2Sh(4)', '- , coefficient of x^4 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM2Sh'][3], 'TwSSM2Sh(5)', '- , coefficient of x^5 term\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ElastoDynTower']['TwSSM2Sh'][4], 'TwSSM2Sh(6)', '- , coefficient of x^6 term\n')) - - f.flush() - os.fsync(f) - f.close() - - def write_BeamDyn(self, bldInd = None): - - # if we have bldInd is None, - if bldInd is None: - - self.fst_vt['BeamDyn']['BldFile'] = self.FAST_namingOut + '_BeamDyn_Blade.dat' - bd_dict = self.fst_vt['BeamDyn'] - beamdyn_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['BDBldFile(1)']) - else: - self.fst_vt['BeamDyn'][bldInd]['BldFile'] = self.FAST_namingOut + '_BeamDyn_Blade_%d.dat'%(bldInd+1) - bd_dict = self.fst_vt['BeamDyn'][bldInd] - beamdyn_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['BDBldFile(%s)'%(bldInd+1)]) - - - self.write_BeamDynBlade(bldInd = bldInd) - - f = open(beamdyn_file, 'w') - - f.write('--------- BEAMDYN with OpenFAST INPUT FILE -------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['QuasiStaticInit'], 'QuasiStaticInit', '- Use quasistatic pre-conditioning with centripetal accelerations in initialization (flag) [dynamic solve only]\n')) - f.write('{:<22} {:<11} {:}'.format(bd_dict['rhoinf'], 'rhoinf', '- Numerical damping parameter for generalized-alpha integrator\n')) - f.write('{:<22d} {:<11} {:}'.format(bd_dict['quadrature'], 'quadrature', '- Quadrature method: 1=Gaussian; 2=Trapezoidal (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(bd_dict['refine'], 'refine', '- Refinement factor for trapezoidal quadrature (-) [DEFAULT = 1; used only when quadrature=2]\n')) - f.write('{:<22} {:<11} {:}'.format(bd_dict['n_fact'], 'n_fact', '- Factorization frequency for the Jacobian in N-R iteration(-) [DEFAULT = 5]\n')) - f.write(float_default_out(bd_dict['DTBeam']) + ' {:<11} {:}'.format('DTBeam', '- Time step size (s).\n')) - f.write(int_default_out(bd_dict['load_retries']) + ' {:<11} {:}'.format('load_retries', '- Number of factored load retries before quitting the aimulation [DEFAULT = 20]\n')) - f.write(int_default_out(bd_dict['NRMax']) + ' {:<11} {:}'.format('NRMax', '- Max number of iterations in Newton-Raphson algorithm (-). [DEFAULT = 10]\n')) - f.write(float_default_out(bd_dict['stop_tol']) + ' {:<11} {:}'.format('stop_tol', '- Tolerance for stopping criterion (-) [DEFAULT = 1E-5]\n')) - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['tngt_stf_fd'], 'tngt_stf_fd', '- Use finite differenced tangent stiffness matrix? (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['tngt_stf_comp'], 'tngt_stf_comp', '- Compare analytical finite differenced tangent stiffness matrix? (flag)\n')) - f.write(float_default_out(bd_dict['tngt_stf_pert']) + ' {:<11} {:}'.format('tngt_stf_pert', '- Perturbation size for finite differencing (-) [DEFAULT = 1E-6]\n')) - f.write(float_default_out(bd_dict['tngt_stf_difftol']) + ' {:<11} {:}'.format('tngt_stf_difftol', '- Maximum allowable relative difference between analytical and fd tangent stiffness (-); [DEFAULT = 0.1]\n')) - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['RotStates'], 'RotStates', '- Orient states in the rotating frame during linearization? (flag) [used only when linearizing]\n')) - f.write('---------------------- GEOMETRY PARAMETER --------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(bd_dict['member_total'], 'member_total', '- Total number of members (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(bd_dict['kp_total'], 'kp_total', '- Total number of key points (-) [must be at least 3]\n')) - for i in range(bd_dict['member_total']): - mem = bd_dict['members'][i] - f.write('{:<22} {:<11} {:}'.format(' '.join(['%d'%(i+1),'%d'%len(mem['kp_xr'])]), '', '- Member number; Number of key points in this member\n')) - f.write(" ".join(['{:^21s}'.format(i) for i in ['kp_xr', 'kp_yr', 'kp_zr', 'initial_twist']])+'\n') - f.write(" ".join(['{:^21s}'.format(i) for i in ['(m)', '(m)', '(m)', '(deg)']])+'\n') - for j in range(len(mem['kp_xr'])): - ln = [] - ln.append('{: 2.14e}'.format(mem['kp_xr'][j])) - ln.append('{: 2.14e}'.format(mem['kp_yr'][j])) - ln.append('{: 2.14e}'.format(mem['kp_zr'][j])) - ln.append('{: 2.14e}'.format(mem['initial_twist'][j])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- MESH PARAMETER ------------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(bd_dict['order_elem'], 'order_elem', '- Order of interpolation (basis) function (-)\n')) - f.write('---------------------- MATERIAL PARAMETER --------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format('"'+bd_dict['BldFile']+'"', 'BldFile', '- Name of file containing properties for blade (quoted string)\n')) - f.write('---------------------- OUTPUTS -------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['SumPrint'], 'SumPrint', '- Print summary data to ".sum" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+bd_dict['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output, excluding the time channel.\n')) - f.write('{:<22} {:<11} {:}'.format(bd_dict['NNodeOuts'], 'NNodeOuts', '- Number of nodes to output to file [0 - 9] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(bd_dict['OutNd']), 'OutNd', '- Nodes whose values will be output (-)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['BeamDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - - # Optional nodal output section - if 'BldNd_BlOutNd' in bd_dict: - f.write('====== Outputs for all blade stations (same ending as above for B1N1.... =========================== [optional section]\n') - # f.write('{:<22d} {:<11} {:}'.format(bd_dict['BldNd_BladesOut'], 'BldNd_BladesOut', '- Number of blades to output all node information at. Up to number of blades on turbine. (-)\n')) - f.write('{!s:<22} {:<11} {:}'.format(bd_dict['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output. Not implemented yet. (-)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn_Nodes tab for a listing of available output channels, (-)\n') - - opt_outlist = self.get_outlist(self.fst_vt['outlist'], ['BeamDyn_Nodes']) - for opt_channel_list in opt_outlist: - for i in range(len(opt_channel_list)): - f.write('"' + opt_channel_list[i] + '"\n') - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - - f.write('---------------------------------------------------------------------------------------') - f.flush() - os.fsync(f) - f.close() - - def write_BeamDynBlade(self, bldInd = None): - - if bldInd is None: - bd_blade_dict = self.fst_vt['BeamDynBlade'] - bd_blade_file = os.path.abspath(os.path.join(self.FAST_runDirectory, self.fst_vt['BeamDyn']['BldFile'])) - else: - bd_blade_dict = self.fst_vt['BeamDynBlade'][bldInd] - bd_blade_file = os.path.abspath(os.path.join(self.FAST_runDirectory, self.fst_vt['BeamDyn'][bldInd]['BldFile'])) - - f = open(bd_blade_file, 'w') - - f.write('------- BEAMDYN INDIVIDUAL BLADE INPUT FILE --------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('------ Blade Parameters --------------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(bd_blade_dict['station_total'], 'station_total', '- Number of blade input stations (-)\n')) - f.write('{:<22} {:<11} {:}'.format(bd_blade_dict['damp_type'], 'damp_type', '- Damping type (switch) {0: none, 1: stiffness-proportional, 2: modal}\n')) - f.write('------ Stiffness-Proportional Damping [used only if damp_type=1] ---------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['mu1','mu2','mu3','mu4','mu5','mu6']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(-)','(-)','(-)','(-)','(-)']])+'\n') - mu = [bd_blade_dict['mu1'], bd_blade_dict['mu2'], bd_blade_dict['mu3'], bd_blade_dict['mu4'], bd_blade_dict['mu5'], bd_blade_dict['mu6']] - f.write(" ".join(['{:^11f}'.format(i) for i in mu])+'\n') - f.write('------ Modal Damping [used only if damp_type=2] --------------------------------\n') - f.write('{:<22} {:<11} {:}\n'.format(bd_blade_dict['n_modes'], 'n_modes', '- Number of modal damping coefficients (-)')) - f.write('{:<22} {:<11} {:}\n'.format(" ".join([repr(v) for v in bd_blade_dict['zeta']]), 'zeta', ' - Damping coefficients for mode 1 through n_modes')) - f.write('------ Distributed Properties --------------------------------------------------\n') - for i in range(len(bd_blade_dict['radial_stations'])): - f.write('{: 2.15e}\n'.format(bd_blade_dict['radial_stations'][i])) - for j in range(6): - f.write(" ".join(['{: 2.15e}'.format(i) for i in bd_blade_dict['beam_stiff'][i,j,:]])+'\n') - f.write('\n') - for j in range(6): - f.write(" ".join(['{: 2.15e}'.format(i) for i in bd_blade_dict['beam_inertia'][i,j,:]])+'\n') - f.write('\n') - - f.write('\n') - f.flush() - os.fsync(f) - f.close() - - def write_InflowWind(self): - self.fst_vt['Fst']['InflowFile'] = self.FAST_namingOut + '_InflowWind.dat' - inflow_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['InflowFile']) - f = open(inflow_file, 'w') - - f.write('------- InflowWind INPUT FILE -------------------------------------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------------------------------------------------------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['WindType'], 'WindType', '- switch for wind file type (1=steady; 2=uniform; 3=binary TurbSim FF; 4=binary Bladed-style FF; 5=HAWC format; 6=User defined; 7=native Bladed FF)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['PropagationDir'], 'PropagationDir', '- Direction of wind propagation (meteoroligical rotation from aligned with X (positive rotates towards -Y) -- degrees) (not used for native Bladed format WindType=7)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['VFlowAng'], 'VFlowAng', '- Upflow angle (degrees) (not used for native Bladed format WindType=7)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['VelInterpCubic'], 'VelInterpCubic', '- Use cubic interpolation for velocity in time (false=linear, true=cubic) [Used with WindType=2,3,4,5,7]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['NWindVel'], 'NWindVel', '- Number of points to output the wind velocity (0 to 9)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['WindVxiList'], dtype=str)), 'WindVxiList', '- List of coordinates in the inertial X direction (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['WindVyiList'], dtype=str)), 'WindVyiList', '- List of coordinates in the inertial Y direction (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['WindVziList'], dtype=str)), 'WindVziList', '- List of coordinates in the inertial Z direction (m)\n')) - f.write('================== Parameters for Steady Wind Conditions [used only for WindType = 1] =========================\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['HWindSpeed'], 'HWindSpeed', '- Horizontal wind speed (m/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['RefHt'], 'RefHt', '- Reference height for horizontal wind speed (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['PLExp'], 'PLExp', '- Power law exponent (-)\n')) - f.write('================== Parameters for Uniform wind file [used only for WindType = 2] ============================\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileName_Uni']+'"', 'FileName_Uni', '- Filename of time series data for uniform wind field. (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['RefHt_Uni'], 'RefHt_Uni', '- Reference height for horizontal wind speed (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['RefLength'], 'RefLength', '- Reference length for linear horizontal and vertical sheer (-)\n')) - f.write('================== Parameters for Binary TurbSim Full-Field files [used only for WindType = 3] ==============\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileName_BTS']+'"', 'FileName_BTS', '- Name of the Full field wind file to use (.bts)\n')) - f.write('================== Parameters for Binary Bladed-style Full-Field files [used only for WindType = 4 or WindType = 7] =========\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileNameRoot']+'"', 'FileNameRoot', '- WindType=4: Rootname of the full-field wind file to use (.wnd, .sum); WindType=7: name of the intermediate file with wind scaling values\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['TowerFile'], 'TowerFile', '- Have tower file (.twr) (flag) ignored when WindType = 7\n')) - f.write('================== Parameters for HAWC-format binary files [Only used with WindType = 5] =====================\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileName_u']+'"', 'FileName_u', '- name of the file containing the u-component fluctuating wind (.bin)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileName_v']+'"', 'FileName_v', '- name of the file containing the v-component fluctuating wind (.bin)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['InflowWind']['FileName_w']+'"', 'FileName_w', '- name of the file containing the w-component fluctuating wind (.bin)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['nx'], 'nx', '- number of grids in the x direction (in the 3 files above) (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['ny'], 'ny', '- number of grids in the y direction (in the 3 files above) (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['nz'], 'nz', '- number of grids in the z direction (in the 3 files above) (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['dx'], 'dx', '- distance (in meters) between points in the x direction (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['dy'], 'dy', '- distance (in meters) between points in the y direction (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['dz'], 'dz', '- distance (in meters) between points in the z direction (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['RefHt_Hawc'], 'RefHt_Hawc', '- reference height; the height (in meters) of the vertical center of the grid (m)\n')) - f.write('------------- Scaling parameters for turbulence ---------------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['ScaleMethod'], 'ScaleMethod', '- Turbulence scaling method [0 = none, 1 = direct scaling, 2 = calculate scaling factor based on a desired standard deviation]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SFx'], 'SFx', '- Turbulence scaling factor for the x direction (-) [ScaleMethod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SFy'], 'SFy', '- Turbulence scaling factor for the y direction (-) [ScaleMethod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SFz'], 'SFz', '- Turbulence scaling factor for the z direction (-) [ScaleMethod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SigmaFx'], 'SigmaFx', '- Turbulence standard deviation to calculate scaling from in x direction (m/s) [ScaleMethod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SigmaFy'], 'SigmaFy', '- Turbulence standard deviation to calculate scaling from in y direction (m/s) [ScaleMethod=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SigmaFz'], 'SigmaFz', '- Turbulence standard deviation to calculate scaling from in z direction (m/s) [ScaleMethod=2]\n')) - f.write('------------- Mean wind profile parameters (added to HAWC-format files) ---------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['URef'], 'URef', '- Mean u-component wind speed at the reference height (m/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['WindProfile'], 'WindProfile', '- Wind profile type (0=constant;1=logarithmic,2=power law)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['PLExp_Hawc'], 'PLExp_Hawc', '- Power law exponent (-) (used for PL wind profile type only)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['Z0'], 'Z0', '- Surface roughness length (m) (used for LG wind profile type only)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['XOffset'], 'XOffset', '- Initial offset in +x direction (shift of wind box) (-)\n')) - f.write('------------- LIDAR Parameters --------------------------------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SensorType'], 'SensorType', '- Switch for lidar configuration (0 = None, 1 = Single Point Beam(s), 2 = Continuous, 3 = Pulsed)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['NumPulseGate'], 'NumPulseGate', '- Number of lidar measurement gates (used when SensorType = 3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['PulseSpacing'], 'PulseSpacing', '- Distance between range gates (m) (used when SensorType = 3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['NumBeam'], 'NumBeam', '- Number of lidar measurement beams (0-5)(used when SensorType = 1)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['FocalDistanceX'], dtype=str)), 'FocalDistanceX', '- Focal distance co-ordinates of the lidar beam in the x direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['FocalDistanceY'], dtype=str)), 'FocalDistanceY', '- Focal distance co-ordinates of the lidar beam in the y direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['FocalDistanceZ'], dtype=str)), 'FocalDistanceZ', '- Focal distance co-ordinates of the lidar beam in the z direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['InflowWind']['RotorApexOffsetPos'], dtype=str)), 'RotorApexOffsetPos', '- Offset of the lidar from hub height (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['URefLid'], 'URefLid', '- Reference average wind speed for the lidar[m/s]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['MeasurementInterval'], 'MeasurementInterval', '- Time between each measurement [s]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['LidRadialVel'], 'LidRadialVel', '- TRUE => return radial component, FALSE => return \'x\' direction estimate\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['ConsiderHubMotion'], 'ConsiderHubMotion', '- Flag whether to consider the hub motion\'s impact on Lidar measurements\n')) - f.write('====================== OUTPUT ==================================================\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['InflowWind']['SumPrint'], 'SumPrint', '- Print summary data to .IfW.sum (flag)\n')) - f.write('OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - - outlist = self.get_outlist(self.fst_vt['outlist'], ['InflowWind']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - f.write('---------------------------------------------------------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_AeroDyn(self): - # AeroDyn v15.03 - - # Generate AeroDyn v15 blade input file - if isinstance(self.fst_vt['AeroDynBlade'], list): - for i_adBld, adBld in enumerate(self.fst_vt['AeroDynBlade']): - self.fst_vt['AeroDyn']['ADBlFile%d'%(i_adBld+1)] = self.FAST_namingOut + '_AeroDyn_blade_%d.dat'%(i_adBld+1) - self.write_AeroDynBlade(bldInd = i_adBld) - - elif isinstance(self.fst_vt['AeroDynBlade'], dict): - self.fst_vt['AeroDyn']['ADBlFile1'] = self.FAST_namingOut + '_AeroDyn_blade.dat' - self.fst_vt['AeroDyn']['ADBlFile2'] = self.fst_vt['AeroDyn']['ADBlFile1'] - self.fst_vt['AeroDyn']['ADBlFile3'] = self.fst_vt['AeroDyn']['ADBlFile1'] - self.write_AeroDynBlade() - - - # self.write_AeroDynBlade() - - # Generate AeroDyn v15 polars - self.write_AeroDynPolar() - - # Generate AeroDyn v15 airfoil coordinates - # some polars may have airfoil coordinates, need to account for all possible scenarios - if any([self.fst_vt['AeroDyn']['af_data'][i][0]['NumCoords'] != '0' for i in range(len(self.fst_vt['AeroDyn']['af_data']))]): - af_coords = [i for i in range(len(self.fst_vt['AeroDyn']['af_data'])) if self.fst_vt['AeroDyn']['af_data'][i][0]['NumCoords'] != '0'] - self.write_AeroDynCoord(af_coords) - - if self.fst_vt['AeroDyn']['Wake_Mod'] == 3: - if self.fst_vt['AeroDyn']['UA_Mod'] > 0: - raise Exception('OLAF is called with unsteady airfoil aerodynamics, but OLAF currently only supports UA_Mod == 0') #TODO: need to check if this holds true now - self.write_OLAF() - - # Generate AeroDyn v15.03 input file - self.fst_vt['Fst']['AeroFile'] = self.FAST_namingOut + '_AeroDyn.dat' - ad_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['AeroFile']) - f = open(ad_file, 'w') - - f.write('------- AERODYN15 INPUT FILE ------------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('====== General Options ============================================================================\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['Echo'], 'Echo', '- Echo the input to ".AD.ech"? (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['DTAero'], 'DTAero', '- Time interval for aerodynamic calculations {or "default"} (s)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['Wake_Mod'], 'Wake_Mod', '- Wake/induction model (switch) {0=none, 1=BEMT, 3=OLAF} [Wake_Mod cannot be 2 or 3 when linearizing]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TwrPotent'], 'TwrPotent', '- Type tower influence on wind based on potential flow around the tower (switch) {0=none, 1=baseline potential flow, 2=potential flow with Bak correction}\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TwrShadow'], 'TwrShadow', '- Calculate tower influence on wind based on downstream tower shadow (switch) {0=none, 1=Powles model, 2=Eames model}\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TwrAero'], 'TwrAero', '- Calculate tower aerodynamic loads? (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['CavitCheck'], 'CavitCheck', '- Perform cavitation check? (flag) [UA_Mod must be 0 when CavitCheck=true]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['NacelleDrag'], 'NacelleDrag', '- Include Nacelle Drag effects? (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['CompAA'], 'CompAA', '- Flag to compute AeroAcoustics calculation [used only when Wake_Mod = 1 or 2]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['AA_InputFile'], 'AA_InputFile', '- AeroAcoustics input file [used only when CompAA=true]\n')) - f.write('====== Environmental Conditions ===================================================================\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['AirDens'], 'AirDens', '- Air density (kg/m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['KinVisc'], 'KinVisc', '- Kinematic viscosity of working fluid (m^2/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SpdSound'], 'SpdSound', '- Speed of sound in working fluid (m/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['Patm'], 'Patm', '- Atmospheric pressure (Pa) [used only when CavitCheck=True]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['Pvap'], 'Pvap', '- Vapour pressure of working fluid (Pa) [used only when CavitCheck=True]\n')) - f.write('====== Blade-Element/Momentum Theory Options ====================================================== [unused when Wake_Mod=0 or 3, except for BEM_Mod]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['BEM_Mod'], 'BEM_Mod', '- BEM model {1=legacy NoSweepPitchTwist, 2=polar} (switch) [used for all Wake_Mod to determine output coordinate system]\n')) - f.write('--- Skew correction\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['Skew_Mod'], 'Skew_Mod', '- Skew model {0=No skew model, -1=Remove non-normal component for linearization, 1=skew model active}\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SkewMomCorr'], 'SkewMomCorr', '- Turn the skew momentum correction on or off [used only when Skew_Mod=1]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SkewRedistr_Mod'], 'SkewRedistr_Mod', '- Type of skewed-wake correction model (switch) {0=no redistribution, 1=Glauert/Pitt/Peters, default=1} [used only when Skew_Mod=1]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SkewRedistrFactor'], 'SkewRedistrFactor', '- Constant used in Pitt/Peters skewed wake model {or "default" is 15/32*pi} (-) [used only when Skew_Mod=1 and SkewRedistr_Mod=1]\n')) - f.write('--- BEM algorithm\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TipLoss'], 'TipLoss', '- Use the Prandtl tip-loss model? (flag) [unused when Wake_Mod=0 or 3]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['HubLoss'], 'HubLoss', '- Use the Prandtl hub-loss model? (flag) [unused when Wake_Mod=0 or 3]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TanInd'], 'TanInd', '- Include tangential induction in BEMT calculations? (flag) [unused when Wake_Mod=0 or 3]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['AIDrag'], 'AIDrag', '- Include the drag term in the axial-induction calculation? (flag) [unused when Wake_Mod=0 or 3]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TIDrag'], 'TIDrag', '- Include the drag term in the tangential-induction calculation? (flag) [unused when Wake_Mod=0,3 or TanInd=FALSE]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['IndToler'], 'IndToler', '- Convergence tolerance for BEMT nonlinear solve residual equation {or "default"} (-) [unused when Wake_Mod=0 or 3]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['MaxIter'], 'MaxIter', '- Maximum number of iteration steps (-) [unused when Wake_Mod=0]\n')) - f.write('--- Shear correction\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SectAvg'], 'SectAvg', '- Use sector averaging (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SectAvgWeighting'], 'SectAvgWeighting', '- Weighting function for sector average {1=Uniform, default=1} within a sector centered on the blade (switch) [used only when SectAvg=True]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SectAvgNPoints'], 'SectAvgNPoints', '- Number of points per sectors (-) {default=5} [used only when SectAvg=True]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SectAvgPsiBwd'], 'SectAvgPsiBwd', '- Backward azimuth relative to blade where the sector starts (<=0) {default=-60} (deg) [used only when SectAvg=True]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SectAvgPsiFwd'], 'SectAvgPsiFwd', '- Forward azimuth relative to blade where the sector ends (>=0) {default=60} (deg) [used only when SectAvg=True]\n')) - - f.write('--- Dynamic wake/inflow\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['DBEMT_Mod'], 'DBEMT_Mod', '- Type of dynamic BEMT (DBEMT) model {0=No Dynamic Wake, -1=Frozen Wake for linearization, 1:constant tau1, 2=time-dependent tau1, 3=constant tau1 with continuous formulation} (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['tau1_const'], 'tau1_const', '- Time constant for DBEMT (s) [used only when DBEMT_Mod=1 or 3]\n')) - f.write('====== OLAF -- cOnvecting LAgrangian Filaments (Free Vortex Wake) Theory Options ================== [used only when Wake_Mod=3]\n') - olaf_file = self.FAST_namingOut + '_OLAF.dat' - f.write('{!s:<22} {:<11} {:}'.format(olaf_file, 'OLAFInputFileName', '- Input file for OLAF [used only when Wake_Mod=3]\n')) - f.write('====== Unsteady Airfoil Aerodynamics Options ===================================== \n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['AoA34'], 'AoA34', "- Sample the angle of attack (AoA) at the 3/4 chord or the AC point {default=True} [always used]\n")) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['UA_Mod'], 'UA_Mod', "- Unsteady Aero Model Switch (switch) {0=Quasi-steady (no UA), 2=B-L Gonzalez, 3=B-L Minnema/Pierce, 4=B-L HGM 4-states, 5=B-L HGM+vortex 5 states, 6=Oye, 7=Boeing-Vertol}\n")) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['FLookup'], 'FLookup', "- Flag to indicate whether a lookup for f' will be calculated (TRUE) or whether best-fit exponential equations will be used (FALSE); if FALSE S1-S4 must be provided in airfoil input files (flag) [used only when UA_Mod>0]\n")) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['IntegrationMethod'], 'IntegrationMethod', "- Switch to indicate which integration method UA uses (1=RK4, 2=AB4, 3=ABM4, 4=BDF2)\n")) - if 'UAStartRad' in self.fst_vt['AeroDyn'] and 'UAEndRad' in self.fst_vt['AeroDyn']: - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['UAStartRad'], 'UAStartRad', '- Starting radius for dynamic stall (fraction of rotor radius [0.0,1.0]) [used only when UA_Mod>0; if line is missing UAStartRad=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['UAEndRad'], 'UAEndRad', '- Ending radius for dynamic stall (fraction of rotor radius [0.0,1.0]) [used only when UA_Mod>0; if line is missing UAEndRad=1]\n')) - f.write('====== Airfoil Information =========================================================================\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['AFTabMod'], 'AFTabMod', '- Interpolation method for multiple airfoil tables {1=1D interpolation on AoA (first table only); 2=2D interpolation on AoA and Re; 3=2D interpolation on AoA and UserProp} (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['InCol_Alfa'], 'InCol_Alfa', '- The column in the airfoil tables that contains the angle of attack (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['InCol_Cl'], 'InCol_Cl', '- The column in the airfoil tables that contains the lift coefficient (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['InCol_Cd'], 'InCol_Cd', '- The column in the airfoil tables that contains the drag coefficient (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['InCol_Cm'], 'InCol_Cm', '- The column in the airfoil tables that contains the pitching-moment coefficient; use zero if there is no Cm column (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['InCol_Cpmin'], 'InCol_Cpmin', '- The column in the airfoil tables that contains the Cpmin coefficient; use zero if there is no Cpmin column (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['NumAFfiles'], 'NumAFfiles', '- Number of airfoil files used (-)\n')) - for i in range(self.fst_vt['AeroDyn']['NumAFfiles']): - if i == 0: - f.write('"' + self.fst_vt['AeroDyn']['AFNames'][i] + '" AFNames - Airfoil file names (NumAFfiles lines) (quoted strings)\n') - else: - f.write('"' + self.fst_vt['AeroDyn']['AFNames'][i] + '"\n') - f.write('====== Rotor/Blade Properties =====================================================================\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['UseBlCm'], 'UseBlCm', '- Include aerodynamic pitching moment in calculations? (flag)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['AeroDyn']['ADBlFile1']+'"', 'ADBlFile(1)', '- Name of file containing distributed aerodynamic properties for Blade #1 (-)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['AeroDyn']['ADBlFile2']+'"', 'ADBlFile(2)', '- Name of file containing distributed aerodynamic properties for Blade #2 (-) [unused if NumBl < 2]\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['AeroDyn']['ADBlFile3']+'"', 'ADBlFile(3)', '- Name of file containing distributed aerodynamic properties for Blade #3 (-) [unused if NumBl < 3]\n')) - f.write('====== Hub Properties ============================================================================== [used only when MHK=1 or 2]\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['VolHub'], 'VolHub', '- Hub volume (m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['HubCenBx'], 'HubCenBx', '- Hub center of buoyancy x direction offset (m)\n')) - f.write('====== Nacelle Properties ========================================================================== [used only when MHK=1 or 2 or when NacelleDrag=True]\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['VolNac'], 'VolNac', '- Nacelle volume (m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['AeroDyn']['NacCenB'], dtype=str)), 'NacCenB', '- Position of nacelle center of buoyancy from yaw bearing in nacelle coordinates (m)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['AeroDyn']['NacArea'], dtype=str)), 'NacArea', '- Projected area of the nacelle in X, Y, Z in the nacelle coordinate system (m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['AeroDyn']['NacCd'], dtype=str)), 'NacCd', '- Drag coefficient for the nacelle areas defined above (-)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['AeroDyn']['NacDragAC'], dtype=str)), 'NacDragAC', '- Position of aerodynamic center of nacelle drag in nacelle coordinates (m)\n')) - f.write('====== Tail Fin Aerodynamics ========================================================================\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['TFinAero'], 'TFinAero', '- Calculate tail fin aerodynamics model (flag)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['AeroDyn']['TFinFile']+'"', 'TFinFile', '- Input file for tail fin aerodynamics [used only when TFinAero=True]\n')) - f.write('====== Tower Influence and Aerodynamics ============================================================ [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or MHK=1 or 2]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['NumTwrNds'], 'NumTwrNds', '- Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or MHK=1 or 2]\n')) - f.write('TwrElev TwrDiam TwrCd TwrTI TwrCb TwrCp TwrCa !TwrTI used only with TwrShadow=2, TwrCb/TwrCp/TwrCa used only with MHK=1 or 2\n') - f.write('(m) (m) (-) (-) (-) (-) (-)\n') - for TwrElev, TwrDiam, TwrCd, TwrTI, TwrCb, TwrCp, TwrCa in zip(self.fst_vt['AeroDyn']['TwrElev'], self.fst_vt['AeroDyn']['TwrDiam'], self.fst_vt['AeroDyn']['TwrCd'], self.fst_vt['AeroDyn']['TwrTI'], self.fst_vt['AeroDyn']['TwrCb'], self.fst_vt['AeroDyn']['TwrCp'], self.fst_vt['AeroDyn']['TwrCa']): - f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} \n'.format(TwrElev, TwrDiam, TwrCd, TwrTI, TwrCb, TwrCp, TwrCa)) - f.write('====== Outputs ====================================================================================\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['SumPrint'], 'SumPrint', '- Generate a summary file listing input options and interpolated properties to ".AD.sum"? (flag)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['NBlOuts'], 'NBlOuts', '- Number of blade node outputs [0 - 9] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['AeroDyn']['BlOutNd']), 'BlOutNd', '- Blade nodes whose values will be output (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['NTwOuts'], 'NTwOuts', '- Number of tower node outputs [0 - 9] (-)\n')) - # if self.fst_vt['AeroDyn']['NTwOuts'] != 0: # TODO its weird that tower nodes is treated differently than blade nodes - # f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['AeroDyn']['TwOutNd']), 'TwOutNd', '- Tower nodes whose values will be output (-)\n')) - # else: - # f.write('{:<22} {:<11} {:}'.format(0, 'TwOutNd', '- Tower nodes whose values will be output (-)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(self.fst_vt['AeroDyn']['TwOutNd'], dtype=str)), 'TwOutNd', '- Tower nodes whose values will be output (-)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - - outlist = self.get_outlist(self.fst_vt['outlist'], ['AeroDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - - # Optional nodal output section - if 'BldNd_BladesOut' in self.fst_vt['AeroDyn']: - f.write('====== Outputs for all blade stations (same ending as above for B1N1.... =========================== [optional section]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['BldNd_BladesOut'], 'BldNd_BladesOut', '- Number of blades to output all node information at. Up to number of blades on turbine. (-)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output. Not implemented yet. (-)\n')) - f.write(' OutList_Nodal - The next line(s) contains a list of output parameters. See OutListParameters.xlsx, AeroDyn_Nodes tab for a listing of available output channels, (-)\n') - - opt_outlist = self.get_outlist(self.fst_vt['outlist'], ['AeroDyn_Nodes']) - for opt_channel_list in opt_outlist: - for i in range(len(opt_channel_list)): - f.write('"' + opt_channel_list[i] + '"\n') - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - - f.write('---------------------------------------------------------------------------------------\n') - f.flush() - os.fsync(f) - f.close() - - def write_AeroDynBlade(self, bldInd = None): - - if bldInd is None: - filename = os.path.join(self.FAST_runDirectory, self.fst_vt['AeroDyn']['ADBlFile1']) - adBld_dict = self.fst_vt['AeroDynBlade'] - else: - filename = os.path.join(self.FAST_runDirectory, self.fst_vt['AeroDyn']['ADBlFile%d'%(bldInd+1)]) - adBld_dict = self.fst_vt['AeroDynBlade'][bldInd] - - f = open(filename, 'w') - - f.write('------- AERODYN15 BLADE DEFINITION INPUT FILE -------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('====== Blade Properties =================================================================\n') - f.write('{:<11d} {:<11} {:}'.format(adBld_dict['NumBlNds'], 'NumBlNds', '- Number of blade nodes used in the analysis (-)\n')) - f.write(' BlSpn BlCrvAC BlSwpAC BlCrvAng BlTwist BlChord BlAFID t_c BlCb BlCenBn BlCenBt BlCpn BlCpt BlCan BlCat BlCam\n') - f.write(' (m) (m) (m) (deg) (deg) (m) (-) (-) (-) (m) (m) (-) (-) (-) (-) (-)\n') - BlSpn = adBld_dict['BlSpn'] - BlCrvAC = adBld_dict['BlCrvAC'] - BlSwpAC = adBld_dict['BlSwpAC'] - BlCrvAng = adBld_dict['BlCrvAng'] - BlTwist = adBld_dict['BlTwist'] - BlChord = adBld_dict['BlChord'] - BlAFID = adBld_dict['BlAFID'] - t_c = adBld_dict['t_c'] - BlCb = adBld_dict['BlCb'] - BlCenBn = adBld_dict['BlCenBn'] - BlCenBt = adBld_dict['BlCenBt'] - BlCpn = adBld_dict['BlCpn'] - BlCpt = adBld_dict['BlCpt'] - BlCan = adBld_dict['BlCan'] - BlCat = adBld_dict['BlCat'] - BlCam = adBld_dict['BlCam'] - for Spn, CrvAC, SwpAC, CrvAng, Twist, Chord, AFID, t_c, BlCb, BlCenBn, BlCenBt, BlCpn, BlCpt, BlCan, BlCat, BlCam in zip(BlSpn, BlCrvAC, BlSwpAC, BlCrvAng, BlTwist, BlChord, BlAFID, t_c, BlCb, BlCenBn, BlCenBt, BlCpn, BlCpt, BlCan, BlCat, BlCam): - f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 8d} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format(Spn, CrvAC, SwpAC, CrvAng, Twist, Chord, int(AFID), t_c, BlCb, BlCenBn, BlCenBt, BlCpn, BlCpt, BlCan, BlCat, BlCam)) - - f.flush() - os.fsync(f) - f.close() - - def write_AeroDynPolar(self): - # Airfoil Info v1.01 - - if not os.path.isdir(os.path.join(self.FAST_runDirectory,'Airfoils')): - try: - os.makedirs(os.path.join(self.FAST_runDirectory,'Airfoils')) - except: - try: - time.sleep(random.random()) - if not os.path.isdir(os.path.join(self.FAST_runDirectory,'Airfoils')): - os.makedirs(os.path.join(self.FAST_runDirectory,'Airfoils')) - except: - print("Error tring to make '%s'!"%os.path.join(self.FAST_runDirectory,'Airfoils')) - - - self.fst_vt['AeroDyn']['NumAFfiles'] = len(self.fst_vt['AeroDyn']['af_data']) - self.fst_vt['AeroDyn']['AFNames'] = ['']*self.fst_vt['AeroDyn']['NumAFfiles'] - - for afi in range(int(self.fst_vt['AeroDyn']['NumAFfiles'])): - - self.fst_vt['AeroDyn']['AFNames'][afi] = os.path.join('Airfoils', self.FAST_namingOut + '_AeroDyn_Polar_%02d.dat'%afi) - af_file = os.path.join(self.FAST_runDirectory, self.fst_vt['AeroDyn']['AFNames'][afi]) - f = open(af_file, 'w') - - f.write('! ------------ AirfoilInfo Input File ----------------------------------\n') - f.write('! Generated with OpenFAST_IO\n') - f.write('! line\n') - f.write('! line\n') - f.write('! ------------------------------------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][0]['InterpOrd'], 'InterpOrd', '! Interpolation order to use for quasi-steady table lookup {1=linear; 3=cubic spline; "default"} [default=3]\n')) - if 'RelThickness' in self.fst_vt['AeroDyn']['af_data'][afi][0]: - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][0]['RelThickness'], 'RelThickness', '! The non-dimensional thickness of the airfoil (thickness/chord) [only used if UAMod=7] [default=0.2] (-)\n')) - - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][0]['NonDimArea'], 'NonDimArea', '! The non-dimensional area of the airfoil (area/chord^2) (set to 1.0 if unsure or unneeded)\n')) - if self.fst_vt['AeroDyn']['af_data'][afi][0]['NumCoords'] != '0': - f.write('@"{:}_AF{:02d}_Coords.txt" {:<11} {:}'.format(self.FAST_namingOut, afi, 'NumCoords', '! The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.\n')) - else: - f.write('{:<22d} {:<11} {:}'.format(0, 'NumCoords', '! The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.\n')) - f.write('AF{:02d}_BL.txt {:<11} {:}'.format(afi, 'BL_file', '! The file name including the boundary layer characteristics of the profile. Ignored if the aeroacoustic module is not called.\n')) - # f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][0]['NumTabs'], 'NumTabs', '! Number of airfoil tables in this file. Each table must have lines for Re and UserProp.\n')) - - - # Check if we have multiple tables per airfoil - # if yes, allocate the number of airfoils to the respective radial stations - if self.fst_vt['AeroDyn']['AFTabMod'] == 2: - num_tab = len(self.fst_vt['AeroDyn']['af_data'][afi]) - elif self.fst_vt['AeroDyn']['AFTabMod'] == 3: - # for tab_orig in range(self.fst_vt['AeroDyn']['af_data'][afi][0]['NumTabs'] - 1): - if len( self.fst_vt['AeroDyn']['af_data'][afi]) == 1 or \ - self.fst_vt['AeroDyn']['af_data'][afi][0]['UserProp'] == self.fst_vt['AeroDyn']['af_data'][afi][1]['UserProp']: - num_tab = 1 # assume that all UserProp angles of the flaps are identical if the first two are -> no flaps! - else: - num_tab = self.fst_vt['AeroDyn']['af_data'][afi][0]['NumTabs'] - else: - num_tab = 1 - # f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][0]['NumTabs'], 'NumTabs','! Number of airfoil tables in this file. Each table must have lines for Re and UserProp.\n')) - f.write('{:<22d} {:<11} {:}'.format(num_tab, 'NumTabs','! Number of airfoil tables in this file. Each table must have lines for Re and UserProp.\n')) - - # for tab in range(self.fst_vt['AeroDyn']['af_data'][afi][0]['NumTabs']): # For writing multiple tables (different Re or UserProp values) - for tab in range(num_tab): # For writing multiple tables (different Re or UserProp values) - f.write('! ------------------------------------------------------------------------------\n') - f.write("! data for table %i \n" % (tab + 1)) - f.write('! ------------------------------------------------------------------------------\n') - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Re']*1.e-6, 'Re', '! Reynolds number in millions\n')) - f.write('{:<22d} {:<11} {:}'.format(int(self.fst_vt['AeroDyn']['af_data'][afi][tab]['UserProp']), 'UserProp', '! User property (control) setting\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['InclUAdata'], 'InclUAdata', '! Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line\n')) - f.write('!........................................\n') - if self.fst_vt['AeroDyn']['af_data'][afi][tab]['InclUAdata']: - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['alpha0'], 'alpha0', '! 0-lift angle of attack, depends on airfoil.\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['alpha1'], 'alpha1', '! Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg)\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['alpha2'], 'alpha2', '! Angle of attack at f=0.7, (approximately the stall angle) for AOA1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['S2'], 'S2', '! Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['S3'], 'S3', '! Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['S4'], 'S4', '! Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cn1'], 'Cn1', '! Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers.\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cn2'], 'Cn2', '! As Cn1 for negative AOAs.\n')) - # f.write('{: 22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi]['St_sh'], 'St_sh', "! Strouhal's shedding frequency constant. [default = 0.19]\n")) - f.write(float_default_out(self.fst_vt['AeroDyn']['af_data'][afi][tab]['St_sh']) + ' {:<11} {:}'.format('St_sh', "! Strouhal's shedding frequency constant. [default = 0.19]\n")) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cd0'], 'Cd0', '! 2D drag coefficient value at 0-lift.\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cm0'], 'Cm0', '! 2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['k0'], 'k0', '! Constant in the \\hat(x)_cp curve best-fit; = (\\hat(x)_AC-0.25). [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['k1'], 'k1', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['k2'], 'k2', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['k3'], 'k3', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['k1_hat'], 'k1_hat', '! Constant in the expression of Cc due to leading edge vortex effects. [ignored if UA_Mod<>1]\n')) - f.write(float_default_out(self.fst_vt['AeroDyn']['af_data'][afi][tab]['x_cp_bar']) + ' {:<11} {:}'.format('x_cp_bar', '! Constant in the expression of \\hat(x)_cp^v. [ignored if UA_Mod<>1, default = 0.2]\n')) - f.write(float_default_out(self.fst_vt['AeroDyn']['af_data'][afi][tab]['UACutout']) + ' {:<11} {:}'.format('UACutout', '! Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string "Default" sets UACutout to 45 degrees]\n')) - f.write(float_default_out(self.fst_vt['AeroDyn']['af_data'][afi][tab]['filtCutOff']) + ' {:<11} {:}'.format('filtCutOff', '! Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5]\n')) - - f.write('!........................................\n') - f.write('! Table of aerodynamics coefficients\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['af_data'][afi][tab]['NumAlf'], 'NumAlf', '! Number of data lines in the following table\n')) - f.write('! Alpha Cl Cd Cm\n') - f.write('! (deg) (-) (-) (-)\n') - - polar_map = [self.fst_vt['AeroDyn']['InCol_Alfa'], self.fst_vt['AeroDyn']['InCol_Cl'], self.fst_vt['AeroDyn']['InCol_Cd'], self.fst_vt['AeroDyn']['InCol_Cm'], self.fst_vt['AeroDyn']['InCol_Cpmin']] - polar_map.remove(0) - polar_map = [i-1 for i in polar_map] - - alpha = np.asarray(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Alpha']) - cl = np.asarray(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cl']) - cd = np.asarray(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cd']) - cm = np.asarray(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cm']) - cpmin = np.asarray(self.fst_vt['AeroDyn']['af_data'][afi][tab]['Cpmin']) - - if alpha[0] != -180.: - print('Airfoil number ' + str(afi) + ' tab number ' + str(tab) + ' has the min angle of attack different than -180 deg, and equal to ' + str(alpha[0]) + ' deg. This is changed to -180 deg now.') - alpha[0] = -180. - if alpha[-1] != 180.: - print('Airfoil number ' + str(afi) + ' tab number ' + str(tab) + ' has the max angle of attack different than 180 deg, and equal to ' + str(alpha[0]) + ' deg. This is changed to 180 deg now.') - alpha[-1] = 180. - if cl[0] != cl[-1]: - print('Airfoil number ' + str(afi) + ' tab number ' + str(tab) + ' has the lift coefficient different between +-180 deg. This is changed to be the same now.') - cl[0] = cl[-1] - if cd[0] != cd[-1]: - print('Airfoil number ' + str(afi) + ' tab number ' + str(tab) + ' has the drag coefficient different between +-180 deg. This is changed to be the same now.') - cd[0] = cd[-1] - if cm[0] != cm[-1]: - print('Airfoil number ' + str(afi) + ' tab number ' + str(tab) + ' has the moment coefficient different between +-180 deg. This is changed to be the same now.') - cm[0] = cm[-1] - - if self.fst_vt['AeroDyn']['InCol_Cm'] == 0: - cm = np.zeros_like(cl) - if self.fst_vt['AeroDyn']['InCol_Cpmin'] == 0: - cpmin = np.zeros_like(cl) - polar = np.column_stack((alpha, cl, cd, cm, cpmin)) - polar = polar[:,polar_map] - - - for row in polar: - f.write(' '.join(['{: 2.14e}'.format(val) for val in row])+'\n') - - f.flush() - os.fsync(f) - f.close() - - def write_AeroDynCoord(self, af_coord): - - self.fst_vt['AeroDyn']['AFNames_coord'] = ['']*self.fst_vt['AeroDyn']['NumAFfiles'] - - for afi in af_coord: - self.fst_vt['AeroDyn']['AFNames_coord'][afi] = os.path.join('Airfoils', self.FAST_namingOut + '_AF%02d_Coords.txt'%afi) - - x = self.fst_vt['AeroDyn']['af_coord'][afi]['x'] - y = self.fst_vt['AeroDyn']['af_coord'][afi]['y'] - coord = np.vstack((x, y)).T - - af_file = os.path.join(self.FAST_runDirectory, self.fst_vt['AeroDyn']['AFNames_coord'][afi]) - f = open(af_file, 'w') - - f.write('{: 22d} {:<11} {:}'.format(len(x)+1, 'NumCoords', '! The number of coordinates in the airfoil shape file (including an extra coordinate for airfoil reference). Set to zero if coordinates not included.\n')) - f.write('! ......... x-y coordinates are next if NumCoords > 0 .............\n') - f.write('! x-y coordinate of airfoil reference\n') - f.write('! x/c y/c\n') - f.write('{: 5f} 0\n'.format(self.fst_vt['AeroDyn']['ac'][afi])) - f.write('! coordinates of airfoil shape\n') - f.write('! interpolation to 200 points\n') - f.write('! x/c y/c\n') - for row in coord: - f.write(' '.join(['{: 2.14e}'.format(val) for val in row])+'\n') - - f.flush() - os.fsync(f) - f.close() - - def write_OLAF(self): - - olaf_file = os.path.join(self.FAST_runDirectory, self.FAST_namingOut + '_OLAF.dat') - f = open(olaf_file, 'w') - - f.write('--------------------------- OLAF (cOnvecting LAgrangian Filaments) INPUT FILE -----------------\n') - f.write('Generated by OpenFAST_IO\n') - f.write('--------------------------- GENERAL OPTIONS ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['IntMethod'], 'IntMethod', '- Integration method {1: RK4, 5: Forward Euler 1st order, default: 5} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['DTfvw'], 'DTfvw', '- Time interval for wake propagation. {default: dtaero} (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['FreeWakeStart'], 'FreeWakeStart', '- Time when wake is free. (-) value = always free. {default: 0.0} (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['FullCircStart'], 'FullCircStart', '- Time at which full circulation is reached. {default: 0.0} (s)\n')) - f.write('--------------------------- CIRCULATION SPECIFICATIONS ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['CircSolvMethod'], 'CircSolvingMethod', '- Circulation solving method {1: Cl-Based, 2: No-Flow Through, 3: Prescribed, default: 1 }(switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['CircSolvConvCrit'], 'CircSolvConvCrit', ' - Convergence criteria {default: 0.001} [only if CircSolvMethod=1] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['CircSolvRelaxation'], 'CircSolvRelaxation', '- Relaxation factor {default: 0.1} [only if CircSolvMethod=1] (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['CircSolvMaxIter'], 'CircSolvMaxIter', ' - Maximum number of iterations for circulation solving {default: 30} (-)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['AeroDyn']['OLAF']['PrescribedCircFile']+'"', 'PrescribedCircFile','- File containing prescribed circulation [only if CircSolvMethod=3] (quoted string)\n')) - f.write('===============================================================================================\n') - f.write('--------------------------- WAKE OPTIONS ------------------------------------------------------\n') - f.write('------------------- WAKE EXTENT AND DISCRETIZATION --------------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nNWPanels'], 'nNWPanels','- Number of near-wake panels (-)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nNWPanelsFree'], 'nNWPanelsFree','- Number of free near-wake panels (-) {default: nNWPanels}\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nFWPanels'], 'nFWPanels','- Number of far-wake panels (-) {default: 0}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nFWPanelsFree'], 'nFWPanelsFree','- Number of free far-wake panels (-) {default: nFWPanels}\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['FWShedVorticity'], 'FWShedVorticity','- Include shed vorticity in the far wake {default: False}\n')) - f.write('------------------- WAKE REGULARIZATIONS AND DIFFUSION -----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['DiffusionMethod'], 'DiffusionMethod','- Diffusion method to account for viscous effects {0: None, 1: Core Spreading, "default": 0}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['RegDeterMethod'], 'RegDeterMethod','- Method to determine the regularization parameters {0: Manual, 1: Optimized, 2: Chord, 3: Span, default: 0 }\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['RegFunction'], 'RegFunction','- Viscous diffusion function {0: None, 1: Rankine, 2: LambOseen, 3: Vatistas, 4: Denominator, "default": 3} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['WakeRegMethod'], 'WakeRegMethod','- Wake regularization method {1: Constant, 2: Stretching, 3: Age, default: 3} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['WakeRegFactor'], 'WakeRegFactor','- Wake regularization factor (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['WingRegFactor'], 'WingRegFactor','- Wing regularization factor (m)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['CoreSpreadEddyVisc'], 'CoreSpreadEddyVisc','- Eddy viscosity in core spreading methods, typical values 1-1000\n')) - f.write('------------------- WAKE TREATMENT OPTIONS ---------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['TwrShadowOnWake'], 'TwrShadowOnWake','- Include tower flow disturbance effects on wake convection {default:false} [only if TwrPotent or TwrShadow]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['ShearModel'], 'ShearModel','- Shear Model {0: No treatment, 1: Mirrored vorticity, default: 0}\n')) - f.write('------------------- SPEEDUP OPTIONS -----------------------------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['VelocityMethod'], 'VelocityMethod','- Method to determine the velocity {1:Segment N^2, 2:Particle tree, 3:Particle N^2, 4:Segment tree, default: 2}\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['TreeBranchFactor'], 'TreeBranchFactor','- Branch radius fraction above which a multipole calculation is used {default: 1.5} [only if VelocityMethod=2,4]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['PartPerSegment'], 'PartPerSegment','- Number of particles per segment {default: 1} [only if VelocityMethod=2,3]\n')) - f.write('===============================================================================================\n') - f.write('--------------------------- OUTPUT OPTIONS ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['WrVTk'], 'WrVTk','- Outputs Visualization Toolkit (VTK) (independent of .fst option) {0: NoVTK, 1: Write VTK at VTK_fps, 2: Write VTK at init and final, default: 0} (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nVTKBlades'], 'nVTKBlades','- Number of blades for which VTK files are exported {0: No VTK per blade, n: VTK for blade 1 to n, default: 0} (-) \n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['VTKCoord'], 'VTKCoord','- Coordinate system used for VTK export. {1: Global, 2: Hub, 3: Both, default: 1} \n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['VTK_fps'], 'VTK_fps','- Frame rate for VTK output (frames per second) {"all" for all glue code timesteps, "default" for all OLAF timesteps} [only if WrVTK=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDyn']['OLAF']['nGridOut'], 'nGridOut','- Number of grid outputs\n')) - f.write('GridName GridType TStart TEnd DTGrid XStart XEnd nX YStart YEnd nY ZStart ZEnd nZ\n') - f.write('(-) (-) (s) (s) (s) (m) (m) (-) (m) (m) (-) (m) (m) (-)\n') - f.write('===============================================================================================\n') - f.write('--------------------------- ADVANCED OPTIONS --------------------------------------------------\n') - f.write('===============================================================================================\n') - - f.flush() - os.fsync(f) - f.close() - - def write_AeroDisk(self): - # Writing the aeroDisk input file - self.fst_vt['Fst']['AeroFile'] = self.FAST_namingOut + '_AeroDisk.dat' - adisk_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['AeroFile']) - f = open(adisk_file,'w') - - f.write('--- AERO DISK INPUT FILE -------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('--- SIMULATION CONTROL ---------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['AeroDisk']['Echo'], 'Echo', '- Echo input data to ".ADsk.ech" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['AeroDisk']['DT'], 'DT', '- Integration time step (s)\n')) - f.write('--- ENVIRONMENTAL CONDITIONS ---\n') - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDisk']['AirDens'], 'AirDens', '- Air density (kg/m^3) (or "default")\n')) - f.write('--- ACTUATOR DISK PROPERTIES ---\n') - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['AeroDisk']['RotorRad'], 'RotorRad', '- Rotor radius (m) (or "default")\n')) - f.write('"{:<22}" {:<11} {:}'.format(', '.join(['%s'%i for i in self.fst_vt['AeroDisk']['InColNames']]), 'InColNames', '- Input column headers (string) {may include a combination of "TSR, RtSpd, VRel, Pitch, Skew"} (up to 4 columns) [choose TSR or RtSpd,VRel; if Skew is absent, Skew is modeled as (COS(Skew))^2]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join(['%s'%i for i in self.fst_vt['AeroDisk']['InColDims']]), 'InColDims', '- Number of unique values in each column (-) (must have same number of columns as InColName) [each >=2]\n')) - self.write_AeroDiskProp() - f.write('@{:<22} {:}'.format(self.fst_vt['AeroDisk']['actuatorDiskFile'], '\n')) - f.write('--- OUTPUTS --------------------\n') - f.write('{:<22} {:<11} {:}'.format('OutList', 'OutList', '- The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n')) - - outlist = self.get_outlist(self.fst_vt['outlist'], ['AeroDisk']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - f.write('---------------------------------------------------------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_AeroDiskProp(self): - # Writing the aeroDiskProp input file - - self.fst_vt['AeroDisk']['actuatorDiskFile'] = self.FAST_namingOut + '_AeroDiskProp.csv' - adiskprop_file = os.path.join(self.FAST_runDirectory,self.fst_vt['AeroDisk']['actuatorDiskFile']) - f = open(adiskprop_file,'w') - - f.write('{:<22} {:}'.format(self.fst_vt['AeroDisk']['actuatorDiskTable']['dsc'],'\n')) - f.write('{:<22} {:}'.format(', '.join(['%s'%i for i in self.fst_vt['AeroDisk']['actuatorDiskTable']['attr']]), '\n')) - f.write('{:<22} {:}'.format(', '.join(['%s'%i for i in self.fst_vt['AeroDisk']['actuatorDiskTable']['units']]), '\n')) - for idx in range(len(self.fst_vt['AeroDisk']['actuatorDiskTable']['data'])): - f.write('{:<22} {:}'.format(', '.join(['%.6f'%i for i in self.fst_vt['AeroDisk']['actuatorDiskTable']['data'][idx]]), '\n')) - - f.flush() - os.fsync(f) - f.close() - - - - def write_ServoDyn(self): - # ServoDyn v1.05 Input File - - self.fst_vt['Fst']['ServoFile'] = self.FAST_namingOut + '_ServoDyn.dat' - sd_file = os.path.join(self.FAST_runDirectory,self.fst_vt['Fst']['ServoFile']) - f = open(sd_file,'w') - - f.write('------- SERVODYN INPUT FILE --------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['DT'], 'DT', '- Communication interval for controllers (s) (or "default")\n')) - f.write('---------------------- PITCH CONTROL -------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PCMode'], 'PCMode', '- Pitch control mode {0: none, 3: user-defined from routine PitchCntrl, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TPCOn'], 'TPCOn', '- Time to enable active pitch control (s) [unused when PCMode=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitNeut(1)'], 'PitNeut(1)', '- Blade 1 neutral pitch position--pitch spring moment is zero at this pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitNeut(2)'], 'PitNeut(2)', '- Blade 2 neutral pitch position--pitch spring moment is zero at this pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitNeut(3)'], 'PitNeut(3)', '- Blade 3 neutral pitch position--pitch spring moment is zero at this pitch (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitSpr(1)'], 'PitSpr(1)', '- Blade 1 pitch spring constant (N-m/rad)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitSpr(2)'], 'PitSpr(2)', '- Blade 2 pitch spring constant (N-m/rad)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitSpr(3)'], 'PitSpr(3)', '- Blade 3 pitch spring constant (N-m/rad)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitDamp(1)'], 'PitDamp(1)', '- Blade 1 pitch damping constant (N-m/(rad/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitDamp(2)'], 'PitDamp(2)', '- Blade 2 pitch damping constant (N-m/(rad/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitDamp(3)'], 'PitDamp(3)', '- Blade 3 pitch damping constant (N-m/(rad/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TPitManS(1)'], 'TPitManS(1)', '- Time to start override pitch maneuver for blade 1 and end standard pitch control (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TPitManS(2)'], 'TPitManS(2)', '- Time to start override pitch maneuver for blade 2 and end standard pitch control (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TPitManS(3)'], 'TPitManS(3)', '- Time to start override pitch maneuver for blade 3 and end standard pitch control (s) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitManRat(1)'], 'PitManRat(1)', '- Pitch rate at which override pitch maneuver heads toward final pitch angle for blade 1 (deg/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitManRat(2)'], 'PitManRat(2)', '- Pitch rate at which override pitch maneuver heads toward final pitch angle for blade 2 (deg/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PitManRat(3)'], 'PitManRat(3)', '- Pitch rate at which override pitch maneuver heads toward final pitch angle for blade 3 (deg/s) [unused for 2 blades]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['BlPitchF(1)'], 'BlPitchF(1)', '- Blade 1 final pitch for pitch maneuvers (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['BlPitchF(2)'], 'BlPitchF(2)', '- Blade 2 final pitch for pitch maneuvers (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['BlPitchF(3)'], 'BlPitchF(3)', '- Blade 3 final pitch for pitch maneuvers (degrees) [unused for 2 blades]\n')) - f.write('---------------------- GENERATOR AND TORQUE CONTROL ----------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['VSContrl'], 'VSContrl', '- Variable-speed control mode {0: none, 1: simple VS, 3: user-defined from routine UserVSCont, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenModel'], 'GenModel', '- Generator model {1: simple, 2: Thevenin, 3: user-defined from routine UserGen} (switch) [used only when VSContrl=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenEff'], 'GenEff', '- Generator efficiency [ignored by the Thevenin and user-defined generator models] (%)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenTiStr'], 'GenTiStr', '- Method to start the generator {T: timed using TimGenOn, F: generator speed using SpdGenOn} (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenTiStp'], 'GenTiStp', '- Method to stop the generator {T: timed using TimGenOf, F: when generator power = 0} (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SpdGenOn'], 'SpdGenOn', '- Generator speed to turn on the generator for a startup (HSS speed) (rpm) [used only when GenTiStr=False]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TimGenOn'], 'TimGenOn', '- Time to turn on the generator for a startup (s) [used only when GenTiStr=True]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TimGenOf'], 'TimGenOf', '- Time to turn off the generator (s) [used only when GenTiStp=True]\n')) - f.write('---------------------- SIMPLE VARIABLE-SPEED TORQUE CONTROL --------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['VS_RtGnSp'], 'VS_RtGnSp', '- Rated generator speed for simple variable-speed generator control (HSS side) (rpm) [used only when VSContrl=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['VS_RtTq'], 'VS_RtTq', '- Rated generator torque/constant generator torque in Region 3 for simple variable-speed generator control (HSS side) (N-m) [used only when VSContrl=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['VS_Rgn2K'], 'VS_Rgn2K', '- Generator torque constant in Region 2 for simple variable-speed generator control (HSS side) (N-m/rpm^2) [used only when VSContrl=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['VS_SlPc'], 'VS_SlPc', '- Rated generator slip percentage in Region 2 1/2 for simple variable-speed generator control (%) [used only when VSContrl=1]\n')) - f.write('---------------------- SIMPLE INDUCTION GENERATOR ------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SIG_SlPc'], 'SIG_SlPc', '- Rated generator slip percentage (%) [used only when VSContrl=0 and GenModel=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SIG_SySp'], 'SIG_SySp', '- Synchronous (zero-torque) generator speed (rpm) [used only when VSContrl=0 and GenModel=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SIG_RtTq'], 'SIG_RtTq', '- Rated torque (N-m) [used only when VSContrl=0 and GenModel=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SIG_PORt'], 'SIG_PORt', '- Pull-out ratio (Tpullout/Trated) (-) [used only when VSContrl=0 and GenModel=1]\n')) - f.write('---------------------- THEVENIN-EQUIVALENT INDUCTION GENERATOR -----------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_Freq'], 'TEC_Freq', '- Line frequency [50 or 60] (Hz) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_NPol'], 'TEC_NPol', '- Number of poles [even integer > 0] (-) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_SRes'], 'TEC_SRes', '- Stator resistance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_RRes'], 'TEC_RRes', '- Rotor resistance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_VLL'], 'TEC_VLL', '- Line-to-line RMS voltage (volts) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_SLR'], 'TEC_SLR', '- Stator leakage reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_RLR'], 'TEC_RLR', '- Rotor leakage reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TEC_MR'], 'TEC_MR', '- Magnetizing reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) - f.write('---------------------- HIGH-SPEED SHAFT BRAKE ----------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['HSSBrMode'], 'HSSBrMode', '- HSS brake model {0: none, 1: simple, 3: user-defined from routine UserHSSBr, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['THSSBrDp'], 'THSSBrDp', '- Time to initiate deployment of the HSS brake (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['HSSBrDT'], 'HSSBrDT', '- Time for HSS-brake to reach full deployment once initiated (sec) [used only when HSSBrMode=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['HSSBrTqF'], 'HSSBrTqF', '- Fully deployed HSS-brake torque (N-m)\n')) - f.write('---------------------- NACELLE-YAW CONTROL -------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['YCMode'], 'YCMode', '- Yaw control mode {0: none, 3: user-defined from routine UserYawCont, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TYCOn'], 'TYCOn', '- Time to enable active yaw control (s) [unused when YCMode=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['YawNeut'], 'YawNeut', '- Neutral yaw position--yaw spring force is zero at this yaw (degrees)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['YawSpr'], 'YawSpr', '- Nacelle-yaw spring constant (N-m/rad)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['YawDamp'], 'YawDamp', '- Nacelle-yaw damping constant (N-m/(rad/s))\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TYawManS'], 'TYawManS', '- Time to start override yaw maneuver and end standard yaw control (s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['YawManRat'], 'YawManRat', '- Yaw maneuver rate (in absolute value) (deg/s)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NacYawF'], 'NacYawF', '- Final yaw angle for override yaw maneuvers (degrees)\n')) - f.write('---------------------- Aerodynamic Flow Control -------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['AfCmode'], 'AfCmode', '- Airfoil control mode {0: none, 1: cosine wave cycle, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['AfC_Mean'], 'AfC_Mean', '- Mean level for cosine cycling or steady value (-) [used only with AfCmode==1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['AfC_Amp'], 'AfC_Amp', '- Amplitude for for cosine cycling of flap signal (AfC = AfC_Amp*cos(Azimuth+phase)+AfC_mean) (-) [used only with AfCmode==1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['AfC_Phase'], 'AfC_phase', '- Phase relative to the blade azimuth (0 is vertical) for for cosine cycling of flap signal (deg) [used only with AfCmode==1]\n')) - f.write('---------------------- STRUCTURAL CONTROL ---------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NumBStC'], 'NumBStC', '- Number of blade structural controllers (integer)\n')) - f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(self.fst_vt['ServoDyn']['BStCfiles']) + '"', 'BStCfiles', '- Name of the files for blade structural controllers (quoted strings) [unused when NumBStC==0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NumNStC'], 'NumNStC', '- Number of nacelle structural controllers (integer)\n')) - f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(self.fst_vt['ServoDyn']['NStCfiles']) + '"', 'NStCfiles', '- Name of the files for nacelle structural controllers (quoted strings) [unused when NumNStC==0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NumTStC'], 'NumTStC', '- Number of tower structural controllers (integer)\n')) - f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(self.fst_vt['ServoDyn']['TStCfiles']) + '"', 'TStCfiles', '- Name of the files for tower structural controllers (quoted strings) [unused when NumTStC==0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NumSStC'], 'NumSStC', '- Number of substructure structural controllers (integer)\n')) - f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(self.fst_vt['ServoDyn']['SStCfiles']) + '"', 'SStCfiles', '- Name of the files for substructure structural controllers (quoted strings) [unused when NumSStC==0]\n')) - f.write('---------------------- CABLE CONTROL ---------------------------------------- \n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['CCmode'], 'CCmode', '- Cable control mode {0: none, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) - f.write('---------------------- BLADED INTERFACE ---------------------------------------- [used only with Bladed Interface]\n') - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ServoDyn']['DLL_FileName']+'"', 'DLL_FileName', '- Name/location of the dynamic library {.dll [Windows] or .so [Linux]} in the Bladed-DLL format (-) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ServoDyn']['DLL_InFile']+'"', 'DLL_InFile', '- Name of input file sent to the DLL (-) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ServoDyn']['DLL_ProcName']+'"', 'DLL_ProcName', '- Name of procedure in DLL to be called (-) [case sensitive; used only with DLL Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['DLL_DT'], 'DLL_DT', '- Communication interval for dynamic library (s) (or "default") [used only with Bladed Interface]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['DLL_Ramp'], 'DLL_Ramp', '- Whether a linear ramp should be used between DLL_DT time steps [introduces time shift when true] (flag) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['BPCutoff'], 'BPCutoff', '- Cutoff frequency for low-pass filter on blade pitch from DLL (Hz) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['NacYaw_North'], 'NacYaw_North', '- Reference yaw angle of the nacelle when the upwind end points due North (deg) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Ptch_Cntrl'], 'Ptch_Cntrl', '- Record 28: Use individual pitch control {0: collective pitch; 1: individual pitch control} (switch) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Ptch_SetPnt'], 'Ptch_SetPnt', '- Record 5: Below-rated pitch angle set-point (deg) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Ptch_Min'], 'Ptch_Min', '- Record 6: Minimum pitch angle (deg) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Ptch_Max'], 'Ptch_Max', '- Record 7: Maximum pitch angle (deg) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PtchRate_Min'], 'PtchRate_Min', '- Record 8: Minimum pitch rate (most negative value allowed) (deg/s) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['PtchRate_Max'], 'PtchRate_Max', '- Record 9: Maximum pitch rate (deg/s) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['Gain_OM'], 'Gain_OM', '- Record 16: Optimal mode gain (Nm/(rad/s)^2) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenSpd_MinOM'], 'GenSpd_MinOM', '- Record 17: Minimum generator speed (rpm) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenSpd_MaxOM'], 'GenSpd_MaxOM', '- Record 18: Optimal mode maximum speed (rpm) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenSpd_Dem'], 'GenSpd_Dem', '- Record 19: Demanded generator speed above rated (rpm) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenTrq_Dem'], 'GenTrq_Dem', '- Record 22: Demanded generator torque above rated (Nm) [used only with Bladed Interface]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['GenPwr_Dem'], 'GenPwr_Dem', '- Record 13: Demanded power (W) [used only with Bladed Interface]\n')) - f.write('---------------------- BLADED INTERFACE TORQUE-SPEED LOOK-UP TABLE -------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['DLL_NumTrq'], 'DLL_NumTrq', '- Record 26: No. of points in torque-speed look-up table {0 = none and use the optimal mode parameters; nonzero = ignore the optimal mode PARAMETERs by setting Record 16 to 0.0} (-) [used only with Bladed Interface]\n')) - f.write('{:<22}\t{:<22}\n'.format("GenSpd_TLU", "GenTrq_TLU")) - f.write('{:<22}\t{:<22}\n'.format("(rpm)", "(Nm)")) - for i in range(self.fst_vt['ServoDyn']['DLL_NumTrq']): - a1 = self.fst_vt['ServoDyn']['GenSpd_TLU'][i] - a2 = self.fst_vt['ServoDyn']['GenTrq_TLU'][i] - f.write('{:<22}\t{:<22}\n'.format(a1, a2)) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['SumPrint'], 'SumPrint', '- Print summary data to .sum (flag) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['OutFile'], 'OutFile', '- Switch to determine where output will be placed: {1: in module output file only; 2: in glue code output file only; 3: both} (currently unused)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['ServoDyn']['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output (except time). Resulting field should be 10 characters. (quoted string) (currently unused)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['ServoDyn']['TStart'], 'TStart', '- Time to begin tabular output (s) (currently unused)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - - outlist = self.get_outlist(self.fst_vt['outlist'], ['ServoDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - f.write('---------------------------------------------------------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_DISCON_in(self): - # Generate Bladed style Interface controller input file, intended for ROSCO https://github.com/NREL/ROSCO_toolbox - - # Fill controller and turbine objects for ROSCO - # - controller - controller = type('', (), {})() - - turbine = type('', (), {})() - turbine.Cp = type('', (), {})() - turbine.Ct = type('', (), {})() - turbine.Cq = type('', (), {})() - turbine.v_rated = self.fst_vt['DISCON_in']['v_rated'] - turbine.Cp = self.fst_vt['DISCON_in']['Cp'] - turbine.Ct = self.fst_vt['DISCON_in']['Ct'] - turbine.Cq = self.fst_vt['DISCON_in']['Cq'] - turbine.Cp_table = self.fst_vt['DISCON_in']['Cp_table'] - turbine.Ct_table = self.fst_vt['DISCON_in']['Ct_table'] - turbine.Cq_table = self.fst_vt['DISCON_in']['Cq_table'] - turbine.pitch_initial_rad = self.fst_vt['DISCON_in']['Cp_pitch_initial_rad'] - turbine.TSR_initial = self.fst_vt['DISCON_in']['Cp_TSR_initial'] - turbine.TurbineName = 'OpenFAST_IO Turbine' - - # Define DISCON infile paths - self.fst_vt['ServoDyn']['DLL_InFile'] = self.FAST_namingOut + '_DISCON.IN' - discon_in_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ServoDyn']['DLL_InFile']) - self.fst_vt['DISCON_in']['PerfFileName'] = self.FAST_namingOut + '_Cp_Ct_Cq.txt' - - # Write DISCON input files - ROSCO_utilities.write_rotor_performance( - turbine, - txt_filename=os.path.join(self.FAST_runDirectory, self.fst_vt['DISCON_in']['PerfFileName']) - ) - - ROSCO_utilities.write_DISCON( - turbine, - controller, - param_file=discon_in_file, - txt_filename=self.fst_vt['DISCON_in']['PerfFileName'], - rosco_vt=self.fst_vt['DISCON_in'] - ) - - def write_spd_trq(self): - # generate the spd_trq.dat file when VSContrl == 3 - spd_trq_file = os.path.join(self.FAST_runDirectory, 'spd_trq.dat') - f = open(spd_trq_file, 'w') - - f.write('{:}'.format(self.fst_vt['spd_trq']['header'], '\n')) - for i in range(len(self.fst_vt['spd_trq']['RPM'])): - f.write('{:<22f} {:<22f} {:}'.format(self.fst_vt['spd_trq']['RPM'][i], self.fst_vt['spd_trq']['Torque'][i], '\n')) - - def write_HydroDyn(self): - - # Generate HydroDyn input file - self.fst_vt['Fst']['HydroFile'] = self.FAST_namingOut + '_HydroDyn.dat' - hd_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['HydroFile']) - f = open(hd_file, 'w') - - f.write('------- HydroDyn Input File --------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['Echo'], 'Echo', '- Echo the input file data (flag)\n')) - f.write('---------------------- FLOATING PLATFORM --------------------------------------- [unused with WaveMod=6]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['PotMod'], 'PotMod', '- Potential-flow model {0: none=no potential flow, 1: frequency-to-time-domain transforms based on WAMIT output, 2: fluid-impulse theory (FIT)} (switch)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['ExctnMod'], 'ExctnMod', '- Wave-excitation model {0: no wave-excitation calculation, 1: DFT, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ssexctn INPUT FILE]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['ExctnDisp'], 'ExctnDisp','- Method of computing Wave Excitation {0: use undisplaced position, 1: use displaced position, 2: use low-pass filtered displaced position) [only used when PotMod=1 and ExctnMod>0 and SeaState\'s WaveMod>0]} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['ExctnCutOff'], 'ExctnCutOff','- Cutoff (corner) frequency of the low-pass time-filtered displaced position (Hz) [>0.0] [used only when PotMod=1, ExctnMod>0, and ExctnDisp=2]) [only used when PotMod=1 and ExctnMod>0 and SeaState\'s WaveMod>0]} (switch)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['PtfmYMod'], 'PtfmYMod', '- Model for large platform yaw offset {0: Static reference yaw offset based on PtfmRefY, 1: dynamic reference yaw offset based on low-pass filtering the PRP yaw motion with cutoff frequency PtfmYCutOff} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['PtfmRefY'], 'PtfmRefY', '- Constant (if PtfmYMod=0) or initial (if PtfmYMod=1) platform reference yaw offset (deg)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['PtfmYCutOff'], 'PtfmYCutOff', '- Cutoff frequency for the low-pass filtering of PRP yaw motion when PtfmYMod=1 [unused when PtfmYMod=0] (Hz)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NExctnHdg'], 'NExctnHdg', '- Number of evenly distributed platform yaw/heading angles over the range of [-180, 180) deg for which the wave excitation shall be computed [only used when PtfmYMod=1] (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['RdtnMod'], 'RdtnMod', '- Radiation memory-effect model {0: no memory-effect calculation, 1: convolution, 2: state-space} (switch) [only used when PotMod=1; STATE-SPACE REQUIRES *.ss INPUT FILE]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['RdtnTMax'], 'RdtnTMax', '- Analysis time for wave radiation kernel calculations (sec) [only used when PotMod=1 and RdtnMod>0; determines RdtnDOmega=Pi/RdtnTMax in the cosine transform; MAKE SURE THIS IS LONG ENOUGH FOR THE RADIATION IMPULSE RESPONSE FUNCTIONS TO DECAY TO NEAR-ZERO FOR THE GIVEN PLATFORM!]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['RdtnDT'], 'RdtnDT', '- Time step for wave radiation kernel calculations (sec) [only used when PotMod=1 and ExctnMod>0 or RdtnMod>0; DT<=RdtnDT<=0.1 recommended; determines RdtnOmegaMax=Pi/RdtnDT in the cosine transform]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NBody'], 'NBody', '- Number of WAMIT bodies to be used (-) [>=1; only used when PotMod=1. If NBodyMod=1, the WAMIT data contains a vector of size 6*NBody x 1 and matrices of size 6*NBody x 6*NBody; if NBodyMod>1, there are NBody sets of WAMIT data each with a vector of size 6 x 1 and matrices of size 6 x 6]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NBodyMod'], 'NBodyMod', '- Body coupling model {1: include coupling terms between each body and NBody in HydroDyn equals NBODY in WAMIT, 2: neglect coupling terms between each body and NBODY=1 with XBODY=0 in WAMIT, 3: Neglect coupling terms between each body and NBODY=1 with XBODY=/0 in WAMIT} (switch) [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format('"{}"'.format('", "'.join(self.fst_vt['HydroDyn']['PotFile']) if isinstance(self.fst_vt['HydroDyn']['PotFile'], list) else self.fst_vt['HydroDyn']['PotFile']), 'PotFile', '- Root name of potential-flow model data; WAMIT output files containing the linear, nondimensionalized, hydrostatic restoring matrix (.hst), frequency-dependent hydrodynamic added mass matrix and damping matrix (.1), and frequency- and direction-dependent wave excitation force vector per unit wave amplitude (.3) (quoted string) [1 to NBody if NBodyMod>1] [MAKE SURE THE FREQUENCIES INHERENT IN THESE WAMIT FILES SPAN THE PHYSICALLY-SIGNIFICANT RANGE OF FREQUENCIES FOR THE GIVEN PLATFORM; THEY MUST CONTAIN THE ZERO- AND INFINITE-FREQUENCY LIMITS!]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['WAMITULEN']]), 'WAMITULEN', '- Characteristic body length scale used to redimensionalize WAMIT output (meters) [1 to NBody if NBodyMod>1] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmRefxt']]), 'PtfmRefxt', '- The xt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmRefyt']]), 'PtfmRefyt', '- The yt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmRefzt']]), 'PtfmRefzt', '- The zt offset of the body reference point(s) from (0,0,0) (meters) [1 to NBody] [only used when PotMod=1. If NBodyMod=2,PtfmRefzt=0.0]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmRefztRot']]), 'PtfmRefztRot', '- The rotation about zt of the body reference frame(s) from xt/yt (degrees) [1 to NBody] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmVol0']]), 'PtfmVol0', '- Displaced volume of water when the body is in its undisplaced position (m^3) [1 to NBody] [only used when PotMod=1; USE THE SAME VALUE COMPUTED BY WAMIT AS OUTPUT IN THE .OUT FILE!]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmCOBxt']]), 'PtfmCOBxt', '- The xt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['PtfmCOByt']]), 'PtfmCOByt', '- The yt offset of the center of buoyancy (COB) from (0,0) (meters) [1 to NBody] [only used when PotMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['HydroDyn']['NAddDOF']]), 'NAddDOF', '- Number of additional generalized DOF of each WAMIT body (-) [1 to NBody] [>=0; =0 if NBody>1; only used when PotMod=1]\n')) - f.write('---------------------- 2ND-ORDER FLOATING PLATFORM FORCES ---------------------- [unused with WaveMod=0 or 6, or PotMod=0 or 2]\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['MnDrift'], 'MnDrift', "- Mean-drift 2nd-order forces computed {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero]\n")) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NewmanApp'], 'NewmanApp', "- Mean- and slow-drift 2nd-order forces computed with Newman's approximation {0: None; [7, 8, 9, 10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero. Used only when WaveDirMod=0]\n")) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['DiffQTF'], 'DiffQTF', "- Full difference-frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use} [Only one of MnDrift, NewmanApp, or DiffQTF can be non-zero]\n")) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['SumQTF'], 'SumQTF', "- Full summation -frequency 2nd-order forces computed with full QTF {0: None; [10, 11, or 12]: WAMIT file to use}\n")) - f.write('---------------------- PLATFORM ADDITIONAL STIFFNESS AND DAMPING -------------- [unused with PotMod=0 or 2]\n') - for j in range(6): - if type(self.fst_vt['HydroDyn']['AddF0'][j]) == float: - ln = '{:14} '.format(self.fst_vt['HydroDyn']['AddF0'][j]) - elif type(self.fst_vt['HydroDyn']['AddF0'][j]) in [list, np.ndarray]: - ln = '{:14} '.format(' '.join([f'{val}' for val in self.fst_vt['HydroDyn']['AddF0'][j]])) - else: - raise Exception("Check type of self.fst_vt['HydroDyn']['AddF0']") - - if j == 0: - ln = ln + 'AddF0 - Additional preload (N, N-m) [If NBodyMod=1, one size 6*NBody x 1 vector; if NBodyMod>1, NBody size 6 x 1 vectors]\n' - else: - ln = ln + '\n' - f.write(ln) - for j in range(6): - try: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddCLin'][j,:]]) - except: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddCLin'][j]]) - if j == 0: - ln = ln + " AddCLin - Additional linear stiffness (N/m, N/rad, N-m/m, N-m/rad) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]\n" - else: - ln = ln + "\n" - f.write(ln) - for j in range(6): - try: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddBLin'][j,:]]) - except: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddBLin'][j]]) - if j == 0: - ln = ln + " AddBLin - Additional linear damping(N/(m/s), N/(rad/s), N-m/(m/s), N-m/(rad/s)) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]\n" - else: - ln = ln + "\n" - f.write(ln) - for j in range(6): - try: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddBQuad'][j,:]]) - except: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['HydroDyn']['AddBQuad'][j]]) - if j == 0: - ln = ln + " AddBQuad - Additional quadratic drag(N/(m/s)^2, N/(rad/s)^2, N-m(m/s)^2, N-m/(rad/s)^2) [If NBodyMod=1, one size 6*NBody x 6*NBody matrix; if NBodyMod>1, NBody size 6 x 6 matrices]\n" - else: - ln = ln + "\n" - f.write(ln) - - f.write('---------------------- STRIP THEORY OPTIONS --------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['WaveDisp'], 'WaveDisp', '- Method of computing Wave Kinematics {0: use undisplaced position, 1: use displaced position) } (switch)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['AMMod'], 'AMMod', '- Method of computing distributed added-mass force. (0: Only and always on nodes below SWL at the undisplaced position. 1: Up to the instantaneous free surface) [overwrite to 0 when WaveStMod = 0 in SeaState]]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['HstMod'], 'HstMod', '- Method of computing hydrostatic loads. (0: Up to the still water level. 1: Up to the instantaneous free surface) [overwrite to 0 when WaveStMod = 0 in SeaState]]\n')) - - f.write('---------------------- AXIAL COEFFICIENTS --------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NAxCoef'], 'NAxCoef', '- Number of axial coefficients (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['AxCoefID', 'AxCd', 'AxCa', 'AxCp', 'AxFDMod', 'AxVnCOff', 'AxFDLoFSc']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(-)','(-)','(-)','(switch)','(Hz)','(-)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NAxCoef']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['AxCoefID'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxCd'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxCa'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxCp'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxFDMod'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxVnCOff'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['AxFDLoFSc'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- MEMBER JOINTS -------------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NJoints'], 'NJoints', '- Number of joints (-) [must be exactly 0 or at least 2]\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['JointID', 'Jointxi', 'Jointyi', 'Jointzi', 'JointAxID', 'JointOvrlp']])+' ! [JointOvrlp= 0: do nothing at joint, 1: eliminate overlaps by calculating super member]\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(m)', '(m)', '(m)', '(-)', '(switch)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NJoints']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['JointID'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['Jointxi'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['Jointyi'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['Jointzi'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['JointAxID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['JointOvrlp'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- CYLINDRICAL MEMBER CROSS-SECTION PROPERTIES -------------------------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NPropSetsCyl'], 'NPropSetsCyl', '- Number of cylindrical member property sets (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'PropD', 'PropThck']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(m)', '(m)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NPropSetsCyl']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['CylPropSetID'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylPropD'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylPropThck'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- RECTANGULAR MEMBER CROSS-SECTION PROPERTIES -------------------------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NPropSetsRec'], 'NPropSetsRec', '- Number of rectangular member property sets (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'PropA', 'PropB', 'PropThck']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(m)', '(m)', '(m)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NPropSetsRec']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['RecPropSetID'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecPropA'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecPropB'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecPropThck'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- SIMPLE CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 1) --------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['SimplCd', 'SimplCdMG', 'SimplCa', 'SimplCaMG', 'SimplCp', 'SimplCpMG', 'SimplAxCd', 'SimplAxCdMG', 'SimplAxCa', 'SimplAxCaMG', 'SimplAxCp', 'SimplAxCpMG', 'SimplCb', 'SimplCbMG']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)']*14])+'\n') - ln = [] - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCd'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCdMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCa'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCaMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCp'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCpMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCd'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCdMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCa'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCaMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCp'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplAxCpMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCb'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylSimplCbMG'])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- SIMPLE RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 1) --------------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['SimplCdA', 'SimplCdAMG', 'SimplCdB', 'SimplCdBMG', 'SimplCaA', 'SimplCaAMG', 'SimplCaB', 'SimplCaBMG', 'SimplCp', 'SimplCpMG', 'SimplAxCd', 'SimplAxCdMG', 'SimplAxCa', 'SimplAxCaMG', 'SimplAxCp', 'SimplAxCpMG', 'SimplCb', 'SimplCbMG']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)']*18])+'\n') - ln = [] - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCdA'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCdAMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCdB'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCdBMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCaA'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCaAMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCaB'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCaBMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCp'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCpMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCd'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCdMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCa'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCaMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCp'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplAxCpMG'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCb'])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecSimplCbMG'])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- DEPTH-BASED CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 2) ---------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NCoefDpthCyl'], 'NCoefDpthCyl', '- Number of depth-dependent cylindrical-member coefficients (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['Dpth', 'DpthCd', 'DpthCdMG', 'DpthCa', 'DpthCaMG', 'DpthCp', 'DpthCpMG', 'DpthAxCd', 'DpthAxCdMG', 'DpthAxCa', 'DpthAxCaMG', 'DpthAxCp', 'DpthAxCpMG', 'DpthCb', 'DpthCbMG']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(m)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NCoefDpthCyl']): - ln = [] - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpth'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCd'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCdMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCa'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCaMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCp'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCpMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCd'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCdMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCa'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCaMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCp'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthAxCpMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCb'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylDpthCbMG'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- DEPTH-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 2) ---------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NCoefDpthRec'], 'NCoefDpthRec', '- Number of depth-dependent rectangular-member coefficients (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['Dpth', 'DpthCdA', 'DpthCdAMG', 'DpthCdB', 'DpthCdBMG', 'DpthCaA', 'DpthCaAMG', 'DpthCaB', 'DpthCaBMG', 'DpthCp', 'DpthCpMG', 'DpthAxCd', 'DpthAxCdMG', 'DpthAxCa', 'DpthAxCaMG', 'DpthAxCp', 'DpthAxCpMG', 'DpthCb', 'DpthCbMG']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(m)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NCoefDpthRec']): - ln = [] - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpth'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCdA'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCdAMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCdB'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCdBMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCaA'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCaAMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCaB'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCaBMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCp'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCpMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCd'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCdMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCa'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCaMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCp'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthAxCpMG'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCb'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecDpthCbMG'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- MEMBER-BASED CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 3) --------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NCoefMembersCyl'], 'NCoefMembersCyl', '- Number of member-based cylindrical-member coefficients (-)\n')) - mem_coeff_names = ['MemberID', 'MemberCd1', 'MemberCd2', 'MemberCdMG1', 'MemberCdMG2', 'MemberCa1', 'MemberCa2', 'MemberCaMG1', 'MemberCaMG2', 'MemberCp1', 'MemberCp2', 'MemberCpMG1', 'MemberCpMG2', 'MemberAxCd1', 'MemberAxCd2', 'MemberAxCdMG1', 'MemberAxCdMG2', 'MemberAxCa1', 'MemberAxCa2', 'MemberAxCaMG1', 'MemberAxCaMG2', 'MemberAxCp1', 'MemberAxCp2', 'MemberAxCpMG1', 'MemberAxCpMG2','MemberCb1','MemberCb2','MemberCbMG1','MemberCbMG2'] - f.write(" ".join(['{:^11s}'.format(i) for i in mem_coeff_names])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)']*len(mem_coeff_names)])+'\n') - for i in range(self.fst_vt['HydroDyn']['NCoefMembersCyl']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MemberID_HydCCyl'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCd1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCd2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCdMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCdMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCa1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCa2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCaMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCaMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCp1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCp2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCpMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCpMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCd1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCd2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCdMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCdMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCa1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCa2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCaMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCaMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCp1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCp2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCpMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberAxCpMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCb1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCb2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCbMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['CylMemberCbMG2'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- MEMBER-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 3) --------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NCoefMembersRec'], 'NCoefMembersRec', '- Number of member-based rectangular-member coefficients (-)\n')) - mem_coeff_names = ['MemberID', 'MemberCdA1', 'MemberCdA2', 'MemberCdAMG1', 'MemberCdAMG2', 'MemberCdB1', 'MemberCdB2', 'MemberCdBMG1', 'MemberCdBMG2', 'MemberCaA1', 'MemberCaA2', 'MemberCaAMG1', 'MemberCaAMG2', 'MemberCaB1', 'MemberCaB2', 'MemberCaBMG1', 'MemberCaBMG2', 'MemberCp1', 'MemberCp2', 'MemberCpMG1', 'MemberCpMG2', 'MemberAxCd1', 'MemberAxCd2', 'MemberAxCdMG1', 'MemberAxCdMG2', 'MemberAxCa1', 'MemberAxCa2', 'MemberAxCaMG1', 'MemberAxCaMG2', 'MemberAxCp1', 'MemberAxCp2', 'MemberAxCpMG1', 'MemberAxCpMG2','MemberCb1','MemberCb2','MemberCbMG1','MemberCbMG2'] - f.write(" ".join(['{:^11s}'.format(i) for i in mem_coeff_names])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)']*len(mem_coeff_names)])+'\n') - for i in range(self.fst_vt['HydroDyn']['NCoefMembersRec']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MemberID_HydCRec'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdA1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdA2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdAMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdAMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdB1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdB2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdBMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCdBMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaA1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaA2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaAMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaAMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaB1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaB2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaBMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCaBMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCp1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCp2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCpMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCpMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCd1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCd2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCdMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCdMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCa1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCa2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCaMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCaMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCp1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCp2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCpMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberAxCpMG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCb1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCb2'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCbMG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['RecMemberCbMG2'][i])) - f.write(" ".join(ln) + '\n') - f.write('-------------------- MEMBERS -------------------------------------------------\n') - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NMembers'], 'NMembers', '- Number of members (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', 'MSecGeom', 'MSpinOrient', 'MDivSize', 'MCoefMod', 'MHstLMod', 'PropPot']])+' ! [MCoefMod=1: use simple coeff table, 2: use depth-based coeff table, 3: use member-based coeff table] [ PropPot/=0 if member is modeled with potential-flow theory]\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(-)', '(-)', '(-)', '(-)', '(switch)', '(deg)', '(m)', '(switch)', '(switch)', '(flag)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NMembers']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MemberID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MJointID1'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MJointID2'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MPropSetID1'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MPropSetID2'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MSecGeom'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['MSpinOrient'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['MDivSize'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MCoefMod'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MHstLMod'][i])) - ln.append('{!s:^11}'.format(self.fst_vt['HydroDyn']['PropPot'][i])) - f.write(" ".join(ln) + '\n') - f.write("---------------------- FILLED MEMBERS ------------------------------------------\n") - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NFillGroups'], 'NFillGroups', '- Number of filled member groups (-) [If FillDens = DEFAULT, then FillDens = WtrDens; FillFSLoc is related to MSL2SWL]\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['FillNumM', 'FillMList', 'FillFSLoc', 'FillDens']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(kg/m^3)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NFillGroups']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['FillNumM'][i])) - ln.append(" ".join(['%d'%j for j in self.fst_vt['HydroDyn']['FillMList'][i]])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['FillFSLoc'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['FillDens'][i])) - f.write(" ".join(ln) + '\n') - f.write("---------------------- MARINE GROWTH -------------------------------------------\n") - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NMGDepths'], 'NMGDepths', '- Number of marine-growth depths specified (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['MGDpth', 'MGThck', 'MGDens']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(m)', '(m)', '(kg/m^3)']])+'\n') - for i in range(self.fst_vt['HydroDyn']['NMGDepths']): - ln = [] - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['MGDpth'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['MGThck'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['MGDens'][i])) - f.write(" ".join(ln) + '\n') - f.write("---------------------- MEMBER OUTPUT LIST --------------------------------------\n") - f.write('{:<11d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NMOutputs'], 'NMOutputs', '- Number of member outputs (-) [must be <=99]\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['MemberID_out', 'NOutLoc', 'NodeLocs [NOutLoc < 10; node locations are normalized distance from the start of the member, and must be >=0 and <= 1] [unused if NMOutputs=0]']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)']*3])+'\n') - for i in range(self.fst_vt['HydroDyn']['NMOutputs']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['MemberID_out'][i])) - ln.append('{:^11d}'.format(self.fst_vt['HydroDyn']['NOutLoc'][i])) - ln.append('{:^11}'.format(self.fst_vt['HydroDyn']['NodeLocs'][i])) - f.write(" ".join(ln) + '\n') - f.write("---------------------- JOINT OUTPUT LIST ---------------------------------------\n") - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['NJOutputs'], 'NJOutputs', '- Number of joint outputs [Must be < 10]\n')) - f.write('{:<22} {:<11} {:}'.format(" ".join(["%d"%i for i in self.fst_vt['HydroDyn']['JOutLst']]), 'JOutLst', '- List of JointIDs which are to be output (-)[unused if NJOutputs=0]\n')) - f.write("---------------------- OUTPUT --------------------------------------------------\n") - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['HDSum'], 'HDSum', '- Output a summary file [flag]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['OutAll'], 'OutAll', '- Output all user-specified member and joint loads (only at each member end, not interior locations) [flag]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['HydroDyn']['OutSwtch'], 'OutSwtch', '- Output requested channels to: [1=Hydrodyn.out, 2=GlueCode.out, 3=both files]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['OutFmt'], 'OutFmt', '- Output format for numerical results (quoted string) [not checked for validity!]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['HydroDyn']['OutSFmt'], 'OutSFmt', '- Output format for header strings (quoted string) [not checked for validity!]\n')) - f.write('---------------------- OUTPUT CHANNELS -----------------------------------------\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['HydroDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line)\n') - - f.close() - - def write_SeaState(self): - - # Generate SeaState input file - self.fst_vt['Fst']['SeaStFile'] = self.FAST_namingOut + '_SeaState.dat' - hd_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['SeaStFile']) - f = open(hd_file, 'w') - - f.write('------- SeaState Input File --------------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['Echo'], 'Echo', '- Echo the input file data (flag)\n')) - - f.write('---------------------- ENVIRONMENTAL CONDITIONS --------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WtrDens'],'WtrDens', '- Water density (kg/m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WtrDpth'],'WtrDpth', '- Water depth (meters) relative to MSL\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['MSL2SWL'],'MSL2SWL', '- Offset between still-water level and mean sea level (meters) [positive upward; unused when WaveMod = 6; must be zero if PotMod=1 or 2]\n')) - - - f.write('---------------------- SPATIAL DISCRETIZATION ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['X_HalfWidth'], 'X_HalfWidth', '– Half-width of the domain in the X direction (m) [>0, NOTE: X[nX] = nX*dX, where nX = {-NX+1,-NX+2,…,NX-1} and dX = X_HalfWidth/(NX-1)]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['Y_HalfWidth'], 'Y_HalfWidth', '– Half-width of the domain in the Y direction (m) [>0, NOTE: Y[nY] = nY*dY, where nY = {-NY+1,-NY+2,…,NY-1} and dY = Y_HalfWidth/(NY-1)]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['Z_Depth'], 'Z_Depth', '– Depth of the domain the Z direction (m) relative to SWL [0 < Z_Depth <= WtrDpth+MSL2SWL; "default": Z_Depth = WtrDpth+MSL2SWL; Z[nZ] = ( COS( nZ*dthetaZ ) – 1 )*Z_Depth, where nZ = {0,1,…NZ-1} and dthetaZ = pi/( 2*(NZ-1) )]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['NX'], 'NX', '– Number of nodes in half of the X-direction domain (-) [>=2]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['NY'], 'NY', '– Number of nodes in half of the Y-direction domain (-) [>=2]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['NZ'], 'NZ', '– Number of nodes in the Z direction (-) [>=2]\n')) - - f.write('---------------------- WAVES ---------------------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveMod'], 'WaveMod', '- Incident wave kinematics model {0: none=still water, 1: regular (periodic), 1P#: regular with user-specified phase, 2: JONSWAP/Pierson-Moskowitz spectrum (irregular), 3: White noise spectrum (irregular), 4: user-defined spectrum from routine UserWaveSpctrm (irregular), 5: Externally generated wave-elevation time series, 6: Externally generated full wave-kinematics time series [option 6 is invalid for PotMod/=0]} (switch)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveStMod'], 'WaveStMod', '- Model for stretching incident wave kinematics to instantaneous free surface {0: none=no stretching, 1: vertical stretching, 2: extrapolation stretching, 3: Wheeler stretching} (switch)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WvCrntMod'], 'WvCrntMod', '- Combined wave-current modeling option {0: simple superposition, 1: include Doppler effect, 2: include both Doppler effect and wave amplitude/spectrum scaling} (switch) [unused when WaveMod=0 or WaveMod=6. Also unused when there is no current from SeaState (CurrMod=0) or from InflowWind if MHK.]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveTMax'], 'WaveTMax', '- Analysis time for incident wave calculations (sec) [unused when WaveMod=0; determines WaveDOmega=2Pi/WaveTMax in the IFFT]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveDT'], 'WaveDT', '- Time step for incident wave calculations (sec) [unused when WaveMod=0 or 7; 0.1<=WaveDT<=1.0 recommended; determines WaveOmegaMax=Pi/WaveDT in the IFFT]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveHs'], 'WaveHs', '- Significant wave height of incident waves (meters) [used only when WaveMod=1, 2, or 3]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveTp'], 'WaveTp', '- Peak-spectral period of incident waves (sec) [used only when WaveMod=1 or 2]\n')) - if isinstance(self.fst_vt['SeaState']['WavePkShp'], float): - if self.fst_vt['SeaState']['WavePkShp'] == 0.: - WavePkShp = 'Default' - else: - WavePkShp = self.fst_vt['SeaState']['WavePkShp'] - else: - WavePkShp = self.fst_vt['SeaState']['WavePkShp'] - f.write('{:<22} {:<11} {:}'.format(WavePkShp, 'WavePkShp', '- Peak-shape parameter of incident wave spectrum (-) or DEFAULT (string) [used only when WaveMod=2; use 1.0 for Pierson-Moskowitz]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvLowCOff'], 'WvLowCOff', '- Low cut-off frequency or lower frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvHiCOff'], 'WvHiCOff', '- High cut-off frequency or upper frequency limit of the wave spectrum beyond which the wave spectrum is zeroed (rad/s) [unused when WaveMod=0, 1, or 6]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveDir'], 'WaveDir', '- Incident wave propagation heading direction (degrees) [unused when WaveMod=0, 6 or 7]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveDirMod'], 'WaveDirMod', '- Directional spreading function {0: none, 1: COS2S} (-) [only used when WaveMod=2,3, or 4; must be 0 if WvCrntMod/=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveDirSpread'], 'WaveDirSpread', '- Wave direction spreading coefficient ( > 0 ) (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveNDir'], 'WaveNDir', '- Number of wave directions (-) [only used when WaveMod=2,3, or 4 and WaveDirMod=1; odd number only]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveDirRange'], 'WaveDirRange', '- Range of wave directions (full range: WaveDir +/- 1/2*WaveDirRange) (degrees) [only used when WaveMod=2,3,or 4 and WaveDirMod=1]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveSeed1'], 'WaveSeed(1)', '- First random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6]\n')) - - try: - seed2 = int(self.fst_vt['SeaState']['WaveSeed2']) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveSeed2'], 'WaveSeed(2)', '- Second random seed of incident waves [-2147483648 to 2147483647] (-) [unused when WaveMod=0, 5, or 6]\n')) - except ValueError: - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveSeed2'], 'WaveSeed(2)', '- Second random seed of incident waves [-2147483648 to 2147483647] (-) for intrinsic pRNG, or an alternative pRNG: "RanLux" (-) [unused when WaveMod=0, 5, or 6]\n')) - - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WaveNDAmp'], 'WaveNDAmp', '- Flag for normally distributed amplitudes (flag) [only used when WaveMod=2, 3, or 4]\n')) - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['SeaState']['WvKinFile']+'"', 'WvKinFile', '- Root name of externally generated wave data file(s) (quoted string) [used only when WaveMod=5, 6 or 7]\n')) - f.write('---------------------- 2ND-ORDER WAVES ----------------------------------------- [unused with WaveMod=0 or 6]\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvDiffQTF'], 'WvDiffQTF', '- Full difference-frequency 2nd-order wave kinematics (flag) [Must be false if WvCrntMod/=0]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvSumQTF'], 'WvSumQTF', '- Full summation-frequency 2nd-order wave kinematics (flag) [Must be false if WvCrntMod/=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvLowCOffD'], 'WvLowCOffD', '- Low frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvHiCOffD'], 'WvHiCOffD', '- High frequency cutoff used in the difference-frequencies (rad/s) [Only used with a difference-frequency method]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvLowCOffS'], 'WvLowCOffS', '- Low frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['WvHiCOffS'], 'WvHiCOffS', '- High frequency cutoff used in the summation-frequencies (rad/s) [Only used with a summation-frequency method]\n')) - f.write('---------------------- CONSTRAINED WAVES ----------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['ConstWaveMod'], 'ConstWaveMod', '- Constrained wave model: 0=none; 1=Constrained wave with specified crest elevation, alpha; 2=Constrained wave with guaranteed peak-to-trough crest height, HCrest (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CrestHmax'], 'CrestHmax', '- Crest height (2*alpha for ConstWaveMod=1 or HCrest for ConstWaveMod=2), must be larger than WaveHs (m) [unused when ConstWaveMod=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CrestTime'], 'CrestTime', '- Time at which the crest appears (s) [unused when ConstWaveMod=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CrestXi'], 'CrestXi', '- X-position of the crest. (m) [unused when ConstWaveMod=0]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CrestYi'], 'CrestYi', '- Y-position of the crest. (m) [unused when ConstWaveMod=0]\n')) - f.write('---------------------- CURRENT ------------------------------------------------- [unused with WaveMod=6]\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrMod'], 'CurrMod', '- Current profile model {0: none=no current, 1: standard, 2: user-defined from routine UserCurrent} (switch)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrSSV0'], 'CurrSSV0', '- Sub-surface current velocity at still water level (m/s) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrSSDir'], 'CurrSSDir', '- Sub-surface current heading direction (degrees) or DEFAULT (string) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrNSRef'], 'CurrNSRef', '- Near-surface current reference depth (meters) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrNSV0'], 'CurrNSV0', '- Near-surface current velocity at still water level (m/s) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrNSDir'], 'CurrNSDir', '- Near-surface current heading direction (degrees) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrDIV'], 'CurrDIV', '- Depth-independent current velocity (m/s) [used only when CurrMod=1]\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['CurrDIDir'], 'CurrDIDir', '- Depth-independent current heading direction (degrees) [used only when CurrMod=1]\n')) - f.write('---------------------- MacCamy-Fuchs Diffraction Model -------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['MCFD'],'MCFD', '- MacCamy-Fuchs member radius (ignored if radius <= 0) [must be 0 when WaveMod 0 or 6] \n')) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['SeaStSum'], 'SeaStSum', '- Output a summary file [flag]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['OutSwtch'], 'OutSwtch','- Output requested channels to: [1=SeaState.out, 2=GlueCode.out, 3=both files]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['OutFmt'], 'OutFmt','- Output format for numerical results (quoted string) [not checked for validity!]\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SeaState']['OutSFmt'], 'OutSFmt','- Output format for header strings (quoted string) [not checked for validity!]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['NWaveElev'], 'NWaveElev','- Number of points where the incident wave elevations can be computed (-) [maximum of 9 output locations]\n')) - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{float(val):f}' for val in self.fst_vt['SeaState']['WaveElevxi']]), 'WaveElevxi', '- List of xi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0]\n')) - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{float(val):f}' for val in self.fst_vt['SeaState']['WaveElevyi']]), 'WaveElevyi', '- List of yi-coordinates for points where the incident wave elevations can be output (meters) [NWaveElev points, separated by commas or white space; usused if NWaveElev = 0]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SeaState']['NWaveKin'], 'NWaveKin','- Number of points where the wave kinematics can be output (-) [maximum of 9 output locations]\n')) - - if self.fst_vt['SeaState']['NWaveKin'] > 0 : - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{val:f}' for val in self.fst_vt['SeaState']['WaveKinxi']]), 'WaveKinxi', '- List of xi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{val:f}' for val in self.fst_vt['SeaState']['WaveKinyi']]), 'WaveKinyi', '- List of yi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{val:f}' for val in self.fst_vt['SeaState']['WaveKinzi']]), 'WaveKinzi', '- List of zi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - else: - f.write('{:<11} {:}'.format('WaveKinxi', '- List of xi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - f.write('{:<11} {:}'.format('WaveKinyi', '- List of yi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - f.write('{:<11} {:}'.format('WaveKinzi', '- List of zi-coordinates for points where the wave kinematics can be output (meters) [NWaveKin points, separated by commas or white space; usused if NWaveKin = 0]\n')) - - f.write('---------------------- OUTPUT CHANNELS -----------------------------------------\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['SeaState']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - - f.write('END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line)\n') - - f.close() - - def write_SubDyn(self): - # Generate SubDyn input file - self.fst_vt['Fst']['SubFile'] = self.FAST_namingOut + '_SubDyn.dat' - sd_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['SubFile']) - f = open(sd_file, 'w') - - f.write('----------- SubDyn MultiMember Support Structure Input File ------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('-------------------------- SIMULATION CONTROL ---------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['Echo'], 'Echo', '- Echo input data to ".SD.ech" (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['SDdeltaT'], 'SDdeltaT', '- Local Integration Step. If "default", the glue-code integration step will be used.\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['IntMethod'], 'IntMethod', '- Integration Method [1/2/3/4 = RK4/AB4/ABM4/AM2].\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['SttcSolve'], 'SttcSolve', '- Solve dynamics about static equilibrium point\n')) - # f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['GuyanLoadCorrection'], 'GuyanLoadCorrection', '- Include extra moment from lever arm at interface and rotate FEM for floating.\n')) - f.write('-------------------- FEA and CRAIG-BAMPTON PARAMETERS---------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['FEMMod'], 'FEMMod', '- FEM switch: element model in the FEM. [1= Euler-Bernoulli(E-B); 2=Tapered E-B (unavailable); 3= 2-node Timoshenko; 4= 2-node tapered Timoshenko (unavailable)]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NDiv'], 'NDiv', '- Number of sub-elements per member\n')) - # f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['CBMod'], 'CBMod', '- [T/F] If True perform C-B reduction, else full FEM dofs will be retained. If True, select Nmodes to retain in C-B reduced system.\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['Nmodes'], 'Nmodes', '- Number of internal modes to retain. If Nmodes=0 --> Guyan Reduction. If Nmodes<0 --> retain all modes.\n')) - - JDampings = self.fst_vt['SubDyn']['JDampings'] - if isinstance(JDampings, float): - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['JDampings'], 'JDampings', '- Damping Ratios for each retained mode (% of critical) If Nmodes>0, list Nmodes structural damping ratios for each retained mode (% of critical), or a single damping ratio to be applied to all retained modes. (last entered value will be used for all remaining modes).\n')) - else: # list of floats - f.write('{:<22} {:<11} {:}'.format(", ".join([f'{d:f}' for d in self.fst_vt['SubDyn']['JDampings']]), 'JDampings', '- Damping Ratios for each retained mode (% of critical) If Nmodes>0, list Nmodes structural damping ratios for each retained mode (% of critical), or a single damping ratio to be applied to all retained modes. (last entered value will be used for all remaining modes).\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['GuyanDampMod'], 'GuyanDampMod', '- Guyan damping {0=none, 1=Rayleigh Damping, 2=user specified 6x6 matrix}.\n')) - f.write('{:<10} {:<10} {:<11} {:}'.format(self.fst_vt['SubDyn']['RayleighDamp'][0], self.fst_vt['SubDyn']['RayleighDamp'][1], 'RayleighDamp', '- Mass and stiffness proportional damping coefficients (Rayleigh Damping) [only if GuyanDampMod=1].\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['GuyanDampSize'], 'GuyanDampSize', '- Guyan damping matrix (6nTPx6nTP if fixed bottom or 6(nTP-1)-by-6(nTP-1) if floating) [only if GuyanDampMod=2].\n')) - for j in range(self.fst_vt['SubDyn']['GuyanDampSize']): - try: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['SubDyn']['GuyanDamp'][j,:]]) - except: - ln = " ".join(['{:14}'.format(i) for i in self.fst_vt['SubDyn']['GuyanDamp'][j]]) - ln += "\n" - f.write(ln) - - f.write('------- INITIAL RIGID-BODY POSITION [used only for floating structure with more than one transition pieces] -------\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['RBSurge', 'RBSway', 'RBHeave', 'RBRoll', 'RBPitch', 'RBYaw']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(m)', '(m)', '(m)', '(deg)', '(deg)', '(deg)']])+'\n') - ln = [] - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBSurge'])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBSway'])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBHeave'])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBRoll'])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBPitch'])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RBYaw'])) - f.write(" ".join(ln) + '\n') - - f.write('---- STRUCTURE JOINTS: joints connect structure members (~Hydrodyn Input File)---\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NJoints'], 'NJoints', '- Number of joints (-)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['JointID','JointXss','JointYss','JointZss','JointType','JointDirX','JointDirY','JointDirZ','JointStiff']])+' ![Coordinates of Member joints in SS-Coordinate System][JointType={1:cantilever, 2:universal joint, 3:revolute joint, 4:spherical joint}]\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(m)','(m)','(m)','(-)','(-)','(-)','(-)','(Nm/rad)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NJoints']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['JointID'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointXss'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointYss'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointZss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['JointType'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointDirX'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointDirY'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointDirZ'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JointStiff'][i])) - f.write(" ".join(ln) + '\n') - f.write('------------------- BASE REACTION JOINTS: 1/0 for Locked/Free DOF @ each Reaction Node ---------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NReact'], 'NReact', '- Number of Joints with reaction forces; be sure to remove all rigid motion DOFs of the structure (else det([K])=[0])\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['RJointID', 'RctTDXss', 'RctTDYss', 'RctTDZss', 'RctRDXss', 'RctRDYss', 'RctRDZss','SSIfile']])+' ! [Global Coordinate System]\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(flag)', '(flag)', '(flag)', '(flag)', '(flag)', '(flag)', '(string)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NReact']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RJointID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctTDXss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctTDYss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctTDZss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctRDXss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctRDYss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RctRDZss'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['Rct_SoilFile'][i])) - f.write(" ".join(ln) + '\n') - f.write('------- INTERFACE JOINTS: 1/0 for Locked (to the TP)/Free DOF @each Interface Joint (only Locked-to-TP implemented thus far (=rigid TP)) ---------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NInterf'], 'NInterf', '- Number of interface joints locked to the Transition Piece (TP): be sure to remove all rigid motion dofs\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['IJointID', 'TPID', 'ItfTDXss', 'ItfTDYss', 'ItfTDZss', 'ItfRDXss', 'ItfRDYss', 'ItfRDZss']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)', '(-)', '(flag)', '(flag)', '(flag)', '(flag)', '(flag)', '(flag)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NInterf']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['IJointID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['TPID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfTDXss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfTDYss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfTDZss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfRDXss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfRDYss'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['ItfRDZss'][i])) - f.write(" ".join(ln) + '\n') - f.write('----------------------------------- MEMBERS --------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NMembers'], 'NMembers', '- Number of frame members\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', 'MType', 'MSpin/COSMID']])+' ![MType={1c:beam circ., 1r: beam rect., 2:cable, 3:rigid, 4:beam arb., 5:spring}. COMSID={-1:none}]\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(-)','(-)','(-)','(-)','(-)','(deg/-)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NMembers']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MemberID'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MJointID1'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MJointID2'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MPropSetID1'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MPropSetID2'][i])) - if self.fst_vt['SubDyn']['MType'][i] == -1: - ln.append('{:^11s}'.format('1r')) - elif self.fst_vt['SubDyn']['MType'][i] == 1: - ln.append('{:^11s}'.format('1c')) - else: - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MType'][i])) - # Need to change M_COSMID None elements to -1 - self.fst_vt['SubDyn']['M_COSMID'][i] = -1 if self.fst_vt['SubDyn']['M_COSMID'][i] is None else self.fst_vt['SubDyn']['M_COSMID'][i] - - if self.fst_vt['SubDyn']['MType'][i] == 5: - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['M_COSMID'][i])) - else: - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['M_Spin'][i])) - f.write(" ".join(ln) + '\n') - f.write('------------------ CIRCULAR BEAM CROSS-SECTION PROPERTIES -----------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NPropSetsCyl'], 'NPropSetsCyl', '- Number of structurally unique circular cross-sections (if 0 the following table is ignored)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecD', 'XsecT']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(N/m2)','(N/m2)','(kg/m3)','(m)','(m)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NPropSetsCyl']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['PropSetID1'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['YoungE1'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['ShearG1'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MatDens1'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecD'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecT'][i])) - f.write(" ".join(ln) + '\n') - f.write('------------------ RECTANGULAR BEAM CROSS-SECTION PROPERTIES -----------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NPropSetsRec'], 'NPropSetsRec', '- Number of structurally unique rectangular cross-sections (if 0 the following table is ignored)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecSa', 'XsecSb', 'XsecT']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(N/m2)','(N/m2)','(kg/m3)','(m)','(m)','(m)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NPropSetsRec']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['PropSetID2'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['YoungE2'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['ShearG2'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MatDens2'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecSa'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecSb'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecT2'][i])) - f.write(" ".join(ln) + '\n') - f.write('----------------- ARBITRARY BEAM CROSS-SECTION PROPERTIES -----------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NXPropSets'], 'NXPropSets', '- Number of structurally unique arbitrary cross-sections (if 0 the following table is ignored)\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecA', 'XsecAsx', 'XsecAsy', 'XsecJxx', 'XsecJyy', 'XsecJ0', 'XsecJt']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(N/m2)','(N/m2)','(kg/m3)','(m2)','(m2)','(m2)','(m4)','(m4)','(m4)','(m4)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NXPropSets']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['PropSetID3'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['YoungE3'][i])) - ln.append('{:^11e}'.format(self.fst_vt['SubDyn']['ShearG3'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MatDens3'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecA'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecAsx'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecAsy'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecJxx'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecJyy'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecJ0'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['XsecJt'][i])) - f.write(" ".join(ln) + '\n') - f.write('-------------------------- CABLE PROPERTIES -------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NCablePropSets'], 'NCablePropSets', '- Number of cable cable properties\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'EA', 'MatDens', 'T0']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(N)','(kg/m)','(N)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NCablePropSets']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['CablePropSetID'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['CableEA'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['CableMatDens'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['CableT0'][i])) - f.write(" ".join(ln) + '\n') - f.write('----------------------- RIGID LINK PROPERTIES ------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NRigidPropSets'], 'NRigidPropSets', '- Number of rigid link properties\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['PropSetID', 'MatDens']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(kg/m)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NRigidPropSets']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['RigidPropSetID'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['RigidMatDens'][i])) - f.write(" ".join(ln) + '\n') - f.write('----------------------- SPRING ELEMENT PROPERTIES ------------------------------------\n') - spring_list = ['k11','k12','k13','k14','k15','k16', - 'k22','k23','k24','k25','k26', - 'k33','k34','k35','k36', - 'k44','k45','k46', - 'k55','k56', - 'k66'] - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NSpringPropSets'], 'NSpringPropSets', '- Number of spring properties\n')) - f.write("PropSetID k11 k12 k13 k14 k15 k16 k22 k23 k24 k25 k26 k33 k34 k35 k36 k44 k45 k46 k55 k56 k66 \n") - f.write(" (-) (N/m) (N/m) (N/m) (N/rad) (N/rad) (N/rad) (N/m) (N/m) (N/rad) (N/rad) (N/rad) (N/m) (N/rad) (N/rad) (N/rad) (Nm/rad) (Nm/rad) (Nm/rad) (Nm/rad) (Nm/rad) (Nm/rad) \n") - for i in range(self.fst_vt['SubDyn']['NSpringPropSets']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['SpringPropSetID'][i])) - for sl in spring_list: - ln.append('{:^11}'.format(self.fst_vt['SubDyn'][sl][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- MEMBER COSINE MATRICES COSM(i,j) ------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NCOSMs'], 'NCOSMs', '- Number of unique cosine matrices (i.e., of unique member alignments including principal axis rotations); ignored if NXPropSets=0 or 9999 in any element below\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['COSMID', 'COSM11', 'COSM12', 'COSM13', 'COSM21', 'COSM22', 'COSM23', 'COSM31', 'COSM32', 'COSM33']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(-)','(-)','(-)','(-)','(-)','(-)','(-)','(-)','(-)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NCOSMs']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['COSMID'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM11'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM12'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM13'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM21'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM22'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM23'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM31'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM32'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['COSM33'][i])) - f.write(" ".join(ln) + '\n') - f.write('------------------------ JOINT ADDITIONAL CONCENTRATED MASSES--------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NCmass'], 'NCmass', '- Number of joints with concentrated masses; Global Coordinate System\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['CMJointID','JMass','JMXX','JMYY','JMZZ','JMXY','JMXZ','JMYZ','MCGX','MCGY','MCGZ']])+'\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(kg)','(kg*m^2)','(kg*m^2)','(kg*m^2)','(kg*m^2)','(kg*m^2)','(kg*m^2)','(m)','(m)','(m)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NCmass']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['CMJointID'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMass'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMXX'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMYY'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMZZ'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMXY'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMXZ'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['JMYZ'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MCGX'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MCGY'][i])) - ln.append('{:^11}'.format(self.fst_vt['SubDyn']['MCGZ'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------------- OUTPUT: SUMMARY & OUTFILE ------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['SumPrint'], 'SumPrint', '- Output a Summary File (flag).It contains: matrices K,M and C-B reduced M_BB, M-BM, K_BB, K_MM(OMG^2), PHI_R, PHI_L. It can also contain COSMs if requested.\n')) - if 'OutCBModes' in self.fst_vt['SubDyn']: - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutCBModes'], 'OutCBModes', '- Output Guyan and Craig-Bampton modes {0 No output, 1 JSON output}, (flag)\n')) - if 'OutFEMModes' in self.fst_vt['SubDyn']: - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutFEMModes'], 'OutFEMModes', '- Output first 30 FEM modes {0 No output, 1 JSON output} (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutCOSM'], 'OutCOSM', '- Output cosine matrices with the selected output member forces (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutAll'], 'OutAll', "- [T/F] Output all members' end forces\n")) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutSwtch'], 'OutSwtch', '- [1/2/3] Output requested channels to: 1=.SD.out; 2=.out (generated by FAST); 3=both files.\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['TabDelim'], 'TabDelim', '- Generate a tab-delimited output in the .SD.out file\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutDec'], 'OutDec', '- Decimation of output in the .SD.out file\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutFmt'], 'OutFmt', '- Output format for numerical results in the .SD.out file\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['SubDyn']['OutSFmt'], 'OutSFmt', '- Output format for header strings in the .SD.out file\n')) - f.write('------------------------- MEMBER OUTPUT LIST ------------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['SubDyn']['NMOutputs'], 'NMOutputs', '- Number of members whose forces/displacements/velocities/accelerations will be output (-) [Must be <= 99].\n')) - f.write(" ".join(['{:^11s}'.format(i) for i in ['MemberID', 'NOutCnt', 'NodeCnt']])+' ! [NOutCnt=how many nodes to get output for [< 10]; NodeCnt are local ordinal numbers from the start of the member, and must be >=1 and <= NDiv+1] If NMOutputs=0 leave blank as well.\n') - f.write(" ".join(['{:^11s}'.format(i) for i in ['(-)','(-)','(-)']])+'\n') - for i in range(self.fst_vt['SubDyn']['NMOutputs']): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['MemberID_out'][i])) - ln.append('{:^11d}'.format(self.fst_vt['SubDyn']['NOutCnt'][i])) - ln.append(" ".join(['{:^11d}'.format(node) for node in self.fst_vt['SubDyn']['NodeCnt'][i]])) - f.write(" ".join(ln) + '\n') - f.write('------------------------- SDOutList: The next line(s) contains a list of output parameters that will be output in .SD.out or .out. ------\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['SubDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - f.write('END of output channels and end of file. (the word "END" must appear in the first 3 columns of this line)\n') - f.flush() - os.fsync(f) - f.close() - - def write_ExtPtfm(self): - # Generate ExtPtfm input files - - self.write_Superelement() - - if self.fst_vt['ExtPtfm']['HasConnections']: - self.write_Connections() - - if self.fst_vt['ExtPtfm']['HasUserForcing']: - self.write_UserForcing() - - if self.fst_vt['ExtPtfm']['HasConnForcing']: - self.write_ConnForcing() - - self.fst_vt['Fst']['SubFile'] = self.FAST_namingOut + '_ExtPtfm.dat' - ep_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['SubFile']) - f = open(ep_file, 'w') - - f.write('---------------------- EXTPTFM INPUT FILE --------------------------------------\n') - f.write('Comment describing the model\n') - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['DT'], 'DT', '- Communication interval for controllers (s) (or "default")\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['IntMethod'], 'IntMethod', '- Integration Method {1:RK4; 2:AB4, 3:ABM4} (switch)\n')) - f.write('---------------------- REDUCTION INPUTS ----------------------------------------\n') - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['RBMod'], 'RBMod', '- Method for handling rigid-body motion (switch) {0: No special handling for rigid-body motion; 1: Transform to rigid-body frame of reference; 2: Transform to rigid-body frame of reference and add fictitious forces and exact self-weight}\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['Red_FileName'], 'Red_FileName', '- Path of the file containing Guyan/Craig-Bampton inputs (-)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NActiveDOFList'], 'NActiveDOFList', '- Number of active CB mode listed in ActiveDOFList, use -1 for all modes (integer)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['ActiveDOFList']]), 'ActiveDOFList', '- List of CB modes index that are active, [unused if NActiveDOFList<=0]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NInitPosList'], 'NInitPosList', '- Number of initial positions listed in InitPosList, using 0 implies all DOF initialized to 0 (integer)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['InitPosList']]), 'InitPosList', '- List of initial positions for the CB modes [unused if NInitPosList<=0 or EquilStart=True]\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NInitVelList'], 'NInitVelList', '- Number of initial positions listed in InitVelList, using 0 implies all DOF initialized to 0 (integer)\n')) - f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['InitVelList']]), 'InitVelList', '- List of initial velocities for the CB modes [unused if NInitVelPosList<=0 or EquilStart=True]\n')) - f.write('---------------------- CONNECTION INPUTS ---------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['HasConnections'], 'Connections', '- Flag for connection points on the structure (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['Conn_FileName'], 'Conn_FileName', '- Path of the file containing connection points (-)\n')) - f.write('---------------------- USER FORCING INPUTS -------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['HasUserForcing'], 'UserForcing', '- Flag for user-prescribed modal forcing (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['Force_FileName'], 'Force_FileName', '- Path of the file containing user forcing time series (-)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['HasConnForcing'], 'ConnForcing', '- Flag for user-prescribed connection forcing (flag)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['FConn_FileName'], 'FConn_FileName', '- Path of the file containing user connection force time series (-)\n')) - f.write('---------------------- OUTPUT --------------------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['SumPrint'], 'SumPrint', '- Print summary data to .sum (flag)\n')) - f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['OutFile'], 'OutFile', '- Switch to determine where output will be placed: {1: in module output file only; 2: in glue code output file only; 3: both} (currently unused)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag) (currently unused)\n')) - f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['OutFmt'], 'OutFmt', '- Format used for text tabular output (except time). Resulting field should be 10 characters. (quoted string) (currently unused)\n')) - f.write('{:<22f} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['TStart'], 'TStart', '- Time to begin tabular output (s) (currently unused)\n')) - f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['ExtPtfm']) - - for channel_list in outlist: - for i in range(len(channel_list)): - f.write('"' + channel_list[i] + '"\n') - f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') - f.flush() - os.fsync(f) - f.close() - - - def write_Superelement(self): - - def toString(SuperElement): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L2034 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - s='' - s+='!Dimension: {}\n'.format(SuperElement['nDOF']) - - s+='\n!Mass Matrix\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in SuperElement['MassMatrix']) - - s+='\n\n!Stiffness Matrix\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in SuperElement['StiffnessMatrix']) - - s+='\n\n!Damping Matrix\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in SuperElement['DampingMatrix']) - - s+='\n\n!Weight constant\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in SuperElement['WeightConstant']) - - s+='\n\n!Weight stiffness matrix\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in SuperElement['WeightStiffness']) - - return s - - # Generate Superelement input file - self.fst_vt['ExtPtfm']['Red_FileName'] = self.FAST_namingOut + '_ExtPtfm_SE.dat' - se_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ExtPtfm']['Red_FileName']) - f = open(se_file, 'w') - - f.write(toString(self.fst_vt['ExtPtfm']['FlexASCII'])) - - f.flush() - os.fsync(f) - f.close() - - - def write_Connections(self): - - def toString(Connections): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L2034 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - s='' - s+='!nConn: {}\n'.format(Connections['nConn']) - - s+='\n!Connections\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in Connections['Position']) - - s+='\n\n!Displacement\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in Connections['Displacement']) - - return s - - # Generate Superelement input file - self.fst_vt['ExtPtfm']['Conn_FileName'] = self.FAST_namingOut + '_ExtPtfm_Conn.dat' - conn_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ExtPtfm']['Conn_FileName']) - f = open(conn_file, 'w') - - f.write(toString(self.fst_vt['ExtPtfm']['Connections'])) - - f.flush() - os.fsync(f) - f.close() - - - def write_UserForcing(self): - - def toString(UserForcing): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L2034 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - s='' - s+='!nSteps: {}\n'.format(UserForcing['nSteps']) - - s+='\n!Forcing:\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in UserForcing['ForceTimeSeries']) - - return s - - # Generate user forcing input file - self.fst_vt['ExtPtfm']['Force_FileName'] = self.FAST_namingOut + '_ExtPtfm_UserFrc.dat' - UserFrc_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ExtPtfm']['Force_FileName']) - f = open(UserFrc_file, 'w') - - f.write(toString(self.fst_vt['ExtPtfm']['UserForcing'])) - - f.flush() - os.fsync(f) - f.close() - - - def write_ConnForcing(self): - - def toString(ConnForcing): - # Function based on https://github.com/OpenFAST/openfast_toolbox/blob/353643ed917d113ec8dfd765813fef7d09752757/openfast_toolbox/io/fast_input_file.py#L2034 - # Developed by Emmanuel Branlard (https://github.com/ebranlard) - s='' - s+='!nSteps: {}\n'.format(ConnForcing['nSteps']) - - s+='\n!Forcing:\n' - s+='\n'.join(''.join('{:16.8e}'.format(x) for x in y) for y in ConnForcing['ForceTimeSeries']) - - return s - - # Generate user forcing input file - self.fst_vt['ExtPtfm']['FConn_FileName'] = self.FAST_namingOut + '_ExtPtfm_ConnFrc.dat' - ConnFrc_file = os.path.join(self.FAST_runDirectory, self.fst_vt['ExtPtfm']['FConn_FileName']) - f = open(ConnFrc_file, 'w') - - f.write(toString(self.fst_vt['ExtPtfm']['ConnForcing'])) - - f.flush() - os.fsync(f) - f.close() - - - def write_MAP(self): - - # Generate MAP++ input file - self.fst_vt['Fst']['MooringFile'] = self.FAST_namingOut + '_MAP.dat' - map_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['MooringFile']) - f = open(map_file, 'w') - - f.write('---------------------- LINE DICTIONARY ---------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['LineType', 'Diam', 'MassDenInAir', 'EA', 'CB', 'CIntDamp', 'Ca', 'Cdn', 'Cdt']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(m)', '(kg/m)', '(N)', '(-)', '(Pa-s)', '(-)', '(-)', '(-)']])+'\n') - ln =[] - for i in range(1): - ln = [] - ln.append('{:^11}'.format(self.fst_vt['MAP']['LineType'][i])) - ln.append('{:^11}'.format(self.fst_vt['MAP']['Diam'][i])) - ln.append('{:^11}'.format(self.fst_vt['MAP']['MassDenInAir'][i])) - ln.append('{:^11}'.format(self.fst_vt['MAP']['EA'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['CB'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['CIntDamp'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Ca'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Cdn'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Cdt'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- NODE PROPERTIES ---------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['Node', 'Type', 'X', 'Y', 'Z', 'M', 'B', 'FX', 'FY', 'FZ']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(m)', '(m)', '(kg)', '(m^3)', '(N)', '(N)', '(N)']])+'\n') - for i, type in enumerate(self.fst_vt['MAP']['Type']): - ln = [] - ln.append('{:<11}'.format(self.fst_vt['MAP']['Node'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Type'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['X'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Y'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['Z'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['M'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['B'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['FX'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['FY'][i])) - ln.append('{:<11}'.format(self.fst_vt['MAP']['FZ'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- LINE PROPERTIES ---------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['Line', 'LineType', 'UnstrLen', 'NodeAnch', 'NodeFair', 'Flags']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(-)', '(-)', '(-)']])+'\n') - for i in range(len(self.fst_vt['MAP']['Line'])): - ln = [] - ln.append('{:^11d}'.format(self.fst_vt['MAP']['Line'][i])) - ln.append('{:^11}'.format(self.fst_vt['MAP']['LineType'][i])) - ln.append('{:^11}'.format(self.fst_vt['MAP']['UnstrLen'][i])) - ln.append('{:^11d}'.format(self.fst_vt['MAP']['NodeAnch'][i])) - ln.append('{:^11d}'.format(self.fst_vt['MAP']['NodeFair'][i])) - # ln.append('{:^11}'.format(self.fst_vt['MAP']['Outputs'][i])) - ln.append('{:<11}'.format(" ".join(self.fst_vt['MAP']['Flags'][i]))) - f.write(" ".join(ln) + '\n') - ln =[] - f.write('---------------------- SOLVER OPTIONS-----------------------------------------\n') - f.write('{:<11s}'.format('Option'+'\n')) - f.write('{:<11s}'.format('(-)')+'\n') - for i in range(len(self.fst_vt['MAP']['Option'])): - ln = [] - ln.append('{:<11}'.format(" ".join(self.fst_vt['MAP']['Option'][i]))) - f.write("\n".join(ln) + '\n') - ln = [] - f.write('\n') # adding a blank line after all solver options - f.flush() - os.fsync(f) - f.close() - - def write_MoorDyn(self): - - self.fst_vt['Fst']['MooringFile'] = self.FAST_namingOut + '_MoorDyn.dat' - moordyn_file = os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['MooringFile']) - f = open(moordyn_file, 'w') - - f.write('--------------------- MoorDyn Input File ------------------------------------\n') - f.write('Generated with OpenFAST_IO\n') - f.write('Comments from seed file have been dropped. Output Channels are not yet fully supported\n') - f.write('NOTE: MoorDyn does not use ECHO, instead use WriteLog of 0, 1, 2, or 3 in the options list \n') - f.write('----------------------- LINE TYPES ------------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['Name', 'Diam', 'MassDen', 'EA', 'BA/-zeta', 'EI', 'Cd', 'Ca', 'CdAx', 'CaAx', 'Cl (optional)', 'dF (optional)', 'cF (optional)']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(m)', '(kg/m)', '(N)', '(N-s/-)', '(N-m^2)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)', '(-)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Name'])): - ln = [] - ln.append('{:<11}'.format(self.fst_vt['MoorDyn']['Name'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Diam'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['MassDen'][i])) - ln.append('|'.join([float_default_out(a, trim=True) for a in self.fst_vt['MoorDyn']['EA'][i]])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['BA_zeta'][i]])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['EI'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Cd'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Ca'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CdAx'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CaAx'][i])) - if self.fst_vt['MoorDyn']['Cl'][i] != None: - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Cl'][i])) - if self.fst_vt['MoorDyn']['dF'][i] != None: - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['dF'][i])) - if self.fst_vt['MoorDyn']['cF'][i] != None: - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['cF'][i])) - f.write(" ".join(ln) + '\n') - - if self.fst_vt['MoorDyn']['NonLinearEA'][i] != None: - self.write_NonLinearEA(self.fst_vt['MoorDyn']['EA'][i][0], self.fst_vt['MoorDyn']['Name'][i], self.fst_vt['MoorDyn']['NonLinearEA'][i]) - - if 'Rod_Name' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Rod_Name']: - f.write('----------------------- ROD TYPES ------------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['TypeName', 'Diam', 'Mass/m', 'Cd', 'Ca', 'CdEnd', 'CaEnd']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(name)', '(m)', '(kg/m)', '(-)', '(-)', '(-)', '(-)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Rod_Name'])): - ln = [] - ln.append('{:<11}'.format(self.fst_vt['MoorDyn']['Rod_Name'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_Diam'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_MassDen'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_Cd'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_Ca'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_CdEnd'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Rod_CaEnd'][i])) - f.write(" ".join(ln) + '\n') - - - if 'Body_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Body_ID']: - f.write('----------------------- BODIES ------------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'Attachement', 'X0', 'Y0', 'Z0', 'r0', 'p0','y0','Mass','CG*','I*','Volume','CdA*','Ca*']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(#)', '(word)', '(m)', '(m)', '(m)', '(deg)', '(deg)','(deg)','(kg)','(m)','(kg-m^2)','(m^3)','m^2','(kg/m^3)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Body_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Body_ID'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Body_Attachment'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['X0'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Y0'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Z0'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['r0'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['p0'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['y0'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['Body_Mass'][i])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Body_CG'][i]])) - ln.append('|'.join(['{:^.4e}'.format(a) for a in self.fst_vt['MoorDyn']['Body_I'][i]])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['Body_Volume'][i])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Body_CdA'][i]])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Body_Ca'][i]])) - f.write(" ".join(ln) + '\n') - - if 'Rod_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Rod_ID']: - f.write('----------------------- RODS ------------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'RodType', 'Attachment', 'Xa', 'Ya', 'Za', 'Xb','Yb','Zb','NumSegs','RodOutputs']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(#)', '(name)', '(word/ID)', '(m)', '(m)', '(m)', '(m)','(m)','(m)','(-)','(-)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Rod_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Rod_ID'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_Type'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Rod_Attachment'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Xa'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Ya'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Za'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Xb'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Yb'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Zb'][i])) - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['Rod_NumSegs'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['RodOutputs'][i])) - f.write(" ".join(ln) + '\n') - - - f.write('---------------------- POINTS --------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['ID', 'Attachment', 'X', 'Y', 'Z', 'M', 'V', 'CdA', 'CA']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(m)', '(m)', '(kg)', '(m^3)', '(m^2)', '(-)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Point_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Point_ID'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Attachment'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['X'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Y'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['Z'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['M'][i])) - ln.append('{:^11.4e}'.format(self.fst_vt['MoorDyn']['V'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CdA'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['CA'][i])) - f.write(" ".join(ln) + '\n') - f.write('---------------------- LINES --------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['Line', 'LineType', 'AttachA', 'AttachB', 'UnstrLen', 'NumSegs', 'Outputs']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(-)', '(-)', '(m)', '(-)', '(-)']])+'\n') - for i in range(len(self.fst_vt['MoorDyn']['Line_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Line_ID'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['LineType'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['AttachA'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['AttachB'][i])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['UnstrLen'][i])) - ln.append('{:^11d}'.format(self.fst_vt['MoorDyn']['NumSegs'][i])) - ln.append('{:^11}'.format(self.fst_vt['MoorDyn']['Outputs'][i])) - f.write(" ".join(ln) + '\n') - - if 'Failure_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['Failure_ID']: # there are failure inputs - f.write('---------------------- FAILURE ----------------------\n') - f.write('FailureID Point Line(s) FailTime FailTen\n') - f.write('() () (,) (s or 0) (N or 0)\n') - for i_line in range(len(self.fst_vt['MoorDyn']['Failure_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['Failure_ID'][i_line])) - ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['Failure_Point'][i_line])) - ln.append('{:^11s}'.format(','.join(['{:^d}'.format(a) for a in self.fst_vt['MoorDyn']['Failure_Line(s)'][i_line]]))) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['FailTime'][i_line])) - ln.append('{:^11.4f}'.format(self.fst_vt['MoorDyn']['FailTen'][i_line])) - f.write(" ".join(ln) + '\n') - - if 'ChannelID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['ChannelID']: # There are control inputs - f.write('---------------------- CONTROL ---------------------------------------\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['ChannelID', 'Line(s)']])+'\n') - f.write(" ".join(['{:<11s}'.format(i) for i in ['()', '(,)']])+'\n') - for i_line in range(len(self.fst_vt['MoorDyn']['ChannelID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['ChannelID'][i_line])) - ln.append(','.join(self.fst_vt['MoorDyn']['Lines_Control'][i_line])) - f.write(" ".join(ln) + '\n') - - if 'External_ID' in self.fst_vt['MoorDyn'] and self.fst_vt['MoorDyn']['External_ID']: # there are external load outputs - f.write('---------------------- EXTERNAL LOADS --------------------------------\n') - f.write('ID Object Fext Blin Bquad CSys\n') - f.write('(#) (name) (N) (Ns/m) (Ns^2/m^2) (-)\n') - for i_line in range(len(self.fst_vt['MoorDyn']['External_ID'])): - ln = [] - ln.append('{:<7d}'.format(self.fst_vt['MoorDyn']['External_ID'][i_line])) - ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['Object'][i_line])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Fext'][i_line]])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Blin'][i_line]])) - ln.append('|'.join(['{:^.4f}'.format(a) for a in self.fst_vt['MoorDyn']['Bquad'][i_line]])) - ln.append('{:^11s}'.format(self.fst_vt['MoorDyn']['CSys'][i_line])) - f.write(" ".join(ln) + '\n') - - f.write('---------------------- SOLVER OPTIONS ---------------------------------------\n') - for i in range(len(self.fst_vt['MoorDyn']['option_values'])): - - if self.fst_vt['MoorDyn']['option_names'][i].upper() == 'WATERKIN' and self.fst_vt['WaterKin']: - # WATERKIN needs to be a string, and should have already been read in and part of fst_vt - self.fst_vt['MoorDyn']['WaterKin_file'] = self.FAST_namingOut + '_WaterKin.dat' - f.write('{:<22} {:<11} {:}'.format('"'+self.fst_vt['MoorDyn']['WaterKin_file']+'"', self.fst_vt['MoorDyn']['option_names'][i], self.fst_vt['MoorDyn']['option_descriptions'][i]+'\n')) - elif self.fst_vt['MoorDyn']['option_names'][i].upper() in ['INERTIALF','WATERKIN']: - # These options need to be an integer - f.write('{:<22} {:<11} {:}'.format(int_default_out(self.fst_vt['MoorDyn']['option_values'][i]), self.fst_vt['MoorDyn']['option_names'][i], self.fst_vt['MoorDyn']['option_descriptions'][i]+'\n')) - else: # if not handle normally - f.write('{:<22} {:<11} {:}'.format(float_default_out(self.fst_vt['MoorDyn']['option_values'][i]), self.fst_vt['MoorDyn']['option_names'][i], self.fst_vt['MoorDyn']['option_descriptions'][i]+'\n')) - - f.write('------------------------ OUTPUTS --------------------------------------------\n') - outlist = self.get_outlist(self.fst_vt['outlist'], ['MoorDyn']) - for channel_list in outlist: - for i in range(len(channel_list)): - f.write(channel_list[i] + '\n') - f.write('------------------------- END --------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_WaterKin(self,WaterKin_file): - f = open(WaterKin_file, 'w') - - f.write('MoorDyn v2 Waves and Currents input file set up\n') - f.write('This file was written by FAST_writer.py, comments from seed file have been dropped.\n') - f.write('--------------------------- WAVES -------------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['WaveKinMod'], 'WaveKinMod', '- flag for waves (0 no waves; 1 compute waves using provided elevation timeseries; 2 use wave elevation data from seastate)\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['WaveKinFile'], 'WaveKinFile', '- file containing wave elevation time series at 0,0,0. This is ignored if WaveKinMod is not 1\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['dtWave'], 'dtWave', '- time step to use in setting up wave kinematics grid (s). This is ignored if WaveKinMod is not 1\n')) - f.write('{:<22} {:<11} {:}'.format(self.fst_vt['WaterKin']['WaveDir'], 'WaveDir', '- wave heading (deg). This is ignored if WaveKinMod is not 1\n')) - f.write('{:<22} {:}'.format(self.fst_vt['WaterKin']['X_Type'], '- X wave input type (0: not used; 1: list values in ascending order; 2: uniform specified by -xlim, xlim, num)\n')) - f.write('{:<22} {:}'.format(', '.join(['{:.3f}'.format(i) for i in self.fst_vt['WaterKin']['X_Grid']]), '- X wave grid point data separated by commas\n')) - f.write('{:<22} {:}'.format(self.fst_vt['WaterKin']['Y_Type'], '- Y wave input type (0: not used; 1: list values in ascending order; 2: uniform specified by -Ylim, Ylim, num)\n')) - f.write('{:<22} {:}'.format(', '.join(['{:.3f}'.format(i) for i in self.fst_vt['WaterKin']['Y_Grid']]), '- Y wave grid point data separated by commas\n')) - f.write('{:<22} {:}'.format(self.fst_vt['WaterKin']['Z_Type'], '- Z wave input type (0: not used; 1: list values in ascending order; 2: uniform specified by -Zlim, Zlim, num)\n')) - f.write('{:<22} {:}'.format(', '.join(['{:.3f}'.format(i) for i in self.fst_vt['WaterKin']['Z_Grid']]), '- Z wave grid point data separated by commas\n')) - f.write('--------------------------- CURRENT -------------------------------------\n') - f.write('{:} CurrentMod - flag for currents (0 no current; 1 use depth and current described below; 2 use current speed data from SeaState and depth spacing defined below) \n'.format(self.fst_vt['WaterKin']['CurrentMod'])) - # depending on CurrentMod, the rest of the WaterKin input file changes - if self.fst_vt['WaterKin']['CurrentMod'] == 2: # user provided depths - f.write('{:<22} {:}'.format(self.fst_vt['WaterKin']['current_Z_Type'], '- Z current input type (0: not used; 1: list values in ascending order; 2: uniform specified by -Zlim, Zlim, num). This is ignored unless CurrentMod = 2\n')) - f.write('{:<22} {:}'.format(', '.join(['{:.3f}'.format(i) for i in self.fst_vt['WaterKin']['current_Z_Grid']]), '- Z current grid point data separated by commas. This is ignored unless CurrentMod = 2\n')) - elif self.fst_vt['WaterKin']['CurrentMod'] == 1: # user provided depths and current speeds - f.write('z-depth x-current y-current - This table is ignored unless CurrentMod = 1\n') - f.write('(m) (m/s) (m/s)\n') - if self.fst_vt['WaterKin']['z-depth']: - for i in range(len(self.fst_vt['WaterKin']['z-depth'])): - f.write('{:.3f} {:.3f} {:.3f}'.format(self.fst_vt['WaterKin']['z-depth'][i], self.fst_vt['WaterKin']['x-current'][i], self.fst_vt['WaterKin']['y-current'][i])) - f.write('--------------------- need this line ------------------\n') - - f.flush() - os.fsync(f) - f.close() - - def write_NonLinearEA(self,Stiffness_file, Linetype, NonLinearEA): - f = open(Stiffness_file, 'w') - - f.write('---{:}---\n'.format(Linetype)) - f.write('Strain Tension\n') - f.write('(-) (N)\n') - for i in range(len(NonLinearEA["Strain"])): - f.write('{:.3f} {:.3f}'.format(NonLinearEA["Strain"][i], NonLinearEA["Tension"][i])) - - f.flush() - os.fsync(f) - f.close() - - - def write_StC(self,StC_vt,StC_filename): - - sd_file = os.path.normpath(os.path.join(self.FAST_runDirectory, self.fst_vt['Fst']['ServoFile'])) - stc_file = os.path.join(os.path.dirname(sd_file), StC_filename) - f = open(stc_file, 'w') - - f.write('------- STRUCTURAL CONTROL (StC) INPUT FILE ----------------------------\n') - f.write('Generated with OpenFAST_IO\n') - - f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['Echo'], 'Echo', '- Echo input data to ".SD.ech" (flag)\n')) - - f.write('---------------------- StC DEGREES OF FREEDOM ----------------------------------\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_DOF_MODE'], 'StC_DOF_MODE', '- DOF mode (switch) {0: No StC or TLCD DOF; 1: StC_X_DOF, StC_Y_DOF, and/or StC_Z_DOF (three independent StC DOFs); 2: StC_XY_DOF (Omni-Directional StC); 3: StC_XYZ_DOF (Omni-Directional StC); 5: TLCD; 6: Prescribed force/moment time series; 7: Force determined by external DLL}\n')) - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['StC_X_DOF'], 'StC_X_DOF', '- DOF on or off for StC X (flag) [Used only when StC_DOF_MODE=1]\n')) - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['StC_Y_DOF'], 'StC_Y_DOF', '- DOF on or off for StC Y (flag) [Used only when StC_DOF_MODE=1]\n')) - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['StC_Z_DOF'], 'StC_Z_DOF', '- DOF on or off for StC Z (flag) [Used only when StC_DOF_MODE=1]\n')) - - f.write('---------------------- StC LOCATION ------------------------------------------- [relative to the reference origin of component attached to]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_P_X'], 'StC_P_X', '- At rest X position of StC (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_P_Y'], 'StC_P_Y', '- At rest Y position of StC (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_P_Z'], 'StC_P_Z', '- At rest Z position of StC (m)\n')) - - f.write('---------------------- StC INITIAL CONDITIONS --------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_DSP'], 'StC_X_DSP', '- StC X initial displacement (m) [relative to at rest position]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_DSP'], 'StC_Y_DSP', '- StC Y initial displacement (m) [relative to at rest position]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_DSP'], 'StC_Z_DSP', '- StC Z initial displacement (m) [relative to at rest position; used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['StC_Z_PreLd'], 'StC_Z_PreLd', '- StC Z pre-load (N) {"gravity" to offset for gravity load; "none" or 0 to turn off} [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - - f.write('---------------------- StC CONFIGURATION -------------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_PSP'], 'StC_X_PSP', '- Positive stop position (maximum X mass displacement) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_NSP'], 'StC_X_NSP', '- Negative stop position (minimum X mass displacement) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_PSP'], 'StC_Y_PSP', '- Positive stop position (maximum Y mass displacement) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_NSP'], 'StC_Y_NSP', '- Negative stop position (minimum Y mass displacement) (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_PSP'], 'StC_Z_PSP', '- Positive stop position (maximum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_NSP'], 'StC_Z_NSP', '- Negative stop position (minimum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - - f.write('---------------------- StC MASS, STIFFNESS, & DAMPING ------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_M'], 'StC_X_M', '- StC X mass (kg) [used only when StC_DOF_MODE=1 and StC_X_DOF=TRUE]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_M'], 'StC_Y_M', '- StC Y mass (kg) [used only when StC_DOF_MODE=1 and StC_Y_DOF=TRUE]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_M'], 'StC_Z_M', '- StC Z mass (kg) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Omni_M'], 'StC_Omni_M', '- StC omni mass (kg) [used only when StC_DOF_MODE=2 or 3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_K'], 'StC_X_K', '- StC X stiffness (N/m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_K'], 'StC_Y_K', '- StC Y stiffness (N/m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_K'], 'StC_Z_K', '- StC Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_C'], 'StC_X_C', '- StC X damping (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_C'], 'StC_Y_C', '- StC Y damping (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_C'], 'StC_Z_C', '- StC Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_KS'], 'StC_X_KS', '- Stop spring X stiffness (N/m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_KS'], 'StC_Y_KS', '- Stop spring Y stiffness (N/m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_KS'], 'StC_Z_KS', '- Stop spring Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_CS'], 'StC_X_CS', '- Stop spring X damping (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_CS'], 'StC_Y_CS', '- Stop spring Y damping (N/(m/s))\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_CS'], 'StC_Z_CS', '- Stop spring Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - - f.write('---------------------- StC USER-DEFINED SPRING FORCES ------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['Use_F_TBL'], 'Use_F_TBL', '- Use spring force from user-defined table (flag)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['NKInpSt'], 'NKInpSt', '- Number of spring force input stations\n')) - - f.write('---------------------- StC SPRING FORCES TABLE -------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') - f.write('X F_X Y F_Y Z F_Z\n') - f.write('(m) (N) (m) (N) (m) (N)\n') - table = StC_vt['SpringForceTable'] - for x, f_x, y, f_y, z, f_z in zip(table['X'],table['F_X'],table['Y'],table['F_Y'],table['Z'],table['F_Z']): - row = [x, f_x, y, f_y, z, f_z] - f.write(' '.join(['{: 2.8e}'.format(val) for val in row])+'\n') - - f.write('---------------------- StructUserProp CONTROL -------------------------------------------- [used only when StC_DOF_MODE=1, 2, 3, or 7]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_CMODE'], 'StC_CMODE', '- Control mode (switch) {0:none; 1: Semi-Active Control Mode; 3: Active Control Mode through user subroutine; 4: Active Control Mode through Simulink (not available); 5: Active Control Mode through Bladed interface}\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_CChan'], 'StC_CChan', '- Control channel group (1:10) for stiffness and damping (StC_[XYZ]_K, StC_[XYZ]_C, and StC_[XYZ]_Brake) (specify additional channels for blade instances of StC active control -- one channel per blade) [used only when StC_DOF_MODE=1, 2, 3, or 7, and StC_CMODE=4 or 5]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_SA_MODE'], 'StC_SA_MODE', '- Semi-Active control mode {1: velocity-based ground hook control; 2: Inverse velocity-based ground hook control; 3: displacement-based ground hook control 4: Phase difference Algorithm with Friction Force 5: Phase difference Algorithm with Damping Force} (-)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_C_HIGH'], 'StC_X_C_HIGH', '- StC X high damping for ground hook control\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_C_LOW'], 'StC_X_C_LOW', '- StC X low damping for ground hook control\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_C_HIGH'], 'StC_Y_C_HIGH', '- StC Y high damping for ground hook control\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_C_LOW'], 'StC_Y_C_LOW', '- StC Y low damping for ground hook control\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_C_HIGH'], 'StC_Z_C_HIGH', '- StC Z high damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_C_LOW'], 'StC_Z_C_LOW', '- StC Z low damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_X_C_BRAKE'], 'StC_X_C_BRAKE', '- StC X high damping for braking the StC (Don''t use it now. should be zero)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Y_C_BRAKE'], 'StC_Y_C_BRAKE', '- StC Y high damping for braking the StC (Don''t use it now. should be zero)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['StC_Z_C_BRAKE'], 'StC_Z_C_BRAKE', '- StC Z high damping for braking the StC (Don''t use it now. should be zero) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) - - f.write('---------------------- TLCD --------------------------------------------------- [used only when StC_DOF_MODE=5]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['L_X'], 'L_X', '- X TLCD total length (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['B_X'], 'B_X', '- X TLCD horizontal length (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['area_X'], 'area_X', '- X TLCD cross-sectional area of vertical column (m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['area_ratio_X'], 'area_ratio_X', '- X TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['headLossCoeff_X'], 'headLossCoeff_X', '- X TLCD head loss coeff (-)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['rho_X'], 'rho_X', '- X TLCD liquid density (kg/m^3)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['L_Y'], 'L_Y', '- Y TLCD total length (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['B_Y'], 'B_Y', '- Y TLCD horizontal length (m)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['area_Y'], 'area_Y', '- Y TLCD cross-sectional area of vertical column (m^2)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['area_ratio_Y'], 'area_ratio_Y', '- Y TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['headLossCoeff_Y'], 'headLossCoeff_Y', '- Y TLCD head loss coeff (-)\n')) - f.write('{:<22} {:<11} {:}'.format(StC_vt['rho_Y'], 'rho_Y', '- Y TLCD liquid density (kg/m^3)\n')) - - f.write('---------------------- PRESCRIBED TIME SERIES --------------------------------- [used only when StC_DOF_MODE=6]\n') - f.write('{:<22} {:<11} {:}'.format(StC_vt['PrescribedForcesCoord'], 'PrescribedForcesCoord', '- Prescribed forces are in global or local coordinates (switch) {1: global; 2: local}\n')) - f.write('{!s:<22} {:<11} {:}'.format(StC_vt['PrescribedForcesFile'], 'PrescribedForcesFile', '- Time series force and moment (7 columns of time, FX, FY, FZ, MX, MY, MZ)\n')) - f.write('-------------------------------------------------------------------------------\n') - - f.flush() - os.fsync(f) - f.close() - -if __name__=="__main__": - - from openfast_io.FAST_reader import InputReader_OpenFAST - from openfast_io.FileTools import check_rtest_cloned - from pathlib import Path - - fst_update = {} - fst_update['Fst', 'TMax'] = 20. - fst_update['AeroDyn', 'TwrAero'] = False - - parent_dir = os.path.dirname( os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) ) + os.sep - build_of_io_dir = os.path.join(parent_dir, 'build_ofio') - Path(build_of_io_dir).mkdir(parents=True, exist_ok=True) - - # Read the model - fast = InputReader_OpenFAST() - fast.FAST_InputFile = '5MW_Land_BD_DLL_WTurb.fst' # FAST input file (ext=.fst) - fast.FAST_directory = os.path.join(parent_dir, 'reg_tests', 'r-test', - 'glue-codes', 'openfast', - '5MW_Land_BD_DLL_WTurb') # Path to fst directory files - - check_rtest_cloned(os.path.join(fast.FAST_directory)) - - fast.execute() - - # Write out the model - fastout = InputWriter_OpenFAST() - fastout.fst_vt = fast.fst_vt - fastout.FAST_runDirectory = os.path.join(build_of_io_dir,'fast_write_main_test') - fastout.FAST_namingOut = '5MW_Land_BD_DLL_WTurb_write' - fastout.update(fst_update=fst_update) - fastout.execute() +""" +FAST_writer.py -- backwards-compatible facade over the new composable IO layer. + +This file preserves the public API that WEIS and other downstream code depends +on (``InputWriter_OpenFAST``, ``update``, ``get_outlist``, ``update_outlist``, +helper functions, etc.) while delegating all heavy lifting to +``OpenFASTDriver.write()`` and the per-module IO classes under ``openfast_io.io``. + +The original monolithic implementation is available in git history +(branch ``openfast_io_arch~1``) for reference. +""" +from __future__ import annotations + +import copy +import os +import operator +from functools import reduce +from pathlib import Path + +import numpy as np + + +# -- Re-exported helper functions (external code may import these) ------------- + +def auto_format(f, var): + """Error handling for variables with 'Default' options.""" + if isinstance(var, str): + f.write('{:}\n'.format(var)) + elif isinstance(var, int): + f.write('{:3}\n'.format(var)) + elif isinstance(var, float): + f.write('{: 2.15e}\n'.format(var)) + + +def float_default_out(val, trim=False): + """Formatted float output when 'default' is an option.""" + if type(val) is float: + return '{:.4f}'.format(val) if trim else '{: 22f}'.format(val) + else: + return '{:}'.format(val) if trim else '{:<22}'.format(val) + + +def int_default_out(val, trim=False): + """Formatted int output when 'default' is an option.""" + if type(val) is float: + return '{:d}'.format(int(val)) if trim else '{:<22d}'.format(int(val)) + else: + return '{:}'.format(val) if trim else '{:<22}'.format(val) + + +def get_dict(vartree, branch): + """Given a list of nested dictionary keys, return the dict at that point.""" + return reduce(operator.getitem, branch, vartree) + + +# -- New driver layer ---------------------------------------------------------- +from openfast_io.drivers.openfast import OpenFASTDriver + + +class InputWriter_OpenFAST: + """OpenFAST input file writer -- backwards-compatible facade. + + .. deprecated:: + This facade class is deprecated and will be removed in a future version. + Use ``openfast_io.drivers.openfast.OpenFASTDriver`` directly instead. + + Usage is identical to the legacy writer:: + + writer = InputWriter_OpenFAST() + writer.fst_vt = reader.fst_vt + writer.FAST_runDirectory = '/output/path' + writer.FAST_namingOut = 'my_case' + writer.update(fst_update={'Fst': {'TMax': 20.0}}) + writer.execute() + """ + + def __init__(self): + import warnings + warnings.warn( + "InputWriter_OpenFAST is deprecated. " + "Use openfast_io.drivers.openfast.OpenFASTDriver directly.", + DeprecationWarning, + stacklevel=2, + ) + self.FAST_namingOut = None # Base name for output files + self.FAST_runDirectory = None # Output directory + self.fst_vt = {} + self.fst_update = {} + self._driver = OpenFASTDriver() + + # -- Core API -------------------------------------------------------------- + + def execute(self): + """Write all enabled module input files. Delegates to ``OpenFASTDriver.write()``.""" + if self.FAST_runDirectory is None: + raise ValueError('FAST_runDirectory must be set before calling execute()') + if self.FAST_namingOut is None: + raise ValueError('FAST_namingOut must be set before calling execute()') + + if not os.path.exists(self.FAST_runDirectory): + os.makedirs(self.FAST_runDirectory) + + self._driver.write( + self.fst_vt, + Path(self.FAST_runDirectory), + self.FAST_namingOut, + ) + + # -- Update helpers (unchanged from legacy -- pure dict manipulation) ------ + + def update(self, fst_update={}): + """Apply user-supplied overrides to ``self.fst_vt``.""" + if fst_update: + self.fst_update = fst_update + + def loop_dict(vartree, branch): + for var in vartree.keys(): + branch_i = copy.copy(branch) + branch_i.append(var) + if type(vartree[var]) is dict: + loop_dict(vartree[var], branch_i) + else: + try: + get_dict(self.fst_vt, branch_i[:-1])[branch_i[-1]] = ( + get_dict(self.fst_update, branch_i[:-1])[branch_i[-1]] + ) + except (KeyError, TypeError): + pass + + if self.fst_update: + # Check if keys are tuples (WEIS-style: {('Fst','TMax'): 20.0}) + first_key = next(iter(self.fst_update)) + if isinstance(first_key, tuple): + fst_update_orig = copy.copy(self.fst_update) + self.fst_update = {} + for var_list in fst_update_orig.keys(): + branch = [] + for i, var in enumerate(var_list[0:-1]): + if var not in get_dict(self.fst_update, branch).keys(): + get_dict(self.fst_update, branch)[var] = {} + branch.append(var) + get_dict(self.fst_update, branch)[var_list[-1]] = fst_update_orig[var_list] + + loop_dict(self.fst_update, []) + + # -- Outlist helpers (unchanged from legacy) ------------------------------- + + def get_outlist(self, vartree_head, channel_list=[]): + """Recursively find values set to True in the nested outlist dict.""" + + def loop_dict(vartree, outlist_i): + for var in vartree.keys(): + if type(vartree[var]) is dict: + loop_dict(vartree[var], outlist_i) + else: + if vartree[var]: + outlist_i.append(var) + return outlist_i + + if not channel_list: + channel_list = vartree_head.keys() + + outlist = [] + for var in channel_list: + var = var.replace(' ', '') + outlist_i = loop_dict(vartree_head[var], []) + if outlist_i: + outlist.append(sorted(outlist_i)) + + return outlist + + def update_outlist(self, channels): + """Set output channels to specified boolean values.""" + + def get_outlist_dict(vartree, branch): + return reduce(operator.getitem, branch, self.fst_vt['outlist']) + + def set_outlist_dict(vartree, branch, val): + get_outlist_dict(vartree, branch[:-1])[branch[-1]] = val + + def loop_dict(vartree, search_var, val, branch): + for var in vartree.keys(): + branch_i = copy.copy(branch) + branch_i.append(var) + if type(vartree[var]) is dict: + loop_dict(vartree[var], search_var, val, branch_i) + else: + if var == search_var: + set_outlist_dict(self.fst_vt['outlist'], branch_i, val) + + channel_list = channels.keys() + for var in channel_list: + val = channels[var] + var = var.replace(' ', '') + loop_dict(self.fst_vt['outlist'], var, val, []) + + +if __name__ == "__main__": + from openfast_io.FAST_reader import InputReader_OpenFAST + from openfast_io.FileTools import check_rtest_cloned + + fst_update = {} + fst_update['Fst', 'TMax'] = 20. + fst_update['AeroDyn', 'TwrAero'] = False + + parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + os.sep + build_of_io_dir = os.path.join(parent_dir, 'build_ofio') + Path(build_of_io_dir).mkdir(parents=True, exist_ok=True) + + # Read the model + fast = InputReader_OpenFAST() + fast.FAST_InputFile = '5MW_Land_BD_DLL_WTurb.fst' + fast.FAST_directory = os.path.join( + parent_dir, 'reg_tests', 'r-test', + 'glue-codes', 'openfast', + '5MW_Land_BD_DLL_WTurb', + ) + check_rtest_cloned(fast.FAST_directory) + fast.execute() + + # Write out the model + fastout = InputWriter_OpenFAST() + fastout.fst_vt = fast.fst_vt + fastout.FAST_runDirectory = os.path.join(build_of_io_dir, 'fast_write_main_test') + fastout.FAST_namingOut = '5MW_Land_BD_DLL_WTurb_write' + fastout.update(fst_update=fst_update) + fastout.execute() diff --git a/openfast_io/openfast_io/drivers/aerodisk_driver.py b/openfast_io/openfast_io/drivers/aerodisk_driver.py new file mode 100644 index 0000000000..919c68cd96 --- /dev/null +++ b/openfast_io/openfast_io/drivers/aerodisk_driver.py @@ -0,0 +1,141 @@ +"""AeroDisk standalone driver — reads/writes AeroDisk driver input files (.dvr). + +Uses right-aligned values and ``@filename.csv`` syntax for timeseries data. +Handles geometry, environment, and case analysis parameters. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Dict + +from ..io.aerodisk import AeroDiskIO +from ..parsing import bool_read, float_read, int_read, quoted_read + + +class AeroDiskStandaloneDriver: + """Reads and writes AeroDisk standalone driver input files (.dvr).""" + + def __init__(self) -> None: + self._aerodisk = AeroDiskIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, dvr_path: Path) -> dict: + """Read an AeroDisk driver file and the referenced AeroDisk input. + + Returns a dict with keys: + ``'AeroDiskDriver'`` — driver-level parameters + ``'AeroDisk'`` — from AeroDiskIO + """ + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr: Dict[str, Any] = {} + + with open(dvr_path) as f: + # --- Header --- + f.readline() # title line + dvr['description'] = f.readline().rstrip() + + # --- Echo --- + f.readline() # separator + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- Input files --- + f.readline() # separator + dvr['ADskIptFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + + # --- Geometry and Environment --- + f.readline() # section header + dvr['AirDens'] = float_read(f.readline().split()[0]) + dvr['RotorRad'] = float_read(f.readline().split()[0]) + dvr['RotorHeight'] = float_read(f.readline().split()[0]) + dvr['ShftTilt'] = float_read(f.readline().split()[0]) + + # --- Case Analysis --- + f.readline() # section header + dvr['TStart'] = float_read(f.readline().split()[0]) + ln = f.readline().split() + dvr['DT'] = ln[0].strip('"') # may be "DEFAULT" + ln = f.readline().split() + dvr['NumTimeSteps'] = ln[0].strip('"') # may be "DEFAULT" or int + + # Column headers and units + f.readline() # column headers + f.readline() # column units + + # Timeseries: either @filename or inline data + line = f.readline().strip() + if line.startswith('@'): + dvr['TimeseriesFile'] = line[1:] # strip @ prefix + dvr['TimeseriesData'] = [] + else: + dvr['TimeseriesFile'] = '' + rows = [] + while line and not line.upper().startswith('END'): + parts = line.split() + if parts: + rows.append([float_read(p) for p in parts]) + line = f.readline().strip() + dvr['TimeseriesData'] = rows + + result: Dict[str, Any] = {'AeroDiskDriver': dvr} + + # --- Delegate to AeroDiskIO --- + adsk_file = dvr.get('ADskIptFile', '') + adsk_path = os.path.normpath(os.path.join(str(base_dir), adsk_file)) + if adsk_file and os.path.isfile(adsk_path): + try: + adsk_data = self._aerodisk.read(Path(adsk_path), base_dir) + result.update(adsk_data) + except Exception: + pass # module file parse failure — driver data still valid + + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, dvr_path: Path) -> None: + """Write an AeroDisk driver file and the referenced AeroDisk input.""" + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr = data['AeroDiskDriver'] + + with open(dvr_path, 'w') as f: + f.write('------- AeroDisk driver v1.0 INPUT FILE ----------------------------------------------------------------------\n') + f.write(dvr.get('description', 'Generated by OpenFAST_IO') + '\n') + f.write('--------------------------------------------------------------------------------------------------------------\n') + f.write('{!s:<35} {:<15} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo input data to .ech (flag)')) + f.write('--------------------------------------------------------------------------------------------------------------\n') + + adsk_name = dvr.get('ADskIptFile', 'adsk_primary.inp') + f.write('{:<35} {:<15} {:}\n'.format('"' + adsk_name + '"', 'ADskIptFile', '- AeroDisk input file')) + f.write('{:<35} {:<15} {:}\n'.format('"' + dvr.get('OutRootName', 'adsk_driver') + '"', 'OutRootName', '- RootName for output files')) + f.write('----- Geometry and Environment -------------------------------------------------------------------------------\n') + f.write('{:<35} {:<15} {:}\n'.format(dvr['AirDens'], 'AirDens', '- Air density (kg/m^3)')) + f.write('{:<35} {:<15} {:}\n'.format(dvr['RotorRad'], 'RotorRad', '- Rotor radius (m)')) + f.write('{:<35} {:<15} {:}\n'.format(dvr['RotorHeight'], 'RotorHeight', '- Rotor height (m)')) + f.write('{:<35} {:<15} {:}\n'.format(dvr['ShftTilt'], 'ShftTilt', '- Shaft tilt (deg)')) + f.write('----- Case Analysis ------------------------------------------------------------------------------------------\n') + f.write('{:<35} {:<15} {:}\n'.format(dvr['TStart'], 'TStart', '- Start time (currently unused)')) + f.write('{:<35} {:<15} {:}\n'.format('"' + str(dvr['DT']) + '"', 'DT', '- Time step (DEFAULT to use smallest DT from input series)')) + f.write('{:<35} {:<15} {:}\n'.format(str(dvr['NumTimeSteps']), 'NumTimeSteps', '- Number of timesteps (DEFAULT to run to end of time)')) + f.write('Time WndSpeed_X WndSpeed_Y WndSpeed_Z RotSpd Pitch Yaw\n') + f.write('(s) (m/s) (m/s) (m/s) (rpm) (deg) (deg)\n') + ts_file = dvr.get('TimeseriesFile', '') + if ts_file: + f.write('@{}\n'.format(ts_file)) + else: + for row in dvr.get('TimeseriesData', []): + f.write(' '.join(str(v) for v in row) + '\n') + + # --- Delegate to AeroDiskIO --- + if 'AeroDisk' in data: + adsk_path = os.path.normpath(os.path.join(str(base_dir), adsk_name)) + try: + self._aerodisk.write({'AeroDisk': data['AeroDisk']}, adsk_path, str(base_dir)) + except Exception: + pass diff --git a/openfast_io/openfast_io/drivers/aerodyn_driver.py b/openfast_io/openfast_io/drivers/aerodyn_driver.py new file mode 100644 index 0000000000..c043947ce3 --- /dev/null +++ b/openfast_io/openfast_io/drivers/aerodyn_driver.py @@ -0,0 +1,389 @@ +"""AeroDyn standalone driver — reads/writes AeroDyn driver input files (.dvr). + +Supports all three analysis types: + 1 = multiple turbines, one simulation + 2 = one turbine, time-dependent simulation + 3 = one turbine, combined cases + +Both BasicHAWTFormat and advanced (generic) turbine geometry are supported. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict, List, Optional + +from ..io.aerodyn import AeroDynIO +from ..io.inflowwind import InflowWindIO +from ..io.seastate import SeaStateIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, fmt_field, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +def _fw(val, width: int = 22) -> str: + """Format a value field with guaranteed 2-space separator.""" + return fmt_field(val, min_width=width) + + +class AeroDynStandaloneDriver: + """Reads and writes AeroDyn standalone driver input files (.dvr).""" + + def __init__(self) -> None: + self._aerodyn = AeroDynIO() + self._inflowwind = InflowWindIO() + self._seastate = SeaStateIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, dvr_path: Path) -> dict: + """Read an AeroDyn driver file and all referenced module files. + + Returns a dict with keys: + ``'AeroDynDriver'`` — driver-level parameters + ``'AeroDyn'``, ``'AeroDynBlade'``, etc. — from AeroDynIO + ``'InflowWind'`` — from InflowWindIO (if referenced) + """ + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr: Dict[str, Any] = {} + + with open(dvr_path) as f: + # --- Header --- + f.readline() # title line + dvr['description'] = f.readline().rstrip() + + # --- Input Configuration --- + f.readline() # section header + dvr['Echo'] = bool_read(f.readline().split()[0]) + dvr['MHK'] = int_read(f.readline().split()[0]) + dvr['AnalysisType'] = int_read(f.readline().split()[0]) + dvr['TMax'] = float_read(f.readline().split()[0]) + dvr['DT'] = float_read(f.readline().split()[0]) + dvr['AeroFile'] = quoted_read(f.readline().split()[0]) + + # --- Environmental Conditions --- + f.readline() # section header + dvr['FldDens'] = float_read(f.readline().split()[0]) + dvr['KinVisc'] = float_read(f.readline().split()[0]) + dvr['SpdSound'] = float_read(f.readline().split()[0]) + dvr['Patm'] = float_read(f.readline().split()[0]) + dvr['Pvap'] = float_read(f.readline().split()[0]) + dvr['WtrDpth'] = float_read(f.readline().split()[0]) + + # --- Inflow Data --- + f.readline() # section header + dvr['CompInflow'] = int_read(f.readline().split()[0]) + dvr['InflowFile'] = quoted_read(f.readline().split()[0]) + dvr['HWindSpeed'] = float_read(f.readline().split()[0]) + dvr['RefHt'] = float_read(f.readline().split()[0]) + dvr['PLExp'] = float_read(f.readline().split()[0]) + + # --- SeaState Data --- + f.readline() # section header + dvr['CompSeaSt'] = int_read(f.readline().split()[0]) + dvr['SeaStFile'] = quoted_read(f.readline().split()[0]) + + # --- Turbine Data --- + f.readline() # section header + dvr['NumTurbines'] = int_read(f.readline().split()[0]) + + # --- Per-turbine data --- + turbines: List[Dict[str, Any]] = [] + for i_turb in range(dvr['NumTurbines']): + turb: Dict[str, Any] = {} + f.readline() # turbine section header + + ln = f.readline().split() + turb['BasicHAWTFormat'] = bool_read(ln[0]) + + # Read origin common to both formats + turb['BaseOriginInit'] = _read_csv_vec(f.readline()) + + if turb['BasicHAWTFormat']: + # Basic HAWT format: 8 more lines + turb['NumBlades'] = int_read(f.readline().split()[0]) + turb['HubRad'] = float_read(f.readline().split()[0]) + turb['HubHt'] = float_read(f.readline().split()[0]) + turb['Overhang'] = float_read(f.readline().split()[0]) + turb['ShftTilt'] = float_read(f.readline().split()[0]) + turb['Precone'] = float_read(f.readline().split()[0]) + turb['Twr2Shft'] = float_read(f.readline().split()[0]) + else: + # Advanced (generic) format + turb['BaseOrientationInit'] = _read_csv_vec(f.readline()) + turb['HasTower'] = bool_read(f.readline().split()[0]) + turb['HAWTprojection'] = bool_read(f.readline().split()[0]) + turb['TwrOrigin_t'] = _read_csv_vec(f.readline()) + turb['NacOrigin_t'] = _read_csv_vec(f.readline()) + turb['HubOrigin_n'] = _read_csv_vec(f.readline()) + turb['HubOrientation_n'] = _read_csv_vec(f.readline()) + + # Blades section + f.readline() # blade section header + turb['NumBlades'] = int_read(f.readline().split()[0]) + n_bld = turb['NumBlades'] + turb['BldOrigin_h'] = [_read_csv_vec(f.readline()) for _ in range(n_bld)] + turb['BldOrientation_h'] = [_read_csv_vec(f.readline()) for _ in range(n_bld)] + turb['BldHubRad_bl'] = [float_read(f.readline().split()[0]) for _ in range(n_bld)] + + # --- Motion (AnalysisType=1) --- + f.readline() # motion section header + if turb['BasicHAWTFormat']: + turb['BaseMotionType'] = int_read(f.readline().split()[0]) + turb['DegreeOfFreedom'] = int_read(f.readline().split()[0]) + turb['Amplitude'] = float_read(f.readline().split()[0]) + turb['Frequency'] = float_read(f.readline().split()[0]) + turb['BaseMotionFileName'] = quoted_read(f.readline().split()[0]) + turb['NacYaw'] = float_read(f.readline().split()[0]) + turb['RotSpeed'] = float_read(f.readline().split()[0]) + turb['BldPitch'] = float_read(f.readline().split()[0]) + else: + turb['BaseMotionType'] = int_read(f.readline().split()[0]) + turb['DegreeOfFreedom'] = int_read(f.readline().split()[0]) + turb['Amplitude'] = float_read(f.readline().split()[0]) + turb['Frequency'] = float_read(f.readline().split()[0]) + turb['BaseMotionFileName'] = quoted_read(f.readline().split()[0]) + turb['NacMotionType'] = int_read(f.readline().split()[0]) + turb['NacYaw'] = float_read(f.readline().split()[0]) + turb['NacMotionFileName'] = quoted_read(f.readline().split()[0]) + turb['RotMotionType'] = int_read(f.readline().split()[0]) + turb['RotSpeed'] = float_read(f.readline().split()[0]) + turb['RotMotionFileName'] = quoted_read(f.readline().split()[0]) + turb['BldMotionType'] = int_read(f.readline().split()[0]) + n_bld = turb['NumBlades'] + turb['BldPitch'] = [float_read(f.readline().split()[0]) for _ in range(n_bld)] + turb['BldMotionFileName'] = [quoted_read(f.readline().split()[0]) for _ in range(n_bld)] + + turbines.append(turb) + + dvr['Turbines'] = turbines + + # --- Time-dependent Analysis --- + f.readline() # section header + dvr['TimeAnalysisFileName'] = quoted_read(f.readline().split()[0]) + + # --- Combined-Case Analysis --- + f.readline() # section header + dvr['NumCases'] = int_read(f.readline().split()[0]) + f.readline() # column headers + f.readline() # column units + cases: List[Dict[str, float]] = [] + for _ in range(dvr['NumCases']): + ln = f.readline().split() + cases.append({ + 'HWndSpeed': float_read(ln[0]), + 'PLExp': float_read(ln[1]), + 'RotSpd': float_read(ln[2]), + 'Pitch': float_read(ln[3]), + 'Yaw': float_read(ln[4]), + 'dT': float_read(ln[5]), + 'Tmax': float_read(ln[6]), + 'DOF': int_read(ln[7]), + 'Amplitude': float_read(ln[8]), + 'Frequency': float_read(ln[9]), + }) + dvr['Cases'] = cases + + # --- Output Settings --- + f.readline() # section header + dvr['OutFmt'] = quoted_read(f.readline().split()[0]) + dvr['OutFileFmt'] = int_read(f.readline().split()[0]) + dvr['WrVTK'] = int_read(f.readline().split()[0]) + dvr['WrVTK_Type'] = int_read(f.readline().split()[0]) + dvr['VTKHubRad'] = float_read(f.readline().split()[0]) + dvr['VTKNacDim'] = _read_csv_vec(f.readline()) + + result: Dict[str, Any] = {'AeroDynDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so the AeroDyn + # and InflowWind OutList sections survive a standalone read → write + # roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to AeroDynIO for the primary AeroDyn file --- + aero_file = dvr.get('AeroFile', '') + aero_path = os.path.normpath(os.path.join(str(base_dir), aero_file)) + if aero_file and os.path.isfile(aero_path): + n_bld = turbines[0]['NumBlades'] if turbines else 3 + try: + ad_data = self._aerodyn.read(Path(aero_path), base_dir, num_blades=n_bld, + outlist=outlist, read_outlist_fn=_cap) + result.update(ad_data) + except Exception: + pass # module file parse failure — driver data still valid + + # --- Delegate to InflowWindIO --- + if dvr.get('CompInflow', 0) == 1: + ifw_file = dvr.get('InflowFile', '') + ifw_path = os.path.normpath(os.path.join(str(base_dir), ifw_file)) + if ifw_file and os.path.isfile(ifw_path): + try: + ifw_data = self._inflowwind.read(Path(ifw_path), base_dir, + outlist=outlist, read_outlist_fn=_cap) + result.update(ifw_data) + except Exception: + pass + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, dvr_path: Path) -> None: + """Write an AeroDyn driver file and all referenced module files.""" + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr = data['AeroDynDriver'] + + with open(dvr_path, 'w') as f: + f.write('----- AeroDyn Driver Input File ---------------------------------------------------------\n') + f.write(dvr.get('description', 'Generated by OpenFAST_IO') + '\n') + f.write('----- Input Configuration ---------------------------------------------------------------\n') + f.write('{!s:<16} {:<12} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo input parameters to ".ech"?')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['MHK'], 'MHK', '- MHK turbine type (switch)')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['AnalysisType'], 'AnalysisType', '- {1: multiple turbines, 2: time-dependent, 3: combined cases}')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['TMax'], 'TMax', '- Total run time (s)')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['DT'], 'DT', '- Simulation time step (s)')) + + ad_file = dvr.get('AeroFile', 'AeroDyn.dat') + f.write('{:<16} {:<12} {:}\n'.format('"' + ad_file + '"', 'AeroFile', '- Name of the primary AeroDyn input file')) + f.write('----- Environmental Conditions ----------------------------------------------------------\n') + f.write('{:<26} {:<13} {:}\n'.format(dvr['FldDens'], 'FldDens', '- Density of working fluid (kg/m^3)')) + f.write('{:<26} {:<13} {:}\n'.format(dvr['KinVisc'], 'KinVisc', '- Kinematic viscosity of working fluid (m^2/s)')) + f.write('{:<26} {:<13} {:}\n'.format(dvr['SpdSound'], 'SpdSound', '- Speed of sound in working fluid (m/s)')) + f.write('{:<26} {:<13} {:}\n'.format(dvr['Patm'], 'Patm', '- Atmospheric pressure (Pa)')) + f.write('{:<26} {:<13} {:}\n'.format(dvr['Pvap'], 'Pvap', '- Vapour pressure of working fluid (Pa)')) + f.write('{:<26} {:<13} {:}\n'.format(dvr['WtrDpth'], 'WtrDpth', '- Water depth (m)')) + f.write('----- Inflow Data -----------------------------------------------------------------------\n') + f.write('{:<16} {:<12} {:}\n'.format(dvr['CompInflow'], 'CompInflow', '- Compute inflow wind velocities (switch)')) + ifw_file = dvr.get('InflowFile', 'unused') + f.write('{:<16} {:<12} {:}\n'.format('"' + ifw_file + '"', 'InflowFile', '- Name of the InflowWind input file')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['HWindSpeed'], 'HWindSpeed', '- Horizontal wind speed (m/s)')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['RefHt'], 'RefHt', '- Reference height for horizontal wind speed (m)')) + f.write('{:<16} {:<12} {:}\n'.format(dvr['PLExp'], 'PLExp', '- Power law exponent (-)')) + f.write('----- SeaState Data [used only when MHK = 1 or 2] ---------------------------------------\n') + f.write('{:<16} {:<27} {:}\n'.format(dvr['CompSeaSt'], 'CompSeaSt', '- Compute wave velocities (switch)')) + ss_file = dvr.get('SeaStFile', 'unused') + f.write('{:<16} {:<27} {:}\n'.format('"' + ss_file + '"', 'SeaStFile', '- Name of the SeaState input file')) + f.write('----- Turbine Data ----------------------------------------------------------------------\n') + f.write('{:<16} {:<12} {:}\n'.format(dvr['NumTurbines'], 'NumTurbines', '- Number of turbines')) + + for i_turb, turb in enumerate(dvr.get('Turbines', [])): + f.write('----- Turbine({}) ------------------------------------------------------------------------\n'.format(i_turb + 1)) + f.write('{!s:<16} {:<22} {:}\n'.format(turb['BasicHAWTFormat'], 'BasicHAWTFormat({})'.format(i_turb + 1), '- Flag to switch between basic or generic input format')) + f.write(_fw(','.join(str(v) for v in turb['BaseOriginInit'])) + '{:<22} {:}\n'.format('BaseOriginInit({})'.format(i_turb + 1), '- x,y,z coordinates of base origin (m)')) + + if turb['BasicHAWTFormat']: + f.write(_fw(turb['NumBlades']) + '{:<22} {:}\n'.format('NumBlades({})'.format(i_turb + 1), '- Number of blades')) + f.write(_fw(turb['HubRad']) + '{:<22} {:}\n'.format('HubRad({})'.format(i_turb + 1), '- Hub radius (m)')) + f.write(_fw(turb['HubHt']) + '{:<22} {:}\n'.format('HubHt({})'.format(i_turb + 1), '- Hub height (m)')) + f.write(_fw(turb['Overhang']) + '{:<22} {:}\n'.format('Overhang({})'.format(i_turb + 1), '- Overhang (m)')) + f.write(_fw(turb['ShftTilt']) + '{:<22} {:}\n'.format('ShftTilt({})'.format(i_turb + 1), '- Shaft tilt (deg)')) + f.write(_fw(turb['Precone']) + '{:<22} {:}\n'.format('Precone({})'.format(i_turb + 1), '- Precone (deg)')) + f.write(_fw(turb['Twr2Shft']) + '{:<22} {:}\n'.format('Twr2Shft({})'.format(i_turb + 1), '- Twr2Shft (m)')) + else: + f.write(_fw(','.join(str(v) for v in turb['BaseOrientationInit'])) + '{:<22} {:}\n'.format('BaseOrientationInit({})'.format(i_turb + 1), '- successive rotations defining initial orientation of the base frame (deg)')) + f.write('{!s:<16} {:<22} {:}\n'.format(turb['HasTower'], 'HasTower({})'.format(i_turb + 1), '- True if turbine has a tower (flag)')) + f.write('{!s:<16} {:<22} {:}\n'.format(turb['HAWTprojection'], 'HAWTprojection({})'.format(i_turb + 1), '- True if turbine is a horizontal axis turbine (flag)')) + f.write(_fw(','.join(str(v) for v in turb['TwrOrigin_t'])) + '{:<22} {:}\n'.format('TwrOrigin_t({})'.format(i_turb + 1), '- Coordinate of tower base in base coordinates (m)')) + f.write(_fw(','.join(str(v) for v in turb['NacOrigin_t'])) + '{:<22} {:}\n'.format('NacOrigin_t({})'.format(i_turb + 1), '- x,y,z coordinates of nacelle origin from base (m)')) + f.write(_fw(','.join(str(v) for v in turb['HubOrigin_n'])) + '{:<22} {:}\n'.format('HubOrigin_n({})'.format(i_turb + 1), '- x,y,z coordinates of hub origin from nacelle (m)')) + f.write(_fw(','.join(str(v) for v in turb['HubOrientation_n'])) + '{:<22} {:}\n'.format('HubOrientation_n({})'.format(i_turb + 1), '- successive rotations defining hub frame from nacelle frame (deg)')) + + f.write('----- Turbine({}) Blades -----------------------------------------------------------------\n'.format(i_turb + 1)) + f.write(_fw(turb['NumBlades']) + '{:<22} {:}\n'.format('NumBlades({})'.format(i_turb + 1), '- Number of blades for current rotor (-)')) + n_bld = turb['NumBlades'] + for j in range(n_bld): + f.write(_fw(','.join(str(v) for v in turb['BldOrigin_h'][j])) + '{:<22} {:}\n'.format('BldOrigin_h({0}_{1})'.format(i_turb + 1, j + 1), '- Orign of blade {:d} wrt. hub origin in hub coordinates (m)'.format(j + 1))) + for j in range(n_bld): + f.write(_fw(','.join(str(v) for v in turb['BldOrientation_h'][j])) + '{:<22} {:}\n'.format('BldOrientation_h({0}_{1})'.format(i_turb + 1, j + 1), '- successive rotations defining blade {:d} frame from hub frame (deg)'.format(j + 1))) + for j in range(n_bld): + f.write(_fw(turb['BldHubRad_bl'][j]) + '{:<22} {:}\n'.format('BldHubRad_bl({0}_{1})'.format(i_turb + 1, j + 1), '- z-offset where radial input data start for blade {:d} (m)'.format(j + 1))) + + f.write('----- Turbine({}) Motion [used only when AnalysisType=1] ---------------------------------\n'.format(i_turb + 1)) + if turb['BasicHAWTFormat']: + f.write('{:<16} {:<28} {:}\n'.format(turb['BaseMotionType'], 'BaseMotionType({})'.format(i_turb + 1), '- Type of motion prescribed for this base (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['DegreeOfFreedom'], 'DegreeOfFreedom({})'.format(i_turb + 1), '- Degree of freedom (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['Amplitude'], 'Amplitude({})'.format(i_turb + 1), '- Amplitude of sinusoidal motion (m or rad)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['Frequency'], 'Frequency({})'.format(i_turb + 1), '- Frequency of sinusoidal motion (Hz)')) + f.write('{:<16} {:<28} {:}\n'.format('"' + turb['BaseMotionFileName'] + '"', 'BaseMotionFileName({})'.format(i_turb + 1), '- Filename for arbitrary base motion')) + f.write('{:<16} {:<28} {:}\n'.format(turb['NacYaw'], 'NacYaw({})'.format(i_turb + 1), '- Yaw angle of the nacelle (deg)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['RotSpeed'], 'RotSpeed({})'.format(i_turb + 1), '- Rotational speed (rpm)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['BldPitch'], 'BldPitch({})'.format(i_turb + 1), '- Blade pitch (deg)')) + else: + f.write('{:<16} {:<28} {:}\n'.format(turb['BaseMotionType'], 'BaseMotionType({})'.format(i_turb + 1), '- Type of motion prescribed for this base (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb.get('DegreeOfFreedom', 1), 'DegreeOfFreedom({})'.format(i_turb + 1), '- {1:xt, 2:yt, 3:zt, 4:theta_xt, 5:theta_yt, 6:theta_zt} (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['Amplitude'], 'Amplitude({})'.format(i_turb + 1), '- Amplitude of sinusoidal motion (m or rad)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['Frequency'], 'Frequency({})'.format(i_turb + 1), '- Frequency of sinusoidal motion (Hz)')) + f.write('{:<16} {:<28} {:}\n'.format('"' + turb['BaseMotionFileName'] + '"', 'BaseMotionFileName({})'.format(i_turb + 1), '- Filename for arbitrary base motion')) + f.write('{:<16} {:<28} {:}\n'.format(turb['NacMotionType'], 'NacMotionType({})'.format(i_turb + 1), '- Type of motion prescribed for the nacelle (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['NacYaw'], 'NacYaw({})'.format(i_turb + 1), '- Yaw angle of the nacelle (deg)')) + f.write('{:<16} {:<28} {:}\n'.format('"' + turb['NacMotionFileName'] + '"', 'NacMotionFileName({})'.format(i_turb + 1), '- Filename for yaw motion')) + f.write('{:<16} {:<28} {:}\n'.format(turb['RotMotionType'], 'RotMotionType({})'.format(i_turb + 1), '- Type of motion prescribed for this rotor (flag)')) + f.write('{:<16} {:<28} {:}\n'.format(turb['RotSpeed'], 'RotSpeed({})'.format(i_turb + 1), '- Rotational speed (rpm)')) + f.write('{:<16} {:<28} {:}\n'.format('"' + turb['RotMotionFileName'] + '"', 'RotMotionFileName({})'.format(i_turb + 1), '- Filename for rotor motion')) + f.write('{:<16} {:<28} {:}\n'.format(turb['BldMotionType'], 'BldMotionType({})'.format(i_turb + 1), '- Type of pitch motion prescribed for the blades (flag)')) + n_bld = turb['NumBlades'] + for j in range(n_bld): + f.write('{:<16} {:<28} {:}\n'.format(turb['BldPitch'][j], 'BldPitch({0}_{1})'.format(i_turb + 1, j + 1), '- Blade {:d} pitch (deg)'.format(j + 1))) + for j in range(n_bld): + f.write('{:<16} {:<28} {:}\n'.format('"' + turb['BldMotionFileName'][j] + '"', 'BldMotionFileName({0}_{1})'.format(i_turb + 1, j + 1), '- Filename containing blade pitch motion')) + + f.write('----- Time-dependent Analysis [used only when AnalysisType=2, numTurbines=1] ------------\n') + f.write('{:<17} {:<22} {:}\n'.format('"' + dvr.get('TimeAnalysisFileName', 'unused') + '"', 'TimeAnalysisFileName', '- Filename containing time series')) + f.write('----- Combined-Case Analysis [used only when AnalysisType=3, numTurbines=1] -------------\n') + f.write('{:<5} {:<13} {:}\n'.format(dvr.get('NumCases', 0), 'NumCases', '- Number of cases to run')) + f.write('HWndSpeed PLExp RotSpd Pitch Yaw dT Tmax DOF Amplitude Frequency\n') + f.write('(m/s) (-) (rpm) (deg) (deg) (s) (s) (-) (m or rad) (Hz)\n') + for case in dvr.get('Cases', []): + f.write('{:<13} {:<13} {:<13} {:<13} {:<8} {:<8} {:<6} {:<5} {:<10} {}\n'.format( + case['HWndSpeed'], case['PLExp'], case['RotSpd'], case['Pitch'], + case['Yaw'], case['dT'], case['Tmax'], case['DOF'], + case['Amplitude'], case['Frequency'])) + f.write('----- Output Settings -------------------------------------------------------------------\n') + f.write('{:<12} {:<12} {:}\n'.format('"' + dvr.get('OutFmt', 'ES15.8E2') + '"', 'OutFmt', '- Format used for text tabular output')) + f.write('{:<12} {:<12} {:}\n'.format(dvr['OutFileFmt'], 'OutFileFmt', '- Format for tabular output file')) + f.write('{:<12} {:<12} {:}\n'.format(dvr['WrVTK'], 'WrVTK', '- VTK visualization data output')) + f.write('{:<12} {:<12} {:}\n'.format(dvr['WrVTK_Type'], 'WrVTK_Type', '- VTK visualization data type')) + f.write('{:<12} {:<12} {:}\n'.format(dvr['VTKHubRad'], 'VTKHubRad', '- HubRadius for VTK visualization (m)')) + f.write(_fw(','.join(str(v) for v in dvr['VTKNacDim'])) + '{:<12} {:}\n'.format('VTKNacDim', '- Nacelle Dimension for VTK visualization x0,y0,z0,Lx,Ly,Lz (m)')) + + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + + # --- Delegate to AeroDynIO --- + if 'AeroDyn' in data: + n_bld = dvr['Turbines'][0]['NumBlades'] if dvr.get('Turbines') else 3 + ad_path = os.path.normpath(os.path.join(str(base_dir), ad_file)) + ad_write_data = {k: data[k] for k in ('AeroDyn', 'AeroDynBlade', 'af_data', 'af_coord', 'ac') if k in data} + try: + self._aerodyn.write(ad_write_data, Path(ad_path), base_dir, + naming_out=dvr_path.stem, outlist=outlist) + except Exception: + pass + + # --- Delegate to InflowWindIO --- + if dvr.get('CompInflow', 0) == 1 and 'InflowWind' in data: + ifw_path = os.path.normpath(os.path.join(str(base_dir), ifw_file)) + try: + self._inflowwind.write({'InflowWind': data['InflowWind']}, Path(ifw_path), base_dir, outlist=outlist) + except Exception: + pass + + +def _read_csv_vec(line: str) -> list: + """Parse a comma- or space-separated vector from a line like '0,0,0 name ...'.""" + # Take everything before the first alphabetic token (the param name) + raw = line.split()[0] + parts = raw.split(',') + return [float_read(p.strip()) for p in parts] diff --git a/openfast_io/openfast_io/drivers/beamdyn_driver.py b/openfast_io/openfast_io/drivers/beamdyn_driver.py new file mode 100644 index 0000000000..8c6e11ce48 --- /dev/null +++ b/openfast_io/openfast_io/drivers/beamdyn_driver.py @@ -0,0 +1,226 @@ +"""BeamDyn standalone driver — reads/writes BeamDyn driver input files (.inp). + +Handles simulation control, gravity, frame parameters (including 3x3 DCM), +root velocity, applied forces, point loads, and primary input file reference. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict, List + +from ..io.beamdyn import BeamDynIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, fmt_field, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +def _fw(val, width: int = 16) -> str: + """Format a value field with guaranteed 2-space separator.""" + return fmt_field(val, min_width=width) + + +class BeamDynStandaloneDriver: + """Reads and writes BeamDyn standalone driver input files (.inp).""" + + def __init__(self) -> None: + self._beamdyn = BeamDynIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, inp_path: Path) -> dict: + """Read a BeamDyn driver file and the referenced primary input file. + + Returns a dict with keys: + ``'BeamDynDriver'`` — driver-level parameters + ``'BeamDyn'``, ``'BeamDynBlade'`` — from BeamDynIO + """ + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr: Dict[str, Any] = {} + + with open(inp_path) as f: + # --- Header --- + f.readline() # title line + dvr['description'] = f.readline().rstrip() + + # --- Simulation Control --- + f.readline() # section header + dvr['DynamicSolve'] = bool_read(f.readline().split()[0]) + dvr['t_initial'] = float_read(f.readline().split()[0]) + dvr['t_final'] = float_read(f.readline().split()[0]) + dvr['dt'] = float_read(f.readline().split()[0]) + + # --- Gravity Parameter --- + f.readline() # section header + dvr['Gx'] = float_read(f.readline().split()[0]) + dvr['Gy'] = float_read(f.readline().split()[0]) + dvr['Gz'] = float_read(f.readline().split()[0]) + + # --- Frame Parameter --- + f.readline() # section header + dvr['GlbPos'] = [ + float_read(f.readline().split()[0]), + float_read(f.readline().split()[0]), + float_read(f.readline().split()[0]), + ] + + # 3x3 direction cosine matrix + f.readline() # DCM comment line 1 + f.readline() # DCM comment line 2 + dcm: List[List[float]] = [] + for _ in range(3): + row = [float_read(v) for v in f.readline().split()] + dcm.append(row) + dvr['GlbDCM'] = dcm + + dvr['GlbRotBladeT0'] = bool_read(f.readline().split()[0]) + + # --- Root Velocity Parameter --- + f.readline() # section header + dvr['RootVel'] = [ + float_read(f.readline().split()[0]), + float_read(f.readline().split()[0]), + float_read(f.readline().split()[0]), + ] + + # --- Applied Force --- + f.readline() # section header + dvr['DistrLoad'] = [float_read(f.readline().split()[0]) for _ in range(6)] + dvr['TipLoad'] = [float_read(f.readline().split()[0]) for _ in range(6)] + dvr['NumPointLoads'] = int_read(f.readline().split()[0]) + + # Point loads table + f.readline() # column headers + f.readline() # column units + dvr['PointLoads'] = [] + for _ in range(dvr['NumPointLoads']): + ln = f.readline().split() + dvr['PointLoads'].append({ + 'eta': float_read(ln[0]), + 'Fx': float_read(ln[1]), + 'Fy': float_read(ln[2]), + 'Fz': float_read(ln[3]), + 'Mx': float_read(ln[4]), + 'My': float_read(ln[5]), + 'Mz': float_read(ln[6]), + }) + + # --- Primary Input File --- + f.readline() # section header + dvr['InputFile'] = quoted_read(f.readline().split()[0]) + + # --- Output Settings --- + f.readline() # section header + dvr['WrVTK'] = int_read(f.readline().split()[0]) + try: + dvr['VTK_fps'] = int_read(f.readline().split()[0]) + except (IndexError, ValueError): + dvr['VTK_fps'] = 15 + + result: Dict[str, Any] = {'BeamDynDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so the BeamDyn + # OutList section survives a standalone read → write roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to BeamDynIO --- + bd_file = dvr.get('InputFile', '') + bd_path = os.path.normpath(os.path.join(str(base_dir), bd_file)) + if bd_file and os.path.isfile(bd_path): + bd_data = self._beamdyn.read(Path(bd_path), base_dir, + outlist=outlist, read_outlist_fn=_cap) + result.update(bd_data) + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, inp_path: Path) -> None: + """Write a BeamDyn driver file and the referenced primary input.""" + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr = data['BeamDynDriver'] + + with open(inp_path, 'w') as f: + f.write('------- BEAMDYN Driver with OpenFAST INPUT FILE --------------------------------\n') + f.write(dvr.get('description', 'Generated by OpenFAST_IO') + '\n') + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write(_fw(dvr['DynamicSolve']) + '{:<14} {:}\n'.format('DynamicSolve', '- Dynamic solve (false for static solve) (-)')) + f.write(_fw(dvr['t_initial']) + '{:<14} {:}\n'.format('t_initial', '- Starting time of simulation (s)')) + f.write(_fw(dvr['t_final']) + '{:<14} {:}\n'.format('t_final', '- Ending time of simulation (s)')) + f.write(_fw(dvr['dt']) + '{:<14} {:}\n'.format('dt', '- Time increment size (s)')) + f.write('---------------------- GRAVITY PARAMETER --------------------------------------\n') + f.write(_fw(dvr['Gx']) + '{:<14} {:}\n'.format('Gx', '- Component of gravity vector along X direction (m/s^2)')) + f.write(_fw(dvr['Gy']) + '{:<14} {:}\n'.format('Gy', '- Component of gravity vector along Y direction (m/s^2)')) + f.write(_fw(dvr['Gz']) + '{:<14} {:}\n'.format('Gz', '- Component of gravity vector along Z direction (m/s^2)')) + f.write('---------------------- FRAME PARAMETER --------------------------------------\n') + f.write(_fw(dvr['GlbPos'][0]) + '{:<14} {:}\n'.format('GlbPos(1)', '- Component of position vector along X direction (m)')) + f.write(_fw(dvr['GlbPos'][1]) + '{:<14} {:}\n'.format('GlbPos(2)', '- Component of position vector along Y direction (m)')) + f.write(_fw(dvr['GlbPos'][2]) + '{:<14} {:}\n'.format('GlbPos(3)', '- Component of position vector along Z direction (m)')) + f.write('---The following 3 by 3 matrix is the direction cosine matirx ,GlbDCM(3,3),\n') + f.write('---relates global frame to the initial blade root frame\n') + for row in dvr['GlbDCM']: + f.write(' '.join('{:.7E}'.format(v) for v in row) + '\n') + f.write('{!s:<14} {:<14} {:}\n'.format(dvr['GlbRotBladeT0'], 'GlbRotBladeT0', '- Reference orientation aligned with initial blade root?')) + f.write('---------------------- ROOT VELOCITY PARAMETER ----------------------------------\n') + f.write(_fw(dvr['RootVel'][0]) + '{:<14} {:}\n'.format('RootVel(4)', '- Component of angular velocity about X axis (rad/s)')) + f.write(_fw(dvr['RootVel'][1]) + '{:<14} {:}\n'.format('RootVel(5)', '- Component of angular velocity about Y axis (rad/s)')) + f.write(_fw(dvr['RootVel'][2]) + '{:<14} {:}\n'.format('RootVel(6)', '- Component of angular velocity about Z axis (rad/s)')) + f.write('---------------------- APPLIED FORCE ----------------------------------\n') + labels = ['DistrLoad(1)', 'DistrLoad(2)', 'DistrLoad(3)', 'DistrLoad(4)', 'DistrLoad(5)', 'DistrLoad(6)'] + descs = [ + '- Component of distributed force vector along X direction (N/m)', + '- Component of distributed force vector along Y direction (N/m)', + '- Component of distributed force vector along Z direction (N/m)', + '- Component of distributed moment vector along X direction (N-m/m)', + '- Component of distributed moment vector along Y direction (N-m/m)', + '- Component of distributed moment vector along Z direction (N-m/m)', + ] + for i in range(6): + f.write(_fw(dvr['DistrLoad'][i]) + '{:<14} {:}\n'.format(labels[i], descs[i])) + tip_labels = ['TipLoad(1)', 'TipLoad(2)', 'TipLoad(3)', 'TipLoad(4)', 'TipLoad(5)', 'TipLoad(6)'] + tip_descs = [ + '- Component of concentrated force at blade tip along X direction (N)', + '- Component of concentrated force at blade tip along Y direction (N)', + '- Component of concentrated force at blade tip along Z direction (N)', + '- Component of concentrated moment at blade tip along X direction (N-m)', + '- Component of concentrated moment at blade tip along Y direction (N-m)', + '- Component of concentrated moment at blade tip along Z direction (N-m)', + ] + for i in range(6): + f.write(_fw(dvr['TipLoad'][i]) + '{:<14} {:}\n'.format(tip_labels[i], tip_descs[i])) + f.write(_fw(dvr['NumPointLoads']) + '{:<14} {:}\n'.format('NumPointLoads', '- Number of point loads along blade')) + f.write('Non-dim blade-span eta Fx Fy Fz Mx My Mz\n') + f.write('(-) (N) (N) (N) (N-m) (N-m) (N-m)\n') + for pl in dvr.get('PointLoads', []): + f.write('{:<10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}\n'.format( + pl['eta'], pl['Fx'], pl['Fy'], pl['Fz'], pl['Mx'], pl['My'], pl['Mz'])) + + f.write('---------------------- PRIMARY INPUT FILE --------------------------------------\n') + bd_name = dvr.get('InputFile', 'bd_primary.inp') + f.write('{:<14} {:<14} {:}\n'.format('"' + bd_name + '"', 'InputFile', '- Name of the primary BeamDyn input file')) + + f.write('----- Output Settings -------------------------------------------------------------------\n') + f.write(_fw(dvr.get('WrVTK', 0)) + '{:<14} {:}\n'.format('WrVTK', '- VTK visualization data output (switch)')) + f.write(_fw(dvr.get('VTK_fps', 15)) + '{:<14} {:}\n'.format('VTK_fps', '- Frame rate for VTK output (frames per second)')) + + # --- Delegate to BeamDynIO --- + if 'BeamDyn' in data: + bd_path = os.path.normpath(os.path.join(str(base_dir), bd_name)) + bd_data = {'BeamDyn': data['BeamDyn']} + if 'BeamDynBlade' in data: + bd_data['BeamDynBlade'] = data['BeamDynBlade'] + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + self._beamdyn.write(bd_data, Path(bd_path), base_dir, outlist=outlist) diff --git a/openfast_io/openfast_io/drivers/fastfarm.py b/openfast_io/openfast_io/drivers/fastfarm.py new file mode 100644 index 0000000000..910952ddff --- /dev/null +++ b/openfast_io/openfast_io/drivers/fastfarm.py @@ -0,0 +1,155 @@ +"""FASTFarmDriver — reads/writes FAST.Farm (.fstf) simulation decks. + +Composes ``OpenFASTDriver`` per turbine, plus farm-level parameters. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Dict, List + +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, +) +from .openfast import OpenFASTDriver + + +class FASTFarmDriver: + """Reads and writes a FAST.Farm simulation deck. + + The result dict has: + - ``'FASTFarm'``: farm-level scalars (sim control, ambient wind, wake, output, …) + - ``'Turbines'``: list of per-turbine dicts, each containing a full ``fst_vt`` + """ + + def __init__(self): + self._of_driver = OpenFASTDriver() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, fstf_path: Path) -> dict: + fstf_path = Path(fstf_path) + base_dir = fstf_path.parent + ff: Dict[str, Any] = {} + + f = open(fstf_path) + f.readline() # header 1 + ff['description'] = f.readline().rstrip() + + # --- SIMULATION CONTROL --- + f.readline() + ff['Echo'] = bool_read(f.readline().split()[0]) + ff['AbortLevel'] = f.readline().split()[0] + ff['TMax'] = float_read(f.readline().split()[0]) + ff['Mod_AmbWind'] = int_read(f.readline().split()[0]) + ff['Mod_WaveField'] = int_read(f.readline().split()[0]) + ff['Mod_SharedMooring'] = int_read(f.readline().split()[0]) + + # --- SHARED MOORING --- + f.readline() + ff['SharedMoorFile'] = quoted_read(f.readline().split()[0]) + ff['DT_Mooring'] = float_read(f.readline().split()[0]) + ff['WrMooringVis'] = bool_read(f.readline().split()[0]) + + # --- AMBIENT WIND: VTK --- + f.readline() + ff['DT_Low-VTK'] = float_read(f.readline().split()[0]) + ff['DT_High-VTK'] = float_read(f.readline().split()[0]) + ff['WindFilePath'] = quoted_read(f.readline().split()[0]) + ff['ChkWndFiles'] = bool_read(f.readline().split()[0]) + + # --- AMBIENT WIND: INFLOWWIND --- + f.readline() + ff['DT_Low'] = float_read(f.readline().split()[0]) + ff['DT_High'] = float_read(f.readline().split()[0]) + ff['NX_Low'] = int_read(f.readline().split()[0]) + ff['NY_Low'] = int_read(f.readline().split()[0]) + ff['NZ_Low'] = int_read(f.readline().split()[0]) + ff['X0_Low'] = float_read(f.readline().split()[0]) + ff['Y0_Low'] = float_read(f.readline().split()[0]) + ff['Z0_Low'] = float_read(f.readline().split()[0]) + ff['dX_Low'] = float_read(f.readline().split()[0]) + ff['dY_Low'] = float_read(f.readline().split()[0]) + ff['dZ_Low'] = float_read(f.readline().split()[0]) + ff['NX_High'] = int_read(f.readline().split()[0]) + ff['NY_High'] = int_read(f.readline().split()[0]) + ff['NZ_High'] = int_read(f.readline().split()[0]) + ff['InflowFile'] = quoted_read(f.readline().split()[0]) + + # --- AMBIENT WIND: AMReX --- + f.readline() + ff['WindDirPrefix'] = quoted_read(f.readline().split()[0]) + ff['DirStartIndex'] = int_read(f.readline().split()[0]) + ff['DT_Low-AMReX'] = float_read(f.readline().split()[0]) + ff['DT_High-AMReX'] = float_read(f.readline().split()[0]) + + # --- WIND TURBINES --- + f.readline() + ff['NumTurbines'] = int_read(f.readline().split()[0]) + f.readline() # column headers (WT_X WT_Y WT_Z WT_FASTInFile …) + f.readline() # column units (m) (m) (m) (string) … + + turbine_rows = [] + for _ in range(ff['NumTurbines']): + ln = f.readline().split() + row = { + 'WT_X': float_read(ln[0]), + 'WT_Y': float_read(ln[1]), + 'WT_Z': float_read(ln[2]), + 'WT_FASTInFile': ln[3].strip('"'), + 'X0_High': float_read(ln[4]) if len(ln) > 4 else 0.0, + 'Y0_High': float_read(ln[5]) if len(ln) > 5 else 0.0, + 'Z0_High': float_read(ln[6]) if len(ln) > 6 else 0.0, + 'dX_High': float_read(ln[7]) if len(ln) > 7 else 0.0, + 'dY_High': float_read(ln[8]) if len(ln) > 8 else 0.0, + 'dZ_High': float_read(ln[9]) if len(ln) > 9 else 0.0, + } + turbine_rows.append(row) + ff['TurbineRows'] = turbine_rows + + # Read remaining lines as raw text (wake dynamics, curl, WAT, viz, output) + remaining = f.read() + ff['_remaining'] = remaining + f.close() + + # --- Read per-turbine .fst files --- + turbines: List[dict] = [] + for row in turbine_rows: + fst_rel = row['WT_FASTInFile'] + fst_file = os.path.normpath(os.path.join(str(base_dir), fst_rel)) + if os.path.isfile(fst_file): + turb_vt = self._of_driver.read(Path(fst_file)) + else: + turb_vt = {} + turb_vt['_farm_position'] = { + 'WT_X': row['WT_X'], + 'WT_Y': row['WT_Y'], + 'WT_Z': row['WT_Z'], + } + turbines.append(turb_vt) + + return {'FASTFarm': ff, 'Turbines': turbines} + + # ------------------------------------------------------------------ + # WRITE (stub — farm-level write is complex, for now just the .fstf) + # ------------------------------------------------------------------ + def write( + self, + data: dict, + output_dir: Path, + case_name: str, + ) -> list: + """Write the .fstf file and per-turbine files. + + Currently writes only the .fstf file (no per-turbine write delegation yet). + Returns list of written paths. + """ + raise NotImplementedError( + 'FASTFarmDriver.write() is not yet implemented. ' + 'Use OpenFASTDriver.write() for individual turbine files.' + ) diff --git a/openfast_io/openfast_io/drivers/hydrodyn_driver.py b/openfast_io/openfast_io/drivers/hydrodyn_driver.py new file mode 100644 index 0000000000..6b69ddf6de --- /dev/null +++ b/openfast_io/openfast_io/drivers/hydrodyn_driver.py @@ -0,0 +1,179 @@ +"""HydroDyn standalone driver — reads/writes HydroDyn driver input files (.inp). + +Handles environmental conditions, primary HD + SeaState file references, +PRP (Platform Reference Point) inputs, and steady-state 6-DOF vectors. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict + +from ..io.hydrodyn import HydroDynIO +from ..io.seastate import SeaStateIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +class HydroDynStandaloneDriver: + """Reads and writes HydroDyn standalone driver input files (.inp).""" + + def __init__(self) -> None: + self._hydrodyn = HydroDynIO() + self._seastate = SeaStateIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, inp_path: Path) -> dict: + """Read a HydroDyn driver file and all referenced module files. + + Returns a dict with keys: + ``'HydroDynDriver'`` — driver-level parameters + ``'HydroDyn'`` — from HydroDynIO + ``'SeaState'`` — from SeaStateIO + """ + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr: Dict[str, Any] = {} + + with open(inp_path) as f: + # --- Header --- + f.readline() # title line + dvr['description'] = f.readline().rstrip() + + # --- Environmental Conditions --- + dvr['Echo'] = bool_read(f.readline().split()[0]) + f.readline() # section header + dvr['Gravity'] = float_read(f.readline().split()[0]) + dvr['WtrDens'] = float_read(f.readline().split()[0]) + dvr['WtrDpth'] = float_read(f.readline().split()[0]) + dvr['MSL2SWL'] = float_read(f.readline().split()[0]) + + # --- HydroDyn --- + f.readline() # section header + dvr['HDInputFile'] = quoted_read(f.readline().split()[0]) + dvr['SeaStateInputFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + dvr['Linearize'] = bool_read(f.readline().split()[0]) + dvr['NSteps'] = int_read(f.readline().split()[0]) + dvr['TimeInterval'] = float_read(f.readline().split()[0]) + + # --- PRP Inputs --- + f.readline() # section header + dvr['PRPInputsMod'] = int_read(f.readline().split()[0]) + dvr['NAddDOF'] = int_read(f.readline().split()[0]) + dvr['PtfmRefzt'] = float_read(f.readline().split()[0]) + dvr['PRPInputsFile'] = quoted_read(f.readline().split()[0]) + + # --- PRP Steady State Inputs --- + f.readline() # section header + dvr['uPRPInSteady'] = _read_6dof_vec(f.readline()) + dvr['uDotPRPInSteady'] = _read_6dof_vec(f.readline()) + dvr['uDotDotPRPInSteady'] = _read_6dof_vec(f.readline()) + + result: Dict[str, Any] = {'HydroDynDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so both the + # HydroDyn and SeaState output channel sections survive a standalone + # read → write roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to HydroDynIO --- + hd_file = dvr.get('HDInputFile', '') + hd_path = os.path.normpath(os.path.join(str(base_dir), hd_file)) + if hd_file and os.path.isfile(hd_path): + hd_data = self._hydrodyn.read(Path(hd_path), base_dir, + outlist=outlist, read_outlist_fn=_cap) + result.update(hd_data) + + # --- Delegate to SeaStateIO --- + ss_file = dvr.get('SeaStateInputFile', '') + ss_path = os.path.normpath(os.path.join(str(base_dir), ss_file)) + if ss_file and os.path.isfile(ss_path): + ss_data = self._seastate.read( + Path(ss_path), base_dir, + outlist=outlist, + read_outlist_fn=lambda f, module: _cap(f, module, freeform=True), + ) + result.update(ss_data) + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, inp_path: Path) -> None: + """Write a HydroDyn driver file and all referenced module files.""" + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr = data['HydroDynDriver'] + + with open(inp_path, 'w') as f: + f.write('------- HydroDyn Driver Input File --------------------------------------------\n') + f.write('Generated by OpenFAST_IO\n') + f.write('{!s:<14} {:<20} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo the input file data (flag)')) + f.write('---------------------- ENVIRONMENTAL CONDITIONS -------------------------------\n') + f.write('{:<14} {:<20} {:}\n'.format(dvr['Gravity'], 'Gravity', '- Gravity (m/s^2)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['WtrDens'], 'WtrDens', '- Water density (kg/m^3)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['WtrDpth'], 'WtrDpth', '- Water depth (m)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['MSL2SWL'], 'MSL2SWL', '- Offset between still-water level and mean sea level (m) [positive upward]')) + f.write('---------------------- HYDRODYN -----------------------------------------------\n') + + hd_name = dvr.get('HDInputFile', 'HydroDyn.dat') + ss_name = dvr.get('SeaStateInputFile', 'SeaState.dat') + f.write('{:<14} {:<20} {:}\n'.format('"' + hd_name + '"', 'HDInputFile', '- Primary HydroDyn input file name (quoted string)')) + f.write('{:<14} {:<20} {:}\n'.format('"' + ss_name + '"', 'SeaStateInputFile', '- Primary SeaState input file name (quoted string)')) + f.write('{:<14} {:<20} {:}\n'.format('"' + dvr.get('OutRootName', './driver') + '"', 'OutRootName', '- The name which prefixes all HydroDyn generated files (quoted string)')) + f.write('{!s:<14} {:<20} {:}\n'.format(dvr['Linearize'], 'Linearize', '- Flag to enable linearization')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['NSteps'], 'NSteps', '- Number of time steps in the simulations (-)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['TimeInterval'], 'TimeInterval', '- TimeInterval for the simulation (sec)')) + f.write('---------------------- PRP INPUTS (Platform Reference Point) ------------------\n') + f.write('{:<14} {:<20} {:}\n'.format(dvr['PRPInputsMod'], 'PRPInputsMod', '- Model for the PRP inputs (switch)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['NAddDOF'], 'NAddDOF', '- Number of additional generalized DOF (-)')) + f.write('{:<14} {:<20} {:}\n'.format(dvr['PtfmRefzt'], 'PtfmRefzt', '- Vertical distance from ground to platform reference point (m)')) + f.write('{:<14} {:<20} {:}\n'.format('"' + dvr.get('PRPInputsFile', '') + '"', 'PRPInputsFile', '- Filename for the PRP inputs')) + f.write('---------------------- PRP STEADY STATE INPUTS -------------------------------\n') + f.write('{:>10},{:>10},{:>10},{:>10},{:>10},{:>10} {:<25} {:}\n'.format( + *dvr['uPRPInSteady'], 'uPRPInSteady', '- PRP Steady-state displacements and rotations')) + f.write('{:>10},{:>10},{:>10},{:>10},{:>10},{:>10} {:<25} {:}\n'.format( + *dvr['uDotPRPInSteady'], 'uDotPRPInSteady', '- PRP Steady-state velocities')) + f.write('{:>10},{:>10},{:>10},{:>10},{:>10},{:>10} {:<25} {:}\n'.format( + *dvr['uDotDotPRPInSteady'], 'uDotDotPRPInSteady', '- PRP Steady-state accelerations')) + + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + + # --- Delegate to HydroDynIO --- + if 'HydroDyn' in data: + hd_path = os.path.normpath(os.path.join(str(base_dir), hd_name)) + self._hydrodyn.write({'HydroDyn': data['HydroDyn']}, hd_path, str(base_dir), outlist=outlist) + + # --- Delegate to SeaStateIO --- + if 'SeaState' in data: + ss_path = os.path.normpath(os.path.join(str(base_dir), ss_name)) + self._seastate.write({'SeaState': data['SeaState']}, ss_path, str(base_dir), outlist=outlist) + + +def _read_6dof_vec(line: str) -> list: + """Parse a 6-value comma-separated vector from a line like '1, 2, -3, 0, 0, 0 name'.""" + # Split on whitespace, take tokens that look numeric (before the parameter name) + parts = line.replace(',', ' ').split() + values = [] + for p in parts: + try: + values.append(float_read(p)) + except (ValueError, IndexError): + break + if len(values) == 6: + break + return values diff --git a/openfast_io/openfast_io/drivers/inflowwind_driver.py b/openfast_io/openfast_io/drivers/inflowwind_driver.py new file mode 100644 index 0000000000..8df57ce92f --- /dev/null +++ b/openfast_io/openfast_io/drivers/inflowwind_driver.py @@ -0,0 +1,165 @@ +"""InflowWind standalone driver — reads/writes InflowWind driver input files (.inp). + +Uses ``=====`` section separators and ``--`` comment syntax (different from most +OpenFAST drivers). Handles file conversion options, interpolation tests, points +file input, output grid, and VTK slice output. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict + +from ..io.inflowwind import InflowWindIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +class InflowWindStandaloneDriver: + """Reads and writes InflowWind standalone driver input files (.inp).""" + + def __init__(self) -> None: + self._inflowwind = InflowWindIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, inp_path: Path) -> dict: + """Read an InflowWind driver file and the referenced primary input. + + Returns a dict with keys: + ``'InflowWindDriver'`` — driver-level parameters + ``'InflowWind'`` — from InflowWindIO + """ + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr: Dict[str, Any] = {} + + with open(inp_path) as f: + # --- Header --- + f.readline() # title line 1 + f.readline() # title line 2 + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- InflowWind input filename --- + f.readline() # separator + dvr['IfWFileName'] = quoted_read(f.readline().split()[0]) + + # --- File Conversion Options --- + f.readline() # separator + dvr['WrHAWC'] = bool_read(f.readline().split()[0]) + dvr['WrBladed'] = bool_read(f.readline().split()[0]) + dvr['WrVTK'] = bool_read(f.readline().split()[0]) + dvr['WrUniform'] = bool_read(f.readline().split()[0]) + + # --- Interpolation Options --- + f.readline() # separator + dvr['NumTSteps'] = int_read(f.readline().split()[0]) + dvr['TStart'] = float_read(f.readline().split()[0]) + dvr['DT'] = float_read(f.readline().split()[0]) + dvr['Summary'] = bool_read(f.readline().split()[0]) + dvr['SummaryFile'] = bool_read(f.readline().split()[0]) + dvr['BoxExceedAllow'] = bool_read(f.readline().split()[0]) + + # --- Points file input --- + f.readline() # separator + dvr['PointsFlag'] = bool_read(f.readline().split()[0]) + dvr['PointsFileName'] = quoted_read(f.readline().split()[0]) + dvr['CalcAccel'] = bool_read(f.readline().split()[0]) + + # --- Output grid --- + f.readline() # separator + dvr['WindGrid'] = bool_read(f.readline().split()[0]) + dvr['GridCtrCoord'] = _read_csv_vec(f.readline()) + dvr['GridDXYZ'] = _read_csv_vec(f.readline()) + dvr['GridNXYZ'] = _read_csv_vec(f.readline()) + + # --- Output VTK slices --- + f.readline() # separator + dvr['NOutWindXY'] = int_read(f.readline().split()[0]) + dvr['OutWindZ'] = float_read(f.readline().split()[0]) + + result: Dict[str, Any] = {'InflowWindDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so the + # InflowWind OutList section survives a standalone read → write + # roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to InflowWindIO --- + ifw_file = dvr.get('IfWFileName', '') + ifw_path = os.path.normpath(os.path.join(str(base_dir), ifw_file)) + if ifw_file and os.path.isfile(ifw_path): + ifw_data = self._inflowwind.read(Path(ifw_path), base_dir, + outlist=outlist, read_outlist_fn=_cap) + result.update(ifw_data) + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, inp_path: Path) -> None: + """Write an InflowWind driver file and all referenced module files.""" + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr = data['InflowWindDriver'] + + with open(inp_path, 'w') as f: + f.write('InflowWind driver input file\n') + f.write('InflowWind driver input file. V1.00\n') + f.write('{!s:<8} {:}\n'.format(dvr['Echo'], 'echo (flag)')) + f.write('===============================================================================\n') + + ifw_name = dvr.get('IfWFileName', 'InflowWind.dat') + f.write('{:<16} {:}\n'.format('"' + ifw_name + '"', 'IfWFileName -- IfW input filename (-)')) + f.write('===================== File Conversion Options =================================\n') + f.write(' {!s:<14} {:<15} {:}\n'.format(dvr['WrHAWC'], 'WrHAWC', '-- Convert all data to HAWC2 format? (flag)')) + f.write(' {!s:<14} {:<15} {:}\n'.format(dvr['WrBladed'], 'WrBladed', '-- Convert all data to Bladed format? (flag)')) + f.write(' {!s:<14} {:<15} {:}\n'.format(dvr['WrVTK'], 'WrVTK', '-- Convert all data to VTK format? (flag)')) + f.write(' {!s:<14} {:<15} {:}\n'.format(dvr.get('WrUniform', False), 'WrUniform', '-- Convert data to Uniform wind format? (flag)')) + f.write('===================== Tests of Interpolation Options =========================\n') + f.write('{:<15} {:<15} {:}\n'.format(dvr['NumTSteps'], 'NumTSteps', '-- number of timesteps to run (DEFAULT for all) (-)')) + f.write('{:<15} {:<15} {:}\n'.format(dvr['TStart'], 'TStart', '-- Start time (s)')) + f.write('{:<15} {:<15} {:}\n'.format(dvr['DT'], 'DT', '-- timestep size for driver to take (s, or DEFAULT for what the file contains)')) + f.write('{!s:<15} {:<15} {:}\n'.format(dvr['Summary'], 'Summary', '-- Summarize the data extents in the windfile (flag)')) + f.write('{!s:<15} {:<15} {:}\n'.format(dvr['SummaryFile'], 'SummaryFile', '-- Write summary to file .dvr.sum (flag)')) + f.write('{!s:<15} {:<15} {:}\n'.format(dvr.get('BoxExceedAllow', False), 'BoxExceedAllow', '-- Allow point sampling outside grid')) + f.write('---- Points file input (output given as PointsFileName.Velocity.dat) --------\n') + f.write('{!s:<15} {:<15} {:}\n'.format(dvr['PointsFlag'], 'PointsFileName', '-- read in a list of points from a file (flag)')) + f.write('{:<15} {:<15} {:}\n'.format('"' + dvr['PointsFileName'] + '"', 'PointsFileName', '-- name of points file (-)')) + f.write('{!s:<15} {:<15} {:}\n'.format(dvr.get('CalcAccel', False), 'CalcAccel', '-- calculate and output acceleration at points')) + f.write('---- Output grid (Points below ground will simply be ignored) ---------------\n') + f.write('{!s:<15} {:<15} {:}\n'.format(dvr['WindGrid'], 'WindGrid', '-- report wind data at set of Y,Z coordinates (flag)')) + f.write('{:<15} {:<15} {:}\n'.format(','.join(str(v) for v in dvr['GridCtrCoord']), 'GridCtrCoord', '-- coordinates of center of grid (m)')) + f.write('{:<15} {:<15} {:}\n'.format(','.join(str(v) for v in dvr['GridDXYZ']), 'GridDX,GridDY,GridDZ', '-- Stepsize of grid (m)')) + f.write('{:<15} {:<15} {:}\n'.format(','.join(str(v) for v in dvr['GridNXYZ']), 'GridNX,GridNY,GridNZ', '-- number of grid points in X, Y and Z directions (-)')) + f.write('---- Output VTK slices ------------------------------------------------------\n') + f.write('{:>4} {:<14} {:}\n'.format(dvr['NOutWindXY'], 'NOutWindXY', '-- Number of XY planes for output (-) [0 to 9]')) + f.write('{:>4} {:<14} {:}\n'.format(dvr['OutWindZ'], 'OutWindZ', '-- Z coordinates of XY planes for output (m)')) + f.write('END of driver input file\n') + + # --- Copy referenced data files (Points.inp, wind files, etc.) --- + # These are external data files the driver references but doesn't generate. + + # --- Delegate to InflowWindIO --- + if 'InflowWind' in data: + ifw_path = os.path.normpath(os.path.join(str(base_dir), ifw_name)) + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + self._inflowwind.write({'InflowWind': data['InflowWind']}, Path(ifw_path), base_dir, outlist=outlist) + + +def _read_csv_vec(line: str) -> list: + """Parse a comma-separated vector from a line like '0,0,150 name ...'.""" + raw = line.split()[0] + return [float_read(p.strip()) for p in raw.split(',')] diff --git a/openfast_io/openfast_io/drivers/moordyn_driver.py b/openfast_io/openfast_io/drivers/moordyn_driver.py new file mode 100644 index 0000000000..a18c57cf38 --- /dev/null +++ b/openfast_io/openfast_io/drivers/moordyn_driver.py @@ -0,0 +1,126 @@ +"""MoorDyn standalone driver — reads/writes MoorDyn driver input files (.inp). + +Handles environmental conditions, MoorDyn input file reference, simulation +timing (TMax/dtC), input modes, and initial position table. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Dict, List + +from ..io.moordyn import MoorDynIO +from ..parsing import bool_read, float_read, int_read, quoted_read + + +class MoorDynStandaloneDriver: + """Reads and writes MoorDyn standalone driver input files (.inp).""" + + def __init__(self) -> None: + self._moordyn = MoorDynIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, inp_path: Path) -> dict: + """Read a MoorDyn driver file and the referenced MoorDyn input. + + Returns a dict with keys: + ``'MoorDynDriver'`` — driver-level parameters + ``'MoorDyn'`` — from MoorDynIO + """ + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr: Dict[str, Any] = {} + + with open(inp_path) as f: + # --- Header --- + f.readline() # title line + f.readline() # comment line + + # --- Environmental Conditions --- + f.readline() # section header + dvr['Gravity'] = float_read(f.readline().split()[0]) + dvr['rhoW'] = float_read(f.readline().split()[0]) + dvr['WtrDpth'] = float_read(f.readline().split()[0]) + + # --- MoorDyn --- + f.readline() # section header + dvr['MDInputFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + dvr['TMax'] = float_read(f.readline().split()[0]) + dvr['dtC'] = float_read(f.readline().split()[0]) + dvr['InputsMode'] = int_read(f.readline().split()[0]) + dvr['InputsFile'] = quoted_read(f.readline().split()[0]) + dvr['NumTurbines'] = int_read(f.readline().split()[0]) + + # --- Initial Positions --- + f.readline() # section header + f.readline() # column headers + f.readline() # column units + n_rows = max(1, dvr['NumTurbines']) + positions: List[Dict[str, float]] = [] + for _ in range(n_rows): + ln = f.readline().split() + positions.append({ + 'ref_X': float_read(ln[0]), + 'ref_Y': float_read(ln[1]), + 'surge_init': float_read(ln[2]), + 'sway_init': float_read(ln[3]), + 'heave_init': float_read(ln[4]), + 'roll_init': float_read(ln[5]), + 'pitch_init': float_read(ln[6]), + 'yaw_init': float_read(ln[7]), + }) + dvr['InitialPositions'] = positions + + result: Dict[str, Any] = {'MoorDynDriver': dvr} + + # --- Delegate to MoorDynIO --- + md_file = dvr.get('MDInputFile', '') + md_path = os.path.normpath(os.path.join(str(base_dir), md_file)) + if md_file and os.path.isfile(md_path): + md_data = self._moordyn.read(Path(md_path), base_dir) + result.update(md_data) + + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, inp_path: Path) -> None: + """Write a MoorDyn driver file and the referenced MoorDyn input.""" + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr = data['MoorDynDriver'] + + with open(inp_path, 'w') as f: + f.write('MoorDyn driver input file\n') + f.write('Generated by OpenFAST_IO\n') + f.write('---------------------- ENVIRONMENTAL CONDITIONS -------------------------------\n') + f.write('{:<20} {:<17} {:}\n'.format(dvr['Gravity'], 'Gravity', '- Gravity (m/s^2)')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['rhoW'], 'rhoW', '- Water density (kg/m^3)')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['WtrDpth'], 'WtrDpth', '- Water depth (m)')) + f.write('---------------------- MOORDYN ------------------------------------------------\n') + + md_name = dvr.get('MDInputFile', 'moordyn.dat') + f.write('{:<20} {:<17} {:}\n'.format('"' + md_name + '"', 'MDInputFile', '- Primary MoorDyn input file name (quoted string)')) + f.write('{:<20} {:<17} {:}\n'.format('"' + dvr.get('OutRootName', 'driver') + '"', 'OutRootName', '- The name which prefixes all MoorDyn generated files (quoted string)')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['TMax'], 'TMax', '- Total simulation time (s)')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['dtC'], 'dtC', '- TimeInterval for the simulation (sec)')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['InputsMode'], 'InputsMode', '- MoorDyn coupled object inputs {0: zero, 1: time-series} (switch)')) + f.write('{:<20} {:<17} {:}\n'.format('"' + dvr.get('InputsFile', '') + '"', 'InputsFile', '- Filename for the MoorDyn inputs file')) + f.write('{:<20} {:<17} {:}\n'.format(dvr['NumTurbines'], 'NumTurbines', '- Number of wind turbines (-)')) + f.write('---------------------- Initial Positions --------------------------------------\n') + f.write('ref_X ref_Y surge_init sway_init heave_init roll_init pitch_init yaw_init\n') + f.write(' (m) (m) (m) (m) (m) (deg) (deg) (deg)\n') + for pos in dvr.get('InitialPositions', []): + f.write(' {:<9} {:<9} {:<13} {:<11} {:<12} {:<11} {:<13} {}\n'.format( + pos['ref_X'], pos['ref_Y'], pos['surge_init'], pos['sway_init'], + pos['heave_init'], pos['roll_init'], pos['pitch_init'], pos['yaw_init'])) + f.write('END of driver input file\n') + + # --- Delegate to MoorDynIO --- + if 'MoorDyn' in data: + md_path = os.path.normpath(os.path.join(str(base_dir), md_name)) + self._moordyn.write({'MoorDyn': data['MoorDyn']}, md_path, str(base_dir)) diff --git a/openfast_io/openfast_io/drivers/openfast.py b/openfast_io/openfast_io/drivers/openfast.py new file mode 100644 index 0000000000..95b291359f --- /dev/null +++ b/openfast_io/openfast_io/drivers/openfast.py @@ -0,0 +1,754 @@ +"""OpenFAST driver — orchestrates module-level IO classes to read/write complete simulation decks. + +This module provides: + - init_fst_vt(): Initialize the fst_vt variable tree structure + - OpenFASTDriver: Compose module IOs to read/write .fst + all referenced files +""" +import os, copy +from pathlib import Path + +from ..io.elastodyn import ElastoDynIO +from ..io.aerodyn import AeroDynIO +from ..io.inflowwind import InflowWindIO +from ..io.beamdyn import BeamDynIO +from ..io.servodyn import ServoDynIO +from ..io.hydrodyn import HydroDynIO +from ..io.seastate import SeaStateIO +from ..io.subdyn import SubDynIO +from ..io.moordyn import MoorDynIO +from ..io.map_io import MAPIO +from ..io.extptfm import ExtPtfmIO +from ..io.simple_elastodyn import SimpleElastoDynIO +from ..io.aerodisk import AeroDiskIO +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, + fix_path, +) + +try: + from openfast_io.FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + +from openfast_io.outlist import OutList, capture_outlist, emit_outlist + + +def init_fst_vt() -> dict: + """Initialize the fst_vt structure. + + Key names and structure match the legacy InputReader_OpenFAST.fst_vt. + WEIS and downstream code access these keys directly — do not rename or remove. + + Notes: + - ElastoDynBlade, AeroDynBlade, BeamDynBlade default to [{},{},{}] (list + of per-blade dicts). The reader's blade-dedup logic may collapse to a + single dict when all blades are identical. + - BStC/NStC/TStC/SStC are lists of StC parameter dicts, empty by default. + - _path keys are set dynamically during read(), not here. + """ + return { + 'Fst': {}, + # Mirror the legacy openfast_io reader exactly: start from FstOutput (its + # built-in defaults) and ADD the deck's channels during read() via + # capture_outlist. The legacy reader does deepcopy(FstOutput) + set_outlist + # per module; reproducing that init is required for behavior parity (the prior + # bug was skipping the per-module capture entirely, leaving ONLY the defaults). + 'outlist': copy.deepcopy(FstOutput) if FstOutput else {}, + 'description': '', + 'ElastoDyn': {}, + 'SimpleElastoDyn': {}, + 'ElastoDynBlade': [{}, {}, {}], + 'ElastoDynTower': {}, + 'InflowWind': {}, + 'AeroDyn': {}, + 'AeroDisk': {}, + 'AeroDynBlade': [{}, {}, {}], + 'AeroDynPolar': [], + 'ServoDyn': {}, + 'DISCON_in': {}, + 'spd_trq': {}, + 'BStC': [], + 'NStC': [], + 'TStC': [], + 'SStC': [], + 'HydroDyn': {}, + 'SeaState': {}, + 'MoorDyn': {}, + 'SubDyn': {}, + 'ExtPtfm': {}, + 'MAP': {}, + 'BeamDyn': [{}, {}, {}], + 'BeamDynBlade': [{}, {}, {}], + 'WaterKin': {}, + 'SoilDyn': {}, + } + + +class OpenFASTDriver: + """Reads and writes a complete OpenFAST simulation deck. + + Composes module-level IO objects. Returns fst_vt with the same + structure as the legacy InputReader_OpenFAST.fst_vt. + """ + + def __init__(self): + self._elastodyn = ElastoDynIO() + self._aerodyn = AeroDynIO() + self._inflowwind = InflowWindIO() + self._beamdyn = BeamDynIO() + self._servodyn = ServoDynIO() + self._hydrodynamics = HydroDynIO() + self._seastate = SeaStateIO() + self._subdyn = SubDynIO() + self._moordyn = MoorDynIO() + self._map = MAPIO() + self._extptfm = ExtPtfmIO() + self._simple_elastodyn = SimpleElastoDynIO() + self._aerodisk = AeroDiskIO() + + def read(self, fst_path: Path) -> dict: + """Read .fst and all referenced module files. Returns complete fst_vt.""" + fst_path = Path(fst_path) + fst_vt = init_fst_vt() + base_dir = fst_path.parent + + # Callback that captures a module's OutList section into fst_vt['outlist'], + # mirroring legacy openfast_io read_outlist/set_outlist. Modules that take a + # read_outlist_fn use this; freeform modules (SubDyn/SeaState) pass freeform=True. + def _cap(f, module, freeform=False): + return capture_outlist(f, fst_vt['outlist'], module, freeform=freeform) + + def _cap_ff(f, module): + return capture_outlist(f, fst_vt['outlist'], module, freeform=True) + + fst_vt['Fst'] = self._read_main_input(fst_path, base_dir) + + n_rotors = fst_vt['Fst'].get('NRotors', 1) + if n_rotors > 1: + raise ValueError( + 'openfast_io does not currently support multi-rotor turbines (NRotors > 1), ' + 'this feature will be added in a future release' + ) + + ed_file = os.path.join(str(base_dir), fst_vt['Fst'].get('EDFile', '')) + fastdir = str(base_dir) + + # ------- ElastoDyn ------- + comp_elast = fst_vt['Fst'].get('CompElast', 1) + if comp_elast == 3: + sed_rel = fst_vt['Fst'].get('EDFile', '') + sed_file = os.path.normpath(os.path.join(fastdir, sed_rel)) + if os.path.isfile(sed_file): + sed_data = self._simple_elastodyn.read(sed_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['SimpleElastoDyn'] = sed_data.get('SimpleElastoDyn', {}) + elif comp_elast in (1, 2): + if os.path.isfile(ed_file): + fst_vt['Fst']['EDFile_path'] = os.path.split(fst_vt['Fst']['EDFile'])[0] + ed_data = self._elastodyn.read(Path(ed_file), Path(os.path.dirname(ed_file)), outlist=fst_vt['outlist']) + + fst_vt['ElastoDyn'] = ed_data.get('ElastoDyn', {}) + fst_vt['ElastoDynTower'] = ed_data.get('ElastoDynTower', {}) + + # Blade deduplication logic — match legacy behavior + blades = ed_data.get('ElastoDynBlade', [{}, {}, {}]) + bldFile1 = fst_vt['ElastoDyn'].get('BldFile1', '') + bldFile2 = fst_vt['ElastoDyn'].get('BldFile2', '') + bldFile3 = fst_vt['ElastoDyn'].get('BldFile3', '') + num_bl = fst_vt['ElastoDyn'].get('NumBl', 3) + + if bldFile1 == bldFile2 and bldFile1 == bldFile3: + fst_vt['ElastoDynBlade'] = blades[0] if blades else {} + elif num_bl == 2 and bldFile1 == bldFile2: + fst_vt['ElastoDynBlade'] = blades[0] if blades else {} + elif num_bl == 1: + fst_vt['ElastoDynBlade'] = blades[0] if blades else {} + else: + fst_vt['ElastoDynBlade'] = blades + + # ------- BeamDyn (per-blade) ------- + # Match the legacy openfast_io FAST_reader: read BeamDyn whenever BDBldFile(1) + # exists on disk, regardless of CompElast. The legacy reader reads (and captures + # the OutList of) an inactive BeamDyn when its blade file is present; reproduce that. + _bd1 = os.path.normpath(os.path.join(fastdir, fst_vt['Fst'].get('BDBldFile(1)', ''))) + if comp_elast == 2 or os.path.isfile(_bd1): + num_bl = fst_vt['ElastoDyn'].get('NumBl', 3) + bd_blades = [] + bd_blade_data = [] + for i in range(min(num_bl, 3)): + bd_file_key = f'BDBldFile({i+1})' + bd_rel = fst_vt['Fst'].get(bd_file_key, '') + bd_file = os.path.normpath(os.path.join(fastdir, bd_rel)) + if os.path.isfile(bd_file): + bd_data = self._beamdyn.read(bd_file, base_dir=os.path.dirname(bd_file), outlist=fst_vt['outlist'], read_outlist_fn=_cap) + bd_blades.append(bd_data.get('BeamDyn', {})) + bd_blade_data.append(bd_data.get('BeamDynBlade', {})) + else: + bd_blades.append({}) + bd_blade_data.append({}) + # Blade dedup — match legacy (FAST_reader): collapse to a single dict when + # the BeamDyn blade files are identical. Mirrors the ElastoDyn block above. + bd1 = fst_vt['Fst'].get('BDBldFile(1)', '') + bd2 = fst_vt['Fst'].get('BDBldFile(2)', '') + bd3 = fst_vt['Fst'].get('BDBldFile(3)', '') + if (bd1 == bd2 == bd3) or num_bl == 1 or (num_bl == 2 and bd1 == bd2): + fst_vt['BeamDyn'] = bd_blades[0] if bd_blades else {} + fst_vt['BeamDynBlade'] = bd_blade_data[0] if bd_blade_data else {} + else: + fst_vt['BeamDyn'] = bd_blades + fst_vt['BeamDynBlade'] = bd_blade_data + + # ------- InflowWind ------- + comp_inflow = fst_vt['Fst'].get('CompInflow', 0) + if comp_inflow == 1: + ifw_rel = fst_vt['Fst'].get('InflowFile', '') + ifw_file = os.path.normpath(os.path.join(fastdir, ifw_rel)) + if os.path.isfile(ifw_file): + ifw_data = self._inflowwind.read(ifw_file, base_dir=os.path.dirname(ifw_file), outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['InflowWind'] = ifw_data.get('InflowWind', {}) + + # ------- AeroDyn ------- + comp_aero = fst_vt['Fst'].get('CompAero', 0) + if comp_aero == 2: + aero_rel = fst_vt['Fst'].get('AeroFile', '') + aero_file = os.path.normpath(os.path.join(fastdir, aero_rel)) + if os.path.isfile(aero_file): + num_bl = fst_vt['ElastoDyn'].get('NumBl', 3) + ad_data = self._aerodyn.read( + aero_file, + base_dir=os.path.dirname(aero_file), + num_blades=num_bl, + aero_file_path=fst_vt['Fst'].get('AeroFile_path', ''), + outlist=fst_vt['outlist'], + read_outlist_fn=_cap, + ) + fst_vt['AeroDyn'] = ad_data.get('AeroDyn', {}) + + # AeroDynBlade dedup (same pattern as ElastoDyn) + ad_blade = ad_data.get('AeroDynBlade', {}) + if isinstance(ad_blade, dict): + fst_vt['AeroDynBlade'] = ad_blade # already collapsed + else: + fst_vt['AeroDynBlade'] = ad_blade + + fst_vt['AeroDynPolar'] = ad_data.get('AeroDynPolar', []) + elif comp_aero == 1: # AeroDisk {0=None; 1=AeroDisk; 2=AeroDyn; 3=ExtLoads} + aero_rel = fst_vt['Fst'].get('AeroFile', '') + aero_file = os.path.normpath(os.path.join(fastdir, aero_rel)) + if os.path.isfile(aero_file): + adsk_data = self._aerodisk.read(aero_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['AeroDisk'] = adsk_data.get('AeroDisk', {}) + + # ------- ServoDyn ------- + comp_servo = fst_vt['Fst'].get('CompServo', 0) + if comp_servo == 1: + sd_rel = fst_vt['Fst'].get('ServoFile', '') + sd_file = os.path.normpath(os.path.join(fastdir, sd_rel)) + if os.path.isfile(sd_file): + sd_data = self._servodyn.read( + sd_file, + base_dir=fastdir, + servo_file_rel=sd_rel, + outlist=fst_vt['outlist'], + read_outlist_fn=_cap, + ) + fst_vt['ServoDyn'] = sd_data.get('ServoDyn', {}) + fst_vt['BStC'] = sd_data.get('BStC', []) + fst_vt['NStC'] = sd_data.get('NStC', []) + fst_vt['TStC'] = sd_data.get('TStC', []) + fst_vt['SStC'] = sd_data.get('SStC', []) + if 'DISCON_in' in sd_data: + fst_vt['DISCON_in'] = sd_data['DISCON_in'] + if 'spd_trq' in sd_data: + fst_vt['spd_trq'] = sd_data['spd_trq'] + + # ------- HydroDyn ------- + comp_hydro = fst_vt['Fst'].get('CompHydro', 0) + if comp_hydro == 1: + hd_rel = fst_vt['Fst'].get('HydroFile', '') + hd_file = os.path.normpath(os.path.join(fastdir, hd_rel)) + if os.path.isfile(hd_file): + fst_vt['Fst']['HydroFile_path'] = os.path.split(hd_rel)[0] + hd_data = self._hydrodynamics.read(hd_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['HydroDyn'] = hd_data.get('HydroDyn', {}) + + # ------- SeaState ------- + comp_seast = fst_vt['Fst'].get('CompSeaSt', 0) + if comp_seast == 1: + ss_rel = fst_vt['Fst'].get('SeaStFile', '') + ss_file = os.path.normpath(os.path.join(fastdir, ss_rel)) + if os.path.isfile(ss_file): + ss_data = self._seastate.read(ss_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap_ff) + fst_vt['SeaState'] = ss_data.get('SeaState', {}) + + # ------- SubDyn / ExtPtfm ------- + comp_sub = fst_vt['Fst'].get('CompSub', 0) + if comp_sub == 1: + sub_rel = fst_vt['Fst'].get('SubFile', '') + sub_file = os.path.normpath(os.path.join(fastdir, sub_rel)) + if os.path.isfile(sub_file): + fst_vt['Fst']['SubFile_path'] = os.path.split(sub_rel)[0] + sub_data = self._subdyn.read(sub_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap_ff) + fst_vt['SubDyn'] = sub_data.get('SubDyn', {}) + elif comp_sub == 2: + sub_rel = fst_vt['Fst'].get('SubFile', '') + sub_file = os.path.normpath(os.path.join(fastdir, sub_rel)) + if os.path.isfile(sub_file): + fst_vt['Fst']['SubFile_path'] = os.path.split(sub_rel)[0] + ep_data = self._extptfm.read(sub_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['ExtPtfm'] = ep_data.get('ExtPtfm', {}) + + # ------- MoorDyn / MAP ------- + comp_mooring = fst_vt['Fst'].get('CompMooring', 0) + if comp_mooring == 1: # MAP + moor_rel = fst_vt['Fst'].get('MooringFile', '') + moor_file = os.path.normpath(os.path.join(fastdir, moor_rel)) + if os.path.isfile(moor_file): + fst_vt['Fst']['MooringFile_path'] = os.path.split(moor_rel)[0] + map_data = self._map.read(moor_file) + fst_vt['MAP'] = map_data.get('MAP', {}) + elif comp_mooring == 3: # MoorDyn + moor_rel = fst_vt['Fst'].get('MooringFile', '') + moor_file = os.path.normpath(os.path.join(fastdir, moor_rel)) + if os.path.isfile(moor_file): + fst_vt['Fst']['MooringFile_path'] = os.path.split(moor_rel)[0] + md_data = self._moordyn.read(moor_file, outlist=fst_vt['outlist'], read_outlist_fn=_cap) + fst_vt['MoorDyn'] = md_data.get('MoorDyn', {}) + + return fst_vt + + def write(self, fst_vt: dict, output_dir: Path, case_name: str) -> list: + """Write all enabled module files + .fst. Returns list of written paths. + + Parameters + ---------- + fst_vt : dict + Complete variable tree (same shape as returned by read()). + output_dir : Path + Directory where all files will be written. + case_name : str + Base name for the .fst and sub-files (e.g. "5MW_Land"). + """ + import numpy as np + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + written = [] + fst = fst_vt['Fst'] + + # ------- ElastoDyn / SimpleElastoDyn ------- + comp_elast = fst.get('CompElast', 1) + if comp_elast == 3: + sed_name = case_name + '_SimpleElastoDyn.dat' + fst['EDFile'] = sed_name + sed_path = str(output_dir / sed_name) + self._simple_elastodyn.write({'SimpleElastoDyn': fst_vt.get('SimpleElastoDyn', {})}, sed_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(sed_path) + elif comp_elast in (1, 2): + # ElastoDyn blade(s) + edblade = fst_vt.get('ElastoDynBlade', {}) + if isinstance(edblade, list): + for i, bld in enumerate(edblade): + bld_name = '{}_{}_ElastoDynBlade_{}.dat'.format(case_name, 'ElastoDyn', i + 1) + fst_vt['ElastoDyn']['BldFile{}'.format(i + 1)] = bld_name + bld_path = str(output_dir / bld_name) + self._elastodyn._write_blade(bld, bld_path) + written.append(bld_path) + elif isinstance(edblade, dict) and edblade: + bld_name = case_name + '_ElastoDynBlade.dat' + fst_vt['ElastoDyn']['BldFile1'] = bld_name + fst_vt['ElastoDyn']['BldFile2'] = bld_name + fst_vt['ElastoDyn']['BldFile3'] = bld_name + bld_path = str(output_dir / bld_name) + self._elastodyn._write_blade(edblade, bld_path) + written.append(bld_path) + + # Tower + twr_name = case_name + '_ElastoDynTower.dat' + fst_vt['ElastoDyn']['TwrFile'] = twr_name + twr_path = str(output_dir / twr_name) + self._elastodyn._write_tower(fst_vt.get('ElastoDynTower', {}), twr_path) + written.append(twr_path) + + # Main ED + ed_name = case_name + '_ElastoDyn.dat' + fst['EDFile'] = ed_name + ed_path = str(output_dir / ed_name) + self._elastodyn.write( + {'ElastoDyn': fst_vt.get('ElastoDyn', {}), + 'ElastoDynTower': fst_vt.get('ElastoDynTower', {}), + 'ElastoDynBlade': fst_vt.get('ElastoDynBlade', {})}, + ed_path, + base_dir=str(output_dir), + outlist=fst_vt.get('outlist'), + ) + written.append(ed_path) + + # ------- BeamDyn ------- + # Write whenever fst_vt['BeamDyn'] is populated (symmetric with the read guard, + # which reads BeamDyn whenever the blade file exists) — not gated on CompElast. + # Handle both the collapsed-dict shape (identical blades) and the list shape + # (distinct blades), and assign a UNIQUE per-blade BldFile so the writer does not + # send every blade to the same path (silent blade-property collision). + bd_data = fst_vt.get('BeamDyn') + bd_blade_all = fst_vt.get('BeamDynBlade') + num_bl_w = fst_vt.get('ElastoDyn', {}).get('NumBl', 3) + + def _write_bd_blade(idx, bd_src, blade, main_name, blade_name): + bd = dict(bd_src) + bd['BldFile'] = blade_name + bd_path = str(output_dir / main_name) + self._beamdyn.write( + {'BeamDyn': bd, 'BeamDynBlade': blade}, + bd_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist'), + ) + written.append(bd_path) + + if isinstance(bd_data, dict) and bd_data: + # Collapsed identical blades: one file, all BDBldFile(i) point at it. + blade = bd_blade_all if isinstance(bd_blade_all, dict) else \ + (bd_blade_all[0] if isinstance(bd_blade_all, list) and bd_blade_all else {}) + main_name = case_name + '_BeamDyn.dat' + _write_bd_blade(0, bd_data, blade, main_name, case_name + '_BeamDyn_Blade.dat') + for k in range(num_bl_w): + fst['BDBldFile({})'.format(k + 1)] = main_name + elif isinstance(bd_data, list): + for i, bd in enumerate(bd_data): + if bd: + main_name = case_name + '_BeamDyn_{}.dat'.format(i + 1) + fst['BDBldFile({})'.format(i + 1)] = main_name + blade = bd_blade_all[i] if isinstance(bd_blade_all, list) and i < len(bd_blade_all) else {} + _write_bd_blade(i, bd, blade, main_name, case_name + '_BeamDyn_Blade_{}.dat'.format(i + 1)) + + # ------- InflowWind ------- + if fst.get('CompInflow', 0) == 1: + ifw_name = case_name + '_InflowWind.dat' + fst['InflowFile'] = ifw_name + ifw_path = str(output_dir / ifw_name) + self._inflowwind.write({'InflowWind': fst_vt.get('InflowWind', {})}, ifw_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(ifw_path) + + # ------- AeroDyn / AeroDisk ------- + comp_aero = fst.get('CompAero', 0) + if comp_aero == 2: + ad_name = case_name + '_AeroDyn.dat' + fst['AeroFile'] = ad_name + ad_path = str(output_dir / ad_name) + self._aerodyn.write( + {'AeroDyn': fst_vt.get('AeroDyn', {}), + 'AeroDynBlade': fst_vt.get('AeroDynBlade', {}), + 'AeroDynPolar': fst_vt.get('AeroDynPolar', [])}, + ad_path, + base_dir=str(output_dir), + outlist=fst_vt.get('outlist'), + ) + written.append(ad_path) + elif comp_aero == 1: # AeroDisk {0=None; 1=AeroDisk; 2=AeroDyn; 3=ExtLoads} + adsk_name = case_name + '_AeroDisk.dat' + fst['AeroFile'] = adsk_name + adsk_path = str(output_dir / adsk_name) + self._aerodisk.write({'AeroDisk': fst_vt.get('AeroDisk', {})}, adsk_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(adsk_path) + + # ------- ServoDyn ------- + if fst.get('CompServo', 0) == 1: + sd_name = case_name + '_ServoDyn.dat' + fst['ServoFile'] = sd_name + sd_path = str(output_dir / sd_name) + self._servodyn.write( + {'ServoDyn': fst_vt.get('ServoDyn', {}), + 'BStC': fst_vt.get('BStC', []), + 'NStC': fst_vt.get('NStC', []), + 'TStC': fst_vt.get('TStC', []), + 'SStC': fst_vt.get('SStC', []), + 'spd_trq': fst_vt.get('spd_trq')}, + sd_path, + base_dir=str(output_dir), + outlist=fst_vt.get('outlist'), + ) + written.append(sd_path) + + # ------- SeaState ------- + if fst.get('CompSeaSt', 0) == 1: + ss_name = case_name + '_SeaState.dat' + fst['SeaStFile'] = ss_name + ss_path = str(output_dir / ss_name) + self._seastate.write({'SeaState': fst_vt.get('SeaState', {})}, ss_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(ss_path) + + # ------- HydroDyn ------- + if fst.get('CompHydro', 0) == 1: + hd_name = case_name + '_HydroDyn.dat' + fst['HydroFile'] = hd_name + hd_path = str(output_dir / hd_name) + self._hydrodynamics.write({'HydroDyn': fst_vt.get('HydroDyn', {})}, hd_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(hd_path) + + # ------- SubDyn / ExtPtfm ------- + comp_sub = fst.get('CompSub', 0) + if comp_sub == 1: + sub_name = case_name + '_SubDyn.dat' + fst['SubFile'] = sub_name + sub_path = str(output_dir / sub_name) + self._subdyn.write({'SubDyn': fst_vt.get('SubDyn', {})}, sub_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(sub_path) + elif comp_sub == 2: + ep_name = case_name + '_ExtPtfm.dat' + fst['SubFile'] = ep_name + ep_path = str(output_dir / ep_name) + self._extptfm.write({'ExtPtfm': fst_vt.get('ExtPtfm', {})}, ep_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(ep_path) + + # ------- MoorDyn / MAP ------- + comp_mooring = fst.get('CompMooring', 0) + if comp_mooring == 1: + map_name = case_name + '_MAP.dat' + fst['MooringFile'] = map_name + map_path = str(output_dir / map_name) + self._map.write({'MAP': fst_vt.get('MAP', {})}, map_path, base_dir=str(output_dir)) + written.append(map_path) + elif comp_mooring == 3: + md_name = case_name + '_MoorDyn.dat' + fst['MooringFile'] = md_name + md_path = str(output_dir / md_name) + self._moordyn.write({'MoorDyn': fst_vt.get('MoorDyn', {})}, md_path, base_dir=str(output_dir), outlist=fst_vt.get('outlist')) + written.append(md_path) + + # ------- Main .fst file ------- + fst_path = str(output_dir / (case_name + '.fst')) + self._write_main_input(fst_vt, fst_path) + written.append(fst_path) + + return written + + def _write_main_input(self, fst_vt: dict, fst_path: str) -> None: + """Write the main .fst input file.""" + import numpy as np + fst = fst_vt['Fst'] + + with open(fst_path, 'w') as f: + f.write('------- OpenFAST INPUT FILE -------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + + # Simulation Control + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(fst['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + str(fst.get('AbortLevel', 'FATAL')) + '"', 'AbortLevel', '- Error level when simulation should abort\n')) + f.write('{:<22} {:<11} {:}'.format(fst['TMax'], 'TMax', '- Total run time (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst['DT'], 'DT', '- Recommended module time step (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('ModCoupling', 1), 'ModCoupling', '- Module coupling method (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('InterpOrder', 1), 'InterpOrder', '- Interpolation order\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('NumCrctn', 0), 'NumCrctn', '- Numerical damping parameter\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('RhoInf', 1), 'RhoInf', '- Convergence iteration error tolerance\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('ConvTol', 1e-5), 'ConvTol', '- Convergence tolerance\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('MaxConvIter', 4), 'MaxConvIter', '- Maximum number of convergence iterations\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('DT_UJac', 'default'), 'DT_UJac', '- Time between Jacobian calls\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('UJacSclFact', 1e6), 'UJacSclFact', '- Scaling factor for Jacobians\n')) + + # Feature Switches + f.write('---------------------- FEATURE SWITCHES AND FLAGS ------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(fst.get('NRotors', 1), 'NRotors', '- Number of rotors\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompElast', 1), 'CompElast', '- Compute structural dynamics\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompInflow', 0), 'CompInflow', '- Compute inflow wind velocities\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompAero', 0), 'CompAero', '- Compute aerodynamic loads\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompServo', 0), 'CompServo', '- Compute control and electrical-drive dynamics\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompSeaSt', 0), 'CompSeaSt', '- Compute sea state information\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompHydro', 0), 'CompHydro', '- Compute hydrodynamic loads\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompSub', 0), 'CompSub', '- Compute sub-structural dynamics\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompMooring', 0), 'CompMooring', '- Compute mooring system\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompIce', 0), 'CompIce', '- Compute ice loads\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('CompSoil', 0), 'CompSoil', '- Compute soil dynamics\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('MHK', 0), 'MHK', '- MHK turbine type\n')) + mirror = fst.get('MirrorRotor', [False]) + f.write('{:<22} {:<11} {:}'.format(' '.join([str(b)[0] for b in np.array(mirror, dtype=bool)]), 'MirrorRotor', '- List of rotor rotation directions\n')) + + # Environmental Conditions + f.write('---------------------- ENVIRONMENTAL CONDITIONS --------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(fst.get('Gravity', 9.81), 'Gravity', '- Gravitational acceleration (m/s^2)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('AirDens', 1.225), 'AirDens', '- Air density (kg/m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('WtrDens', 1025.0), 'WtrDens', '- Water density (kg/m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('KinVisc', 1.464e-5), 'KinVisc', '- Kinematic viscosity (m^2/s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('SpdSound', 335.0), 'SpdSound', '- Speed of sound (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('Patm', 103500.0), 'Patm', '- Atmospheric pressure (Pa)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('Pvap', 1700.0), 'Pvap', '- Vapour pressure (Pa)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('WtrDpth', 0.0), 'WtrDpth', '- Water depth (m)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('MSL2SWL', 0.0), 'MSL2SWL', '- Offset between still-water level and mean sea level (m)\n')) + + # Input Files + f.write('---------------------- INPUT FILES ---------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('EDFile', 'unused') + '"', 'EDFile', '- ElastoDyn input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('BDBldFile(1)', 'unused') + '"', 'BDBldFile(1)', '- BeamDyn blade 1 input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('BDBldFile(2)', 'unused') + '"', 'BDBldFile(2)', '- BeamDyn blade 2 input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('BDBldFile(3)', 'unused') + '"', 'BDBldFile(3)', '- BeamDyn blade 3 input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('InflowFile', 'unused') + '"', 'InflowFile', '- InflowWind input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('AeroFile', 'unused') + '"', 'AeroFile', '- AeroDyn input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('ServoFile', 'unused') + '"', 'ServoFile', '- ServoDyn input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('SeaStFile', 'unused') + '"', 'SeaStFile', '- SeaState input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('HydroFile', 'unused') + '"', 'HydroFile', '- HydroDyn input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('SubFile', 'unused') + '"', 'SubFile', '- SubDyn/ExtPtfm input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('MooringFile', 'unused') + '"', 'MooringFile', '- MoorDyn/MAP input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('IceFile', 'unused') + '"', 'IceFile', '- Ice input file\n')) + f.write('{:<22} {:<11} {:}'.format('"' + fst.get('SoilFile', 'unused') + '"', 'SoilFile', '- Soil input file\n')) + + # Output + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(fst.get('SumPrint', False), 'SumPrint', '- Print summary data\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('SttsTime', 10.0), 'SttsTime', '- Screen status message interval (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('ChkptTime', 99999.9), 'ChkptTime', '- Checkpoint interval (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('DT_Out', 'default'), 'DT_Out', '- Time step for tabular output (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('TStart', 0.0), 'TStart', '- Time to begin tabular output (s)\n')) + f.write('{:<22d} {:<11} {:}'.format(fst.get('OutFileFmt', 2), 'OutFileFmt', '- Output file format\n')) + f.write('{!s:<22} {:<11} {:}'.format(fst.get('TabDelim', True), 'TabDelim', '- Tab delimited output\n')) + f.write('{:<22} {:<11} {:}'.format('"' + str(fst.get('OutFmt', 'ES10.3E2')) + '"', 'OutFmt', '- Format for text tabular output\n')) + + # Linearization + f.write('---------------------- LINEARIZATION -------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(fst.get('Linearize', False), 'Linearize', '- Linearization analysis\n')) + f.write('{!s:<22} {:<11} {:}'.format(fst.get('CalcSteady', False), 'CalcSteady', '- Calculate steady-state periodic operating point\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('TrimCase', 3), 'TrimCase', '- Controller parameter to trim\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('TrimTol', 0.001), 'TrimTol', '- Trim tolerance\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('TrimGain', 0.001), 'TrimGain', '- Trim gain\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('Twr_Kdmp', 0), 'Twr_Kdmp', '- Tower damping factor\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('Bld_Kdmp', 0), 'Bld_Kdmp', '- Blade damping factor\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('NLinTimes', 2), 'NLinTimes', '- Number of linearization times\n')) + lin_times = fst.get('LinTimes', [30.0, 60.0]) + f.write('{:<22} {:<11} {:}'.format(', '.join(['{:f}'.format(t) for t in np.array(lin_times, dtype=float)]), 'LinTimes', '- Linearization times (s)\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('LinInputs', 1), 'LinInputs', '- Inputs in linearization\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('LinOutputs', 1), 'LinOutputs', '- Outputs in linearization\n')) + f.write('{!s:<22} {:<11} {:}'.format(fst.get('LinOutJac', False), 'LinOutJac', '- Include full Jacobians\n')) + f.write('{!s:<22} {:<11} {:}'.format(fst.get('LinOutMod', False), 'LinOutMod', '- Write module-level linearization\n')) + + # Visualization + f.write('---------------------- VISUALIZATION ------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(fst.get('WrVTK', 0), 'WrVTK', '- VTK visualization data output\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('VTK_type', 1), 'VTK_type', '- Type of VTK visualization data\n')) + f.write('{!s:<22} {:<11} {:}'.format(fst.get('VTK_fields', False), 'VTK_fields', '- Write mesh fields to VTK\n')) + f.write('{:<22} {:<11} {:}'.format(fst.get('VTK_fps', 1), 'VTK_fps', '- Frame rate for VTK output\n')) + + def _read_main_input(self, fst_path: Path, base_dir: Path) -> dict: + """Read main .fst file. Logic from FAST_reader.read_MainInput().""" + fst = {} + f = open(fst_path) + + # Header + f.readline() + # We store the description but it goes in fst_vt['description'], not fst dict + # For now, skip — the driver's read() can set it separately + f.readline() + + # Simulation Control + f.readline() + fst['Echo'] = bool_read(f.readline().split()[0]) + fst['AbortLevel'] = quoted_read(f.readline().split()[0]) + fst['TMax'] = float_read(f.readline().split()[0]) + fst['DT'] = float_read(f.readline().split()[0]) + fst['ModCoupling'] = int(f.readline().split()[0]) + fst['InterpOrder'] = int(f.readline().split()[0]) + fst['NumCrctn'] = int(f.readline().split()[0]) + fst['RhoInf'] = float_read(f.readline().split()[0]) + fst['ConvTol'] = float_read(f.readline().split()[0]) + fst['MaxConvIter'] = int(f.readline().split()[0]) + fst['DT_UJac'] = float_read(f.readline().split()[0]) + fst['UJacSclFact'] = float_read(f.readline().split()[0]) + + # Feature Switches and Flags + f.readline() + fst['NRotors'] = int(f.readline().split()[0]) + fst['CompElast'] = int(f.readline().split()[0]) + fst['CompInflow'] = int(f.readline().split()[0]) + fst['CompAero'] = int(f.readline().split()[0]) + fst['CompServo'] = int(f.readline().split()[0]) + fst['CompSeaSt'] = int(f.readline().split()[0]) + fst['CompHydro'] = int(f.readline().split()[0]) + fst['CompSub'] = int(f.readline().split()[0]) + fst['CompMooring'] = int(f.readline().split()[0]) + fst['CompIce'] = int(f.readline().split()[0]) + fst['CompSoil'] = int(f.readline().split()[0]) + fst['MHK'] = int(f.readline().split()[0]) + fst['MirrorRotor'] = read_array(f, fst['NRotors'], array_type=bool) + + # Environmental conditions + f.readline() + fst['Gravity'] = float_read(f.readline().split()[0]) + fst['AirDens'] = float_read(f.readline().split()[0]) + fst['WtrDens'] = float_read(f.readline().split()[0]) + fst['KinVisc'] = float_read(f.readline().split()[0]) + fst['SpdSound'] = float_read(f.readline().split()[0]) + fst['Patm'] = float_read(f.readline().split()[0]) + fst['Pvap'] = float_read(f.readline().split()[0]) + fst['WtrDpth'] = float_read(f.readline().split()[0]) + fst['MSL2SWL'] = float_read(f.readline().split()[0]) + + # Input Files + f.readline() + fst['EDFile'] = quoted_read(f.readline().split()[0]) + fst['BDBldFile(1)'] = quoted_read(f.readline().split()[0]) + fst['BDBldFile(2)'] = quoted_read(f.readline().split()[0]) + fst['BDBldFile(3)'] = quoted_read(f.readline().split()[0]) + fst['InflowFile'] = quoted_read(f.readline().split()[0]) + fst['AeroFile'] = quoted_read(f.readline().split()[0]) + fst['ServoFile'] = quoted_read(f.readline().split()[0]) + fst['SeaStFile'] = quoted_read(f.readline().split()[0]) + fst['HydroFile'] = quoted_read(f.readline().split()[0]) + fst['SubFile'] = quoted_read(f.readline().split()[0]) + fst['MooringFile'] = quoted_read(f.readline().split()[0]) + fst['IceFile'] = quoted_read(f.readline().split()[0]) + fst['SoilFile'] = quoted_read(f.readline().split()[0]) + + # Output Parameters + f.readline() + fst['SumPrint'] = bool_read(f.readline().split()[0]) + fst['SttsTime'] = float_read(f.readline().split()[0]) + fst['ChkptTime'] = float_read(f.readline().split()[0]) + fst['DT_Out'] = float_read(f.readline().split()[0]) + fst['TStart'] = float_read(f.readline().split()[0]) + fst['OutFileFmt'] = int(f.readline().split()[0]) + fst['TabDelim'] = bool_read(f.readline().split()[0]) + fst['OutFmt'] = quoted_read(f.readline().split()[0]) + + # Linearization + f.readline() + fst['Linearize'] = f.readline().split()[0] + fst['CalcSteady'] = f.readline().split()[0] + fst['TrimCase'] = f.readline().split()[0] + fst['TrimTol'] = f.readline().split()[0] + fst['TrimGain'] = f.readline().split()[0] + fst['Twr_Kdmp'] = f.readline().split()[0] + fst['Bld_Kdmp'] = f.readline().split()[0] + fst['NLinTimes'] = int(f.readline().split()[0]) + fst['LinTimes'] = read_array(f, fst['NLinTimes'], array_type=float) + fst['LinInputs'] = f.readline().split()[0] + fst['LinOutputs'] = f.readline().split()[0] + fst['LinOutJac'] = f.readline().split()[0] + fst['LinOutMod'] = f.readline().split()[0] + + # Visualization + f.readline() + fst['WrVTK'] = int(f.readline().split()[0]) + fst['VTK_type'] = int(f.readline().split()[0]) + fst['VTK_fields'] = bool_read(f.readline().split()[0]) + fst['VTK_fps'] = float_read(f.readline().split()[0]) + + f.close() + + # File paths + fst['EDFile_path'] = os.path.split(fst['EDFile'])[0] + fst['BDBldFile(1_path)'] = os.path.split(fst['BDBldFile(1)'])[0] + fst['BDBldFile(2_path)'] = os.path.split(fst['BDBldFile(2)'])[0] + fst['BDBldFile(3_path)'] = os.path.split(fst['BDBldFile(3)'])[0] + fst['InflowFile_path'] = os.path.split(fst['InflowFile'])[0] + fst['AeroFile_path'] = os.path.split(fst['AeroFile'])[0] + fst['ServoFile_path'] = os.path.split(fst['ServoFile'])[0] + fst['HydroFile_path'] = os.path.split(fst['HydroFile'])[0] + fst['SubFile_path'] = os.path.split(fst['SubFile'])[0] + fst['MooringFile_path'] = os.path.split(fst['MooringFile'])[0] + fst['IceFile_path'] = os.path.split(fst['IceFile'])[0] + + return fst + + @staticmethod + def _resolve(base_dir: Path, rel_path: str) -> Path: + return base_dir / rel_path.strip('"').strip("'") diff --git a/openfast_io/openfast_io/drivers/seastate_driver.py b/openfast_io/openfast_io/drivers/seastate_driver.py new file mode 100644 index 0000000000..52c794d9df --- /dev/null +++ b/openfast_io/openfast_io/drivers/seastate_driver.py @@ -0,0 +1,125 @@ +"""SeaState standalone driver — reads/writes SeaState driver input files (.inp). + +The simplest of the standalone drivers — just environmental conditions, +SeaState file reference, wave output options, and simulation timing. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict + +from ..io.seastate import SeaStateIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +class SeaStateStandaloneDriver: + """Reads and writes SeaState standalone driver input files (.inp).""" + + def __init__(self) -> None: + self._seastate = SeaStateIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, inp_path: Path) -> dict: + """Read a SeaState driver file and the referenced SeaState input. + + Returns a dict with keys: + ``'SeaStateDriver'`` — driver-level parameters + ``'SeaState'`` — from SeaStateIO + """ + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr: Dict[str, Any] = {} + + with open(inp_path) as f: + # --- Header --- + f.readline() # title line + f.readline() # compatibility line + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- Environmental Conditions --- + f.readline() # section header + dvr['Gravity'] = float_read(f.readline().split()[0]) + dvr['WtrDens'] = float_read(f.readline().split()[0]) + dvr['WtrDpth'] = float_read(f.readline().split()[0]) + dvr['MSL2SWL'] = float_read(f.readline().split()[0]) + + # --- SeaState --- + f.readline() # section header + dvr['SeaStateInputFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + dvr['WrWvKinMod'] = int_read(f.readline().split()[0]) + dvr['NSteps'] = int_read(f.readline().split()[0]) + dvr['TimeInterval'] = float_read(f.readline().split()[0]) + + # --- Waves multipoint elevation output --- + f.readline() # section header + dvr['WaveElevSeriesFlag'] = bool_read(f.readline().split()[0]) + + result: Dict[str, Any] = {'SeaStateDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so the SeaState + # output channel section survives a standalone read → write roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to SeaStateIO --- + ss_file = dvr.get('SeaStateInputFile', '') + ss_path = os.path.normpath(os.path.join(str(base_dir), ss_file)) + if ss_file and os.path.isfile(ss_path): + ss_data = self._seastate.read( + Path(ss_path), base_dir, + outlist=outlist, + read_outlist_fn=lambda f, module: _cap(f, module, freeform=True), + ) + result.update(ss_data) + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, inp_path: Path) -> None: + """Write a SeaState driver file and all referenced module files.""" + inp_path = Path(inp_path) + base_dir = inp_path.parent + dvr = data['SeaStateDriver'] + + with open(inp_path, 'w') as f: + f.write('Seastate driver file for stand-alone applications.\n') + f.write('Compatible with SeaState v1.00\n') + f.write('{!s:<17} {:<19} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo the input file data (flag)')) + f.write('---------------------- ENVIRONMENTAL CONDITIONS -------------------------------\n') + f.write('{:<17} {:<19} {:}\n'.format(dvr['Gravity'], 'Gravity', '- Gravity (m/s^2)')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['WtrDens'], 'WtrDens', '- Water density (kg/m^3)')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['WtrDpth'], 'WtrDpth', '- Water depth (m)')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['MSL2SWL'], 'MSL2SWL', '- Offset between still-water level and mean sea level (m) [positive upward]')) + f.write('---------------------- SEASTATE -----------------------------------------------\n') + + ss_name = dvr.get('SeaStateInputFile', 'SeaState.dat') + f.write('{:<17} {:<19} {:}\n'.format('"' + ss_name + '"', 'SeaStateInputFile', '- Primary SeaState input file name (quoted string)')) + f.write('{:<17} {:<19} {:}\n'.format('"' + dvr.get('OutRootName', './seastate') + '"', 'OutRootName', '- The name which prefixes all SeaState generated files (quoted string)')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['WrWvKinMod'], 'WrWvKinMod', '- Write Wave Kinematics? [0: none, 1: (0,0) elevations, 2: complete]')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['NSteps'], 'NSteps', '- Number of time steps in the simulations (-)')) + f.write('{:<17} {:<19} {:}\n'.format(dvr['TimeInterval'], 'TimeInterval', '- TimeInterval for the simulation (sec)')) + f.write('---------------------- Waves multipoint elevation output ----------------------\n') + f.write('{!s:<17} {:<19} {:}\n'.format(dvr['WaveElevSeriesFlag'], 'WaveElevSeriesFlag', '- T/F flag to output the wave elevation field (for movies)')) + f.write('END of driver input file\n') + + # --- Delegate to SeaStateIO --- + if 'SeaState' in data: + ss_path = os.path.normpath(os.path.join(str(base_dir), ss_name)) + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + self._seastate.write({'SeaState': data['SeaState']}, ss_path, str(base_dir), outlist=outlist) diff --git a/openfast_io/openfast_io/drivers/simple_elastodyn_driver.py b/openfast_io/openfast_io/drivers/simple_elastodyn_driver.py new file mode 100644 index 0000000000..76a3e68f00 --- /dev/null +++ b/openfast_io/openfast_io/drivers/simple_elastodyn_driver.py @@ -0,0 +1,129 @@ +"""SimpleElastoDyn standalone driver — reads/writes SED driver input files (.dvr). + +Very similar to AeroDisk driver format: right-aligned values and +``@filename.csv`` syntax for timeseries data. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Dict + +from ..io.simple_elastodyn import SimpleElastoDynIO +from ..parsing import bool_read, float_read, int_read, quoted_read + + +class SimpleElastoDynStandaloneDriver: + """Reads and writes SimpleElastoDyn standalone driver input files (.dvr).""" + + def __init__(self) -> None: + self._sed = SimpleElastoDynIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, dvr_path: Path) -> dict: + """Read a SimpleElastoDyn driver file and the referenced SED input. + + Returns a dict with keys: + ``'SimpleElastoDynDriver'`` — driver-level parameters + ``'SimpleElastoDyn'`` — from SimpleElastoDynIO + """ + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr: Dict[str, Any] = {} + + with open(dvr_path) as f: + # --- Header --- + f.readline() # title line + dvr['description'] = f.readline().rstrip() + + # --- Echo --- + f.readline() # separator + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- Input files --- + f.readline() # separator + dvr['SEDiptFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + + # --- Output --- + f.readline() # section header + dvr['WrVTK'] = int_read(f.readline().split()[0]) + + # --- Case Analysis --- + f.readline() # section header + dvr['TStart'] = float_read(f.readline().split()[0]) + ln = f.readline().split() + dvr['DT'] = ln[0].strip('"') # may be "DEFAULT" + ln = f.readline().split() + dvr['NumTimeSteps'] = ln[0].strip('"') # may be "DEFAULT" or int + + # Column headers and units + f.readline() # column headers + f.readline() # column units + + # Timeseries: either @filename or inline data + line = f.readline().strip() + if line.startswith('@'): + dvr['TimeseriesFile'] = line[1:] + dvr['TimeseriesData'] = [] + else: + dvr['TimeseriesFile'] = '' + rows = [] + while line and not line.upper().startswith('END'): + parts = line.split() + if parts: + rows.append([float_read(p) for p in parts]) + line = f.readline().strip() + dvr['TimeseriesData'] = rows + + result: Dict[str, Any] = {'SimpleElastoDynDriver': dvr} + + # --- Delegate to SimpleElastoDynIO --- + sed_file = dvr.get('SEDiptFile', '') + sed_path = os.path.normpath(os.path.join(str(base_dir), sed_file)) + if sed_file and os.path.isfile(sed_path): + sed_data = self._sed.read(Path(sed_path), base_dir) + result.update(sed_data) + + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, dvr_path: Path) -> None: + """Write a SimpleElastoDyn driver file and the referenced SED input.""" + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr = data['SimpleElastoDynDriver'] + + with open(dvr_path, 'w') as f: + f.write('------- SED driver INPUT FILE --------------------------------------------------------------------------------\n') + f.write(dvr.get('description', 'Generated by OpenFAST_IO') + '\n') + f.write('--------------------------------------------------------------------------------------------------------------\n') + f.write('{!s:<35} {:<15} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo input data to .ech (flag)')) + f.write('--------------------------------------------------------------------------------------------------------------\n') + + sed_name = dvr.get('SEDiptFile', 'sed_primary.inp') + f.write('{:<35} {:<15} {:}\n'.format('"' + sed_name + '"', 'SEDiptFile', '- SED input file')) + f.write('{:<35} {:<15} {:}\n'.format('"' + dvr.get('OutRootName', 'sed_driver') + '"', 'OutRootName', '- RootName for output files')) + f.write('----- Output ------------------------------------------------------------------------------------------------\n') + f.write('{:<35} {:<15} {:}\n'.format(dvr.get('WrVTK', 0), 'WrVTK', '- Option for writing VTK visualization files [0: none, 1: init only, 2: animation]')) + f.write('----- Case Analysis ------------------------------------------------------------------------------------------\n') + f.write('{:<35} {:<15} {:}\n'.format(dvr['TStart'], 'TStart', '- Start time (currently unused)')) + f.write('{:<35} {:<15} {:}\n'.format('"' + str(dvr['DT']) + '"', 'DT', '- Time step (DEFAULT to use smallest DT from input series)')) + f.write('{:<35} {:<15} {:}\n'.format(str(dvr['NumTimeSteps']), 'NumTimeSteps', '- Number of timesteps (DEFAULT to run to end of time)')) + f.write('Time AerTrq HSSBrTrqC GenTrq BlPitchCom Yaw YawRate\n') + f.write('(s) (N-m) (N-m) (N-m) (deg) (deg) (deg/s)\n') + ts_file = dvr.get('TimeseriesFile', '') + if ts_file: + f.write('@{}\n'.format(ts_file)) + else: + for row in dvr.get('TimeseriesData', []): + f.write(' '.join(str(v) for v in row) + '\n') + + # --- Delegate to SimpleElastoDynIO --- + if 'SimpleElastoDyn' in data: + sed_path = os.path.normpath(os.path.join(str(base_dir), sed_name)) + self._sed.write({'SimpleElastoDyn': data['SimpleElastoDyn']}, sed_path, str(base_dir)) diff --git a/openfast_io/openfast_io/drivers/subdyn_driver.py b/openfast_io/openfast_io/drivers/subdyn_driver.py new file mode 100644 index 0000000000..3325ba6b4d --- /dev/null +++ b/openfast_io/openfast_io/drivers/subdyn_driver.py @@ -0,0 +1,175 @@ +"""SubDyn standalone driver — reads/writes SubDyn driver input files (.dvr). + +Handles environmental conditions, SubDyn input file reference, TP reference +points, input modes, steady-state 6-DOF vectors, and applied loads table. +""" +from __future__ import annotations + +import copy +import os +from pathlib import Path +from typing import Any, Dict, List + +from ..io.subdyn import SubDynIO +from ..outlist import capture_outlist +from ..parsing import bool_read, float_read, int_read, quoted_read + +try: + from ..FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +class SubDynStandaloneDriver: + """Reads and writes SubDyn standalone driver input files (.dvr).""" + + def __init__(self) -> None: + self._subdyn = SubDynIO() + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, dvr_path: Path) -> dict: + """Read a SubDyn driver file and the referenced SubDyn input. + + Returns a dict with keys: + ``'SubDynDriver'`` — driver-level parameters + ``'SubDyn'`` — from SubDynIO + """ + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr: Dict[str, Any] = {} + + with open(dvr_path) as f: + # --- Header --- + f.readline() # title line + f.readline() # compatibility line + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- Environmental Conditions --- + f.readline() # section header + dvr['Gravity'] = float_read(f.readline().split()[0]) + dvr['WtrDpth'] = float_read(f.readline().split()[0]) + + # --- SubDyn --- + f.readline() # section header + dvr['SDInputFile'] = quoted_read(f.readline().split()[0]) + dvr['OutRootName'] = quoted_read(f.readline().split()[0]) + dvr['NSteps'] = int_read(f.readline().split()[0]) + dvr['TimeInterval'] = float_read(f.readline().split()[0]) + dvr['NTPs'] = int_read(f.readline().split()[0]) + dvr['TP_RefPoint_X'] = float_read(f.readline().split()[0]) + dvr['TP_RefPoint_Y'] = float_read(f.readline().split()[0]) + dvr['TP_RefPoint_Z'] = float_read(f.readline().split()[0]) + dvr['SubRotateZ'] = float_read(f.readline().split()[0]) + + # --- Inputs --- + f.readline() # section header + dvr['InputsMod'] = int_read(f.readline().split()[0]) + dvr['InputsFile'] = quoted_read(f.readline().split()[0]) + + # --- Steady Inputs --- + f.readline() # section header + dvr['uTPInSteady'] = _read_6dof_vec(f.readline()) + dvr['uDotTPInSteady'] = _read_6dof_vec(f.readline()) + dvr['uDotDotTPInSteady'] = _read_6dof_vec(f.readline()) + + # --- Loads --- + f.readline() # section header + dvr['nAppliedLoads'] = int_read(f.readline().split()[0]) + f.readline() # column headers + f.readline() # column units + loads: List[Dict[str, Any]] = [] + for _ in range(dvr['nAppliedLoads']): + ln = f.readline().split() + loads.append({ + 'ALJointID': int_read(ln[0]), + 'Fx': float_read(ln[1]), + 'Fy': float_read(ln[2]), + 'Fz': float_read(ln[3]), + 'Mx': float_read(ln[4]), + 'My': float_read(ln[5]), + 'Mz': float_read(ln[6]), + 'UnsteadyFile': ln[7] if len(ln) > 7 else '', + }) + dvr['AppliedLoads'] = loads + + result: Dict[str, Any] = {'SubDynDriver': dvr} + + # Shared OutList registry (mirrors OpenFASTDriver.read) so the SubDyn + # SSOutList section survives a standalone read → write roundtrip. + outlist: Dict[str, Any] = copy.deepcopy(FstOutput) if FstOutput else {} + + def _cap(f, module, freeform=False): + return capture_outlist(f, outlist, module, freeform=freeform) + + # --- Delegate to SubDynIO --- + sd_file = dvr.get('SDInputFile', '') + sd_path = os.path.normpath(os.path.join(str(base_dir), sd_file)) + if sd_file and os.path.isfile(sd_path): + sd_data = self._subdyn.read( + Path(sd_path), base_dir, + outlist=outlist, + read_outlist_fn=lambda f, module: _cap(f, module, freeform=True), + ) + result.update(sd_data) + + result['outlist'] = outlist + return result + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, dvr_path: Path) -> None: + """Write a SubDyn driver file and the referenced SubDyn input.""" + dvr_path = Path(dvr_path) + base_dir = dvr_path.parent + dvr = data['SubDynDriver'] + + with open(dvr_path, 'w') as f: + f.write('SubDyn Driver file for stand-alone applications\n') + f.write('Compatible with SubDyn v1.xx.x\n') + f.write('{!s:<19} {:<15} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo the input file data (flag).')) + f.write('---------------------- ENVIRONMENTAL CONDITIONS -------------------------------------------------\n') + f.write('{:<19} {:<15} {:}\n'.format(dvr['Gravity'], 'Gravity', '- Gravity (m/s^2).')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['WtrDpth'], 'WtrDpth', '- Water Depth (m) positive value.')) + f.write('---------------------- SubDyn -------------------------------------------------------------------\n') + + sd_name = dvr.get('SDInputFile', 'SubDyn.dat') + f.write('{:<19} {:<15} {:}\n'.format('"' + sd_name + '"', 'SDInputFile', '- SubDyn input file.')) + f.write('{:<19} {:<15} {:}\n'.format('"' + dvr.get('OutRootName', 'SubDyn') + '"', 'OutRootName', '- All the output files will have this name.')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['NSteps'], 'NSteps', '- Number of time steps in the simulations (-).')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['TimeInterval'], 'TimeInterval', '- TimeInterval for the simulation (sec).')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['NTPs'], 'NTPs', '- Number of transition pieces')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['TP_RefPoint_X'], 'TP_RefPoint_X', '- X location of the TP reference points in global coordinates (m)')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['TP_RefPoint_Y'], 'TP_RefPoint_Y', '- Y location of the TP reference points in global coordinates (m)')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['TP_RefPoint_Z'], 'TP_RefPoint_Z', '- Z location of the TP reference points in global coordinates (m)')) + f.write('{:<19} {:<15} {:}\n'.format(dvr['SubRotateZ'], 'SubRotateZ', '- Rotation angle of the structure geometry in [deg] about the global Z axis.')) + f.write('---------------------- INPUTS -------------------------------------------------------------------\n') + f.write('{:<19} {:<15} {:}\n'.format(dvr['InputsMod'], 'InputsMod', '- Inputs model {0: zero, 1: steady state, 2: from file} (switch)')) + f.write('{:<19} {:<15} {:}\n'.format('"' + dvr.get('InputsFile', '') + '"', 'InputsFile', '- Name of the inputs file if InputsMod = 2.')) + f.write('---------------------- STEADY INPUTS (for InputsMod = 1) ----------------------------------------\n') + f.write('{} {}\n'.format(' '.join('{}'.format(v) for v in dvr['uTPInSteady']), 'uTPInSteady - input displacements and rotations ([m], [rad])')) + f.write('{} {}\n'.format(' '.join('{}'.format(v) for v in dvr['uDotTPInSteady']), 'uDotTPInSteady - input translational and rotational velocities ([m/s], [rad/s])')) + f.write('{} {}\n'.format(' '.join('{}'.format(v) for v in dvr['uDotDotTPInSteady']), 'uDotTPInSteady - input translational and rotational accelerations([m/s^2], [rad/s^2])')) + f.write('---------------------- LOADS --------------------------------------------------------------------\n') + f.write('{:<5} {:<15} {:}\n'.format(dvr['nAppliedLoads'], 'nAppliedLoads', '- Number of applied loads at given nodes')) + f.write('ALJointID Fx Fy Fz Mx My Mz UnsteadyFile\n') + f.write(' (-) (N) (N) (N) (Nm) (Nm) (Nm) (-)\n') + for load in dvr.get('AppliedLoads', []): + f.write('{:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8} {}\n'.format( + load['ALJointID'], load['Fx'], load['Fy'], load['Fz'], + load['Mx'], load['My'], load['Mz'], load.get('UnsteadyFile', ''))) + f.write('END of driver input file\n') + + # --- Delegate to SubDynIO --- + if 'SubDyn' in data: + sd_path = os.path.normpath(os.path.join(str(base_dir), sd_name)) + outlist = data.get('outlist') or (copy.deepcopy(FstOutput) if FstOutput else {}) + self._subdyn.write({'SubDyn': data['SubDyn']}, sd_path, str(base_dir), outlist=outlist) + + +def _read_6dof_vec(line: str) -> list: + """Parse a 6-value space-separated vector from a line like '0.5 0.0 0.0 0.0 0.0 0.0 name'.""" + parts = line.split() + return [float_read(parts[i]) for i in range(min(6, len(parts)))] diff --git a/openfast_io/openfast_io/drivers/unsteadyaero_driver.py b/openfast_io/openfast_io/drivers/unsteadyaero_driver.py new file mode 100644 index 0000000000..db06265ec5 --- /dev/null +++ b/openfast_io/openfast_io/drivers/unsteadyaero_driver.py @@ -0,0 +1,228 @@ +"""UnsteadyAero standalone driver — reads/writes UA driver input files (.dvr). + +The most complex standalone driver format. Supports three simulation modes: + 1 = reduced frequency + 2 = prescribed-aero time series + 3 = elastic cross section (with 3x3 matrices) +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, List + +from ..parsing import bool_read, float_read, fmt_field, int_read, quoted_read + + +def _fw(val, width: int = 28) -> str: + """Format a value field with guaranteed 2-space separator.""" + return fmt_field(val, min_width=width) + + +class UnsteadyAeroStandaloneDriver: + """Reads and writes UnsteadyAero standalone driver input files (.dvr).""" + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read(self, dvr_path: Path) -> dict: + """Read an UnsteadyAero driver file. + + No sub-module delegation — the airfoil table is referenced by filename + but is a simple polar table, not a full ModuleIO file. + + Returns a dict with key ``'UnsteadyAeroDriver'``. + """ + dvr_path = Path(dvr_path) + dvr: Dict[str, Any] = {} + + with open(dvr_path) as f: + # --- Header --- + f.readline() # title line + f.readline() # separator / comment + dvr['Echo'] = bool_read(f.readline().split()[0]) + + # --- Environmental Conditions --- + f.readline() # section header + dvr['FldDens'] = float_read(f.readline().split()[0]) + dvr['KinVisc'] = float_read(f.readline().split()[0]) + dvr['SpdSound'] = float_read(f.readline().split()[0]) + + # --- UnsteadyAero --- + f.readline() # section header + dvr['UAMod'] = int_read(f.readline().split()[0]) + dvr['FLookup'] = bool_read(f.readline().split()[0]) + + # --- Airfoil Properties --- + f.readline() # section header + dvr['AirFoil'] = quoted_read(f.readline().split()[0]) + dvr['Chord'] = float_read(f.readline().split()[0]) + dvr['Vec_AQ'] = _read_csv_vec(f.readline()) + dvr['Vec_AT'] = _read_csv_vec(f.readline()) + dvr['UseCm'] = bool_read(f.readline().split()[0]) + + # --- Simulation Control --- + f.readline() # section header + dvr['SimMod'] = int_read(f.readline().split()[0]) + + # --- Reduced-Frequency Simulation (SimMod=1) --- + f.readline() # section header + dvr['InflowVel'] = float_read(f.readline().split()[0]) + dvr['NCycles'] = int_read(f.readline().split()[0]) + dvr['StepsPerCycle'] = int_read(f.readline().split()[0]) + dvr['Frequency'] = float_read(f.readline().split()[0]) + dvr['Amplitude'] = float_read(f.readline().split()[0]) + dvr['Mean'] = float_read(f.readline().split()[0]) + dvr['Phase'] = float_read(f.readline().split()[0]) + + # --- Prescribed-Aero Simulation (SimMod=2) --- + f.readline() # section header + dvr['TMax_PA'] = float_read(f.readline().split()[0]) + dvr['DT_PA'] = float_read(f.readline().split()[0]) + dvr['AeroTSFile'] = quoted_read(f.readline().split()[0]) + + # --- Aero-Elastic Simulation (SimMod=3) --- + f.readline() # section header + dvr['TMax'] = float_read(f.readline().split()[0]) + dvr['DT'] = float_read(f.readline().split()[0]) + dvr['ActiveDOF'] = _read_csv_bools(f.readline()) + dvr['InitPos'] = _read_csv_vec(f.readline()) + dvr['InitVel'] = _read_csv_vec(f.readline()) + + # 3x3 matrices: GFScaling, Mass, Damp, Stif + dvr['GFScaling'] = _read_3x3(f) + dvr['MassMatrix'] = _read_3x3(f) + dvr['DampMatrix'] = _read_3x3(f) + dvr['StifMatrix'] = _read_3x3(f) + + dvr['Twist'] = float_read(f.readline().split()[0]) + dvr['InflowMod'] = int_read(f.readline().split()[0]) + dvr['Inflow'] = _read_csv_vec(f.readline()) + dvr['InflowTSFile'] = quoted_read(f.readline().split()[0]) + dvr['MotionMod'] = int_read(f.readline().split()[0]) + dvr['MotionTSFile'] = quoted_read(f.readline().split()[0]) + + # --- Output Control --- + f.readline() # section header + dvr['SumPrint'] = bool_read(f.readline().split()[0]) + dvr['WrAFITables'] = bool_read(f.readline().split()[0]) + + return {'UnsteadyAeroDriver': dvr} + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write(self, data: dict, dvr_path: Path) -> None: + """Write an UnsteadyAero driver file. + + No sub-module delegation — all data lives in 'UnsteadyAeroDriver'. + """ + dvr_path = Path(dvr_path) + dvr = data['UnsteadyAeroDriver'] + + with open(dvr_path, 'w') as f: + f.write('----------- Unsteady Aerodynamics standalone driver ---------------------------\n') + f.write('-------------------------------------------------------------------------------\n') + f.write('{!s:<14} {:<20} {:}\n'.format(dvr['Echo'], 'Echo', '- Echo the input file data (flag)')) + f.write('---------------------- ENVIRONMENTAL CONDITIONS -------------------------------\n') + f.write('{:<13} {:<20} {:}\n'.format(dvr['FldDens'], 'FldDens', '- Density of working fluid (kg/m^3)')) + f.write('{:<13} {:<20} {:}\n'.format(dvr['KinVisc'], 'KinVisc', '- Kinematic viscosity of working fluid (m^2/s)')) + f.write('{:<13} {:<20} {:}\n'.format(dvr['SpdSound'], 'SpdSound', '- Speed of sound of working fluid (m/s)')) + f.write('---------------------- UNSTEADYAERO -------------------------------------------\n') + f.write('{:<13} {:<20} {:}\n'.format(dvr['UAMod'], 'UAMod', '- Unsteady Aero Model Switch (switch)')) + f.write('{!s:<13} {:<20} {:}\n'.format(dvr['FLookup'], 'FLookup', '- Flag for f\' lookup table (flag)')) + f.write('------------------- AIRFOIL PROPERTIES ----------------------------------------\n') + f.write(_fw('"' + dvr['AirFoil'] + '"') + '{:<22} {:}\n'.format('AirFoil', '- Airfoil table file')) + f.write(_fw(dvr['Chord']) + '{:<22} {:}\n'.format('Chord', '- Chord length (m)')) + f.write(_fw(', '.join(str(v) for v in dvr['Vec_AQ'])) + '{:<22} {:}\n'.format('Vec_AQ', '- Vector from reference point A to aerodynamic center Q')) + f.write(_fw(', '.join(str(v) for v in dvr['Vec_AT'])) + '{:<22} {:}\n'.format('Vec_AT', '- Vector from reference point A to three-quarter chord point T')) + f.write('{!s:<14} {:<22} {:}\n'.format(dvr['UseCm'], 'UseCm', '- Use Cm (moment coefficient) data (flag)')) + f.write('------------------- SIMULATION CONTROL ----------------------------------------\n') + f.write(_fw(dvr['SimMod']) + '{:<22} {:}\n'.format('SimMod', '- Simulation model {1=reduced frequency, 2=prescribed-aero, 3=elastic}')) + f.write('---------- REDUCED-FREQUENCY SIMULATION [used only when SimMod=1] -------------\n') + f.write(_fw(dvr['InflowVel']) + '{:<22} {:}\n'.format('InflowVel', '- Inflow velocity (m/s)')) + f.write(_fw(dvr['NCycles']) + '{:<22} {:}\n'.format('NCycles', '- Number of angle-of-attack oscillations (-)')) + f.write(_fw(dvr['StepsPerCycle']) + '{:<22} {:}\n'.format('StepsPerCycle', '- Number of timesteps per cycle (-)')) + f.write(_fw(dvr['Frequency']) + '{:<22} {:}\n'.format('Frequency', '- Frequency for the airfoil oscillations (Hz)')) + f.write(_fw(dvr['Amplitude']) + '{:<22} {:}\n'.format('Amplitude', '- Amplitude of the angle of attack oscillations (deg)')) + f.write(_fw(dvr['Mean']) + '{:<22} {:}\n'.format('Mean', '- Cycle mean (deg)')) + f.write(_fw(dvr['Phase']) + '{:<22} {:}\n'.format('Phase', '- Initial phase (num steps).')) + f.write('---------- PRESCRIBED-AERO INPUTS SIMULATION [used only when SimMod=2] --------\n') + f.write(_fw(dvr['TMax_PA']) + '{:<22} {:}\n'.format('TMax_PA', '- Total run time (s)')) + f.write(_fw(dvr['DT_PA']) + '{:<22} {:}\n'.format('DT_PA', '- Recommended module time step (s)')) + f.write(_fw('"' + dvr['AeroTSFile'] + '"') + '{:<22} {:}\n'.format('AeroTSFile', '- Time series data input file')) + f.write('---------- AERO-ELASTIC SIMULATION [used only when SimMod=3] ------------------\n') + f.write(_fw(dvr['TMax']) + '{:<22} {:}\n'.format('TMax', '- Total run time (s)')) + f.write(_fw(dvr['DT']) + '{:<22} {:}\n'.format('DT', '- Recommended module time step (s)')) + f.write(_fw(', '.join('T' if v else 'F' for v in dvr['ActiveDOF'])) + '{:<22} {:}\n'.format('ActiveDOF', '- List of active degrees of freedom (true or false)')) + f.write(_fw(', '.join(str(v) for v in dvr['InitPos'])) + '{:<22} {:}\n'.format('InitPos', '- List of initial positions for elastic DOFs')) + f.write(_fw(', '.join(str(v) for v in dvr['InitVel'])) + '{:<22} {:}\n'.format('InitVel', '- List of initial velocities for elastic DOFs')) + for i, row in enumerate(dvr['GFScaling']): + f.write(_fw(', '.join(str(v) for v in row)) + '{:<22} {:}\n'.format('GFScalingL{}'.format(i+1), '- Generalized force scaling factors')) + for i, row in enumerate(dvr['MassMatrix']): + f.write(_fw(', '.join(str(v) for v in row)) + '{:<22} {:}\n'.format('MassMatrixL{}'.format(i+1), '- Mass matrix')) + for i, row in enumerate(dvr['DampMatrix']): + f.write(_fw(', '.join(str(v) for v in row)) + '{:<22} {:}\n'.format('DampMatrixL{}'.format(i+1), '- Damping matrix')) + for i, row in enumerate(dvr['StifMatrix']): + f.write(_fw(', '.join(str(v) for v in row)) + '{:<22} {:}\n'.format('StifMatrixL{}'.format(i+1), '- Stiffness matrix')) + f.write(_fw(dvr['Twist']) + '{:<22} {:}\n'.format('Twist', '- Fixed twist of the section (deg)')) + f.write(_fw(dvr['InflowMod']) + '{:<22} {:}\n'.format('InflowMod', '- Model for the inflow velocity')) + f.write(_fw(', '.join(str(v) for v in dvr['Inflow'])) + '{:<22} {:}\n'.format('Inflow', '- Inflow velocity in x and y direction')) + f.write(_fw('"' + dvr['InflowTSFile'] + '"') + '{:<22} {:}\n'.format('InflowTSFile', '- Input file for inflow velocity time series')) + f.write(_fw(dvr['MotionMod']) + '{:<22} {:}\n'.format('MotionMod', '- Model for the motion of the degrees of freedom')) + f.write(_fw('"' + dvr['MotionTSFile'] + '"') + '{:<22} {:}\n'.format('MotionTSFile', '- Input file for prescribed motion')) + f.write('------------------- OUTPUT CONTROL --------------------------------------------\n') + f.write('{!s:<14} {:<22} {:}\n'.format(dvr['SumPrint'], 'SumPrint', '- Write unsteady aerodynamics summary file (flag)')) + f.write('{!s:<14} {:<22} {:}\n'.format(dvr['WrAFITables'], 'WrAFITables', '- Write back the aerodynamic coefficients used internally (flag)')) + f.write('END of driver input file\n') + + +def _read_csv_vec(line: str) -> list: + """Parse comma-separated numeric values from the start of a line.""" + raw = line.split()[0] + # Handle case where values span multiple tokens (e.g. "1, 10") + # by re-joining and splitting on comma + tokens = line.strip().split() + # Collect numeric comma-separated tokens until we hit a non-numeric word + numeric_part = [] + for t in tokens: + clean = t.rstrip(',').lstrip(',') + try: + float(clean) + numeric_part.append(t) + except ValueError: + break + joined = ' '.join(numeric_part).replace(',', ' ') + return [float_read(p) for p in joined.split() if p] + + +def _read_csv_bools(line: str) -> list: + """Parse comma-separated bool values like 'T, T, T ActiveDOF ...'.""" + tokens = line.strip().split() + bools = [] + for t in tokens: + clean = t.rstrip(',').lstrip(',') + if clean.upper() in ('T', 'TRUE'): + bools.append(True) + elif clean.upper() in ('F', 'FALSE'): + bools.append(False) + else: + break + return bools + + +def _read_3x3(f) -> List[List[float]]: + """Read 3 rows of 3 comma-separated floats each.""" + mat: List[List[float]] = [] + for _ in range(3): + line = f.readline() + tokens = line.strip().split() + numeric_part = [] + for t in tokens: + clean = t.rstrip(',').lstrip(',') + try: + float(clean) + numeric_part.append(clean) + except ValueError: + break + mat.append([float_read(p) for p in numeric_part]) + return mat diff --git a/openfast_io/openfast_io/facade.py b/openfast_io/openfast_io/facade.py new file mode 100644 index 0000000000..0a1c8e6b26 --- /dev/null +++ b/openfast_io/openfast_io/facade.py @@ -0,0 +1,11 @@ +""" +Facade aliases -- re-exports from the canonical locations. + +``InputReader_Facade`` and ``InputWriter_Facade`` are convenience aliases +that point to the same classes now living in ``FAST_reader.py`` and +``FAST_writer.py`` (which themselves are thin facades over the new IO layer). +""" +from openfast_io.FAST_reader import InputReader_OpenFAST as InputReader_Facade # noqa: F401 +from openfast_io.FAST_writer import InputWriter_OpenFAST as InputWriter_Facade # noqa: F401 + +__all__ = ["InputReader_Facade", "InputWriter_Facade"] diff --git a/openfast_io/openfast_io/formats.py b/openfast_io/openfast_io/formats.py new file mode 100644 index 0000000000..f95bb05b3a --- /dev/null +++ b/openfast_io/openfast_io/formats.py @@ -0,0 +1,32 @@ +"""JSON and YAML serialization helpers for fst_vt dicts.""" +import json +import yaml +from .FileTools import remove_numpy + + +def fst_vt_to_json(fst_vt: dict, indent: int = 2) -> str: + """Serialize fst_vt to JSON. Numpy types are converted to Python natives.""" + return json.dumps(remove_numpy(fst_vt), indent=indent, default=str) + + +def fst_vt_from_json(json_str: str) -> dict: + """Deserialize fst_vt from JSON. + + Note: numpy arrays become plain Python lists. InputWriter_OpenFAST + handles both — it tests for list/array equivalence, not exact type. + """ + return json.loads(json_str) + + +def fst_vt_to_yaml(fst_vt: dict) -> str: + """Serialize fst_vt to YAML string.""" + return yaml.dump(remove_numpy(fst_vt), default_flow_style=False, sort_keys=False) + + +def fst_vt_from_yaml(yaml_str_or_path: str) -> dict: + """Deserialize fst_vt from YAML string or file path.""" + try: + with open(yaml_str_or_path) as f: + return yaml.safe_load(f) + except (OSError, TypeError): + return yaml.safe_load(yaml_str_or_path) diff --git a/openfast_io/openfast_io/io/aerodisk.py b/openfast_io/openfast_io/io/aerodisk.py new file mode 100644 index 0000000000..93551dcb0b --- /dev/null +++ b/openfast_io/openfast_io/io/aerodisk.py @@ -0,0 +1,182 @@ +""" +AeroDiskIO – read / write AeroDisk input files. + +Produces ``{'AeroDisk': ad}`` + +AeroDisk references an external CSV file containing CpCtCq tables. +""" +from __future__ import annotations + +import os +from typing import Any, Callable, Dict, Optional + +from .base import ModuleIO +from ..outlist import emit_outlist +from ..parsing import bool_read, float_read, quoted_read + +try: + from openfast_io.FAST_output_reader import load_ascii_output +except ImportError: + load_ascii_output = None + + +class AeroDiskIO(ModuleIO): + """Read / write AeroDisk input files.""" + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + ad: Dict[str, Any] = {} + ad_dir = os.path.dirname(file_path) or '.' + + f = open(file_path) + f.readline() + f.readline() + f.readline() + + # Simulation Control + ad['Echo'] = bool_read(f.readline().split()[0]) + ad['DT'] = float_read(f.readline().split()[0]) + + # Environmental Conditions + f.readline() + ad['AirDens'] = float_read(f.readline().split()[0]) + + # Actuator Disk Properties + f.readline() + ad['RotorRad'] = float_read(f.readline().split()[0]) + + # InColNames + ad['InColNames'] = [x.strip() for x in quoted_read(f.readline().split('InColNames')[0]).split(',')] + + # InColDims + ad['InColDims'] = [int(x) for x in f.readline().split('InColDims')[0].split(',')] + + # CSV file reference (starts with '@') + line = f.readline() + if line.strip().startswith('@'): + csv_rel = line.strip()[1:].strip() + ad['actuatorDiskFile'] = os.path.join(ad_dir, csv_rel) + + if load_ascii_output is not None: + data, info = load_ascii_output( + ad['actuatorDiskFile'], + headerLines=3, + descriptionLine=0, + attributeLine=1, + unitLine=2, + delimiter=',', + ) + ad['actuatorDiskTable'] = { + 'dsc': info['description'], + 'attr': info['attribute_names'], + 'units': info['attribute_units'], + 'data': data, + } + else: + ad['actuatorDiskTable'] = {} + else: + raise Exception('Expecting a file reference to the actuator disk CSV file') + + # Output + f.readline() + f.readline() + + # Read output list — route into the shared registry (mirrors legacy openfast_io) + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'AeroDisk') + else: + ad['_outlist'] = {} + data_line = f.readline() + while data_line.split().__len__() == 0: + data_line = f.readline() + while data_line.split()[0] != 'END': + if data_line.find('"') >= 0: + channels = data_line.split('"') + channel_list = channels[1].split(',') + else: + row_string = data_line.split(',') + if len(row_string) == 1: + channel_list = row_string[0].split('\n')[0] + else: + channel_list = row_string + if isinstance(channel_list, list): + for ch in channel_list: + ch = ch.strip() + if ch: + ad['_outlist'][ch] = True + else: + ch = channel_list.strip() + if ch: + ad['_outlist'][ch] = True + data_line = f.readline() + + f.close() + return {'AeroDisk': ad} + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + ad = data['AeroDisk'] + out_dir = os.path.dirname(file_path) or '.' + + # Write CSV table first + csv_name = os.path.basename(ad.get('actuatorDiskFile', 'AeroDiskProp.csv')) + csv_path = os.path.join(out_dir, csv_name) + if 'actuatorDiskTable' in ad and ad['actuatorDiskTable']: + tbl = ad['actuatorDiskTable'] + with open(csv_path, 'w') as cf: + cf.write('{}\n'.format(tbl.get('dsc', ''))) + cf.write('{}\n'.format(', '.join(str(a) for a in tbl['attr']))) + cf.write('{}\n'.format(', '.join(str(u) for u in tbl['units']))) + for row in tbl['data']: + cf.write('{}\n'.format(', '.join('{:.6f}'.format(v) for v in row))) + + with open(file_path, 'w') as f: + f.write('--- AERO DISK INPUT FILE -------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('--- SIMULATION CONTROL ---------\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['Echo'], 'Echo', '- Echo input data to ".ADsk.ech" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['DT'], 'DT', '- Integration time step (s)\n')) + f.write('--- ENVIRONMENTAL CONDITIONS ---\n') + f.write('{:<22f} {:<11} {:}'.format(ad['AirDens'], 'AirDens', '- Air density (kg/m^3) (or "default")\n')) + f.write('--- ACTUATOR DISK PROPERTIES ---\n') + f.write('{:<22f} {:<11} {:}'.format(ad['RotorRad'], 'RotorRad', '- Rotor radius (m) (or "default")\n')) + f.write('"{}" {:<11} {:}'.format( + ', '.join(ad['InColNames']), + 'InColNames', + '- Input column headers\n', + )) + f.write('{:<22} {:<11} {:}'.format( + ', '.join(str(d) for d in ad['InColDims']), + 'InColDims', + '- Number of unique values in each column\n', + )) + f.write('@{}\n'.format(csv_name)) + f.write('--- OUTPUTS --------------------\n') + f.write('{:<22} {:<11} {:}'.format('OutList', 'OutList', '- The next line(s) contains a list of output parameters.\n')) + if outlist is not None: + emit_outlist(f, outlist, 'AeroDisk') + else: + for ch in ad.get('_outlist', {}): # standalone fallback (no shared registry) + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + f.write('---------------------------------------------------------------------------------------\n') diff --git a/openfast_io/openfast_io/io/aerodyn.py b/openfast_io/openfast_io/io/aerodyn.py new file mode 100644 index 0000000000..6f505e8029 --- /dev/null +++ b/openfast_io/openfast_io/io/aerodyn.py @@ -0,0 +1,940 @@ +"""AeroDyn module IO — reads and writes AeroDyn15 input files. + +Extracted from FAST_reader.py and FAST_writer.py. Covers: + - Main AeroDyn15 input (.dat) + - AeroDyn blade files + - Airfoil polar files + - Airfoil coordinate files + - OLAF input file + +All parsing logic is identical to the original; only the data target changes +(local dict vs self.fst_vt). +""" +from __future__ import annotations + +import copy +import os +import random +import time +from pathlib import Path + +import numpy as np + +from .base import ModuleIO +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + fix_path, + readline_filterComments, +) + + +# --------------------------------------------------------------------------- +# Writer helpers (copied from FAST_writer.py to avoid coupling) +# --------------------------------------------------------------------------- + +def _float_default_out(val, trim=False): + if isinstance(val, float): + return '{:.4f}'.format(val) if trim else '{: 22f}'.format(val) + else: + return '{:}'.format(val) if trim else '{:<22}'.format(val) + + +def _get_outlist(outlist_dict, channel_list): + """Extract True channels from an outlist dict for the given modules.""" + def loop_dict(vartree, outlist_i): + for var in vartree.keys(): + if isinstance(vartree[var], dict): + loop_dict(vartree[var], outlist_i) + else: + if vartree[var]: + outlist_i.append(var) + return outlist_i + + if not channel_list: + channel_list = outlist_dict.keys() + + outlist = [] + for var in channel_list: + var = var.replace(' ', '') + outlist_i = loop_dict(outlist_dict[var], []) + if outlist_i: + outlist.append(sorted(outlist_i)) + return outlist + + +class AeroDynIO(ModuleIO): + """Reads and writes AeroDyn v15.03+ input files. + + read() returns:: + + { + 'AeroDyn': { ... main params, tower arrays, af_data, af_coord, OLAF ... }, + 'AeroDynBlade': [ {blade0}, {blade1}, {blade2} ] or {single_blade}, + } + + write() accepts data with keys 'AeroDyn' and 'AeroDynBlade', plus outlist + and Fst context. Writes the main AD file, blade files, polars, coords, OLAF. + """ + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + + def read(self, file_path: Path, base_dir: Path, *, num_blades: int = 3, + aero_file_path: str = '', outlist: dict | None = None, + read_outlist_fn=None) -> dict: + """Read AeroDyn main file and all sub-files. + + Parameters + ---------- + file_path : path to AeroDyn main input file + base_dir : root directory for resolving relative paths + num_blades : number of blades (from ElastoDyn) + aero_file_path : relative directory from base_dir where sub-files live + outlist : optional outlist dict to populate via read_outlist_fn + read_outlist_fn : callback(f, module_name) to read outlist sections + """ + ad = {} + ad_blade = [{}, {}, {}] + file_path = str(file_path) + base_dir = str(base_dir) if base_dir else '' + + f = open(file_path) + + # General Options + f.readline(); f.readline(); f.readline() + ad['Echo'] = bool_read(f.readline().split()[0]) + ad['DTAero'] = float_read(f.readline().split()[0]) + ad['Wake_Mod'] = int(f.readline().split()[0]) + ad['TwrPotent'] = int(f.readline().split()[0]) + ad['TwrShadow'] = int(f.readline().split()[0]) + ad['TwrAero'] = bool_read(f.readline().split()[0]) + ad['CavitCheck'] = bool_read(f.readline().split()[0]) + ad['NacelleDrag'] = bool_read(f.readline().split()[0]) + ad['CompAA'] = bool_read(f.readline().split()[0]) + ad['AA_InputFile'] = f.readline().split()[0] + + # Environmental Conditions + f.readline() + ad['AirDens'] = float_read(f.readline().split()[0]) + ad['KinVisc'] = float_read(f.readline().split()[0]) + ad['SpdSound'] = float_read(f.readline().split()[0]) + ad['Patm'] = float_read(f.readline().split()[0]) + ad['Pvap'] = float_read(f.readline().split()[0]) + + f.readline() + ad['BEM_Mod'] = int(f.readline().split()[0]) + + # BEM Options + f.readline() + ad['Skew_Mod'] = int_read(f.readline().split()[0]) + ad['SkewMomCorr'] = bool_read(f.readline().split()[0]) + ad['SkewRedistr_Mod'] = int_read(f.readline().split()[0]) + ad['SkewRedistrFactor'] = float_read(f.readline().split()[0]) + f.readline() + ad['TipLoss'] = bool_read(f.readline().split()[0]) + ad['HubLoss'] = bool_read(f.readline().split()[0]) + ad['TanInd'] = bool_read(f.readline().split()[0]) + ad['AIDrag'] = bool_read(f.readline().split()[0]) + ad['TIDrag'] = bool_read(f.readline().split()[0]) + ad['IndToler'] = float_read(f.readline().split()[0]) + ad['MaxIter'] = int(f.readline().split()[0]) + f.readline() + ad['SectAvg'] = bool_read(f.readline().split()[0]) + ad['SectAvgWeighting'] = int_read(f.readline().split()[0]) + ad['SectAvgNPoints'] = int_read(f.readline().split()[0]) + ad['SectAvgPsiBwd'] = float_read(f.readline().split()[0]) + ad['SectAvgPsiFwd'] = float_read(f.readline().split()[0]) + + # Dynamic BEM + f.readline() + ad['DBEMT_Mod'] = int(f.readline().split()[0]) + ad['tau1_const'] = float_read(f.readline().split()[0]) + + # OLAF + f.readline() + ad['OLAFInputFileName'] = quoted_read(f.readline().split()[0]) + + # Unsteady Airfoil Aero + f.readline() + ad['AoA34'] = bool_read(f.readline().split()[0]) + ad['UA_Mod'] = int(f.readline().split()[0]) + ad['FLookup'] = bool_read(f.readline().split()[0]) + ad['IntegrationMethod'] = int(f.readline().split()[0]) + + file_pos = f.tell() + line = f.readline() + if 'UAStartRad' in line: + ad['UAStartRad'] = float_read(line.split()[0]) + else: + f.seek(file_pos) + + file_pos = f.tell() + line = f.readline() + if 'UAEndRad' in line: + ad['UAEndRad'] = float_read(line.split()[0]) + else: + f.seek(file_pos) + + # Airfoil Information + f.readline() + ad['AFTabMod'] = int(f.readline().split()[0]) + ad['InCol_Alfa'] = int(f.readline().split()[0]) + ad['InCol_Cl'] = int(f.readline().split()[0]) + ad['InCol_Cd'] = int(f.readline().split()[0]) + ad['InCol_Cm'] = int(f.readline().split()[0]) + ad['InCol_Cpmin'] = int(f.readline().split()[0]) + ad['NumAFfiles'] = int(f.readline().split()[0]) + ad['AFNames'] = [None] * ad['NumAFfiles'] + for i in range(ad['NumAFfiles']): + af_filename = fix_path(f.readline().split()[0])[1:-1] + ad['AFNames'][i] = os.path.abspath(os.path.join(base_dir, aero_file_path, af_filename)) + + # Rotor/Blade Properties + f.readline() + ad['UseBlCm'] = bool_read(f.readline().split()[0]) + ad['ADBlFile1'] = quoted_read(f.readline().split()[0]) + ad['ADBlFile2'] = quoted_read(f.readline().split()[0]) + ad['ADBlFile3'] = quoted_read(f.readline().split()[0]) + + # Hub, nacelle, tail fin + f.readline() + ad['VolHub'] = float_read(f.readline().split()[0]) + ad['HubCenBx'] = float_read(f.readline().split()[0]) + f.readline() + ad['VolNac'] = float_read(f.readline().split()[0]) + ad['NacCenB'] = [idx.strip() for idx in f.readline().split('NacCenB')[0].split(',')] + ad['NacArea'] = [idx.strip() for idx in f.readline().split('NacArea')[0].split(',')] + ad['NacCd'] = [idx.strip() for idx in f.readline().split('NacCd')[0].split(',')] + ad['NacDragAC'] = [idx.strip() for idx in f.readline().split('NacDragAC')[0].split(',')] + f.readline() + ad['TFinAero'] = bool_read(f.readline().split()[0]) + tfa_filename = fix_path(f.readline().split()[0])[1:-1] + ad['TFinFile'] = os.path.abspath(os.path.join(base_dir, tfa_filename)) + + # Tower Influence and Aerodynamics + f.readline() + ad['NumTwrNds'] = int(f.readline().split()[0]) + f.readline(); f.readline() + for k in ('TwrElev', 'TwrDiam', 'TwrCd', 'TwrTI', 'TwrCb', 'TwrCp', 'TwrCa'): + ad[k] = [None] * ad['NumTwrNds'] + for i in range(ad['NumTwrNds']): + data = [float(val) for val in f.readline().split()] + ad['TwrElev'][i] = data[0] + ad['TwrDiam'][i] = data[1] + ad['TwrCd'][i] = data[2] + ad['TwrTI'][i] = data[3] + ad['TwrCb'][i] = data[4] + ad['TwrCp'][i] = data[5] + ad['TwrCa'][i] = data[6] + + # Outputs + f.readline() + ad['SumPrint'] = bool_read(f.readline().split()[0]) + ad['NBlOuts'] = int(f.readline().split()[0]) + ad['BlOutNd'] = [idx.strip() for idx in f.readline().split('BlOutNd')[0].split(',')] + ad['NTwOuts'] = int(f.readline().split()[0]) + ad['TwOutNd'] = [idx.strip() for idx in f.readline().split('TwOutNd')[0].split(',')] + + # AeroDyn OutList + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'AeroDyn') + else: + # skip outlist section + line = f.readline() + while line and 'END' not in line.split('!')[0].upper()[:3]: + line = f.readline() + + # Optional nodal output + try: + f.readline() + ad['BldNd_BladesOut'] = int(f.readline().split()[0]) + ad['BldNd_BlOutNd'] = f.readline().split()[0] + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'AeroDyn_Nodes') + else: + line = f.readline() + while line and 'END' not in line.split('!')[0].upper()[:3]: + line = f.readline() + except Exception: + pass + + f.close() + + # ── Read blade files ── + ad_bld_file1 = os.path.join(base_dir, aero_file_path, ad['ADBlFile1']) + ad_bld_file2 = os.path.join(base_dir, aero_file_path, ad['ADBlFile2']) + ad_bld_file3 = os.path.join(base_dir, aero_file_path, ad['ADBlFile3']) + + if ad_bld_file1 == ad_bld_file2 and ad_bld_file1 == ad_bld_file3: + self._read_blade(ad_blade, ad_bld_file1, 0) + ad_blade = ad_blade[0] + elif num_blades == 2 and ad_bld_file1 == ad_bld_file2: + self._read_blade(ad_blade, ad_bld_file1, 0) + ad_blade = ad_blade[0] + else: + self._read_blade(ad_blade, ad_bld_file1, 0) + if num_blades > 1: + self._read_blade(ad_blade, ad_bld_file2, 1) + if num_blades > 2: + self._read_blade(ad_blade, ad_bld_file3, 2) + else: + ad_blade = ad_blade[0] + + # ── Read polars ── + self._read_polars(ad) + + # ── Read coords ── + self._read_coords(ad) + + # ── Read OLAF if present ── + olaf_filename = os.path.join(base_dir, ad['OLAFInputFileName']) + if os.path.isfile(olaf_filename): + self._read_olaf(ad, olaf_filename, base_dir) + + return {'AeroDyn': ad, 'AeroDynBlade': ad_blade} + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + + def write(self, data: dict, file_path: Path, base_dir: Path, *, + naming_out: str = 'openfast', outlist: dict | None = None) -> None: + """Write AeroDyn main file and all sub-files. + + Parameters + ---------- + data : dict with 'AeroDyn', 'AeroDynBlade' keys (and optionally 'outlist') + file_path : output path for main AeroDyn .dat file + base_dir : directory where sub-files will be written + naming_out : base naming for generated files + outlist : optional outlist dict for channel output sections + """ + ad = data['AeroDyn'] + ad_blade = data['AeroDynBlade'] + run_dir = str(base_dir) + + # ── Write blade files ── + if isinstance(ad_blade, list): + for i_bld, _ in enumerate(ad_blade): + ad['ADBlFile%d' % (i_bld + 1)] = naming_out + '_AeroDyn_blade_%d.dat' % (i_bld + 1) + self._write_blade(ad, ad_blade, run_dir, bld_ind=i_bld) + elif isinstance(ad_blade, dict): + ad['ADBlFile1'] = naming_out + '_AeroDyn_blade.dat' + ad['ADBlFile2'] = ad['ADBlFile1'] + ad['ADBlFile3'] = ad['ADBlFile1'] + self._write_blade(ad, ad_blade, run_dir) + + # ── Write polars ── + self._write_polars(ad, run_dir, naming_out) + + # ── Write coords ── + if any(ad['af_data'][i][0]['NumCoords'] != '0' for i in range(len(ad['af_data']))): + af_coords = [i for i in range(len(ad['af_data'])) if ad['af_data'][i][0]['NumCoords'] != '0'] + self._write_coords(ad, af_coords, run_dir, naming_out) + + # ── Write OLAF ── + if ad['Wake_Mod'] == 3: + self._write_olaf(ad, run_dir, naming_out) + + # ── Write main AeroDyn file ── + f = open(str(file_path), 'w') + + f.write('------- AERODYN15 INPUT FILE ------------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('====== General Options ============================================================================\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['Echo'], 'Echo', '- Echo the input to ".AD.ech"? (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['DTAero'], 'DTAero', '- Time interval for aerodynamic calculations {or "default"} (s)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['Wake_Mod'], 'Wake_Mod', '- Wake/induction model (switch) {0=none, 1=BEMT, 3=OLAF} [Wake_Mod cannot be 2 or 3 when linearizing]\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['TwrPotent'], 'TwrPotent', '- Type tower influence on wind based on potential flow around the tower (switch) {0=none, 1=baseline potential flow, 2=potential flow with Bak correction}\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['TwrShadow'], 'TwrShadow', '- Calculate tower influence on wind based on downstream tower shadow (switch) {0=none, 1=Powles model, 2=Eames model}\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['TwrAero'], 'TwrAero', '- Calculate tower aerodynamic loads? (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['CavitCheck'], 'CavitCheck', '- Perform cavitation check? (flag) [UA_Mod must be 0 when CavitCheck=true]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['NacelleDrag'], 'NacelleDrag', '- Include Nacelle Drag effects? (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['CompAA'], 'CompAA', '- Flag to compute AeroAcoustics calculation [used only when Wake_Mod = 1 or 2]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['AA_InputFile'], 'AA_InputFile', '- AeroAcoustics input file [used only when CompAA=true]\n')) + f.write('====== Environmental Conditions ===================================================================\n') + f.write('{:<22} {:<11} {:}'.format(ad['AirDens'], 'AirDens', '- Air density (kg/m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['KinVisc'], 'KinVisc', '- Kinematic viscosity of working fluid (m^2/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['SpdSound'], 'SpdSound', '- Speed of sound in working fluid (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['Patm'], 'Patm', '- Atmospheric pressure (Pa) [used only when CavitCheck=True]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['Pvap'], 'Pvap', '- Vapour pressure of working fluid (Pa) [used only when CavitCheck=True]\n')) + f.write('====== Blade-Element/Momentum Theory Options ====================================================== [unused when Wake_Mod=0 or 3, except for BEM_Mod]\n') + f.write('{:<22d} {:<11} {:}'.format(ad['BEM_Mod'], 'BEM_Mod', '- BEM model {1=legacy NoSweepPitchTwist, 2=polar} (switch) [used for all Wake_Mod to determine output coordinate system]\n')) + f.write('--- Skew correction\n') + f.write('{:<22d} {:<11} {:}'.format(ad['Skew_Mod'], 'Skew_Mod', '- Skew model {0=No skew model, -1=Remove non-normal component for linearization, 1=skew model active}\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SkewMomCorr'], 'SkewMomCorr', '- Turn the skew momentum correction on or off [used only when Skew_Mod=1]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SkewRedistr_Mod'], 'SkewRedistr_Mod', '- Type of skewed-wake correction model (switch) {0=no redistribution, 1=Glauert/Pitt/Peters, default=1} [used only when Skew_Mod=1]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SkewRedistrFactor'], 'SkewRedistrFactor', '- Constant used in Pitt/Peters skewed wake model {or "default" is 15/32*pi} (-) [used only when Skew_Mod=1 and SkewRedistr_Mod=1]\n')) + f.write('--- BEM algorithm\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['TipLoss'], 'TipLoss', '- Use the Prandtl tip-loss model? (flag) [unused when Wake_Mod=0 or 3]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['HubLoss'], 'HubLoss', '- Use the Prandtl hub-loss model? (flag) [unused when Wake_Mod=0 or 3]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['TanInd'], 'TanInd', '- Include tangential induction in BEMT calculations? (flag) [unused when Wake_Mod=0 or 3]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['AIDrag'], 'AIDrag', '- Include the drag term in the axial-induction calculation? (flag) [unused when Wake_Mod=0 or 3]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['TIDrag'], 'TIDrag', '- Include the drag term in the tangential-induction calculation? (flag) [unused when Wake_Mod=0,3 or TanInd=FALSE]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['IndToler'], 'IndToler', '- Convergence tolerance for BEMT nonlinear solve residual equation {or "default"} (-) [unused when Wake_Mod=0 or 3]\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['MaxIter'], 'MaxIter', '- Maximum number of iteration steps (-) [unused when Wake_Mod=0]\n')) + f.write('--- Shear correction\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['SectAvg'], 'SectAvg', '- Use sector averaging (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SectAvgWeighting'], 'SectAvgWeighting', '- Weighting function for sector average {1=Uniform, default=1} within a sector centered on the blade (switch) [used only when SectAvg=True]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SectAvgNPoints'], 'SectAvgNPoints', '- Number of points per sectors (-) {default=5} [used only when SectAvg=True]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SectAvgPsiBwd'], 'SectAvgPsiBwd', '- Backward azimuth relative to blade where the sector starts (<=0) {default=-60} (deg) [used only when SectAvg=True]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['SectAvgPsiFwd'], 'SectAvgPsiFwd', '- Forward azimuth relative to blade where the sector ends (>=0) {default=60} (deg) [used only when SectAvg=True]\n')) + f.write('--- Dynamic wake/inflow\n') + f.write('{:<22d} {:<11} {:}'.format(ad['DBEMT_Mod'], 'DBEMT_Mod', '- Type of dynamic BEMT (DBEMT) model {0=No Dynamic Wake, -1=Frozen Wake for linearization, 1:constant tau1, 2=time-dependent tau1, 3=constant tau1 with continuous formulation} (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['tau1_const'], 'tau1_const', '- Time constant for DBEMT (s) [used only when DBEMT_Mod=1 or 3]\n')) + f.write('====== OLAF -- cOnvecting LAgrangian Filaments (Free Vortex Wake) Theory Options ================== [used only when Wake_Mod=3]\n') + olaf_file = naming_out + '_OLAF.dat' + f.write('{!s:<22} {:<11} {:}'.format(olaf_file, 'OLAFInputFileName', '- Input file for OLAF [used only when Wake_Mod=3]\n')) + f.write('====== Unsteady Airfoil Aerodynamics Options ===================================== \n') + f.write('{!s:<22} {:<11} {:}'.format(ad['AoA34'], 'AoA34', "- Sample the angle of attack (AoA) at the 3/4 chord or the AC point {default=True} [always used]\n")) + f.write('{:<22d} {:<11} {:}'.format(ad['UA_Mod'], 'UA_Mod', "- Unsteady Aero Model Switch (switch) {0=Quasi-steady (no UA), 2=B-L Gonzalez, 3=B-L Minnema/Pierce, 4=B-L HGM 4-states, 5=B-L HGM+vortex 5 states, 6=Oye, 7=Boeing-Vertol}\n")) + f.write('{!s:<22} {:<11} {:}'.format(ad['FLookup'], 'FLookup', "- Flag to indicate whether a lookup for f' will be calculated (TRUE) or whether best-fit exponential equations will be used (FALSE); if FALSE S1-S4 must be provided in airfoil input files (flag) [used only when UA_Mod>0]\n")) + f.write('{!s:<22} {:<11} {:}'.format(ad['IntegrationMethod'], 'IntegrationMethod', "- Switch to indicate which integration method UA uses (1=RK4, 2=AB4, 3=ABM4, 4=BDF2)\n")) + if 'UAStartRad' in ad and 'UAEndRad' in ad: + f.write('{:<22} {:<11} {:}'.format(ad['UAStartRad'], 'UAStartRad', '- Starting radius for dynamic stall (fraction of rotor radius [0.0,1.0]) [used only when UA_Mod>0; if line is missing UAStartRad=0]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['UAEndRad'], 'UAEndRad', '- Ending radius for dynamic stall (fraction of rotor radius [0.0,1.0]) [used only when UA_Mod>0; if line is missing UAEndRad=1]\n')) + f.write('====== Airfoil Information =========================================================================\n') + f.write('{:<22d} {:<11} {:}'.format(ad['AFTabMod'], 'AFTabMod', '- Interpolation method for multiple airfoil tables {1=1D interpolation on AoA (first table only); 2=2D interpolation on AoA and Re; 3=2D interpolation on AoA and UserProp} (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['InCol_Alfa'], 'InCol_Alfa', '- The column in the airfoil tables that contains the angle of attack (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['InCol_Cl'], 'InCol_Cl', '- The column in the airfoil tables that contains the lift coefficient (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['InCol_Cd'], 'InCol_Cd', '- The column in the airfoil tables that contains the drag coefficient (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['InCol_Cm'], 'InCol_Cm', '- The column in the airfoil tables that contains the pitching-moment coefficient; use zero if there is no Cm column (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['InCol_Cpmin'], 'InCol_Cpmin', '- The column in the airfoil tables that contains the Cpmin coefficient; use zero if there is no Cpmin column (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['NumAFfiles'], 'NumAFfiles', '- Number of airfoil files used (-)\n')) + for i in range(ad['NumAFfiles']): + if i == 0: + f.write('"' + ad['AFNames'][i] + '" AFNames - Airfoil file names (NumAFfiles lines) (quoted strings)\n') + else: + f.write('"' + ad['AFNames'][i] + '"\n') + f.write('====== Rotor/Blade Properties =====================================================================\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['UseBlCm'], 'UseBlCm', '- Include aerodynamic pitching moment in calculations? (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ad['ADBlFile1'] + '"', 'ADBlFile(1)', '- Name of file containing distributed aerodynamic properties for Blade #1 (-)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ad['ADBlFile2'] + '"', 'ADBlFile(2)', '- Name of file containing distributed aerodynamic properties for Blade #2 (-) [unused if NumBl < 2]\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ad['ADBlFile3'] + '"', 'ADBlFile(3)', '- Name of file containing distributed aerodynamic properties for Blade #3 (-) [unused if NumBl < 3]\n')) + f.write('====== Hub Properties ============================================================================== [used only when MHK=1 or 2]\n') + f.write('{:<22} {:<11} {:}'.format(ad['VolHub'], 'VolHub', '- Hub volume (m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['HubCenBx'], 'HubCenBx', '- Hub center of buoyancy x direction offset (m)\n')) + f.write('====== Nacelle Properties ========================================================================== [used only when MHK=1 or 2 or when NacelleDrag=True]\n') + f.write('{:<22} {:<11} {:}'.format(ad['VolNac'], 'VolNac', '- Nacelle volume (m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ad['NacCenB'], dtype=str)), 'NacCenB', '- Position of nacelle center of buoyancy from yaw bearing in nacelle coordinates (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ad['NacArea'], dtype=str)), 'NacArea', '- Projected area of the nacelle in X, Y, Z in the nacelle coordinate system (m^2)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ad['NacCd'], dtype=str)), 'NacCd', '- Drag coefficient for the nacelle areas defined above (-)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ad['NacDragAC'], dtype=str)), 'NacDragAC', '- Position of aerodynamic center of nacelle drag in nacelle coordinates (m)\n')) + f.write('====== Tail Fin Aerodynamics ========================================================================\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['TFinAero'], 'TFinAero', '- Calculate tail fin aerodynamics model (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ad['TFinFile'] + '"', 'TFinFile', '- Input file for tail fin aerodynamics [used only when TFinAero=True]\n')) + f.write('====== Tower Influence and Aerodynamics ============================================================ [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or MHK=1 or 2]\n') + f.write('{:<22d} {:<11} {:}'.format(ad['NumTwrNds'], 'NumTwrNds', '- Number of tower nodes used in the analysis (-) [used only when TwrPotent/=0, TwrShadow/=0, TwrAero=True, or MHK=1 or 2]\n')) + f.write('TwrElev TwrDiam TwrCd TwrTI TwrCb TwrCp TwrCa !TwrTI used only with TwrShadow=2, TwrCb/TwrCp/TwrCa used only with MHK=1 or 2\n') + f.write('(m) (m) (-) (-) (-) (-) (-)\n') + for TwrElev, TwrDiam, TwrCd, TwrTI, TwrCb, TwrCp, TwrCa in zip( + ad['TwrElev'], ad['TwrDiam'], ad['TwrCd'], ad['TwrTI'], + ad['TwrCb'], ad['TwrCp'], ad['TwrCa']): + f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} \n'.format( + TwrElev, TwrDiam, TwrCd, TwrTI, TwrCb, TwrCp, TwrCa)) + f.write('====== Outputs ====================================================================================\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['SumPrint'], 'SumPrint', '- Generate a summary file listing input options and interpolated properties to ".AD.sum"? (flag)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['NBlOuts'], 'NBlOuts', '- Number of blade node outputs [0 - 9] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(ad['BlOutNd']), 'BlOutNd', '- Blade nodes whose values will be output (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['NTwOuts'], 'NTwOuts', '- Number of tower node outputs [0 - 9] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ad['TwOutNd'], dtype=str)), 'TwOutNd', '- Tower nodes whose values will be output (-)\n')) + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + + if outlist is not None: + ol = _get_outlist(outlist, ['AeroDyn']) + for channel_list in ol: + for ch in channel_list: + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + # Optional nodal output + if 'BldNd_BladesOut' in ad: + f.write('====== Outputs for all blade stations (same ending as above for B1N1.... =========================== [optional section]\n') + f.write('{:<22d} {:<11} {:}'.format(ad['BldNd_BladesOut'], 'BldNd_BladesOut', '- Number of blades to output all node information at. Up to number of blades on turbine. (-)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output. Not implemented yet. (-)\n')) + f.write(' OutList_Nodal - The next line(s) contains a list of output parameters. See OutListParameters.xlsx, AeroDyn_Nodes tab for a listing of available output channels, (-)\n') + if outlist is not None: + opt_ol = _get_outlist(outlist, ['AeroDyn_Nodes']) + for opt_channel_list in opt_ol: + for ch in opt_channel_list: + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + f.write('---------------------------------------------------------------------------------------\n') + f.flush() + os.fsync(f) + f.close() + + # ------------------------------------------------------------------ + # PRIVATE: Blade read/write + # ------------------------------------------------------------------ + + @staticmethod + def _read_blade(ad_blade_list, blade_file, blade_number): + bld = ad_blade_list[blade_number] + f = open(blade_file) + f.readline(); f.readline(); f.readline() + + bld['NumBlNds'] = int(f.readline().split()[0]) + f.readline(); f.readline() + + for k in ('BlSpn', 't_c', 'BlCrvAC', 'BlSwpAC', 'BlCrvAng', 'BlTwist', + 'BlChord', 'BlAFID', 'BlCb', 'BlCenBn', 'BlCenBt', + 'BlCpn', 'BlCpt', 'BlCan', 'BlCat', 'BlCam'): + bld[k] = [None] * bld['NumBlNds'] + + for i in range(bld['NumBlNds']): + data = [float(val) for val in f.readline().split()] + bld['BlSpn'][i] = data[0] + bld['BlCrvAC'][i] = data[1] + bld['BlSwpAC'][i] = data[2] + bld['BlCrvAng'][i] = data[3] + bld['BlTwist'][i] = data[4] + bld['BlChord'][i] = data[5] + bld['BlAFID'][i] = data[6] + if len(data) == 16: + bld['t_c'][i] = data[7] + bld['BlCb'][i] = data[8] + bld['BlCenBn'][i] = data[9] + bld['BlCenBt'][i] = data[10] + bld['BlCpn'][i] = data[11] + bld['BlCpt'][i] = data[12] + bld['BlCan'][i] = data[13] + bld['BlCat'][i] = data[14] + bld['BlCam'][i] = data[15] + else: + bld['t_c'][i] = 0.0 + bld['BlCb'][i] = 0.0 + bld['BlCenBn'][i] = 0.0 + bld['BlCenBt'][i] = 0.0 + bld['BlCpn'][i] = 0.0 + bld['BlCpt'][i] = 0.0 + bld['BlCan'][i] = 0.0 + bld['BlCat'][i] = 0.0 + bld['BlCam'][i] = 0.0 + + f.close() + + @staticmethod + def _write_blade(ad, ad_blade, run_dir, bld_ind=None): + if bld_ind is None: + filename = os.path.join(run_dir, ad['ADBlFile1']) + bld_dict = ad_blade + else: + filename = os.path.join(run_dir, ad['ADBlFile%d' % (bld_ind + 1)]) + bld_dict = ad_blade[bld_ind] + + f = open(filename, 'w') + f.write('------- AERODYN15 BLADE DEFINITION INPUT FILE -------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('====== Blade Properties =================================================================\n') + f.write('{:<11d} {:<11} {:}'.format(bld_dict['NumBlNds'], 'NumBlNds', '- Number of blade nodes used in the analysis (-)\n')) + f.write(' BlSpn BlCrvAC BlSwpAC BlCrvAng BlTwist BlChord BlAFID t_c BlCb BlCenBn BlCenBt BlCpn BlCpt BlCan BlCat BlCam\n') + f.write(' (m) (m) (m) (deg) (deg) (m) (-) (-) (-) (m) (m) (-) (-) (-) (-) (-)\n') + + for Spn, CrvAC, SwpAC, CrvAng, Twist, Chord, AFID, tc, Cb, CenBn, CenBt, Cpn, Cpt, Can, Cat, Cam in zip( + bld_dict['BlSpn'], bld_dict['BlCrvAC'], bld_dict['BlSwpAC'], + bld_dict['BlCrvAng'], bld_dict['BlTwist'], bld_dict['BlChord'], + bld_dict['BlAFID'], bld_dict['t_c'], bld_dict['BlCb'], + bld_dict['BlCenBn'], bld_dict['BlCenBt'], + bld_dict['BlCpn'], bld_dict['BlCpt'], + bld_dict['BlCan'], bld_dict['BlCat'], bld_dict['BlCam']): + f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 8d} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format( + Spn, CrvAC, SwpAC, CrvAng, Twist, Chord, int(AFID), + tc, Cb, CenBn, CenBt, Cpn, Cpt, Can, Cat, Cam)) + + f.flush() + os.fsync(f) + f.close() + + # ------------------------------------------------------------------ + # PRIVATE: Polar read/write + # ------------------------------------------------------------------ + + @staticmethod + def _read_polars(ad): + ad['af_data'] = [None] * ad['NumAFfiles'] + + for afi, af_filename in enumerate(ad['AFNames']): + f = open(af_filename) + polar = {} + + polar['InterpOrd'] = int_read(readline_filterComments(f).split()[0]) + temp = readline_filterComments(f).split() + if temp[1] == "RelThickness": + polar['RelThickness'] = float_read(temp[0]) + polar['NonDimArea'] = float_read(readline_filterComments(f).split()[0]) + else: + polar['NonDimArea'] = float_read(temp[0]) + polar['NumCoords'] = readline_filterComments(f).split()[0] + polar['BL_file'] = readline_filterComments(f).split()[0] + polar['NumTabs'] = int_read(readline_filterComments(f).split()[0]) + ad['af_data'][afi] = [None] * polar['NumTabs'] + + for tab in range(polar['NumTabs']): + polar['Re'] = float_read(readline_filterComments(f).split()[0]) * 1.e+6 + polar['UserProp'] = int_read(readline_filterComments(f).split()[0]) + polar['InclUAdata'] = bool_read(readline_filterComments(f).split()[0]) + + if polar['InclUAdata']: + polar['alpha0'] = float_read(readline_filterComments(f).split()[0]) + polar['alpha1'] = float_read(readline_filterComments(f).split()[0]) + polar['alpha2'] = float_read(readline_filterComments(f).split()[0]) + polar['eta_e'] = float_read(readline_filterComments(f).split()[0]) + polar['C_nalpha'] = float_read(readline_filterComments(f).split()[0]) + polar['T_f0'] = float_read(readline_filterComments(f).split()[0]) + polar['T_V0'] = float_read(readline_filterComments(f).split()[0]) + polar['T_p'] = float_read(readline_filterComments(f).split()[0]) + polar['T_VL'] = float_read(readline_filterComments(f).split()[0]) + polar['b1'] = float_read(readline_filterComments(f).split()[0]) + polar['b2'] = float_read(readline_filterComments(f).split()[0]) + polar['b5'] = float_read(readline_filterComments(f).split()[0]) + polar['A1'] = float_read(readline_filterComments(f).split()[0]) + polar['A2'] = float_read(readline_filterComments(f).split()[0]) + polar['A5'] = float_read(readline_filterComments(f).split()[0]) + polar['S1'] = float_read(readline_filterComments(f).split()[0]) + polar['S2'] = float_read(readline_filterComments(f).split()[0]) + polar['S3'] = float_read(readline_filterComments(f).split()[0]) + polar['S4'] = float_read(readline_filterComments(f).split()[0]) + polar['Cn1'] = float_read(readline_filterComments(f).split()[0]) + polar['Cn2'] = float_read(readline_filterComments(f).split()[0]) + polar['St_sh'] = float_read(readline_filterComments(f).split()[0]) + polar['Cd0'] = float_read(readline_filterComments(f).split()[0]) + polar['Cm0'] = float_read(readline_filterComments(f).split()[0]) + polar['k0'] = float_read(readline_filterComments(f).split()[0]) + polar['k1'] = float_read(readline_filterComments(f).split()[0]) + polar['k2'] = float_read(readline_filterComments(f).split()[0]) + polar['k3'] = float_read(readline_filterComments(f).split()[0]) + polar['k1_hat'] = float_read(readline_filterComments(f).split()[0]) + polar['x_cp_bar'] = float_read(readline_filterComments(f).split()[0]) + polar['UACutout'] = float_read(readline_filterComments(f).split()[0]) + polar['filtCutOff'] = float_read(readline_filterComments(f).split()[0]) + + polar['NumAlf'] = int_read(readline_filterComments(f).split()[0]) + polar['Alpha'] = [None] * polar['NumAlf'] + polar['Cl'] = [None] * polar['NumAlf'] + polar['Cd'] = [None] * polar['NumAlf'] + polar['Cm'] = [None] * polar['NumAlf'] + polar['Cpmin'] = [None] * polar['NumAlf'] + for i in range(polar['NumAlf']): + data = [float(val) for val in readline_filterComments(f).split()] + if ad['InCol_Alfa'] > 0: + polar['Alpha'][i] = data[ad['InCol_Alfa'] - 1] + if ad['InCol_Cl'] > 0: + polar['Cl'][i] = data[ad['InCol_Cl'] - 1] + if ad['InCol_Cd'] > 0: + polar['Cd'][i] = data[ad['InCol_Cd'] - 1] + if ad['InCol_Cm'] > 0: + polar['Cm'][i] = data[ad['InCol_Cm'] - 1] + if ad['InCol_Cpmin'] > 0: + polar['Cpmin'][i] = data[ad['InCol_Cpmin'] - 1] + + ad['af_data'][afi][tab] = copy.copy(polar) + + f.close() + + @staticmethod + def _write_polars(ad, run_dir, naming_out): + airfoils_dir = os.path.join(run_dir, 'Airfoils') + if not os.path.isdir(airfoils_dir): + try: + os.makedirs(airfoils_dir) + except Exception: + try: + time.sleep(random.random()) + if not os.path.isdir(airfoils_dir): + os.makedirs(airfoils_dir) + except Exception: + print("Error trying to make '%s'!" % airfoils_dir) + + ad['NumAFfiles'] = len(ad['af_data']) + ad['AFNames'] = [''] * ad['NumAFfiles'] + + for afi in range(int(ad['NumAFfiles'])): + ad['AFNames'][afi] = os.path.join('Airfoils', naming_out + '_AeroDyn_Polar_%02d.dat' % afi) + af_file = os.path.join(run_dir, ad['AFNames'][afi]) + f = open(af_file, 'w') + + f.write('! ------------ AirfoilInfo Input File ----------------------------------\n') + f.write('! Generated with OpenFAST_IO\n') + f.write('! line\n') + f.write('! line\n') + f.write('! ------------------------------------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ad['af_data'][afi][0]['InterpOrd'], 'InterpOrd', '! Interpolation order to use for quasi-steady table lookup {1=linear; 3=cubic spline; "default"} [default=3]\n')) + if 'RelThickness' in ad['af_data'][afi][0]: + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][0]['RelThickness'], 'RelThickness', '! The non-dimensional thickness of the airfoil (thickness/chord) [only used if UAMod=7] [default=0.2] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['af_data'][afi][0]['NonDimArea'], 'NonDimArea', '! The non-dimensional area of the airfoil (area/chord^2) (set to 1.0 if unsure or unneeded)\n')) + if ad['af_data'][afi][0]['NumCoords'] != '0': + f.write('@"{:}_AF{:02d}_Coords.txt" {:<11} {:}'.format(naming_out, afi, 'NumCoords', '! The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.\n')) + else: + f.write('{:<22d} {:<11} {:}'.format(0, 'NumCoords', '! The number of coordinates in the airfoil shape file. Set to zero if coordinates not included.\n')) + f.write('AF{:02d}_BL.txt {:<11} {:}'.format(afi, 'BL_file', '! The file name including the boundary layer characteristics of the profile. Ignored if the aeroacoustic module is not called.\n')) + + # Determine number of tabs to write + if ad['AFTabMod'] == 2: + num_tab = len(ad['af_data'][afi]) + elif ad['AFTabMod'] == 3: + if len(ad['af_data'][afi]) == 1 or \ + ad['af_data'][afi][0]['UserProp'] == ad['af_data'][afi][1]['UserProp']: + num_tab = 1 + else: + num_tab = ad['af_data'][afi][0]['NumTabs'] + else: + num_tab = 1 + + f.write('{:<22d} {:<11} {:}'.format(num_tab, 'NumTabs', '! Number of airfoil tables in this file. Each table must have lines for Re and UserProp.\n')) + + for tab in range(num_tab): + f.write('! ------------------------------------------------------------------------------\n') + f.write("! data for table %i \n" % (tab + 1)) + f.write('! ------------------------------------------------------------------------------\n') + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['Re'] * 1.e-6, 'Re', '! Reynolds number in millions\n')) + f.write('{:<22d} {:<11} {:}'.format(int(ad['af_data'][afi][tab]['UserProp']), 'UserProp', '! User property (control) setting\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['af_data'][afi][tab]['InclUAdata'], 'InclUAdata', '! Is unsteady aerodynamics data included in this table? If TRUE, then include 30 UA coefficients below this line\n')) + f.write('!........................................\n') + if ad['af_data'][afi][tab]['InclUAdata']: + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['alpha0'], 'alpha0', '! 0-lift angle of attack, depends on airfoil.\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['alpha1'], 'alpha1', '! Angle of attack at f=0.7, (approximately the stall angle) for AOA>alpha0. (deg)\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['alpha2'], 'alpha2', '! Angle of attack at f=0.7, (approximately the stall angle) for AOA1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['S2'], 'S2', '! Constant in the f curve best-fit for AOA> alpha1; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['S3'], 'S3', '! Constant in the f curve best-fit for alpha2<=AOA< alpha0; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['S4'], 'S4', '! Constant in the f curve best-fit for AOA< alpha2; by definition it depends on the airfoil. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['Cn1'], 'Cn1', '! Critical value of C0n at leading edge separation. It should be extracted from airfoil data at a given Mach and Reynolds number. It can be calculated from the static value of Cn at either the break in the pitching moment or the loss of chord force at the onset of stall. It is close to the condition of maximum lift of the airfoil at low Mach numbers.\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['Cn2'], 'Cn2', '! As Cn1 for negative AOAs.\n')) + f.write(_float_default_out(ad['af_data'][afi][tab]['St_sh']) + ' {:<11} {:}'.format('St_sh', "! Strouhal's shedding frequency constant. [default = 0.19]\n")) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['Cd0'], 'Cd0', '! 2D drag coefficient value at 0-lift.\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['Cm0'], 'Cm0', '! 2D pitching moment coefficient about 1/4-chord location, at 0-lift, positive if nose up. [If the aerodynamics coefficients table does not include a column for Cm, this needs to be set to 0.0]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['k0'], 'k0', '! Constant in the \\hat(x)_cp curve best-fit; = (\\hat(x)_AC-0.25). [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['k1'], 'k1', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['k2'], 'k2', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['k3'], 'k3', '! Constant in the \\hat(x)_cp curve best-fit. [ignored if UA_Mod<>1]\n')) + f.write('{:<22f} {:<11} {:}'.format(ad['af_data'][afi][tab]['k1_hat'], 'k1_hat', '! Constant in the expression of Cc due to leading edge vortex effects. [ignored if UA_Mod<>1]\n')) + f.write(_float_default_out(ad['af_data'][afi][tab]['x_cp_bar']) + ' {:<11} {:}'.format('x_cp_bar', '! Constant in the expression of \\hat(x)_cp^v. [ignored if UA_Mod<>1, default = 0.2]\n')) + f.write(_float_default_out(ad['af_data'][afi][tab]['UACutout']) + ' {:<11} {:}'.format('UACutout', '! Angle of attack above which unsteady aerodynamics are disabled (deg). [Specifying the string "Default" sets UACutout to 45 degrees]\n')) + f.write(_float_default_out(ad['af_data'][afi][tab]['filtCutOff']) + ' {:<11} {:}'.format('filtCutOff', '! Reduced frequency cut-off for low-pass filtering the AoA input to UA, as well as the 1st and 2nd derivatives (-) [default = 0.5]\n')) + + f.write('!........................................\n') + f.write('! Table of aerodynamics coefficients\n') + f.write('{:<22d} {:<11} {:}'.format(ad['af_data'][afi][tab]['NumAlf'], 'NumAlf', '! Number of data lines in the following table\n')) + f.write('! Alpha Cl Cd Cm\n') + f.write('! (deg) (-) (-) (-)\n') + + polar_map = [ad['InCol_Alfa'], ad['InCol_Cl'], ad['InCol_Cd'], ad['InCol_Cm'], ad['InCol_Cpmin']] + polar_map.remove(0) + polar_map = [i - 1 for i in polar_map] + + alpha = np.asarray(ad['af_data'][afi][tab]['Alpha']) + cl = np.asarray(ad['af_data'][afi][tab]['Cl']) + cd = np.asarray(ad['af_data'][afi][tab]['Cd']) + cm = np.asarray(ad['af_data'][afi][tab]['Cm']) + cpmin = np.asarray(ad['af_data'][afi][tab]['Cpmin']) + + if alpha[0] != -180.: + alpha[0] = -180. + if alpha[-1] != 180.: + alpha[-1] = 180. + if cl[0] != cl[-1]: + cl[0] = cl[-1] + if cd[0] != cd[-1]: + cd[0] = cd[-1] + if cm[0] != cm[-1]: + cm[0] = cm[-1] + + if ad['InCol_Cm'] == 0: + cm = np.zeros_like(cl) + if ad['InCol_Cpmin'] == 0: + cpmin = np.zeros_like(cl) + polar = np.column_stack((alpha, cl, cd, cm, cpmin)) + polar = polar[:, polar_map] + + for row in polar: + f.write(' '.join(['{: 2.14e}'.format(val) for val in row]) + '\n') + + f.flush() + os.fsync(f) + f.close() + + # ------------------------------------------------------------------ + # PRIVATE: Coord read/write + # ------------------------------------------------------------------ + + @staticmethod + def _read_coords(ad): + ad['af_coord'] = [] + ad['ac'] = np.zeros(len(ad['AFNames'])) + + for afi, af_filename in enumerate(ad['AFNames']): + ad['af_coord'].append({}) + if not (ad['af_data'][afi][0]['NumCoords'] == 0 or ad['af_data'][afi][0]['NumCoords'] == '0'): + coord_filename = af_filename[:af_filename.rfind(os.sep)] + os.sep + ad['af_data'][afi][0]['NumCoords'][2:-1] + + f = open(coord_filename) + lines = f.readlines() + f.close() + lines = [line for line in lines if not line.strip().startswith('!')] + n_coords = int(lines[0].split()[0]) + + x = np.zeros(n_coords - 1) + y = np.zeros(n_coords - 1) + ad['ac'][afi] = float(lines[1].split()[0]) + + for j in range(2, n_coords + 1): + x[j - 2], y[j - 2] = map(float, lines[j].split()) + + ad['af_coord'][afi]['x'] = x + ad['af_coord'][afi]['y'] = y + + @staticmethod + def _write_coords(ad, af_coord_indices, run_dir, naming_out): + coord_names = [''] * ad['NumAFfiles'] + + for afi in af_coord_indices: + coord_names[afi] = os.path.join('Airfoils', naming_out + '_AF%02d_Coords.txt' % afi) + + x = ad['af_coord'][afi]['x'] + y = ad['af_coord'][afi]['y'] + coord = np.vstack((x, y)).T + + af_file = os.path.join(run_dir, coord_names[afi]) + f = open(af_file, 'w') + + f.write('{: 22d} {:<11} {:}'.format(len(x) + 1, 'NumCoords', '! The number of coordinates in the airfoil shape file (including an extra coordinate for airfoil reference). Set to zero if coordinates not included.\n')) + f.write('! ......... x-y coordinates are next if NumCoords > 0 .............\n') + f.write('! x-y coordinate of airfoil reference\n') + f.write('! x/c y/c\n') + f.write('{: 5f} 0\n'.format(ad['ac'][afi])) + f.write('! coordinates of airfoil shape\n') + f.write('! interpolation to 200 points\n') + f.write('! x/c y/c\n') + for row in coord: + f.write(' '.join(['{: 2.14e}'.format(val) for val in row]) + '\n') + + f.flush() + os.fsync(f) + f.close() + + # ------------------------------------------------------------------ + # PRIVATE: OLAF read/write + # ------------------------------------------------------------------ + + @staticmethod + def _read_olaf(ad, olaf_filename, base_dir): + ad['OLAF'] = {} + f = open(olaf_filename) + f.readline(); f.readline(); f.readline() + ad['OLAF']['IntMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['DTfvw'] = float_read(f.readline().split()[0]) + ad['OLAF']['FreeWakeStart'] = float_read(f.readline().split()[0]) + ad['OLAF']['FullCircStart'] = float_read(f.readline().split()[0]) + f.readline() + ad['OLAF']['CircSolvMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['CircSolvConvCrit'] = float_read(f.readline().split()[0]) + ad['OLAF']['CircSolvRelaxation'] = float_read(f.readline().split()[0]) + ad['OLAF']['CircSolvMaxIter'] = int_read(f.readline().split()[0]) + ad['OLAF']['PrescribedCircFile'] = os.path.join(str(base_dir), quoted_read(f.readline().split()[0])) + f.readline(); f.readline(); f.readline() + ad['OLAF']['nNWPanels'] = int_read(f.readline().split()[0]) + ad['OLAF']['nNWPanelsFree'] = int_read(f.readline().split()[0]) + ad['OLAF']['nFWPanels'] = int_read(f.readline().split()[0]) + ad['OLAF']['nFWPanelsFree'] = int_read(f.readline().split()[0]) + ad['OLAF']['FWShedVorticity'] = bool_read(f.readline().split()[0]) + f.readline() + ad['OLAF']['DiffusionMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['RegDeterMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['RegFunction'] = int_read(f.readline().split()[0]) + ad['OLAF']['WakeRegMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['WakeRegFactor'] = float(f.readline().split()[0]) + ad['OLAF']['WingRegFactor'] = float(f.readline().split()[0]) + ad['OLAF']['CoreSpreadEddyVisc'] = int(f.readline().split()[0]) + f.readline() + ad['OLAF']['TwrShadowOnWake'] = bool_read(f.readline().split()[0]) + ad['OLAF']['ShearModel'] = int_read(f.readline().split()[0]) + f.readline() + ad['OLAF']['VelocityMethod'] = int_read(f.readline().split()[0]) + ad['OLAF']['TreeBranchFactor'] = float_read(f.readline().split()[0]) + ad['OLAF']['PartPerSegment'] = int_read(f.readline().split()[0]) + f.readline(); f.readline() + ad['OLAF']['WrVTk'] = int_read(f.readline().split()[0]) + ad['OLAF']['nVTKBlades'] = int_read(f.readline().split()[0]) + ad['OLAF']['VTKCoord'] = int_read(f.readline().split()[0]) + ad['OLAF']['VTK_fps'] = float_read(f.readline().split()[0]) + ad['OLAF']['nGridOut'] = int_read(f.readline().split()[0]) + f.readline() + f.close() + + @staticmethod + def _write_olaf(ad, run_dir, naming_out): + olaf_file = os.path.join(run_dir, naming_out + '_OLAF.dat') + f = open(olaf_file, 'w') + + f.write('--------------------------- OLAF (cOnvecting LAgrangian Filaments) INPUT FILE -----------------\n') + f.write('Generated by OpenFAST_IO\n') + f.write('--------------------------- GENERAL OPTIONS ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['IntMethod'], 'IntMethod', '- Integration method {1: RK4, 5: Forward Euler 1st order, default: 5} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['DTfvw'], 'DTfvw', '- Time interval for wake propagation. {default: dtaero} (s)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['FreeWakeStart'], 'FreeWakeStart', '- Time when wake is free. (-) value = always free. {default: 0.0} (s)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['FullCircStart'], 'FullCircStart', '- Time at which full circulation is reached. {default: 0.0} (s)\n')) + f.write('--------------------------- CIRCULATION SPECIFICATIONS ----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['CircSolvMethod'], 'CircSolvingMethod', '- Circulation solving method {1: Cl-Based, 2: No-Flow Through, 3: Prescribed, default: 1 }(switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['CircSolvConvCrit'], 'CircSolvConvCrit', ' - Convergence criteria {default: 0.001} [only if CircSolvMethod=1] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['CircSolvRelaxation'], 'CircSolvRelaxation', '- Relaxation factor {default: 0.1} [only if CircSolvMethod=1] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['CircSolvMaxIter'], 'CircSolvMaxIter', ' - Maximum number of iterations for circulation solving {default: 30} (-)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ad['OLAF']['PrescribedCircFile'] + '"', 'PrescribedCircFile', '- File containing prescribed circulation [only if CircSolvMethod=3] (quoted string)\n')) + f.write('===============================================================================================\n') + f.write('--------------------------- WAKE OPTIONS ------------------------------------------------------\n') + f.write('------------------- WAKE EXTENT AND DISCRETIZATION --------------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(ad['OLAF']['nNWPanels'], 'nNWPanels', '- Number of near-wake panels (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['nNWPanelsFree'], 'nNWPanelsFree', '- Number of free near-wake panels (-) {default: nNWPanels}\n')) + f.write('{:<22d} {:<11} {:}'.format(ad['OLAF']['nFWPanels'], 'nFWPanels', '- Number of far-wake panels (-) {default: 0}\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['nFWPanelsFree'], 'nFWPanelsFree', '- Number of free far-wake panels (-) {default: nFWPanels}\n')) + f.write('{!s:<22} {:<11} {:}'.format(ad['OLAF']['FWShedVorticity'], 'FWShedVorticity', '- Include shed vorticity in the far wake {default: False}\n')) + f.write('------------------- WAKE REGULARIZATIONS AND DIFFUSION -----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['DiffusionMethod'], 'DiffusionMethod', '- Diffusion method to account for viscous effects {0: None, 1: Core Spreading, "default": 0}\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['RegDeterMethod'], 'RegDeterMethod', '- Method to determine the regularization parameters {0: Manual, 1: Optimized, 2: Chord, 3: Span, default: 0 }\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['RegFunction'], 'RegFunction', '- Viscous diffusion function {0: None, 1: Rankine, 2: LambOseen, 3: Vatistas, 4: Denominator, "default": 3} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['WakeRegMethod'], 'WakeRegMethod', '- Wake regularization method {1: Constant, 2: Stretching, 3: Age, default: 3} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['WakeRegFactor'], 'WakeRegFactor', '- Wake regularization factor (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['WingRegFactor'], 'WingRegFactor', '- Wing regularization factor (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['CoreSpreadEddyVisc'], 'CoreSpreadEddyVisc', '- Eddy viscosity in core spreading methods, typical values 1-1000\n')) + f.write('------------------- WAKE TREATMENT OPTIONS ---------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ad['OLAF']['TwrShadowOnWake'], 'TwrShadowOnWake', '- Include tower flow disturbance effects on wake convection {default:false} [only if TwrPotent or TwrShadow]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['ShearModel'], 'ShearModel', '- Shear Model {0: No treatment, 1: Mirrored vorticity, default: 0}\n')) + f.write('------------------- SPEEDUP OPTIONS -----------------------------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(ad['OLAF']['VelocityMethod'], 'VelocityMethod', '- Method to determine the velocity {1:Segment N^2, 2:Particle tree, 3:Particle N^2, 4:Segment tree, default: 2}\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['TreeBranchFactor'], 'TreeBranchFactor', '- Branch radius fraction above which a multipole calculation is used {default: 1.5} [only if VelocityMethod=2,4]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['PartPerSegment'], 'PartPerSegment', '- Number of particles per segment {default: 1} [only if VelocityMethod=2,3]\n')) + f.write('===============================================================================================\n') + f.write('--------------------------- OUTPUT OPTIONS ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['WrVTk'], 'WrVTk', '- Outputs Visualization Toolkit (VTK) (independent of .fst option) {0: NoVTK, 1: Write VTK at VTK_fps, 2: Write VTK at init and final, default: 0} (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['nVTKBlades'], 'nVTKBlades', '- Number of blades for which VTK files are exported {0: No VTK per blade, n: VTK for blade 1 to n, default: 0} (-) \n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['VTKCoord'], 'VTKCoord', '- Coordinate system used for VTK export. {1: Global, 2: Hub, 3: Both, default: 1} \n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['VTK_fps'], 'VTK_fps', '- Frame rate for VTK output (frames per second) {"all" for all glue code timesteps, "default" for all OLAF timesteps} [only if WrVTK=1]\n')) + f.write('{:<22} {:<11} {:}'.format(ad['OLAF']['nGridOut'], 'nGridOut', '- Number of grid outputs\n')) + f.write('GridName GridType TStart TEnd DTGrid XStart XEnd nX YStart YEnd nY ZStart ZEnd nZ\n') + f.write('(-) (-) (s) (s) (s) (m) (m) (-) (m) (m) (-) (m) (m) (-)\n') + f.write('===============================================================================================\n') + f.write('--------------------------- ADVANCED OPTIONS --------------------------------------------------\n') + f.write('===============================================================================================\n') + + f.flush() + os.fsync(f) + f.close() diff --git a/openfast_io/openfast_io/io/base.py b/openfast_io/openfast_io/io/base.py new file mode 100644 index 0000000000..c8f9fdb1b0 --- /dev/null +++ b/openfast_io/openfast_io/io/base.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from pathlib import Path + + +class ModuleIO(ABC): + """Base class for all module-level IO classes. + + Each subclass handles reading and writing one OpenFAST module's input files. + read() returns a plain dict (the module's fst_vt slice). + write() accepts that same dict and produces the file(s). + + base_dir is always passed explicitly — module IOs do not assume a working + directory. This makes them testable in isolation. + """ + + @abstractmethod + def read(self, file_path: Path, base_dir: Path) -> dict: + """Read module input file(s). Returns dict matching the fst_vt module slice.""" + ... + + @abstractmethod + def write(self, data: dict, file_path: Path, base_dir: Path) -> None: + """Write module input file(s) from data dict.""" + ... diff --git a/openfast_io/openfast_io/io/beamdyn.py b/openfast_io/openfast_io/io/beamdyn.py new file mode 100644 index 0000000000..8667cdd667 --- /dev/null +++ b/openfast_io/openfast_io/io/beamdyn.py @@ -0,0 +1,322 @@ +"""BeamDyn module IO — reads and writes BeamDyn input files. + +Extracted from FAST_reader.py and FAST_writer.py. +""" +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np + +from .base import ModuleIO +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, +) + + +def _float_default_out(val, trim=False): + if isinstance(val, float): + return '{:.4f}'.format(val) if trim else '{: 22f}'.format(val) + else: + return '{:}'.format(val) if trim else '{:<22}'.format(val) + + +def _int_default_out(val, trim=False): + if isinstance(val, int): + return '{:22d}'.format(val) if not trim else '{:d}'.format(val) + else: + return '{:}'.format(val) if trim else '{:<22}'.format(val) + + +def _get_outlist(outlist_dict, channel_list): + def loop_dict(vartree, outlist_i): + for var in vartree.keys(): + if isinstance(vartree[var], dict): + loop_dict(vartree[var], outlist_i) + else: + if vartree[var]: + outlist_i.append(var) + return outlist_i + + if not channel_list: + channel_list = outlist_dict.keys() + outlist = [] + for var in channel_list: + var = var.replace(' ', '') + outlist_i = loop_dict(outlist_dict[var], []) + if outlist_i: + outlist.append(sorted(outlist_i)) + return outlist + + +class BeamDynIO(ModuleIO): + """Reads and writes BeamDyn input files (per blade). + + read() returns:: + + { + 'BeamDyn': { ... main BD params ... }, + 'BeamDynBlade': { ... blade material props ... }, + } + + Note: Unlike the monolithic reader which stores per-blade arrays indexed by + blade number, this IO handles one blade at a time. + """ + + def read(self, file_path: Path, base_dir: Path, *, + outlist: dict | None = None, + read_outlist_fn=None) -> dict: + bd = {} + file_path = str(file_path) + + f = open(file_path) + f.readline(); f.readline(); f.readline() + + # Simulation control + bd['Echo'] = bool_read(f.readline().split()[0]) + bd['QuasiStaticInit'] = bool_read(f.readline().split()[0]) + bd['rhoinf'] = float_read(f.readline().split()[0]) + bd['quadrature'] = int_read(f.readline().split()[0]) + bd['refine'] = int_read(f.readline().split()[0]) + bd['n_fact'] = int_read(f.readline().split()[0]) + bd['DTBeam'] = float_read(f.readline().split()[0]) + bd['load_retries'] = int_read(f.readline().split()[0]) + bd['NRMax'] = int_read(f.readline().split()[0]) + bd['stop_tol'] = float_read(f.readline().split()[0]) + bd['tngt_stf_fd'] = bool_read(f.readline().split()[0]) + bd['tngt_stf_comp'] = bool_read(f.readline().split()[0]) + bd['tngt_stf_pert'] = float_read(f.readline().split()[0]) + bd['tngt_stf_difftol'] = float_read(f.readline().split()[0]) + bd['RotStates'] = bool_read(f.readline().split()[0]) + f.readline() + + # Geometry + bd['member_total'] = int_read(f.readline().split()[0]) + bd['kp_total'] = int_read(f.readline().split()[0]) + bd['members'] = [] + for i in range(bd['member_total']): + ln = f.readline().split() + n_pts_i = int(ln[1]) + member_i = {} + member_i['kp_xr'] = [None] * n_pts_i + member_i['kp_yr'] = [None] * n_pts_i + member_i['kp_zr'] = [None] * n_pts_i + member_i['initial_twist'] = [None] * n_pts_i + f.readline(); f.readline() + for j in range(n_pts_i): + ln = f.readline().split() + member_i['kp_xr'][j] = float(ln[0]) + member_i['kp_yr'][j] = float(ln[1]) + member_i['kp_zr'][j] = float(ln[2]) + member_i['initial_twist'][j] = float(ln[3]) + bd['members'].append(member_i) + + # Mesh + f.readline() + bd['order_elem'] = int_read(f.readline().split()[0]) + + # Material + f.readline() + bd['BldFile'] = f.readline().split()[0].replace('"', '').replace("'", '') + + # Outputs + f.readline() + bd['SumPrint'] = bool_read(f.readline().split()[0]) + bd['OutFmt'] = quoted_read(f.readline().split()[0]) + bd['NNodeOuts'] = int_read(f.readline().split()[0]) + bd['OutNd'] = [idx.strip() for idx in f.readline().split('OutNd')[0].split(',')] + + # OutList + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'BeamDyn') + else: + line = f.readline() + while line and 'END' not in line.split('!')[0].upper()[:3]: + line = f.readline() + + # Optional nodal output + try: + f.readline() + bd['BldNd_BlOutNd'] = f.readline().split()[0] + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'BeamDyn_Nodes') + else: + line = f.readline() + while line and 'END' not in line.split('!')[0].upper()[:3]: + line = f.readline() + except Exception: + pass + + f.close() + + # Read blade material file + blade_file = os.path.join(os.path.dirname(file_path), bd['BldFile']) + bd_blade = self._read_blade(blade_file) + + return {'BeamDyn': bd, 'BeamDynBlade': bd_blade} + + def write(self, data: dict, file_path: Path, base_dir: Path, *, + naming_out: str = 'openfast', + outlist: dict | None = None, + fst_bd_filename: str | None = None) -> None: + bd = data['BeamDyn'] + bd_blade = data['BeamDynBlade'] + run_dir = str(base_dir) + + # Write blade material file first + blade_file = os.path.abspath(os.path.join(run_dir, bd['BldFile'])) + self._write_blade(bd_blade, blade_file) + + f = open(str(file_path), 'w') + + f.write('--------- BEAMDYN with OpenFAST INPUT FILE -------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(bd['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(bd['QuasiStaticInit'], 'QuasiStaticInit', '- Use quasistatic pre-conditioning with centripetal accelerations in initialization (flag) [dynamic solve only]\n')) + f.write('{:<22} {:<11} {:}'.format(bd['rhoinf'], 'rhoinf', '- Numerical damping parameter for generalized-alpha integrator\n')) + f.write('{:<22d} {:<11} {:}'.format(bd['quadrature'], 'quadrature', '- Quadrature method: 1=Gaussian; 2=Trapezoidal (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(bd['refine'], 'refine', '- Refinement factor for trapezoidal quadrature (-) [DEFAULT = 1; used only when quadrature=2]\n')) + f.write('{:<22} {:<11} {:}'.format(bd['n_fact'], 'n_fact', '- Factorization frequency for the Jacobian in N-R iteration(-) [DEFAULT = 5]\n')) + f.write(_float_default_out(bd['DTBeam']) + ' {:<11} {:}'.format('DTBeam', '- Time step size (s).\n')) + f.write(_int_default_out(bd['load_retries']) + ' {:<11} {:}'.format('load_retries', '- Number of factored load retries before quitting the aimulation [DEFAULT = 20]\n')) + f.write(_int_default_out(bd['NRMax']) + ' {:<11} {:}'.format('NRMax', '- Max number of iterations in Newton-Raphson algorithm (-). [DEFAULT = 10]\n')) + f.write(_float_default_out(bd['stop_tol']) + ' {:<11} {:}'.format('stop_tol', '- Tolerance for stopping criterion (-) [DEFAULT = 1E-5]\n')) + f.write('{!s:<22} {:<11} {:}'.format(bd['tngt_stf_fd'], 'tngt_stf_fd', '- Use finite differenced tangent stiffness matrix? (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(bd['tngt_stf_comp'], 'tngt_stf_comp', '- Compare analytical finite differenced tangent stiffness matrix? (flag)\n')) + f.write(_float_default_out(bd['tngt_stf_pert']) + ' {:<11} {:}'.format('tngt_stf_pert', '- Perturbation size for finite differencing (-) [DEFAULT = 1E-6]\n')) + f.write(_float_default_out(bd['tngt_stf_difftol']) + ' {:<11} {:}'.format('tngt_stf_difftol', '- Maximum allowable relative difference between analytical and fd tangent stiffness (-); [DEFAULT = 0.1]\n')) + f.write('{!s:<22} {:<11} {:}'.format(bd['RotStates'], 'RotStates', '- Orient states in the rotating frame during linearization? (flag) [used only when linearizing]\n')) + f.write('---------------------- GEOMETRY PARAMETER --------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(bd['member_total'], 'member_total', '- Total number of members (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(bd['kp_total'], 'kp_total', '- Total number of key points (-) [must be at least 3]\n')) + for i in range(bd['member_total']): + mem = bd['members'][i] + f.write('{:<22} {:<11} {:}'.format(' '.join(['%d' % (i + 1), '%d' % len(mem['kp_xr'])]), '', '- Member number; Number of key points in this member\n')) + f.write(" ".join(['{:^21s}'.format(h) for h in ['kp_xr', 'kp_yr', 'kp_zr', 'initial_twist']]) + '\n') + f.write(" ".join(['{:^21s}'.format(h) for h in ['(m)', '(m)', '(m)', '(deg)']]) + '\n') + for j in range(len(mem['kp_xr'])): + ln = ['{: 2.14e}'.format(mem['kp_xr'][j]), + '{: 2.14e}'.format(mem['kp_yr'][j]), + '{: 2.14e}'.format(mem['kp_zr'][j]), + '{: 2.14e}'.format(mem['initial_twist'][j])] + f.write(" ".join(ln) + '\n') + f.write('---------------------- MESH PARAMETER ------------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(bd['order_elem'], 'order_elem', '- Order of interpolation (basis) function (-)\n')) + f.write('---------------------- MATERIAL PARAMETER --------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format('"' + bd['BldFile'] + '"', 'BldFile', '- Name of file containing properties for blade (quoted string)\n')) + f.write('---------------------- OUTPUTS -------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(bd['SumPrint'], 'SumPrint', '- Print summary data to ".sum" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + bd['OutFmt'] + '"', 'OutFmt', '- Format used for text tabular output, excluding the time channel.\n')) + f.write('{:<22} {:<11} {:}'.format(bd['NNodeOuts'], 'NNodeOuts', '- Number of nodes to output to file [0 - 9] (-)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(bd['OutNd']), 'OutNd', '- Nodes whose values will be output (-)\n')) + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + + if outlist is not None: + ol = _get_outlist(outlist, ['BeamDyn']) + for channel_list in ol: + for ch in channel_list: + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + if 'BldNd_BlOutNd' in bd: + f.write('====== Outputs for all blade stations (same ending as above for B1N1.... =========================== [optional section]\n') + f.write('{!s:<22} {:<11} {:}'.format(bd['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output. Not implemented yet. (-)\n')) + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx, BeamDyn_Nodes tab for a listing of available output channels, (-)\n') + if outlist is not None: + opt_ol = _get_outlist(outlist, ['BeamDyn_Nodes']) + for opt_channel_list in opt_ol: + for ch in opt_channel_list: + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + f.write('---------------------------------------------------------------------------------------') + f.flush() + os.fsync(f) + f.close() + + # ------------------------------------------------------------------ + # PRIVATE: Blade material read/write + # ------------------------------------------------------------------ + + @staticmethod + def _read_blade(blade_file): + bld = {} + f = open(blade_file) + f.readline(); f.readline(); f.readline() + + bld['station_total'] = int_read(f.readline().split()[0]) + bld['damp_type'] = int_read(f.readline().split()[0]) + f.readline(); f.readline(); f.readline() + + # Stiffness-proportional damping + ln = f.readline().split() + bld['mu1'] = float(ln[0]) + bld['mu2'] = float(ln[1]) + bld['mu3'] = float(ln[2]) + bld['mu4'] = float(ln[3]) + bld['mu5'] = float(ln[4]) + bld['mu6'] = float(ln[5]) + f.readline() + + # Modal damping + n_modes = int(f.readline().split()[0]) + bld['n_modes'] = n_modes + bld['zeta'] = np.array(f.readline().strip().replace(',', ' ').split()[:n_modes], dtype=float).tolist() + f.readline() + + # Distributed properties + bld['radial_stations'] = np.zeros(bld['station_total']) + bld['beam_stiff'] = np.zeros((bld['station_total'], 6, 6)) + bld['beam_inertia'] = np.zeros((bld['station_total'], 6, 6)) + for i in range(bld['station_total']): + bld['radial_stations'][i] = float_read(f.readline().split()[0]) + for j in range(6): + bld['beam_stiff'][i, j, :] = np.array([float(val) for val in f.readline().strip().split()]) + f.readline() + for j in range(6): + bld['beam_inertia'][i, j, :] = np.array([float(val) for val in f.readline().strip().split()]) + f.readline() + + f.close() + return bld + + @staticmethod + def _write_blade(bld, blade_file): + from pathlib import Path as _Path + _Path(blade_file).parent.mkdir(parents=True, exist_ok=True) + f = open(blade_file, 'w') + + f.write('------- BEAMDYN INDIVIDUAL BLADE INPUT FILE --------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('------ Blade Parameters --------------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(bld['station_total'], 'station_total', '- Number of blade input stations (-)\n')) + f.write('{:<22} {:<11} {:}'.format(bld['damp_type'], 'damp_type', '- Damping type (switch) {0: none, 1: stiffness-proportional, 2: modal}\n')) + f.write('------ Stiffness-Proportional Damping [used only if damp_type=1] ---------------\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['mu1', 'mu2', 'mu3', 'mu4', 'mu5', 'mu6']]) + '\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['(-)', '(-)', '(-)', '(-)', '(-)', '(-)']]) + '\n') + mu = [bld['mu1'], bld['mu2'], bld['mu3'], bld['mu4'], bld['mu5'], bld['mu6']] + f.write(" ".join(['{:^11f}'.format(m) for m in mu]) + '\n') + f.write('------ Modal Damping [used only if damp_type=2] --------------------------------\n') + f.write('{:<22} {:<11} {:}\n'.format(bld['n_modes'], 'n_modes', '- Number of modal damping coefficients (-)')) + f.write('{:<22} {:<11} {:}\n'.format(" ".join([repr(v) for v in bld['zeta']]), 'zeta', ' - Damping coefficients for mode 1 through n_modes')) + f.write('------ Distributed Properties --------------------------------------------------\n') + for i in range(len(bld['radial_stations'])): + f.write('{: 2.15e}\n'.format(bld['radial_stations'][i])) + for j in range(6): + f.write(" ".join(['{: 2.15e}'.format(v) for v in bld['beam_stiff'][i, j, :]]) + '\n') + f.write('\n') + for j in range(6): + f.write(" ".join(['{: 2.15e}'.format(v) for v in bld['beam_inertia'][i, j, :]]) + '\n') + f.write('\n') + + f.write('\n') + f.flush() + os.fsync(f) + f.close() diff --git a/openfast_io/openfast_io/io/elastodyn.py b/openfast_io/openfast_io/io/elastodyn.py new file mode 100644 index 0000000000..054e4cad3b --- /dev/null +++ b/openfast_io/openfast_io/io/elastodyn.py @@ -0,0 +1,651 @@ +"""ElastoDyn module IO — reads and writes ElastoDyn, blade, and tower input files. + +Extracted from FAST_reader.py and FAST_writer.py. All parsing logic is identical +to the original; only the data target changes (local dict vs self.fst_vt). +""" +import os +from pathlib import Path + +from .base import ModuleIO +from ..outlist import capture_outlist, emit_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, + fix_path, +) + + +class ElastoDynIO(ModuleIO): + """Reads and writes ElastoDyn input files. + + read() returns:: + + { + 'ElastoDyn': { ... }, + 'ElastoDynBlade': [ {blade0}, {blade1}, {blade2} ], + 'ElastoDynTower': { ... }, + } + + write() accepts data with key 'ElastoDyn' (dict), optional 'ElastoDynBlade' + and 'ElastoDynTower'. Writes the main ED file plus blade/tower sub-files. + """ + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + + def read(self, file_path: Path, base_dir: Path, outlist: dict = None) -> dict: + ed = {} + file_path = Path(file_path) + base_dir = Path(base_dir) + + f = open(file_path) + + f.readline() + f.readline() + + # Simulation Control + f.readline() + ed['Echo'] = bool_read(f.readline().split()[0]) + ed['Method'] = int(f.readline().split()[0]) + ed['DT'] = float_read(f.readline().split()[0]) + + # Degrees of Freedom + f.readline() + ed['FlapDOF1'] = bool_read(f.readline().split()[0]) + ed['FlapDOF2'] = bool_read(f.readline().split()[0]) + ed['EdgeDOF'] = bool_read(f.readline().split()[0]) + ed['PitchDOF'] = bool_read(f.readline().split()[0]) + ed['TeetDOF'] = bool_read(f.readline().split()[0]) + ed['DrTrDOF'] = bool_read(f.readline().split()[0]) + ed['GenDOF'] = bool_read(f.readline().split()[0]) + ed['YawDOF'] = bool_read(f.readline().split()[0]) + ed['TwFADOF1'] = bool_read(f.readline().split()[0]) + ed['TwFADOF2'] = bool_read(f.readline().split()[0]) + ed['TwSSDOF1'] = bool_read(f.readline().split()[0]) + ed['TwSSDOF2'] = bool_read(f.readline().split()[0]) + ed['PtfmSgDOF'] = bool_read(f.readline().split()[0]) + ed['PtfmSwDOF'] = bool_read(f.readline().split()[0]) + ed['PtfmHvDOF'] = bool_read(f.readline().split()[0]) + ed['PtfmRDOF'] = bool_read(f.readline().split()[0]) + ed['PtfmPDOF'] = bool_read(f.readline().split()[0]) + ed['PtfmYDOF'] = bool_read(f.readline().split()[0]) + + # Initial Conditions + f.readline() + ed['OoPDefl'] = float_read(f.readline().split()[0]) + ed['IPDefl'] = float_read(f.readline().split()[0]) + ed['BlPitch1'] = float_read(f.readline().split()[0]) + ed['BlPitch2'] = float_read(f.readline().split()[0]) + ed['BlPitch3'] = float_read(f.readline().split()[0]) + ed['TeetDefl'] = float_read(f.readline().split()[0]) + ed['Azimuth'] = float_read(f.readline().split()[0]) + ed['RotSpeed'] = float_read(f.readline().split()[0]) + ed['NacYaw'] = float_read(f.readline().split()[0]) + ed['TTDspFA'] = float_read(f.readline().split()[0]) + ed['TTDspSS'] = float_read(f.readline().split()[0]) + ed['PtfmSurge'] = float_read(f.readline().split()[0]) + ed['PtfmSway'] = float_read(f.readline().split()[0]) + ed['PtfmHeave'] = float_read(f.readline().split()[0]) + ed['PtfmRoll'] = float_read(f.readline().split()[0]) + ed['PtfmPitch'] = float_read(f.readline().split()[0]) + ed['PtfmYaw'] = float_read(f.readline().split()[0]) + + # Turbine Configuration + f.readline() + ed['NumBl'] = int(f.readline().split()[0]) + ed['TipRad'] = float_read(f.readline().split()[0]) + ed['HubRad'] = float_read(f.readline().split()[0]) + ed['PreCone(1)'] = float_read(f.readline().split()[0]) + ed['PreCone(2)'] = float_read(f.readline().split()[0]) + ed['PreCone(3)'] = float_read(f.readline().split()[0]) + ed['HubCM'] = float_read(f.readline().split()[0]) + ed['UndSling'] = float_read(f.readline().split()[0]) + ed['Delta3'] = float_read(f.readline().split()[0]) + ed['AzimB1Up'] = float_read(f.readline().split()[0]) + ed['OverHang'] = float_read(f.readline().split()[0]) + ed['ShftGagL'] = float_read(f.readline().split()[0]) + ed['ShftTilt'] = float_read(f.readline().split()[0]) + ed['NacCMxn'] = float_read(f.readline().split()[0]) + ed['NacCMyn'] = float_read(f.readline().split()[0]) + ed['NacCMzn'] = float_read(f.readline().split()[0]) + ed['NcIMUxn'] = float_read(f.readline().split()[0]) + ed['NcIMUyn'] = float_read(f.readline().split()[0]) + ed['NcIMUzn'] = float_read(f.readline().split()[0]) + ed['Twr2Shft'] = float_read(f.readline().split()[0]) + ed['TowerHt'] = float_read(f.readline().split()[0]) + ed['TowerBsHt'] = float_read(f.readline().split()[0]) + ed['PtfmCMxt'] = float_read(f.readline().split()[0]) + ed['PtfmCMyt'] = float_read(f.readline().split()[0]) + ed['PtfmCMzt'] = float_read(f.readline().split()[0]) + ed['PtfmRefxt'] = float_read(f.readline().split()[0]) + ed['PtfmRefyt'] = float_read(f.readline().split()[0]) + ed['PtfmRefzt'] = float_read(f.readline().split()[0]) + + # Mass and Inertia + f.readline() + ed['TipMass(1)'] = float_read(f.readline().split()[0]) + ed['TipMass(2)'] = float_read(f.readline().split()[0]) + ed['TipMass(3)'] = float_read(f.readline().split()[0]) + ed['PBrIner(1)'] = float_read(f.readline().split()[0]) + ed['PBrIner(2)'] = float_read(f.readline().split()[0]) + ed['PBrIner(3)'] = float_read(f.readline().split()[0]) + ed['BlPIner(1)'] = float_read(f.readline().split()[0]) + ed['BlPIner(2)'] = float_read(f.readline().split()[0]) + ed['BlPIner(3)'] = float_read(f.readline().split()[0]) + ed['HubMass'] = float_read(f.readline().split()[0]) + ed['HubIner'] = float_read(f.readline().split()[0]) + ed['HubIner_Teeter'] = float_read(f.readline().split()[0]) + ed['GenIner'] = float_read(f.readline().split()[0]) + ed['NacMass'] = float_read(f.readline().split()[0]) + ed['NacYIner'] = float_read(f.readline().split()[0]) + ed['YawBrMass'] = float_read(f.readline().split()[0]) + ed['PtfmMass'] = float_read(f.readline().split()[0]) + ed['PtfmRIner'] = float_read(f.readline().split()[0]) + ed['PtfmPIner'] = float_read(f.readline().split()[0]) + ed['PtfmYIner'] = float_read(f.readline().split()[0]) + ed['PtfmXYIner'] = float_read(f.readline().split()[0]) + ed['PtfmYZIner'] = float_read(f.readline().split()[0]) + ed['PtfmXZIner'] = float_read(f.readline().split()[0]) + + # Blade + f.readline() + ed['BldNodes'] = int(f.readline().split()[0]) + ed['BldFile1'] = quoted_read(f.readline().split()[0]) + ed['BldFile2'] = quoted_read(f.readline().split()[0]) + ed['BldFile3'] = quoted_read(f.readline().split()[0]) + + # Rotor-Teeter + f.readline() + ed['TeetMod'] = int(f.readline().split()[0]) + ed['TeetDmpP'] = float_read(f.readline().split()[0]) + ed['TeetDmp'] = float_read(f.readline().split()[0]) + ed['TeetCDmp'] = float_read(f.readline().split()[0]) + ed['TeetSStP'] = float_read(f.readline().split()[0]) + ed['TeetHStP'] = float_read(f.readline().split()[0]) + ed['TeetSSSp'] = float_read(f.readline().split()[0]) + ed['TeetHSSp'] = float_read(f.readline().split()[0]) + + # Yaw Friction + f.readline() + ed['YawFrctMod'] = int(f.readline().split()[0]) + ed['M_CSmax'] = float_read(f.readline().split()[0]) + ed['M_FCSmax'] = float_read(f.readline().split()[0]) + ed['M_MCSmax'] = float_read(f.readline().split()[0]) + ed['M_CD'] = float_read(f.readline().split()[0]) + ed['M_FCD'] = float_read(f.readline().split()[0]) + ed['M_MCD'] = float_read(f.readline().split()[0]) + ed['sig_v'] = float_read(f.readline().split()[0]) + ed['sig_v2'] = float_read(f.readline().split()[0]) + ed['OmgCut'] = float_read(f.readline().split()[0]) + + # Drivetrain + f.readline() + ed['GBoxEff'] = float_read(f.readline().split()[0]) + ed['GBRatio'] = float_read(f.readline().split()[0]) + ed['DTTorSpr'] = float_read(f.readline().split()[0]) + ed['DTTorDmp'] = float_read(f.readline().split()[0]) + + # Furling + f.readline() + ed['Furling'] = bool_read(f.readline().split()[0]) + ed['FurlFile'] = os.path.join(str(base_dir), quoted_read(f.readline().split()[0])) + + # Tower + f.readline() + ed['TwrNodes'] = int(f.readline().split()[0]) + ed['TwrFile'] = quoted_read(f.readline().split()[0]) + + # Output Parameters + f.readline() + ed['SumPrint'] = bool_read(f.readline().split()[0]) + ed['OutFile'] = int(f.readline().split()[0]) + ed['TabDelim'] = bool_read(f.readline().split()[0]) + ed['OutFmt'] = quoted_read(f.readline().split()[0]) + ed['TStart'] = float_read(f.readline().split()[0]) + ed['DecFact'] = int(f.readline().split()[0]) + ed['NTwGages'] = int(f.readline().split()[0]) + if ed['NTwGages'] != 0: + ed['TwrGagNd'] = read_array(f, ed['NTwGages'], array_type=int) + else: + ed['TwrGagNd'] = 0 + f.readline() + ed['NBlGages'] = int(f.readline().split()[0]) + if ed['NBlGages'] != 0: + ed['BldGagNd'] = read_array(f, ed['NBlGages'], array_type=int) + else: + ed['BldGagNd'] = 0 + + # OutList — capture into the shared registry (mirrors legacy openfast_io read_outlist) + f.readline() + if outlist is not None: + capture_outlist(f, outlist, 'ElastoDyn') + else: + self._read_outlist(f) + + # Optional nodal output — peek to check whether the section exists (EOF means it + # doesn't), then only swallow the specific parse errors a malformed/absent + # section would raise, so unrelated bugs aren't hidden. + section_header = f.readline() + if section_header: + try: + ed['BldNd_BladesOut'] = int(f.readline().split()[0]) + ed['BldNd_BlOutNd'] = f.readline().split()[0] + f.readline() + if outlist is not None: + capture_outlist(f, outlist, 'ElastoDyn_Nodes') + else: + self._read_outlist(f) + except (ValueError, IndexError): + pass + + f.close() + + # ── Read blade files ── + blades = [{}, {}, {}] + bld_files = [ed.get('BldFile1', ''), ed.get('BldFile2', ''), ed.get('BldFile3', '')] + num_bl = ed.get('NumBl', 3) + for i in range(num_bl): + bld_path = base_dir / fix_path(bld_files[i]) + if bld_path.exists(): + blades[i] = self._read_blade(bld_path) + + # ── Read tower file ── + tower = {} + twr_file = ed.get('TwrFile', '') + if twr_file: + twr_path = base_dir / fix_path(twr_file) + if twr_path.exists(): + tower = self._read_tower(twr_path) + + return { + 'ElastoDyn': ed, + 'ElastoDynBlade': blades, + 'ElastoDynTower': tower, + } + + @staticmethod + def _read_outlist(f): + """Skip over an OutList section (consumed but not stored here — driver handles it).""" + data = f.readline() + while data.strip() == '': + data = f.readline() + while data and not data.strip().startswith('END'): + data = f.readline() + while data.strip() == '': + data = f.readline() + + @staticmethod + def _read_blade(blade_path: Path) -> dict: + blade = {} + f = open(blade_path) + + f.readline() + f.readline() + f.readline() + + # Blade Parameters + blade['NBlInpSt'] = int(f.readline().split()[0]) + blade['BldFlDmp1'] = float_read(f.readline().split()[0]) + blade['BldFlDmp2'] = float_read(f.readline().split()[0]) + blade['BldEdDmp1'] = float_read(f.readline().split()[0]) + + # Blade Adjustment Factors + f.readline() + blade['FlStTunr1'] = float_read(f.readline().split()[0]) + blade['FlStTunr2'] = float_read(f.readline().split()[0]) + blade['AdjBlMs'] = float_read(f.readline().split()[0]) + blade['AdjFlSt'] = float_read(f.readline().split()[0]) + blade['AdjEdSt'] = float_read(f.readline().split()[0]) + + # Distributed Blade Properties + f.readline() + f.readline() + f.readline() + n = blade['NBlInpSt'] + blade['BlFract'] = [None] * n + blade['StrcTwst'] = [None] * n + blade['BMassDen'] = [None] * n + blade['FlpStff'] = [None] * n + blade['EdgStff'] = [None] * n + + for i in range(n): + data = f.readline().split() + blade['BlFract'][i] = float_read(data[0]) + blade['StrcTwst'][i] = float_read(data[1]) + blade['BMassDen'][i] = float_read(data[2]) + blade['FlpStff'][i] = float_read(data[3]) + blade['EdgStff'][i] = float_read(data[4]) + + f.readline() + blade['BldFl1Sh'] = [None] * 5 + blade['BldFl2Sh'] = [None] * 5 + blade['BldEdgSh'] = [None] * 5 + for i in range(5): + blade['BldFl1Sh'][i] = float_read(f.readline().split()[0]) + for i in range(5): + blade['BldFl2Sh'][i] = float_read(f.readline().split()[0]) + for i in range(5): + blade['BldEdgSh'][i] = float_read(f.readline().split()[0]) + + f.close() + return blade + + @staticmethod + def _read_tower(tower_path: Path) -> dict: + tower = {} + f = open(tower_path) + + f.readline() + f.readline() + + # General Tower Parameters + f.readline() + tower['NTwInpSt'] = int(f.readline().split()[0]) + tower['TwrFADmp1'] = float_read(f.readline().split()[0]) + tower['TwrFADmp2'] = float_read(f.readline().split()[0]) + tower['TwrSSDmp1'] = float_read(f.readline().split()[0]) + tower['TwrSSDmp2'] = float_read(f.readline().split()[0]) + + # Tower Adjustment Factors + f.readline() + tower['FAStTunr1'] = float_read(f.readline().split()[0]) + tower['FAStTunr2'] = float_read(f.readline().split()[0]) + tower['SSStTunr1'] = float_read(f.readline().split()[0]) + tower['SSStTunr2'] = float_read(f.readline().split()[0]) + tower['AdjTwMa'] = float_read(f.readline().split()[0]) + tower['AdjFASt'] = float_read(f.readline().split()[0]) + tower['AdjSSSt'] = float_read(f.readline().split()[0]) + + # Distributed Tower Properties + f.readline() + f.readline() + f.readline() + n = tower['NTwInpSt'] + tower['HtFract'] = [None] * n + tower['TMassDen'] = [None] * n + tower['TwFAStif'] = [None] * n + tower['TwSSStif'] = [None] * n + + for i in range(n): + data = f.readline().split() + tower['HtFract'][i] = float_read(data[0]) + tower['TMassDen'][i] = float_read(data[1]) + tower['TwFAStif'][i] = float_read(data[2]) + tower['TwSSStif'][i] = float_read(data[3]) + + # Tower Mode Shapes + f.readline() + tower['TwFAM1Sh'] = [None] * 5 + tower['TwFAM2Sh'] = [None] * 5 + for i in range(5): + tower['TwFAM1Sh'][i] = float_read(f.readline().split()[0]) + for i in range(5): + tower['TwFAM2Sh'][i] = float_read(f.readline().split()[0]) + f.readline() + tower['TwSSM1Sh'] = [None] * 5 + tower['TwSSM2Sh'] = [None] * 5 + for i in range(5): + tower['TwSSM1Sh'][i] = float_read(f.readline().split()[0]) + for i in range(5): + tower['TwSSM2Sh'][i] = float_read(f.readline().split()[0]) + + f.close() + return tower + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + + def write(self, data: dict, file_path: Path, base_dir: Path, outlist: dict = None) -> None: + file_path = Path(file_path) + base_dir = Path(base_dir) + + # Accept either {'ElastoDyn': {...}} or a flat ED dict + if 'ElastoDyn' in data: + ed = data['ElastoDyn'] + blades_data = data.get('ElastoDynBlade', None) + tower_data = data.get('ElastoDynTower', None) + else: + ed = data + blades_data = None + tower_data = None + + f = open(file_path, 'w') + + f.write('------- ELASTODYN INPUT FILE -------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + + # Simulation Control + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ed['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['Method'], 'Method', '- Integration method: {1: RK4, 2: AB4, or 3: ABM4} (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['DT'], 'DT', '- Integration time step (s)\n')) + + # Degrees of Freedom + f.write('---------------------- DEGREES OF FREEDOM --------------------------------------\n') + for key in ['FlapDOF1', 'FlapDOF2', 'EdgeDOF', 'PitchDOF', 'TeetDOF', 'DrTrDOF', + 'GenDOF', 'YawDOF', 'TwFADOF1', 'TwFADOF2', 'TwSSDOF1', 'TwSSDOF2', + 'PtfmSgDOF', 'PtfmSwDOF', 'PtfmHvDOF', 'PtfmRDOF', 'PtfmPDOF', 'PtfmYDOF']: + f.write('{!s:<22} {:<11} {:}'.format(ed[key], key, f'- {key} (flag)\n')) + + # Initial Conditions + f.write('---------------------- INITIAL CONDITIONS --------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['OoPDefl'], 'OoPDefl', '- Initial out-of-plane blade-tip displacement (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['IPDefl'], 'IPDefl', '- Initial in-plane blade-tip deflection (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['BlPitch1'], 'BlPitch(1)', '- Blade 1 initial pitch (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['BlPitch2'], 'BlPitch(2)', '- Blade 2 initial pitch (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['BlPitch3'], 'BlPitch(3)', '- Blade 3 initial pitch (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TeetDefl'], 'TeetDefl', '- Initial or fixed teeter angle (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['Azimuth'], 'Azimuth', '- Initial azimuth angle for blade 1 (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['RotSpeed'], 'RotSpeed', '- Initial or fixed rotor speed (rpm)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['NacYaw'], 'NacYaw', '- Initial or fixed nacelle-yaw angle (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TTDspFA'], 'TTDspFA', '- Initial fore-aft tower-top displacement (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TTDspSS'], 'TTDspSS', '- Initial side-to-side tower-top displacement (meters)\n')) + for key in ['PtfmSurge', 'PtfmSway', 'PtfmHeave', 'PtfmRoll', 'PtfmPitch', 'PtfmYaw']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- Initial or fixed platform {key.replace("Ptfm","")} displacement\n')) + + # Turbine Configuration + f.write('---------------------- TURBINE CONFIGURATION -----------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['NumBl'], 'NumBl', '- Number of blades (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TipRad'], 'TipRad', '- The distance from the rotor apex to the blade tip (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['HubRad'], 'HubRad', '- The distance from the rotor apex to the blade root (meters)\n')) + for i in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(ed[f'PreCone({i})'], f'PreCone({i})', f'- Blade {i} cone angle (degrees)\n')) + for key in ['HubCM', 'UndSling', 'Delta3', 'AzimB1Up', 'OverHang', 'ShftGagL', 'ShftTilt']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key}\n')) + for key in ['NacCMxn', 'NacCMyn', 'NacCMzn', 'NcIMUxn', 'NcIMUyn', 'NcIMUzn']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key} (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['Twr2Shft'], 'Twr2Shft', '- Vertical distance from the tower-top to the rotor shaft (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TowerHt'], 'TowerHt', '- Height of tower above ground level (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TowerBsHt'], 'TowerBsHt', '- Height of tower base above ground level (meters)\n')) + for key in ['PtfmCMxt', 'PtfmCMyt', 'PtfmCMzt', 'PtfmRefxt', 'PtfmRefyt', 'PtfmRefzt']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key} (meters)\n')) + + # Mass and Inertia + f.write('---------------------- MASS AND INERTIA ----------------------------------------\n') + for i in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(ed[f'TipMass({i})'], f'TipMass({i})', f'- Tip-brake mass, blade {i} (kg)\n')) + for i in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(ed[f'PBrIner({i})'], f'PBrIner({i})', f'- Pitch bearing inertia, blade {i} (kg m^2)\n')) + for i in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(ed[f'BlPIner({i})'], f'BlPIner({i})', f'- Blade pitch inertia, blade {i} (kg m^2)\n')) + for key in ['HubMass', 'HubIner', 'HubIner_Teeter', 'GenIner', 'NacMass', 'NacYIner', + 'YawBrMass', 'PtfmMass', 'PtfmRIner', 'PtfmPIner', 'PtfmYIner', + 'PtfmXYIner', 'PtfmYZIner', 'PtfmXZIner']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key}\n')) + + # Blade + f.write('---------------------- BLADE ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['BldNodes'], 'BldNodes', '- Number of blade nodes (per blade) used for analysis (-)\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['BldFile1']+'"', 'BldFile(1)', '- Name of file containing properties for blade 1\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['BldFile2']+'"', 'BldFile(2)', '- Name of file containing properties for blade 2\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['BldFile3']+'"', 'BldFile(3)', '- Name of file containing properties for blade 3\n')) + + # Rotor-Teeter + f.write('---------------------- ROTOR-TEETER --------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['TeetMod'], 'TeetMod', '- Rotor-teeter spring/damper model (switch)\n')) + for key in ['TeetDmpP', 'TeetDmp', 'TeetCDmp', 'TeetSStP', 'TeetHStP', 'TeetSSSp', 'TeetHSSp']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key}\n')) + + # Yaw Friction + f.write('---------------------- YAW-FRICTION --------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['YawFrctMod'], 'YawFrctMod', '- Yaw-friction model (switch)\n')) + for key in ['M_CSmax', 'M_FCSmax', 'M_MCSmax', 'M_CD', 'M_FCD', 'M_MCD', 'sig_v', 'sig_v2', 'OmgCut']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key}\n')) + + # Drivetrain + f.write('---------------------- DRIVETRAIN ----------------------------------------------\n') + for key in ['GBoxEff', 'GBRatio', 'DTTorSpr', 'DTTorDmp']: + f.write('{:<22} {:<11} {:}'.format(ed[key], key, f'- {key}\n')) + + # Furling + f.write('---------------------- FURLING -------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ed['Furling'], 'Furling', '- Read in additional model properties for furling turbine (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['FurlFile']+'"', 'FurlFile', '- Name of file containing furling properties\n')) + + # Tower + f.write('---------------------- TOWER ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ed['TwrNodes'], 'TwrNodes', '- Number of tower nodes used for analysis (-)\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['TwrFile']+'"', 'TwrFile', '- Name of file containing tower properties\n')) + + # Output + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ed['SumPrint'], 'SumPrint', '- Print summary data to ".sum" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['OutFile'], 'OutFile', '- Switch to determine where output will be placed (currently unused)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ed['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"'+ed['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output\n')) + f.write('{:<22} {:<11} {:}'.format(ed['TStart'], 'TStart', '- Time to begin tabular output (s)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['DecFact'], 'DecFact', '- Decimation factor for tabular output (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ed['NTwGages'], 'NTwGages', '- Number of tower nodes that have strain gages for output (-)\n')) + if ed['TwrGagNd'] != 0: + f.write('{:<22} {:<11} {:}'.format(', '.join(['%d'%int(i) for i in ed['TwrGagNd']]), 'TwrGagNd', '- List of tower nodes that have strain gages\n')) + else: + f.write('{:<22} {:<11} {:}'.format('', 'TwrGagNd', '- List of tower nodes that have strain gages\n')) + f.write('{:<22} {:<11} {:}'.format(ed['NBlGages'], 'NBlGages', '- Number of blade nodes that have strain gages for output (-)\n')) + if ed['BldGagNd'] != 0: + f.write('{:<22} {:<11} {:}'.format(', '.join(['%d'%int(i) for i in ed['BldGagNd']]), 'BldGagNd', '- List of blade nodes that have strain gages\n')) + else: + f.write('{:<22} {:<11} {:}'.format('', 'BldGagNd', '- List of blade nodes that have strain gages\n')) + + # OutList — emit the captured channels (mirrors legacy openfast_io write) + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + if outlist is not None: + emit_outlist(f, outlist, 'ElastoDyn') + f.write('END of OutList section (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + # Optional nodal output + if 'BldNd_BladesOut' in ed: + f.write('====== Outputs for all blade stations =========================== [optional section]\n') + f.write('{:<22d} {:<11} {:}'.format(ed['BldNd_BladesOut'], 'BldNd_BladesOut', '- Number of blades to output all node information at (-)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ed['BldNd_BlOutNd'], 'BldNd_BlOutNd', '- Future feature will allow selecting a portion of the nodes to output (-)\n')) + f.write(' OutList - The next line(s) contains a list of output parameters.\n') + if outlist is not None: + emit_outlist(f, outlist, 'ElastoDyn_Nodes') + f.write('END (the word "END" must appear in the first 3 columns of this last OutList line in the optional nodal output section)\n') + + f.write('---------------------------------------------------------------------------------------\n') + f.flush() + os.fsync(f) + f.close() + + # Write blade files + if blades_data is not None and blades_data: + if isinstance(blades_data, dict): + # Single dict = all blades identical + for i in range(1, 4): + bld_file = ed.get(f'BldFile{i}', '') + if bld_file: + self._write_blade(blades_data, base_dir / bld_file) + elif isinstance(blades_data, list): + for i, blade in enumerate(blades_data): + bld_file = ed.get(f'BldFile{i+1}', '') + if bld_file and blade: + self._write_blade(blade, base_dir / bld_file) + + # Write tower file + if tower_data is not None and tower_data: + twr_file = ed.get('TwrFile', '') + if twr_file: + self._write_tower(tower_data, base_dir / twr_file) + + @staticmethod + def _write_blade(blade: dict, blade_path: Path) -> None: + f = open(blade_path, 'w') + + f.write('------- ELASTODYN INDIVIDUAL BLADE INPUT FILE --------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------- BLADE PARAMETERS ----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(blade['NBlInpSt'], 'NBlInpSt', '- Number of blade input stations (-)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['BldFlDmp1'], 'BldFlDmp(1)', '- Blade flap mode #1 structural damping in percent of critical (%)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['BldFlDmp2'], 'BldFlDmp(2)', '- Blade flap mode #2 structural damping in percent of critical (%)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['BldEdDmp1'], 'BldEdDmp(1)', '- Blade edge mode #1 structural damping in percent of critical (%)\n')) + f.write('---------------------- BLADE ADJUSTMENT FACTORS --------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(blade['FlStTunr1'], 'FlStTunr(1)', '- Blade flapwise modal stiffness tuner, 1st mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['FlStTunr2'], 'FlStTunr(2)', '- Blade flapwise modal stiffness tuner, 2nd mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['AdjBlMs'], 'AdjBlMs', '- Factor to adjust blade mass density (-)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['AdjFlSt'], 'AdjFlSt', '- Factor to adjust blade flap stiffness (-)\n')) + f.write('{:<22} {:<11} {:}'.format(blade['AdjEdSt'], 'AdjEdSt', '- Factor to adjust blade edge stiffness (-)\n')) + f.write('---------------------- DISTRIBUTED BLADE PROPERTIES ----------------------------\n') + f.write(' BlFract StrcTwst BMassDen FlpStff EdgStff\n') + f.write(' (-) (deg) (kg/m) (Nm^2) (Nm^2)\n') + for i in range(blade['NBlInpSt']): + f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format( + blade['BlFract'][i], blade['StrcTwst'][i], blade['BMassDen'][i], + blade['FlpStff'][i], blade['EdgStff'][i])) + f.write('---------------------- BLADE MODE SHAPES ---------------------------------------\n') + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(blade['BldFl1Sh'][i], f'BldFl1Sh({i+2})', f'- Flap mode 1, coeff of x^{i+2}\n')) + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(blade['BldFl2Sh'][i], f'BldFl2Sh({i+2})', f'- Flap mode 2, coeff of x^{i+2}\n')) + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(blade['BldEdgSh'][i], f'BldEdgSh({i+2})', f'- Edge mode 1, coeff of x^{i+2}\n')) + + f.flush() + os.fsync(f) + f.close() + + @staticmethod + def _write_tower(tower: dict, tower_path: Path) -> None: + f = open(tower_path, 'w') + + f.write('------- ELASTODYN TOWER INPUT FILE -------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------- TOWER PARAMETERS ----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(tower['NTwInpSt'], 'NTwInpSt', '- Number of input stations to specify tower geometry\n')) + f.write('{:<22} {:<11} {:}'.format(tower['TwrFADmp1'], 'TwrFADmp(1)', '- Tower 1st fore-aft mode structural damping ratio (%)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['TwrFADmp2'], 'TwrFADmp(2)', '- Tower 2nd fore-aft mode structural damping ratio (%)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['TwrSSDmp1'], 'TwrSSDmp(1)', '- Tower 1st side-to-side mode structural damping ratio (%)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['TwrSSDmp2'], 'TwrSSDmp(2)', '- Tower 2nd side-to-side mode structural damping ratio (%)\n')) + f.write('---------------------- TOWER ADJUSTMUNT FACTORS --------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(tower['FAStTunr1'], 'FAStTunr(1)', '- Tower fore-aft modal stiffness tuner, 1st mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['FAStTunr2'], 'FAStTunr(2)', '- Tower fore-aft modal stiffness tuner, 2nd mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['SSStTunr1'], 'SSStTunr(1)', '- Tower side-to-side stiffness tuner, 1st mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['SSStTunr2'], 'SSStTunr(2)', '- Tower side-to-side stiffness tuner, 2nd mode (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['AdjTwMa'], 'AdjTwMa', '- Factor to adjust tower mass density (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['AdjFASt'], 'AdjFASt', '- Factor to adjust tower fore-aft stiffness (-)\n')) + f.write('{:<22} {:<11} {:}'.format(tower['AdjSSSt'], 'AdjSSSt', '- Factor to adjust tower side-to-side stiffness (-)\n')) + f.write('---------------------- DISTRIBUTED TOWER PROPERTIES ----------------------------\n') + f.write(' HtFract TMassDen TwFAStif TwSSStif\n') + f.write(' (-) (kg/m) (Nm^2) (Nm^2)\n') + for i in range(tower['NTwInpSt']): + f.write('{: 2.15e} {: 2.15e} {: 2.15e} {: 2.15e}\n'.format( + tower['HtFract'][i], tower['TMassDen'][i], + tower['TwFAStif'][i], tower['TwSSStif'][i])) + f.write('---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------\n') + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(tower['TwFAM1Sh'][i], f'TwFAM1Sh({i+2})', f'- Mode 1, coefficient of x^{i+2} term\n')) + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(tower['TwFAM2Sh'][i], f'TwFAM2Sh({i+2})', f'- Mode 2, coefficient of x^{i+2} term\n')) + f.write('---------------------- TOWER SIDE-TO-SIDE MODE SHAPES --------------------------\n') + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(tower['TwSSM1Sh'][i], f'TwSSM1Sh({i+2})', f'- Mode 1, coefficient of x^{i+2} term\n')) + for i in range(5): + f.write('{:<22} {:<11} {:}'.format(tower['TwSSM2Sh'][i], f'TwSSM2Sh({i+2})', f'- Mode 2, coefficient of x^{i+2} term\n')) + + f.flush() + os.fsync(f) + f.close() diff --git a/openfast_io/openfast_io/io/extptfm.py b/openfast_io/openfast_io/io/extptfm.py new file mode 100644 index 0000000000..f1c329eb22 --- /dev/null +++ b/openfast_io/openfast_io/io/extptfm.py @@ -0,0 +1,365 @@ +""" +ExtPtfmIO – read / write ExtPtfm (External Platform) input files. + +Produces ``{'ExtPtfm': ep}`` + +ExtPtfm reads the main input file plus up to four sub-files: + - Superelement (Guyan/Craig-Bampton reduction matrices) + - Connections (optional) + - UserForcing (optional) + - ConnForcing (optional) +""" +from __future__ import annotations + +import os +import re +from typing import Any, Dict, Optional, Callable + +import numpy as np + +from .base import ModuleIO +from ..outlist import emit_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, +) + + +class ExtPtfmIO(ModuleIO): + """Read / write ExtPtfm input files.""" + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + ep: Dict[str, Any] = {} + ep_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + ep_dir = os.path.dirname(ep_file) + + f = open(ep_file) + f.readline() + f.readline() + f.readline() + + # Simulation Control + ep['Echo'] = bool_read(f.readline().split()[0]) + ep['DT'] = float_read(f.readline().split()[0]) + ep['IntMethod'] = int_read(f.readline().split()[0]) + f.readline() + + # Reduction inputs + ep['RBMod'] = int_read(f.readline().split()[0]) + ep['Red_FileName'] = os.path.join(ep_dir, quoted_read(f.readline().split()[0])) + ep['NActiveDOFList'] = int_read(f.readline().split()[0]) + ep['ActiveDOFList'] = read_array(f, None, split_val='ActiveDOFList', array_type=int) + ep['NInitPosList'] = int_read(f.readline().split()[0]) + ep['InitPosList'] = read_array(f, None, split_val='InitPosList', array_type=float) + ep['NInitVelList'] = int_read(f.readline().split()[0]) + ep['InitVelList'] = read_array(f, None, split_val='InitVelList', array_type=float) + f.readline() + + # Connection inputs + ep['HasConnections'] = bool_read(f.readline().split()[0]) + ep['Conn_FileName'] = os.path.join(ep_dir, quoted_read(f.readline().split()[0])) + f.readline() + + # User forcing inputs + ep['HasUserForcing'] = bool_read(f.readline().split()[0]) + ep['Force_FileName'] = os.path.join(ep_dir, quoted_read(f.readline().split()[0])) + ep['HasConnForcing'] = bool_read(f.readline().split()[0]) + ep['FConn_FileName'] = os.path.join(ep_dir, quoted_read(f.readline().split()[0])) + f.readline() + + # Output + ep['SumPrint'] = bool_read(f.readline().split()[0]) + ep['OutFile'] = int_read(f.readline().split()[0]) + ep['TabDelim'] = bool_read(f.readline().split()[0]) + ep['OutFmt'] = quoted_read(f.readline().split()[0]) + ep['TStart'] = float_read(f.readline().split()[0]) + + # Output channels + f.readline() + data = f.readline() + while data.split().__len__() == 0: + data = f.readline() + + ep['_outlist'] = {} + while data.split()[0] != 'END': + if data.find('"') >= 0: + channels = data.split('"') + channel_list = channels[1].split(',') + else: + row_string = data.split(',') + if len(row_string) == 1: + channel_list = row_string[0].split('\n')[0] + else: + channel_list = row_string + if isinstance(channel_list, list): + for ch in channel_list: + ch = ch.strip() + if ch: + ep['_outlist'][ch] = True + else: + ch = channel_list.strip() + if ch: + ep['_outlist'][ch] = True + data = f.readline() + + # Populate the shared registry (freeform — ExtPtfm CBD*/CBF* channels are + # not in the standard channel registry). data was pre-read, so we copy the + # parsed set rather than re-reading the section. + if outlist is not None: + outlist.setdefault('ExtPtfm', {}) + for ch in ep['_outlist']: + outlist['ExtPtfm'][ch] = True + # Channels now live in the shared registry; don't pollute fst_vt['ExtPtfm'] + # with a private '_outlist' key the legacy openfast_io reader never creates. + ep.pop('_outlist', None) + + f.close() + + # Read sub-files + ep['FlexASCII'] = {} + self._read_superelement(ep['Red_FileName'], ep['FlexASCII']) + + if ep['HasConnections']: + ep['Connections'] = {} + self._read_connections(ep['Conn_FileName'], ep['FlexASCII']['nDOF'], ep['Connections']) + + if ep['HasUserForcing']: + ep['UserForcing'] = {} + self._read_user_forcing(ep['Force_FileName'], ep['FlexASCII']['nDOF'], ep['UserForcing']) + + if ep['HasConnForcing']: + ep['ConnForcing'] = {} + nConn = ep['Connections']['nConn'] + self._read_conn_forcing(ep['FConn_FileName'], nConn, ep['ConnForcing']) + + return {'ExtPtfm': ep} + + # ------------------------------------------------------------------ + # Sub-file readers + # ------------------------------------------------------------------ + + @staticmethod + def _readmat(n, m, lines, iStart): + M = np.zeros((n, m)) + for j in range(n): + M[j, :] = np.array(lines[iStart + j].split()).astype(float) + return M + + def _read_superelement(self, se_file, flex): + """Read superelement file into *flex* dict.""" + with open(se_file) as fh: + lines = fh.read().splitlines() + + nDOF = -1 + i = 0 + while i < len(lines): + lo = lines[i].lower() + if lo.find('!mass') == 0: + flex['MassMatrix'] = self._readmat(nDOF, nDOF, lines, i + 1) + i += nDOF + elif lo.find('!stiffness') == 0: + flex['StiffnessMatrix'] = self._readmat(nDOF, nDOF, lines, i + 1) + i += nDOF + elif lo.find('!damping') == 0: + flex['DampingMatrix'] = self._readmat(nDOF, nDOF, lines, i + 1) + i += nDOF + elif lo.find('!weight constant') == 0: + flex['WeightConstant'] = self._readmat(1, nDOF, lines, i + 1) + i += 1 + elif lo.find('!weight stiffness') == 0: + flex['WeightStiffness'] = self._readmat(nDOF, nDOF, lines, i + 1) + i += nDOF + elif len(lo) > 0 and lo[0] == '!' and lo.find('!dimension') == 0: + flex['nDOF'] = int(lo.split(':')[1]) + nDOF = flex['nDOF'] + i += 1 + + def _read_connections(self, conn_file, nDOF, conn): + """Read connections file into *conn* dict.""" + with open(conn_file) as fh: + lines = fh.read().splitlines() + + nConn = -1 + i = 0 + while i < len(lines): + lo = lines[i].lower() + if lo.find('!connection') == 0: + conn['Position'] = self._readmat(nConn, 3, lines, i + 1) + i += nConn + elif lo.find('!displacement') == 0: + conn['Displacement'] = self._readmat(3 * nConn, nDOF, lines, i + 1) + i += 3 * nConn + elif len(lo) > 0 and lo[0] == '!' and lo.find('!nconn') == 0: + conn['nConn'] = int(lo.split(':')[1]) + nConn = conn['nConn'] + i += 1 + + def _read_user_forcing(self, force_file, nDOF, uf): + """Read user forcing file into *uf* dict.""" + with open(force_file) as fh: + lines = fh.read().splitlines() + + nSteps = -1 + i = 0 + while i < len(lines): + lo = lines[i].lower() + if lo.find('!forcing') == 0: + uf['ForceTimeSeries'] = self._readmat(nSteps, 1 + nDOF, lines, i + 1) + i += nSteps + elif len(lo) > 0 and lo[0] == '!' and lo.find('!nsteps') == 0: + uf['nSteps'] = int(lo.split(':')[1]) + nSteps = uf['nSteps'] + i += 1 + + def _read_conn_forcing(self, fconn_file, nConn, cf): + """Read connection forcing file into *cf* dict.""" + with open(fconn_file) as fh: + lines = fh.read().splitlines() + + nSteps = -1 + i = 0 + while i < len(lines): + lo = lines[i].lower() + if lo.find('!forcing') == 0: + cf['ForceTimeSeries'] = self._readmat(nSteps, 1 + 3 * nConn, lines, i + 1) + i += nSteps + elif len(lo) > 0 and lo[0] == '!' and lo.find('!nsteps') == 0: + cf['nSteps'] = int(lo.split(':')[1]) + nSteps = cf['nSteps'] + i += 1 + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + ep = data['ExtPtfm'] + out_dir = os.path.dirname(file_path) or '.' + + # Write sub-files first + se_name = os.path.basename(ep.get('Red_FileName', 'ExtPtfm_SE.dat')) + se_path = os.path.join(out_dir, se_name) + self._write_superelement(ep['FlexASCII'], se_path) + + if ep.get('HasConnections') and 'Connections' in ep: + conn_name = os.path.basename(ep.get('Conn_FileName', 'ExtPtfm_Conn.dat')) + conn_path = os.path.join(out_dir, conn_name) + self._write_connections(ep['Connections'], conn_path) + + if ep.get('HasUserForcing') and 'UserForcing' in ep: + frc_name = os.path.basename(ep.get('Force_FileName', 'ExtPtfm_UserFrc.dat')) + frc_path = os.path.join(out_dir, frc_name) + self._write_user_forcing(ep['UserForcing'], frc_path) + + if ep.get('HasConnForcing') and 'ConnForcing' in ep: + fcn_name = os.path.basename(ep.get('FConn_FileName', 'ExtPtfm_ConnFrc.dat')) + fcn_path = os.path.join(out_dir, fcn_name) + self._write_conn_forcing(ep['ConnForcing'], fcn_path) + + with open(file_path, 'w') as f: + f.write('---------------------- EXTPTFM INPUT FILE --------------------------------------\n') + f.write('Comment describing the model\n') + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ep['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ep['DT'], 'DT', '- Communication interval for controllers (s) (or "default")\n')) + f.write('{:<22d} {:<11} {:}'.format(ep['IntMethod'], 'IntMethod', '- Integration Method {1:RK4; 2:AB4, 3:ABM4} (switch)\n')) + f.write('---------------------- REDUCTION INPUTS ----------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(ep['RBMod'], 'RBMod', '- Method for handling rigid-body motion\n')) + f.write('{!s:<22} {:<11} {:}'.format('"' + se_name + '"', 'Red_FileName', '- Path of superelement file\n')) + f.write('{:<22d} {:<11} {:}'.format(ep['NActiveDOFList'], 'NActiveDOFList', '- Number of active CB modes\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([str(v) for v in ep['ActiveDOFList']]), 'ActiveDOFList', '- List of active CB mode indices\n')) + f.write('{:<22d} {:<11} {:}'.format(ep['NInitPosList'], 'NInitPosList', '- Number of initial positions\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([str(v) for v in ep['InitPosList']]), 'InitPosList', '- List of initial positions\n')) + f.write('{:<22d} {:<11} {:}'.format(ep['NInitVelList'], 'NInitVelList', '- Number of initial velocities\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([str(v) for v in ep['InitVelList']]), 'InitVelList', '- List of initial velocities\n')) + f.write('---------------------- CONNECTION INPUTS ---------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ep['HasConnections'], 'Connections', '- Flag for connection points\n')) + conn_name_w = os.path.basename(ep.get('Conn_FileName', 'none')) + f.write('{!s:<22} {:<11} {:}'.format('"' + conn_name_w + '"', 'Conn_FileName', '- Path of connection file\n')) + f.write('---------------------- USER FORCING INPUTS -------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ep['HasUserForcing'], 'UserForcing', '- Flag for user forcing\n')) + frc_name_w = os.path.basename(ep.get('Force_FileName', 'none')) + f.write('{!s:<22} {:<11} {:}'.format('"' + frc_name_w + '"', 'Force_FileName', '- Path of user forcing file\n')) + f.write('{!s:<22} {:<11} {:}'.format(ep['HasConnForcing'], 'ConnForcing', '- Flag for connection forcing\n')) + fcn_name_w = os.path.basename(ep.get('FConn_FileName', 'none')) + f.write('{!s:<22} {:<11} {:}'.format('"' + fcn_name_w + '"', 'FConn_FileName', '- Path of connection forcing file\n')) + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ep['SumPrint'], 'SumPrint', '- Print summary data\n')) + f.write('{:<22d} {:<11} {:}'.format(ep['OutFile'], 'OutFile', '- Output switch\n')) + f.write('{!s:<22} {:<11} {:}'.format(ep['TabDelim'], 'TabDelim', '- Tab delimited output\n')) + f.write('{!s:<22} {:<11} {:}'.format(ep['OutFmt'], 'OutFmt', '- Output format\n')) + f.write('{:<22f} {:<11} {:}'.format(ep['TStart'], 'TStart', '- Time to begin output\n')) + f.write(' OutList\n') + if outlist is not None: + emit_outlist(f, outlist, 'ExtPtfm') + else: + for ch in ep.get('_outlist', {}): # standalone fallback (no shared registry) + f.write('"' + ch + '"\n') + f.write('END of input file\n') + + # ------------------------------------------------------------------ + # Sub-file writers + # ------------------------------------------------------------------ + + @staticmethod + def _mat_to_string(M): + return '\n'.join(''.join('{:16.8e}'.format(x) for x in row) for row in M) + + def _write_superelement(self, flex, path): + with open(path, 'w') as f: + f.write('!Dimension: {}\n'.format(flex['nDOF'])) + f.write('\n!Mass Matrix\n') + f.write(self._mat_to_string(flex['MassMatrix'])) + f.write('\n\n!Stiffness Matrix\n') + f.write(self._mat_to_string(flex['StiffnessMatrix'])) + f.write('\n\n!Damping Matrix\n') + f.write(self._mat_to_string(flex['DampingMatrix'])) + f.write('\n\n!Weight constant\n') + f.write(self._mat_to_string(flex['WeightConstant'])) + f.write('\n\n!Weight stiffness matrix\n') + f.write(self._mat_to_string(flex['WeightStiffness'])) + f.write('\n') + + def _write_connections(self, conn, path): + with open(path, 'w') as f: + f.write('!nConn: {}\n'.format(conn['nConn'])) + f.write('\n!Connections\n') + f.write(self._mat_to_string(conn['Position'])) + f.write('\n\n!Displacement\n') + f.write(self._mat_to_string(conn['Displacement'])) + f.write('\n') + + def _write_user_forcing(self, uf, path): + with open(path, 'w') as f: + f.write('!nSteps: {}\n'.format(uf['nSteps'])) + f.write('\n!Forcing:\n') + f.write(self._mat_to_string(uf['ForceTimeSeries'])) + f.write('\n') + + def _write_conn_forcing(self, cf, path): + with open(path, 'w') as f: + f.write('!nSteps: {}\n'.format(cf['nSteps'])) + f.write('\n!Forcing:\n') + f.write(self._mat_to_string(cf['ForceTimeSeries'])) + f.write('\n') diff --git a/openfast_io/openfast_io/io/hydrodyn.py b/openfast_io/openfast_io/io/hydrodyn.py new file mode 100644 index 0000000000..d4075de381 --- /dev/null +++ b/openfast_io/openfast_io/io/hydrodyn.py @@ -0,0 +1,686 @@ +""" +HydroDynIO -- read / write HydroDyn input files. + +Produces ``{'HydroDyn': hd}`` +""" +from __future__ import annotations + +import os +from typing import Any, Dict, Optional, Callable + +import numpy as np + +from .base import ModuleIO +from ..outlist import emit_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, +) + + +def _get_outlist(outlist_dict: dict, keys: list) -> list: + out = [] + for key in keys: + if key in outlist_dict: + out.append(outlist_dict[key]) + return out + + +# ====================================================================== +# HydroDynIO +# ====================================================================== +class HydroDynIO(ModuleIO): + """Read / write HydroDyn input files.""" + + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + hd: Dict[str, Any] = {} + hd_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + f = open(hd_file) + + f.readline() + f.readline() + + hd['Echo'] = bool_read(f.readline().split()[0]) + + # FLOATING PLATFORM + f.readline() + hd['PotMod'] = int_read(f.readline().split()[0]) + hd['ExctnMod'] = int_read(f.readline().split()[0]) + hd['ExctnDisp'] = int_read(f.readline().split()[0]) + hd['ExctnCutOff'] = int_read(f.readline().split()[0]) + hd['PtfmYMod'] = int_read(f.readline().split()[0]) + hd['PtfmRefY'] = float_read(f.readline().split()[0]) + hd['PtfmYCutOff'] = float_read(f.readline().split()[0]) + hd['NExctnHdg'] = int_read(f.readline().split()[0]) + hd['RdtnMod'] = int_read(f.readline().split()[0]) + hd['RdtnTMax'] = float_read(f.readline().split()[0]) + hd['RdtnDT'] = float_read(f.readline().split()[0]) + hd['NBody'] = int_read(f.readline().split()[0]) + hd['NBodyMod'] = int_read(f.readline().split()[0]) + + pot_strings = read_array(f, hd['NBody'], str) + pot_strings = [os.path.normpath(os.path.join(os.path.split(hd_file)[0], ps)) for ps in pot_strings] + hd['PotFile'] = pot_strings + hd['WAMITULEN'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmRefxt'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmRefyt'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmRefzt'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmRefztRot'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmVol0'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmCOBxt'] = read_array(f, hd['NBody'], array_type=float) + hd['PtfmCOByt'] = read_array(f, hd['NBody'], array_type=float) + hd['NAddDOF'] = read_array(f, hd['NBody'], array_type=int) + + # 2ND-ORDER FLOATING PLATFORM FORCES + f.readline() + hd['MnDrift'] = int_read(f.readline().split()[0]) + hd['NewmanApp'] = int_read(f.readline().split()[0]) + hd['DiffQTF'] = int_read(f.readline().split()[0]) + hd['SumQTF'] = int_read(f.readline().split()[0]) + + # PLATFORM ADDITIONAL STIFFNESS AND DAMPING + f.readline() + NBody = hd['NBody'] + if hd['NBodyMod'] == 1: + hd['AddF0'] = [float(f.readline().strip().split()[0]) for _ in range(6 * NBody)] + elif hd['NBodyMod'] > 1: + hd['AddF0'] = [[float(idx) for idx in f.readline().strip().split()[:NBody]] for _ in range(6)] + else: + raise Exception("Invalid value for NBodyMod") + + _mat_rows = 6 * NBody if hd['NBodyMod'] == 1 else 6 + hd['AddCLin'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6 * NBody]] for _ in range(_mat_rows)]) + hd['AddBLin'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6 * NBody]] for _ in range(_mat_rows)]) + hd['AddBQuad'] = np.array([[float(idx) for idx in f.readline().strip().split()[:6 * NBody]] for _ in range(_mat_rows)]) + + # STRIP THEORY OPTIONS + f.readline() + hd['WaveDisp'] = int_read(f.readline().split()[0]) + hd['AMMod'] = int_read(f.readline().split()[0]) + hd['HstMod'] = int_read(f.readline().split()[0]) + + # AXIAL COEFFICIENTS + f.readline() + hd['NAxCoef'] = int_read(f.readline().split()[0]) + n = hd['NAxCoef'] + hd['AxCoefID'] = [None] * n + hd['AxCd'] = [None] * n + hd['AxCa'] = [None] * n + hd['AxCp'] = [None] * n + hd['AxFDMod'] = [None] * n + hd['AxVnCOff'] = [None] * n + hd['AxFDLoFSc'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['AxCoefID'][i] = int(ln[0]) + hd['AxCd'][i] = float_read(ln[1]) + hd['AxCa'][i] = float_read(ln[2]) + hd['AxCp'][i] = float_read(ln[3]) + hd['AxFDMod'][i] = float_read(ln[4]) + hd['AxVnCOff'][i] = float_read(ln[5]) + hd['AxFDLoFSc'][i] = float_read(ln[6]) + + # MEMBER JOINTS + f.readline() + hd['NJoints'] = int_read(f.readline().split()[0]) + n = hd['NJoints'] + hd['JointID'] = [None] * n + hd['Jointxi'] = [None] * n + hd['Jointyi'] = [None] * n + hd['Jointzi'] = [None] * n + hd['JointAxID'] = [None] * n + hd['JointOvrlp'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['JointID'][i] = int(ln[0]) + hd['Jointxi'][i] = float(ln[1]) + hd['Jointyi'][i] = float(ln[2]) + hd['Jointzi'][i] = float(ln[3]) + hd['JointAxID'][i] = int(ln[4]) + hd['JointOvrlp'][i] = int(ln[5]) + + # CIRCULAR MEMBER CROSS-SECTION PROPERTIES + f.readline() + hd['NPropSetsCyl'] = int_read(f.readline().split()[0]) + n = hd['NPropSetsCyl'] + hd['CylPropSetID'] = [None] * n + hd['CylPropD'] = [None] * n + hd['CylPropThck'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['CylPropSetID'][i] = int(ln[0]) + hd['CylPropD'][i] = float(ln[1]) + hd['CylPropThck'][i] = float(ln[2]) + + # RECTANGULAR MEMBER CROSS-SECTION PROPERTIES + f.readline() + hd['NPropSetsRec'] = int_read(f.readline().split()[0]) + n = hd['NPropSetsRec'] + hd['RecPropSetID'] = [None] * n + hd['RecPropA'] = [None] * n + hd['RecPropB'] = [None] * n + hd['RecPropThck'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['RecPropSetID'][i] = int(ln[0]) + hd['RecPropA'][i] = float(ln[1]) + hd['RecPropB'][i] = float(ln[2]) + hd['RecPropThck'][i] = float(ln[3]) + + # SIMPLE CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline(); f.readline(); f.readline() + ln = f.readline().split() + for j, key in enumerate(['CylSimplCd', 'CylSimplCdMG', 'CylSimplCa', 'CylSimplCaMG', + 'CylSimplCp', 'CylSimplCpMG', 'CylSimplAxCd', 'CylSimplAxCdMG', + 'CylSimplAxCa', 'CylSimplAxCaMG', 'CylSimplAxCp', 'CylSimplAxCpMG', + 'CylSimplCb', 'CylSimplCbMG']): + hd[key] = float_read(ln[j]) + + # SIMPLE RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline(); f.readline(); f.readline() + ln = f.readline().split() + for j, key in enumerate(['RecSimplCdA', 'RecSimplCdAMG', 'RecSimplCdB', 'RecSimplCdBMG', + 'RecSimplCaA', 'RecSimplCaAMG', 'RecSimplCaB', 'RecSimplCaBMG', + 'RecSimplCp', 'RecSimplCpMG', 'RecSimplAxCd', 'RecSimplAxCdMG', + 'RecSimplAxCa', 'RecSimplAxCaMG', 'RecSimplAxCp', 'RecSimplAxCpMG', + 'RecSimplCb', 'RecSimplCbMG']): + hd[key] = float_read(ln[j]) + + # DEPTH-BASED CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline() + hd['NCoefDpthCyl'] = int_read(f.readline().split()[0]) + n = hd['NCoefDpthCyl'] + cyl_dpth_keys = ['CylDpth', 'CylDpthCd', 'CylDpthCdMG', 'CylDpthCa', 'CylDpthCaMG', + 'CylDpthCp', 'CylDpthCpMG', 'CylDpthAxCd', 'CylDpthAxCdMG', + 'CylDpthAxCa', 'CylDpthAxCaMG', 'CylDpthAxCp', 'CylDpthAxCpMG', + 'CylDpthCb', 'CylDpthCbMG'] + for k in cyl_dpth_keys: + hd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + for j, k in enumerate(cyl_dpth_keys): + hd[k][i] = float_read(ln[j]) + + # DEPTH-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline() + hd['NCoefDpthRec'] = int_read(f.readline().split()[0]) + n = hd['NCoefDpthRec'] + rec_dpth_keys = ['RecDpth', 'RecDpthCdA', 'RecDpthCdAMG', 'RecDpthCdB', 'RecDpthCdBMG', + 'RecDpthCaA', 'RecDpthCaAMG', 'RecDpthCaB', 'RecDpthCaBMG', + 'RecDpthCp', 'RecDpthCpMG', 'RecDpthAxCd', 'RecDpthAxCdMG', + 'RecDpthAxCa', 'RecDpthAxCaMG', 'RecDpthAxCp', 'RecDpthAxCpMG', + 'RecDpthCb', 'RecDpthCbMG'] + for k in rec_dpth_keys: + hd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + for j, k in enumerate(rec_dpth_keys): + hd[k][i] = float_read(ln[j]) + + # MEMBER-BASED CIRCULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline() + hd['NCoefMembersCyl'] = int_read(f.readline().split()[0]) + n = hd['NCoefMembersCyl'] + cyl_mem_keys = ['MemberID_HydCCyl', + 'CylMemberCd1', 'CylMemberCd2', 'CylMemberCdMG1', 'CylMemberCdMG2', + 'CylMemberCa1', 'CylMemberCa2', 'CylMemberCaMG1', 'CylMemberCaMG2', + 'CylMemberCp1', 'CylMemberCp2', 'CylMemberCpMG1', 'CylMemberCpMG2', + 'CylMemberAxCd1', 'CylMemberAxCd2', 'CylMemberAxCdMG1', 'CylMemberAxCdMG2', + 'CylMemberAxCa1', 'CylMemberAxCa2', 'CylMemberAxCaMG1', 'CylMemberAxCaMG2', + 'CylMemberAxCp1', 'CylMemberAxCp2', 'CylMemberAxCpMG1', 'CylMemberAxCpMG2', + 'CylMemberCb1', 'CylMemberCb2', 'CylMemberCbMG1', 'CylMemberCbMG2'] + for k in cyl_mem_keys: + hd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['MemberID_HydCCyl'][i] = int(ln[0]) + for j, k in enumerate(cyl_mem_keys[1:], 1): + hd[k][i] = float_read(ln[j]) + + # MEMBER-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS + f.readline() + hd['NCoefMembersRec'] = int_read(f.readline().split()[0]) + n = hd['NCoefMembersRec'] + rec_mem_keys = ['MemberID_HydCRec', + 'RecMemberCdA1', 'RecMemberCdA2', 'RecMemberCdAMG1', 'RecMemberCdAMG2', + 'RecMemberCdB1', 'RecMemberCdB2', 'RecMemberCdBMG1', 'RecMemberCdBMG2', + 'RecMemberCaA1', 'RecMemberCaA2', 'RecMemberCaAMG1', 'RecMemberCaAMG2', + 'RecMemberCaB1', 'RecMemberCaB2', 'RecMemberCaBMG1', 'RecMemberCaBMG2', + 'RecMemberCp1', 'RecMemberCp2', 'RecMemberCpMG1', 'RecMemberCpMG2', + 'RecMemberAxCd1', 'RecMemberAxCd2', 'RecMemberAxCdMG1', 'RecMemberAxCdMG2', + 'RecMemberAxCa1', 'RecMemberAxCa2', 'RecMemberAxCaMG1', 'RecMemberAxCaMG2', + 'RecMemberAxCp1', 'RecMemberAxCp2', 'RecMemberAxCpMG1', 'RecMemberAxCpMG2', + 'RecMemberCb1', 'RecMemberCb2', 'RecMemberCbMG1', 'RecMemberCbMG2'] + for k in rec_mem_keys: + hd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['MemberID_HydCRec'][i] = int(ln[0]) + for j, k in enumerate(rec_mem_keys[1:], 1): + hd[k][i] = float_read(ln[j]) + + # MEMBERS + f.readline() + hd['NMembers'] = int_read(f.readline().split()[0]) + n = hd['NMembers'] + mem_keys = ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', + 'MSecGeom', 'MSpinOrient', 'MDivSize', 'MCoefMod', 'MHstLMod', 'PropPot'] + for k in mem_keys: + hd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['MemberID'][i] = int(ln[0]) + hd['MJointID1'][i] = int(ln[1]) + hd['MJointID2'][i] = int(ln[2]) + hd['MPropSetID1'][i] = int(ln[3]) + hd['MPropSetID2'][i] = int(ln[4]) + hd['MSecGeom'][i] = int(ln[5]) + hd['MSpinOrient'][i] = float(ln[6]) + hd['MDivSize'][i] = float(ln[7]) + hd['MCoefMod'][i] = int(ln[8]) + hd['MHstLMod'][i] = int(ln[9]) + hd['PropPot'][i] = bool_read(ln[10]) + + # FILLED MEMBERS + f.readline() + hd['NFillGroups'] = int_read(f.readline().split()[0]) + n = hd['NFillGroups'] + hd['FillNumM'] = [None] * n + hd['FillMList'] = [None] * n + hd['FillFSLoc'] = [None] * n + hd['FillDens'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + n_fill = int(ln[0]) + hd['FillNumM'][i] = n_fill + hd['FillMList'][i] = [int(j) for j in ln[1:1 + n_fill]] + hd['FillFSLoc'][i] = float(ln[n_fill + 1]) + if ln[n_fill + 2] == 'DEFAULT': + hd['FillDens'][i] = 'DEFAULT' + else: + hd['FillDens'][i] = float(ln[n_fill + 2]) + + # MARINE GROWTH + f.readline() + hd['NMGDepths'] = int_read(f.readline().split()[0]) + n = hd['NMGDepths'] + hd['MGDpth'] = [None] * n + hd['MGThck'] = [None] * n + hd['MGDens'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['MGDpth'][i] = float(ln[0]) + hd['MGThck'][i] = float(ln[1]) + hd['MGDens'][i] = float(ln[2]) + + # MEMBER OUTPUT LIST + f.readline() + hd['NMOutputs'] = int_read(f.readline().split()[0]) + n = hd['NMOutputs'] + hd['MemberID_out'] = [None] * n + hd['NOutLoc'] = [None] * n + hd['NodeLocs'] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + hd['MemberID_out'][i] = int(ln[0]) + hd['NOutLoc'][i] = int(ln[1]) + hd['NodeLocs'][i] = float(ln[2]) + + # JOINT OUTPUT LIST + f.readline() + hd['NJOutputs'] = int_read(f.readline().split()[0]) + if int(hd['NJOutputs']) > 0: + hd['JOutLst'] = [int(idx.strip()) for idx in f.readline().split('JOutLst')[0].split(',')] + else: + f.readline() + hd['JOutLst'] = [0] + + # OUTPUT + f.readline() + hd['HDSum'] = bool_read(f.readline().split()[0]) + hd['OutAll'] = bool_read(f.readline().split()[0]) + hd['OutSwtch'] = int_read(f.readline().split()[0]) + hd['OutFmt'] = quoted_read(f.readline().split()[0]) + hd['OutSFmt'] = quoted_read(f.readline().split()[0]) + + # Outlist + f.readline() + if read_outlist_fn is not None: + read_outlist_fn(f, 'HydroDyn') + + f.close() + return {'HydroDyn': hd} + + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + hd = data['HydroDyn'] + + with open(file_path, 'w') as f: + f.write('------- HydroDyn Input File --------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('{!s:<22} {:<11} {:}'.format(hd['Echo'], 'Echo', '- Echo the input file data (flag)\n')) + f.write('---------------------- FLOATING PLATFORM --------------------------------------- [unused with WaveMod=6]\n') + f.write('{:<22d} {:<11} {:}'.format(hd['PotMod'], 'PotMod', '- Potential-flow model {0: none=no potential flow, 1: frequency-to-time-domain transforms based on WAMIT output, 2: fluid-impulse theory (FIT)} (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['ExctnMod'], 'ExctnMod', '- Wave-excitation model (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['ExctnDisp'], 'ExctnDisp', '- Method of computing Wave Excitation (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['ExctnCutOff'], 'ExctnCutOff', '- Cutoff frequency (Hz)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['PtfmYMod'], 'PtfmYMod', '- Model for large platform yaw offset (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['PtfmRefY'], 'PtfmRefY', '- Platform reference yaw offset (deg)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['PtfmYCutOff'], 'PtfmYCutOff', '- Cutoff frequency for PRP yaw filtering (Hz)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['NExctnHdg'], 'NExctnHdg', '- Number of platform yaw/heading angles (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['RdtnMod'], 'RdtnMod', '- Radiation memory-effect model (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['RdtnTMax'], 'RdtnTMax', '- Analysis time for wave radiation kernel calculations (sec)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['RdtnDT'], 'RdtnDT', '- Time step for wave radiation kernel calculations (sec)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['NBody'], 'NBody', '- Number of WAMIT bodies (-)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['NBodyMod'], 'NBodyMod', '- Body coupling model (switch)\n')) + pot = hd['PotFile'] + f.write('{:<22} {:<11} {:}'.format('"{}"'.format('", "'.join(pot) if isinstance(pot, list) else pot), 'PotFile', '- Root name of potential-flow model data\n')) + + for arr_key, label, desc in [ + ('WAMITULEN', 'WAMITULEN', '- Characteristic body length scale (m)'), + ('PtfmRefxt', 'PtfmRefxt', '- xt offset of body reference point (m)'), + ('PtfmRefyt', 'PtfmRefyt', '- yt offset of body reference point (m)'), + ('PtfmRefzt', 'PtfmRefzt', '- zt offset of body reference point (m)'), + ('PtfmRefztRot', 'PtfmRefztRot', '- Rotation about zt (deg)'), + ('PtfmVol0', 'PtfmVol0', '- Displaced volume (m^3)'), + ('PtfmCOBxt', 'PtfmCOBxt', '- xt offset of COB (m)'), + ('PtfmCOByt', 'PtfmCOByt', '- yt offset of COB (m)'), + ('NAddDOF', 'NAddDOF', '- Number of additional DOF (-)'), + ]: + f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in hd[arr_key]]), label, desc + '\n')) + + f.write('---------------------- 2ND-ORDER FLOATING PLATFORM FORCES ----------------------\n') + f.write('{:<22} {:<11} {:}'.format(hd['MnDrift'], 'MnDrift', '- Mean-drift 2nd-order forces\n')) + f.write('{:<22} {:<11} {:}'.format(hd['NewmanApp'], 'NewmanApp', '- Newman approximation\n')) + f.write('{:<22} {:<11} {:}'.format(hd['DiffQTF'], 'DiffQTF', '- Full difference-frequency QTF\n')) + f.write('{:<22} {:<11} {:}'.format(hd['SumQTF'], 'SumQTF', '- Full summation-frequency QTF\n')) + + f.write('---------------------- PLATFORM ADDITIONAL STIFFNESS AND DAMPING --------------\n') + _n_f0 = 6 * hd['NBody'] if hd['NBodyMod'] == 1 else 6 + for j in range(_n_f0): + val = hd['AddF0'][j] + if isinstance(val, float): + ln = '{:14} '.format(val) + elif isinstance(val, (list, np.ndarray)): + ln = '{:14} '.format(' '.join([f'{v}' for v in val])) + else: + ln = '{:14} '.format(val) + if j == 0: + ln += 'AddF0 - Additional preload (N, N-m)\n' + else: + ln += '\n' + f.write(ln) + for mat_key, mat_label in [('AddCLin', 'AddCLin'), ('AddBLin', 'AddBLin'), ('AddBQuad', 'AddBQuad')]: + n_mat_rows = len(hd[mat_key]) + for j in range(n_mat_rows): + try: + ln = " ".join(['{:14}'.format(i) for i in hd[mat_key][j, :]]) + except Exception: + ln = " ".join(['{:14}'.format(i) for i in hd[mat_key][j]]) + if j == 0: + ln += f" {mat_label}\n" + else: + ln += "\n" + f.write(ln) + + f.write('---------------------- STRIP THEORY OPTIONS --------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(hd['WaveDisp'], 'WaveDisp', '- Wave kinematics method (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['AMMod'], 'AMMod', '- Added-mass force method (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['HstMod'], 'HstMod', '- Hydrostatic loads method (switch)\n')) + + # AXIAL COEFFICIENTS + f.write('---------------------- AXIAL COEFFICIENTS --------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(hd['NAxCoef'], 'NAxCoef', '- Number of axial coefficients (-)\n')) + ax_hdrs = ['AxCoefID', 'AxCd', 'AxCa', 'AxCp', 'AxFDMod', 'AxVnCOff', 'AxFDLoFSc'] + f.write(" ".join(['{:^11s}'.format(h) for h in ax_hdrs]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in ax_hdrs]) + '\n') + for i in range(hd['NAxCoef']): + ln = ['{:^11d}'.format(hd['AxCoefID'][i])] + for k in ax_hdrs[1:]: + ln.append('{:^11}'.format(hd[k][i])) + f.write(" ".join(ln) + '\n') + + # MEMBER JOINTS + f.write('---------------------- MEMBER JOINTS -------------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(hd['NJoints'], 'NJoints', '- Number of joints (-)\n')) + jt_hdrs = ['JointID', 'Jointxi', 'Jointyi', 'Jointzi', 'JointAxID', 'JointOvrlp'] + f.write(" ".join(['{:^11s}'.format(h) for h in jt_hdrs]) + '\n') + f.write(" ".join(['{:^11s}'.format(u) for u in ['(-)', '(m)', '(m)', '(m)', '(-)', '(switch)']]) + '\n') + for i in range(hd['NJoints']): + ln = ['{:^11d}'.format(hd['JointID'][i]), + '{:^11}'.format(hd['Jointxi'][i]), '{:^11}'.format(hd['Jointyi'][i]), + '{:^11}'.format(hd['Jointzi'][i]), + '{:^11d}'.format(hd['JointAxID'][i]), '{:^11d}'.format(hd['JointOvrlp'][i])] + f.write(" ".join(ln) + '\n') + + # CYLINDRICAL MEMBER CROSS-SECTION PROPERTIES + f.write('---------------------- CYLINDRICAL MEMBER CROSS-SECTION PROPERTIES -------------------------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NPropSetsCyl'], 'NPropSetsCyl', '- Number of cylindrical member property sets (-)\n')) + f.write(" ".join(['{:^11s}'.format(h) for h in ['PropSetID', 'PropD', 'PropThck']]) + '\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['(-)', '(m)', '(m)']]) + '\n') + for i in range(hd['NPropSetsCyl']): + f.write(" ".join(['{:^11d}'.format(hd['CylPropSetID'][i]), + '{:^11}'.format(hd['CylPropD'][i]), + '{:^11}'.format(hd['CylPropThck'][i])]) + '\n') + + # RECTANGULAR MEMBER CROSS-SECTION PROPERTIES + f.write('---------------------- RECTANGULAR MEMBER CROSS-SECTION PROPERTIES -------------------------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NPropSetsRec'], 'NPropSetsRec', '- Number of rectangular member property sets (-)\n')) + f.write(" ".join(['{:^11s}'.format(h) for h in ['PropSetID', 'PropA', 'PropB', 'PropThck']]) + '\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['(-)', '(m)', '(m)', '(m)']]) + '\n') + for i in range(hd['NPropSetsRec']): + f.write(" ".join(['{:^11d}'.format(hd['RecPropSetID'][i]), + '{:^11}'.format(hd['RecPropA'][i]), + '{:^11}'.format(hd['RecPropB'][i]), + '{:^11}'.format(hd['RecPropThck'][i])]) + '\n') + + # SIMPLE CYLINDRICAL HYDRO COEFFICIENTS + f.write('---------------------- SIMPLE CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 1) --------------\n') + cyl_simpl_keys = ['CylSimplCd', 'CylSimplCdMG', 'CylSimplCa', 'CylSimplCaMG', 'CylSimplCp', 'CylSimplCpMG', + 'CylSimplAxCd', 'CylSimplAxCdMG', 'CylSimplAxCa', 'CylSimplAxCaMG', + 'CylSimplAxCp', 'CylSimplAxCpMG', 'CylSimplCb', 'CylSimplCbMG'] + hdr_names = ['SimplCd', 'SimplCdMG', 'SimplCa', 'SimplCaMG', 'SimplCp', 'SimplCpMG', + 'SimplAxCd', 'SimplAxCdMG', 'SimplAxCa', 'SimplAxCaMG', 'SimplAxCp', 'SimplAxCpMG', 'SimplCb', 'SimplCbMG'] + f.write(" ".join(['{:^11s}'.format(h) for h in hdr_names]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in hdr_names]) + '\n') + f.write(" ".join(['{:^11}'.format(hd[k]) for k in cyl_simpl_keys]) + '\n') + + # SIMPLE RECTANGULAR HYDRO COEFFICIENTS + f.write('---------------------- SIMPLE RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 1) --------------\n') + rec_simpl_keys = ['RecSimplCdA', 'RecSimplCdAMG', 'RecSimplCdB', 'RecSimplCdBMG', + 'RecSimplCaA', 'RecSimplCaAMG', 'RecSimplCaB', 'RecSimplCaBMG', + 'RecSimplCp', 'RecSimplCpMG', 'RecSimplAxCd', 'RecSimplAxCdMG', + 'RecSimplAxCa', 'RecSimplAxCaMG', 'RecSimplAxCp', 'RecSimplAxCpMG', + 'RecSimplCb', 'RecSimplCbMG'] + rec_hdr = ['SimplCdA', 'SimplCdAMG', 'SimplCdB', 'SimplCdBMG', 'SimplCaA', 'SimplCaAMG', + 'SimplCaB', 'SimplCaBMG', 'SimplCp', 'SimplCpMG', 'SimplAxCd', 'SimplAxCdMG', + 'SimplAxCa', 'SimplAxCaMG', 'SimplAxCp', 'SimplAxCpMG', 'SimplCb', 'SimplCbMG'] + f.write(" ".join(['{:^11s}'.format(h) for h in rec_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in rec_hdr]) + '\n') + f.write(" ".join(['{:^11}'.format(hd[k]) for k in rec_simpl_keys]) + '\n') + + # DEPTH-BASED CYL COEFFICIENTS + f.write('---------------------- DEPTH-BASED CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 2) ---------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NCoefDpthCyl'], 'NCoefDpthCyl', '- Number of depth-dependent cylindrical-member coefficients (-)\n')) + cyl_dpth_hdr = ['Dpth', 'DpthCd', 'DpthCdMG', 'DpthCa', 'DpthCaMG', 'DpthCp', 'DpthCpMG', + 'DpthAxCd', 'DpthAxCdMG', 'DpthAxCa', 'DpthAxCaMG', 'DpthAxCp', 'DpthAxCpMG', 'DpthCb', 'DpthCbMG'] + f.write(" ".join(['{:^11s}'.format(h) for h in cyl_dpth_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in cyl_dpth_hdr]) + '\n') + cyl_dpth_keys_list = ['CylDpth', 'CylDpthCd', 'CylDpthCdMG', 'CylDpthCa', 'CylDpthCaMG', + 'CylDpthCp', 'CylDpthCpMG', 'CylDpthAxCd', 'CylDpthAxCdMG', + 'CylDpthAxCa', 'CylDpthAxCaMG', 'CylDpthAxCp', 'CylDpthAxCpMG', + 'CylDpthCb', 'CylDpthCbMG'] + for i in range(hd['NCoefDpthCyl']): + f.write(" ".join(['{:^11}'.format(hd[k][i]) for k in cyl_dpth_keys_list]) + '\n') + + # DEPTH-BASED REC COEFFICIENTS + f.write('---------------------- DEPTH-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 2) ---------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NCoefDpthRec'], 'NCoefDpthRec', '- Number of depth-dependent rectangular-member coefficients (-)\n')) + rec_dpth_hdr = ['Dpth', 'DpthCdA', 'DpthCdAMG', 'DpthCdB', 'DpthCdBMG', + 'DpthCaA', 'DpthCaAMG', 'DpthCaB', 'DpthCaBMG', 'DpthCp', 'DpthCpMG', + 'DpthAxCd', 'DpthAxCdMG', 'DpthAxCa', 'DpthAxCaMG', 'DpthAxCp', 'DpthAxCpMG', 'DpthCb', 'DpthCbMG'] + f.write(" ".join(['{:^11s}'.format(h) for h in rec_dpth_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in rec_dpth_hdr]) + '\n') + rec_dpth_keys_list = ['RecDpth', 'RecDpthCdA', 'RecDpthCdAMG', 'RecDpthCdB', 'RecDpthCdBMG', + 'RecDpthCaA', 'RecDpthCaAMG', 'RecDpthCaB', 'RecDpthCaBMG', + 'RecDpthCp', 'RecDpthCpMG', 'RecDpthAxCd', 'RecDpthAxCdMG', + 'RecDpthAxCa', 'RecDpthAxCaMG', 'RecDpthAxCp', 'RecDpthAxCpMG', + 'RecDpthCb', 'RecDpthCbMG'] + for i in range(hd['NCoefDpthRec']): + f.write(" ".join(['{:^11}'.format(hd[k][i]) for k in rec_dpth_keys_list]) + '\n') + + # MEMBER-BASED CYL COEFFICIENTS + f.write('---------------------- MEMBER-BASED CYLINDRICAL-MEMBER HYDRODYNAMIC COEFFICIENTS (model 3) --------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NCoefMembersCyl'], 'NCoefMembersCyl', '- Number of member-based cylindrical-member coefficients (-)\n')) + cyl_mem_hdr = ['MemberID', 'MemberCd1', 'MemberCd2', 'MemberCdMG1', 'MemberCdMG2', + 'MemberCa1', 'MemberCa2', 'MemberCaMG1', 'MemberCaMG2', + 'MemberCp1', 'MemberCp2', 'MemberCpMG1', 'MemberCpMG2', + 'MemberAxCd1', 'MemberAxCd2', 'MemberAxCdMG1', 'MemberAxCdMG2', + 'MemberAxCa1', 'MemberAxCa2', 'MemberAxCaMG1', 'MemberAxCaMG2', + 'MemberAxCp1', 'MemberAxCp2', 'MemberAxCpMG1', 'MemberAxCpMG2', + 'MemberCb1', 'MemberCb2', 'MemberCbMG1', 'MemberCbMG2'] + f.write(" ".join(['{:^11s}'.format(h) for h in cyl_mem_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in cyl_mem_hdr]) + '\n') + cyl_mem_w_keys = ['MemberID_HydCCyl', + 'CylMemberCd1', 'CylMemberCd2', 'CylMemberCdMG1', 'CylMemberCdMG2', + 'CylMemberCa1', 'CylMemberCa2', 'CylMemberCaMG1', 'CylMemberCaMG2', + 'CylMemberCp1', 'CylMemberCp2', 'CylMemberCpMG1', 'CylMemberCpMG2', + 'CylMemberAxCd1', 'CylMemberAxCd2', 'CylMemberAxCdMG1', 'CylMemberAxCdMG2', + 'CylMemberAxCa1', 'CylMemberAxCa2', 'CylMemberAxCaMG1', 'CylMemberAxCaMG2', + 'CylMemberAxCp1', 'CylMemberAxCp2', 'CylMemberAxCpMG1', 'CylMemberAxCpMG2', + 'CylMemberCb1', 'CylMemberCb2', 'CylMemberCbMG1', 'CylMemberCbMG2'] + for i in range(hd['NCoefMembersCyl']): + ln = ['{:^11d}'.format(hd['MemberID_HydCCyl'][i])] + for k in cyl_mem_w_keys[1:]: + ln.append('{:^11}'.format(hd[k][i])) + f.write(" ".join(ln) + '\n') + + # MEMBER-BASED REC COEFFICIENTS + f.write('---------------------- MEMBER-BASED RECTANGULAR-MEMBER HYDRODYNAMIC COEFFICIENTS (model 3) --------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NCoefMembersRec'], 'NCoefMembersRec', '- Number of member-based rectangular-member coefficients (-)\n')) + rec_mem_hdr = ['MemberID', 'MemberCdA1', 'MemberCdA2', 'MemberCdAMG1', 'MemberCdAMG2', + 'MemberCdB1', 'MemberCdB2', 'MemberCdBMG1', 'MemberCdBMG2', + 'MemberCaA1', 'MemberCaA2', 'MemberCaAMG1', 'MemberCaAMG2', + 'MemberCaB1', 'MemberCaB2', 'MemberCaBMG1', 'MemberCaBMG2', + 'MemberCp1', 'MemberCp2', 'MemberCpMG1', 'MemberCpMG2', + 'MemberAxCd1', 'MemberAxCd2', 'MemberAxCdMG1', 'MemberAxCdMG2', + 'MemberAxCa1', 'MemberAxCa2', 'MemberAxCaMG1', 'MemberAxCaMG2', + 'MemberAxCp1', 'MemberAxCp2', 'MemberAxCpMG1', 'MemberAxCpMG2', + 'MemberCb1', 'MemberCb2', 'MemberCbMG1', 'MemberCbMG2'] + f.write(" ".join(['{:^11s}'.format(h) for h in rec_mem_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in rec_mem_hdr]) + '\n') + rec_mem_w_keys = ['MemberID_HydCRec', + 'RecMemberCdA1', 'RecMemberCdA2', 'RecMemberCdAMG1', 'RecMemberCdAMG2', + 'RecMemberCdB1', 'RecMemberCdB2', 'RecMemberCdBMG1', 'RecMemberCdBMG2', + 'RecMemberCaA1', 'RecMemberCaA2', 'RecMemberCaAMG1', 'RecMemberCaAMG2', + 'RecMemberCaB1', 'RecMemberCaB2', 'RecMemberCaBMG1', 'RecMemberCaBMG2', + 'RecMemberCp1', 'RecMemberCp2', 'RecMemberCpMG1', 'RecMemberCpMG2', + 'RecMemberAxCd1', 'RecMemberAxCd2', 'RecMemberAxCdMG1', 'RecMemberAxCdMG2', + 'RecMemberAxCa1', 'RecMemberAxCa2', 'RecMemberAxCaMG1', 'RecMemberAxCaMG2', + 'RecMemberAxCp1', 'RecMemberAxCp2', 'RecMemberAxCpMG1', 'RecMemberAxCpMG2', + 'RecMemberCb1', 'RecMemberCb2', 'RecMemberCbMG1', 'RecMemberCbMG2'] + for i in range(hd['NCoefMembersRec']): + ln = ['{:^11d}'.format(hd['MemberID_HydCRec'][i])] + for k in rec_mem_w_keys[1:]: + ln.append('{:^11}'.format(hd[k][i])) + f.write(" ".join(ln) + '\n') + + # MEMBERS + f.write('-------------------- MEMBERS -------------------------------------------------\n') + f.write('{:<11d} {:<11} {:}'.format(hd['NMembers'], 'NMembers', '- Number of members (-)\n')) + mem_hdr = ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', + 'MSecGeom', 'MSpinOrient', 'MDivSize', 'MCoefMod', 'MHstLMod', 'PropPot'] + f.write(" ".join(['{:^11s}'.format(h) for h in mem_hdr]) + '\n') + f.write(" ".join(['{:^11s}'.format(u) for u in ['(-)', '(-)', '(-)', '(-)', '(-)', '(switch)', '(deg)', '(m)', '(switch)', '(switch)', '(flag)']]) + '\n') + for i in range(hd['NMembers']): + ln = ['{:^11d}'.format(hd['MemberID'][i]), + '{:^11d}'.format(hd['MJointID1'][i]), '{:^11d}'.format(hd['MJointID2'][i]), + '{:^11d}'.format(hd['MPropSetID1'][i]), '{:^11d}'.format(hd['MPropSetID2'][i]), + '{:^11d}'.format(hd['MSecGeom'][i]), '{:^11}'.format(hd['MSpinOrient'][i]), + '{:^11}'.format(hd['MDivSize'][i]), + '{:^11d}'.format(hd['MCoefMod'][i]), '{:^11d}'.format(hd['MHstLMod'][i]), + '{!s:^11}'.format(hd['PropPot'][i])] + f.write(" ".join(ln) + '\n') + + # FILLED MEMBERS + f.write("---------------------- FILLED MEMBERS ------------------------------------------\n") + f.write('{:<11d} {:<11} {:}'.format(hd['NFillGroups'], 'NFillGroups', '- Number of filled member groups (-)\n')) + f.write(" ".join(['{:^11s}'.format(h) for h in ['FillNumM', 'FillMList', 'FillFSLoc', 'FillDens']]) + '\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['(-)', '(-)', '(m)', '(kg/m^3)']]) + '\n') + for i in range(hd['NFillGroups']): + ln = ['{:^11d}'.format(hd['FillNumM'][i]), + " ".join(['%d' % j for j in hd['FillMList'][i]]), + '{:^11}'.format(hd['FillFSLoc'][i]), + '{:^11}'.format(hd['FillDens'][i])] + f.write(" ".join(ln) + '\n') + + # MARINE GROWTH + f.write("---------------------- MARINE GROWTH -------------------------------------------\n") + f.write('{:<11d} {:<11} {:}'.format(hd['NMGDepths'], 'NMGDepths', '- Number of marine-growth depths specified (-)\n')) + f.write(" ".join(['{:^11s}'.format(h) for h in ['MGDpth', 'MGThck', 'MGDens']]) + '\n') + f.write(" ".join(['{:^11s}'.format(h) for h in ['(m)', '(m)', '(kg/m^3)']]) + '\n') + for i in range(hd['NMGDepths']): + f.write(" ".join(['{:^11}'.format(hd['MGDpth'][i]), + '{:^11}'.format(hd['MGThck'][i]), + '{:^11}'.format(hd['MGDens'][i])]) + '\n') + + # MEMBER OUTPUT LIST + f.write("---------------------- MEMBER OUTPUT LIST --------------------------------------\n") + f.write('{:<11d} {:<11} {:}'.format(hd['NMOutputs'], 'NMOutputs', '- Number of member outputs (-)\n')) + f.write(" ".join(['{:^11s}'.format(h) for h in ['MemberID_out', 'NOutLoc', 'NodeLocs']]) + '\n') + f.write(" ".join(['{:^11s}'.format('(-)') for _ in range(3)]) + '\n') + for i in range(hd['NMOutputs']): + f.write(" ".join(['{:^11d}'.format(hd['MemberID_out'][i]), + '{:^11d}'.format(hd['NOutLoc'][i]), + '{:^11}'.format(hd['NodeLocs'][i])]) + '\n') + + # JOINT OUTPUT LIST + f.write("---------------------- JOINT OUTPUT LIST ---------------------------------------\n") + f.write('{:<22d} {:<11} {:}'.format(hd['NJOutputs'], 'NJOutputs', '- Number of joint outputs\n')) + f.write('{:<22} {:<11} {:}'.format(" ".join(["%d" % i for i in hd['JOutLst']]), 'JOutLst', '- List of JointIDs\n')) + + # OUTPUT + f.write("---------------------- OUTPUT --------------------------------------------------\n") + f.write('{!s:<22} {:<11} {:}'.format(hd['HDSum'], 'HDSum', '- Output a summary file [flag]\n')) + f.write('{!s:<22} {:<11} {:}'.format(hd['OutAll'], 'OutAll', '- Output all member and joint loads [flag]\n')) + f.write('{:<22d} {:<11} {:}'.format(hd['OutSwtch'], 'OutSwtch', '- Output channels to file (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(hd['OutFmt'], 'OutFmt', '- Output format\n')) + f.write('{:<22} {:<11} {:}'.format(hd['OutSFmt'], 'OutSFmt', '- Output format for header strings\n')) + + f.write('---------------------- OUTPUT CHANNELS -----------------------------------------\n') + if outlist is not None: + emit_outlist(f, outlist, 'HydroDyn') + f.write('END of output channels and end of file.\n') diff --git a/openfast_io/openfast_io/io/inflowwind.py b/openfast_io/openfast_io/io/inflowwind.py new file mode 100644 index 0000000000..b426c84e2e --- /dev/null +++ b/openfast_io/openfast_io/io/inflowwind.py @@ -0,0 +1,239 @@ +"""InflowWind module IO — reads and writes InflowWind input files. + +Extracted from FAST_reader.py and FAST_writer.py. +""" +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np + +from .base import ModuleIO +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, +) + + +def _get_outlist(outlist_dict, channel_list): + def loop_dict(vartree, outlist_i): + for var in vartree.keys(): + if isinstance(vartree[var], dict): + loop_dict(vartree[var], outlist_i) + else: + if vartree[var]: + outlist_i.append(var) + return outlist_i + + if not channel_list: + channel_list = outlist_dict.keys() + outlist = [] + for var in channel_list: + var = var.replace(' ', '') + outlist_i = loop_dict(outlist_dict[var], []) + if outlist_i: + outlist.append(sorted(outlist_i)) + return outlist + + +class InflowWindIO(ModuleIO): + """Reads and writes InflowWind v3.01+ input files. + + read() returns:: + + {'InflowWind': { ... all params ... }} + + write() accepts data with key 'InflowWind'. + """ + + def read(self, file_path: Path, base_dir: Path, *, + outlist: dict | None = None, + read_outlist_fn=None) -> dict: + ifw = {} + file_path = str(file_path) + inflow_dir = os.path.dirname(file_path) + + f = open(file_path) + f.readline(); f.readline(); f.readline() + + # Header + ifw['Echo'] = bool_read(f.readline().split()[0]) + ifw['WindType'] = int(f.readline().split()[0]) + ifw['PropagationDir'] = float_read(f.readline().split()[0]) + ifw['VFlowAng'] = float_read(f.readline().split()[0]) + ifw['VelInterpCubic'] = bool_read(f.readline().split()[0]) + ifw['NWindVel'] = int(f.readline().split()[0]) + ifw['WindVxiList'] = [idx.strip() for idx in f.readline().split('WindVxiList')[0].split(',')] + ifw['WindVyiList'] = [idx.strip() for idx in f.readline().split('WindVyiList')[0].split(',')] + ifw['WindVziList'] = [idx.strip() for idx in f.readline().split('WindVziList')[0].split(',')] + + # Steady Wind + f.readline() + ifw['HWindSpeed'] = float_read(f.readline().split()[0]) + ifw['RefHt'] = float_read(f.readline().split()[0]) + ifw['PLExp'] = float_read(f.readline().split()[0]) + + # Uniform Wind + f.readline() + ifw['FileName_Uni'] = os.path.join(inflow_dir, quoted_read(f.readline().split()[0])) + ifw['RefHt_Uni'] = float_read(f.readline().split()[0]) + ifw['RefLength'] = float_read(f.readline().split()[0]) + + # TurbSim FF + f.readline() + ifw['FileName_BTS'] = os.path.join(inflow_dir, quoted_read(f.readline().split()[0])) + + # Bladed FF + f.readline() + ifw['FileNameRoot'] = os.path.join(inflow_dir, quoted_read(f.readline().split()[0])) + ifw['TowerFile'] = bool_read(f.readline().split()[0]) + + # HAWC + f.readline() + ifw['FileName_u'] = os.path.normpath(os.path.join(inflow_dir, quoted_read(f.readline().split()[0]))) + ifw['FileName_v'] = os.path.normpath(os.path.join(inflow_dir, quoted_read(f.readline().split()[0]))) + ifw['FileName_w'] = os.path.normpath(os.path.join(inflow_dir, quoted_read(f.readline().split()[0]))) + ifw['nx'] = int(f.readline().split()[0]) + ifw['ny'] = int(f.readline().split()[0]) + ifw['nz'] = int(f.readline().split()[0]) + ifw['dx'] = float_read(f.readline().split()[0]) + ifw['dy'] = float_read(f.readline().split()[0]) + ifw['dz'] = float_read(f.readline().split()[0]) + ifw['RefHt_Hawc'] = float_read(f.readline().split()[0]) + + # HAWC scaling + f.readline() + ifw['ScaleMethod'] = int(f.readline().split()[0]) + ifw['SFx'] = float_read(f.readline().split()[0]) + ifw['SFy'] = float_read(f.readline().split()[0]) + ifw['SFz'] = float_read(f.readline().split()[0]) + ifw['SigmaFx'] = float_read(f.readline().split()[0]) + ifw['SigmaFy'] = float_read(f.readline().split()[0]) + ifw['SigmaFz'] = float_read(f.readline().split()[0]) + + # HAWC mean profile + f.readline() + ifw['URef'] = float_read(f.readline().split()[0]) + ifw['WindProfile'] = int(f.readline().split()[0]) + ifw['PLExp_Hawc'] = float_read(f.readline().split()[0]) + ifw['Z0'] = float_read(f.readline().split()[0]) + ifw['XOffset'] = float_read(f.readline().split()[0]) + + # LIDAR + f.readline() + ifw['SensorType'] = int(f.readline().split()[0]) + ifw['NumPulseGate'] = int(f.readline().split()[0]) + ifw['PulseSpacing'] = float_read(f.readline().split()[0]) + ifw['NumBeam'] = int(f.readline().split()[0]) + ifw['FocalDistanceX'] = [idx.strip() for idx in f.readline().split('FocalDistanceX')[0].split(',')] + ifw['FocalDistanceY'] = [idx.strip() for idx in f.readline().split('FocalDistanceY')[0].split(',')] + ifw['FocalDistanceZ'] = [idx.strip() for idx in f.readline().split('FocalDistanceZ')[0].split(',')] + ifw['RotorApexOffsetPos'] = [idx.strip() for idx in f.readline().split('RotorApexOffsetPos')[0].split(',')] + ifw['URefLid'] = float_read(f.readline().split()[0]) + ifw['MeasurementInterval'] = float_read(f.readline().split()[0]) + ifw['LidRadialVel'] = bool_read(f.readline().split()[0]) + ifw['ConsiderHubMotion'] = int(f.readline().split()[0]) + + # Output + f.readline() + ifw['SumPrint'] = bool_read(f.readline().split()[0]) + + # OutList + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'InflowWind') + else: + line = f.readline() + while line and 'END' not in line.split('!')[0].upper()[:3]: + line = f.readline() + + f.close() + return {'InflowWind': ifw} + + def write(self, data: dict, file_path: Path, base_dir: Path, *, + naming_out: str = 'openfast', outlist: dict | None = None) -> None: + ifw = data['InflowWind'] + f = open(str(file_path), 'w') + + f.write('------- InflowWind INPUT FILE -------------------------------------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------------------------------------------------------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ifw['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['WindType'], 'WindType', '- switch for wind file type (1=steady; 2=uniform; 3=binary TurbSim FF; 4=binary Bladed-style FF; 5=HAWC format; 6=User defined; 7=native Bladed FF)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['PropagationDir'], 'PropagationDir', '- Direction of wind propagation (meteoroligical rotation from aligned with X (positive rotates towards -Y) -- degrees) (not used for native Bladed format WindType=7)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['VFlowAng'], 'VFlowAng', '- Upflow angle (degrees) (not used for native Bladed format WindType=7)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ifw['VelInterpCubic'], 'VelInterpCubic', '- Use cubic interpolation for velocity in time (false=linear, true=cubic) [Used with WindType=2,3,4,5,7]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['NWindVel'], 'NWindVel', '- Number of points to output the wind velocity (0 to 9)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['WindVxiList'], dtype=str)), 'WindVxiList', '- List of coordinates in the inertial X direction (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['WindVyiList'], dtype=str)), 'WindVyiList', '- List of coordinates in the inertial Y direction (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['WindVziList'], dtype=str)), 'WindVziList', '- List of coordinates in the inertial Z direction (m)\n')) + f.write('================== Parameters for Steady Wind Conditions [used only for WindType = 1] =========================\n') + f.write('{:<22} {:<11} {:}'.format(ifw['HWindSpeed'], 'HWindSpeed', '- Horizontal wind speed (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['RefHt'], 'RefHt', '- Reference height for horizontal wind speed (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['PLExp'], 'PLExp', '- Power law exponent (-)\n')) + f.write('================== Parameters for Uniform wind file [used only for WindType = 2] ============================\n') + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileName_Uni'] + '"', 'FileName_Uni', '- Filename of time series data for uniform wind field. (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['RefHt_Uni'], 'RefHt_Uni', '- Reference height for horizontal wind speed (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['RefLength'], 'RefLength', '- Reference length for linear horizontal and vertical sheer (-)\n')) + f.write('================== Parameters for Binary TurbSim Full-Field files [used only for WindType = 3] ==============\n') + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileName_BTS'] + '"', 'FileName_BTS', '- Name of the Full field wind file to use (.bts)\n')) + f.write('================== Parameters for Binary Bladed-style Full-Field files [used only for WindType = 4 or WindType = 7] =========\n') + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileNameRoot'] + '"', 'FileNameRoot', '- WindType=4: Rootname of the full-field wind file to use (.wnd, .sum); WindType=7: name of the intermediate file with wind scaling values\n')) + f.write('{!s:<22} {:<11} {:}'.format(ifw['TowerFile'], 'TowerFile', '- Have tower file (.twr) (flag) ignored when WindType = 7\n')) + f.write('================== Parameters for HAWC-format binary files [Only used with WindType = 5] =====================\n') + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileName_u'] + '"', 'FileName_u', '- name of the file containing the u-component fluctuating wind (.bin)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileName_v'] + '"', 'FileName_v', '- name of the file containing the v-component fluctuating wind (.bin)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ifw['FileName_w'] + '"', 'FileName_w', '- name of the file containing the w-component fluctuating wind (.bin)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['nx'], 'nx', '- number of grids in the x direction (in the 3 files above) (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['ny'], 'ny', '- number of grids in the y direction (in the 3 files above) (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['nz'], 'nz', '- number of grids in the z direction (in the 3 files above) (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['dx'], 'dx', '- distance (in meters) between points in the x direction (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['dy'], 'dy', '- distance (in meters) between points in the y direction (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['dz'], 'dz', '- distance (in meters) between points in the z direction (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['RefHt_Hawc'], 'RefHt_Hawc', '- reference height; the height (in meters) of the vertical center of the grid (m)\n')) + f.write('------------- Scaling parameters for turbulence ---------------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ifw['ScaleMethod'], 'ScaleMethod', '- Turbulence scaling method [0 = none, 1 = direct scaling, 2 = calculate scaling factor based on a desired standard deviation]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SFx'], 'SFx', '- Turbulence scaling factor for the x direction (-) [ScaleMethod=1]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SFy'], 'SFy', '- Turbulence scaling factor for the y direction (-) [ScaleMethod=1]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SFz'], 'SFz', '- Turbulence scaling factor for the z direction (-) [ScaleMethod=1]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SigmaFx'], 'SigmaFx', '- Turbulence standard deviation to calculate scaling from in x direction (m/s) [ScaleMethod=2]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SigmaFy'], 'SigmaFy', '- Turbulence standard deviation to calculate scaling from in y direction (m/s) [ScaleMethod=2]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['SigmaFz'], 'SigmaFz', '- Turbulence standard deviation to calculate scaling from in z direction (m/s) [ScaleMethod=2]\n')) + f.write('------------- Mean wind profile parameters (added to HAWC-format files) ---------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ifw['URef'], 'URef', '- Mean u-component wind speed at the reference height (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['WindProfile'], 'WindProfile', '- Wind profile type (0=constant;1=logarithmic,2=power law)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['PLExp_Hawc'], 'PLExp_Hawc', '- Power law exponent (-) (used for PL wind profile type only)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['Z0'], 'Z0', '- Surface roughness length (m) (used for LG wind profile type only)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['XOffset'], 'XOffset', '- Initial offset in +x direction (shift of wind box) (-)\n')) + f.write('------------- LIDAR Parameters --------------------------------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ifw['SensorType'], 'SensorType', '- Switch for lidar configuration (0 = None, 1 = Single Point Beam(s), 2 = Continuous, 3 = Pulsed)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['NumPulseGate'], 'NumPulseGate', '- Number of lidar measurement gates (used when SensorType = 3)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['PulseSpacing'], 'PulseSpacing', '- Distance between range gates (m) (used when SensorType = 3)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['NumBeam'], 'NumBeam', '- Number of lidar measurement beams (0-5)(used when SensorType = 1)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['FocalDistanceX'], dtype=str)), 'FocalDistanceX', '- Focal distance co-ordinates of the lidar beam in the x direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['FocalDistanceY'], dtype=str)), 'FocalDistanceY', '- Focal distance co-ordinates of the lidar beam in the y direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['FocalDistanceZ'], dtype=str)), 'FocalDistanceZ', '- Focal distance co-ordinates of the lidar beam in the z direction (relative to hub height) (only first coordinate used for SensorType 2 and 3) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join(np.array(ifw['RotorApexOffsetPos'], dtype=str)), 'RotorApexOffsetPos', '- Offset of the lidar from hub height (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['URefLid'], 'URefLid', '- Reference average wind speed for the lidar[m/s]\n')) + f.write('{:<22} {:<11} {:}'.format(ifw['MeasurementInterval'], 'MeasurementInterval', '- Time between each measurement [s]\n')) + f.write('{!s:<22} {:<11} {:}'.format(ifw['LidRadialVel'], 'LidRadialVel', "- TRUE => return radial component, FALSE => return 'x' direction estimate\n")) + f.write('{:<22} {:<11} {:}'.format(ifw['ConsiderHubMotion'], 'ConsiderHubMotion', "- Flag whether to consider the hub motion's impact on Lidar measurements\n")) + f.write('====================== OUTPUT ==================================================\n') + f.write('{!s:<22} {:<11} {:}'.format(ifw['SumPrint'], 'SumPrint', '- Print summary data to .IfW.sum (flag)\n')) + f.write('OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + + if outlist is not None: + ol = _get_outlist(outlist, ['InflowWind']) + for channel_list in ol: + for ch in channel_list: + f.write('"' + ch + '"\n') + + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + f.write('---------------------------------------------------------------------------------------\n') + + f.flush() + os.fsync(f) + f.close() diff --git a/openfast_io/openfast_io/io/map_io.py b/openfast_io/openfast_io/io/map_io.py new file mode 100644 index 0000000000..c0f4cb3446 --- /dev/null +++ b/openfast_io/openfast_io/io/map_io.py @@ -0,0 +1,154 @@ +""" +MAPIO – read / write MAP++ mooring input files. + +Produces ``{'MAP': m}`` +""" +from __future__ import annotations + +import os +from typing import Any, Dict, Optional, Callable + +from .base import ModuleIO +from ..parsing import float_read + + +class MAPIO(ModuleIO): + """Read / write MAP++ input files.""" + + def read( + self, + file_path: str, + base_dir: str = '', + **kwargs, + ) -> dict: + m: Dict[str, Any] = {} + map_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + f = open(map_file) + + # --- LINE DICTIONARY --- + f.readline() # dashed header line + f.readline() # column names + f.readline() # units + for k in ['LineType', 'Diam', 'MassDenInAir', 'EA', 'CB', 'CIntDamp', 'Ca', 'Cdn', 'Cdt']: + m[k] = [] + data_line = f.readline().strip().split() + while data_line and data_line[0][:3] != '---': + m['LineType'].append(str(data_line[0])) + m['Diam'].append(float_read(data_line[1])) + m['MassDenInAir'].append(float_read(data_line[2])) + m['EA'].append(float_read(data_line[3])) + m['CB'].append(float_read(data_line[4])) + m['CIntDamp'].append(float_read(data_line[5])) + m['Ca'].append(float_read(data_line[6])) + m['Cdn'].append(float_read(data_line[7])) + m['Cdt'].append(float_read(data_line[8])) + data_line = f.readline().strip().split() + + # --- NODE PROPERTIES --- + f.readline() # column names + f.readline() # units + for k in ['Node', 'Type', 'X', 'Y', 'Z', 'M', 'B', 'FX', 'FY', 'FZ']: + m[k] = [] + data_node = f.readline().strip().split() + while data_node and data_node[0][:3] != '---': + m['Node'].append(int(data_node[0])) + m['Type'].append(str(data_node[1])) + m['X'].append(float_read(data_node[2])) + m['Y'].append(float_read(data_node[3])) + m['Z'].append(float_read(data_node[4])) + m['M'].append(float_read(data_node[5])) + m['B'].append(float_read(data_node[6])) + m['FX'].append(float_read(data_node[7])) + m['FY'].append(float_read(data_node[8])) + m['FZ'].append(float_read(data_node[9])) + data_node = f.readline().strip().split() + + # --- LINE PROPERTIES --- + f.readline() # column names + f.readline() # units + for k in ['Line', 'LineType_prop', 'UnstrLen', 'NodeAnch', 'NodeFair', 'Flags']: + m[k] = [] + data_lp = f.readline().strip().split() + while data_lp and data_lp[0][:3] != '---': + m['Line'].append(int(data_lp[0])) + m['LineType_prop'].append(str(data_lp[1])) + m['UnstrLen'].append(float_read(data_lp[2])) + m['NodeAnch'].append(int(data_lp[3])) + m['NodeFair'].append(int(data_lp[4])) + m['Flags'].append([str(val) for val in data_lp[5:]]) + data_lp = f.readline().strip().split() + + # --- SOLVER OPTIONS --- + f.readline() # column names (Option) + f.readline() # units (-) + m['Option'] = [] + data_solver = f.readline().strip().split() + while len(data_solver) > 0: + m['Option'].append([str(val) for val in data_solver]) + data_solver = f.readline().strip().split() + + f.close() + return {'MAP': m} + + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + **kwargs, + ) -> None: + m = data['MAP'] + with open(file_path, 'w') as f: + f.write('---------------------- LINE DICTIONARY ---------------------------------------\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['LineType', 'Diam', 'MassDenInAir', 'EA', 'CB', 'CIntDamp', 'Ca', 'Cdn', 'Cdt']]) + '\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(m)', '(kg/m)', '(N)', '(-)', '(Pa-s)', '(-)', '(-)', '(-)']]) + '\n') + for i in range(len(m.get('Diam', []))): + ln = [] + ln.append('{:^11}'.format(m['LineType'][i])) + ln.append('{:^11}'.format(m['Diam'][i])) + ln.append('{:^11}'.format(m['MassDenInAir'][i])) + ln.append('{:^11}'.format(m['EA'][i])) + ln.append('{:<11}'.format(m['CB'][i])) + ln.append('{:<11}'.format(m['CIntDamp'][i])) + ln.append('{:<11}'.format(m['Ca'][i])) + ln.append('{:<11}'.format(m['Cdn'][i])) + ln.append('{:<11}'.format(m['Cdt'][i])) + f.write(" ".join(ln) + '\n') + + f.write('---------------------- NODE PROPERTIES ---------------------------------------\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['Node', 'Type', 'X', 'Y', 'Z', 'M', 'B', 'FX', 'FY', 'FZ']]) + '\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(m)', '(m)', '(kg)', '(m^3)', '(N)', '(N)', '(N)']]) + '\n') + for i in range(len(m.get('Node', []))): + ln = [] + ln.append('{:<11}'.format(m['Node'][i])) + ln.append('{:<11}'.format(m['Type'][i])) + ln.append('{:<11}'.format(m['X'][i])) + ln.append('{:<11}'.format(m['Y'][i])) + ln.append('{:<11}'.format(m['Z'][i])) + ln.append('{:<11}'.format(m['M'][i])) + ln.append('{:<11}'.format(m['B'][i])) + ln.append('{:<11}'.format(m['FX'][i])) + ln.append('{:<11}'.format(m['FY'][i])) + ln.append('{:<11}'.format(m['FZ'][i])) + f.write(" ".join(ln) + '\n') + + f.write('---------------------- LINE PROPERTIES ---------------------------------------\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['Line', 'LineType', 'UnstrLen', 'NodeAnch', 'NodeFair', 'Flags']]) + '\n') + f.write(" ".join(['{:<11s}'.format(i) for i in ['(-)', '(-)', '(m)', '(-)', '(-)', '(-)']]) + '\n') + for i in range(len(m.get('Line', []))): + ln = [] + ln.append('{:^11d}'.format(m['Line'][i])) + ln.append('{:^11}'.format(m['LineType_prop'][i])) + ln.append('{:^11}'.format(m['UnstrLen'][i])) + ln.append('{:^11d}'.format(m['NodeAnch'][i])) + ln.append('{:^11d}'.format(m['NodeFair'][i])) + ln.append('{:<11}'.format(" ".join(m['Flags'][i]))) + f.write(" ".join(ln) + '\n') + + f.write('---------------------- SOLVER OPTIONS-----------------------------------------\n') + f.write('{:<11s}'.format('Option') + '\n') + f.write('{:<11s}'.format('(-)') + '\n') + for opt in m.get('Option', []): + f.write(" ".join(opt) + '\n') + f.write('\n') diff --git a/openfast_io/openfast_io/io/moordyn.py b/openfast_io/openfast_io/io/moordyn.py new file mode 100644 index 0000000000..7896f90aa8 --- /dev/null +++ b/openfast_io/openfast_io/io/moordyn.py @@ -0,0 +1,401 @@ +""" +MoorDynIO – read / write MoorDyn input files. + +Produces ``{'MoorDyn': md}`` + +The MoorDyn file format is section-header-based rather than line-sequential, +so parsing loops on header detection. +""" +from __future__ import annotations + +import os +import re +from typing import Any, Dict, Optional, Callable + +from .base import ModuleIO +from ..parsing import ( + float_read, + readline_filterComments, +) + + +class MoorDynIO(ModuleIO): + """Read / write MoorDyn input files.""" + + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + md: Dict[str, Any] = {} + md_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + # Init optional headers + md['Rod_Name'] = [] + md['Body_ID'] = [] + md['Rod_ID'] = [] + + f = open(md_file) + data_line = f.readline() + + while data_line: + tag = ''.join(data_line.strip().split()).lower() + + # ---- LINE TYPES ---- + if 'linetypes' in tag or 'linedictionary' in tag: + f.readline(); f.readline() + for k in ['Name', 'Diam', 'MassDen', 'EA', 'NonLinearEA', 'BA_zeta', 'EI', + 'Cd', 'Ca', 'CdAx', 'CaAx', 'Cl', 'dF', 'cF']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Name'].append(str(dl[0])) + md['Diam'].append(float(dl[1])) + md['MassDen'].append(float(dl[2])) + md['EA'].append([float_read(x) for x in dl[3].split('|')]) + md['BA_zeta'].append([float(x) for x in dl[4].split('|')]) + md['EI'].append(float(dl[5])) + md['Cd'].append(float(dl[6])) + md['Ca'].append(float(dl[7])) + md['CdAx'].append(float(dl[8])) + md['CaAx'].append(float(dl[9])) + if len(dl) == 10: + md['Cl'].append(None); md['dF'].append(None); md['cF'].append(None) + elif len(dl) == 11: + md['Cl'].append(float(dl[10])); md['dF'].append(None); md['cF'].append(None) + elif len(dl) >= 13: + md['Cl'].append(float(dl[10])); md['dF'].append(float(dl[11])); md['cF'].append(float(dl[12])) + + if isinstance(md['EA'][-1], list) and len(md['EA'][-1]) == 1 and isinstance(md['EA'][-1][0], str): + ea_file = os.path.normpath(os.path.join(os.path.dirname(md_file), md['EA'][-1][0])) + md['NonLinearEA'].append(_read_nonlinear_ea(ea_file)) + else: + md['NonLinearEA'].append(None) + + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- ROD TYPES ---- + elif 'rodtypes' in tag or 'roddictionary' in tag: + f.readline(); f.readline() + for k in ['Rod_Diam', 'Rod_MassDen', 'Rod_Cd', 'Rod_Ca', 'Rod_CdEnd', 'Rod_CaEnd']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Rod_Name'].append(dl[0]) + md['Rod_Diam'].append(float(dl[1])) + md['Rod_MassDen'].append(float(dl[2])) + md['Rod_Cd'].append(float(dl[3])) + md['Rod_Ca'].append(float(dl[4])) + md['Rod_CdEnd'].append(float(dl[5])) + md['Rod_CaEnd'].append(float(dl[6])) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- BODIES ---- + elif 'bodies' in tag or 'bodylist' in tag or 'bodyproperties' in tag: + f.readline(); f.readline() + for k in ['Body_Attachment', 'X0', 'Y0', 'Z0', 'r0', 'p0', 'y0', + 'Body_Mass', 'Body_CG', 'Body_I', 'Body_Volume', 'Body_CdA', 'Body_Ca']: + md[k] = [] + md['Body_ID'] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Body_ID'].append(int(dl[0])) + md['Body_Attachment'].append(dl[1]) + md['X0'].append(float(dl[2])); md['Y0'].append(float(dl[3])); md['Z0'].append(float(dl[4])) + md['r0'].append(float(dl[5])); md['p0'].append(float(dl[6])); md['y0'].append(float(dl[7])) + md['Body_Mass'].append(float(dl[8])) + md['Body_CG'].append([float(x) for x in dl[9].split('|')]) + md['Body_I'].append([float(x) for x in dl[10].split('|')]) + md['Body_Volume'].append(float(dl[11])) + md['Body_CdA'].append([float(x) for x in dl[12].split('|')]) + md['Body_Ca'].append([float(x) for x in dl[13].split('|')]) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- RODS ---- + elif 'rods' in tag or 'rodlist' in tag or 'rodproperties' in tag: + f.readline(); f.readline() + for k in ['Rod_Type', 'Rod_Attachment', 'Xa', 'Ya', 'Za', 'Xb', 'Yb', 'Zb', 'Rod_NumSegs', 'RodOutputs']: + md[k] = [] + md['Rod_ID'] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Rod_ID'].append(dl[0]) + md['Rod_Type'].append(dl[1]) + md['Rod_Attachment'].append(dl[2]) + md['Xa'].append(float(dl[3])); md['Ya'].append(float(dl[4])); md['Za'].append(float(dl[5])) + md['Xb'].append(float(dl[6])); md['Yb'].append(float(dl[7])); md['Zb'].append(float(dl[8])) + md['Rod_NumSegs'].append(int(dl[9])) + md['RodOutputs'].append(dl[10]) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- POINTS ---- + elif 'points' in tag or 'connectionproperties' in tag or \ + 'nodeproperties' in tag or 'pointproperties' in tag or 'pointlist' in tag: + f.readline(); f.readline() + for k in ['Point_ID', 'Attachment', 'X', 'Y', 'Z', 'M', 'V', 'CdA', 'CA']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Point_ID'].append(int(dl[0])) + md['Attachment'].append(str(dl[1])) + md['X'].append(float(dl[2])); md['Y'].append(float(dl[3])); md['Z'].append(float(dl[4])) + md['M'].append(float(dl[5])); md['V'].append(float(dl[6])) + md['CdA'].append(float(dl[7])); md['CA'].append(float(dl[8])) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- LINES ---- + elif 'lines' in tag or 'lineproperties' in tag or 'linelist' in tag: + f.readline(); f.readline() + for k in ['Line_ID', 'LineType', 'AttachA', 'AttachB', 'UnstrLen', 'NumSegs', 'Outputs']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Line_ID'].append(int(dl[0])) + md['LineType'].append(str(dl[1])) + md['AttachA'].append(str(dl[2])); md['AttachB'].append(str(dl[3])) + md['UnstrLen'].append(float(dl[4])) + md['NumSegs'].append(int(dl[5])) + md['Outputs'].append(str(dl[6])) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- FAILURE ---- + elif 'failure' in tag: + f.readline(); f.readline() + for k in ['Failure_ID', 'Failure_Point', 'Failure_Line(s)', 'FailTime', 'FailTen']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['Failure_ID'].append(int(dl[0])) + md['Failure_Point'].append(dl[1]) + md['Failure_Line(s)'].append([int(x) for x in dl[2].split(',')]) + md['FailTime'].append(float(dl[3])) + md['FailTen'].append(float(dl[4])) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- CONTROL ---- + elif 'control' in tag: + f.readline(); f.readline() + md['ChannelID'] = [] + md['Lines_Control'] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['ChannelID'].append(int(dl[0])) + control_lines = [] + for lines in dl[1:]: + for line in lines.split(','): + control_lines.append(line.strip(',')) + while '' in control_lines: + control_lines.remove('') + md['Lines_Control'].append(control_lines) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- EXTERNAL ---- + elif 'external' in tag: + f.readline(); f.readline() + for k in ['External_ID', 'Object', 'Fext', 'Blin', 'Bquad', 'CSys']: + md[k] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + md['External_ID'].append(int(dl[0])) + md['Object'].append(dl[1]) + md['Fext'].append([float(x) for x in dl[2].split('|')]) + md['Blin'].append([float(x) for x in dl[3].split('|')]) + md['Bquad'].append([float(x) for x in dl[4].split('|')]) + md['CSys'].append(dl[5]) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- OPTIONS ---- + elif 'options' in tag: + md['option_values'] = [] + md['option_names'] = [] + md['option_descriptions'] = [] + dl = readline_filterComments(f).split() + while dl[0] and dl[0][:3] != '---': + option_value = dl[0].upper() + option_name = dl[1].upper() + option_description = ' '.join(dl[2:]) if len(dl) > 2 else '-' + if option_name == 'WATERKIN': + md['WaterKin'] = option_value.strip('"') + md['option_values'].append(float_read(option_value.strip('"'))) + md['option_names'].append(option_name) + md['option_descriptions'].append(option_description) + dl = readline_filterComments(f).split() + data_line = ''.join(dl) + + # ---- OUTPUTS ---- + elif 'outputs' in tag: + outlist_md = {} + dl = readline_filterComments(f) + while (dl and dl[0:3] != '---') and ('END' not in dl): + if '"' in dl: + # Strip surrounding quotes from each channel token + dl = dl.replace('"', '') + channels = [c.strip() for c in dl.split(',')] + for c in channels: + if c: + outlist_md[c] = True + dl = readline_filterComments(f) + if outlist is not None: + outlist['MoorDyn'] = outlist_md + else: + # standalone path only (no shared registry) — avoid polluting + # fst_vt['MoorDyn'] with a private key when the driver supplies outlist. + md['_outlist'] = outlist_md + f.close() + break + + else: + data_line = f.readline() + + return {'MoorDyn': md} + + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + md = data['MoorDyn'] + + with open(file_path, 'w') as f: + f.write('--------------------- MoorDyn Input File ------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + + # Line Types + if md.get('Name'): + f.write('----------------------- LINE TYPES ------------------------------------------\n') + f.write('Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx\n') + f.write('(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-)\n') + for i in range(len(md['Name'])): + ea_str = '|'.join([str(v) for v in md['EA'][i]]) + ba_str = '|'.join([str(v) for v in md['BA_zeta'][i]]) + ln = f"{md['Name'][i]} {md['Diam'][i]} {md['MassDen'][i]} {ea_str} {ba_str} {md['EI'][i]} {md['Cd'][i]} {md['Ca'][i]} {md['CdAx'][i]} {md['CaAx'][i]}" + if md.get('Cl') and md['Cl'][i] is not None: + ln += f" {md['Cl'][i]}" + if md.get('dF') and md['dF'][i] is not None: + ln += f" {md['dF'][i]} {md['cF'][i]}" + f.write(ln + '\n') + + # Rod Types + if md.get('Rod_Name'): + f.write('----------------------- ROD TYPES -------------------------------------------\n') + f.write('TypeName Diam MassDenInAir Cd Ca CdEnd CaEnd\n') + f.write('(name) (m) (kg/m) (-) (-) (-) (-)\n') + for i in range(len(md['Rod_Name'])): + f.write(f"{md['Rod_Name'][i]} {md['Rod_Diam'][i]} {md['Rod_MassDen'][i]} {md['Rod_Cd'][i]} {md['Rod_Ca'][i]} {md['Rod_CdEnd'][i]} {md['Rod_CaEnd'][i]}\n") + + # Bodies + if md.get('Body_ID'): + f.write('----------------------- BODIES ----------------------------------------------\n') + f.write('ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG I Volume CdA Ca\n') + f.write('(-) (-) (m) (m) (m) (deg)(deg)(deg)(kg) (m) (kg-m^2) (m^3) (m^2) (-)\n') + for i in range(len(md['Body_ID'])): + cg = '|'.join([str(v) for v in md['Body_CG'][i]]) + bi = '|'.join([str(v) for v in md['Body_I'][i]]) + cda = '|'.join([str(v) for v in md['Body_CdA'][i]]) + ca = '|'.join([str(v) for v in md['Body_Ca'][i]]) + f.write(f"{md['Body_ID'][i]} {md['Body_Attachment'][i]} {md['X0'][i]} {md['Y0'][i]} {md['Z0'][i]} {md['r0'][i]} {md['p0'][i]} {md['y0'][i]} {md['Body_Mass'][i]} {cg} {bi} {md['Body_Volume'][i]} {cda} {ca}\n") + + # Rods + if md.get('Rod_ID'): + f.write('----------------------- RODS ------------------------------------------------\n') + f.write('ID RodType Attachment Xa Ya Za Xb Yb Zb NumSegs RodOutputs\n') + f.write('(-) (-) (-) (m) (m) (m) (m) (m) (m) (-) (-)\n') + for i in range(len(md['Rod_ID'])): + f.write(f"{md['Rod_ID'][i]} {md['Rod_Type'][i]} {md['Rod_Attachment'][i]} {md['Xa'][i]} {md['Ya'][i]} {md['Za'][i]} {md['Xb'][i]} {md['Yb'][i]} {md['Zb'][i]} {md['Rod_NumSegs'][i]} {md['RodOutputs'][i]}\n") + + # Points + if md.get('Point_ID'): + f.write('----------------------- POINTS -----------------------------------------------\n') + f.write('ID Attachment X Y Z M V CdA CA\n') + f.write('(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-)\n') + for i in range(len(md['Point_ID'])): + f.write(f"{md['Point_ID'][i]} {md['Attachment'][i]} {md['X'][i]} {md['Y'][i]} {md['Z'][i]} {md['M'][i]} {md['V'][i]} {md['CdA'][i]} {md['CA'][i]}\n") + + # Lines + if md.get('Line_ID'): + f.write('----------------------- LINES -----------------------------------------------\n') + f.write('ID LineType AttachA AttachB UnstrLen NumSegs LineOutputs\n') + f.write('(-) (-) (-) (-) (m) (-) (-)\n') + for i in range(len(md['Line_ID'])): + f.write(f"{md['Line_ID'][i]} {md['LineType'][i]} {md['AttachA'][i]} {md['AttachB'][i]} {md['UnstrLen'][i]} {md['NumSegs'][i]} {md['Outputs'][i]}\n") + + # Failure + if md.get('Failure_ID'): + f.write('----------------------- FAILURE ----------------------------------------------\n') + f.write('ID Point Line(s) FailTime FailTen\n') + f.write('(-) (-) (-) (s) (N)\n') + for i in range(len(md['Failure_ID'])): + lines_str = ','.join([str(x) for x in md['Failure_Line(s)'][i]]) + f.write(f"{md['Failure_ID'][i]} {md['Failure_Point'][i]} {lines_str} {md['FailTime'][i]} {md['FailTen'][i]}\n") + + # Control + if md.get('ChannelID'): + f.write('----------------------- CONTROL -----------------------------------------------\n') + f.write('ChannelID Line(s)\n') + f.write('(-) (-)\n') + for i in range(len(md['ChannelID'])): + cl = ','.join(md['Lines_Control'][i]) + f.write(f"{md['ChannelID'][i]} {cl}\n") + + # External + if md.get('External_ID'): + f.write('----------------------- EXTERNAL -----------------------------------------------\n') + f.write('ID Object Fext Blin Bquad CSys\n') + f.write('(-) (-) (N) (N/(m/s)) (N/(m/s)^2) (-)\n') + for i in range(len(md['External_ID'])): + fext = '|'.join([str(v) for v in md['Fext'][i]]) + blin = '|'.join([str(v) for v in md['Blin'][i]]) + bquad = '|'.join([str(v) for v in md['Bquad'][i]]) + f.write(f"{md['External_ID'][i]} {md['Object'][i]} {fext} {blin} {bquad} {md['CSys'][i]}\n") + + # Options + if md.get('option_names'): + f.write('----------------------- OPTIONS -----------------------------------------------\n') + for i in range(len(md['option_names'])): + val = md['option_values'][i] + name = md['option_names'][i] + desc = md['option_descriptions'][i] + f.write(f"{val} {name} {desc}\n") + + # Outputs + f.write('----------------------- OUTPUTS ------------------------------------------------\n') + out = md.get('_outlist', {}) + if outlist and 'MoorDyn' in outlist: + out = outlist['MoorDyn'] + # Only emit truthy channels — registries built via the public + # OutList.to_fst_output() contain explicit False entries for every + # known channel, which must not be written out as enabled. + for ch, enabled in out.items(): + if enabled: + f.write(f'"{ch}"\n') + f.write('END\n') + f.write('----------------------- need this line ------------------\n') + + +def _read_nonlinear_ea(filepath): + """Read a non-linear EA file (strain-EA curves).""" + try: + import numpy as np + data = np.loadtxt(filepath) + return data + except Exception: + return None diff --git a/openfast_io/openfast_io/io/seastate.py b/openfast_io/openfast_io/io/seastate.py new file mode 100644 index 0000000000..d50a7850ba --- /dev/null +++ b/openfast_io/openfast_io/io/seastate.py @@ -0,0 +1,266 @@ +""" +SeaStateIO -- read / write SeaState input files. + +Produces ``{'SeaState': ss}`` +""" +from __future__ import annotations + +import os +from typing import Any, Dict, Optional, Callable + +from .base import ModuleIO +from ..outlist import emit_outlist, capture_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, +) + + +def _get_outlist(outlist_dict: dict, keys: list) -> list: + out = [] + for key in keys: + if key in outlist_dict: + out.append(outlist_dict[key]) + return out + + +class SeaStateIO(ModuleIO): + """Read / write SeaState input files.""" + + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + ss: Dict[str, Any] = {} + ss_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + f = open(ss_file) + f.readline() + f.readline() + + ss['Echo'] = bool_read(f.readline().split()[0]) + + # ENVIRONMENTAL CONDITIONS + f.readline() + ss['WtrDens'] = float_read(f.readline().split()[0]) + ss['WtrDpth'] = float_read(f.readline().split()[0]) + ss['MSL2SWL'] = float_read(f.readline().split()[0]) + + # SPATIAL DISCRETIZATION + f.readline() + ss['X_HalfWidth'] = float_read(f.readline().split()[0]) + ss['Y_HalfWidth'] = float_read(f.readline().split()[0]) + ss['Z_Depth'] = float_read(f.readline().split()[0]) + ss['NX'] = int_read(f.readline().split()[0]) + ss['NY'] = int_read(f.readline().split()[0]) + ss['NZ'] = int_read(f.readline().split()[0]) + + # WAVES + f.readline() + ss['WaveMod'] = int_read(f.readline().split()[0]) + ss['WaveStMod'] = int_read(f.readline().split()[0]) + ss['WvCrntMod'] = int_read(f.readline().split()[0]) + ss['WaveTMax'] = float_read(f.readline().split()[0]) + ss['WaveDT'] = float_read(f.readline().split()[0]) + ss['WaveHs'] = float_read(f.readline().split()[0]) + ss['WaveTp'] = float_read(f.readline().split()[0]) + ss['WavePkShp'] = float_read(f.readline().split()[0]) + ss['WvLowCOff'] = float_read(f.readline().split()[0]) + ss['WvHiCOff'] = float_read(f.readline().split()[0]) + ss['WaveDir'] = float_read(f.readline().split()[0]) + ss['WaveDirMod'] = int_read(f.readline().split()[0]) + ss['WaveDirSpread'] = float_read(f.readline().split()[0]) + ss['WaveNDir'] = int_read(f.readline().split()[0]) + ss['WaveDirRange'] = float_read(f.readline().split()[0]) + ss['WaveSeed1'] = int_read(f.readline().split()[0]) + ss['WaveSeed2'] = int_read(f.readline().split()[0]) + ss['WaveNDAmp'] = bool_read(f.readline().split()[0]) + ss['WvKinFile'] = quoted_read(f.readline().split()[0]) + + # 2ND-ORDER WAVES + f.readline() + ss['WvDiffQTF'] = bool_read(f.readline().split()[0]) + ss['WvSumQTF'] = bool_read(f.readline().split()[0]) + ss['WvLowCOffD'] = float_read(f.readline().split()[0]) + ss['WvHiCOffD'] = float_read(f.readline().split()[0]) + ss['WvLowCOffS'] = float_read(f.readline().split()[0]) + ss['WvHiCOffS'] = float_read(f.readline().split()[0]) + + # CONSTRAINED WAVE + f.readline() + ss['ConstWaveMod'] = int_read(f.readline().split()[0]) + ss['CrestHmax'] = float_read(f.readline().split()[0]) + ss['CrestTime'] = float_read(f.readline().split()[0]) + ss['CrestXi'] = float_read(f.readline().split()[0]) + ss['CrestYi'] = float_read(f.readline().split()[0]) + + # CURRENT + f.readline() + ss['CurrMod'] = int_read(f.readline().split()[0]) + ss['CurrSSV0'] = float_read(f.readline().split()[0]) + ss['CurrSSDir'] = float_read(f.readline().split()[0]) + ss['CurrNSRef'] = float_read(f.readline().split()[0]) + ss['CurrNSV0'] = float_read(f.readline().split()[0]) + ss['CurrNSDir'] = float_read(f.readline().split()[0]) + ss['CurrDIV'] = float_read(f.readline().split()[0]) + ss['CurrDIDir'] = float_read(f.readline().split()[0]) + + # MacCamy-Fuchs + f.readline() + ss['MCFD'] = float_read(f.readline().split()[0]) + + # OUTPUT + f.readline() + ss['SeaStSum'] = bool_read(f.readline().split()[0]) + ss['OutSwtch'] = int_read(f.readline().split()[0]) + ss['OutFmt'] = quoted_read(f.readline().split()[0]) + ss['OutSFmt'] = quoted_read(f.readline().split()[0]) + ss['NWaveElev'] = int_read(f.readline().split()[0]) + ss['WaveElevxi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveElevxi')[0].replace(',', ' ').split()] + ss['WaveElevyi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveElevyi')[0].replace(',', ' ').split()] + ss['NWaveKin'] = int_read(f.readline().split()[0]) + if ss['NWaveKin']: + ss['WaveKinxi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinxi')[0].replace(',', ' ').split()] + ss['WaveKinyi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinyi')[0].replace(',', ' ').split()] + ss['WaveKinzi'] = [float_read(idx.strip()) for idx in f.readline().split('WaveKinzi')[0].replace(',', ' ').split()] + else: + [f.readline() for _ in range(3)] + ss['WaveKinxi'] = [0] + ss['WaveKinyi'] = [0] + ss['WaveKinzi'] = [0] + + # Outlist (legacy uses read_outlist_freeForm) + f.readline() + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'SeaState') + else: + # Standalone use with no shared registry — consume the section + # safely and stash the found channels so write() can still emit + # them (mirrors the aerodisk.py fallback pattern). + channels = capture_outlist(f, None, 'SeaState', freeform=True) + ss['_outlist'] = {ch: True for ch in channels} + + f.close() + return {'SeaState': ss} + + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + ss = data['SeaState'] + + with open(file_path, 'w') as f: + f.write('------- SeaState Input File --------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('{!s:<22} {:<11} {:}'.format(ss['Echo'], 'Echo', '- Echo the input file data (flag)\n')) + + f.write('---------------------- ENVIRONMENTAL CONDITIONS --------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ss['WtrDens'], 'WtrDens', '- Water density (kg/m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WtrDpth'], 'WtrDpth', '- Water depth (m) relative to MSL\n')) + f.write('{:<22} {:<11} {:}'.format(ss['MSL2SWL'], 'MSL2SWL', '- Offset between SWL and MSL (m)\n')) + + f.write('---------------------- SPATIAL DISCRETIZATION ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ss['X_HalfWidth'], 'X_HalfWidth', '- Half-width of X domain (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['Y_HalfWidth'], 'Y_HalfWidth', '- Half-width of Y domain (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['Z_Depth'], 'Z_Depth', '- Depth of Z domain (m)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['NX'], 'NX', '- Number of nodes in half X-direction (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['NY'], 'NY', '- Number of nodes in half Y-direction (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['NZ'], 'NZ', '- Number of nodes in Z direction (-)\n')) + + f.write('---------------------- WAVES ---------------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ss['WaveMod'], 'WaveMod', '- Incident wave kinematics model (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['WaveStMod'], 'WaveStMod', '- Wave stretching model (switch)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['WvCrntMod'], 'WvCrntMod', '- Wave-current model (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveTMax'], 'WaveTMax', '- Analysis time for wave calculations (sec)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveDT'], 'WaveDT', '- Time step for wave calculations (sec)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveHs'], 'WaveHs', '- Significant wave height (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveTp'], 'WaveTp', '- Peak-spectral period (sec)\n')) + # WavePkShp special handling + wpks = ss['WavePkShp'] + if isinstance(wpks, float) and wpks == 0.0: + wpks = 'Default' + f.write('{:<22} {:<11} {:}'.format(wpks, 'WavePkShp', '- Peak-shape parameter (-) or DEFAULT\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvLowCOff'], 'WvLowCOff', '- Low cut-off frequency (rad/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvHiCOff'], 'WvHiCOff', '- High cut-off frequency (rad/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveDir'], 'WaveDir', '- Wave propagation heading (deg)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['WaveDirMod'], 'WaveDirMod', '- Directional spreading function (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveDirSpread'], 'WaveDirSpread', '- Wave direction spreading coeff (-)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['WaveNDir'], 'WaveNDir', '- Number of wave directions (-)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WaveDirRange'], 'WaveDirRange', '- Range of wave directions (deg)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['WaveSeed1'], 'WaveSeed(1)', '- First random seed (-)\n')) + try: + seed2 = int(ss['WaveSeed2']) + f.write('{:<22d} {:<11} {:}'.format(ss['WaveSeed2'], 'WaveSeed(2)', '- Second random seed (-)\n')) + except (ValueError, TypeError): + f.write('{!s:<22} {:<11} {:}'.format(ss['WaveSeed2'], 'WaveSeed(2)', '- Second random seed (-)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ss['WaveNDAmp'], 'WaveNDAmp', '- Normally distributed amplitudes (flag)\n')) + f.write('{:<22} {:<11} {:}'.format('"' + ss['WvKinFile'] + '"', 'WvKinFile', '- External wave data file root name\n')) + + f.write('---------------------- 2ND-ORDER WAVES -----------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ss['WvDiffQTF'], 'WvDiffQTF', '- Difference-frequency 2nd-order (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ss['WvSumQTF'], 'WvSumQTF', '- Summation-frequency 2nd-order (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvLowCOffD'], 'WvLowCOffD', '- Low freq cutoff for diff (rad/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvHiCOffD'], 'WvHiCOffD', '- High freq cutoff for diff (rad/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvLowCOffS'], 'WvLowCOffS', '- Low freq cutoff for sum (rad/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['WvHiCOffS'], 'WvHiCOffS', '- High freq cutoff for sum (rad/s)\n')) + + f.write('---------------------- CONSTRAINED WAVES ----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ss['ConstWaveMod'], 'ConstWaveMod', '- Constrained wave model (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CrestHmax'], 'CrestHmax', '- Crest height (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CrestTime'], 'CrestTime', '- Time of crest (s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CrestXi'], 'CrestXi', '- X-position of crest (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CrestYi'], 'CrestYi', '- Y-position of crest (m)\n')) + + f.write('---------------------- CURRENT -------------------------------------------------\n') + f.write('{:<22d} {:<11} {:}'.format(ss['CurrMod'], 'CurrMod', '- Current profile model (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrSSV0'], 'CurrSSV0', '- Sub-surface current velocity (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrSSDir'], 'CurrSSDir', '- Sub-surface current heading (deg)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrNSRef'], 'CurrNSRef', '- Near-surface reference depth (m)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrNSV0'], 'CurrNSV0', '- Near-surface current velocity (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrNSDir'], 'CurrNSDir', '- Near-surface current heading (deg)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrDIV'], 'CurrDIV', '- Depth-independent current velocity (m/s)\n')) + f.write('{:<22} {:<11} {:}'.format(ss['CurrDIDir'], 'CurrDIDir', '- Depth-independent current heading (deg)\n')) + + f.write('---------------------- MacCamy-Fuchs Diffraction Model -------------------------\n') + f.write('{:<22} {:<11} {:}'.format(ss['MCFD'], 'MCFD', '- MacCamy-Fuchs member radius\n')) + + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(ss['SeaStSum'], 'SeaStSum', '- Output a summary file [flag]\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['OutSwtch'], 'OutSwtch', '- Output channels (switch)\n')) + f.write('{!s:<22} {:<11} {:}'.format(ss['OutFmt'], 'OutFmt', '- Output format\n')) + f.write('{!s:<22} {:<11} {:}'.format(ss['OutSFmt'], 'OutSFmt', '- Output format for headers\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['NWaveElev'], 'NWaveElev', '- Number of wave elevation output points (-)\n')) + f.write('{:<22} {:<11} {:}'.format(", ".join([f'{float(v):f}' for v in ss['WaveElevxi']]), 'WaveElevxi', '- xi-coordinates for wave elevation output (m)\n')) + f.write('{:<22} {:<11} {:}'.format(", ".join([f'{float(v):f}' for v in ss['WaveElevyi']]), 'WaveElevyi', '- yi-coordinates for wave elevation output (m)\n')) + f.write('{:<22d} {:<11} {:}'.format(ss['NWaveKin'], 'NWaveKin', '- Number of wave kinematics output points (-)\n')) + + if ss['NWaveKin'] > 0: + f.write('{:<22} {:<11} {:}'.format(", ".join([f'{v:f}' for v in ss['WaveKinxi']]), 'WaveKinxi', '- xi-coordinates for wave kinematics (m)\n')) + f.write('{:<22} {:<11} {:}'.format(", ".join([f'{v:f}' for v in ss['WaveKinyi']]), 'WaveKinyi', '- yi-coordinates for wave kinematics (m)\n')) + f.write('{:<22} {:<11} {:}'.format(", ".join([f'{v:f}' for v in ss['WaveKinzi']]), 'WaveKinzi', '- zi-coordinates for wave kinematics (m)\n')) + else: + f.write('{:<11} {:}'.format('WaveKinxi', '- xi-coordinates for wave kinematics (m)\n')) + f.write('{:<11} {:}'.format('WaveKinyi', '- yi-coordinates for wave kinematics (m)\n')) + f.write('{:<11} {:}'.format('WaveKinzi', '- zi-coordinates for wave kinematics (m)\n')) + + f.write('---------------------- OUTPUT CHANNELS -----------------------------------------\n') + if outlist is not None: + emit_outlist(f, outlist, 'SeaState') + else: + for ch in ss.get('_outlist', {}): # standalone fallback (no shared registry) + f.write('"' + ch + '"\n') + f.write('END of output channels and end of file.\n') diff --git a/openfast_io/openfast_io/io/servodyn.py b/openfast_io/openfast_io/io/servodyn.py new file mode 100644 index 0000000000..30af3012cb --- /dev/null +++ b/openfast_io/openfast_io/io/servodyn.py @@ -0,0 +1,782 @@ +""" +ServoDynIO – read / write ServoDyn, StC, DISCON, and spd_trq files. + +Produces the dict structure expected by ``fst_vt``: + + { + 'ServoDyn': { ... }, + 'BStC': [ {stc_dict}, ... ], + 'NStC': [ {stc_dict}, ... ], + 'TStC': [ {stc_dict}, ... ], + 'SStC': [ {stc_dict}, ... ], + 'DISCON_in': { ... } | None, + 'spd_trq': { ... } | None, + } +""" +from __future__ import annotations + +import os +from typing import Any, Dict, List, Optional, Callable + +from .base import ModuleIO +from ..outlist import emit_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, +) + + +# --------------------------------------------------------------------------- +# tiny helpers copied from FAST_writer (avoids coupling to the writer class) +# --------------------------------------------------------------------------- + +def _float_default_out(val: Any) -> str: + if isinstance(val, float): + return f'{val:<22}' + return f'{val!s:<22}' + + +def _int_default_out(val: Any) -> str: + if isinstance(val, int): + return f'{val:<22d}' + return f'{val!s:<22}' + + +def _get_outlist(outlist_dict: dict, keys: list) -> list: + """Return the list-of-lists for *keys* in an outlist dict.""" + out = [] + for key in keys: + if key in outlist_dict: + out.append(outlist_dict[key]) + return out + + +# --------------------------------------------------------------------------- +# Optional ROSCO imports +# --------------------------------------------------------------------------- +try: + from rosco.toolbox.utilities import read_DISCON, load_from_txt + from rosco.toolbox import turbine as ROSCO_turbine + from rosco.toolbox import utilities as ROSCO_utilities + _ROSCO = True +except ImportError: + _ROSCO = False + + +class ServoDynIO(ModuleIO): + """Read / write ServoDyn v1.05+ input files and sub-files.""" + + # ------------------------------------------------------------------ + # read + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + path2dll: Optional[str] = None, + servo_file_rel: str = '', + ) -> dict: + """Read a ServoDyn input file and all referenced sub-files. + + Parameters + ---------- + file_path : str + Path to the ServoDyn ``.dat`` file. + base_dir : str + Base directory for resolving relative paths. + outlist : dict, optional + Pre-allocated outlist dict; a 'ServoDyn' key will be populated. + read_outlist_fn : callable, optional + ``read_outlist(f, key)`` helper that reads the OutList section. + path2dll : str, optional + Override path for DLL_FileName. + servo_file_rel : str + Relative path from ``base_dir`` to the ServoDyn file (used to + resolve StC file paths the same way the legacy reader does). + + Returns + ------- + dict with keys ``ServoDyn``, ``BStC``, ``NStC``, ``TStC``, ``SStC``, + ``DISCON_in`` (or absent), ``spd_trq`` (or absent). + """ + sd = {} + sd_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + with open(sd_file) as f: + f.readline() # header 1 + f.readline() # header 2 + + # -- Simulation Control -- + f.readline() + sd['Echo'] = bool_read(f.readline().split()[0]) + sd['DT'] = float_read(f.readline().split()[0]) + + # -- Pitch Control -- + f.readline() + sd['PCMode'] = int(f.readline().split()[0]) + sd['TPCOn'] = float_read(f.readline().split()[0]) + sd['PitNeut(1)'] = float_read(f.readline().split()[0]) + sd['PitNeut(2)'] = float_read(f.readline().split()[0]) + sd['PitNeut(3)'] = float_read(f.readline().split()[0]) + sd['PitSpr(1)'] = float_read(f.readline().split()[0]) + sd['PitSpr(2)'] = float_read(f.readline().split()[0]) + sd['PitSpr(3)'] = float_read(f.readline().split()[0]) + sd['PitDamp(1)'] = float_read(f.readline().split()[0]) + sd['PitDamp(2)'] = float_read(f.readline().split()[0]) + sd['PitDamp(3)'] = float_read(f.readline().split()[0]) + sd['TPitManS(1)'] = float_read(f.readline().split()[0]) + sd['TPitManS(2)'] = float_read(f.readline().split()[0]) + sd['TPitManS(3)'] = float_read(f.readline().split()[0]) + sd['PitManRat(1)'] = float_read(f.readline().split()[0]) + sd['PitManRat(2)'] = float_read(f.readline().split()[0]) + sd['PitManRat(3)'] = float_read(f.readline().split()[0]) + sd['BlPitchF(1)'] = float_read(f.readline().split()[0]) + sd['BlPitchF(2)'] = float_read(f.readline().split()[0]) + sd['BlPitchF(3)'] = float_read(f.readline().split()[0]) + + # -- Generator and Torque Control -- + f.readline() + sd['VSContrl'] = int(f.readline().split()[0]) + sd['GenModel'] = int(f.readline().split()[0]) + sd['GenEff'] = float_read(f.readline().split()[0]) + sd['GenTiStr'] = bool_read(f.readline().split()[0]) + sd['GenTiStp'] = bool_read(f.readline().split()[0]) + sd['SpdGenOn'] = float_read(f.readline().split()[0]) + sd['TimGenOn'] = float_read(f.readline().split()[0]) + sd['TimGenOf'] = float_read(f.readline().split()[0]) + + # -- Simple Variable-Speed Torque Control -- + f.readline() + sd['VS_RtGnSp'] = float_read(f.readline().split()[0]) + sd['VS_RtTq'] = float_read(f.readline().split()[0]) + sd['VS_Rgn2K'] = float_read(f.readline().split()[0]) + sd['VS_SlPc'] = float_read(f.readline().split()[0]) + + # -- Simple Induction Generator -- + f.readline() + sd['SIG_SlPc'] = float_read(f.readline().split()[0]) + sd['SIG_SySp'] = float_read(f.readline().split()[0]) + sd['SIG_RtTq'] = float_read(f.readline().split()[0]) + sd['SIG_PORt'] = float_read(f.readline().split()[0]) + + # -- Thevenin-Equivalent Induction Generator -- + f.readline() + sd['TEC_Freq'] = float_read(f.readline().split()[0]) + sd['TEC_NPol'] = int(f.readline().split()[0]) + sd['TEC_SRes'] = float_read(f.readline().split()[0]) + sd['TEC_RRes'] = float_read(f.readline().split()[0]) + sd['TEC_VLL'] = float_read(f.readline().split()[0]) + sd['TEC_SLR'] = float_read(f.readline().split()[0]) + sd['TEC_RLR'] = float_read(f.readline().split()[0]) + sd['TEC_MR'] = float_read(f.readline().split()[0]) + + # -- High-Speed Shaft Brake -- + f.readline() + sd['HSSBrMode'] = int(f.readline().split()[0]) + sd['THSSBrDp'] = float_read(f.readline().split()[0]) + sd['HSSBrDT'] = float_read(f.readline().split()[0]) + sd['HSSBrTqF'] = float_read(f.readline().split()[0]) + + # -- Nacelle-Yaw Control -- + f.readline() + sd['YCMode'] = int(f.readline().split()[0]) + sd['TYCOn'] = float_read(f.readline().split()[0]) + sd['YawNeut'] = float_read(f.readline().split()[0]) + sd['YawSpr'] = float_read(f.readline().split()[0]) + sd['YawDamp'] = float_read(f.readline().split()[0]) + sd['TYawManS'] = float_read(f.readline().split()[0]) + sd['YawManRat'] = float_read(f.readline().split()[0]) + sd['NacYawF'] = float_read(f.readline().split()[0]) + + # -- Aero Flow Control -- + f.readline() + sd['AfCmode'] = int(f.readline().split()[0]) + sd['AfC_Mean'] = float_read(f.readline().split()[0]) + sd['AfC_Amp'] = float_read(f.readline().split()[0]) + sd['AfC_Phase'] = float_read(f.readline().split()[0]) + + # -- Structural Control -- + f.readline() + sd['NumBStC'] = int(f.readline().split()[0]) + sd['BStCfiles'] = read_array(f, sd['NumBStC'], array_type=str) + sd['NumNStC'] = int(f.readline().split()[0]) + sd['NStCfiles'] = read_array(f, sd['NumNStC'], array_type=str) + sd['NumTStC'] = int(f.readline().split()[0]) + sd['TStCfiles'] = read_array(f, sd['NumTStC'], array_type=str) + sd['NumSStC'] = int(f.readline().split()[0]) + sd['SStCfiles'] = read_array(f, sd['NumSStC'], array_type=str) + + # -- Cable Control -- + f.readline() + sd['CCmode'] = int(f.readline().split()[0]) + + # -- Bladed Interface -- + f.readline() + if not path2dll: + sd['DLL_FileName'] = os.path.abspath( + os.path.normpath(os.path.join(os.path.split(sd_file)[0], + quoted_read(f.readline().split()[0])))) + else: + f.readline() + sd['DLL_FileName'] = path2dll + + sd['DLL_InFile'] = os.path.abspath( + os.path.normpath(os.path.join(os.path.split(sd_file)[0], + quoted_read(f.readline().split()[0])))) + sd['DLL_ProcName'] = quoted_read(f.readline().split()[0]) + dll_dt_line = f.readline().split()[0] + try: + sd['DLL_DT'] = float_read(dll_dt_line) + except Exception: + sd['DLL_DT'] = dll_dt_line[1:-1] + sd['DLL_Ramp'] = bool_read(f.readline().split()[0]) + sd['BPCutoff'] = float_read(f.readline().split()[0]) + sd['NacYaw_North'] = float_read(f.readline().split()[0]) + sd['Ptch_Cntrl'] = int(f.readline().split()[0]) + sd['Ptch_SetPnt'] = float_read(f.readline().split()[0]) + sd['Ptch_Min'] = float_read(f.readline().split()[0]) + sd['Ptch_Max'] = float_read(f.readline().split()[0]) + sd['PtchRate_Min'] = float_read(f.readline().split()[0]) + sd['PtchRate_Max'] = float_read(f.readline().split()[0]) + sd['Gain_OM'] = float_read(f.readline().split()[0]) + sd['GenSpd_MinOM'] = float_read(f.readline().split()[0]) + sd['GenSpd_MaxOM'] = float_read(f.readline().split()[0]) + sd['GenSpd_Dem'] = float_read(f.readline().split()[0]) + sd['GenTrq_Dem'] = float_read(f.readline().split()[0]) + sd['GenPwr_Dem'] = float_read(f.readline().split()[0]) + + f.readline() # section header + + sd['DLL_NumTrq'] = int(f.readline().split()[0]) + f.readline() # column names + f.readline() # units + sd['GenSpd_TLU'] = [None] * sd['DLL_NumTrq'] + sd['GenTrq_TLU'] = [None] * sd['DLL_NumTrq'] + for i in range(sd['DLL_NumTrq']): + data = f.readline().split() + sd['GenSpd_TLU'][i] = float_read(data[0]) + sd['GenTrq_TLU'][i] = float_read(data[1]) + + # -- Output -- + f.readline() + sd['SumPrint'] = bool_read(f.readline().split()[0]) + sd['OutFile'] = int(f.readline().split()[0]) + sd['TabDelim'] = bool_read(f.readline().split()[0]) + sd['OutFmt'] = quoted_read(f.readline().split()[0]) + sd['TStart'] = float_read(f.readline().split()[0]) + + # -- Outlist -- + f.readline() + if read_outlist_fn is not None: + read_outlist_fn(f, 'ServoDyn') + + # ------------------------------------------------------------------ + # Build result dict + # ------------------------------------------------------------------ + result: Dict[str, Any] = {'ServoDyn': sd} + + # Read StC files (paths are relative to the ServoDyn file directory) + svd_dir = os.path.dirname(servo_file_rel) if servo_file_rel else '' + + result['BStC'] = [] + for fn in sd['BStCfiles']: + result['BStC'].append(self._read_stc(fn, base_dir, svd_dir)) + result['NStC'] = [] + for fn in sd['NStCfiles']: + result['NStC'].append(self._read_stc(fn, base_dir, svd_dir)) + result['TStC'] = [] + for fn in sd['TStCfiles']: + result['TStC'].append(self._read_stc(fn, base_dir, svd_dir)) + result['SStC'] = [] + for fn in sd['SStCfiles']: + result['SStC'].append(self._read_stc(fn, base_dir, svd_dir)) + + # Read DISCON_in (ROSCO) + if _ROSCO: + discon = self._read_discon(sd, base_dir) + if discon is not None: + result['DISCON_in'] = discon + + # Read spd_trq + if sd['VSContrl'] == 3: + result['spd_trq'] = self._read_spd_trq('spd_trq.dat', base_dir) + + return result + + # ------------------------------------------------------------------ + # write + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + run_dir: str = '', + naming_out: str = '', + ) -> None: + """Write a ServoDyn input file and referenced sub-files. + + Parameters + ---------- + data + Dict with keys ``ServoDyn``, ``BStC``, ``NStC``, etc. + file_path + Target path for the ServoDyn ``.dat`` file. + base_dir + Base directory for output (defaults to dirname of *file_path*). + outlist + Outlist dict – 'ServoDyn' key used. + run_dir + Run directory used for writing sub-files (DISCON, StC, spd_trq). + Defaults to ``os.path.dirname(file_path)``. + naming_out + Naming prefix for generated sub-files. + """ + sd = data['ServoDyn'] + out_dir = run_dir or os.path.dirname(file_path) or '.' + + with open(file_path, 'w') as f: + f.write('------- SERVODYN INPUT FILE --------------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sd['Echo'], 'Echo', '- Echo input data to .ech (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['DT'], 'DT', '- Communication interval for controllers (s) (or "default")\n')) + f.write('---------------------- PITCH CONTROL -------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['PCMode'], 'PCMode', '- Pitch control mode {0: none, 3: user-defined from routine PitchCntrl, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TPCOn'], 'TPCOn', '- Time to enable active pitch control (s) [unused when PCMode=0]\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'PitNeut({idx})'], f'PitNeut({idx})', f'- Blade {idx} neutral pitch position--pitch spring moment is zero at this pitch (degrees)\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'PitSpr({idx})'], f'PitSpr({idx})', f'- Blade {idx} pitch spring constant (N-m/rad)\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'PitDamp({idx})'], f'PitDamp({idx})', f'- Blade {idx} pitch damping constant (N-m/(rad/s))\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'TPitManS({idx})'], f'TPitManS({idx})', f'- Time to start override pitch maneuver for blade {idx} and end standard pitch control (s)\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'PitManRat({idx})'], f'PitManRat({idx})', f'- Pitch rate at which override pitch maneuver heads toward final pitch angle for blade {idx} (deg/s)\n')) + for idx in range(1, 4): + f.write('{:<22} {:<11} {:}'.format(sd[f'BlPitchF({idx})'], f'BlPitchF({idx})', f'- Blade {idx} final pitch for pitch maneuvers (degrees)\n')) + + f.write('---------------------- GENERATOR AND TORQUE CONTROL ----------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['VSContrl'], 'VSContrl', '- Variable-speed control mode {0: none, 1: simple VS, 3: user-defined from routine UserVSCont, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenModel'], 'GenModel', '- Generator model {1: simple, 2: Thevenin, 3: user-defined from routine UserGen} (switch) [used only when VSContrl=0]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenEff'], 'GenEff', '- Generator efficiency [ignored by the Thevenin and user-defined generator models] (%)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['GenTiStr'], 'GenTiStr', '- Method to start the generator {T: timed using TimGenOn, F: generator speed using SpdGenOn} (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['GenTiStp'], 'GenTiStp', '- Method to stop the generator {T: timed using TimGenOf, F: when generator power = 0} (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['SpdGenOn'], 'SpdGenOn', '- Generator speed to turn on the generator for a startup (HSS speed) (rpm) [used only when GenTiStr=False]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TimGenOn'], 'TimGenOn', '- Time to turn on the generator for a startup (s) [used only when GenTiStr=True]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TimGenOf'], 'TimGenOf', '- Time to turn off the generator (s) [used only when GenTiStp=True]\n')) + + f.write('---------------------- SIMPLE VARIABLE-SPEED TORQUE CONTROL --------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['VS_RtGnSp'], 'VS_RtGnSp', '- Rated generator speed for simple variable-speed generator control (HSS side) (rpm) [used only when VSContrl=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['VS_RtTq'], 'VS_RtTq', '- Rated generator torque/constant generator torque in Region 3 for simple variable-speed generator control (HSS side) (N-m) [used only when VSContrl=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['VS_Rgn2K'], 'VS_Rgn2K', '- Generator torque constant in Region 2 for simple variable-speed generator control (HSS side) (N-m/rpm^2) [used only when VSContrl=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['VS_SlPc'], 'VS_SlPc', '- Rated generator slip percentage in Region 2 1/2 for simple variable-speed generator control (%) [used only when VSContrl=1]\n')) + + f.write('---------------------- SIMPLE INDUCTION GENERATOR ------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['SIG_SlPc'], 'SIG_SlPc', '- Rated generator slip percentage (%) [used only when VSContrl=0 and GenModel=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['SIG_SySp'], 'SIG_SySp', '- Synchronous (zero-torque) generator speed (rpm) [used only when VSContrl=0 and GenModel=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['SIG_RtTq'], 'SIG_RtTq', '- Rated torque (N-m) [used only when VSContrl=0 and GenModel=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['SIG_PORt'], 'SIG_PORt', '- Pull-out ratio (Tpullout/Trated) (-) [used only when VSContrl=0 and GenModel=1]\n')) + + f.write('---------------------- THEVENIN-EQUIVALENT INDUCTION GENERATOR -----------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['TEC_Freq'], 'TEC_Freq', '- Line frequency [50 or 60] (Hz) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_NPol'], 'TEC_NPol', '- Number of poles [even integer > 0] (-) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_SRes'], 'TEC_SRes', '- Stator resistance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_RRes'], 'TEC_RRes', '- Rotor resistance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_VLL'], 'TEC_VLL', '- Line-to-line RMS voltage (volts) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_SLR'], 'TEC_SLR', '- Stator leakage reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_RLR'], 'TEC_RLR', '- Rotor leakage reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TEC_MR'], 'TEC_MR', '- Magnetizing reactance (ohms) [used only when VSContrl=0 and GenModel=2]\n')) + + f.write('---------------------- HIGH-SPEED SHAFT BRAKE ----------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['HSSBrMode'], 'HSSBrMode', '- HSS brake model {0: none, 1: simple, 3: user-defined from routine UserHSSBr, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['THSSBrDp'], 'THSSBrDp', '- Time to initiate deployment of the HSS brake (s)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['HSSBrDT'], 'HSSBrDT', '- Time for HSS-brake to reach full deployment once initiated (sec) [used only when HSSBrMode=1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['HSSBrTqF'], 'HSSBrTqF', '- Fully deployed HSS-brake torque (N-m)\n')) + + f.write('---------------------- NACELLE-YAW CONTROL -------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['YCMode'], 'YCMode', '- Yaw control mode {0: none, 3: user-defined from routine UserYawCont, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TYCOn'], 'TYCOn', '- Time to enable active yaw control (s) [unused when YCMode=0]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['YawNeut'], 'YawNeut', '- Neutral yaw position--yaw spring force is zero at this yaw (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['YawSpr'], 'YawSpr', '- Nacelle-yaw spring constant (N-m/rad)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['YawDamp'], 'YawDamp', '- Nacelle-yaw damping constant (N-m/(rad/s))\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TYawManS'], 'TYawManS', '- Time to start override yaw maneuver and end standard yaw control (s)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['YawManRat'], 'YawManRat', '- Yaw maneuver rate (in absolute value) (deg/s)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['NacYawF'], 'NacYawF', '- Final yaw angle for override yaw maneuvers (degrees)\n')) + + f.write('---------------------- Aerodynamic Flow Control -------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['AfCmode'], 'AfCmode', '- Airfoil control mode {0: none, 1: cosine wave cycle, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['AfC_Mean'], 'AfC_Mean', '- Mean level for cosine cycling or steady value (-) [used only with AfCmode==1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['AfC_Amp'], 'AfC_Amp', '- Amplitude for for cosine cycling of flap signal (AfC = AfC_Amp*cos(Azimuth+phase)+AfC_mean) (-) [used only with AfCmode==1]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['AfC_Phase'], 'AfC_phase', '- Phase relative to the blade azimuth (0 is vertical) for for cosine cycling of flap signal (deg) [used only with AfCmode==1]\n')) + + f.write('---------------------- STRUCTURAL CONTROL ---------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['NumBStC'], 'NumBStC', '- Number of blade structural controllers (integer)\n')) + f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(sd['BStCfiles']) + '"', 'BStCfiles', '- Name of the files for blade structural controllers (quoted strings) [unused when NumBStC==0]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['NumNStC'], 'NumNStC', '- Number of nacelle structural controllers (integer)\n')) + f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(sd['NStCfiles']) + '"', 'NStCfiles', '- Name of the files for nacelle structural controllers (quoted strings) [unused when NumNStC==0]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['NumTStC'], 'NumTStC', '- Number of tower structural controllers (integer)\n')) + f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(sd['TStCfiles']) + '"', 'TStCfiles', '- Name of the files for tower structural controllers (quoted strings) [unused when NumTStC==0]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['NumSStC'], 'NumSStC', '- Number of substructure structural controllers (integer)\n')) + f.write('{!s:<22} {:<11} {:}'.format('"' + '" "'.join(sd['SStCfiles']) + '"', 'SStCfiles', '- Name of the files for substructure structural controllers (quoted strings) [unused when NumSStC==0]\n')) + + f.write('---------------------- CABLE CONTROL ---------------------------------------- \n') + f.write('{:<22} {:<11} {:}'.format(sd['CCmode'], 'CCmode', '- Cable control mode {0: none, 4: user-defined from Simulink/Labview, 5: user-defined from Bladed-style DLL} (switch)\n')) + + f.write('---------------------- BLADED INTERFACE ---------------------------------------- [used only with Bladed Interface]\n') + f.write('{:<22} {:<11} {:}'.format('"'+sd['DLL_FileName']+'"', 'DLL_FileName', '- Name/location of the dynamic library {.dll [Windows] or .so [Linux]} in the Bladed-DLL format (-) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format('"'+sd['DLL_InFile']+'"', 'DLL_InFile', '- Name of input file sent to the DLL (-) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format('"'+sd['DLL_ProcName']+'"', 'DLL_ProcName', '- Name of procedure in DLL to be called (-) [case sensitive; used only with DLL Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['DLL_DT'], 'DLL_DT', '- Communication interval for dynamic library (s) (or "default") [used only with Bladed Interface]\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['DLL_Ramp'], 'DLL_Ramp', '- Whether a linear ramp should be used between DLL_DT time steps [introduces time shift when true] (flag) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['BPCutoff'], 'BPCutoff', '- Cutoff frequency for low-pass filter on blade pitch from DLL (Hz) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['NacYaw_North'], 'NacYaw_North', '- Reference yaw angle of the nacelle when the upwind end points due North (deg) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['Ptch_Cntrl'], 'Ptch_Cntrl', '- Record 28: Use individual pitch control {0: collective pitch; 1: individual pitch control} (switch) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['Ptch_SetPnt'], 'Ptch_SetPnt', '- Record 5: Below-rated pitch angle set-point (deg) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['Ptch_Min'], 'Ptch_Min', '- Record 6: Minimum pitch angle (deg) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['Ptch_Max'], 'Ptch_Max', '- Record 7: Maximum pitch angle (deg) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['PtchRate_Min'], 'PtchRate_Min', '- Record 8: Minimum pitch rate (most negative value allowed) (deg/s) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['PtchRate_Max'], 'PtchRate_Max', '- Record 9: Maximum pitch rate (deg/s) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['Gain_OM'], 'Gain_OM', '- Record 16: Optimal mode gain (Nm/(rad/s)^2) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenSpd_MinOM'], 'GenSpd_MinOM', '- Record 17: Minimum generator speed (rpm) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenSpd_MaxOM'], 'GenSpd_MaxOM', '- Record 18: Optimal mode maximum speed (rpm) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenSpd_Dem'], 'GenSpd_Dem', '- Record 19: Demanded generator speed above rated (rpm) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenTrq_Dem'], 'GenTrq_Dem', '- Record 22: Demanded generator torque above rated (Nm) [used only with Bladed Interface]\n')) + f.write('{:<22} {:<11} {:}'.format(sd['GenPwr_Dem'], 'GenPwr_Dem', '- Record 13: Demanded power (W) [used only with Bladed Interface]\n')) + + f.write('---------------------- BLADED INTERFACE TORQUE-SPEED LOOK-UP TABLE -------------\n') + f.write('{:<22} {:<11} {:}'.format(sd['DLL_NumTrq'], 'DLL_NumTrq', '- Record 26: No. of points in torque-speed look-up table {0 = none and use the optimal mode parameters; nonzero = ignore the optimal mode PARAMETERs by setting Record 16 to 0.0} (-) [used only with Bladed Interface]\n')) + f.write('{:<22}\t{:<22}\n'.format('GenSpd_TLU', 'GenTrq_TLU')) + f.write('{:<22}\t{:<22}\n'.format('(rpm)', '(Nm)')) + for i in range(sd['DLL_NumTrq']): + f.write('{:<22}\t{:<22}\n'.format(sd['GenSpd_TLU'][i], sd['GenTrq_TLU'][i])) + + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sd['SumPrint'], 'SumPrint', '- Print summary data to .sum (flag) (currently unused)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['OutFile'], 'OutFile', '- Switch to determine where output will be placed: {1: in module output file only; 2: in glue code output file only; 3: both} (currently unused)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['TabDelim'], 'TabDelim', '- Use tab delimiters in text tabular output file? (flag) (currently unused)\n')) + f.write('{:<22} {:<11} {:}'.format('"'+sd['OutFmt']+'"', 'OutFmt', '- Format used for text tabular output (except time). Resulting field should be 10 characters. (quoted string) (currently unused)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['TStart'], 'TStart', '- Time to begin tabular output (s) (currently unused)\n')) + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + + if outlist is not None: + emit_outlist(f, outlist, 'ServoDyn') + + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + f.write('---------------------------------------------------------------------------------------\n') + + # Write StC files + for i, stc in enumerate(data.get('BStC', [])): + self._write_stc(stc, sd['BStCfiles'][i], file_path, out_dir) + for i, stc in enumerate(data.get('NStC', [])): + self._write_stc(stc, sd['NStCfiles'][i], file_path, out_dir) + for i, stc in enumerate(data.get('TStC', [])): + self._write_stc(stc, sd['TStCfiles'][i], file_path, out_dir) + for i, stc in enumerate(data.get('SStC', [])): + self._write_stc(stc, sd['SStCfiles'][i], file_path, out_dir) + + # Write spd_trq + if sd['VSContrl'] == 3 and 'spd_trq' in data: + self._write_spd_trq(data['spd_trq'], out_dir) + + # ================================================================== + # Private helpers + # ================================================================== + + # ---- StC reader -------------------------------------------------- + @staticmethod + def _read_stc(filename: str, base_dir: str, svd_dir: str) -> dict: + """Read a single Structural-Controller input file.""" + stc: Dict[str, Any] = {} + + with open(os.path.join(base_dir, svd_dir, filename)) as f: + f.readline() # header 1 + f.readline() # header 2 + f.readline() # sim control header + stc['Echo'] = bool_read(f.readline().split()[0]) + + # DOF + f.readline() + stc['StC_DOF_MODE'] = int_read(f.readline().split()[0]) + stc['StC_X_DOF'] = bool_read(f.readline().split()[0]) + stc['StC_Y_DOF'] = bool_read(f.readline().split()[0]) + stc['StC_Z_DOF'] = bool_read(f.readline().split()[0]) + + # Location + f.readline() + stc['StC_P_X'] = float_read(f.readline().split()[0]) + stc['StC_P_Y'] = float_read(f.readline().split()[0]) + stc['StC_P_Z'] = float_read(f.readline().split()[0]) + + # Initial conditions + f.readline() + stc['StC_X_DSP'] = float_read(f.readline().split()[0]) + stc['StC_Y_DSP'] = float_read(f.readline().split()[0]) + stc['StC_Z_DSP'] = float_read(f.readline().split()[0]) + stc['StC_Z_PreLd'] = f.readline().split()[0] + + # Configuration + f.readline() + stc['StC_X_PSP'] = float_read(f.readline().split()[0]) + stc['StC_X_NSP'] = float_read(f.readline().split()[0]) + stc['StC_Y_PSP'] = float_read(f.readline().split()[0]) + stc['StC_Y_NSP'] = float_read(f.readline().split()[0]) + stc['StC_Z_PSP'] = float_read(f.readline().split()[0]) + stc['StC_Z_NSP'] = float_read(f.readline().split()[0]) + + # Mass, stiffness, damping + f.readline() + stc['StC_X_M'] = float_read(f.readline().split()[0]) + stc['StC_Y_M'] = float_read(f.readline().split()[0]) + stc['StC_Z_M'] = float_read(f.readline().split()[0]) + stc['StC_Omni_M'] = float_read(f.readline().split()[0]) + stc['StC_X_K'] = float_read(f.readline().split()[0]) + stc['StC_Y_K'] = float_read(f.readline().split()[0]) + stc['StC_Z_K'] = float_read(f.readline().split()[0]) + stc['StC_X_C'] = float_read(f.readline().split()[0]) + stc['StC_Y_C'] = float_read(f.readline().split()[0]) + stc['StC_Z_C'] = float_read(f.readline().split()[0]) + stc['StC_X_KS'] = float_read(f.readline().split()[0]) + stc['StC_Y_KS'] = float_read(f.readline().split()[0]) + stc['StC_Z_KS'] = float_read(f.readline().split()[0]) + stc['StC_X_CS'] = float_read(f.readline().split()[0]) + stc['StC_Y_CS'] = float_read(f.readline().split()[0]) + stc['StC_Z_CS'] = float_read(f.readline().split()[0]) + + # User-defined spring forces + f.readline() + stc['Use_F_TBL'] = bool_read(f.readline().split()[0]) + stc['NKInpSt'] = int_read(f.readline().split()[0]) + + table: Dict[str, list] = {} + table['X'] = [None] * stc['NKInpSt'] + table['F_X'] = [None] * stc['NKInpSt'] + table['Y'] = [None] * stc['NKInpSt'] + table['F_Y'] = [None] * stc['NKInpSt'] + table['Z'] = [None] * stc['NKInpSt'] + table['F_Z'] = [None] * stc['NKInpSt'] + + f.readline() # section header + f.readline() # col names + f.readline() # units + for i in range(stc['NKInpSt']): + ln = f.readline().split() + table['X'][i] = float(ln[0]) + table['F_X'][i] = float(ln[1]) + table['Y'][i] = float(ln[2]) + table['F_Y'][i] = float(ln[3]) + table['Z'][i] = float(ln[4]) + table['F_Z'][i] = float(ln[5]) + stc['SpringForceTable'] = table + + # Control + f.readline() + stc['StC_CMODE'] = int_read(f.readline().split()[0]) + stc['StC_CChan'] = int_read(f.readline().split()[0]) + stc['StC_SA_MODE'] = int_read(f.readline().split()[0]) + stc['StC_X_C_LOW'] = float_read(f.readline().split()[0]) + stc['StC_X_C_HIGH'] = float_read(f.readline().split()[0]) + stc['StC_Y_C_HIGH'] = float_read(f.readline().split()[0]) + stc['StC_Y_C_LOW'] = float_read(f.readline().split()[0]) + stc['StC_Z_C_HIGH'] = float_read(f.readline().split()[0]) + stc['StC_Z_C_LOW'] = float_read(f.readline().split()[0]) + stc['StC_X_C_BRAKE'] = float_read(f.readline().split()[0]) + stc['StC_Y_C_BRAKE'] = float_read(f.readline().split()[0]) + stc['StC_Z_C_BRAKE'] = float_read(f.readline().split()[0]) + + # TLCD + f.readline() + stc['L_X'] = float_read(f.readline().split()[0]) + stc['B_X'] = float_read(f.readline().split()[0]) + stc['area_X'] = float_read(f.readline().split()[0]) + stc['area_ratio_X'] = float_read(f.readline().split()[0]) + stc['headLossCoeff_X'] = float_read(f.readline().split()[0]) + stc['rho_X'] = float_read(f.readline().split()[0]) + stc['L_Y'] = float_read(f.readline().split()[0]) + stc['B_Y'] = float_read(f.readline().split()[0]) + stc['area_Y'] = float_read(f.readline().split()[0]) + stc['area_ratio_Y'] = float_read(f.readline().split()[0]) + stc['headLossCoeff_Y'] = float_read(f.readline().split()[0]) + stc['rho_Y'] = float_read(f.readline().split()[0]) + + # Prescribed time series + f.readline() + stc['PrescribedForcesCoord'] = int_read(f.readline().split()[0]) + stc['PrescribedForcesFile'] = os.path.join(base_dir, quoted_read(f.readline().split()[0])) + f.readline() # trailing line + + return stc + + # ---- StC writer -------------------------------------------------- + @staticmethod + def _write_stc(stc: dict, filename: str, sd_file_path: str, out_dir: str) -> None: + """Write a single StC input file.""" + sd_dir = os.path.dirname(sd_file_path) + stc_file = os.path.join(sd_dir, filename) + + with open(stc_file, 'w') as f: + f.write('------- STRUCTURAL CONTROL (StC) INPUT FILE ----------------------------\n') + f.write('Generated with OpenFAST_IO\n') + + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(stc['Echo'], 'Echo', '- Echo input data to ".SD.ech" (flag)\n')) + + f.write('---------------------- StC DEGREES OF FREEDOM ----------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_DOF_MODE'], 'StC_DOF_MODE', '- DOF mode (switch) {0: No StC or TLCD DOF; 1: StC_X_DOF, StC_Y_DOF, and/or StC_Z_DOF (three independent StC DOFs); 2: StC_XY_DOF (Omni-Directional StC); 3: StC_XYZ_DOF (Omni-Directional StC); 5: TLCD; 6: Prescribed force/moment time series; 7: Force determined by external DLL}\n')) + f.write('{!s:<22} {:<11} {:}'.format(stc['StC_X_DOF'], 'StC_X_DOF', '- DOF on or off for StC X (flag) [Used only when StC_DOF_MODE=1]\n')) + f.write('{!s:<22} {:<11} {:}'.format(stc['StC_Y_DOF'], 'StC_Y_DOF', '- DOF on or off for StC Y (flag) [Used only when StC_DOF_MODE=1]\n')) + f.write('{!s:<22} {:<11} {:}'.format(stc['StC_Z_DOF'], 'StC_Z_DOF', '- DOF on or off for StC Z (flag) [Used only when StC_DOF_MODE=1]\n')) + + f.write('---------------------- StC LOCATION ------------------------------------------- [relative to the reference origin of component attached to]\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_P_X'], 'StC_P_X', '- At rest X position of StC (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_P_Y'], 'StC_P_Y', '- At rest Y position of StC (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_P_Z'], 'StC_P_Z', '- At rest Z position of StC (m)\n')) + + f.write('---------------------- StC INITIAL CONDITIONS --------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_DSP'], 'StC_X_DSP', '- StC X initial displacement (m) [relative to at rest position]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_DSP'], 'StC_Y_DSP', '- StC Y initial displacement (m) [relative to at rest position]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_DSP'], 'StC_Z_DSP', '- StC Z initial displacement (m) [relative to at rest position; used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{!s:<22} {:<11} {:}'.format(stc['StC_Z_PreLd'], 'StC_Z_PreLd', '- StC Z pre-load (N) {"gravity" to offset for gravity load; "none" or 0 to turn off} [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + + f.write('---------------------- StC CONFIGURATION -------------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_PSP'], 'StC_X_PSP', '- Positive stop position (maximum X mass displacement) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_NSP'], 'StC_X_NSP', '- Negative stop position (minimum X mass displacement) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_PSP'], 'StC_Y_PSP', '- Positive stop position (maximum Y mass displacement) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_NSP'], 'StC_Y_NSP', '- Negative stop position (minimum Y mass displacement) (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_PSP'], 'StC_Z_PSP', '- Positive stop position (maximum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_NSP'], 'StC_Z_NSP', '- Negative stop position (minimum Z mass displacement) (m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + + f.write('---------------------- StC MASS, STIFFNESS, & DAMPING ------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_M'], 'StC_X_M', '- StC X mass (kg) [used only when StC_DOF_MODE=1 and StC_X_DOF=TRUE]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_M'], 'StC_Y_M', '- StC Y mass (kg) [used only when StC_DOF_MODE=1 and StC_Y_DOF=TRUE]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_M'], 'StC_Z_M', '- StC Z mass (kg) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Omni_M'], 'StC_Omni_M', '- StC omni mass (kg) [used only when StC_DOF_MODE=2 or 3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_K'], 'StC_X_K', '- StC X stiffness (N/m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_K'], 'StC_Y_K', '- StC Y stiffness (N/m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_K'], 'StC_Z_K', '- StC Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_C'], 'StC_X_C', '- StC X damping (N/(m/s))\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_C'], 'StC_Y_C', '- StC Y damping (N/(m/s))\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_C'], 'StC_Z_C', '- StC Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_KS'], 'StC_X_KS', '- Stop spring X stiffness (N/m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_KS'], 'StC_Y_KS', '- Stop spring Y stiffness (N/m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_KS'], 'StC_Z_KS', '- Stop spring Z stiffness (N/m) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_CS'], 'StC_X_CS', '- Stop spring X damping (N/(m/s))\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_CS'], 'StC_Y_CS', '- Stop spring Y damping (N/(m/s))\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_CS'], 'StC_Z_CS', '- Stop spring Z damping (N/(m/s)) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + + f.write('---------------------- StC USER-DEFINED SPRING FORCES ------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') + f.write('{!s:<22} {:<11} {:}'.format(stc['Use_F_TBL'], 'Use_F_TBL', '- Use spring force from user-defined table (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['NKInpSt'], 'NKInpSt', '- Number of spring force input stations\n')) + + f.write('---------------------- StC SPRING FORCES TABLE -------------------------------- [used only when StC_DOF_MODE=1, 2, or 3]\n') + f.write('X F_X Y F_Y Z F_Z\n') + f.write('(m) (N) (m) (N) (m) (N)\n') + table = stc['SpringForceTable'] + for x, f_x, y, f_y, z, f_z in zip(table['X'], table['F_X'], table['Y'], table['F_Y'], table['Z'], table['F_Z']): + row = [x, f_x, y, f_y, z, f_z] + f.write(' '.join(['{: 2.8e}'.format(val) for val in row]) + '\n') + + f.write('---------------------- StructUserProp CONTROL -------------------------------------------- [used only when StC_DOF_MODE=1, 2, 3, or 7]\n') + f.write('{:<22} {:<11} {:}'.format(stc['StC_CMODE'], 'StC_CMODE', '- Control mode (switch) {0:none; 1: Semi-Active Control Mode; 3: Active Control Mode through user subroutine; 4: Active Control Mode through Simulink (not available); 5: Active Control Mode through Bladed interface}\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_CChan'], 'StC_CChan', '- Control channel group (1:10) for stiffness and damping (StC_[XYZ]_K, StC_[XYZ]_C, and StC_[XYZ]_Brake) (specify additional channels for blade instances of StC active control -- one channel per blade) [used only when StC_DOF_MODE=1, 2, 3, or 7, and StC_CMODE=4 or 5]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_SA_MODE'], 'StC_SA_MODE', '- Semi-Active control mode {1: velocity-based ground hook control; 2: Inverse velocity-based ground hook control; 3: displacement-based ground hook control 4: Phase difference Algorithm with Friction Force 5: Phase difference Algorithm with Damping Force} (-)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_C_HIGH'], 'StC_X_C_HIGH', '- StC X high damping for ground hook control\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_C_LOW'], 'StC_X_C_LOW', '- StC X low damping for ground hook control\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_C_HIGH'], 'StC_Y_C_HIGH', '- StC Y high damping for ground hook control\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_C_LOW'], 'StC_Y_C_LOW', '- StC Y low damping for ground hook control\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_C_HIGH'], 'StC_Z_C_HIGH', '- StC Z high damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_C_LOW'], 'StC_Z_C_LOW', '- StC Z low damping for ground hook control [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_X_C_BRAKE'], 'StC_X_C_BRAKE', '- StC X high damping for braking the StC (Don\'t use it now. should be zero)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Y_C_BRAKE'], 'StC_Y_C_BRAKE', '- StC Y high damping for braking the StC (Don\'t use it now. should be zero)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['StC_Z_C_BRAKE'], 'StC_Z_C_BRAKE', '- StC Z high damping for braking the StC (Don\'t use it now. should be zero) [used only when StC_DOF_MODE=1 and StC_Z_DOF=TRUE or when StC_DOF_MODE=3]\n')) + + f.write('---------------------- TLCD --------------------------------------------------- [used only when StC_DOF_MODE=5]\n') + f.write('{:<22} {:<11} {:}'.format(stc['L_X'], 'L_X', '- X TLCD total length (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['B_X'], 'B_X', '- X TLCD horizontal length (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['area_X'], 'area_X', '- X TLCD cross-sectional area of vertical column (m^2)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['area_ratio_X'], 'area_ratio_X', '- X TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['headLossCoeff_X'], 'headLossCoeff_X', '- X TLCD head loss coeff (-)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['rho_X'], 'rho_X', '- X TLCD liquid density (kg/m^3)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['L_Y'], 'L_Y', '- Y TLCD total length (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['B_Y'], 'B_Y', '- Y TLCD horizontal length (m)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['area_Y'], 'area_Y', '- Y TLCD cross-sectional area of vertical column (m^2)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['area_ratio_Y'], 'area_ratio_Y', '- Y TLCD cross-sectional area ratio (vertical column area divided by horizontal column area) (-)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['headLossCoeff_Y'], 'headLossCoeff_Y', '- Y TLCD head loss coeff (-)\n')) + f.write('{:<22} {:<11} {:}'.format(stc['rho_Y'], 'rho_Y', '- Y TLCD liquid density (kg/m^3)\n')) + + f.write('---------------------- PRESCRIBED TIME SERIES --------------------------------- [used only when StC_DOF_MODE=6]\n') + f.write('{:<22} {:<11} {:}'.format(stc['PrescribedForcesCoord'], 'PrescribedForcesCoord', '- Prescribed forces are in global or local coordinates (switch) {1: global; 2: local}\n')) + f.write('{!s:<22} {:<11} {:}'.format(stc['PrescribedForcesFile'], 'PrescribedForcesFile', '- Time series force and moment (7 columns of time, FX, FY, FZ, MX, MY, MZ)\n')) + f.write('-------------------------------------------------------------------------------\n') + + # ---- DISCON reader ----------------------------------------------- + @staticmethod + def _read_discon(sd: dict, base_dir: str) -> Optional[dict]: + """Read ROSCO DISCON input file if it exists.""" + if not _ROSCO: + return None + + discon_in_file = os.path.normpath( + os.path.join(base_dir, sd['DLL_InFile'])) + + if not os.path.exists(discon_in_file): + return None + + discon = read_DISCON(discon_in_file) + + # Additional filename parsing + discon_dir = os.path.dirname(discon_in_file) + discon['PerfFileName'] = os.path.abspath( + os.path.join(discon_dir, discon['PerfFileName'])) + + # Try to read rotor performance data + try: + pitch_vector, tsr_vector, Cp_table, Ct_table, Cq_table = \ + load_from_txt(discon['PerfFileName']) + RotorPerformance = ROSCO_turbine.RotorPerformance + discon['Cp'] = RotorPerformance(Cp_table, pitch_vector, tsr_vector) + discon['Ct'] = RotorPerformance(Ct_table, pitch_vector, tsr_vector) + discon['Cq'] = RotorPerformance(Cq_table, pitch_vector, tsr_vector) + discon['Cp_pitch_initial_rad'] = pitch_vector + discon['Cp_TSR_initial'] = tsr_vector + discon['Cp_table'] = Cp_table + discon['Ct_table'] = Ct_table + discon['Cq_table'] = Cq_table + except Exception: + print('WARNING: Cp table not loaded!') + + discon['v_rated'] = 1.0 + return discon + + # ---- spd_trq reader ---------------------------------------------- + @staticmethod + def _read_spd_trq(filename: str, base_dir: str) -> dict: + """Read speed-torque look-up table.""" + spd_trq: Dict[str, Any] = {} + filepath = os.path.normpath(os.path.join(base_dir, filename)) + with open(filepath) as f: + spd_trq['header'] = f.readline() + data = f.readlines() + spd_trq['RPM'] = [float(line.split()[0]) for line in data] + spd_trq['Torque'] = [float(line.split()[1]) for line in data] + return spd_trq + + # ---- spd_trq writer ---------------------------------------------- + @staticmethod + def _write_spd_trq(spd_trq: dict, out_dir: str) -> None: + """Write speed-torque look-up table.""" + filepath = os.path.join(out_dir, 'spd_trq.dat') + with open(filepath, 'w') as f: + f.write('{:}'.format(spd_trq['header'], '\n')) + for i in range(len(spd_trq['RPM'])): + f.write('{:<22f} {:<22f} {:}'.format( + spd_trq['RPM'][i], spd_trq['Torque'][i], '\n')) diff --git a/openfast_io/openfast_io/io/simple_elastodyn.py b/openfast_io/openfast_io/io/simple_elastodyn.py new file mode 100644 index 0000000000..52d4a161ef --- /dev/null +++ b/openfast_io/openfast_io/io/simple_elastodyn.py @@ -0,0 +1,161 @@ +""" +SimpleElastoDynIO – read / write Simplified ElastoDyn (SED) input files. + +Produces ``{'SimpleElastoDyn': sed}`` +""" +from __future__ import annotations + +import os +from typing import Any, Callable, Dict, Optional + +from .base import ModuleIO +from ..outlist import emit_outlist +from ..parsing import bool_read, float_read, int_read + + +class SimpleElastoDynIO(ModuleIO): + """Read / write SimpleElastoDyn input files.""" + + # ------------------------------------------------------------------ + # READ + # ------------------------------------------------------------------ + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + sed: Dict[str, Any] = {} + f = open(file_path) + + f.readline() + f.readline() + f.readline() + sed['Echo'] = bool_read(f.readline().split()[0]) + sed['IntMethod'] = int_read(f.readline().split()[0]) + sed['DT'] = float_read(f.readline().split()[0]) + + # Degrees of Freedom + f.readline() + sed['GenDOF'] = bool_read(f.readline().split()[0]) + sed['YawDOF'] = bool_read(f.readline().split()[0]) + + # Initial Conditions + f.readline() + sed['Azimuth'] = float_read(f.readline().split()[0]) + sed['BlPitch'] = float_read(f.readline().split()[0]) + sed['RotSpeed'] = float_read(f.readline().split()[0]) + sed['NacYaw'] = float_read(f.readline().split()[0]) + sed['PtfmPitch'] = float_read(f.readline().split()[0]) + + # Turbine Configuration + f.readline() + sed['NumBl'] = int_read(f.readline().split()[0]) + sed['TipRad'] = float_read(f.readline().split()[0]) + sed['HubRad'] = float_read(f.readline().split()[0]) + sed['PreCone'] = float_read(f.readline().split()[0]) + sed['OverHang'] = float_read(f.readline().split()[0]) + sed['ShftTilt'] = float_read(f.readline().split()[0]) + sed['Twr2Shft'] = float_read(f.readline().split()[0]) + sed['TowerHt'] = float_read(f.readline().split()[0]) + + # Mass and Inertia + f.readline() + sed['RotIner'] = float_read(f.readline().split()[0]) + sed['GenIner'] = float_read(f.readline().split()[0]) + + # Drivetrain + f.readline() + sed['GBoxRatio'] = float_read(f.readline().split()[0]) + + # Output + f.readline() + f.readline() + + # Read output list — route into the shared registry (mirrors legacy openfast_io) + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'SimpleElastoDyn') + else: + sed['_outlist'] = {} + data = f.readline() + while data.split().__len__() == 0: + data = f.readline() + while data.split()[0] != 'END': + if data.find('"') >= 0: + channels = data.split('"') + channel_list = channels[1].split(',') + else: + row_string = data.split(',') + if len(row_string) == 1: + channel_list = row_string[0].split('\n')[0] + else: + channel_list = row_string + if isinstance(channel_list, list): + for ch in channel_list: + ch = ch.strip() + if ch: + sed['_outlist'][ch] = True + else: + ch = channel_list.strip() + if ch: + sed['_outlist'][ch] = True + data = f.readline() + + f.close() + return {'SimpleElastoDyn': sed} + + # ------------------------------------------------------------------ + # WRITE + # ------------------------------------------------------------------ + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + sed = data['SimpleElastoDyn'] + with open(file_path, 'w') as f: + f.write('------- SIMPLIFIED ELASTODYN INPUT FILE ----------------------------------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('---------------------- SIMULATION CONTROL --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sed['Echo'], 'Echo', '- Echo input data to ".ech" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['IntMethod'], 'IntMethod', '- Integration method: {1: RK4, 2: AB4, or 3: ABM4} (-)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['DT'], 'DT', '- Integration time step (s)\n')) + f.write('---------------------- DEGREES OF FREEDOM --------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sed['GenDOF'], 'GenDOF', '- Generator DOF (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sed['YawDOF'], 'YawDOF', '- Yaw degree of freedom -- controlled by controller (flag)\n')) + f.write('---------------------- INITIAL CONDITIONS --------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sed['Azimuth'], 'Azimuth', '- Initial azimuth angle for blades (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['BlPitch'], 'BlPitch', '- Blades initial pitch (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['RotSpeed'], 'RotSpeed', '- Initial or fixed rotor speed (rpm)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['NacYaw'], 'NacYaw', '- Initial or fixed nacelle-yaw angle (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['PtfmPitch'], 'PtfmPitch', '- Fixed pitch tilt rotational displacement of platform (degrees)\n')) + f.write('---------------------- TURBINE CONFIGURATION -----------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sed['NumBl'], 'NumBl', '- Number of blades (-)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['TipRad'], 'TipRad', '- The distance from the rotor apex to the blade tip (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['HubRad'], 'HubRad', '- The distance from the rotor apex to the blade root (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['PreCone'], 'PreCone', '- Blades cone angle (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['OverHang'], 'OverHang', '- Distance from yaw axis to rotor apex [3 blades] or teeter pin [2 blades] (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['ShftTilt'], 'ShftTilt', '- Rotor shaft tilt angle (degrees)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['Twr2Shft'], 'Twr2Shft', '- Vertical distance from the tower-top to the rotor shaft (meters)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['TowerHt'], 'TowerHt', '- Height of tower above ground level [onshore] or MSL [offshore] (meters)\n')) + f.write('---------------------- MASS AND INERTIA ----------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sed['RotIner'], 'RotIner', '- Rot inertia about rotor axis [blades + hub] (kg m^2)\n')) + f.write('{:<22} {:<11} {:}'.format(sed['GenIner'], 'GenIner', '- Generator inertia about HSS (kg m^2)\n')) + f.write('---------------------- DRIVETRAIN ----------------------------------------------\n') + f.write('{:<22} {:<11} {:}'.format(sed['GBoxRatio'], 'GBoxRatio', '- Gearbox ratio (-)\n')) + f.write('---------------------- OUTPUT --------------------------------------------------\n') + f.write(' OutList - The next line(s) contains a list of output parameters. See OutListParameters.xlsx for a listing of available output channels, (-)\n') + if outlist is not None: + emit_outlist(f, outlist, 'SimpleElastoDyn') + else: + for ch in sed.get('_outlist', {}): # standalone fallback (no shared registry) + f.write('"' + ch + '"\n') + f.write('END of input file (the word "END" must appear in the first 3 columns of the last OutList line)\n') + f.write('---------------------------------------------------------------------------------------\n') diff --git a/openfast_io/openfast_io/io/subdyn.py b/openfast_io/openfast_io/io/subdyn.py new file mode 100644 index 0000000000..e8377daf20 --- /dev/null +++ b/openfast_io/openfast_io/io/subdyn.py @@ -0,0 +1,569 @@ +""" +SubDynIO – read / write SubDyn input files. + +Produces ``{'SubDyn': sd}`` +""" +from __future__ import annotations + +import os +from typing import Any, Dict, Optional, Callable + +import numpy as np + +from .base import ModuleIO +from ..outlist import emit_outlist, capture_outlist +from ..parsing import ( + bool_read, + float_read, + int_read, + quoted_read, + read_array, + readline_filterComments, +) + + +class SubDynIO(ModuleIO): + """Read / write SubDyn input files.""" + + def read( + self, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + read_outlist_fn: Optional[Callable] = None, + **kwargs, + ) -> dict: + sd: Dict[str, Any] = {} + sd_file = os.path.normpath(os.path.join(base_dir, file_path)) if base_dir else file_path + + f = open(sd_file) + f.readline() + f.readline() + f.readline() + + # SIMULATION CONTROL + sd['Echo'] = bool_read(f.readline().split()[0]) + sd['SDdeltaT'] = float_read(f.readline().split()[0]) + sd['IntMethod'] = int_read(f.readline().split()[0]) + sd['SttcSolve'] = bool_read(f.readline().split()[0]) + f.readline() + + # FEA and CRAIG-BAMPTON PARAMETERS + sd['FEMMod'] = int_read(f.readline().split()[0]) + sd['NDiv'] = int_read(f.readline().split()[0]) + sd['Nmodes'] = int_read(f.readline().split()[0]) + sd['JDampings'] = float_read(f.readline().split()[0]) + sd['GuyanDampMod'] = int_read(f.readline().split()[0]) + sd['RayleighDamp'] = read_array(f, 2, array_type=float) + sd['GuyanDampSize'] = int_read(f.readline().split()[0]) + sd['GuyanDamp'] = np.array([ + [float_read(idx.strip(',')) for idx in f.readline().strip().split()[:sd['GuyanDampSize']]] + for _ in range(sd['GuyanDampSize']) + ]) + + f.readline(); f.readline(); f.readline() + + # INITIAL RIGID-BODY POSITION + ln = f.readline().split() + sd['RBSurge'] = float(ln[0]) + sd['RBSway'] = float(ln[1]) + sd['RBHeave'] = float(ln[2]) + sd['RBRoll'] = float(ln[3]) + sd['RBPitch'] = float(ln[4]) + sd['RBYaw'] = float(ln[5]) + + f.readline() + + # STRUCTURE JOINTS + sd['NJoints'] = int_read(f.readline().split()[0]) + n = sd['NJoints'] + for k in ['JointID', 'JointXss', 'JointYss', 'JointZss', 'JointType', + 'JointDirX', 'JointDirY', 'JointDirZ', 'JointStiff']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['JointID'][i] = int(ln[0]) + sd['JointXss'][i] = float(ln[1]) + sd['JointYss'][i] = float(ln[2]) + sd['JointZss'][i] = float(ln[3]) + sd['JointType'][i] = int(ln[4]) + sd['JointDirX'][i] = float(ln[5]) + sd['JointDirY'][i] = float(ln[6]) + sd['JointDirZ'][i] = float(ln[7]) + sd['JointStiff'][i] = float(ln[8]) + + f.readline() + + # BASE REACTION JOINTS + sd['NReact'] = int_read(f.readline().split()[0]) + n = sd['NReact'] + for k in ['RJointID', 'RctTDXss', 'RctTDYss', 'RctTDZss', + 'RctRDXss', 'RctRDYss', 'RctRDZss', 'Rct_SoilFile']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['RJointID'][i] = int(ln[0]) + sd['RctTDXss'][i] = int(ln[1]) + sd['RctTDYss'][i] = int(ln[2]) + sd['RctTDZss'][i] = int(ln[3]) + sd['RctRDXss'][i] = int(ln[4]) + sd['RctRDYss'][i] = int(ln[5]) + sd['RctRDZss'][i] = int(ln[6]) + sd['Rct_SoilFile'][i] = ln[7] if len(ln) == 8 else 'None' + + f.readline() + + # INTERFACE JOINTS + sd['NInterf'] = int_read(f.readline().split()[0]) + n = sd['NInterf'] + for k in ['IJointID', 'TPID', 'ItfTDXss', 'ItfTDYss', 'ItfTDZss', + 'ItfRDXss', 'ItfRDYss', 'ItfRDZss']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['IJointID'][i] = int(ln[0]) + sd['TPID'][i] = int(ln[1]) + sd['ItfTDXss'][i] = int(ln[2]) + sd['ItfTDYss'][i] = int(ln[3]) + sd['ItfTDZss'][i] = int(ln[4]) + sd['ItfRDXss'][i] = int(ln[5]) + sd['ItfRDYss'][i] = int(ln[6]) + sd['ItfRDZss'][i] = int(ln[7]) + + f.readline() + + # MEMBERS + sd['NMembers'] = int_read(f.readline().split()[0]) + n = sd['NMembers'] + for k in ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', + 'MType', 'M_Spin', 'M_COSMID']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['MemberID'][i] = int(ln[0]) + sd['MJointID1'][i] = int(ln[1]) + sd['MJointID2'][i] = int(ln[2]) + sd['MPropSetID1'][i] = int(ln[3]) + sd['MPropSetID2'][i] = int(ln[4]) + if ln[5].lower() == '1c': + sd['MType'][i] = 1 + elif ln[5].lower() == '1r': + sd['MType'][i] = -1 + else: + sd['MType'][i] = int(ln[5]) + if sd['MType'][i] == 5: + sd['M_Spin'][i] = 0. + sd['M_COSMID'][i] = int(ln[6]) + else: + sd['M_Spin'][i] = float(ln[6]) + sd['M_COSMID'][i] = -1 + + f.readline() + + # MEMBER X-SECTION PROPERTY DATA 1/3 — Circular + sd['NPropSetsCyl'] = int_read(f.readline().split()[0]) + n = sd['NPropSetsCyl'] + for k in ['PropSetID1', 'YoungE1', 'ShearG1', 'MatDens1', 'XsecD', 'XsecT']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['PropSetID1'][i] = int(ln[0]) + sd['YoungE1'][i] = float(ln[1]) + sd['ShearG1'][i] = float(ln[2]) + sd['MatDens1'][i] = float(ln[3]) + sd['XsecD'][i] = float(ln[4]) + sd['XsecT'][i] = float(ln[5]) + + f.readline() + + # MEMBER X-SECTION PROPERTY DATA 2/3 — Rectangular + sd['NPropSetsRec'] = int_read(f.readline().split()[0]) + n = sd['NPropSetsRec'] + for k in ['PropSetID2', 'YoungE2', 'ShearG2', 'MatDens2', 'XsecSa', 'XsecSb', 'XsecT2']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['PropSetID2'][i] = int(ln[0]) + sd['YoungE2'][i] = float(ln[1]) + sd['ShearG2'][i] = float(ln[2]) + sd['MatDens2'][i] = float(ln[3]) + sd['XsecSa'][i] = float(ln[4]) + sd['XsecSb'][i] = float(ln[5]) + sd['XsecT2'][i] = float(ln[6]) + + f.readline() + + # MEMBER X-SECTION PROPERTY DATA 3/3 — Arbitrary + sd['NXPropSets'] = int_read(f.readline().split()[0]) + n = sd['NXPropSets'] + arb_keys = ['PropSetID3', 'YoungE3', 'ShearG3', 'MatDens3', 'XsecA', + 'XsecAsx', 'XsecAsy', 'XsecJxx', 'XsecJyy', 'XsecJ0', 'XsecJt'] + for k in arb_keys: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['PropSetID3'][i] = int(ln[0]) + for j, k in enumerate(arb_keys[1:], 1): + sd[k][i] = float(ln[j]) + + # CABLE PROPERTIES + f.readline() + sd['NCablePropSets'] = int_read(f.readline().split()[0]) + n = sd['NCablePropSets'] + for k in ['CablePropSetID', 'CableEA', 'CableMatDens', 'CableT0']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['CablePropSetID'][i] = int(ln[0]) + sd['CableEA'][i] = float(ln[1]) + sd['CableMatDens'][i] = float(ln[2]) + sd['CableT0'][i] = float(ln[3]) + + # RIGID LINK PROPERTIES + f.readline() + sd['NRigidPropSets'] = int_read(f.readline().split()[0]) + n = sd['NRigidPropSets'] + for k in ['RigidPropSetID', 'RigidMatDens']: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['RigidPropSetID'][i] = int(ln[0]) + sd['RigidMatDens'][i] = float(ln[1]) + + # SPRING ELEMENT PROPERTIES + f.readline() + sd['NSpringPropSets'] = int_read(f.readline().split()[0]) + n = sd['NSpringPropSets'] + spring_list = ['k11','k12','k13','k14','k15','k16', + 'k22','k23','k24','k25','k26', + 'k33','k34','k35','k36', + 'k44','k45','k46', + 'k55','k56', + 'k66'] + sd['SpringPropSetID'] = [None] * n + for sl in spring_list: + sd[sl] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['SpringPropSetID'][i] = int(ln[0]) + for j, sl in enumerate(spring_list): + sd[sl][i] = ln[j + 1] + + # MEMBER COSINE MATRICES + f.readline() + sd['NCOSMs'] = int_read(f.readline().split()[0]) + n = sd['NCOSMs'] + cosm_keys = ['COSMID', 'COSM11', 'COSM12', 'COSM13', + 'COSM21', 'COSM22', 'COSM23', + 'COSM31', 'COSM32', 'COSM33'] + for k in cosm_keys: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['COSMID'][i] = int(ln[0]) + for j in range(1, 10): + sd[cosm_keys[j]][i] = float(ln[j]) + + f.readline() + + # JOINT ADDITIONAL CONCENTRATED MASSES + sd['NCmass'] = int_read(f.readline().split()[0]) + n = sd['NCmass'] + mass_keys = ['CMJointID', 'JMass', 'JMXX', 'JMYY', 'JMZZ', + 'JMXY', 'JMXZ', 'JMYZ', 'MCGX', 'MCGY', 'MCGZ'] + for k in mass_keys: + sd[k] = [None] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split() + sd['CMJointID'][i] = int(ln[0]) + for j, k in enumerate(mass_keys[1:], 1): + sd[k][i] = float(ln[j]) + + f.readline() + + # OUTPUT + sd['SumPrint'] = bool_read(f.readline().split()[0]) + + # Optional OutCBModes / OutFEMModes + file_pos = f.tell() + line = f.readline() + if 'OutCBModes' in line: + sd['OutCBModes'] = int_read(line.split()[0]) + else: + f.seek(file_pos) + file_pos = f.tell() + line = f.readline() + if 'OutFEMModes' in line: + sd['OutFEMModes'] = int_read(line.split()[0]) + else: + f.seek(file_pos) + + sd['OutCOSM'] = bool_read(f.readline().split()[0]) + sd['OutAll'] = bool_read(f.readline().split()[0]) + sd['OutSwtch'] = int_read(f.readline().split()[0]) + sd['TabDelim'] = bool_read(f.readline().split()[0]) + sd['OutDec'] = int_read(f.readline().split()[0]) + sd['OutFmt'] = quoted_read(f.readline().split()[0]) + sd['OutSFmt'] = quoted_read(f.readline().split()[0]) + + f.readline() + + # MEMBER OUTPUT LIST + sd['NMOutputs'] = int_read(f.readline().split()[0]) + n = sd['NMOutputs'] + sd['MemberID_out'] = [None] * n + sd['NOutCnt'] = [None] * n + sd['NodeCnt'] = [[None]] * n + f.readline(); f.readline() + for i in range(n): + ln = f.readline().split('!')[0].split() + sd['MemberID_out'][i] = int(ln[0]) + sd['NOutCnt'][i] = int(ln[1]) + sd['NodeCnt'][i] = [int(node) for node in ln[2:]] + + f.readline() + + # SSOutList + if read_outlist_fn is not None and outlist is not None: + read_outlist_fn(f, 'SubDyn') + else: + # Standalone use with no shared registry — consume the section + # safely and stash the found channels so write() can still emit + # them (mirrors the aerodisk.py fallback pattern). + channels = capture_outlist(f, None, 'SubDyn', freeform=True) + sd['_outlist'] = {ch: True for ch in channels} + + f.close() + return {'SubDyn': sd} + + def write( + self, + data: dict, + file_path: str, + base_dir: str = '', + *, + outlist: Optional[dict] = None, + **kwargs, + ) -> None: + sd = data['SubDyn'] + with open(file_path, 'w') as f: + f.write('----------- SubDyn MultiMember Support Structure Input File ------------\n') + f.write('Generated with OpenFAST_IO\n') + f.write('-------------------------- SIMULATION CONTROL ---------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sd['Echo'], 'Echo', '- Echo input data to ".SD.ech" (flag)\n')) + f.write('{:<22} {:<11} {:}'.format(sd['SDdeltaT'], 'SDdeltaT', '- Local Integration Step. Use "default" to base it on the glue-code time step.\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['IntMethod'], 'IntMethod', '- Integration Method [1/2/3/4 = RK4/AB4/ABM4/AM2].\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['SttcSolve'], 'SttcSolve', '- Solve dynamics about static equilibrium point\n')) + f.write('--- FEA and CRAIG-BAMPTON PARAMETERS ---\n') + f.write('{:<22d} {:<11} {:}'.format(sd['FEMMod'], 'FEMMod', '- FEM switch: element model in the FEM. [1= Euler-Bernoulli(E-B); 2=Timoshenko; 3= E-B w/ shear; 4=MITC3+]\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['NDiv'], 'NDiv', '- Number of sub-elements per member\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['Nmodes'], 'Nmodes', '- Number of internal modes to retain.\n')) + f.write('{:<22} {:<11} {:}'.format(sd['JDampings'], 'JDampings', '- Damping Ratios for each retained mode.\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['GuyanDampMod'], 'GuyanDampMod', '- Guyan damping (switch)\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([str(v) for v in sd['RayleighDamp']]), 'RayleighDamp', '- Mass and stiffness proportional damping coefficients\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['GuyanDampSize'], 'GuyanDampSize', '- Guyan damping matrix size (square)\n')) + for i in range(sd['GuyanDampSize']): + try: + f.write(' '.join(['{:14}'.format(v) for v in sd['GuyanDamp'][i, :]]) + '\n') + except Exception: + f.write(' '.join(['{:14}'.format(v) for v in sd['GuyanDamp'][i]]) + '\n') + f.write('------- INITIAL RIGID-BODY POSITION [used only for floating structure with more than one transition pieces] -------\n') + f.write(" ".join(['{:^11s}'.format(i) for i in ['RBSurge', 'RBSway', 'RBHeave', 'RBRoll', 'RBPitch', 'RBYaw']]) + '\n') + f.write(" ".join(['{:^11s}'.format(i) for i in ['(m)', '(m)', '(m)', '(deg)', '(deg)', '(deg)']]) + '\n') + f.write(" ".join(['{:^11}'.format(sd[k]) for k in ['RBSurge', 'RBSway', 'RBHeave', 'RBRoll', 'RBPitch', 'RBYaw']]) + '\n') + f.write('---- STRUCTURE JOINTS ----\n') + + # Helper for writing a table section + def _write_table(keys, header_keys, units, count_key, count_label, desc): + f.write('{:<22d} {:<11} {:}'.format(sd[count_key], count_label, desc + '\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in header_keys]) + '\n') + f.write(' '.join(['{:^11s}'.format(u) for u in units]) + '\n') + + # Joints + _write_table([], ['JointID', 'JointXss', 'JointYss', 'JointZss', 'JointType', 'JointDirX', 'JointDirY', 'JointDirZ', 'JointStiff'], + ['(-)', '(m)', '(m)', '(m)', '(-)', '(-)', '(-)', '(-)', '(Nm/rad)'], + 'NJoints', 'NJoints', '- Number of joints (-)') + for i in range(sd['NJoints']): + f.write(' '.join(['{:^11d}'.format(sd['JointID'][i]), + '{:^11}'.format(sd['JointXss'][i]), '{:^11}'.format(sd['JointYss'][i]), '{:^11}'.format(sd['JointZss'][i]), + '{:^11d}'.format(sd['JointType'][i]), + '{:^11}'.format(sd['JointDirX'][i]), '{:^11}'.format(sd['JointDirY'][i]), '{:^11}'.format(sd['JointDirZ'][i]), + '{:^11}'.format(sd['JointStiff'][i])]) + '\n') + + # Base Reactions + f.write('---- BASE REACTION JOINTS ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NReact'], 'NReact', '- Number of joints with reaction forces\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['RJointID', 'RctTDXss', 'RctTDYss', 'RctTDZss', 'RctRDXss', 'RctRDYss', 'RctRDZss', 'Rct_SoilFile']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(8)]) + '\n') + for i in range(sd['NReact']): + f.write(' '.join(['{:^11d}'.format(sd['RJointID'][i]), + '{:^11d}'.format(sd['RctTDXss'][i]), '{:^11d}'.format(sd['RctTDYss'][i]), '{:^11d}'.format(sd['RctTDZss'][i]), + '{:^11d}'.format(sd['RctRDXss'][i]), '{:^11d}'.format(sd['RctRDYss'][i]), '{:^11d}'.format(sd['RctRDZss'][i]), + '{:^11}'.format(sd['Rct_SoilFile'][i])]) + '\n') + + # Interface Joints + f.write('---- INTERFACE JOINTS ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NInterf'], 'NInterf', '- Number of interface joints\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['IJointID', 'TPID', 'ItfTDXss', 'ItfTDYss', 'ItfTDZss', 'ItfRDXss', 'ItfRDYss', 'ItfRDZss']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(8)]) + '\n') + for i in range(sd['NInterf']): + f.write(' '.join(['{:^11d}'.format(sd['IJointID'][i]), '{:^11d}'.format(sd['TPID'][i]), + '{:^11d}'.format(sd['ItfTDXss'][i]), '{:^11d}'.format(sd['ItfTDYss'][i]), '{:^11d}'.format(sd['ItfTDZss'][i]), + '{:^11d}'.format(sd['ItfRDXss'][i]), '{:^11d}'.format(sd['ItfRDYss'][i]), '{:^11d}'.format(sd['ItfRDZss'][i])]) + '\n') + + # Members + f.write('---- MEMBERS ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NMembers'], 'NMembers', '- Number of frame members\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['MemberID', 'MJointID1', 'MJointID2', 'MPropSetID1', 'MPropSetID2', 'MType', 'COSMID/Spin']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(7)]) + '\n') + for i in range(sd['NMembers']): + if sd['MType'][i] == 5: + spin_val = '{:^11d}'.format(sd['M_COSMID'][i]) + else: + spin_val = '{:^11}'.format(sd['M_Spin'][i]) + f.write(' '.join(['{:^11d}'.format(sd['MemberID'][i]), + '{:^11d}'.format(sd['MJointID1'][i]), '{:^11d}'.format(sd['MJointID2'][i]), + '{:^11d}'.format(sd['MPropSetID1'][i]), '{:^11d}'.format(sd['MPropSetID2'][i]), + '{:^11d}'.format(sd['MType'][i]), spin_val]) + '\n') + + # Cylindrical Section Properties + f.write('---- MEMBER X-SECTION PROPERTY data 1/3 [isotropic material for circular cross-sections] ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NPropSetsCyl'], 'NPropSetsCyl', '- Number of circular member property sets\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecD', 'XsecT']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(6)]) + '\n') + for i in range(sd['NPropSetsCyl']): + f.write(' '.join(['{:^11d}'.format(sd['PropSetID1'][i]), + '{:^11}'.format(sd['YoungE1'][i]), '{:^11}'.format(sd['ShearG1'][i]), + '{:^11}'.format(sd['MatDens1'][i]), + '{:^11}'.format(sd['XsecD'][i]), '{:^11}'.format(sd['XsecT'][i])]) + '\n') + + # Rectangular + f.write('---- MEMBER X-SECTION PROPERTY data 2/3 [isotropic material for rectangular cross-sections] ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NPropSetsRec'], 'NPropSetsRec', '- Number of rectangular member property sets\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecSa', 'XsecSb', 'XsecT']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(7)]) + '\n') + for i in range(sd['NPropSetsRec']): + f.write(' '.join(['{:^11d}'.format(sd['PropSetID2'][i]), + '{:^11}'.format(sd['YoungE2'][i]), '{:^11}'.format(sd['ShearG2'][i]), + '{:^11}'.format(sd['MatDens2'][i]), + '{:^11}'.format(sd['XsecSa'][i]), '{:^11}'.format(sd['XsecSb'][i]), + '{:^11}'.format(sd['XsecT2'][i])]) + '\n') + + # Arbitrary + f.write('---- MEMBER X-SECTION PROPERTY data 3/3 [arbitrary cross-sections] ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NXPropSets'], 'NXPropSets', '- Number of arbitrary member property sets\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID', 'YoungE', 'ShearG', 'MatDens', 'XsecA', 'XsecAsx', 'XsecAsy', 'XsecJxx', 'XsecJyy', 'XsecJ0', 'XsecJt']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(11)]) + '\n') + arb_keys = ['PropSetID3', 'YoungE3', 'ShearG3', 'MatDens3', 'XsecA', + 'XsecAsx', 'XsecAsy', 'XsecJxx', 'XsecJyy', 'XsecJ0', 'XsecJt'] + for i in range(sd['NXPropSets']): + ln = ['{:^11d}'.format(int(sd['PropSetID3'][i]))] + for k in arb_keys[1:]: + ln.append('{:^11}'.format(sd[k][i])) + f.write(' '.join(ln) + '\n') + + # Cable + f.write('---- CABLE PROPERTIES ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NCablePropSets'], 'NCablePropSets', '- Number of cable property sets\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID', 'EA', 'MatDens', 'T0']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(4)]) + '\n') + for i in range(sd['NCablePropSets']): + f.write(' '.join(['{:^11d}'.format(sd['CablePropSetID'][i]), + '{:^11}'.format(sd['CableEA'][i]), + '{:^11}'.format(sd['CableMatDens'][i]), + '{:^11}'.format(sd['CableT0'][i])]) + '\n') + + # Rigid Link + f.write('---- RIGID LINK PROPERTIES ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NRigidPropSets'], 'NRigidPropSets', '- Number of rigid link property sets\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID', 'MatDens']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(2)]) + '\n') + for i in range(sd['NRigidPropSets']): + f.write(' '.join(['{:^11d}'.format(sd['RigidPropSetID'][i]), + '{:^11}'.format(sd['RigidMatDens'][i])]) + '\n') + + # Spring + f.write('---- SPRING ELEMENT PROPERTIES ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NSpringPropSets'], 'NSpringPropSets', '- Number of spring element property sets\n')) + spring_list = ['k11','k12','k13','k14','k15','k16', + 'k22','k23','k24','k25','k26', + 'k33','k34','k35','k36', + 'k44','k45','k46', + 'k55','k56','k66'] + f.write(' '.join(['{:^11s}'.format(h) for h in ['PropSetID'] + spring_list]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(len(spring_list) + 1)]) + '\n') + for i in range(sd['NSpringPropSets']): + ln = ['{:^11d}'.format(sd['SpringPropSetID'][i])] + for sl in spring_list: + ln.append('{:^11}'.format(sd[sl][i])) + f.write(' '.join(ln) + '\n') + + # COSM + f.write('---- MEMBER COSINE MATRICES ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NCOSMs'], 'NCOSMs', '- Number of unique cosine matrices\n')) + cosm_keys = ['COSMID', 'COSM11', 'COSM12', 'COSM13', 'COSM21', 'COSM22', 'COSM23', 'COSM31', 'COSM32', 'COSM33'] + f.write(' '.join(['{:^11s}'.format(h) for h in cosm_keys]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in cosm_keys]) + '\n') + for i in range(sd['NCOSMs']): + ln = ['{:^11d}'.format(sd['COSMID'][i])] + for k in cosm_keys[1:]: + ln.append('{:^11}'.format(sd[k][i])) + f.write(' '.join(ln) + '\n') + + # Concentrated Masses + f.write('---- JOINT ADDITIONAL CONCENTRATED MASSES ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NCmass'], 'NCmass', '- Number of joints with concentrated masses\n')) + mass_keys = ['CMJointID', 'JMass', 'JMXX', 'JMYY', 'JMZZ', 'JMXY', 'JMXZ', 'JMYZ', 'MCGX', 'MCGY', 'MCGZ'] + f.write(' '.join(['{:^11s}'.format(h) for h in mass_keys]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in mass_keys]) + '\n') + for i in range(sd['NCmass']): + ln = ['{:^11d}'.format(sd['CMJointID'][i])] + for k in mass_keys[1:]: + ln.append('{:^11}'.format(sd[k][i])) + f.write(' '.join(ln) + '\n') + + # Output + f.write('---- OUTPUT ------------------------------------------------------------------\n') + f.write('{!s:<22} {:<11} {:}'.format(sd['SumPrint'], 'SumPrint', '- Output summary file\n')) + if 'OutCBModes' in sd: + f.write('{:<22d} {:<11} {:}'.format(sd['OutCBModes'], 'OutCBModes', '- Output CB modes\n')) + if 'OutFEMModes' in sd: + f.write('{:<22d} {:<11} {:}'.format(sd['OutFEMModes'], 'OutFEMModes', '- Output FEM modes\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['OutCOSM'], 'OutCOSM', '- Output cosine matrices (flag)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['OutAll'], 'OutAll', '- Output all member/joint loads (flag)\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['OutSwtch'], 'OutSwtch', '- Output channels (switch)\n')) + f.write('{!s:<22} {:<11} {:}'.format(sd['TabDelim'], 'TabDelim', '- Tab-delimited output (flag)\n')) + f.write('{:<22d} {:<11} {:}'.format(sd['OutDec'], 'OutDec', '- Decimation of output\n')) + f.write('{:<22} {:<11} {:}'.format(sd['OutFmt'], 'OutFmt', '- Output format\n')) + f.write('{:<22} {:<11} {:}'.format(sd['OutSFmt'], 'OutSFmt', '- Output format for header strings\n')) + + # Member Output List + f.write('---- MEMBER OUTPUT LIST ----\n') + f.write('{:<22d} {:<11} {:}'.format(sd['NMOutputs'], 'NMOutputs', '- Number of member outputs\n')) + f.write(' '.join(['{:^11s}'.format(h) for h in ['MemberID', 'NOutCnt', 'NodeCnt']]) + '\n') + f.write(' '.join(['{:^11s}'.format('(-)') for _ in range(3)]) + '\n') + for i in range(sd['NMOutputs']): + f.write(' '.join(['{:^11d}'.format(sd['MemberID_out'][i]), + '{:^11d}'.format(sd['NOutCnt'][i]), + ' '.join([str(nc) for nc in sd['NodeCnt'][i]])]) + '\n') + + f.write('---- SSOutList ----\n') + if outlist is not None: + emit_outlist(f, outlist, 'SubDyn') + else: + for ch in sd.get('_outlist', {}): # standalone fallback (no shared registry) + f.write('"' + ch + '"\n') + f.write('END of output channels and end of file.\n') diff --git a/openfast_io/openfast_io/outlist.py b/openfast_io/openfast_io/outlist.py new file mode 100644 index 0000000000..230051db21 --- /dev/null +++ b/openfast_io/openfast_io/outlist.py @@ -0,0 +1,249 @@ +"""OutList: clean interface for OpenFAST output channel management. + +Replaces the per-channel bool-dict pattern in FAST_vars_out.FstOutput with +a set-based approach. The legacy bool-dict is still available via to_fst_output() +for backwards compatibility. +""" +from __future__ import annotations +import copy +from typing import Iterable + +try: + from .FAST_vars_out import FstOutput +except ImportError: + FstOutput = {} + + +class OutList: + """Manages output channel selections for an OpenFAST simulation. + + Internally stores enabled channels per module as sets of channel name strings. + The FstOutput registry is used for validation only, not as the primary data store. + + Usage:: + + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1', 'GenPwr']) + ol.enable('AeroDyn', ['RtAeroCp', 'RtAeroCt']) + print(ol.enabled('ElastoDyn')) # {'RotSpeed', 'BldPitch1', 'GenPwr'} + print(ol.all_enabled()) # flat list of all enabled channel names + """ + + def __init__(self): + # module_name → set of enabled channel names + self._channels: dict[str, set[str]] = {} + + def enable(self, module: str, channels: Iterable[str]) -> None: + """Enable output channels for a module.""" + if module not in self._channels: + self._channels[module] = set() + self._channels[module].update(channels) + + def disable(self, module: str, channels: Iterable[str]) -> None: + """Disable specific channels for a module.""" + if module in self._channels: + self._channels[module] -= set(channels) + + def enabled(self, module: str) -> set[str]: + """Get set of enabled channel names for a module.""" + return set(self._channels.get(module, set())) + + def all_enabled(self) -> list[str]: + """Get flat sorted list of ALL enabled channel names across all modules.""" + result = [] + for module in sorted(self._channels): + result.extend(sorted(self._channels[module])) + return result + + def enabled_by_module(self) -> dict[str, list[str]]: + """Get enabled channels grouped by module, sorted.""" + return {m: sorted(chs) for m, chs in sorted(self._channels.items()) if chs} + + def is_enabled(self, channel: str) -> bool: + """Check if a channel is enabled in any module.""" + return any(channel in chs for chs in self._channels.values()) + + def clear(self, module: str | None = None) -> None: + """Clear all channels for a module, or all modules if None.""" + if module is None: + self._channels.clear() + elif module in self._channels: + self._channels[module].clear() + + def validate(self) -> list[str]: + """Return list of enabled channels not found in the FstOutput registry. + + These channels may cause OpenFAST to error at runtime. + The FstOutput registry is auto-generated from + openfast/docs/OtherSupporting/OutListParameters.xlsx. + """ + registry = FstOutput # read-only check — no deepcopy + unknown = [] + for module, channels in self._channels.items(): + reg_module = registry.get(module, {}) + if not reg_module: + unknown.extend(f"{module}.{ch}" for ch in sorted(channels)) + else: + known = _flatten_keys(reg_module) + for ch in sorted(channels): + if ch not in known: + unknown.append(f"{module}.{ch}") + return unknown + + # ── Conversion to/from legacy bool-dict format ── + + def to_fst_output(self) -> dict: + """Convert to the legacy FstOutput bool-dict format for backwards compatibility. + + Returns a fresh deepcopy of FstOutput with all channels set to False, + then only the enabled channels set to True. + """ + out = copy.deepcopy(FstOutput) if FstOutput else {} + # Reset everything to False first + for module in out: + if isinstance(out[module], dict): + _set_all_false(out[module]) + # Now enable only the channels in our sets + for module, channels in self._channels.items(): + if module in out: + _set_channels_in_dict(out[module], channels) + return out + + @classmethod + def from_fst_output(cls, fst_output: dict) -> 'OutList': + """Create OutList from a legacy FstOutput bool-dict (e.g., fst_vt['outlist']).""" + ol = cls() + for module, vartree in fst_output.items(): + if isinstance(vartree, dict): + enabled = _extract_true_channels(vartree) + if enabled: + ol._channels[module] = enabled + return ol + + # ── File I/O (used by drivers) ── + + @staticmethod + def read_from_file(f, module: str) -> set[str]: + """Read an OutList section from an open file handle. + + Works for both structured and free-form OutList formats. + Returns the set of channel names found. + """ + channels = set() + data = f.readline() + while data.strip() == '': + data = f.readline() + while data and not data.strip().startswith('END'): + line = data.split('!')[0] # strip comment + line = line.split('-')[0] if '-' in line and '"' not in line.split('-')[0] else line.split('!')[0] + for delim in ['"', "'", ',', ';', '\t']: + line = line.replace(delim, ' ') + tokens = [w.strip() for w in line.split() if w.strip()] + channels.update(tokens) + data = f.readline() + while data.strip() == '': + data = f.readline() + return channels + + def write_to_file(self, f, module: str) -> None: + """Write an OutList section to an open file handle.""" + channels = sorted(self._channels.get(module, set())) + for ch in channels: + f.write(f'"{ch}"\n') + f.write('END of OutList section (the word "END" must appear in the first 3 columns of the last OutList line)\n') + + +def _parse_outlist_section(f) -> list[str]: + """Parse channel tokens from an OutList section until 'END'. + + Mirrors legacy openfast_io FAST_reader.read_outlist parsing verbatim (split before the + '-' comment, strip quote/comma/semicolon/tab delimiters) so the captured set + matches the legacy reader exactly. + """ + all_channels: list[str] = [] + data = f.readline() + while data.strip() == '': + data = f.readline() + while data and not data.strip().startswith('END'): + line = data.split('-')[0] + for delim in ['"', "'", ',', ';', '\t']: + line = line.replace(delim, ' ') + line_channels = [w.strip() for w in line.split() if w.strip()] + if line_channels: + all_channels.extend(line_channels) + data = f.readline() + while data.strip() == '': + data = f.readline() + return all_channels + + +def capture_outlist(f, registry: dict, module: str, freeform: bool = False) -> list[str]: + """Read an OutList section and set the found channels True in registry[module]. + + Mirrors legacy openfast_io FAST_reader.read_outlist (freeform=False, registry-filtered via + set_outlist semantics) and read_outlist_freeForm (freeform=True, stores every + channel even if not in the registry — used by SubDyn/SeaState). + """ + channels = _parse_outlist_section(f) + if registry is None: + return channels + if not isinstance(registry.get(module), dict): + registry[module] = {} + if freeform: + for ch in channels: + registry[module][ch] = True + elif channels: + _set_channels_in_dict(registry[module], set(channels)) + return channels + + +def emit_outlist(f, registry: dict, module: str) -> None: + """Write the truthy channels of registry[module] as quoted OutList lines. + + The caller writes the section header and the 'END' line; this writes only the + channel body in between. + """ + if not registry: + return + for ch in sorted(_extract_true_channels(registry.get(module, {}))): + f.write(f'"{ch}"\n') + + +def _flatten_keys(d: dict) -> set[str]: + """Recursively collect all leaf keys from a nested dict.""" + keys = set() + for k, v in d.items(): + if isinstance(v, dict): + keys.update(_flatten_keys(v)) + else: + keys.add(k) + return keys + + +def _set_all_false(vartree: dict) -> None: + """Recursively set all leaf bool values to False in a nested dict.""" + for k, v in vartree.items(): + if isinstance(v, dict): + _set_all_false(v) + elif isinstance(v, bool): + vartree[k] = False + + +def _set_channels_in_dict(vartree: dict, channels: set[str]) -> None: + """Recursively set matching channel keys to True in a nested dict.""" + for k, v in vartree.items(): + if isinstance(v, dict): + _set_channels_in_dict(v, channels) + elif k in channels: + vartree[k] = True + + +def _extract_true_channels(vartree: dict) -> set[str]: + """Recursively extract channel names set to True from a nested dict.""" + result = set() + for k, v in vartree.items(): + if isinstance(v, dict): + result.update(_extract_true_channels(v)) + elif v is True: + result.add(k) + return result diff --git a/openfast_io/openfast_io/parsing.py b/openfast_io/openfast_io/parsing.py new file mode 100644 index 0000000000..f9cdc47d8a --- /dev/null +++ b/openfast_io/openfast_io/parsing.py @@ -0,0 +1,204 @@ +"""Low-level parsing utilities for OpenFAST input files. + +Extracted from FAST_reader.py to enable use by both the legacy monolithic +reader and the new per-module IO classes without circular imports. + +All functions preserve their original behavior exactly. +""" +import os +import re + + +def readline_filterComments(f): + """ + Filter out comments and empty lines from a file + + Args: + f: file handle + + Returns: + line: next line in the file that is not a comment or empty + """ + read = True + while read: + line = f.readline().strip() + if len(line)>0: + if line[0] != '!': + read = False + return line + +def readline_ignoreComments(f, char = '#'): # see line 64 in NWTC_IO.f90 + """ + returns line before comment character + + Args: + f: file handle + char: comment character + + Returns: + line: content of next line in the file before comment character + """ + + line = f.readline().strip().split(char) + + return line[0] + +def read_array(f,len,split_val=None,array_type=str): + """ + Read an array of values from a line in a file + + Args: + f: file handle + len: number of values to read + split_val: value to stop reading at + array_type: type of values to return + + Returns: + arr: list of values read from the file line with the specified type + """ + + + strings = re.split(',| ',f.readline().strip()) + while '' in strings: # remove empties + strings.remove('') + + if len is None and split_val is None: + raise Exception('Must have len or split_val to use read_array') + + if len is not None: + arr = strings[:len] # select len strings + else: + arr = [] + for s in strings: + if s != split_val: + arr.append(s) + else: + break + + if array_type==str: + arr = [ar.replace('"','') for ar in arr] # remove quotes and commas + elif array_type==float: + arr = [float_read(ar) for ar in arr] + elif array_type==int: + arr = [int_read(ar) for ar in arr] + elif array_type==bool: + arr = [bool_read(ar) for ar in arr] + else: + raise Exception(f"read_array with type {str(array_type)} not currently supported") + + return arr + +def fix_path(name): + """ + split a path, then reconstruct it using os.path.join + + Args: + name: path to fix + + Returns: + new: reconstructed path + """ + name = re.split("\\|/", name) + new = name[0] + for i in range(1,len(name)): + new = os.path.join(new, name[i]) + return new + +def bool_read(text): + """ + Read a boolean value from a string + + Args: + text: string to read + + Returns: + True if the string is 'true', False otherwise + """ + if 'default' in text.lower(): + return str(text) + else: + text = text.lower() + if text == 'true' or text == 't': + return True + else: + return False + +def float_read(text): + """ + Read a float value from a string, with error handling for 'default' values + + Args: + text: string to read + + Returns: + float value if the string can be converted, string otherwise + """ + if 'default' in text.lower(): + return str(text) + else: + try: + return float(text) + except: + return str(text) + +def int_read(text): + """ + Read an integer value from a string, with error handling for 'default' values + + Args: + text: string to read + + Returns: + int value if the string can be converted, string otherwise + """ + if 'default' in text.lower(): + return str(text) + else: + try: + return int(text) + except: + return str(text) + +def quoted_read(text): + """ + Read a quoted value from a string (i.e. a value between quotes) + + Args: + text: string to read + + Returns: + quoted value if the string is quoted, unquoted value otherwise + + """ + if '"' in text: + return text.split('"')[1] + elif "'" in text: + return text.split("'")[1] + else: + return text + + +def fmt_field(val, min_width: int = 28) -> str: + """Format a single value field for OpenFAST input file lines. + + Returns a left-justified string that is guaranteed to have at least 2 + trailing spaces regardless of how long the string representation of *val* + is. This prevents the common bug where a value whose string exactly fills + the format width concatenates with the parameter name on the right. + + Usage:: + + f.write(fmt_field(dvr['SomeVec']) + 'ParamName - description\\n') + + Args: + val: The value to format. May be any type — it is coerced to + ``str``. + min_width: Minimum field width (not including the mandatory 2-space + separator). Defaults to 28. + + Returns: + ``str(val).ljust(max(len(str(val)) + 2, min_width))`` + """ + s = str(val) + width = max(len(s) + 2, min_width) + return s.ljust(width) diff --git a/openfast_io/openfast_io/schema.py b/openfast_io/openfast_io/schema.py new file mode 100644 index 0000000000..6fc10b746d --- /dev/null +++ b/openfast_io/openfast_io/schema.py @@ -0,0 +1,161 @@ +"""Human-written parameter schema for openfast_io modules. + +DO NOT auto-generate this file from Fortran Registry files. +Entries are written deliberately — description and units require domain knowledge. + +To find what parameters are missing for a new OpenFAST version, run: + python -m openfast_io.tools.check_registry_drift --openfast-root /path/to/openfast +""" + +# Schema dict: version → module → param_name → metadata +_SCHEMA: dict[str, dict[str, dict]] = { + '5.0.0': { + 'ElastoDyn': { + 'Echo': {'type': bool, 'desc': 'Echo input to .ech file', 'units': None}, + 'Method': {'type': int, 'desc': 'Integration method {1:RK4, 2:AB4, 3:ABM4}', + 'units': None, 'enum': [1, 2, 3]}, + 'DT': {'type': float, 'desc': 'ElastoDyn integration time step', 'units': 's'}, + 'FlapDOF1': {'type': bool, 'desc': 'First flapwise blade mode DOF', 'units': None}, + 'FlapDOF2': {'type': bool, 'desc': 'Second flapwise blade mode DOF', 'units': None}, + 'EdgeDOF': {'type': bool, 'desc': 'First edgewise blade mode DOF', 'units': None}, + 'PitchDOF': {'type': bool, 'desc': 'Blade pitch DOF', 'units': None}, + 'TeetDOF': {'type': bool, 'desc': 'Rotor-teeter DOF [2-blade only]', 'units': None}, + 'DrTrDOF': {'type': bool, 'desc': 'Drivetrain rotational-flexibility DOF', 'units': None}, + 'GenDOF': {'type': bool, 'desc': 'Generator DOF', 'units': None}, + 'YawDOF': {'type': bool, 'desc': 'Nacelle-yaw DOF', 'units': None}, + 'TwFADOF1': {'type': bool, 'desc': 'First fore-aft tower bending-mode DOF', 'units': None}, + 'TwFADOF2': {'type': bool, 'desc': 'Second fore-aft tower bending-mode DOF', 'units': None}, + 'TwSSDOF1': {'type': bool, 'desc': 'First side-to-side tower bending-mode DOF', 'units': None}, + 'TwSSDOF2': {'type': bool, 'desc': 'Second side-to-side tower bending-mode DOF', 'units': None}, + 'PtfmSgDOF': {'type': bool, 'desc': 'Platform horizontal surge translation DOF', 'units': None}, + 'PtfmSwDOF': {'type': bool, 'desc': 'Platform horizontal sway translation DOF', 'units': None}, + 'PtfmHvDOF': {'type': bool, 'desc': 'Platform vertical heave translation DOF', 'units': None}, + 'PtfmRDOF': {'type': bool, 'desc': 'Platform roll tilt rotation DOF', 'units': None}, + 'PtfmPDOF': {'type': bool, 'desc': 'Platform pitch tilt rotation DOF', 'units': None}, + 'PtfmYDOF': {'type': bool, 'desc': 'Platform yaw rotation DOF', 'units': None}, + 'OoPDefl': {'type': float, 'desc': 'Initial out-of-plane blade-tip displacement', 'units': 'm'}, + 'IPDefl': {'type': float, 'desc': 'Initial in-plane blade-tip displacement', 'units': 'm'}, + 'BlPitch1': {'type': float, 'desc': 'Blade 1 initial pitch', 'units': 'deg'}, + 'BlPitch2': {'type': float, 'desc': 'Blade 2 initial pitch', 'units': 'deg'}, + 'BlPitch3': {'type': float, 'desc': 'Blade 3 initial pitch', 'units': 'deg'}, + 'TeetDefl': {'type': float, 'desc': 'Initial or fixed teeter angle', 'units': 'deg'}, + 'Azimuth': {'type': float, 'desc': 'Initial azimuth angle for blade 1', 'units': 'deg'}, + 'RotSpeed': {'type': float, 'desc': 'Initial or fixed rotor speed', 'units': 'rpm'}, + 'NacYaw': {'type': float, 'desc': 'Initial or fixed nacelle-yaw angle', 'units': 'deg'}, + 'NumBl': {'type': int, 'desc': 'Number of blades', 'units': None, 'enum': [1, 2, 3]}, + 'TipRad': {'type': float, 'desc': 'Blade tip-to-hub radius (preconed)', 'units': 'm'}, + 'HubRad': {'type': float, 'desc': 'Hub radius (preconed)', 'units': 'm'}, + 'TowerHt': {'type': float, 'desc': 'Height of tower above ground or MSL', 'units': 'm'}, + 'TowerBsHt': {'type': float, 'desc': 'Height of tower base above ground or MSL', 'units': 'm'}, + 'GBoxEff': {'type': float, 'desc': 'Gearbox efficiency', 'units': '%'}, + 'GBRatio': {'type': float, 'desc': 'Gearbox ratio', 'units': None}, + 'DTTorSpr': {'type': float, 'desc': 'Drivetrain torsional spring', 'units': 'N-m/rad'}, + 'DTTorDmp': {'type': float, 'desc': 'Drivetrain torsional damper', 'units': 'N-m/(rad/s)'}, + 'BldFile': {'type': list, 'desc': 'Blade property file paths', 'units': None, + 'is_array': True, 'item_type': str, 'is_file_ref': True}, + 'TwrFile': {'type': str, 'desc': 'Tower property file path', 'units': None, + 'is_file_ref': True}, + 'BldNodes': {'type': int, 'desc': 'Number of blade nodes per blade for analysis', 'units': None}, + 'TwrNodes': {'type': int, 'desc': 'Number of tower nodes for analysis', 'units': None}, + 'HubMass': {'type': float, 'desc': 'Hub mass', 'units': 'kg'}, + 'NacMass': {'type': float, 'desc': 'Nacelle mass', 'units': 'kg'}, + 'PtfmMass': {'type': float, 'desc': 'Platform mass', 'units': 'kg'}, + 'GenIner': {'type': float, 'desc': 'Generator inertia about HSS', 'units': 'kg m^2'}, + 'ShftTilt': {'type': float, 'desc': 'Rotor shaft tilt angle', 'units': 'deg'}, + 'OverHang': {'type': float, 'desc': 'Distance from yaw axis to rotor apex', 'units': 'm'}, + 'Twr2Shft': {'type': float, 'desc': 'Vertical distance from tower-top to rotor shaft', 'units': 'm'}, + }, + 'Fst': { + 'Echo': {'type': bool, 'desc': 'Echo input to .ech file', 'units': None}, + 'AbortLevel': {'type': str, 'desc': 'Error level for aborting', 'units': None}, + 'TMax': {'type': float, 'desc': 'Total run time', 'units': 's'}, + 'DT': {'type': float, 'desc': 'Global recommended time step', 'units': 's'}, + 'ModCoupling': {'type': int, 'desc': 'Module coupling method', 'units': None, 'enum': [1, 2, 3]}, + 'InterpOrder': {'type': int, 'desc': 'Interpolation order for input-output extrap', 'units': None}, + 'NRotors': {'type': int, 'desc': 'Number of rotors', 'units': None}, + 'CompElast': {'type': int, 'desc': 'Structural dynamics module {1:ElastoDyn, 2:ED+BD, 3:SimpleED}', + 'units': None, 'enum': [1, 2, 3]}, + 'CompInflow': {'type': int, 'desc': 'Inflow wind module {0:still air, 1:InflowWind, 2:ExtInflow}', + 'units': None, 'enum': [0, 1, 2]}, + 'CompAero': {'type': int, 'desc': 'Aerodynamics module {0:none, 1:AeroDisk, 2:AeroDyn, 3:ExtLoads}', + 'units': None, 'enum': [0, 1, 2, 3]}, + 'CompServo': {'type': int, 'desc': 'Controller/drive module {0:none, 1:ServoDyn}', + 'units': None, 'enum': [0, 1]}, + 'CompHydro': {'type': int, 'desc': 'Hydrodynamics module {0:none, 1:HydroDyn}', + 'units': None, 'enum': [0, 1]}, + 'CompSeaSt': {'type': int, 'desc': 'Sea state module {0:none, 1:SeaState}', + 'units': None, 'enum': [0, 1]}, + 'CompSub': {'type': int, 'desc': 'Substructure module {0:none, 1:SubDyn, 2:ExtPtfm}', + 'units': None, 'enum': [0, 1, 2]}, + 'CompMooring': {'type': int, 'desc': 'Mooring module {0:none, 1:MAP, 2:FEAMooring, 3:MoorDyn, 4:OrcaFlex}', + 'units': None, 'enum': [0, 1, 2, 3, 4]}, + 'CompIce': {'type': int, 'desc': 'Ice module {0:none, 1:IceFloe, 2:IceDyn}', + 'units': None, 'enum': [0, 1, 2]}, + 'CompSoil': {'type': int, 'desc': 'Soil module {0:none, 1:SoilDyn}', + 'units': None, 'enum': [0, 1]}, + 'MHK': {'type': int, 'desc': 'MHK turbine type {0:Not MHK, 1:Fixed MHK, 2:Floating MHK}', + 'units': None, 'enum': [0, 1, 2]}, + 'Gravity': {'type': float, 'desc': 'Gravitational acceleration', 'units': 'm/s^2'}, + 'AirDens': {'type': float, 'desc': 'Air density', 'units': 'kg/m^3'}, + 'WtrDens': {'type': float, 'desc': 'Water density', 'units': 'kg/m^3'}, + 'WtrDpth': {'type': float, 'desc': 'Water depth', 'units': 'm'}, + 'EDFile': {'type': str, 'desc': 'ElastoDyn input file path', 'units': None, + 'is_file_ref': True}, + 'AeroFile': {'type': str, 'desc': 'AeroDyn input file path', 'units': None, + 'is_file_ref': True}, + 'ServoFile': {'type': str, 'desc': 'ServoDyn input file path', 'units': None, + 'is_file_ref': True}, + 'InflowFile': {'type': str, 'desc': 'InflowWind input file path', 'units': None, + 'is_file_ref': True}, + 'HydroFile': {'type': str, 'desc': 'HydroDyn input file path', 'units': None, + 'is_file_ref': True}, + 'SeaStFile': {'type': str, 'desc': 'SeaState input file path', 'units': None, + 'is_file_ref': True}, + 'SubFile': {'type': str, 'desc': 'SubDyn/ExtPtfm input file path', 'units': None, + 'is_file_ref': True}, + 'MooringFile': {'type': str, 'desc': 'Mooring input file path', 'units': None, + 'is_file_ref': True}, + 'SoilFile': {'type': str, 'desc': 'SoilDyn input file path', 'units': None, + 'is_file_ref': True}, + }, + }, + '4.0.0': { + # Params present in 4.x but removed in 5.0.0 + 'AeroDyn': { + 'Buoyancy': {'type': bool, 'desc': 'Enable buoyancy effects (removed in v5.0.0)', 'units': None}, + }, + 'BeamDyn': { + 'UsePitchAct': {'type': bool, 'desc': 'Use a pitch actuator (removed in v5.0.0)', 'units': None}, + 'PitchJ': {'type': float, 'desc': 'Pitch actuator inertia (removed in v5.0.0)', 'units': 'kg*m^2'}, + 'PitchK': {'type': float, 'desc': 'Pitch actuator stiffness (removed in v5.0.0)', 'units': 'N*m/rad'}, + 'PitchC': {'type': float, 'desc': 'Pitch actuator damping (removed in v5.0.0)', 'units': 'N*m*s/rad'}, + }, + }, +} + +# File-reference parameters by module — used by validation +FILE_REF_PARAMS: dict[str, list[str]] = { + 'Fst': ['EDFile', 'BDBldFile(1)', 'BDBldFile(2)', 'BDBldFile(3)', + 'InflowFile', 'AeroFile', 'ServoFile', + 'SeaStFile', 'HydroFile', 'SubFile', 'MooringFile', 'SoilFile'], + 'ElastoDyn': ['BldFile1', 'BldFile2', 'BldFile3', 'TwrFile'], + 'AeroDyn': ['ADBlFile(1)', 'ADBlFile(2)', 'ADBlFile(3)'], + 'ServoDyn': ['DLL_FileName'], +} + + +def get_schema(module: str, version: str = '5.0.0') -> dict: + """Get parameter schema for a module at a given version. + + For v5.0.0, returns the v5 schema directly. + For v4.0.0, returns the v5 schema merged with v4-specific params (those removed in v5). + """ + base = _SCHEMA.get('5.0.0', {}).get(module, {}).copy() + if version != '5.0.0' and version in _SCHEMA: + base.update(_SCHEMA[version].get(module, {})) + return base + + +def get_param_info(module: str, param: str, version: str = '5.0.0') -> dict: + """Get metadata for a single parameter. Returns {} if not in schema.""" + return get_schema(module, version).get(param, {}) diff --git a/openfast_io/openfast_io/tests/conftest.py b/openfast_io/openfast_io/tests/conftest.py index f4421a8519..c2344e5c29 100644 --- a/openfast_io/openfast_io/tests/conftest.py +++ b/openfast_io/openfast_io/tests/conftest.py @@ -1,6 +1,7 @@ import pytest import os.path as osp import platform +from pathlib import Path # looking up OS for the correct executable extension mactype = platform.system().lower() @@ -22,3 +23,307 @@ def pytest_addoption(parser): parser.addoption("--source_dir", action="store", default=REPOSITORY_ROOT, help="Path to the openfast repository") parser.addoption("--build_dir", action="store", default=BUILD_DIR, help="Path to the test data directory") + +# ── Fixtures for new IO/Driver tests ── + +@pytest.fixture +def r_test_dir(): + """Path to r-test root. Skip if not available.""" + candidate = Path(REPOSITORY_ROOT) / "reg_tests" / "r-test" + if not candidate.exists(): + pytest.skip("r-test repo not available") + return candidate + + +@pytest.fixture +def r_test_5mw_dir(r_test_dir): + """Path to r-test 5MW_Land_DLL_WTurb case.""" + candidate = r_test_dir / "glue-codes" / "openfast" / "5MW_Land_DLL_WTurb" + if not candidate.exists(): + pytest.skip("5MW_Land_DLL_WTurb r-test case not found") + return candidate + + +@pytest.fixture +def r_test_fastfarm_dir(r_test_dir): + """Path to r-test FAST.Farm cases.""" + candidate = r_test_dir / "glue-codes" / "fast-farm" + if not candidate.exists(): + pytest.skip("r-test FAST.Farm cases not available") + return candidate + + +@pytest.fixture +def sample_ed_file(tmp_path): + """Minimal valid ElastoDyn .dat content for testing.""" + # Create minimal blade file + blade_content = """\ +------- ELASTODYN INDIVIDUAL BLADE INPUT FILE -------------------------- +Test blade file +---------------------- BLADE PARAMETERS ---------------------------------------- +6 NBlInpSt - Number of blade input stations (-) +1.0 BldFlDmp(1) - Blade flap mode #1 structural damping (%) +1.0 BldFlDmp(2) - Blade flap mode #2 structural damping (%) +1.0 BldEdDmp(1) - Blade edge mode #1 structural damping (%) +---------------------- BLADE ADJUSTMENT FACTORS -------------------------------- +1.0 FlStTunr(1) - Blade flapwise modal stiffness tuner, 1st mode (-) +1.0 FlStTunr(2) - Blade flapwise modal stiffness tuner, 2nd mode (-) +1.0 AdjBlMs - Factor to adjust blade mass density (-) +1.0 AdjFlSt - Factor to adjust blade flap stiffness (-) +1.0 AdjEdSt - Factor to adjust blade edge stiffness (-) +---------------------- DISTRIBUTED BLADE PROPERTIES ---------------------------- + BlFract StrcTwst BMassDen FlpStff EdgStff + (-) (deg) (kg/m) (Nm^2) (Nm^2) + 0.000000000000000e+00 1.330000000000000e+01 6.780000000000000e+02 1.810000000000000e+10 1.810000000000000e+10 + 2.000000000000000e-01 1.330000000000000e+01 6.780000000000000e+02 1.810000000000000e+10 1.810000000000000e+10 + 4.000000000000000e-01 1.000000000000000e+01 4.000000000000000e+02 9.000000000000000e+09 9.000000000000000e+09 + 6.000000000000000e-01 7.000000000000000e+00 2.500000000000000e+02 5.000000000000000e+09 5.000000000000000e+09 + 8.000000000000000e-01 3.000000000000000e+00 1.200000000000000e+02 2.000000000000000e+09 2.000000000000000e+09 + 1.000000000000000e+00 1.000000000000000e+00 1.000000000000000e+01 1.000000000000000e+08 1.000000000000000e+08 +---------------------- BLADE MODE SHAPES --------------------------------------- + 0.0622 BldFl1Sh(2) - Flap mode 1, coeff of x^2 + 1.7254 BldFl1Sh(3) - , coeff of x^3 +-3.2452 BldFl1Sh(4) - , coeff of x^4 + 4.7131 BldFl1Sh(5) - , coeff of x^5 +-2.2555 BldFl1Sh(6) - , coeff of x^6 +-0.5809 BldFl2Sh(2) - Flap mode 2, coeff of x^2 + 1.2067 BldFl2Sh(3) - , coeff of x^3 +-15.5349 BldFl2Sh(4) - , coeff of x^4 + 29.7347 BldFl2Sh(5) - , coeff of x^5 +-13.8255 BldFl2Sh(6) - , coeff of x^6 + 0.3877 BldEdgSh(2) - Edge mode 1, coeff of x^2 + 2.1179 BldEdgSh(3) - , coeff of x^3 +-4.4402 BldEdgSh(4) - , coeff of x^4 + 5.8456 BldEdgSh(5) - , coeff of x^5 +-2.9110 BldEdgSh(6) - , coeff of x^6 +""" + blade_path = tmp_path / "test_blade.dat" + blade_path.write_text(blade_content) + + # Create minimal tower file + tower_content = """\ +------- ELASTODYN TOWER INPUT FILE ------------------------------------- +Test tower file +---------------------- TOWER PARAMETERS ---------------------------------------- +3 NTwInpSt - Number of input stations to specify tower geometry +1.0 TwrFADmp(1) - Tower 1st fore-aft mode structural damping ratio (%) +1.0 TwrFADmp(2) - Tower 2nd fore-aft mode structural damping ratio (%) +1.0 TwrSSDmp(1) - Tower 1st side-to-side mode structural damping ratio (%) +1.0 TwrSSDmp(2) - Tower 2nd side-to-side mode structural damping ratio (%) +---------------------- TOWER ADJUSTMUNT FACTORS -------------------------------- +1.0 FAStTunr(1) - Tower fore-aft modal stiffness tuner, 1st mode (-) +1.0 FAStTunr(2) - Tower fore-aft modal stiffness tuner, 2nd mode (-) +1.0 SSStTunr(1) - Tower side-to-side stiffness tuner, 1st mode (-) +1.0 SSStTunr(2) - Tower side-to-side stiffness tuner, 2nd mode (-) +1.0 AdjTwMa - Factor to adjust tower mass density (-) +1.0 AdjFASt - Factor to adjust tower fore-aft stiffness (-) +1.0 AdjSSSt - Factor to adjust tower side-to-side stiffness (-) +---------------------- DISTRIBUTED TOWER PROPERTIES ---------------------------- + HtFract TMassDen TwFAStif TwSSStif + (-) (kg/m) (Nm^2) (Nm^2) + 0.000000000000000e+00 8.857000000000000e+03 6.144000000000000e+11 6.144000000000000e+11 + 5.000000000000000e-01 5.000000000000000e+03 3.000000000000000e+11 3.000000000000000e+11 + 1.000000000000000e+00 3.475000000000000e+03 1.858000000000000e+11 1.858000000000000e+11 +---------------------- TOWER FORE-AFT MODE SHAPES ------------------------------ + 0.7004 TwFAM1Sh(2) - Mode 1, coefficient of x^2 term + 2.1963 TwFAM1Sh(3) - , coefficient of x^3 term +-5.6202 TwFAM1Sh(4) - , coefficient of x^4 term + 6.2275 TwFAM1Sh(5) - , coefficient of x^5 term +-2.5040 TwFAM1Sh(6) - , coefficient of x^6 term +-26.0840 TwFAM2Sh(2) - Mode 2, coefficient of x^2 term + 70.5765 TwFAM2Sh(3) - , coefficient of x^3 term +-79.6498 TwFAM2Sh(4) - , coefficient of x^4 term + 51.0700 TwFAM2Sh(5) - , coefficient of x^5 term +-14.9127 TwFAM2Sh(6) - , coefficient of x^6 term +---------------------- TOWER SIDE-TO-SIDE MODE SHAPES -------------------------- + 0.7004 TwSSM1Sh(2) - Mode 1, coefficient of x^2 term + 2.1963 TwSSM1Sh(3) - , coefficient of x^3 term +-5.6202 TwSSM1Sh(4) - , coefficient of x^4 term + 6.2275 TwSSM1Sh(5) - , coefficient of x^5 term +-2.5040 TwSSM1Sh(6) - , coefficient of x^6 term +-26.0840 TwSSM2Sh(2) - Mode 2, coefficient of x^2 term + 70.5765 TwSSM2Sh(3) - , coefficient of x^3 term +-79.6498 TwSSM2Sh(4) - , coefficient of x^4 term + 51.0700 TwSSM2Sh(5) - , coefficient of x^5 term +-14.9127 TwSSM2Sh(6) - , coefficient of x^6 term +""" + tower_path = tmp_path / "test_tower.dat" + tower_path.write_text(tower_content) + + # Create minimal ElastoDyn main file + ed_content = f"""\ +------- ELASTODYN INPUT FILE ------------------------------------------- +Test ElastoDyn file +---------------------- SIMULATION CONTROL -------------------------------------- +False Echo - Echo input data to ".ech" (flag) + 3 Method - Integration method {{1: RK4, 2: AB4, 3: ABM4}} (-) + 0.00625 DT - Integration time step (s) +---------------------- DEGREES OF FREEDOM -------------------------------------- +True FlapDOF1 - First flapwise blade mode DOF (flag) +True FlapDOF2 - Second flapwise blade mode DOF (flag) +True EdgeDOF - First edgewise blade mode DOF (flag) +True PitchDOF - Blade pitch DOF (flag) +False TeetDOF - Rotor-teeter DOF (flag) +False DrTrDOF - Drivetrain rotational-flexibility DOF (flag) +True GenDOF - Generator DOF (flag) +False YawDOF - Nacelle-yaw DOF (flag) +True TwFADOF1 - First fore-aft tower bending-mode DOF (flag) +True TwFADOF2 - Second fore-aft tower bending-mode DOF (flag) +True TwSSDOF1 - First side-to-side tower bending-mode DOF (flag) +True TwSSDOF2 - Second side-to-side tower bending-mode DOF (flag) +False PtfmSgDOF - Platform horizontal surge translation DOF (flag) +False PtfmSwDOF - Platform horizontal sway translation DOF (flag) +False PtfmHvDOF - Platform vertical heave translation DOF (flag) +False PtfmRDOF - Platform roll tilt rotation DOF (flag) +False PtfmPDOF - Platform pitch tilt rotation DOF (flag) +False PtfmYDOF - Platform yaw rotation DOF (flag) +---------------------- INITIAL CONDITIONS -------------------------------------- + 0 OoPDefl - Initial out-of-plane blade-tip displacement (m) + 0 IPDefl - Initial in-plane blade-tip displacement (m) + 0 BlPitch(1) - Blade 1 initial pitch (degrees) + 0 BlPitch(2) - Blade 2 initial pitch (degrees) + 0 BlPitch(3) - Blade 3 initial pitch (degrees) + 0 TeetDefl - Initial or fixed teeter angle (degrees) + 0 Azimuth - Initial azimuth angle for blade 1 (degrees) + 12.1 RotSpeed - Initial or fixed rotor speed (rpm) + 0 NacYaw - Initial or fixed nacelle-yaw angle (degrees) + 0 TTDspFA - Initial fore-aft tower-top displacement (m) + 0 TTDspSS - Initial side-to-side tower-top displacement (m) + 0 PtfmSurge - Initial platform surge (m) + 0 PtfmSway - Initial platform sway (m) + 0 PtfmHeave - Initial platform heave (m) + 0 PtfmRoll - Initial platform roll (deg) + 0 PtfmPitch - Initial platform pitch (deg) + 0 PtfmYaw - Initial platform yaw (deg) +---------------------- TURBINE CONFIGURATION ----------------------------------- + 3 NumBl - Number of blades (-) + 63.0 TipRad - The distance from the rotor apex to the blade tip (meters) + 1.5 HubRad - The distance from the rotor apex to the blade root (meters) + -2.5 PreCone(1) - Blade 1 cone angle (degrees) + -2.5 PreCone(2) - Blade 2 cone angle (degrees) + -2.5 PreCone(3) - Blade 3 cone angle (degrees) + 0 HubCM - Distance from rotor apex to hub mass (meters) + 0 UndSling - Undersling length (meters) + 0 Delta3 - Delta-3 angle for teetering rotors (degrees) + 0 AzimB1Up - Azimuth value to use for I/O when blade 1 points up (degrees) + -5.0 OverHang - Distance from yaw axis to rotor apex (meters) + 1.9 ShftGagL - Distance from rotor apex to shaft strain gages (meters) + -5.0 ShftTilt - Rotor shaft tilt angle (degrees) + -3.09528 NacCMxn - Downwind distance from tower-top to nacelle CM (meters) + 0 NacCMyn - Lateral distance from tower-top to nacelle CM (meters) + 1.75 NacCMzn - Vertical distance from tower-top to nacelle CM (meters) + -3.09528 NcIMUxn - Downwind distance from tower-top to nacelle IMU (meters) + 0 NcIMUyn - Lateral distance from tower-top to nacelle IMU (meters) + 1.75 NcIMUzn - Vertical distance from tower-top to nacelle IMU (meters) + 1.96256 Twr2Shft - Vertical distance from tower-top to rotor shaft (meters) + 87.6 TowerHt - Height of tower above ground level (meters) + 10.0 TowerBsHt - Height of tower base above ground level (meters) + 0 PtfmCMxt - Downwind distance from ground to platform CM (meters) + 0 PtfmCMyt - Lateral distance from ground to platform CM (meters) + 0 PtfmCMzt - Vertical distance from ground to platform CM (meters) + 0 PtfmRefxt - Downwind distance from ground to platform ref point (meters) + 0 PtfmRefyt - Lateral distance from ground to platform ref point (meters) + 0 PtfmRefzt - Vertical distance from ground to platform ref point (meters) +---------------------- MASS AND INERTIA ---------------------------------------- + 0 TipMass(1) - Tip-brake mass, blade 1 (kg) + 0 TipMass(2) - Tip-brake mass, blade 2 (kg) + 0 TipMass(3) - Tip-brake mass, blade 3 (kg) + 0 PBrIner(1) - Pitch bearing inertia, blade 1 (kg m^2) + 0 PBrIner(2) - Pitch bearing inertia, blade 2 (kg m^2) + 0 PBrIner(3) - Pitch bearing inertia, blade 3 (kg m^2) + 0 BlPIner(1) - Blade pitch inertia, blade 1 (kg m^2) + 0 BlPIner(2) - Blade pitch inertia, blade 2 (kg m^2) + 0 BlPIner(3) - Blade pitch inertia, blade 3 (kg m^2) + 56780 HubMass - Hub mass (kg) + 115926 HubIner - Hub inertia about rotor axis (kg m^2) + 0 HubIner_Teeter - Hub inertia about teeter axis (kg m^2) + 534.116 GenIner - Generator inertia about HSS (kg m^2) + 240000 NacMass - Nacelle mass (kg) + 2607890 NacYIner - Nacelle inertia about yaw axis (kg m^2) + 0 YawBrMass - Yaw bearing mass (kg) + 0 PtfmMass - Platform mass (kg) + 0 PtfmRIner - Platform inertia for roll tilt rotation (kg m^2) + 0 PtfmPIner - Platform inertia for pitch tilt rotation (kg m^2) + 0 PtfmYIner - Platform inertia for yaw rotation (kg m^2) + 0 PtfmXYIner - Platform xy moment of inertia (kg m^2) + 0 PtfmYZIner - Platform yz moment of inertia (kg m^2) + 0 PtfmXZIner - Platform xz moment of inertia (kg m^2) +---------------------- BLADE --------------------------------------------------- + 49 BldNodes - Number of blade nodes (per blade) used for analysis (-) +"test_blade.dat" BldFile(1) - Name of file containing properties for blade 1 +"test_blade.dat" BldFile(2) - Name of file containing properties for blade 2 +"test_blade.dat" BldFile(3) - Name of file containing properties for blade 3 +---------------------- ROTOR-TEETER -------------------------------------------- + 0 TeetMod - Rotor-teeter spring/damper model (switch) + 0 TeetDmpP - Rotor-teeter damper position (degrees) + 0 TeetDmp - Rotor-teeter damping constant (N-m/(rad/s)) + 0 TeetCDmp - Rotor-teeter rate-independent Coulomb-damping moment (N-m) + 0 TeetSStP - Rotor-teeter soft-stop position (degrees) + 0 TeetHStP - Rotor-teeter hard-stop position (degrees) + 0 TeetSSSp - Rotor-teeter soft-stop linear-spring constant (N-m/rad) + 0 TeetHSSp - Rotor-teeter hard-stop linear-spring constant (N-m/rad) +---------------------- YAW-FRICTION -------------------------------------------- + 0 YawFrctMod - Yaw-friction model (switch) + 0 M_CSmax - Maximum static Coulomb friction torque + 0 M_FCSmax - Maximum static Coulomb friction torque proportional to shear force + 0 M_MCSmax - Maximum static Coulomb friction torque proportional to bending moment + 0 M_CD - Dynamic Coulomb friction moment + 0 M_FCD - Dynamic Coulomb friction moment proportional to shear force + 0 M_MCD - Dynamic Coulomb friction moment proportional to bending moment + 0 sig_v - Linear viscous friction coefficient + 0 sig_v2 - Quadratic viscous friction coefficient + 0 OmgCut - Yaw angular velocity cutoff +---------------------- DRIVETRAIN ---------------------------------------------- + 97.0 GBoxEff - Gearbox efficiency (%) + 97.028 GBRatio - Gearbox ratio (-) + 8.676e8 DTTorSpr - Drivetrain torsional spring (N-m/rad) + 6.215e6 DTTorDmp - Drivetrain torsional damper (N-m/(rad/s)) +---------------------- FURLING ------------------------------------------------- +False Furling - Read in additional model properties for furling turbine (flag) +"unused" FurlFile - Name of file containing furling properties +---------------------- TOWER --------------------------------------------------- + 20 TwrNodes - Number of tower nodes used for analysis (-) +"test_tower.dat" TwrFile - Name of file containing tower properties +---------------------- OUTPUT -------------------------------------------------- +True SumPrint - Print summary data to ".sum" (flag) + 1 OutFile - Switch to determine where output will be placed +False TabDelim - Use tab delimiters in text tabular output file? (flag) +"ES10.3E2" OutFmt - Format used for text tabular output + 0 TStart - Time to begin tabular output (s) + 1 DecFact - Decimation factor for tabular output (-) + 0 NTwGages - Number of tower nodes that have strain gages for output + 0 TwrGagNd - List of tower nodes that have strain gages + 0 NBlGages - Number of blade nodes that have strain gages for output + 0 BldGagNd - List of blade nodes that have strain gages + OutList - The next line(s) contains a list of output parameters. +END of OutList section (the word "END" must appear in the first 3 columns of the last OutList line) +--------------------------------------------------------------------------------------- +""" + p = tmp_path / "ElastoDyn.dat" + p.write_text(ed_content) + return p + + +@pytest.fixture +def sample_ed_file_with_nodal(sample_ed_file): + """sample_ed_file with a populated optional nodal OutList section + (BldNd_BladesOut > 0 plus nodal channels) inserted before the closing + dashed line — regression fixture for the ElastoDyn nodal-OutList + read/write bug (channels captured under the wrong registry key and never + emitted on write).""" + nodal_section = ( + '====== Outputs for all blade stations =========================== [optional section]\n' + ' 1 BldNd_BladesOut - Number of blades to output all node information at (-)\n' + '"ALL" BldNd_BlOutNd - Future feature will allow selecting a portion of the nodes to output (-)\n' + ' OutList - The next line(s) contains a list of output parameters.\n' + '"TDx"\n' + '"TDy"\n' + '"RDx"\n' + 'END (the word "END" must appear in the first 3 columns of this last OutList line in the optional nodal output section)\n' + ) + footer = '---------------------------------------------------------------------------------------\n' + content = sample_ed_file.read_text() + assert content.count(footer) == 1 + sample_ed_file.write_text(content.replace(footer, nodal_section + footer)) + return sample_ed_file + diff --git a/openfast_io/openfast_io/tests/test_check_registry_drift.py b/openfast_io/openfast_io/tests/test_check_registry_drift.py new file mode 100644 index 0000000000..756be60219 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_check_registry_drift.py @@ -0,0 +1,67 @@ +from pathlib import Path +import pytest +from openfast_io.tools.check_registry_drift import parse_registry, scan_reader_params, RegistryParam + + +def test_parse_registry_returns_list_of_params(tmp_path): + """parse_registry correctly extracts ED_InputFile entries.""" + reg_content = """\ +# ElastoDyn Registry test +typedef\t^\tED_InputFile\tLOGICAL\tFlapDOF1\t-\t-\t-\t"First flapwise blade mode DOF"\t- +typedef\t^\tED_InputFile\tReKi\tRotSpeed\t-\t-\t-\t"Initial rotor speed"\trad/s +typedef\t^\tContinuousStateType\tR8Ki\tQT\t{:}\t-\t-\t"Displacement DOF vector"\t- +""" + reg_path = tmp_path / "ElastoDyn_Registry.txt" + reg_path.write_text(reg_content) + + params = parse_registry(reg_path, 'ED_InputFile') + names = [p.name for p in params] + + assert 'FlapDOF1' in names + assert 'RotSpeed' in names + assert 'QT' not in names # ContinuousStateType, not InputFile + + +def test_parse_registry_captures_units(tmp_path): + reg_content = "typedef\t^\tED_InputFile\tReKi\tRotSpeed\t-\t-\t-\t\"Initial rotor speed\"\trad/s\n" + reg_path = tmp_path / "ED_Registry.txt" + reg_path.write_text(reg_content) + params = parse_registry(reg_path, 'ED_InputFile') + assert params[0].units == 'rad/s' + + +def test_scan_reader_params_finds_fst_vt_assignments(tmp_path): + reader_content = """\ +def read_ElastoDyn(self): + fst_vt = self.fst_vt + fst_vt['ElastoDyn']['FlapDOF1'] = True + fst_vt['ElastoDyn']['RotSpeed'] = 12.1 + fst_vt['AeroDyn']['TwrAero'] = False +""" + reader_path = tmp_path / "FAST_reader.py" + reader_path.write_text(reader_content) + + params = scan_reader_params(reader_path, 'ElastoDyn') + assert 'FlapDOF1' in params + assert 'RotSpeed' in params + assert 'TwrAero' not in params # AeroDyn key, not ElastoDyn + + +def test_parse_registry_handles_empty_file(tmp_path): + reg_path = tmp_path / "empty.txt" + reg_path.write_text("") + params = parse_registry(reg_path, 'ED_InputFile') + assert params == [] + + +def test_parse_registry_skips_comments(tmp_path): + reg_content = """\ +# This is a comment +! This is also a comment +typedef\t^\tED_InputFile\tReKi\tDT\t-\t-\t-\t"Time step"\ts +""" + reg_path = tmp_path / "reg.txt" + reg_path.write_text(reg_content) + params = parse_registry(reg_path, 'ED_InputFile') + assert len(params) == 1 + assert params[0].name == 'DT' diff --git a/openfast_io/openfast_io/tests/test_driver_fastfarm.py b/openfast_io/openfast_io/tests/test_driver_fastfarm.py new file mode 100644 index 0000000000..223994972f --- /dev/null +++ b/openfast_io/openfast_io/tests/test_driver_fastfarm.py @@ -0,0 +1,90 @@ +"""Tests for FASTFarmDriver.""" + +import pytest +from pathlib import Path + +from openfast_io.drivers.fastfarm import FASTFarmDriver + + +@pytest.fixture +def ff_tsinflow_dir(r_test_fastfarm_dir): + """Path to TSinflow test case.""" + candidate = r_test_fastfarm_dir / "TSinflow" + if not candidate.exists(): + pytest.skip("TSinflow FAST.Farm case not found") + return candidate + + +@pytest.fixture +def ff_data(ff_tsinflow_dir): + """Read the TSinflow FAST.Farm file and return the result dict.""" + drv = FASTFarmDriver() + return drv.read(ff_tsinflow_dir / "FAST.Farm.fstf") + + +class TestFASTFarmDriverInit: + def test_instantiates(self): + drv = FASTFarmDriver() + assert drv is not None + + def test_has_read_method(self): + drv = FASTFarmDriver() + assert callable(getattr(drv, "read", None)) + + +class TestFASTFarmRead: + def test_read_returns_dict_with_keys(self, ff_data): + assert "FASTFarm" in ff_data + assert "Turbines" in ff_data + + def test_farm_scalars(self, ff_data): + ff = ff_data["FASTFarm"] + assert ff["TMax"] == pytest.approx(90.0) + assert ff["NumTurbines"] == 2 + assert ff["Mod_AmbWind"] in (1, 2, 3) + + def test_turbine_count_matches(self, ff_data): + assert len(ff_data["Turbines"]) == ff_data["FASTFarm"]["NumTurbines"] + + def test_turbine_rows_positions(self, ff_data): + rows = ff_data["FASTFarm"]["TurbineRows"] + assert len(rows) == 2 + # Each row should have WT_X, WT_Y, WT_Z, WT_FASTInFile + for row in rows: + assert "WT_X" in row + assert "WT_Y" in row + assert "WT_Z" in row + assert "WT_FASTInFile" in row + + def test_each_turbine_has_fst_vt_structure(self, ff_data): + """Each turbine entry should contain OpenFAST driver output keys.""" + for turb in ff_data["Turbines"]: + # Should have at least ElastoDyn (CompElast=1) and ServoDyn + assert "ElastoDyn" in turb, "Turbine missing ElastoDyn" + assert "ServoDyn" in turb, "Turbine missing ServoDyn" + + def test_turbine_farm_positions(self, ff_data): + """Each turbine has _farm_position metadata.""" + for turb in ff_data["Turbines"]: + pos = turb.get("_farm_position") + assert pos is not None + assert "WT_X" in pos + assert "WT_Y" in pos + + def test_shared_mooring_fields(self, ff_data): + ff = ff_data["FASTFarm"] + assert "SharedMoorFile" in ff + assert "DT_Mooring" in ff + + def test_ambient_wind_inflowwind_fields(self, ff_data): + ff = ff_data["FASTFarm"] + assert "DT_Low" in ff + assert "DT_High" in ff + assert "NX_Low" in ff + assert "InflowFile" in ff + + def test_remaining_text_captured(self, ff_data): + ff = ff_data["FASTFarm"] + # _remaining should capture wake dynamics, curled-wake, output sections + assert "_remaining" in ff + assert len(ff["_remaining"]) > 0 diff --git a/openfast_io/openfast_io/tests/test_driver_openfast.py b/openfast_io/openfast_io/tests/test_driver_openfast.py new file mode 100644 index 0000000000..e3516f85e5 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_driver_openfast.py @@ -0,0 +1,222 @@ +"""OpenFASTDriver read/write tests. + +Strengthened 2026-06-23 after a differential audit found the prior round-trip test +(4 scalars only) passed while OutList read/write was broken on 63/77 decks, ServoDyn/ +HydroDyn/SeaState writers dumped the full channel registry, and BeamDyn blades collided. +The round-trip checks here now assert OutList preservation, write->reread idempotency, +and absence of private-key pollution — the properties those bugs violated. +""" +import copy +import tempfile +from pathlib import Path + +import pytest + +from openfast_io.drivers.openfast import OpenFASTDriver, init_fst_vt + + +def _case(r_test_dir, name): + d = r_test_dir / "glue-codes" / "openfast" / name + if not d.exists(): + pytest.skip(f"r-test case not found: {name}") + return d / f"{name}.fst" + + +# --------------------------------------------------------------------------- +# Helpers — the invariants the audit-found bugs violated +# --------------------------------------------------------------------------- + +def outlist_sets(fst_vt): + """{module: frozenset(channels set True)} from fst_vt['outlist'].""" + def true_ch(d): + out = set() + for k, v in d.items(): + if isinstance(v, dict): + out |= true_ch(v) + elif v is True: + out.add(k) + return out + return {m: frozenset(true_ch(v)) for m, v in fst_vt.get("outlist", {}).items() + if isinstance(v, dict) and true_ch(v)} + + +def assert_no_outlist_pollution(fst_vt): + """No module data dict should carry a private '_outlist' key (channels live only in + fst_vt['outlist']). Regression guard for the ExtPtfm/AeroDisk/SED pollution bug.""" + for mod, val in fst_vt.items(): + if mod == "outlist": + continue + for d in (val if isinstance(val, list) else [val]): + if isinstance(d, dict): + assert "_outlist" not in d, f"fst_vt['{mod}'] leaks a private '_outlist' key" + + +def roundtrip(driver, fst_path, tmp_path): + """read -> write -> reread, returning (src, reread). Asserts the invariants that the + OutList / full-registry-emit / spd_trq / pollution bugs all broke.""" + src = driver.read(fst_path) + driver.write(src, tmp_path, "rt") + reread = driver.read(tmp_path / "rt.fst") # must not raise (spd_trq regression) + + # The exact requested OutList channel set must survive per module — catches both + # silent drops AND the full-registry explosion (e.g. ServoDyn 2 -> 518). + assert outlist_sets(reread) == outlist_sets(src), ( + f"OutList not preserved on round-trip\n src={outlist_sets(src)}\n re ={outlist_sets(reread)}" + ) + assert_no_outlist_pollution(src) + assert_no_outlist_pollution(reread) + return src, reread + + +# --------------------------------------------------------------------------- +# Structure +# --------------------------------------------------------------------------- + +def test_init_fst_vt_has_expected_keys(): + fst_vt = init_fst_vt() + for key in ['Fst', 'outlist', 'ElastoDyn', 'AeroDyn', 'ServoDyn', 'HydroDyn', + 'SeaState', 'MoorDyn', 'SubDyn', 'BeamDyn', 'ElastoDynBlade', + 'AeroDynBlade', 'BeamDynBlade', 'BStC', 'NStC', 'TStC', 'SStC', + 'DISCON_in', 'spd_trq', 'AeroDynPolar', 'SimpleElastoDyn', 'description', + 'WaterKin', 'SoilDyn', 'ExtPtfm', 'MAP', 'AeroDisk']: + assert key in fst_vt, f"fst_vt missing key: {key}" + + +def test_init_fst_vt_stc_are_lists(): + fst_vt = init_fst_vt() + for key in ['BStC', 'NStC', 'TStC', 'SStC']: + assert isinstance(fst_vt[key], list) + + +# --------------------------------------------------------------------------- +# Read population (per module) +# --------------------------------------------------------------------------- + +def test_driver_reads_main_input(r_test_5mw_dir): + fst_vt = OpenFASTDriver().read(r_test_5mw_dir / "5MW_Land_DLL_WTurb.fst") + assert fst_vt['Fst']['TMax'] == 60.0 + assert fst_vt['Fst']['CompElast'] == 1 + + +def test_driver_populates_elastodyn(r_test_5mw_dir): + fst_vt = OpenFASTDriver().read(r_test_5mw_dir / "5MW_Land_DLL_WTurb.fst") + assert fst_vt['ElastoDyn']['NumBl'] == 3 + assert isinstance(fst_vt['ElastoDynBlade'], dict), "identical blades collapse to a dict" + assert fst_vt['ElastoDynTower'] and 'NTwInpSt' in fst_vt['ElastoDynTower'] + + +def test_driver_populates_inflowwind_aerodyn_servodyn(r_test_5mw_dir): + fst_vt = OpenFASTDriver().read(r_test_5mw_dir / "5MW_Land_DLL_WTurb.fst") + assert 'WindType' in fst_vt['InflowWind'] + assert 'Wake_Mod' in fst_vt['AeroDyn'] + assert fst_vt['AeroDynBlade'] + assert 'VSContrl' in fst_vt['ServoDyn'] + assert isinstance(fst_vt['BStC'], list) and isinstance(fst_vt['NStC'], list) + + +def test_driver_populates_hydrodynamics(r_test_dir): + fst_vt = OpenFASTDriver().read(_case(r_test_dir, "StC_test_OC4Semi")) + hd = fst_vt['HydroDyn'] + assert hd.get('NBody', 0) >= 1 and hd.get('NJoints', 0) > 0 and hd.get('NMembers', 0) > 0 + assert isinstance(fst_vt['SeaState'].get('WaveMod'), int) + + +def test_driver_populates_subdyn(r_test_dir): + sd = OpenFASTDriver().read(_case(r_test_dir, "5MW_OC3Mnpl_DLL_WTurb_WavesIrr"))['SubDyn'] + assert sd.get('NJoints', 0) > 0 and sd.get('NMembers', 0) > 0 + + +def test_driver_populates_moordyn(r_test_dir): + md = OpenFASTDriver().read(_case(r_test_dir, "5MW_OC4Semi_WSt_WavesWN"))['MoorDyn'] + assert 'Name' in md and len(md['Name']) >= 1 + + +def test_driver_populates_map(r_test_dir): + m = OpenFASTDriver().read(_case(r_test_dir, "5MW_OC3Spar_DLL_WTurb_WavesIrr"))['MAP'] + assert 'LineType' in m and len(m['LineType']) >= 1 + assert 'Node' in m and len(m['Node']) > 0 + + +# --------------------------------------------------------------------------- +# OutList capture — regression guard for the "ElastoDyn-only" read bug +# --------------------------------------------------------------------------- + +def test_outlist_captured_for_every_active_module(r_test_5mw_dir): + """The read bug dropped every module's OutList except ElastoDyn. Assert the deck's + actually-requested channels are captured for InflowWind and ServoDyn too.""" + sets = outlist_sets(OpenFASTDriver().read(r_test_5mw_dir / "5MW_Land_DLL_WTurb.fst")) + assert len(sets.get('ElastoDyn', ())) > 10 + assert len(sets.get('InflowWind', ())) >= 1, "InflowWind OutList dropped on read" + assert len(sets.get('ServoDyn', ())) >= 1, "ServoDyn OutList dropped on read" + + +# --------------------------------------------------------------------------- +# BeamDyn — collapse contract + the HIGH-severity distinct-blade collision +# --------------------------------------------------------------------------- + +def test_beamdyn_identical_blades_collapse_to_dict(r_test_dir): + """5MW_Land_BD has 3 identical blade files -> fst_vt['BeamDyn'] must be a single dict + (the fst_vt list->dict contract WEIS/WISDEM rely on), mirroring ElastoDynBlade.""" + fst_vt = OpenFASTDriver().read(_case(r_test_dir, "5MW_Land_BD_DLL_WTurb")) + assert isinstance(fst_vt['BeamDyn'], dict), "identical BeamDyn blades must collapse to a dict" + assert 'member_total' in fst_vt['BeamDyn'] + assert isinstance(fst_vt['BeamDynBlade'], dict) + + +def test_beamdyn_distinct_blades_do_not_collide(r_test_dir): + """HIGH-severity regression: the writer must give each distinct blade its own file. + Previously every blade was written to the same path, so blades 1&2 silently inherited + blade 3's properties. The corpus has no distinct-blade deck, so synthesize one.""" + driver = OpenFASTDriver() + fst_vt = driver.read(_case(r_test_dir, "5MW_Land_BD_DLL_WTurb")) + bd0 = fst_vt['BeamDyn'] if isinstance(fst_vt['BeamDyn'], dict) else fst_vt['BeamDyn'][0] + bl0 = fst_vt['BeamDynBlade'] if isinstance(fst_vt['BeamDynBlade'], dict) else fst_vt['BeamDynBlade'][0] + + def mark(val): + b = copy.deepcopy(bl0) + b['beam_stiff'][0][0][0] = val # (nstation,6,6) ndarray — a round-trippable field + return b + + fst_vt['BeamDyn'] = [copy.deepcopy(bd0) for _ in range(3)] + fst_vt['BeamDynBlade'] = [mark(1000.0), mark(2000.0), mark(3000.0)] + for i in range(3): + fst_vt['Fst'][f'BDBldFile({i + 1})'] = f'blade_{i + 1}.dat' + + out = Path(tempfile.mkdtemp(prefix="bd_distinct_")) + driver.write(fst_vt, out, "case") + blades = driver.read(out / "case.fst")['BeamDynBlade'] + assert not isinstance(blades, dict), "3 distinct blades must not collapse to one dict" + markers = sorted(b['beam_stiff'][0][0][0] for b in blades) + assert markers == [1000.0, 2000.0, 3000.0], f"blade collision — got {markers}" + + +# --------------------------------------------------------------------------- +# Round-trip — OutList preservation + idempotency across deck types. +# Each deck exercises a different bug the weak 4-scalar test missed. +# --------------------------------------------------------------------------- + +ROUNDTRIP_CASES = [ + "5MW_Land_DLL_WTurb", # land: ElastoDyn/AeroDyn/ServoDyn/InflowWind + "5MW_Land_BD_DLL_WTurb", # active BeamDyn + "5MW_Land_DLL_WTurb_ADsk", # AeroDisk private-_outlist path + "5MW_Land_DLL_WTurb_SED", # SimpleElastoDyn private-_outlist path + "5MW_OC4Semi_WSt_WavesWN", # HydroDyn/SeaState/MoorDyn — full-registry-emit bug + "5MW_OC4Jckt_ExtPtfm", # ExtPtfm pollution path + "SWRT_YFree_VS_WTurb", # VSContrl=3 -> spd_trq.dat round-trip (R2) +] + + +@pytest.mark.parametrize("name", ROUNDTRIP_CASES) +def test_driver_roundtrip_preserves_outlist_and_scalars(r_test_dir, name, tmp_path): + src, reread = roundtrip(OpenFASTDriver(), _case(r_test_dir, name), tmp_path) + for k in ('TMax', 'CompElast', 'CompAero'): + assert reread['Fst'][k] == src['Fst'][k] + + +def test_servodyn_outlist_does_not_explode_on_write(r_test_dir, tmp_path): + """Direct guard for the full-registry-emit bug: ServoDyn round-tripped to 518 channels + (the whole registry) instead of the ~2 the deck requests.""" + src, reread = roundtrip(OpenFASTDriver(), _case(r_test_dir, "5MW_OC4Semi_WSt_WavesWN"), tmp_path) + for mod in ('ServoDyn', 'HydroDyn', 'SeaState'): + n_src, n_re = len(outlist_sets(src).get(mod, ())), len(outlist_sets(reread).get(mod, ())) + assert n_re == n_src, f"{mod} OutList exploded on write: {n_src} -> {n_re}" diff --git a/openfast_io/openfast_io/tests/test_driver_roundtrip.py b/openfast_io/openfast_io/tests/test_driver_roundtrip.py new file mode 100644 index 0000000000..c5179490ce --- /dev/null +++ b/openfast_io/openfast_io/tests/test_driver_roundtrip.py @@ -0,0 +1,697 @@ +""" +Standalone driver tests: read, roundtrip (read → write → re-read → compare), +and key-field smoke checks. + +Test case names mirror the CTestList.cmake entries for each module driver so +this file is the Python counterpart to the CMake regression suite. Each +parametrize list intentionally matches the *same* identifiers used in: + + reg_tests/CTestList.cmake + +Pattern: + 1. Read the driver input file from the r-test directory. + 2. Write to a temporary directory. + 3. Re-read from the temporary directory. + 4. Compare original and re-read dicts with compare_fst_vt. + +Only the driver dict keys are compared (not simulation outputs). +External data files that are only *referenced* (e.g. csv timeseries, wind +files, binary outb files) are not copied to the temp dir because they are not +written by the driver — the comparison uses removeFileRef=True so file-name +strings are excluded. + +Smoke tests (``test_*_read_smoke``) verify a handful of known scalar values +from specific r-test cases to confirm parsing correctness beyond structural +roundtrip equality. +""" +from __future__ import annotations + +import os +import shutil +import tempfile +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Locate r-test root (same logic as conftest.py: parents[2] = openfast repo) +# --------------------------------------------------------------------------- +_HERE = Path(__file__).resolve().parent +_REPO_ROOT = _HERE.parents[2] # tests/ → openfast_io (pkg) → openfast_io (proj) → openfast +_MODULES_DIR = _REPO_ROOT / 'reg_tests' / 'r-test' / 'modules' + +# --------------------------------------------------------------------------- +# Import driver classes and comparison helper +# --------------------------------------------------------------------------- +from openfast_io.drivers.aerodyn_driver import AeroDynStandaloneDriver +from openfast_io.drivers.beamdyn_driver import BeamDynStandaloneDriver +from openfast_io.drivers.hydrodyn_driver import HydroDynStandaloneDriver +from openfast_io.drivers.subdyn_driver import SubDynStandaloneDriver +from openfast_io.drivers.inflowwind_driver import InflowWindStandaloneDriver +from openfast_io.drivers.seastate_driver import SeaStateStandaloneDriver +from openfast_io.drivers.moordyn_driver import MoorDynStandaloneDriver +from openfast_io.drivers.aerodisk_driver import AeroDiskStandaloneDriver +from openfast_io.drivers.simple_elastodyn_driver import SimpleElastoDynStandaloneDriver +from openfast_io.drivers.unsteadyaero_driver import UnsteadyAeroStandaloneDriver +from openfast_io.FileTools import compare_fst_vt +from openfast_io.io.moordyn import MoorDynIO +from openfast_io.tests.test_io_offshore import _make_minimal_sd + + +# --------------------------------------------------------------------------- +# Generic roundtrip helper +# --------------------------------------------------------------------------- + +def _roundtrip(driver, dvr_path: Path, case_dir: Path) -> dict: + """Read → write to tmpdir → re-read. Returns diff dict (empty = pass).""" + + # --- READ --- + data_orig = driver.read(dvr_path) + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + out_dvr = tmp_path / dvr_path.name + + # Copy external data files that the driver references but does not + # write (timeseries csv, wind files, polar tables, motion files ...). + # We copy *all* non-module-input files from the case dir so that the + # re-read can resolve every reference without modification. + for src in case_dir.iterdir(): + if src.is_file(): + shutil.copy2(src, tmp_path / src.name) + # Copy any sub-directories (e.g. Airfoils/) + for src in case_dir.iterdir(): + if src.is_dir(): + shutil.copytree(src, tmp_path / src.name, dirs_exist_ok=True) + + # --- WRITE --- + driver.write(data_orig, out_dvr) + + # --- RE-READ --- + data_reread = driver.read(out_dvr) + + # --- COMPARE --- + diff = compare_fst_vt( + data_orig, data_reread, + ignoreVars=['OutRootName', 'TMax', 'TStart', 'OutFileFmt'], + removeFileRef=True, + removeArrayProps=True, + print_diff=False, + ) + + # --- OutList channel preservation --- + # Standalone drivers now thread an explicit 'outlist' registry through + # read()/write() (see drivers/*_driver.py). Whatever channels were + # enabled in the original read must still be enabled after a + # write → re-read cycle. + orig_outlist = data_orig.get('outlist') + reread_outlist = data_reread.get('outlist') + if orig_outlist is not None and reread_outlist is not None: + for module, channels in orig_outlist.items(): + if not isinstance(channels, dict): + continue + orig_true = {ch for ch, v in channels.items() if v} + reread_true = {ch for ch, v in reread_outlist.get(module, {}).items() if v} + if orig_true != reread_true: + diff[f'outlist.{module}'] = (sorted(orig_true), sorted(reread_true)) + + return diff + + +# --------------------------------------------------------------------------- +# Helper: skip if r-test case dir doesn't exist +# --------------------------------------------------------------------------- + +def _case_dir(module: str, case_name: str) -> Path: + return _MODULES_DIR / module / case_name + + +def _dvr_file(case_dir: Path, filename: str) -> Path: + return case_dir / filename + + +# =========================================================================== +# AeroDyn (mirrors ad_regression calls in CTestList.cmake) +# =========================================================================== + +# Each entry: (case_name, driver_filename) +_AD_CASES = [ + ('ad_timeseries_shutdown', 'ad_driver.dvr'), + ('ad_BAR_SineMotion', 'ad_driver.dvr'), + ('ad_BAR_SineMotion_UA4_DBEMT3', 'ad_driver.dvr'), + ('ad_BAR_RNAMotion', 'ad_driver.dvr'), + ('ad_MHK_RM1_Fixed', 'ad_driver.dvr'), + ('ad_MHK_RM1_Fixed_IfW', 'ad_driver.dvr'), + ('ad_MHK_RM1_Floating', 'ad_driver.dvr'), + ('ad_MultipleHAWT', 'ad_driver.dvr'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _AD_CASES, ids=[c[0] for c in _AD_CASES]) +def test_aerodyn_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('aerodyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = AeroDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'AeroDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# BeamDyn (mirrors bd_regression calls in CTestList.cmake) +# =========================================================================== + +_BD_CASES = [ + ('bd_5MW_dynamic', 'bd_driver.inp'), + ('bd_5MW_dynamic_gravity_Az00', 'bd_driver.inp'), + ('bd_5MW_dynamic_gravity_Az90', 'bd_driver.inp'), + ('bd_5MW_dynamic_modal_damping', 'bd_driver.inp'), + ('bd_curved_beam', 'bd_driver.inp'), + ('bd_isotropic_rollup', 'bd_driver.inp'), + ('bd_static_cantilever_beam', 'bd_driver.inp'), + ('bd_static_twisted_with_k1', 'bd_driver.inp'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _BD_CASES, ids=[c[0] for c in _BD_CASES]) +def test_beamdyn_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('beamdyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = BeamDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'BeamDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# HydroDyn (mirrors hd_regression calls in CTestList.cmake) +# =========================================================================== + +_HD_CASES = [ + ('hd_5MW_ITIBarge_DLL_WTurb_WavesIrr', 'hd_driver.inp'), + ('hd_5MW_OC3Spar_DLL_WTurb_WavesIrr', 'hd_driver.inp'), + ('hd_5MW_OC4Semi_WSt_WavesWN', 'hd_driver.inp'), + ('hd_5MW_TLP_DLL_WTurb_WavesIrr_WavesMulti', 'hd_driver.inp'), + ('hd_NBodyMod1', 'hd_driver.inp'), + ('hd_NBodyMod2', 'hd_driver.inp'), + ('hd_NBodyMod3', 'hd_driver.inp'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _HD_CASES) +def test_hydrodyn_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('hydrodyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = HydroDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'HydroDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# SubDyn (mirrors sd_regression calls in CTestList.cmake) +# =========================================================================== + +def _sd_dvr_name(case_name: str) -> str: + """SubDyn driver files are named .dvr.""" + return case_name + '.dvr' + + +_SD_CASES = [ + 'SD_Cable_5Joints', + 'SD_PendulumDamp', + 'SD_Rigid', + 'SD_SparHanging', + 'SD_AnsysComp2_Cable', + 'SD_Spring_Case1', + 'SD_Spring_Case2', + 'SD_Spring_Case3', + 'SD_Revolute_Joint', + 'SD_2Beam_Spring', + 'SD_2Beam_Cantilever', + 'SD_CantileverBeam_Rectangular', +] + + +@pytest.mark.parametrize('case_name', _SD_CASES) +def test_subdyn_driver_roundtrip(case_name): + cdir = _case_dir('subdyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, _sd_dvr_name(case_name)) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = SubDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'SubDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# InflowWind (mirrors ifw_regression calls in CTestList.cmake) +# =========================================================================== + +_IFW_CASES = [ + ('ifw_turbsimff', 'ifw_driver.inp'), + ('ifw_uniform', 'ifw_driver.inp'), + ('ifw_BoxExceed', 'ifw_driver.inp'), + ('ifw_BoxExceedTwr', 'ifw_driver.inp'), + ('ifw_HAWC', 'ifw_driver.inp'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _IFW_CASES, ids=[c[0] for c in _IFW_CASES]) +def test_inflowwind_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('inflowwind', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = InflowWindStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'InflowWind roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# SeaState (mirrors seast_regression calls in CTestList.cmake) +# =========================================================================== + +_SS_CASES = [ + ('seastate_1', 'seastate_driver.inp'), + ('seastate_wr_kin1', 'seastate_driver.inp'), + ('seastate_CNW1', 'seastate_driver.inp'), + ('seastate_CNW2', 'seastate_driver.inp'), + ('seastate_WaveMod7_WaveStMod1', 'seastate_driver.inp'), + ('seastate_WaveMod7_WaveStMod2', 'seastate_driver.inp'), + ('seastate_WaveMod7_WaveStMod3', 'seastate_driver.inp'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _SS_CASES, ids=[c[0] for c in _SS_CASES]) +def test_seastate_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('seastate', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = SeaStateStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'SeaState roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# MoorDyn (mirrors md_regression calls in CTestList.cmake) +# =========================================================================== + +_MD_CASES = [ + ('md_5MW_OC4Semi', 'md_driver.inp'), + ('md_BodiesAndRods','md_driver.inp'), + ('md_bodyDrag', 'md_driver.inp'), + ('md_cable', 'md_driver.inp'), + ('md_case2', 'md_driver.inp'), + ('md_case5', 'md_driver.inp'), + ('md_float', 'md_driver.inp'), + ('md_horizontal', 'md_driver.inp'), + ('md_no_line', 'md_driver.inp'), + ('md_vertical', 'md_driver.inp'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _MD_CASES, ids=[c[0] for c in _MD_CASES]) +def test_moordyn_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('moordyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = MoorDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'MoorDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# AeroDisk (mirrors adsk_regression calls in CTestList.cmake) +# =========================================================================== + +_ADSK_CASES = [ + ('adsk_timeseries_shutdown', 'adsk_driver.dvr'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _ADSK_CASES, ids=[c[0] for c in _ADSK_CASES]) +def test_aerodisk_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('aerodisk', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = AeroDiskStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'AeroDisk roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# SimpleElastoDyn (mirrors sed_regression calls in CTestList.cmake) +# =========================================================================== + +_SED_CASES = [ + ('sed_test_HSSbrk', 'sed_driver.dvr'), + ('sed_test_freewheel', 'sed_driver.dvr'), +] + + +@pytest.mark.parametrize('case_name,dvr_filename', _SED_CASES, ids=[c[0] for c in _SED_CASES]) +def test_simple_elastodyn_driver_roundtrip(case_name, dvr_filename): + cdir = _case_dir('simple-elastodyn', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + dvr_path = _dvr_file(cdir, dvr_filename) + if not dvr_path.is_file(): + pytest.skip(f'driver file not found: {dvr_path}') + + driver = SimpleElastoDynStandaloneDriver() + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'SimpleElastoDyn roundtrip diff for {case_name}:\n{diff}' + + +# =========================================================================== +# UnsteadyAero (mirrors ua_regression calls in CTestList.cmake) +# =========================================================================== + +def _ua_dvr_files(case_dir: Path): + """UnsteadyAero cases can have multiple .dvr files (UA2.dvr, UA3.dvr, ...).""" + return sorted(case_dir.glob('UA*.dvr')) + + +_UA_CASES = ['ua_redfreq', 'ua_elast'] + + +@pytest.mark.parametrize('case_name', _UA_CASES) +def test_unsteadyaero_driver_roundtrip(case_name): + cdir = _case_dir('unsteadyaero', case_name) + if not cdir.is_dir(): + pytest.skip(f'r-test dir not found: {cdir}') + + dvr_files = _ua_dvr_files(cdir) + if not dvr_files: + pytest.skip(f'No UA*.dvr files found in {cdir}') + + driver = UnsteadyAeroStandaloneDriver() + for dvr_path in dvr_files: + diff = _roundtrip(driver, dvr_path, cdir) + assert diff == {}, f'UnsteadyAero roundtrip diff for {case_name}/{dvr_path.name}:\n{diff}' + + +# =========================================================================== +# Smoke tests — verify known scalar values from specific r-test cases. +# These confirm parsing correctness beyond structural roundtrip equality. +# =========================================================================== + +class TestDriverReadSmoke: + """Spot-check key fields from known r-test driver files.""" + + def test_aerodyn_read_smoke(self): + cdir = _case_dir('aerodyn', 'ad_BAR_SineMotion') + if not cdir.is_dir(): + pytest.skip('ad_BAR_SineMotion r-test not available') + dvr = AeroDynStandaloneDriver().read(cdir / 'ad_driver.dvr')['AeroDynDriver'] + assert dvr['AnalysisType'] == 1 + assert dvr['NumTurbines'] == 1 + assert abs(dvr['TMax'] - 7.0) < 0.1 + turb = dvr['Turbines'][0] + assert turb['NumBlades'] == 3 + assert turb['BasicHAWTFormat'] is False + + def test_beamdyn_read_smoke(self): + cdir = _case_dir('beamdyn', 'bd_5MW_dynamic') + if not cdir.is_dir(): + pytest.skip('bd_5MW_dynamic r-test not available') + dvr = BeamDynStandaloneDriver().read(cdir / 'bd_driver.inp')['BeamDynDriver'] + assert dvr['DynamicSolve'] is True + assert dvr['t_final'] == 30.0 + assert abs(dvr['dt'] - 0.002) < 1e-6 + assert abs(dvr['Gy'] - (-9.8)) < 0.01 + assert len(dvr['GlbDCM']) == 3 + + def test_hydrodyn_read_smoke(self): + cdir = _case_dir('hydrodyn', 'hd_5MW_OC4Semi_WSt_WavesWN') + if not cdir.is_dir(): + pytest.skip('hd_5MW_OC4Semi r-test not available') + dvr = HydroDynStandaloneDriver().read(cdir / 'hd_driver.inp')['HydroDynDriver'] + assert abs(dvr['Gravity'] - 9.80665) < 0.001 + assert dvr['WtrDens'] == 1025 + assert dvr['WtrDpth'] == 200 + assert dvr['PRPInputsMod'] == 2 + + def test_subdyn_read_smoke(self): + cdir = _case_dir('subdyn', 'SD_Rigid') + if not cdir.is_dir(): + pytest.skip('SD_Rigid r-test not available') + dvr = SubDynStandaloneDriver().read(cdir / 'SD_Rigid.dvr')['SubDynDriver'] + assert abs(dvr['Gravity'] - 9.81) < 0.01 + assert dvr['NSteps'] == 2000 + assert dvr['TP_RefPoint_Z'] == 30.0 + + def test_inflowwind_read_smoke(self): + cdir = _case_dir('inflowwind', 'ifw_uniform') + if not cdir.is_dir(): + pytest.skip('ifw_uniform r-test not available') + dvr = InflowWindStandaloneDriver().read(cdir / 'ifw_driver.inp')['InflowWindDriver'] + assert dvr['IfWFileName'] == 'ifw_primary.inp' + assert dvr['NumTSteps'] == 8 + assert dvr['DT'] == 0.1 + + def test_seastate_read_smoke(self): + cdir = _case_dir('seastate', 'seastate_1') + if not cdir.is_dir(): + pytest.skip('seastate_1 r-test not available') + dvr = SeaStateStandaloneDriver().read(cdir / 'seastate_driver.inp')['SeaStateDriver'] + assert abs(dvr['Gravity'] - 9.80665) < 0.001 + assert dvr['WtrDens'] == 1025 + assert dvr['NSteps'] == 801 + + def test_moordyn_read_smoke(self): + cdir = _case_dir('moordyn', 'md_5MW_OC4Semi') + if not cdir.is_dir(): + pytest.skip('md_5MW_OC4Semi r-test not available') + dvr = MoorDynStandaloneDriver().read(cdir / 'md_driver.inp')['MoorDynDriver'] + assert abs(dvr['Gravity'] - 9.80665) < 0.001 + assert dvr['rhoW'] == 1025.0 + assert dvr['TMax'] == 60 + + def test_aerodisk_read_smoke(self): + cdir = _case_dir('aerodisk', 'adsk_timeseries_shutdown') + if not cdir.is_dir(): + pytest.skip('adsk_timeseries_shutdown r-test not available') + dvr = AeroDiskStandaloneDriver().read(cdir / 'adsk_driver.dvr')['AeroDiskDriver'] + assert dvr['AirDens'] == 1.225 + assert dvr['RotorRad'] == 63.0 + + def test_simple_elastodyn_read_smoke(self): + cdir = _case_dir('simple-elastodyn', 'sed_test_freewheel') + if not cdir.is_dir(): + pytest.skip('sed_test_freewheel r-test not available') + dvr = SimpleElastoDynStandaloneDriver().read(cdir / 'sed_driver.dvr')['SimpleElastoDynDriver'] + assert dvr['SEDiptFile'] == 'sed_primary.inp' + assert dvr['TimeseriesFile'] == 'Free.csv' + + def test_unsteadyaero_read_smoke(self): + cdir = _case_dir('unsteadyaero', 'ua_redfreq') + if not cdir.is_dir(): + pytest.skip('ua_redfreq r-test not available') + dvr_files = sorted(cdir.glob('UA*.dvr')) + if not dvr_files: + pytest.skip('No UA*.dvr files found') + dvr = UnsteadyAeroStandaloneDriver().read(dvr_files[0])['UnsteadyAeroDriver'] + assert dvr['FldDens'] == 1.225 + assert dvr['UAMod'] == 2 + assert dvr['Chord'] == 3.5 + assert len(dvr['MassMatrix']) == 3 + + +# =========================================================================== +# OutList threading — synthetic (no r-test data required). +# +# These exercise the standalone-driver → module-IO OutList registry wiring +# fixed in this review: SubDyn/SeaState drivers previously never passed +# outlist/read_outlist_fn to SubDynIO/SeaStateIO, so an OutList section +# would silently vanish on a .dvr roundtrip. Building minimal synthetic +# decks (rather than depending on reg_tests/r-test, which is not checked +# out in this environment) lets the fix be verified directly. +# =========================================================================== + +def _make_minimal_subdyn_driver_dict() -> dict: + return { + 'Echo': False, 'Gravity': 9.81, 'WtrDpth': 0.0, + 'SDInputFile': 'SubDyn.dat', 'OutRootName': 'sd_test', + 'NSteps': 10, 'TimeInterval': 0.01, 'NTPs': 1, + 'TP_RefPoint_X': 0.0, 'TP_RefPoint_Y': 0.0, 'TP_RefPoint_Z': 0.0, + 'SubRotateZ': 0.0, 'InputsMod': 1, 'InputsFile': '', + 'uTPInSteady': [0, 0, 0, 0, 0, 0], + 'uDotTPInSteady': [0, 0, 0, 0, 0, 0], + 'uDotDotTPInSteady': [0, 0, 0, 0, 0, 0], + 'nAppliedLoads': 0, 'AppliedLoads': [], + } + + +def test_subdyn_driver_outlist_roundtrip(tmp_path): + """SubDynStandaloneDriver must thread an OutList registry end-to-end. + + Regression for the finding that subdyn_driver.py never passed + outlist/read_outlist_fn to SubDynIO, silently dropping the SSOutList + section on a .dvr roundtrip. + """ + driver = SubDynStandaloneDriver() + channels = {'M1N1FKxe', 'M1N1MKxe'} + data = { + 'SubDynDriver': _make_minimal_subdyn_driver_dict(), + 'SubDyn': _make_minimal_sd(), + 'outlist': {'SubDyn': {ch: True for ch in channels}}, + } + + dvr_path = tmp_path / 'sd_driver.dvr' + driver.write(data, dvr_path) + + sd_dat = (tmp_path / 'SubDyn.dat').read_text() + for ch in channels: + assert f'"{ch}"' in sd_dat, f'{ch} was not written into SubDyn.dat SSOutList' + + reread = driver.read(dvr_path) + survived = {ch for ch, v in reread['outlist'].get('SubDyn', {}).items() if v} + assert survived == channels, f'SubDyn OutList channels did not survive driver roundtrip: {survived}' + + +def _make_minimal_seastate_dict() -> dict: + return { + 'Echo': False, 'WtrDens': 1025.0, 'WtrDpth': 200.0, 'MSL2SWL': 0.0, + 'X_HalfWidth': 100.0, 'Y_HalfWidth': 100.0, 'Z_Depth': 200.0, + 'NX': 2, 'NY': 2, 'NZ': 2, + 'WaveMod': 0, 'WaveStMod': 0, 'WvCrntMod': 0, 'WaveTMax': 0.0, + 'WaveDT': 0.25, 'WaveHs': 0.0, 'WaveTp': 0.0, 'WavePkShp': 0.0, + 'WvLowCOff': 0.0, 'WvHiCOff': 3.14, 'WaveDir': 0.0, 'WaveDirMod': 0, + 'WaveDirSpread': 1.0, 'WaveNDir': 1, 'WaveDirRange': 180.0, + 'WaveSeed1': 123, 'WaveSeed2': 456, 'WaveNDAmp': False, 'WvKinFile': '', + 'WvDiffQTF': False, 'WvSumQTF': False, + 'WvLowCOffD': 0.0, 'WvHiCOffD': 3.14, 'WvLowCOffS': 0.0, 'WvHiCOffS': 3.14, + 'ConstWaveMod': 0, 'CrestHmax': 0.0, 'CrestTime': 0.0, 'CrestXi': 0.0, 'CrestYi': 0.0, + 'CurrMod': 0, 'CurrSSV0': 0.0, 'CurrSSDir': 0.0, 'CurrNSRef': 0.0, + 'CurrNSV0': 0.0, 'CurrNSDir': 0.0, 'CurrDIV': 0.0, 'CurrDIDir': 0.0, + 'MCFD': 0.0, + 'SeaStSum': False, 'OutSwtch': 1, 'OutFmt': '"ES10.3E2"', 'OutSFmt': '"A11"', + 'NWaveElev': 0, 'WaveElevxi': [0.0], 'WaveElevyi': [0.0], + 'NWaveKin': 0, 'WaveKinxi': [0], 'WaveKinyi': [0], 'WaveKinzi': [0], + } + + +def _make_minimal_seastate_driver_dict() -> dict: + return { + 'Echo': False, 'Gravity': 9.80665, 'WtrDens': 1025.0, 'WtrDpth': 200.0, + 'MSL2SWL': 0.0, 'SeaStateInputFile': 'SeaState.dat', 'OutRootName': 'ss_test', + 'WrWvKinMod': 0, 'NSteps': 10, 'TimeInterval': 0.25, 'WaveElevSeriesFlag': False, + } + + +def test_seastate_driver_outlist_roundtrip(tmp_path): + """SeaStateStandaloneDriver must thread an OutList registry end-to-end. + + Regression for the finding that seastate_driver.py never passed + outlist/read_outlist_fn to SeaStateIO. + """ + driver = SeaStateStandaloneDriver() + channels = {'Wave1Elev', 'WavesF1yi'} + data = { + 'SeaStateDriver': _make_minimal_seastate_driver_dict(), + 'SeaState': _make_minimal_seastate_dict(), + 'outlist': {'SeaState': {ch: True for ch in channels}}, + } + + inp_path = tmp_path / 'ss_driver.inp' + driver.write(data, inp_path) + + ss_dat = (tmp_path / 'SeaState.dat').read_text() + for ch in channels: + assert f'"{ch}"' in ss_dat, f'{ch} was not written into SeaState.dat OUTPUT CHANNELS' + + reread = driver.read(inp_path) + survived = {ch for ch, v in reread['outlist'].get('SeaState', {}).items() if v} + assert survived == channels, f'SeaState OutList channels did not survive driver roundtrip: {survived}' + + +@pytest.mark.parametrize('module_name,driver_path', [ + ('AeroDyn', 'openfast_io.drivers.aerodyn_driver'), + ('BeamDyn', 'openfast_io.drivers.beamdyn_driver'), + ('InflowWind', 'openfast_io.drivers.inflowwind_driver'), + ('HydroDyn', 'openfast_io.drivers.hydrodyn_driver'), + ('SeaState', 'openfast_io.drivers.seastate_driver'), + ('SubDyn', 'openfast_io.drivers.subdyn_driver'), +]) +def test_driver_outlist_registry_seeded_with_known_channels(module_name, driver_path): + """Guard against the outlist registry silently becoming an empty dict. + + capture_outlist()'s non-freeform path (used by AeroDyn/BeamDyn/InflowWind/ + HydroDyn) only marks a channel True if it already exists as a key in + registry[module] — i.e. the registry must be seeded from FstOutput (as + OpenFASTDriver.init_fst_vt does), not initialized as a bare {}. If a + future edit swaps the seeding for `{}`, every captured channel would be + silently dropped with no error. SeaState/SubDyn are freeform (any channel + name is accepted) but are included here too since they share the same + seeding call in each driver. + """ + import importlib + mod = importlib.import_module(driver_path) + assert mod.FstOutput, f'{driver_path} has no FstOutput registry available' + assert module_name in mod.FstOutput, f'{module_name} missing from {driver_path}.FstOutput' + assert len(mod.FstOutput[module_name]) > 0 + + +# =========================================================================== +# MoorDyn OutList truthy filtering (fix for io/moordyn.py write()). +# =========================================================================== + +def test_moordyn_write_filters_false_channels(tmp_path): + """io/moordyn.py write() must only emit truthy channels. + + A registry built via the public OutList.to_fst_output() API contains an + explicit False entry for every known channel (not just the enabled + ones). Before this fix, MoorDynIO.write() emitted every key in + outlist['MoorDyn'] regardless of its value, which would write disabled + channels into the driver file as if they were enabled. + """ + io = MoorDynIO() + md: dict = {'Rod_Name': [], 'Body_ID': [], 'Rod_ID': []} + registry = { + 'MoorDyn': { + 'FairTen1': True, + 'AnchTen1': False, # must NOT be written + 'FairTen2': False, # must NOT be written + } + } + + out_file = tmp_path / 'moordyn_test.dat' + io.write({'MoorDyn': md}, str(out_file), outlist=registry) + + written = out_file.read_text() + assert '"FairTen1"' in written + assert 'AnchTen1' not in written + assert 'FairTen2' not in written + diff --git a/openfast_io/openfast_io/tests/test_facade.py b/openfast_io/openfast_io/tests/test_facade.py new file mode 100644 index 0000000000..edddf1538d --- /dev/null +++ b/openfast_io/openfast_io/tests/test_facade.py @@ -0,0 +1,158 @@ +"""Tests for facade classes (backward-compat wrappers). + +Tests both import paths: + - from openfast_io.FAST_reader import InputReader_OpenFAST (canonical) + - from openfast_io.facade import InputReader_Facade (alias) +""" +import os +import tempfile + +import pytest + +from openfast_io.FAST_reader import InputReader_OpenFAST +from openfast_io.FAST_writer import InputWriter_OpenFAST +from openfast_io.facade import InputReader_Facade, InputWriter_Facade + +_HERE = os.path.dirname(__file__) +_RTEST = os.path.join(_HERE, '..', '..', '..', 'reg_tests', 'r-test', + 'glue-codes', 'openfast') + +_5MW_DIR = os.path.join(_RTEST, '5MW_Land_DLL_WTurb') +_FST_FILE = '5MW_Land_DLL_WTurb.fst' + +_has_5mw = os.path.isfile(os.path.join(_5MW_DIR, _FST_FILE)) +skip5mw = pytest.mark.skipif(not _has_5mw, reason='5MW r-test data missing') + + +class TestReaderFacade: + @skip5mw + def test_reader_execute(self): + reader = InputReader_Facade() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + assert reader.fst_vt['Fst']['TMax'] == 60.0 + assert reader.fst_vt['Fst']['CompElast'] == 1 + + @skip5mw + def test_reader_populates_elastodyn(self): + reader = InputReader_Facade() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + assert 'NumBl' in reader.fst_vt['ElastoDyn'] + + @skip5mw + def test_reader_has_fst_vt_attribute(self): + reader = InputReader_Facade() + assert hasattr(reader, 'fst_vt') + assert 'Fst' in reader.fst_vt + + +class TestWriterFacade: + @skip5mw + def test_writer_execute(self): + reader = InputReader_Facade() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + + with tempfile.TemporaryDirectory() as tmpdir: + writer = InputWriter_Facade() + writer.fst_vt = reader.fst_vt + writer.FAST_namingOut = 'test_write' + writer.FAST_runDirectory = tmpdir + writer.execute() + + fst_out = os.path.join(tmpdir, 'test_write.fst') + assert os.path.isfile(fst_out) + + @skip5mw + def test_writer_roundtrip(self): + """Write then re-read preserves key scalars.""" + reader = InputReader_Facade() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + original = reader.fst_vt + + with tempfile.TemporaryDirectory() as tmpdir: + writer = InputWriter_Facade() + writer.fst_vt = original + writer.FAST_namingOut = 'rt' + writer.FAST_runDirectory = tmpdir + writer.execute() + + reader2 = InputReader_Facade() + reader2.FAST_InputFile = 'rt.fst' + reader2.FAST_directory = tmpdir + reader2.execute() + + assert reader2.fst_vt['Fst']['TMax'] == original['Fst']['TMax'] + assert reader2.fst_vt['Fst']['CompElast'] == original['Fst']['CompElast'] + + def test_writer_update(self): + writer = InputWriter_Facade() + writer.fst_vt = {'Fst': {'TMax': 60.0, 'DT': 0.005}} + writer.update({'Fst': {'TMax': 120.0}}) + assert writer.fst_vt['Fst']['TMax'] == 120.0 + assert writer.fst_vt['Fst']['DT'] == 0.005 + + +class TestLegacyImportPaths: + """Verify that the canonical FAST_reader / FAST_writer import paths work.""" + + def test_facade_aliases_are_same_class(self): + assert InputReader_Facade is InputReader_OpenFAST + assert InputWriter_Facade is InputWriter_OpenFAST + + @skip5mw + def test_legacy_reader_execute(self): + reader = InputReader_OpenFAST() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + assert reader.fst_vt['Fst']['TMax'] == 60.0 + assert 'NumBl' in reader.fst_vt['ElastoDyn'] + + @skip5mw + def test_legacy_writer_roundtrip(self): + reader = InputReader_OpenFAST() + reader.FAST_InputFile = _FST_FILE + reader.FAST_directory = _5MW_DIR + reader.execute() + + with tempfile.TemporaryDirectory() as tmpdir: + writer = InputWriter_OpenFAST() + writer.fst_vt = reader.fst_vt + writer.FAST_namingOut = 'legacy_rt' + writer.FAST_runDirectory = tmpdir + writer.execute() + + reader2 = InputReader_OpenFAST() + reader2.FAST_InputFile = 'legacy_rt.fst' + reader2.FAST_directory = tmpdir + reader2.execute() + + assert reader2.fst_vt['Fst']['TMax'] == reader.fst_vt['Fst']['TMax'] + + def test_reader_instantiation_warns_deprecation(self): + with pytest.warns(DeprecationWarning, match="InputReader_OpenFAST is deprecated"): + InputReader_OpenFAST() + + def test_writer_instantiation_warns_deprecation(self): + with pytest.warns(DeprecationWarning, match="InputWriter_OpenFAST is deprecated"): + InputWriter_OpenFAST() + + def test_parsing_re_exports(self): + """External code importing parsing helpers from FAST_reader still works.""" + from openfast_io.FAST_reader import bool_read, float_read, int_read, quoted_read + assert bool_read('True') is True + assert float_read('3.14') == pytest.approx(3.14) + assert int_read('42') == 42 + + def test_writer_helper_functions(self): + """External code importing helper functions from FAST_writer still works.""" + from openfast_io.FAST_writer import auto_format, float_default_out, int_default_out + assert callable(auto_format) + assert '3.1400' in float_default_out(3.14, trim=True) diff --git a/openfast_io/openfast_io/tests/test_formats.py b/openfast_io/openfast_io/tests/test_formats.py new file mode 100644 index 0000000000..d178023b28 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_formats.py @@ -0,0 +1,40 @@ +import json +import pytest +from openfast_io.formats import fst_vt_to_json, fst_vt_from_json, fst_vt_to_yaml, fst_vt_from_yaml + + +def test_json_roundtrip_preserves_values(): + fst_vt = {'ElastoDyn': {'NumBl': 3, 'RotSpeed': 12.1, 'FlapDOF1': True}} + s = fst_vt_to_json(fst_vt) + recovered = fst_vt_from_json(s) + assert recovered['ElastoDyn']['NumBl'] == 3 + assert abs(recovered['ElastoDyn']['RotSpeed'] - 12.1) < 1e-9 + assert recovered['ElastoDyn']['FlapDOF1'] is True + + +def test_json_is_valid_json(): + fst_vt = {'Fst': {'TMax': 60.0}, 'ElastoDyn': {'NumBl': 3}} + s = fst_vt_to_json(fst_vt) + parsed = json.loads(s) + assert parsed['Fst']['TMax'] == 60.0 + + +def test_yaml_roundtrip(): + fst_vt = {'ElastoDyn': {'NumBl': 3, 'RotSpeed': 12.1}} + s = fst_vt_to_yaml(fst_vt) + recovered = fst_vt_from_yaml(s) + assert recovered['ElastoDyn']['NumBl'] == 3 + + +def test_json_handles_empty(): + fst_vt = {'Fst': {}, 'ElastoDyn': {}} + s = fst_vt_to_json(fst_vt) + recovered = fst_vt_from_json(s) + assert recovered == fst_vt + + +def test_yaml_handles_lists(): + fst_vt = {'AeroDynPolar': [{'Re': 1e6}, {'Re': 2e6}]} + s = fst_vt_to_yaml(fst_vt) + recovered = fst_vt_from_yaml(s) + assert len(recovered['AeroDynPolar']) == 2 diff --git a/openfast_io/openfast_io/tests/test_io_base.py b/openfast_io/openfast_io/tests/test_io_base.py new file mode 100644 index 0000000000..6526bb45a9 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_io_base.py @@ -0,0 +1,64 @@ +from pathlib import Path +import pytest +from openfast_io.io.base import ModuleIO + +from openfast_io.io.aerodyn import AeroDynIO +from openfast_io.io.elastodyn import ElastoDynIO +from openfast_io.io.simple_elastodyn import SimpleElastoDynIO +from openfast_io.io.beamdyn import BeamDynIO +from openfast_io.io.inflowwind import InflowWindIO +from openfast_io.io.aerodisk import AeroDiskIO +from openfast_io.io.servodyn import ServoDynIO +from openfast_io.io.hydrodyn import HydroDynIO +from openfast_io.io.seastate import SeaStateIO +from openfast_io.io.subdyn import SubDynIO +from openfast_io.io.moordyn import MoorDynIO +from openfast_io.io.map_io import MAPIO +from openfast_io.io.extptfm import ExtPtfmIO + + +# Every concrete module IO class — replaces the per-module +# *_is_module_io / *_implements_module_io boilerplate. +ALL_IO_CLASSES = [ + AeroDynIO, ElastoDynIO, SimpleElastoDynIO, BeamDynIO, InflowWindIO, + AeroDiskIO, ServoDynIO, HydroDynIO, SeaStateIO, SubDynIO, MoorDynIO, + MAPIO, ExtPtfmIO, +] + + +@pytest.mark.parametrize("cls", ALL_IO_CLASSES, ids=lambda c: c.__name__) +def test_io_class_is_module_io(cls): + """Each concrete IO class instantiates and is a ModuleIO subclass.""" + assert isinstance(cls(), ModuleIO) + + +def test_module_io_is_abstract(): + """ModuleIO cannot be instantiated directly.""" + with pytest.raises(TypeError): + ModuleIO() + + +def test_module_io_subclass_must_implement_read(): + class NoRead(ModuleIO): + def write(self, data, file_path, base_dir): + pass + with pytest.raises(TypeError): + NoRead() + + +def test_module_io_subclass_must_implement_write(): + class NoWrite(ModuleIO): + def read(self, file_path, base_dir): + return {} + with pytest.raises(TypeError): + NoWrite() + + +def test_concrete_subclass_is_instantiable(): + class ConcreteIO(ModuleIO): + def read(self, file_path: Path, base_dir: Path) -> dict: + return {} + def write(self, data: dict, file_path: Path, base_dir: Path) -> None: + pass + io = ConcreteIO() + assert io.read(Path("."), Path(".")) == {} diff --git a/openfast_io/openfast_io/tests/test_io_offshore.py b/openfast_io/openfast_io/tests/test_io_offshore.py new file mode 100644 index 0000000000..a0554f0b7f --- /dev/null +++ b/openfast_io/openfast_io/tests/test_io_offshore.py @@ -0,0 +1,272 @@ +"""Edge-case IO tests for offshore modules. + +Consolidated from the former per-module test files. Read/write/OutList +round-trip integration is covered by ``test_driver_openfast.py`` and the +external differential harness; only unique edge cases survive here. + +Modules: HydroDyn, SeaState, SubDyn, MoorDyn, MAP, ExtPtfm. +""" +import os +import tempfile + +import numpy as np +import pytest + +from openfast_io.io.hydrodyn import HydroDynIO +from openfast_io.io.seastate import SeaStateIO +from openfast_io.io.subdyn import SubDynIO +from openfast_io.io.moordyn import MoorDynIO +from openfast_io.io.map_io import MAPIO +from openfast_io.io.extptfm import ExtPtfmIO +from openfast_io.outlist import capture_outlist + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(__file__) +_RTEST = os.path.join(_HERE, '..', '..', '..', 'reg_tests', 'r-test', + 'glue-codes', 'openfast') + +_OC4_DIR = os.path.join(_RTEST, 'StC_test_OC4Semi') +_HD_FILE = os.path.join(_OC4_DIR, + 'NRELOffshrBsline5MW_OC4DeepCwindSemi_HydroDyn.dat') +_SS_FILE = os.path.join(_OC4_DIR, 'SeaState.dat') + +_OC3_DIR = os.path.join(_RTEST, '5MW_OC3Mnpl_DLL_WTurb_WavesIrr') +_SD_FILE = os.path.join(_OC3_DIR, 'NRELOffshrBsline5MW_OC3Monopile_SubDyn.dat') + +_MD_FILE = os.path.join(_OC4_DIR, 'NRELOffshrBsline5MW_OC4DeepCwindSemi_MoorDyn.dat') + +_MAP_DIR = os.path.join(_RTEST, '5MW_OC4Semi_WSt_WavesWN') +_MAP_FILE = os.path.join(_MAP_DIR, 'NRELOffshrBsline5MW_OC4DeepCwindSemi_MAP.dat') + +_EP_DIR = os.path.join(_RTEST, '5MW_OC4Jckt_ExtPtfm') +_EP_FILE = os.path.join(_EP_DIR, 'ExtPtfm.dat') + +_has_oc4 = os.path.isfile(_HD_FILE) and os.path.isfile(_SS_FILE) +_has_sd = os.path.isfile(_SD_FILE) +_has_md = os.path.isfile(_MD_FILE) +_has_map = os.path.isfile(_MAP_FILE) +_has_ep = os.path.isfile(_EP_FILE) + +skipoc4 = pytest.mark.skipif(not _has_oc4, reason='r-test data missing') +skipsd = pytest.mark.skipif(not _has_sd, reason='SubDyn r-test data missing') +skipmd = pytest.mark.skipif(not _has_md, reason='MoorDyn r-test data missing') +skipmap = pytest.mark.skipif(not _has_map, reason='MAP r-test data missing') +skipep = pytest.mark.skipif(not _has_ep, reason='ExtPtfm r-test data missing') + + +# =================================================================== +# HydroDyn edge cases +# =================================================================== +@skipoc4 +def test_hydrodyn_potfile_paths(): + io = HydroDynIO() + hd = io.read(_HD_FILE)['HydroDyn'] + assert isinstance(hd['PotFile'], list) + assert len(hd['PotFile']) == hd['NBody'] + + +@skipoc4 +def test_hydrodyn_arrays(): + io = HydroDynIO() + hd = io.read(_HD_FILE)['HydroDyn'] + assert len(hd['WAMITULEN']) == hd['NBody'] + assert len(hd['PtfmRefxt']) == hd['NBody'] + + +@skipoc4 +def test_hydrodyn_matrices(): + io = HydroDynIO() + hd = io.read(_HD_FILE)['HydroDyn'] + assert hd['AddCLin'].shape[0] == 6 + assert hd['AddBLin'].shape[0] == 6 + assert hd['AddBQuad'].shape[0] == 6 + + +@skipoc4 +def test_hydrodyn_joints(): + io = HydroDynIO() + hd = io.read(_HD_FILE)['HydroDyn'] + n = hd['NJoints'] + assert len(hd['JointID']) == n + assert len(hd['Jointxi']) == n + + +@skipoc4 +def test_hydrodyn_members(): + io = HydroDynIO() + hd = io.read(_HD_FILE)['HydroDyn'] + n = hd['NMembers'] + assert len(hd['MemberID']) == n + assert len(hd['PropPot']) == n + + +# =================================================================== +# SeaState edge cases +# =================================================================== +@skipoc4 +def test_seastate_wave_params(): + io = SeaStateIO() + ss = io.read(_SS_FILE)['SeaState'] + assert isinstance(ss['WaveHs'], float) + assert isinstance(ss['WaveTp'], float) + assert isinstance(ss['WaveSeed1'], int) + + +@skipoc4 +def test_seastate_current(): + io = SeaStateIO() + ss = io.read(_SS_FILE)['SeaState'] + assert isinstance(ss['CurrMod'], int) + + +# =================================================================== +# SubDyn edge cases +# =================================================================== +@skipsd +def test_subdyn_joints(): + io = SubDynIO() + sd = io.read(_SD_FILE)['SubDyn'] + assert len(sd['JointID']) == sd['NJoints'] + assert len(sd['JointXss']) == sd['NJoints'] + + +@skipsd +def test_subdyn_members(): + io = SubDynIO() + sd = io.read(_SD_FILE)['SubDyn'] + assert len(sd['MemberID']) == sd['NMembers'] + + +@skipsd +def test_subdyn_prop_sets(): + io = SubDynIO() + sd = io.read(_SD_FILE)['SubDyn'] + assert len(sd['PropSetID1']) == sd['NPropSetsCyl'] + + +def _make_minimal_sd() -> dict: + """Construct a minimal SubDyn data dict for writing — every table section is + empty except a single member-node output request, so io/subdyn.py's write() + reaches the SSOutList section without needing real geometry/property data.""" + sd = { + 'Echo': False, 'SDdeltaT': 'default', 'IntMethod': 3, 'SttcSolve': False, + 'FEMMod': 3, 'NDiv': 1, 'Nmodes': 0, 'JDampings': 1.0, 'GuyanDampMod': 0, + 'RayleighDamp': [0.0, 0.0], 'GuyanDampSize': 0, 'GuyanDamp': np.zeros((0, 0)), + 'RBSurge': 0.0, 'RBSway': 0.0, 'RBHeave': 0.0, + 'RBRoll': 0.0, 'RBPitch': 0.0, 'RBYaw': 0.0, + 'NJoints': 0, 'NReact': 0, 'NInterf': 0, 'NMembers': 0, + 'NPropSetsCyl': 0, 'NPropSetsRec': 0, 'NXPropSets': 0, + 'NCablePropSets': 0, 'NRigidPropSets': 0, 'NSpringPropSets': 0, + 'NCOSMs': 0, 'NCmass': 0, + 'SumPrint': False, 'OutCOSM': False, 'OutAll': False, 'OutSwtch': 1, + 'TabDelim': True, 'OutDec': 1, 'OutFmt': '"ES10.3E2"', 'OutSFmt': '"A11"', + 'NMOutputs': 1, 'MemberID_out': [1], 'NOutCnt': [1], 'NodeCnt': [[1]], + } + return sd + + +def test_subdyn_ssoutlist_emit_and_roundtrip(tmp_path): + """Regression for commit 7eff56947 (fix(subdyn): emit SSOutList via emit_outlist). + + That fix had zero test coverage — no test greps SSOutList. Exercise + io/subdyn.py's own read+write path directly (not the driver) with a registry + holding SubDyn member-node channels (e.g. M1N1FKxe), and assert they are + actually written into the file and survive a read-back roundtrip. + """ + io = SubDynIO() + sd = _make_minimal_sd() + channels = {'M1N1FKxe', 'M1N1MKxe'} + write_registry = {'SubDyn': {ch: True for ch in channels}} + + out_file = tmp_path / 'subdyn_test.dat' + io.write({'SubDyn': sd}, str(out_file), outlist=write_registry) + + written = out_file.read_text() + for ch in channels: + assert f'"{ch}"' in written, f'{ch} was not emitted into the written SSOutList' + + read_registry: dict = {} + + def _cap_ff(f, module): + return capture_outlist(f, read_registry, module, freeform=True) + + result = io.read(str(out_file), outlist=read_registry, read_outlist_fn=_cap_ff) + assert result['SubDyn']['NMOutputs'] == 1 + + survived = {ch for ch, v in read_registry.get('SubDyn', {}).items() if v is True} + assert survived == channels, f'SSOutList channels did not survive roundtrip: {survived}' + + +# =================================================================== +# MoorDyn edge cases +# =================================================================== +@skipmd +def test_moordyn_line_types(): + io = MoorDynIO() + md = io.read(_MD_FILE)['MoorDyn'] + assert 'Name' in md + assert len(md['Name']) >= 1 + + +@skipmd +def test_moordyn_points(): + io = MoorDynIO() + md = io.read(_MD_FILE)['MoorDyn'] + assert 'Point_ID' in md + assert len(md['Point_ID']) > 0 + + +@skipmd +def test_moordyn_lines(): + io = MoorDynIO() + md = io.read(_MD_FILE)['MoorDyn'] + assert 'Line_ID' in md + assert len(md['Line_ID']) > 0 + + +# =================================================================== +# MAP edge cases +# =================================================================== +@skipmap +def test_map_line_types(): + io = MAPIO() + m = io.read(_MAP_FILE)['MAP'] + assert 'LineType' in m + assert len(m['LineType']) >= 1 + + +@skipmap +def test_map_nodes(): + io = MAPIO() + m = io.read(_MAP_FILE)['MAP'] + assert 'Node' in m + assert len(m['Node']) > 0 + + +# =================================================================== +# ExtPtfm edge cases +# =================================================================== +@skipep +def test_extptfm_superelement(): + io = ExtPtfmIO() + ep = io.read(_EP_FILE)['ExtPtfm'] + flex = ep['FlexASCII'] + assert flex['nDOF'] == 31 + assert flex['MassMatrix'].shape == (31, 31) + assert flex['StiffnessMatrix'].shape == (31, 31) + assert flex['DampingMatrix'].shape == (31, 31) + assert flex['WeightConstant'].shape == (1, 31) + assert flex['WeightStiffness'].shape == (31, 31) + + +@skipep +def test_extptfm_user_forcing(): + io = ExtPtfmIO() + ep = io.read(_EP_FILE)['ExtPtfm'] + assert 'UserForcing' in ep + uf = ep['UserForcing'] + assert uf['nSteps'] == 501 + # time + 31 DOF columns + assert uf['ForceTimeSeries'].shape == (501, 32) diff --git a/openfast_io/openfast_io/tests/test_io_onshore.py b/openfast_io/openfast_io/tests/test_io_onshore.py new file mode 100644 index 0000000000..bf2bc9fd68 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_io_onshore.py @@ -0,0 +1,398 @@ +"""Edge-case IO tests for onshore modules. + +Consolidated from the former per-module test files. Read/write/OutList +round-trip integration is covered by ``test_driver_openfast.py`` and the +external differential harness; only unique edge cases survive here. + +Modules: ElastoDyn, SimpleElastoDyn, BeamDyn, AeroDyn, AeroDisk, +InflowWind, ServoDyn. +""" +import copy +import os +import tempfile + +import pytest +from pathlib import Path + +from openfast_io.FAST_vars_out import FstOutput +from openfast_io.io.aerodyn import AeroDynIO +from openfast_io.io.elastodyn import ElastoDynIO +from openfast_io.io.simple_elastodyn import SimpleElastoDynIO +from openfast_io.io.beamdyn import BeamDynIO +from openfast_io.io.inflowwind import InflowWindIO +from openfast_io.io.aerodisk import AeroDiskIO +from openfast_io.io.servodyn import ServoDynIO + + +# ── Fixtures / paths ───────────────────────────────────────────────────────── + +R_TEST_5MW = os.path.join( + os.path.dirname(__file__), '..', '..', '..', + 'reg_tests', 'r-test', 'glue-codes', 'openfast', '5MW_Land_DLL_WTurb' +) + + +@pytest.fixture +def r_test_5mw_dir(): + d = os.path.normpath(R_TEST_5MW) + if not os.path.isdir(d): + pytest.skip('r-test 5MW data not found') + return d + + +@pytest.fixture +def ad_io(): + return AeroDynIO() + + +R_TEST_BASE = os.path.normpath(os.path.join( + os.path.dirname(__file__), '..', '..', '..', + 'reg_tests', 'r-test', 'glue-codes', 'openfast' +)) + + +@pytest.fixture +def baseline_dir(): + d = os.path.join(R_TEST_BASE, '5MW_Baseline') + if not os.path.isdir(d): + pytest.skip('r-test 5MW_Baseline data not found') + return d + + +# AeroDisk / SimpleElastoDyn r-test paths +_HERE = os.path.dirname(__file__) +_RTEST = os.path.join(_HERE, '..', '..', '..', 'reg_tests', 'r-test', + 'glue-codes', 'openfast') +_ADSK_SED_DIR = os.path.join(_RTEST, '5MW_Land_DLL_WTurb_ADsk_SED') +_SED_FILE = os.path.join(_ADSK_SED_DIR, 'NREL_5MW_Simplified-ElastoDyn.dat') +_ADSK_FILE = os.path.join(_ADSK_SED_DIR, 'NRELOffshrBsline5MW_Onshore_AeroDisk.dat') + +_has_sed = os.path.isfile(_SED_FILE) +_has_adsk = os.path.isfile(_ADSK_FILE) +skipsed = pytest.mark.skipif(not _has_sed, reason='SimpleElastoDyn r-test data missing') +skipadsk = pytest.mark.skipif(not _has_adsk, reason='AeroDisk r-test data missing') + +# ServoDyn r-test paths +_5MW_DIR = os.path.join(_RTEST, '5MW_Land_DLL_WTurb') +_5MW_SD = os.path.join(_5MW_DIR, 'NRELOffshrBsline5MW_Onshore_ServoDyn.dat') +_STC_DIR = os.path.join(_RTEST, 'StC_test_OC4Semi') +_STC_SD = os.path.join(_STC_DIR, 'ServoDyn_with_StC.dat') +_HAVE_5MW = os.path.isfile(_5MW_SD) +_HAVE_STC = os.path.isfile(_STC_SD) + + +# ── AeroDyn edge cases ─────────────────────────────────────────────────────── + +def test_aerodyn_polars(ad_io, r_test_5mw_dir): + """Verify airfoil polar data is read.""" + ad_file = os.path.join(r_test_5mw_dir, 'NRELOffshrBsline5MW_Onshore_AeroDyn.dat') + result = ad_io.read(ad_file, r_test_5mw_dir, num_blades=3, aero_file_path='') + + ad = result['AeroDyn'] + assert 'af_data' in ad + assert len(ad['af_data']) == ad['NumAFfiles'] + + # Each af_data entry should be a list of tab dicts + for afi, tabs in enumerate(ad['af_data']): + assert isinstance(tabs, list) + assert len(tabs) > 0 + polar = tabs[0] + assert 'Alpha' in polar + assert 'Cl' in polar + assert 'Cd' in polar + assert polar['NumAlf'] > 0 + + +def test_aerodyn_coords(ad_io, r_test_5mw_dir): + """Verify airfoil coordinate data is read.""" + ad_file = os.path.join(r_test_5mw_dir, 'NRELOffshrBsline5MW_Onshore_AeroDyn.dat') + result = ad_io.read(ad_file, r_test_5mw_dir, num_blades=3, aero_file_path='') + + ad = result['AeroDyn'] + assert 'af_coord' in ad + assert 'ac' in ad + assert len(ad['af_coord']) == ad['NumAFfiles'] + + +def test_aerodyn_bem_options(ad_io, r_test_5mw_dir): + """BEM-specific parameters should be present.""" + ad_file = os.path.join(r_test_5mw_dir, 'NRELOffshrBsline5MW_Onshore_AeroDyn.dat') + result = ad_io.read(ad_file, r_test_5mw_dir, num_blades=3, aero_file_path='') + ad = result['AeroDyn'] + + # BEM params + assert 'Skew_Mod' in ad + assert 'TipLoss' in ad + assert 'HubLoss' in ad + assert 'TanInd' in ad + assert 'MaxIter' in ad + assert 'SectAvg' in ad + + +def test_aerodyn_nacelle_and_hub(ad_io, r_test_5mw_dir): + """Hub and nacelle parameters should be present.""" + ad_file = os.path.join(r_test_5mw_dir, 'NRELOffshrBsline5MW_Onshore_AeroDyn.dat') + result = ad_io.read(ad_file, r_test_5mw_dir, num_blades=3, aero_file_path='') + ad = result['AeroDyn'] + + assert 'VolHub' in ad + assert 'HubCenBx' in ad + assert 'VolNac' in ad + assert 'NacCenB' in ad + assert 'TFinAero' in ad + + +# ── ElastoDyn edge cases ───────────────────────────────────────────────────── + +def test_elastodyn_blade_data(sample_ed_file, tmp_path): + io = ElastoDynIO() + result = io.read(sample_ed_file, tmp_path) + blades = result['ElastoDynBlade'] + assert isinstance(blades, list) + assert len(blades) == 3 + # All 3 blades reference the same file, so all should have data + assert blades[0]['NBlInpSt'] == 6 + assert len(blades[0]['BlFract']) == 6 + + +def test_elastodyn_tower_data(sample_ed_file, tmp_path): + io = ElastoDynIO() + result = io.read(sample_ed_file, tmp_path) + tower = result['ElastoDynTower'] + assert tower['NTwInpSt'] == 3 + assert len(tower['HtFract']) == 3 + + +def _new_outlist_registry() -> dict: + """A fresh registry pre-populated with the ElastoDyn + ElastoDyn_Nodes schema + (all channels False), matching how OpenFASTDriver.init_fst_vt seeds fst_vt['outlist'] + before handing it to capture_outlist.""" + return { + 'ElastoDyn': copy.deepcopy(FstOutput['ElastoDyn']), + 'ElastoDyn_Nodes': copy.deepcopy(FstOutput['ElastoDyn_Nodes']), + } + + +def test_elastodyn_nodal_outlist_roundtrip(sample_ed_file_with_nodal, tmp_path): + """Regression: ElastoDyn's optional nodal OutList section (BldNd_BladesOut > 0) + was captured under the wrong registry key ('ElastoDyn' instead of + 'ElastoDyn_Nodes') on read, and never emitted at all on write — silently + dropping every nodal channel on a read->write->read roundtrip. + """ + io = ElastoDynIO() + + registry = _new_outlist_registry() + pristine_main = copy.deepcopy(registry['ElastoDyn']) + result = io.read(sample_ed_file_with_nodal, tmp_path, outlist=registry) + ed = result['ElastoDyn'] + assert ed['BldNd_BladesOut'] == 1 + + nodal_true = {k for k, v in registry['ElastoDyn_Nodes'].items() if v is True} + assert nodal_true == {'TDx', 'TDy', 'RDx'}, ( + f"nodal channels not captured under ElastoDyn_Nodes: {nodal_true}" + ) + # The main OutList section in the fixture is empty (no channels listed), so + # the main 'ElastoDyn' registry entry must be untouched by the nodal read — + # guards against the nodal channels being captured under the wrong ('ElastoDyn') + # key, which is the bug being fixed here. + assert registry['ElastoDyn'] == pristine_main, ( + "nodal read leaked channels into the main ElastoDyn registry entry" + ) + + out_file = tmp_path / "ElastoDyn_written.dat" + io.write(result, out_file, tmp_path, outlist=registry) + + written = out_file.read_text() + for ch in ('TDx', 'TDy', 'RDx'): + assert f'"{ch}"' in written, f'{ch} was not emitted into the written nodal OutList' + + registry2 = _new_outlist_registry() + result2 = io.read(out_file, tmp_path, outlist=registry2) + assert result2['ElastoDyn']['BldNd_BladesOut'] == 1 + nodal_true2 = {k for k, v in registry2['ElastoDyn_Nodes'].items() if v is True} + assert nodal_true2 == {'TDx', 'TDy', 'RDx'}, ( + f"nodal channels did not survive roundtrip: {nodal_true2}" + ) + + +# ── BeamDyn edge cases ─────────────────────────────────────────────────────── + +def test_beamdyn_blade(baseline_dir): + io = BeamDynIO() + bd_file = os.path.join(baseline_dir, 'NRELOffshrBsline5MW_BeamDyn.dat') + result = io.read(bd_file, baseline_dir) + + bld = result['BeamDynBlade'] + assert bld['station_total'] > 0 + assert len(bld['radial_stations']) == bld['station_total'] + assert bld['beam_stiff'].shape == (bld['station_total'], 6, 6) + assert bld['beam_inertia'].shape == (bld['station_total'], 6, 6) + + +def test_beamdyn_member_geometry(baseline_dir): + io = BeamDynIO() + bd_file = os.path.join(baseline_dir, 'NRELOffshrBsline5MW_BeamDyn.dat') + result = io.read(bd_file, baseline_dir) + + bd = result['BeamDyn'] + for mem in bd['members']: + assert 'kp_xr' in mem + assert 'kp_yr' in mem + assert 'kp_zr' in mem + assert 'initial_twist' in mem + assert len(mem['kp_xr']) == len(mem['kp_yr']) + + +# ── InflowWind edge cases ──────────────────────────────────────────────────── + +def test_inflowwind_steady(baseline_dir): + io = InflowWindIO() + ifw_file = os.path.join(baseline_dir, 'NRELOffshrBsline5MW_InflowWind_Steady8mps.dat') + result = io.read(ifw_file, baseline_dir) + ifw = result['InflowWind'] + assert ifw['WindType'] == 1 + assert ifw['HWindSpeed'] == 8.0 + + +def test_inflowwind_hawc_params(baseline_dir): + io = InflowWindIO() + ifw_file = os.path.join(baseline_dir, 'NRELOffshrBsline5MW_InflowWind_12mps.dat') + result = io.read(ifw_file, baseline_dir) + ifw = result['InflowWind'] + + # HAWC params should be present even if not used + assert 'ScaleMethod' in ifw + assert 'URef' in ifw + assert 'WindProfile' in ifw + + +# ── AeroDisk edge cases ────────────────────────────────────────────────────── + +@skipadsk +def test_aerodisk_disk_table(): + io = AeroDiskIO() + ad = io.read(_ADSK_FILE)['AeroDisk'] + assert 'actuatorDiskTable' in ad + tbl = ad['actuatorDiskTable'] + assert 'attr' in tbl + assert 'data' in tbl + assert len(tbl['data']) > 0 + + +# ── SimpleElastoDyn edge cases ─────────────────────────────────────────────── + +@skipsed +def test_sed_outlist(): + io = SimpleElastoDynIO() + sed = io.read(_SED_FILE)['SimpleElastoDyn'] + assert '_outlist' in sed + assert len(sed['_outlist']) > 0 + assert 'BlPitch1' in sed['_outlist'] + + +# ── ServoDyn edge cases ────────────────────────────────────────────────────── + +@pytest.mark.skipif(not _HAVE_5MW, reason='r-test data not available') +class TestServoDyn5MW: + + def test_servodyn_generator_torque(self): + io = ServoDynIO() + result = io.read(_5MW_SD, base_dir=_5MW_DIR) + sd = result['ServoDyn'] + assert 'VSContrl' in sd + assert 'GenEff' in sd + + def test_servodyn_bladed_interface(self): + io = ServoDynIO() + result = io.read(_5MW_SD, base_dir=_5MW_DIR) + sd = result['ServoDyn'] + assert 'DLL_FileName' in sd + assert 'DLL_NumTrq' in sd + + +@pytest.mark.skipif(not _HAVE_STC, reason='r-test StC data not available') +class TestServoDynStC: + + def test_servodyn_stc_counts(self): + io = ServoDynIO() + result = io.read(_STC_SD, base_dir=_STC_DIR, + servo_file_rel='ServoDyn_with_StC.dat') + sd = result['ServoDyn'] + # This test case should have StC files defined + # Verify counts match the loaded lists + assert len(result['BStC']) == sd['NumBStC'] + assert len(result['NStC']) == sd['NumNStC'] + assert len(result['TStC']) == sd['NumTStC'] + assert len(result['SStC']) == sd['NumSStC'] + + def test_servodyn_stc_fields(self): + io = ServoDynIO() + result = io.read(_STC_SD, base_dir=_STC_DIR, + servo_file_rel='ServoDyn_with_StC.dat') + # Check at least one StC list has entries and fields are present + all_stc = result['BStC'] + result['NStC'] + result['TStC'] + result['SStC'] + assert len(all_stc) > 0, 'Expected at least one StC file in test case' + stc = all_stc[0] + assert 'StC_DOF_MODE' in stc + assert 'StC_X_M' in stc + assert 'SpringForceTable' in stc + + +class TestServoDynWrite: + + @staticmethod + def _make_minimal_sd() -> dict: + """Construct a minimal ServoDyn data dict for writing.""" + sd = { + 'Echo': False, 'DT': 0.005, + 'PCMode': 0, 'TPCOn': 0.0, + } + for idx in range(1, 4): + sd[f'PitNeut({idx})'] = 0.0 + sd[f'PitSpr({idx})'] = 0.0 + sd[f'PitDamp({idx})'] = 0.0 + sd[f'TPitManS({idx})'] = 9999.9 + sd[f'PitManRat({idx})'] = 2.0 + sd[f'BlPitchF({idx})'] = 0.0 + sd.update({ + 'VSContrl': 5, 'GenModel': 1, 'GenEff': 94.4, + 'GenTiStr': True, 'GenTiStp': True, + 'SpdGenOn': 0.0, 'TimGenOn': 0.0, 'TimGenOf': 9999.9, + 'VS_RtGnSp': 0.0, 'VS_RtTq': 0.0, 'VS_Rgn2K': 0.0, 'VS_SlPc': 0.0, + 'SIG_SlPc': 0.0, 'SIG_SySp': 0.0, 'SIG_RtTq': 0.0, 'SIG_PORt': 0.0, + 'TEC_Freq': 0.0, 'TEC_NPol': 0, 'TEC_SRes': 0.0, 'TEC_RRes': 0.0, + 'TEC_VLL': 0.0, 'TEC_SLR': 0.0, 'TEC_RLR': 0.0, 'TEC_MR': 0.0, + 'HSSBrMode': 0, 'THSSBrDp': 0.0, 'HSSBrDT': 0.0, 'HSSBrTqF': 0.0, + 'YCMode': 0, 'TYCOn': 0.0, 'YawNeut': 0.0, 'YawSpr': 0.0, + 'YawDamp': 0.0, 'TYawManS': 0.0, 'YawManRat': 0.0, 'NacYawF': 0.0, + 'AfCmode': 0, 'AfC_Mean': 0.0, 'AfC_Amp': 0.0, 'AfC_Phase': 0.0, + 'NumBStC': 0, 'BStCfiles': [], + 'NumNStC': 0, 'NStCfiles': [], + 'NumTStC': 0, 'TStCfiles': [], + 'NumSStC': 0, 'SStCfiles': [], + 'CCmode': 0, + 'DLL_FileName': 'libdiscon.so', 'DLL_InFile': 'DISCON.IN', + 'DLL_ProcName': 'DISCON', 'DLL_DT': 'default', + 'DLL_Ramp': False, 'BPCutoff': 0.0, 'NacYaw_North': 0.0, + 'Ptch_Cntrl': 1, 'Ptch_SetPnt': 0.0, 'Ptch_Min': 0.0, + 'Ptch_Max': 90.0, 'PtchRate_Min': -8.0, 'PtchRate_Max': 8.0, + 'Gain_OM': 0.0, 'GenSpd_MinOM': 0.0, 'GenSpd_MaxOM': 0.0, + 'GenSpd_Dem': 0.0, 'GenTrq_Dem': 0.0, 'GenPwr_Dem': 0.0, + 'DLL_NumTrq': 0, 'GenSpd_TLU': [], 'GenTrq_TLU': [], + 'SumPrint': False, 'OutFile': 1, 'TabDelim': True, + 'OutFmt': 'ES10.3E2', 'TStart': 0.0, + }) + return {'ServoDyn': sd, 'BStC': [], 'NStC': [], 'TStC': [], 'SStC': []} + + def test_servodyn_write_reread_synthetic(self): + io = ServoDynIO() + data = self._make_minimal_sd() + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, 'ServoDyn.dat') + io.write(data, out) + result = io.read(out, base_dir=tmp) + sd_orig = data['ServoDyn'] + sd_read = result['ServoDyn'] + assert sd_read['PCMode'] == sd_orig['PCMode'] + assert sd_read['VSContrl'] == sd_orig['VSContrl'] + assert sd_read['GenEff'] == pytest.approx(sd_orig['GenEff']) diff --git a/openfast_io/openfast_io/tests/test_of_io_pytest.py b/openfast_io/openfast_io/tests/test_of_io_pytest.py index d6c4f8dac1..634e3aeb46 100644 --- a/openfast_io/openfast_io/tests/test_of_io_pytest.py +++ b/openfast_io/openfast_io/tests/test_of_io_pytest.py @@ -229,11 +229,10 @@ def test_rtest_cloned(request): REPOSITORY_ROOT = osp.join(request.config.getoption("--source_dir")) path_dict = getPaths(REPOSITORY_ROOT=REPOSITORY_ROOT) - if check_rtest_cloned(path_dict['test_data_dir']): - assert True, "R-tests cloned properly" - else:# stop the test if the r-tests are not cloned properly - print("R-tests not cloned properly") - sys.exit(1) + try: + check_rtest_cloned(path_dict['test_data_dir']) + except FileNotFoundError as e: + pytest.skip(f"r-test data not available: {e}") def test_DLLs_exist(request): """ @@ -247,11 +246,9 @@ def test_DLLs_exist(request): # Check if the DISCON.dll file exists DISCON_DLL = osp.join(path_dict['discon_dir'], "DISCON.dll") - if osp.exists(DISCON_DLL): - assert True, f"DISCON.dll found at {DISCON_DLL}" - else: # stop the test if the DISCON.dll is not found - print(f"DISCON.dll not found at {DISCON_DLL}. Please build with ''' make regression_test_controllers ''' and try again.") - sys.exit(1) + if not osp.exists(DISCON_DLL): + pytest.skip(f"DISCON.dll not found at {DISCON_DLL}. Please build with " + f"'make regression_test_controllers' and try again.") def test_openfast_executable_exists(request): """ @@ -263,11 +260,9 @@ def test_openfast_executable_exists(request): path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--executable"))) - if osp.exists(path_dict['executable']): - assert True, f"OpenFAST executable found at {path_dict['executable']}" - else: # stop the test if the OpenFAST executable is not found - print(f"OpenFAST executable not found at {path_dict['executable']}. Please build OpenFAST and try again.") - sys.exit(1) + if not osp.exists(path_dict['executable']): + pytest.skip(f"OpenFAST executable not found at {path_dict['executable']}. " + f"Please build OpenFAST and try again.") @@ -282,10 +277,16 @@ def test_openfast_io_read_write_run_readOut_verify(folder, request): request (fixture): pytest request """ - path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--executable")), - REPOSITORY_ROOT=osp.join(request.config.getoption("--source_dir")), + path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--executable")), + REPOSITORY_ROOT=osp.join(request.config.getoption("--source_dir")), BUILD_DIR=osp.join(request.config.getoption("--build_dir"))) + case_dir = osp.join(path_dict['test_data_dir'], folder) + if not osp.isdir(case_dir): + pytest.skip(f"r-test case directory not found: {case_dir}") + if not osp.exists(path_dict['executable']): + pytest.skip(f"OpenFAST executable not found at {path_dict['executable']}. " + f"Please build OpenFAST and try again.") try: action_name = "read" diff --git a/openfast_io/openfast_io/tests/test_outlist.py b/openfast_io/openfast_io/tests/test_outlist.py new file mode 100644 index 0000000000..67e5205917 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_outlist.py @@ -0,0 +1,88 @@ +import io +import pytest +from openfast_io.outlist import OutList + + +def test_enable_and_query(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1']) + ol.enable('AeroDyn', ['RtAeroCp']) + assert ol.enabled('ElastoDyn') == {'RotSpeed', 'BldPitch1'} + result = ol.all_enabled() + assert 'RtAeroCp' in result + assert 'BldPitch1' in result + assert 'RotSpeed' in result + + +def test_disable(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1', 'GenPwr']) + ol.disable('ElastoDyn', ['GenPwr']) + assert 'GenPwr' not in ol.enabled('ElastoDyn') + assert ol.is_enabled('RotSpeed') + assert not ol.is_enabled('GenPwr') + + +def test_enabled_by_module(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1']) + ol.enable('AeroDyn', ['RtAeroCp']) + by_mod = ol.enabled_by_module() + assert 'ElastoDyn' in by_mod + assert 'AeroDyn' in by_mod + assert by_mod['ElastoDyn'] == ['BldPitch1', 'RotSpeed'] + + +def test_roundtrip_fst_output(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1']) + legacy = ol.to_fst_output() + # Round-trip back + ol2 = OutList.from_fst_output(legacy) + assert ol2.enabled('ElastoDyn') == ol.enabled('ElastoDyn') + + +def test_validate_catches_unknown_channels(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'TotallyBogusChannel']) + unknown = ol.validate() + assert any('TotallyBogusChannel' in u for u in unknown) + assert not any('RotSpeed' in u for u in unknown) + + +def test_read_from_file(): + content = '"RotSpeed"\n"BldPitch1", "GenPwr"\nEND of OutList\n' + f = io.StringIO(content) + channels = OutList.read_from_file(f, 'ElastoDyn') + assert 'RotSpeed' in channels + assert 'BldPitch1' in channels + assert 'GenPwr' in channels + + +def test_write_to_file(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed', 'BldPitch1']) + buf = io.StringIO() + ol.write_to_file(buf, 'ElastoDyn') + text = buf.getvalue() + assert '"BldPitch1"' in text + assert '"RotSpeed"' in text + assert 'END' in text + + +def test_clear(): + ol = OutList() + ol.enable('ElastoDyn', ['RotSpeed']) + ol.enable('AeroDyn', ['RtAeroCp']) + ol.clear('ElastoDyn') + assert ol.enabled('ElastoDyn') == set() + assert ol.enabled('AeroDyn') == {'RtAeroCp'} + ol.clear() + assert ol.all_enabled() == [] + + +def test_is_enabled_checks_all_modules(): + ol = OutList() + ol.enable('AeroDyn', ['RtAeroCp']) + assert ol.is_enabled('RtAeroCp') + assert not ol.is_enabled('RotSpeed') diff --git a/openfast_io/openfast_io/tests/test_parsing.py b/openfast_io/openfast_io/tests/test_parsing.py new file mode 100644 index 0000000000..5b0c57667c --- /dev/null +++ b/openfast_io/openfast_io/tests/test_parsing.py @@ -0,0 +1,121 @@ +"""Tests for openfast_io.parsing — especially fmt_field boundary cases.""" +import pytest +from openfast_io.parsing import fmt_field, float_read, bool_read, int_read + + +class TestFmtField: + """Boundary tests for fmt_field — the formatter that prevents overflow.""" + + def test_short_string_pads_to_min_width(self): + result = fmt_field(3.14) + assert len(result) >= 28 + assert result.startswith('3.14') + + def test_long_value_extends_beyond_min_width(self): + # A value whose str repr exceeds 28 chars + long_val = -1.23456789012345678e-104 + result = fmt_field(long_val) + assert len(result) >= len(str(long_val)) + 2 + + def test_always_has_trailing_spaces(self): + """Must always have at least 2 trailing spaces (prevents field concatenation).""" + for val in [0, 1.0, -9999999999, 'default', 1e308, -1e-308, True, 'SomeFileName.dat']: + result = fmt_field(val) + assert result.endswith(' '), f'fmt_field({val!r}) lacks 2 trailing spaces: {result!r}' + + def test_integer_zero(self): + result = fmt_field(0) + assert result.strip() == '0' + assert len(result) >= 28 + + def test_large_positive_exponent(self): + result = fmt_field(1.23e+200) + assert '1.23e+200' in result or '1.23E+200' in result.upper() + assert result.endswith(' ') + + def test_large_negative_exponent(self): + result = fmt_field(-1.23e-200) + assert result.rstrip() != result # has trailing space + assert '-1.23e-200' in result or '-1.23E-200' in result.upper() + + def test_string_default(self): + result = fmt_field('default') + assert result.startswith('default') + assert len(result) >= 28 + + def test_boolean_values(self): + assert fmt_field(True).strip() == 'True' + assert fmt_field(False).strip() == 'False' + + def test_none_value(self): + result = fmt_field(None) + assert result.strip() == 'None' + + def test_custom_min_width(self): + result = fmt_field(42, min_width=10) + assert len(result) >= 10 + + def test_custom_min_width_smaller_than_value(self): + result = fmt_field('a_very_long_filename_here.dat', min_width=5) + # Should still have 2 trailing spaces + assert result.endswith(' ') + + def test_negative_float_overflow_width_11(self): + """The exact bug that affected AeroDyn blade tables: {:11} overflow.""" + val = -1.23456789e-04 + result = fmt_field(val, min_width=11) + assert len(result) >= len(str(val)) + 2 + + def test_stiffness_matrix_value(self): + """The exact bug that affected BeamDyn: {:14} overflow for large stiffness.""" + val = 1.8634530e+10 + result = fmt_field(val, min_width=14) + assert result.endswith(' ') + assert len(result) >= len(str(val)) + 2 + + +class TestFloatRead: + def test_normal_float(self): + assert float_read('3.14') == pytest.approx(3.14) + + def test_scientific_notation(self): + assert float_read('1.5e-3') == pytest.approx(0.0015) + + def test_default_string(self): + assert float_read('default') == 'default' + assert float_read('DEFAULT') == 'DEFAULT' + + def test_trailing_comma(self): + """The SubDyn GuyanDamp bug: trailing comma.""" + assert float_read('0.354293E+00') == pytest.approx(0.354293) + + def test_non_numeric(self): + result = float_read('abc') + assert result == 'abc' + + +class TestBoolRead: + def test_true_variants(self): + assert bool_read('True') is True + assert bool_read('true') is True + assert bool_read('T') is True + assert bool_read('t') is True + + def test_false_variants(self): + assert bool_read('False') is False + assert bool_read('false') is False + assert bool_read('F') is False + + def test_default(self): + assert bool_read('default') == 'default' + + +class TestIntRead: + def test_normal_int(self): + assert int_read('42') == 42 + + def test_default_string(self): + assert int_read('default') == 'default' + + def test_non_numeric(self): + assert int_read('abc') == 'abc' diff --git a/openfast_io/openfast_io/tests/test_schema.py b/openfast_io/openfast_io/tests/test_schema.py new file mode 100644 index 0000000000..fe14f2b023 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_schema.py @@ -0,0 +1,54 @@ +from openfast_io.schema import get_schema, get_param_info, FILE_REF_PARAMS + + +def test_get_schema_returns_dict(): + s = get_schema('ElastoDyn', '5.0.0') + assert isinstance(s, dict) + assert len(s) > 10 + + +def test_known_param_has_required_fields(): + s = get_schema('ElastoDyn', '5.0.0') + p = s['FlapDOF1'] + assert 'type' in p + assert 'desc' in p + assert p['type'] == bool + + +def test_get_param_info_returns_description(): + info = get_param_info('ElastoDyn', 'FlapDOF1', '5.0.0') + assert 'desc' in info + assert 'flapwise' in info['desc'].lower() + + +def test_file_ref_params_marks_bldfile(): + s = get_schema('ElastoDyn', '5.0.0') + assert s.get('BldFile', {}).get('is_file_ref') is True or \ + s.get('TwrFile', {}).get('is_file_ref') is True + + +def test_removed_param_not_in_v5_schema(): + s = get_schema('AeroDyn', '5.0.0') + assert 'Buoyancy' not in s + + +def test_removed_param_in_v4_schema(): + s = get_schema('AeroDyn', '4.0.0') + assert 'Buoyancy' in s + + +def test_file_ref_params_dict_has_fst_key(): + assert 'Fst' in FILE_REF_PARAMS + assert 'EDFile' in FILE_REF_PARAMS['Fst'] + + +def test_fst_schema_has_tmax(): + s = get_schema('Fst', '5.0.0') + assert 'TMax' in s + assert s['TMax']['type'] == float + assert s['TMax']['units'] == 's' + + +def test_get_param_info_unknown_param(): + info = get_param_info('ElastoDyn', 'NonExistentParam', '5.0.0') + assert info == {} diff --git a/openfast_io/openfast_io/tests/test_validation.py b/openfast_io/openfast_io/tests/test_validation.py new file mode 100644 index 0000000000..c582828409 --- /dev/null +++ b/openfast_io/openfast_io/tests/test_validation.py @@ -0,0 +1,97 @@ +import pytest +from openfast_io.validation import validate_fst_vt, ValidationIssue + + +def test_valid_deck_returns_no_errors(): + fst_vt = { + 'Fst': {'CompElast': 1, 'CompAero': 2, 'CompServo': 0, + 'CompHydro': 0, 'NRotors': 1}, + 'ElastoDyn': {'NumBl': 3}, + 'AeroDynBlade': [{}, {}, {}], + 'outlist': {}, + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=False) + errors = [i for i in issues if i.severity == 'ERROR'] + assert len(errors) == 0 + + +def test_blade_count_mismatch_is_error(): + fst_vt = { + 'Fst': {'CompElast': 1, 'CompAero': 2, 'CompServo': 0, + 'CompHydro': 0, 'NRotors': 1}, + 'ElastoDyn': {'NumBl': 3}, + 'AeroDynBlade': [{'data': True}, {'data': True}], # only 2 for a 3-blade turbine + 'outlist': {}, + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=False) + errors = [i for i in issues if i.severity == 'ERROR'] + assert any('blade' in i.message.lower() for i in errors) + + +def test_removed_param_produces_warning(): + fst_vt = { + 'Fst': {'CompElast': 1, 'CompAero': 2, 'NRotors': 1}, + 'AeroDyn': {'Buoyancy': True}, + 'ElastoDyn': {'NumBl': 3}, + 'AeroDynBlade': [{}, {}, {}], + 'outlist': {}, + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=False) + warnings = [i for i in issues if i.severity == 'WARNING'] + assert any('Buoyancy' in i.message for i in warnings) + + +def test_hydrodyn_info_when_comp_zero(): + fst_vt = { + 'Fst': {'CompHydro': 0, 'CompServo': 0}, + 'HydroDyn': {'WaveMod': 1}, + 'ElastoDyn': {}, + 'AeroDynBlade': [], + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=False) + infos = [i for i in issues if i.severity == 'INFO'] + assert any('HydroDyn' in i.message for i in infos) + + +def test_validation_issue_dataclass(): + issue = ValidationIssue(severity='ERROR', modules=['Fst'], parameter='TMax', + message='TMax must be positive') + assert issue.severity == 'ERROR' + assert issue.modules == ['Fst'] + + +def test_check_files_with_base_dir_existing_ref_no_issue(tmp_path): + """A file reference stored relative to the case dir (base_dir) should + resolve cleanly when it actually exists there, even though it does not + exist relative to the current working directory.""" + blade_file = tmp_path / "blade1.dat" + blade_file.write_text("dummy blade file\n") + + fst_vt = { + 'Fst': {'CompElast': 1, 'CompAero': 2, 'CompServo': 0, + 'CompHydro': 0, 'NRotors': 1}, + 'ElastoDyn': {'NumBl': 3, 'BldFile1': 'blade1.dat'}, + 'AeroDynBlade': [{}, {}, {}], + 'outlist': {}, + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=True, + base_dir=tmp_path) + errors = [i for i in issues if i.severity == 'ERROR'] + assert not any('BldFile1' in (i.parameter or '') for i in errors) + + +def test_check_files_with_base_dir_missing_ref_is_error(tmp_path): + """A file reference that does not exist under base_dir should still be + flagged as an ERROR.""" + fst_vt = { + 'Fst': {'CompElast': 1, 'CompAero': 2, 'CompServo': 0, + 'CompHydro': 0, 'NRotors': 1}, + 'ElastoDyn': {'NumBl': 3, 'BldFile1': 'does_not_exist.dat'}, + 'AeroDynBlade': [{}, {}, {}], + 'outlist': {}, + } + issues = validate_fst_vt(fst_vt, version='5.0.0', check_files=True, + base_dir=tmp_path) + errors = [i for i in issues if i.severity == 'ERROR'] + assert any('BldFile1' in (i.parameter or '') for i in errors) + assert any('does_not_exist.dat' in i.message for i in errors) diff --git a/openfast_io/openfast_io/tools/check_registry_drift.py b/openfast_io/openfast_io/tools/check_registry_drift.py new file mode 100644 index 0000000000..83afb03561 --- /dev/null +++ b/openfast_io/openfast_io/tools/check_registry_drift.py @@ -0,0 +1,149 @@ +""" +Developer tool: compare Fortran Registry files against openfast_io reader coverage. + +Run after modifying any *_Registry.txt file to find parameters added, removed, or renamed. + +Usage: + python -m openfast_io.tools.check_registry_drift --openfast-root /path/to/openfast [--version 5.0.0] +""" +import re +import argparse +from dataclasses import dataclass +from pathlib import Path + + +REGISTRY_MAP = { + 'ElastoDyn': ('modules/elastodyn/src/ElastoDyn_Registry.txt', 'ED_InputFile'), + 'AeroDyn': ('modules/aerodyn/src/AeroDyn_Registry.txt', 'AD_InputFile'), + 'InflowWind': ('modules/inflowwind/src/InflowWind_Registry.txt', 'InflowWind_InputFile'), + 'ServoDyn': ('modules/servodyn/src/ServoDyn_Registry.txt', 'SrvD_InputFile'), + 'HydroDyn': ('modules/hydrodyn/src/HydroDyn_Registry.txt', 'HD_InputFile'), + 'SubDyn': ('modules/subdyn/src/SubDyn_Registry.txt', 'SD_InputFile'), + 'BeamDyn': ('modules/beamdyn/src/BeamDyn_Registry.txt', 'BD_InputFile'), + 'MoorDyn': ('modules/moordyn/src/MoorDyn_Registry.txt', 'MD_InputFile'), + 'SeaState': ('modules/seastate/src/SeaState_Registry.txt', 'SeaSt_InputFile'), +} + + +@dataclass +class RegistryParam: + name: str + fortran_type: str + dims: str + description: str + units: str | None + + +def parse_registry(registry_path: Path, type_name: str) -> list[RegistryParam]: + """Extract all fields from a specific typedef block in a registry file.""" + params = [] + current_module = None + with open(registry_path, errors='replace') as f: + for line in f: + line = line.rstrip() + if not line or line.lstrip().startswith(('#', '!')): + continue + parts = re.split(r'\t+', line) + if len(parts) < 6 or parts[0].strip() != 'typedef': + continue + module = parts[1].strip() + if module == '^': + module = current_module + else: + current_module = module + if parts[2].strip() != type_name: + continue + desc = parts[8].strip().strip('"') if len(parts) > 8 else '' + units = parts[9].strip() if len(parts) > 9 else None + params.append(RegistryParam( + name=parts[4].strip(), + fortran_type=parts[3].strip(), + dims=parts[5].strip(), + description=desc, + units=units if units and units != '-' else None, + )) + return params + + +def scan_reader_params(reader_path: Path, module_key: str) -> set[str]: + """Scan FAST_reader.py for fst_vt['ModuleKey']['ParamName'] assignments.""" + source = reader_path.read_text(errors='replace') + pattern = r"""fst_vt\[['"]""" + re.escape(module_key) + r"""['"]\]\s*\[['"](\w+)['"]\]""" + return set(re.findall(pattern, source)) + + +def scan_schema_params(module_key: str) -> set[str]: + """Get parameter names currently defined in schema.py for this module.""" + try: + from openfast_io.schema import get_schema + return set(get_schema(module_key).keys()) + except ImportError: + return set() + + +def check_drift(openfast_root: Path, version: str = '5.0.0'): + reader_path = openfast_root / 'openfast_io/openfast_io/FAST_reader.py' + if not reader_path.exists(): + print(f"ERROR: FAST_reader.py not found at {reader_path}") + return + + total_missing_reader = 0 + total_missing_schema = 0 + total_stale = 0 + + for module, (reg_rel, type_name) in REGISTRY_MAP.items(): + reg_path = openfast_root / reg_rel + if not reg_path.exists(): + print(f"\n{module}: ✗ registry not found ({reg_path})") + continue + + reg_params = parse_registry(reg_path, type_name) + reg_names = {p.name for p in reg_params} + reader_names = scan_reader_params(reader_path, module) + schema_names = scan_schema_params(module) + + missing_reader = reg_names - reader_names + missing_schema = reg_names - schema_names + stale = reader_names - reg_names + + total_missing_reader += len(missing_reader) + total_missing_schema += len(missing_schema) + total_stale += len(stale) + + status = '✓' if not missing_reader and not stale else '⚠' + print(f"\n{module} ({type_name}: {len(reg_names)} params in registry) {status}") + + if not missing_reader and not missing_schema and not stale: + print(" ✓ Fully covered") + continue + if not reader_names: + print(" ✗ NO READER — entire module unimplemented in openfast_io") + elif missing_reader: + print(f" ⚠ READER MISSING ({len(missing_reader)}):") + for n in sorted(missing_reader)[:20]: + p = next((x for x in reg_params if x.name == n), None) + desc = f' [{p.fortran_type}] "{p.description}"' if p else '' + print(f" - {n}{desc}") + if len(missing_reader) > 20: + print(f" ... and {len(missing_reader) - 20} more") + if missing_schema: + print(f" ⚠ SCHEMA MISSING ({len(missing_schema)})") + if stale: + print(f" ✗ STALE IN READER ({len(stale)}) — may have been removed/renamed:") + for n in sorted(stale)[:10]: + print(f" - {n}") + if len(stale) > 10: + print(f" ... and {len(stale) - 10} more") + + print(f"\n{'─'*60}") + print(f"Summary: {total_missing_reader} reader gaps, " + f"{total_missing_schema} schema gaps, " + f"{total_stale} stale entries") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Check openfast_io reader vs. registry drift') + parser.add_argument('--openfast-root', required=True, type=Path) + parser.add_argument('--version', default='5.0.0') + args = parser.parse_args() + check_drift(args.openfast_root, args.version) diff --git a/openfast_io/openfast_io/validation.py b/openfast_io/openfast_io/validation.py new file mode 100644 index 0000000000..d217f2f5f6 --- /dev/null +++ b/openfast_io/openfast_io/validation.py @@ -0,0 +1,134 @@ +"""Validation layer for fst_vt dicts. + +Performs schema-based checks, cross-module consistency checks, and +file-reference existence checks. +""" +import os +from dataclasses import dataclass, field +from pathlib import Path +from .schema import get_schema, FILE_REF_PARAMS + + +@dataclass +class ValidationIssue: + severity: str # 'ERROR' | 'WARNING' | 'INFO' + modules: list[str] = field(default_factory=list) + parameter: str | None = None + message: str = '' + source_file: str | None = None + source_line: int | None = None + + +def validate_fst_vt(fst_vt: dict, version: str = '5.0.0', + check_files: bool = True, + base_dir: 'Path | str | None' = None) -> list[ValidationIssue]: + """Validate a loaded fst_vt. Returns list of issues ordered by severity. + + Args: + fst_vt: the loaded variable tree to validate. + version: target OpenFAST version, used for removed-parameter checks. + check_files: if True, verify that file-reference parameters point to + existing files. + base_dir: directory that relative file-reference paths in ``fst_vt`` + are resolved against (fst_vt stores paths relative to the case + directory, not the current working directory). If ``None``, + falls back to resolving against the current working directory. + """ + issues = [] + issues.extend(_check_removed_params(fst_vt, version)) + issues.extend(_check_cross_module(fst_vt)) + if check_files: + issues.extend(_check_file_refs(fst_vt, base_dir=base_dir)) + severity_order = {'ERROR': 0, 'WARNING': 1, 'INFO': 2} + return sorted(issues, key=lambda i: severity_order.get(i.severity, 3)) + + +def _check_removed_params(fst_vt: dict, version: str) -> list[ValidationIssue]: + """Warn if fst_vt contains parameters removed in the target version.""" + issues = [] + from .schema import _SCHEMA + if version == '5.0.0': + removed_in_v5 = _SCHEMA.get('4.0.0', {}) + for module, params in removed_in_v5.items(): + present = fst_vt.get(module, {}) + if isinstance(present, dict): + for param in params: + if param in present: + issues.append(ValidationIssue( + severity='WARNING', modules=[module], parameter=param, + message=f"{module}.{param} was removed in v5.0.0 and will be ignored. " + f"See schema for migration guidance." + )) + return issues + + +def _check_cross_module(fst_vt: dict) -> list[ValidationIssue]: + """Physics-level cross-module consistency checks.""" + issues = [] + fst = fst_vt.get('Fst', {}) + ed = fst_vt.get('ElastoDyn', {}) + + # Blade count consistency + n_blades_ed = ed.get('NumBl', 3) + ad_blades = fst_vt.get('AeroDynBlade', []) + if isinstance(ad_blades, list) and ad_blades and len(ad_blades) != n_blades_ed: + # Only flag if AeroDynBlade is non-empty and list (i.e., AeroDyn was read) + has_data = any(bool(b) for b in ad_blades) + if has_data: + issues.append(ValidationIssue( + severity='ERROR', modules=['ElastoDyn', 'AeroDyn'], + parameter='NumBl', + message=f"Blade count mismatch: ElastoDyn.NumBl={n_blades_ed} " + f"but AeroDynBlade has {len(ad_blades)} entries." + )) + + # ServoDyn DLL not set when CompServo=1 + if fst.get('CompServo', 0) == 1: + dll = fst_vt.get('ServoDyn', {}).get('DLL_FileName', '') + if not dll: + issues.append(ValidationIssue( + severity='WARNING', modules=['Fst', 'ServoDyn'], + parameter='DLL_FileName', + message="CompServo=1 but ServoDyn.DLL_FileName is not set." + )) + + # HydroDyn data present but disabled + if fst.get('CompHydro', 0) == 0 and fst_vt.get('HydroDyn'): + issues.append(ValidationIssue( + severity='INFO', modules=['Fst', 'HydroDyn'], parameter=None, + message="HydroDyn data loaded but CompHydro=0 — it will be ignored at runtime." + )) + + return issues + + +def _check_file_refs(fst_vt: dict, base_dir: 'Path | str | None' = None) -> list[ValidationIssue]: + """Check that file-reference parameters point to existing files. + + Relative paths are resolved against ``base_dir`` when given (fst_vt + stores paths relative to the case directory). When ``base_dir`` is + ``None``, relative paths are resolved against the current working + directory instead, matching the historical behavior. + """ + issues = [] + for module, param_names in FILE_REF_PARAMS.items(): + data = fst_vt.get(module, {}) + if not isinstance(data, dict): + continue + for param in param_names: + val = data.get(param, '') + if not val: + continue + paths = val if isinstance(val, list) else [val] + for p in paths: + p_str = str(p).strip('"').strip("'") + if p_str and p_str.lower() not in ('unused', 'default', ''): + candidate = Path(p_str) + if base_dir is not None and not candidate.is_absolute(): + candidate = Path(os.path.normpath(Path(base_dir) / candidate)) + if not candidate.exists(): + issues.append(ValidationIssue( + severity='ERROR', modules=[module], parameter=param, + message=f"{module}.{param} references a file that does not exist: {p_str}" + )) + return issues diff --git a/openfast_io/pyproject.toml b/openfast_io/pyproject.toml index e5c1aaba7a..90f165038a 100644 --- a/openfast_io/pyproject.toml +++ b/openfast_io/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "numpy>1.0", "pandas>2.0", "ruamel_yaml>0.18", + "pyyaml>6.0", "deepdiff>8.0", ]