diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c8b3c..c0696ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ All notable changes to CableDyn are recorded in this file. The format follows ## [Unreleased] +### Added + +- Torsion of finite-EI lines in the standalone driver (condensed, quasi-static, uniform torque): + `END CONNECTIONS` columns `TorsStiffness NxX NxY NxZ [Pretwist]`, a `motionFile` roll column, + the torque returned to Rigid6 bodies, channels `TorqN`, `TwistN` and `Twist`, + range-graph columns, `DeckModel` and channel support in Python, and the example + `torsion_lazy_wave_hangoff_twist.dat`. Validated in VALIDATION.md (Torsion). Routes outside + its scope, coupled OpenFAST (NLR) runs included, stop with a named error; results without + torsion are unchanged. + ### Fixed - The static solve of a taut, neutrally buoyant finite-EI line (mass per length equal to diff --git a/CMakeLists.txt b/CMakeLists.txt index ca4e0cf..ac8b809 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -323,6 +323,7 @@ add_library(cabledyn_obj OBJECT src/CableDyn_EndConnection.f90 src/CableDyn_Vessel.f90 src/CableDyn_HermiteCable.f90 + src/CableDyn_HermiteTorsion.f90 src/CableDyn_HermiteCableStatic.f90 src/CableDyn_HermiteCableDynamic.f90 src/CableDyn_Conventions.f90 @@ -562,6 +563,28 @@ target_link_libraries(test_hermite_cable PRIVATE cabledyn_core) add_test(NAME hermite_cable COMMAND test_hermite_cable) set_tests_properties(hermite_cable PROPERTIES LABELS "fortran;unit;static") +add_executable(test_hermite_torsion_kernel tests/test_hermite_torsion_kernel.f90) +target_link_libraries(test_hermite_torsion_kernel PRIVATE cabledyn_core) +add_test(NAME hermite_torsion_kernel + COMMAND test_hermite_torsion_kernel ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/torsion_oracle_reference.txt) +set_tests_properties(hermite_torsion_kernel PROPERTIES LABELS "fortran;unit;static") + +add_executable(test_hermite_torsion_static tests/test_hermite_torsion_static.f90) +target_link_libraries(test_hermite_torsion_static PRIVATE cabledyn_core) +add_test(NAME hermite_torsion_static + COMMAND test_hermite_torsion_static ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/torsion_analytic_refs.txt) +set_tests_properties(hermite_torsion_static PROPERTIES LABELS "fortran;unit;static;validation") + +add_executable(test_torsion_validation tests/test_torsion_validation.f90) +target_link_libraries(test_torsion_validation PRIVATE cabledyn_core) +add_test(NAME torsion_validation + COMMAND test_torsion_validation ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/torsion_analytic_refs.txt) +set_tests_properties(torsion_validation PROPERTIES LABELS "fortran;unit;static;validation") +add_executable(test_hermite_torsion_dynamic tests/test_hermite_torsion_dynamic.f90) +target_link_libraries(test_hermite_torsion_dynamic PRIVATE cabledyn_core) +add_test(NAME hermite_torsion_dynamic COMMAND test_hermite_torsion_dynamic) +set_tests_properties(hermite_torsion_dynamic PROPERTIES LABELS "fortran;unit;dynamic;validation") + add_executable(test_hermite_cable_static tests/test_hermite_cable_static.f90) target_link_libraries(test_hermite_cable_static PRIVATE cabledyn_core) add_test(NAME hermite_cable_static COMMAND test_hermite_cable_static) @@ -1180,6 +1203,9 @@ if(CMAKE_Fortran_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_ID STREQUAL "GN COMMAND test_step_allocations ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/range_mixed_alloc.dat 200.0 200 ranges) set_tests_properties(step_allocations_ranges PROPERTIES LABELS "fortran;openfast;dynamic;memory") + # The same gate on a finite-EI cable with condensed torsion (a turning parent and a changing twist). + add_test(NAME step_allocations_torsion COMMAND test_step_allocations torsion) + set_tests_properties(step_allocations_torsion PROPERTIES LABELS "fortran;dynamic;memory") endif() add_executable(test_snap_load tests/test_snap_load.f90) @@ -1650,6 +1676,17 @@ target_link_libraries(test_clamped_cable_ends PRIVATE cabledyn_core) add_test(NAME clamped_cable_ends COMMAND test_clamped_cable_ends) set_tests_properties(clamped_cable_ends PROPERTIES LABELS "fortran;driver;integration;static;dynamic") +add_executable(test_torsion_deck tests/test_torsion_deck.f90) +target_link_libraries(test_torsion_deck PRIVATE cabledyn_core) +add_test(NAME torsion_deck COMMAND test_torsion_deck) +set_tests_properties(torsion_deck PROPERTIES LABELS "fortran;driver;integration;static;dynamic") + +add_executable(test_torsion_orcaflex tests/test_torsion_orcaflex.f90) +target_link_libraries(test_torsion_orcaflex PRIVATE cabledyn_core) +add_test(NAME torsion_orcaflex + COMMAND test_torsion_orcaflex ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/torsion_orcaflex_refs.txt) +set_tests_properties(torsion_orcaflex PROPERTIES LABELS "fortran;driver;integration;static;validation;l3") + add_executable(test_rod_hydro tests/test_rod_hydro.f90) target_link_libraries(test_rod_hydro PRIVATE cabledyn_core) add_test(NAME rod_hydro COMMAND test_rod_hydro) @@ -1795,6 +1832,16 @@ foreach(_lozon_case IN ITEMS LABELS "fortran;driver;integration;example;lozon") endforeach() +# Torsion example: a lazy-wave cable twisted two turns at the hang-off through the motionFile +# roll column (condensed torsion, quasi-static). +add_test( + NAME example_torsion_lazy_wave_hangoff_twist + COMMAND cabledyn + "${CMAKE_CURRENT_SOURCE_DIR}/examples/torsion_lazy_wave_hangoff_twist.dat" + "${CMAKE_CURRENT_BINARY_DIR}/example_torsion_lazy_wave_hangoff_twist") +set_tests_properties(example_torsion_lazy_wave_hangoff_twist PROPERTIES + LABELS "fortran;driver;integration;example;dynamic") + # The teaching/reference deck enumerates all active defaults and documents every # mutually exclusive alternative. Keep it runnable so the canonical OPTIONS # example cannot drift away from the production parser. @@ -1919,6 +1966,7 @@ if(CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") -Wno-compare-reals -Wno-uninitialized -Wno-maybe-uninitialized) + target_compile_options(test_torsion_deck PRIVATE -Wno-uninitialized -Wno-maybe-uninitialized) endif() if(CABLEDYN_ENABLE_COVERAGE) @@ -1957,6 +2005,9 @@ if(CABLEDYN_ENABLE_COVERAGE) test_robust_init test_line test_hermite_cable + test_hermite_torsion_kernel + test_hermite_torsion_static + test_hermite_torsion_dynamic test_hermite_cable_static test_hermite_sequenced test_hermite_resolution diff --git a/VALIDATION.md b/VALIDATION.md index 12a0910..8087502 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -88,6 +88,16 @@ article's methods, are compared in | **L5-wc** | Wall-clock vs explicit C solver | external-C RK2 (WD chain) | L2 0.967 s vs external-C 0.541 s; traces validated | matched comparison reported | Pass measured | | **L5-DPC** | Pinned-TDP lazy-wave spans, 80/200/800 m | OrcaFlex / refined MoorDyn-F | CableDyn 1.55/4.64/7.37 s; OrcaFlex 2.29/4.56/23.17 s; MoorDyn-F 249/145/243 s | resolved meshes, same host, one thread | Pass measured | | **L5-pk** | Snap-load peak, 3-code | MoorDyn-C + OrcaFlex | 4.86 / 6.32 / 10.22 MN | — | Informative | +| **T-1** | Torsion kernel: twist of the centreline, gradient, Hessian | automatic differentiation; finite differences | ≤3.8e-15; ≤5.3e-11 | <1e-10; <1e-6 | Pass | +| **T-2** | Pure torsion (multi-section, end springs, five turns) | `Φ/C`; OrcaFlex 11.6d | round-off; ≤1.7e-8 | <1e-12; <1e-7 | Pass | +| **T-3** | Greenhill onset, clamped, `TL²/EI` −20 to 1600 | van der Heijden et al. (2003) eq. 33 | ≤3.9e-5 (32 elements); ≤6.5e-6 (128) | <1e-4; <1e-3 | Pass | +| **T-4** | Onset, bending-pinned torsional (semi-tangential) end | `tan x = −x/3` | 6.4e-7 / 4.5e-7 | <1e-4 | Pass | +| **T-5** | Onsets against OrcaFlex 11.6d (clamped and pinned) | OrcaFlex, extrapolated to zero segment length | ≤3.0e-5 | <2e-4 | Pass | +| **T-6** | Lazy-wave cable twisted 0–5 turns at the hang-off | OrcaFlex 11.6d | torque ≤0.019%, peak curvature ≤0.056%, offset ≤27 mm, tension ≤0.091% | 1% / 1% / 0.1 m / 0.5% | Pass | +| **T-7** | Kirchhoff helix, four turns | helical equilibrium | end force 1.5e-4, end moment 6.5e-5 | <5e-3 | Pass | +| **T-8** | Cross-check with the Cosserat rod path | Cosserat path | shape 4.2e-4 L, torque 5.2e-5 | <1% | Pass | +| **T-9** | Bordered Newton step on post-buckled states | predicted linear rate without the rank-one term | 3 iterations; rate 0.055 / 0.217 vs 0.055 / 0.218 | ≤4; within 25% | Pass | +| **T-10** | Torsion dynamics, restart and body torque | quasi-static torque, energy, uninterrupted run, pendulum | torque 2.2e-16; energy band 4.9e-4; restart bit-identical; pendulum 6.4e-5 | see [Torsion](#torsion) | Pass | **Pass** meets the stated acceptance value. **Pass measured** records a measured comparison without an acceptance value. **Informative** is reported but not used for @@ -776,6 +786,108 @@ self-driven WaveKinMod-1 waves with WaterKin CurrentMod-2 host current, and a ho SeaState using its own user-current CurrMod-2, which cannot be split into wave and current parts. +## Torsion + +Condensed isotropic torsion of finite-EI lines (theory in the +[manual](https://cabledyn.readthedocs.io/en/latest/theory.html#condensed-torsion)) is checked +against closed forms, against the Cosserat rod path, and against OrcaFlex 11.6d. The OrcaFlex +values are summary values in `tests/data/torsion_orcaflex_refs.txt`, written by +[`validation/scripts/orcaflex_torsion_reference.py`](https://github.com/SMI-Lab-Inha/CableDyn/blob/main/validation/scripts/orcaflex_torsion_reference.py) +(OrcFxAPI 11.6d, line torsion included, statics tolerance 1e-9 for pure torsion and 1e-8 +otherwise, `TensionTorqueCoupling` 0, no seabed friction; about 5 min). Torque is positive for a +right-handed twist of End B relative to End A, the OrcaFlex sign; OrcaFlex reports twist as a +rate in deg/m and end twisting stiffness in kN·m/deg. Every case below is a CTest test (names +in parentheses). + +- **T-1 twist kernel** (`hermite_torsion_kernel`). The geometric twist `Θ` of the centreline, + its gradient and Hessian, with clamped and articulated ends, match automatic differentiation of + an independent implementation on random large-deformation lines (Θ ≤4.4e-16, gradient + ≤9.2e-16, Hessian ≤3.8e-15, relative) and finite differences (Hessian 5.3e-11). `Θ` is + invariant under rigid rotation (2.2e-15), zero for planar curves, and equals `−τℓ` on a helix + (9.0e-7 at 48 elements, fourth order); unwrapping is continuous through three turns; the + `π/2` step limit and the fold guard stop by name. +- **T-2 pure torsion** (`hermite_torsion_static`, `torsion_deck`, `torsion_orcaflex`). A straight + clamped rod with alternating `GJ` and end springs carries `Φ/C` to round-off with no lateral + drift; through the deck, `Torq`, `TwistN` and `Twist` follow `GJ Φ/L` and the + end-spring compliance. Against OrcaFlex (90°, 450°, 1800° and −90°, `γ_A` 30° / `γ_B` 120°, two + sections in series) the deck torque agrees to the 8 digits of the output. OrcaFlex's line in + this case has `EI` 1 kN·m², so every case but ±90° lies above its buckling onset; OrcaFlex + statics stays on the straight branch there, while CableDyn's stability check leaves it for the + buckled shape. The deck therefore uses `EI` 1e3 kN·m²; the straight-branch torque does not + depend on `EI`. +- **T-3 Greenhill onset** (`hermite_torsion_static`, `torsion_validation`). The critical torque + of a straight clamped rod under the dead tension `T`, bisected on the static stability report, + against eq. 33 of van der Heijden et al. (2003): at `TL²/EI` = −20, 0, 10, 50, 200 the 32-element + error is 1.8e-5, 9.0e-6, 7.5e-6, 6.8e-6 and 3.9e-5 with an observed order of 3.7 to 3.9; at + 400 and 1600, where the onset approaches the infinite-rod value `2√(EI T)` (ratio 1.0124 and + 1.0031), the 128-element error is 3.8e-7 and 6.5e-6. Above the onset the straight state is + reported unstable and the static solve descends to a stable buckled state of lower energy and + relaxed torque. Dynamic loop formation and self-contact are not resolved (see Limits below). +- **T-4 semi-tangential end** (`hermite_torsion_static`). A bending-pinned end restrained in + torsion carries the torque about the bisector of the connection direction and the tangent. + The onset is the root of `tan x = −x/3` (4.9112877 EI/L at `T = 0`, 7.2692742 at + `TL²/EI = 10`), reproduced to 6.4e-7 and 4.5e-7; it is not Greenhill's `2π EI/L` for an + axial-torque hinge. +- **T-5 onsets against OrcaFlex** (`torsion_orcaflex`). OrcaFlex detects the onset by + bisection on its modal stability flag and converges at second order in the segment length; + extrapolated, it reproduces eq. 33 to 3e-8–9e-6. CableDyn at 32 elements differs from the + extrapolated OrcaFlex value by 9.1e-6, 7.4e-6, 5.0e-6 and 3.0e-5 (clamped, `TL²/EI` 0, 10, 50, + 200) and 4.9e-6 and 6.8e-7 (OrcaFlex's bending-free end with a twisting spring, `TL²/EI` 0 and + 10), which confirms that both codes transmit torque semi-tangentially at such an end. +- **T-6 lazy-wave cable** (`torsion_orcaflex`). The Lozon et al. (2025) Gulf of Mexico 80 m + cable with an illustrative `GJ` of 50 kN·m² is clamped at both ends in its untwisted static + orientation and twisted 1 to 5 turns at the hang-off. Against OrcaFlex at 0.375 m segments, + the hang-off torque agrees to ≤0.019%, the geometric twist `Θ` to ≤1.8e-4 turn per imposed turn + (`Θ` = 0.188 turn at 5 turns, so the torque is 3.8% below `GJ Φ/L`), the peak interior + curvature to ≤0.056% (0.1197 against 0.1196 1/m at 5 turns), the out-of-plane offset to ≤27 mm + of 6.8 m and the hang-off tension to ≤0.091%. The torque is uniform along the line. At zero + twist the restrained deck reproduces the same cable without torsion columns (positions + identical, tensions to 1.1e-8), the zero-twist parity case. +- **T-7 Kirchhoff helix** (`torsion_validation`). A rod clamped on four turns of a helix (radius + 1, pitch angle 30°, `EI` 1, 96 elements) with `Φ = Θ_helix + M C`, `M` = 0.5, stays on the helix + (radius drift 2.4e-4 of the radius), and the end reaction, taken as the derivative of the + discrete energy, is the helical wrench `F` = 0.058013 along the axis and `K` = 0.899519 about it + to 1.5e-4 and 6.5e-5. A three-dimensional sagging line restrained with `Φ` equal to its own + untwisted `Θ` (0.61 rad) reproduces the solve without torsion to solver tolerance. +- **T-8 Cosserat cross-check** (`torsion_validation`). The same rod, solved by the cubic-Hermite + element with condensed torsion (16 elements) and by the secondary Cosserat rod path (128 + elements with shear, a different formulation with nodal rotations), clamped with its far end + displaced and turned by `exp([0.25, −0.3, 1.2])`: the shapes agree to 4.2e-4 of the length and + the torques to 5.2e-5. Pure torsion agrees to round-off. The Cosserat path has no stability + report, so Greenhill onsets are not cross-checked on it. +- **T-9 bordered Newton step** (`torsion_validation`). On a rod buckled out of plane at 1.05 and + 1.20 times its clamped onset (`GJ = 2.5 EI`, the reference cable's ratio), Newton with the + bordered tangent `B + ∇Θ∇Θᵀ/C` (Sherman–Morrison on the band factor) returns from a 1e-6 + perturbation in 3 iterations. Without the rank-one term the iteration converges only linearly, + at the rate `ρ = ∇Θᵀ B⁻¹ ∇Θ / C` predicted for it: ρ = 0.055 and 0.218, observed 0.055 and + 0.217 (5 and 9 iterations). On these stable states `B` itself remains positive definite. +- **T-10 dynamics, restart and bodies** (`hermite_torsion_dynamic`, `torsion_deck`). An imposed + twist step gives `GJ Φ/L` from the first step (2.2e-16): torsion has no inertia. A parent turned + through 1.5 turns carries the twist without a `2π` slip. A bowed, twisted line in free vibration + (`ρ∞ = 1`, no drag) keeps its total energy, torsional part included, within a band of 4.9e-4 + of the vibration energy over ten periods, while the torsional share swings by 22%. A run + restored from a snapshot or rebuilt from the checkpoint mirror after more than one turn + continues bit-identically. A Rigid6 body on a twisted line receives the torque along the line + axis (3.6e-12), its net wrench equals the line loads, it turns against the torque in statics + (net moment ≤1e-6 N·m), and released with a roll rate it swings as a torsional pendulum at + `√(GJ/(L I))` to 6.4e-5 of the torque amplitude, with clamped and with bending-pinned + torsional ends. Stepped coarsely from rest, at `Ω Δt` = 2 and 5, the pendulum follows the closed + form of the march's trapezoidal body integrator at every step (≤9.7e-8°), so the body–twist + coupling is stable at any step. The `motionFile` roll column and a rolling vessel give + `Twist = −roll` and the quasi-static torque at every step. +- **Backward compatibility and refusals** (`torsion_deck`, full suite). Without torsion columns, + with `Free` columns (whatever their normal), or with one restrained end, every output is + byte-identical to the 6-column deck, and `Rigid`, `Infinity`, `Inf` and a quoted `"Rigid"` + give the same run. Malformed rows, a missing `GJ`, an `EI = 0` line, a rod end, a `Point3` body, + `ATTACHMENTS`, modal analysis, the configuration blend, misuse of the roll column, channels on + an unrestrained line, the coupled entries and a mixed `EI = 0` + finite-EI deck without a body + stop with named errors. + +Limits. The torsion model is quasi-static (no torsional inertia), takes no seabed friction +against twist and no torque–tension coupling, and does not resolve self-contact. Dynamic loop +formation (hockling) and a twisted slack catenary against OrcaFlex are not part of this record, +and there is no experimental torsion case. + ## Cross-code comparison method Cross-code validation compares physical observables, not another solver's internal diff --git a/doc/capabilities.rst b/doc/capabilities.rst index 8086488..4032d82 100644 --- a/doc/capabilities.rst +++ b/doc/capabilities.rst @@ -79,10 +79,23 @@ Line and constitutive capability - Supported with limits - single-section taut dynamic line with OWC and two history states; composite, hydro, finite-EI, current, and wave combinations fail closed by name - * - Torsion, hockling, nonlinear cross-section laws + * - Torsion of finite-EI lines + - Standalone only, with limits + - condensed isotropic torsion (uniform torque, quasi-static: no torsional inertia) of a + finite-EI line restrained in torsion at both ends (``END CONNECTIONS`` torsion columns, + explicit ``GJ``), in statics with a stability check and buckling descent, and in dynamics + with turning Rigid6 bodies, ``vesselMotion``/``vesselRAO`` and the ``motionFile`` roll + column; ``Torq``/``Twist`` channels and range graphs. Stopped by name: coupled OpenFAST + and FAST.Farm, the mixed ``EI = 0`` + finite-EI aggregate, rod ends, bodies other than + Rigid6, lines with ``ATTACHMENTS``, modal analysis, ``False alpha_force_blend``, and C API + object queries (which take ``EI = 0`` decks only). Not modelled: seabed friction against + twist, torque–tension coupling, anisotropic sections, self-contact of a loop (see + :ref:`theory-torsion`) + * - Hockling loops, nonlinear cross-section laws - Not provided - - the production elements carry no torsion; the secondary Cosserat path - (:doc:`solver_paths`) is not a production route + - the buckling onset and the post-buckled shape are computed, but a loop that closes on + itself is not resolved; the secondary Cosserat path (:doc:`solver_paths`) is not a + production route * - Discrete attachments (``ATTACHMENTS``: buoyancy modules, clumps) - Supported - lumped at nodes of a finite-EI cable, standalone and coupled; rejected on the two-moving-end @@ -90,7 +103,8 @@ Line and constitutive capability * - Modal analysis (``nModes``) - Standalone only - natural frequencies and mode shapes of each line about its static equilibrium (all - ``EI = 0`` lines, or all finite-EI lines on the Hermite route; flat seabed) + ``EI = 0`` lines, or all finite-EI lines on the Hermite route; flat seabed); a deck with + torsion is rejected * - VIV (``compVIV``) - Not implemented - use another validated model; CableDyn rejects the request diff --git a/doc/conventions.rst b/doc/conventions.rst index 6fe0bb1..7de8853 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -110,6 +110,8 @@ Sign conventions normal reaction acts in :math:`+z` on a level floor, and along the upward surface normal on a sloped bathymetry (:doc:`theory`). - **Rotations** are right-handed about their axis. +- **Torque** on a line with torsion is positive for a right-handed twist of End B relative to + End A (see `Torsion signs`_). Line topology and arc length ---------------------------- @@ -162,6 +164,32 @@ Angles at line ends End-A→End-B tangent; at a coupled end it is stored in the supporting body's frame and rotates with it. +Torsion signs +------------- + +These apply to a finite-EI line restrained in torsion at both ends (:doc:`driver_format`). + +- The end frame of each end is its direction ``Ez`` (End A → End B) and its reference normal + ``Nx``, in the frame of ``Ez``. ``Pretwist`` rolls the end frame about ``Ez`` by a right-handed + angle, and the imposed twist is :math:`\Phi = \text{Pretwist}_B - \text{Pretwist}_A`. A + ``motionFile`` roll turns the line's moving end right-handed about the tangent pointing into + the line from that end, and enters with a minus sign, + :math:`\Phi = \text{Pretwist}_B - \text{Pretwist}_A - \text{roll}`, whichever end moves. At + End A this is a right-handed rotation about ``Ez``, as of a body or vessel carrying End A; at + End B (a deck listing the anchor as End A) it is about ``−Ez``, the opposite sense to + ``Pretwist`` (B). +- **Torque is positive** for a right-handed twist of End B relative to End A about the End A → + End B tangent: the internal twisting moment :math:`GJ` times the twist rate, with arc length + from End A, as OrcaFlex's ``Torque``. Reversing a line (swapping its ends) keeps the sign of + the torque: the parser negates and swaps the pretwists, and :math:`\Phi` is unchanged. +- **Twist** channels are in degrees: ``TwistN`` is the cable's own twist from End A to + node ``J``, ``Twist`` the total :math:`\Phi - \Theta` with the end-spring windup. OrcaFlex + reports the twist rate in deg/m and end twisting stiffness in kN·m/deg; CableDyn's + ``TorsStiffness`` is in N·m/rad. +- The geometric twist :math:`\Theta` is the angle about the End B direction, right-handed, from + the End B normal to the End A normal transported along the line. It is zero for a line in a + plane whose two normals are perpendicular to that plane. + Orientation angles ------------------ diff --git a/doc/driver_format.md b/doc/driver_format.md index 9f04a36..477365b 100644 --- a/doc/driver_format.md +++ b/doc/driver_format.md @@ -257,7 +257,11 @@ A stock-order header applies only to the 10-column row. torsional stiffness, transverse and axial rotary inertia per length): `Name Diam Mass EA BA EI GAs GJ Irt Irn Cd_n Cd_t Ca_n Ca_t`. The four extra values must be finite and either all `0` or all `> 0`. All zero, or the 10-column row, selects the circular-section -closure `GAs = EA/(2(1+0.3))`, `GJ = EI/(1+0.3)`, `Irt = m d²/16`, `Irn = m d²/8`. +closure `GAs = EA/(2(1+0.3))`, `GJ = EI/(1+0.3)`, `Irt = m d²/16`, `Irn = m d²/8`. The closure +serves the secondary Cosserat path only: a finite-EI line restrained in torsion +([END CONNECTIONS](#end-connections-optional-finite-ei-lines)) takes `GJ` [N·m²/rad] from the +14-column row of each of its sections and stops by name without it. `GAs`, `Irt` and `Irn` are +not used by the cubic-Hermite route, but the all-or-nothing rule still asks for positive values. **Viscoelastic axial stiffness (MoorDyn `ElasticMod`).** The EA and BA columns accept bar-separated parts: @@ -635,7 +639,7 @@ never both: a `SECTIONS` row for a 7-column line is an error naming the line. |--------|---------| | `ID` | unique line id ≥ 1 | | `NodeA`, `NodeB` | End A / End B POINT ids (distinct). **End A is the fairlead (top) end and End B the anchor (lower) end**, as in OrcaFlex. | -| `Outputs` | per-line file flags. `-` = none. `p` (node positions) and `t` (segment tensions) write `.Line.p.out` / `.t.out`, End A first. Static EI = 0 decks write node/segment tables; independent EI = 0, point-system EI = 0, independent finite-EI, rod, and Rigid6 dynamic decks write time series (one row per output time). `r` writes the range graph `.Line.range.out`: the minimum, maximum and mean over the run (from `RangeStart`) of the node tension, curvature, bend moment, declination and seabed clearance, on every standalone route; a coupled OpenFAST deck rejects it. Line-node channels in the main `.out` file are requested in `OUTPUTS`. | +| `Outputs` | per-line file flags. `-` = none. `p` (node positions) and `t` (segment tensions) write `.Line.p.out` / `.t.out`, End A first. Static EI = 0 decks write node/segment tables; independent EI = 0, point-system EI = 0, independent finite-EI, rod, and Rigid6 dynamic decks write time series (one row per output time). `r` writes the range graph `.Line.range.out`: the minimum, maximum and mean over the run (from `RangeStart`) of the node tension, curvature, bend moment, declination and seabed clearance (and torque and twist on a line with torsion), on every standalone route; a coupled OpenFAST deck rejects it. Line-node channels in the main `.out` file are requested in `OUTPUTS`. | **Automatic anchor-first swap.** Stock MoorDyn decks list lines anchor first. When a line's `NodeA` is a `Fixed` point and its `NodeB` is a `Coupled`, `Vessel`, `Body`, `Free`, or @@ -752,6 +756,72 @@ Rows on an `EI = 0` line, duplicate rows, unknown line ids, null directions, neg and non-finite values fail deck validation. OpenFAST linearisation with a platform-relative end connection is rejected at initialisation. +#### Torsion columns + +Four or five optional columns restrain the twist of a finite-EI line end (rows of 10 or 11 +columns; the 6-column rows keep their meaning): + +```text +--------------------- END CONNECTIONS ------------------------------- +LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist +(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg) +1 A Rigid 1.0 0.0 0.0 Rigid 0.0 0.0 1.0 0.0 +1 B Rigid 1.0 0.0 0.0 Rigid 0.0 0.0 1.0 720.0 +``` + +| Column | Meaning | +|--------|---------| +| `TorsStiffness` | `Free`/`Zero`/`0` (default, no torsional restraint), `Rigid`/`Infinity`/`Inf` (any case), or a positive torsional end spring [N·m/rad], a plain finite number | +| `NxX`, `NxY`, `NxZ` | zero-twist reference normal of the end, in the frame of `Ez`; finite numbers; at a restrained end non-zero and not parallel to `Ez` (within about 0.06°), orthonormalised against `Ez` by the parser; unused at a `Free` end | +| `Pretwist` | optional roll of the end frame about `Ez` [deg, right-handed, default 0]; any real value, several turns included | + +Behaviour: + +- Torsion is solved only on a line restrained at **both** ends. With one end restrained the + other end is free to twist, the line carries no torque, and the driver prints a note and + solves the line as without the columns (the rules below then do not apply). `Free` columns, + or no columns, reproduce the 6-column results exactly. +- The imposed twist is `Phi = Pretwist(B) − Pretwist(A)`, measured against the line's own + geometric twist `Theta`, the parallel-transport rotation of the End A normal carried along the + centreline to End B (zero for a line that stays in a plane, with both normals perpendicular to + that plane). The torque is uniform along the line, `M = (Phi − Theta)/C`, with the compliance + `C = Σ L/GJ` over the sections plus `1/k` of each torsional end spring. Positive torque is a + right-handed twist of End B relative to End A about the End A → End B tangent (OrcaFlex's + sign). A non-planar line has `Theta ≠ 0` in its untwisted shape; to start it free of torque, + set the End B pretwist to minus the static `Twist` of a run with zero pretwist (in degrees); + the line then keeps its untwisted shape. +- The torsion end frame turns with what carries the end: a Rigid6 body, the vessel of + `vesselMotion`/`vesselRAO`, or nothing at a `Fixed` or held point. The `motionFile` roll + column rolls the frame of the line's moving end + ([Prescribed motion](#prescribed-motion-motionfile)). +- A Rigid6 body holding such a line turns by less than 90° per step: a step whose turn would be + larger is halved (the twist is read from the body's orientation, which repeats every turn), and + a run that still needs more after six halvings stops by name; reduce `dtM`. +- Torsion may be restrained at a bending-`Pinned` end. The torque then passes through a + constant-velocity-joint idealisation: the end frame is carried from `Ez` to the line tangent by + the smallest rotation, so the end transmits the torque *semi-tangentially*, not as Greenhill's + axial-torque hinge. A straight rod with two such ends buckles at `M = 4.9113 EI/L` at zero + tension (`7.2693 EI/L` at `T L²/EI = 10`), not `2π EI/L`; clamped ends buckle at + `8.9868 EI/L` (see [theory](theory.rst)). +- The torque returns to a Rigid6 body at End A, in the static body equilibrium and in the + dynamics; it is part of `BodyM*`. +- Statics solve the imposed twist as the last load stage, in steps of at most 45°, test every + stage for stability and, above a buckling onset, leave the unstable straight state for the + buckled one. Dynamics carry the torque in every step; torsion has no inertia (quasi-static + torsion), so the torque follows the imposed twist without a torsional wave. + +Scope. A torsionally restrained line needs a finite-EI line with a `Fixed` End B, an explicit +`GJ` on every section, and, at End A, a `Fixed`, `Coupled`/`Vessel` point or a Rigid6 +`Body` point. Each of the following stops with a named error: torsion on an `EI = 0` line, +on a rod end or on a `Point3` body, a line with `ATTACHMENTS`, modal analysis (`nModes`), a +dynamic run with `False alpha_force_blend`, any restrained end (one end included) in a coupled +OpenFAST or FAST.Farm run or in a deck mixing `EI = 0` and finite-EI lines without a body +(`torsion is not yet supported in a deck that mixes ...`; with a body such a deck runs on the +multibody route, which supports torsion), and a `Torq`/`Twist` channel on a line that is not +restrained at both ends. Seabed +friction does not resist twist (the laid part of a line twists freely), and there is no +torque–tension coupling. + ### EQUIVALENT BUOYANCY (optional) Specifies a cable section by its target submerged weight per metre instead of its dry mass. Rows @@ -1051,7 +1121,14 @@ cases. With an active path: `motionFile`. Every non-comment record of the motion file is one row `time point_id x y z vx vy vz ax ay az` of -plain numbers; tokens after the eleventh are ignored. Header lines must be comments. `time` must +plain numbers; tokens after the eleventh are ignored, except that a deck with a line restrained +in torsion at both ends reads a twelfth, `roll` [deg]: at the point, the roll of the frame of the +line's moving end (End A, or End B in a deck that lists the `Fixed` anchor as End A), right-handed +about the line tangent pointing into the line from that end. Either way it imposes +`Phi(t) = Pretwist(B) − Pretwist(A) − roll(t)`: the roll has the sense of `Pretwist(A)` at End A +(where the inward tangent is `Ez`) and the opposite sense to `Pretwist(B)` at End B (where it is +`−Ez`). The roll is 0 at +`t = 0` and is given on every row of the point or on none. Header lines must be comments. `time` must lie on the `0, dtM, 2·dtM, …, TMax` grid and `point_id` must be a prescribed point. Each (point, time) pair appears once, every prescribed point needs a row at every grid time, and rows may appear in any order. See also [Auxiliary file formats](file_formats.rst). @@ -1163,6 +1240,9 @@ bending moment. None of these channels is mandatory. | `TDPx`, `TDPy`, `TDPz` | line L TDP position | m | | `TDPLay` | line L layback: horizontal distance from the TDP to the suspended end | m | | `TDPExc` | line L TDP excursion: horizontal TDP displacement from its initial position, along the initial direction toward the suspended end | m | +| `TorqN` | line L torque at node J (uniform along the line); positive for a right-handed twist of End B relative to End A about the End A → End B tangent. Only on a line restrained in torsion at both ends | N·m | +| `TwistN` | line L material twist accumulated from End A to node J, `M × Σ L/GJ` over the elements in between; 0 at End A, without the end-spring windup | deg | +| `Twist` | line L total twist `Phi − Theta = M C`, end-spring windup included | deg | - ``/`

` are deck LINE/POINT **ids**, not array positions. A channel that matches no supported form, names an unknown id, or carries trailing text (for example `Point2px_raw`) is a diff --git a/doc/examples.rst b/doc/examples.rst index df3d322..b3a6f5d 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -350,6 +350,15 @@ touchdown and lazy-wave shape from the geometry alone; there is no initial-shape 1024-element mesh — :doc:`tutorial_lazywave` - Hermite, modal - ~2 s + * - ``torsion_lazy_wave_hangoff_twist.dat`` + - the 80 m cable with an illustrative ``GJ`` of 50 kN·m², clamped and restrained in torsion + at both ends, twisted two turns at the hang-off over 60 s by the ``motionFile`` roll column + (``data/torsion/hangoff_roll_2turns_60s_dt01.txt``) and held for 30 s. ``Torq1N1`` reaches + 3.54 kN·m and ``Twist1`` 690°: the cable takes up 30° of the 720° by writhing out of its + plane (``L1N58py``, 3.3 m). The range graph carries the torque and twist envelopes. See + :ref:`theory-torsion` + - Hermite, dynamic + - ~2 s The first three set ``TMax = 0`` (static only). The motion file holds absolute position, velocity, and acceleration at every ``dtM`` and is regenerated by @@ -413,6 +422,8 @@ These files are inputs referenced by decks, not decks: ``data/vessel/sample_rao.txt`` — the illustrative RAO table of ``lazy_wave_vessel_rao.dat``. * ``data/range_tdp/chain_surge_5m_30s_dt005.txt`` — the fairlead surge of ``chain_range_tdp.dat``; ``data/range_tdp/generate_chain_surge.py`` writes it. +* ``data/torsion/hangoff_roll_2turns_60s_dt01.txt`` — the held hang-off with its roll column of + ``torsion_lazy_wave_hangoff_twist.dat``; ``data/torsion/generate_hangoff_roll.py`` writes it. * ``moordynC_wavekin/wave_frequencies.txt`` and ``moordynC_wavekin/current_profile.txt`` — the MoorDyn-C kinematics files, read from the folder of the deck beside them. * ``plot_range_envelope.py`` — plots the tension envelope of a range-graph file. diff --git a/doc/file_formats.rst b/doc/file_formats.rst index 4c2eeaf..5958c11 100644 --- a/doc/file_formats.rst +++ b/doc/file_formats.rst @@ -171,8 +171,38 @@ Each data row has eleven whitespace-separated numbers: - m/s² - Absolute acceleration. -Tokens after the eleventh are ignored. Every row must start with a number, so any -header or column-label line must be a comment. +Every row must start with a number, so any header or column-label line must be a comment. +Tokens after the eleventh are ignored, except on a deck with a torsional line (below). + +**Roll column (torsion).** On a deck where some finite-EI line is restrained in torsion at both +ends (``END CONNECTIONS`` ``TorsStiffness``, see :doc:`driver_format`), a numeric twelfth token is +read as the roll of the frame of that line's moving end (End A, or End B in a deck that lists the +anchor as End A), in degrees, right-handed about the line tangent pointing into the line from that +end. It imposes a twist history without turning the point: the line's imposed twist is + +.. math:: + + \Phi(t) = \text{Pretwist}_B - \text{Pretwist}_A - \text{roll}(t), + +whichever end moves, so a positive roll lowers :math:`\Phi`. At End A it has the sense of +``Pretwist`` (A) and of rolling the End A frame through a body or vessel; at End B, where the +inward tangent is ``−Ez``, it has the opposite sense to ``Pretwist`` (B). Rules: + +* a point gives the roll on every row or on none (``gives the roll column (12th) on some rows + only``); +* the roll must be 0 at ``t = 0``, because the static initial condition uses the deck + ``Pretwist``; give a constant twist there (``the roll column of point must be 0 at + t = 0``); +* a non-zero roll is accepted only at the moving end of a line restrained in torsion at both + ends (``has a non-zero roll column, but no line restrained in torsion at both ends ... has its + moving (non-Fixed) end there``); +* the roll is real valued and not wrapped, so several turns are written as they accumulate. + Torsion carries no inertia (see :doc:`theory`), so the torque follows the roll at once and no + roll rate or acceleration column is needed. + +:file:`examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt`, used by +:file:`examples/torsion_lazy_wave_hangoff_twist.dat`, holds the hang-off in place and rolls it +from 0 to −720° over 60 s. Coverage and time grid ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/options.rst b/doc/options.rst index 3ca53a4..55e412d 100644 --- a/doc/options.rst +++ b/doc/options.rst @@ -327,7 +327,8 @@ Finite-EI cable controls Mexico 80 m cable in 3 m surge the mean hang-off tension is 12511 / 10097 / 9507 / 9360 N at ``dtM`` 0.1 / 0.05 / 0.025 / 0.0125 s with ``False`` and 9424 / 9322 / 9314 / 9311 N with ``True``. A run prints a note when a nodal tangent turns by more than 0.25 deg - (``True``) or 0.1 deg (``False``) in one step + (``True``) or 0.1 deg (``False``) in one step. A dynamic run with a torsional line + (``END CONNECTIONS`` ``TorsStiffness`` at both ends) needs ``True`` - standalone and coupled (finite-EI lines) - ``False alpha_force_blend`` * - ``cable_statics`` / ``cablestatics`` @@ -449,7 +450,9 @@ Ambient fluid and prescribed motion dynamic deck), or all finite-EI lines on the cubic-Hermite route (dynamic deck, every End B Fixed). Other decks are rejected; a deck that mixes ``EI = 0`` and finite-EI lines stops with ``OPTION nModes is not supported for mixed EI=0/finite-EI decks; - remove it or set it to 0``. The banded solver has no line-length limit: a + remove it or set it to 0``; a deck with a torsional line stops with ``modal analysis + (OPTION nModes) of a line with torsion is not yet supported``. The banded solver has no + line-length limit: a 1024-element finite-EI cable takes a few seconds. See :doc:`theory` - standalone - ``10 nModes`` @@ -516,7 +519,9 @@ Ambient fluid and prescribed motion - path, or ``0``/``none`` - absent - an active path requires ``dtM`` and ``TMax``; ``0`` or case-insensitive ``none`` disables - it. See the file grammar in :doc:`driver_format` + it. On a deck with a line restrained in torsion at both ends, an optional twelfth column + rolls that line's End A frame (degrees, 0 at ``t = 0``). See the file grammar in + :doc:`file_formats` - standalone ``Coupled``/``Vessel`` points, prescribed rods, and Rigid6 bodies; rejected on ``Connect``/``Free`` point-system, FAILURE, and mixed decks; forbidden in OpenFAST, where the host owns coupled motion diff --git a/doc/outputs.rst b/doc/outputs.rst index 1e7c734..f4e411a 100644 --- a/doc/outputs.rst +++ b/doc/outputs.rst @@ -326,7 +326,8 @@ channels instead) and accept ``r``. * - ``r`` - ``.Line.range.out`` - range graph: minimum, maximum and mean over the run of the node tension, curvature, bend - moment, declination and seabed clearance, one row per node (see below) + moment, declination and seabed clearance (and torque and twist on a line with torsion), + one row per node (see below) ```` is the deck line id. Each file starts with a ``#`` comment line (e.g. ``# CableDyn static line node positions; public node order EndA -> EndB``) and a header row; @@ -356,7 +357,8 @@ the minimum, maximum and mean over the output times of the range window. The win ``.out`` row with ``t ≥ RangeStart`` (OPTION ``RangeStart``, default 0, see :doc:`options`), so a start-up transient can be excluded; a static run has one sample, ``t = 0``. The values are the node channels of the same run, ``TenN``, ``CurvN``, ``BendMomN`` and -``LNDec``, evaluated at every node: the minimum and maximum equal those of the channel time +``LNDec`` (and ``TorqN`` and ``TwistN`` on a line with torsion), evaluated at +every node: the minimum and maximum equal those of the channel time histories exactly, and the mean is their arithmetic mean. The file is written when the run completes; a run that stops early leaves none. @@ -395,6 +397,13 @@ End A -> End B)``. - seabed clearance: node ``z`` minus the seabed elevation below the node (flat ``WtrDpth`` or the ``bathymetryFile`` surface); negative where the node penetrates the penalty seabed. Present only when the deck defines a seabed + * - ``TorqueMin`` ``TorqueMax`` ``TorqueMean`` + - ``(N.m)`` + - torque, as ``TorqN``. Present only on a line restrained in torsion at both ends + * - ``TwistMin`` ``TwistMax`` ``TwistMean`` + - ``(deg)`` + - twist of the cable from End A to the node, as ``TwistN``. Present only on a line + restrained in torsion at both ends The accumulation keeps three numbers per node and quantity and makes no heap allocation per step. Sampling a line evaluates its node channels once per output row; on the 1024-element cubic-Hermite @@ -545,6 +554,23 @@ Line-node channels * - ``LNAzi`` - azimuth of the node's axial tangent from +X toward +Y, in [0, 360) - deg + * - ``TorqN`` + - torque (twisting moment) at node ``J`` of a line restrained in torsion at both ends + (``END CONNECTIONS`` ``TorsStiffness``, see :doc:`driver_format`). It is uniform along the + line, :math:`M = (\Phi - \Theta)/C`; positive for a right-handed twist of End B relative + to End A about the End A → End B tangent, as OrcaFlex's ``Torque`` + - N·m + * - ``TwistN`` + - material twist of the cable itself from End A to node ``J``, + :math:`M \sum L_e/GJ_e` over the elements between them: 0 at End A, and at End B the + twist of the line without the windup of its torsional end springs. OrcaFlex reports the + twist rate (``Twist``, deg/m) instead; here it is integrated from End A + - deg + * - ``Twist`` + - total twist of the line, :math:`\Phi - \Theta = M C`, including the windup of + torsional end springs: the imposed twist :math:`\Phi` less the geometric twist + :math:`\Theta` the line takes up by writhing out of its plane + - deg Touchdown channels ~~~~~~~~~~~~~~~~~~ @@ -684,6 +710,8 @@ with exit code 1. A name is rejected when: * it references an unknown line or point id, or a node number larger than the line's node count, or is a ``TDP`` name with a suffix other than ``s``, ``x``, ``y``, ``z``, ``Lay`` or ``Exc``; +* it is a ``TorqN``, ``TwistN`` or ``Twist`` channel of a line that is not + restrained in torsion at both ends (``... carries no torque``); * on a mixed ``EI = 0`` + finite-EI deck (and in OpenFAST), it is a ``Point

`` channel of a ``Coupled``/``Vessel`` point attached **only** to finite-EI lines — that point is not part of the ``EI = 0`` point system that serves point channels. Use the cable's diff --git a/doc/python.rst b/doc/python.rst index 78145ec..403d56c 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -335,6 +335,11 @@ The matching range-graph views are: tensions.plot_range(725.125) tensions.plot_envelope(600.0, 4200.0) +The solver's own range file (``.Line.range.out``, LINES flag ``r``) is read by +:func:`cabledyn.read_range_graphs`; on a line restrained in torsion it also returns the +``"torque"`` (N·m) and ``"twist"`` (deg, from End A) envelopes, and :func:`cabledyn.read_output` +attaches those units to the ``TorqN``, ``TwistN`` and ``Twist`` channels. + DataFrame and pyDatView interoperability ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/references.rst b/doc/references.rst index 018d64c..0fc43e6 100644 --- a/doc/references.rst +++ b/doc/references.rst @@ -22,6 +22,25 @@ Element formulation and reduced models for geometrically exact Kirchhoff rods. *Computer Methods in Applied Mechanics and Engineering* **290**, 314–341. https://doi.org/10.1016/j.cma.2015.02.029 +.. _ref-meier2014: + +- **Meier, C., Popp, A. & Wall, W. A. (2014).** An objective 3D large deformation finite + element formulation for geometrically exact curved Kirchhoff rods. *Computer Methods in + Applied Mechanics and Engineering* **278**, 445–478. https://doi.org/10.1016/j.cma.2014.05.017 + +.. _ref-bergou2008: + +- **Bergou, M., Wardetzky, M., Robinson, S., Audoly, B. & Grinspun, E. (2008).** Discrete + elastic rods. *ACM Transactions on Graphics* **27**\ (3), 63. + https://doi.org/10.1145/1360612.1360662 + +.. _ref-vanderheijden2003: + +- **van der Heijden, G. H. M., Neukirch, S., Goss, V. G. A. & Thompson, J. M. T. (2003).** + Instability and self-contact phenomena in the writhing of clamped rods. *International + Journal of Mechanical Sciences* **45**\ (1), 161–196. + https://doi.org/10.1016/S0020-7403(02)00183-2 + .. _ref-simo1985: - **Simo, J. C. (1985).** A finite strain beam formulation. The three-dimensional dynamic diff --git a/doc/theory.rst b/doc/theory.rst index bbbed40..ef2b08c 100644 --- a/doc/theory.rst +++ b/doc/theory.rst @@ -137,7 +137,126 @@ element, with a 2 % criterion relative to the largest local magnitude (at least The element follows the geometrically exact Kirchhoff-rod formulation of :ref:`Boyer et al. (2011) ` with the :math:`C^1` Hermite centreline interpolation of :ref:`Meier, Popp & Wall (2015) `, reduced to a torsion-free -cable. +cable. Torsion enters only through the condensed line term below. + +.. _theory-torsion: + +Condensed torsion +~~~~~~~~~~~~~~~~~ + +A line restrained in torsion at both ends (``END CONNECTIONS`` ``TorsStiffness``, see +:doc:`driver_format`) adds the twist of an isotropic Kirchhoff rod without adding degrees of +freedom. For an isotropic section with no distributed torque the twisting moment +:math:`M = GJ\,u_3`, with :math:`u_3` the material twist rate, is uniform along the rod +(:ref:`van der Heijden et al., 2003 `). The twist field +then condenses to one scalar per line, and the line energy gains + +.. math:: + + E_t = \frac{(\Phi - \Theta(\mathbf{q}))^2}{2C}, \qquad + M = \frac{\Phi - \Theta}{C}, \qquad + C = \sum_e \frac{L_{0,e}}{GJ_e} + \frac{1}{k_A} + \frac{1}{k_B} . + +:math:`\Phi` is the imposed relative roll of the End B frame with respect to the End A frame +(``Pretwist`` and the ``motionFile`` roll column), :math:`k_A, k_B` are the optional torsional end +springs (:math:`1/k = 0` for ``Rigid``), and :math:`\Theta(\mathbf{q})` is the geometric twist of +the centreline: the angle about the End B direction :math:`\mathbf{d}_B`, right-handed, from the +End B reference normal :math:`\mathbf{n}_B` to the End A normal :math:`\mathbf{n}_A` carried to End B +by parallel transport. Each end frame :math:`(\mathbf{d}, \mathbf{n})` turns with the body or vessel +that holds the end. A clamped end has :math:`\mathbf{d}` equal to its end tangent; at a +bending-pinned end :math:`\mathbf{d}` is the connection direction and the transport starts with +the smallest rotation from :math:`\mathbf{d}` to the tangent. + +Because the Hermite centreline is :math:`C^1`, the transport factorises into one holonomy per +element and a chain of smallest rotations between the unit node tangents +:math:`\mathbf{t}_k = \mathbf{m}_k/\lvert\mathbf{m}_k\rvert`: + +.. math:: + + \Theta = \sum_e h_e + \chi, \qquad + h_e = \int_e \frac{\mathbf{t}_1 \cdot (\mathbf{a} \times \mathbf{b})} + {\lvert\mathbf{a}\rvert^2 \left(1 + \mathbf{t}_1\cdot\mathbf{a}/\lvert\mathbf{a}\rvert\right)} + \,\mathrm{d}s , + +with :math:`\mathbf{t}_1` the tangent at the element's first node, :math:`\mathbf{a} = \mathbf{r}'`, +:math:`\mathbf{b} = \mathbf{r}''` at the Gauss points of the bending rule, and :math:`\chi` the +angle of the smallest-rotation chain +:math:`\mathbf{d}_A \to \mathbf{t}_1 \to \dots \to \mathbf{t}_N \to \mathbf{d}_B` applied to +:math:`\mathbf{n}_A`. For differentiation, each link of the chain is written with a gauge frozen at +the current configuration as a sum of signed solid angles +:math:`\Omega(\mathbf{x}, \mathbf{y}, \mathbf{z}) = 2\operatorname{atan2}(\mathbf{x}\cdot(\mathbf{y}\times\mathbf{z}), +1 + \mathbf{x}\cdot\mathbf{y} + \mathbf{y}\cdot\mathbf{z} + \mathbf{z}\cdot\mathbf{x})` of geodesic +triangles, each of which involves at most two neighbouring tangents. The gradient and Hessian +of :math:`\Theta` are therefore local, in closed form, and have the half-bandwidth of the cable +tangent; the value of :math:`\Theta` is taken from the sequential product. This is the +discrete parallel transport and holonomy of :ref:`Bergou et al. (2008) ` applied +to the Hermite centreline. The test suite checks the closed form against automatic +differentiation of an independent reference implementation, against finite differences, and +for invariance under rigid rotation. + +:math:`\Theta` is known modulo :math:`2\pi`. The solver carries its unwrapped value as a state +of the line, unwraps each new evaluation to the branch nearest the last accepted value, and +accepts no step that changes :math:`\Theta` by more than :math:`\pi/2`; such a step is cut. A +link or Gauss point at which the tangent turns by more than 120° within one element or between +neighbouring nodes (:math:`1 + \mathbf{t}_1\cdot\mathbf{t} < 0.5`) stops with a named error; a +mesh that resolves the curvature is far from it. + +**Residual and tangent.** The torsion term adds :math:`-M\,\nabla\Theta` to the residual and + +.. math:: + + \mathbf{K} = \underbrace{\mathbf{K}_c - M\,\nabla^2\Theta}_{\mathbf{B}\ \text{(banded)}} + + \frac{1}{C}\,\nabla\Theta\,\nabla\Theta^{\mathsf T} + +to the tangent. The rank-one term is not banded; each Newton step solves the bordered system +by Sherman–Morrison on the band factorisation of :math:`\mathbf{B}` with two right-hand sides, +:math:`\mathbf{B}\mathbf{y} = -\mathbf{R}`, :math:`\mathbf{B}\mathbf{z} = \nabla\Theta`, +:math:`\mathbf{x} = \mathbf{y} - \mathbf{z}\,(\nabla\Theta\cdot\mathbf{y})/(C + \nabla\Theta\cdot\mathbf{z})`, +and accepts it on a backward-error test, with a shifted factorisation and iterative refinement +as the fallback. Dropping the rank-one term would leave a Newton iteration that converges only +linearly, at the rate :math:`\nabla\Theta^{\mathsf T}\mathbf{B}^{-1}\nabla\Theta / C`; on the +post-buckled states of the validation (see :doc:`validation`) that rate is 0.05 to 0.22. The +same term enters the dynamic effective tangent, scaled by :math:`1 - \alpha_f` of the force +blend, and is frozen together with a reused factorisation. + +**Statics and stability.** The imposed twist is the last static load stage, ramped from the +geometric twist of the untwisted equilibrium in steps of at most :math:`\pi/4`. Each converged +stage is tested for stability by the inertia of :math:`\mathbf{K}` on the free DOFs, +:math:`\operatorname{neg}(\mathbf{K}) = \operatorname{neg}(\mathbf{B}) - +[1 + \nabla\Theta^{\mathsf T}\mathbf{B}^{-1}\nabla\Theta/C < 0]`, with the negative eigenvalues +of :math:`\mathbf{B}` counted by a banded :math:`\mathbf{L}\mathbf{D}\mathbf{L}^{\mathsf T}` +factorisation. Above a buckling onset the straight twisted state is a saddle to which Newton +converges quadratically; the static solve then descends along the lowest mode with energy +acceptance onto the buckled branch, and reports the descent. The onset is that of a +clamped–clamped rod under the dead tension :math:`T`, eq. (33) of +:ref:`van der Heijden et al. (2003) `; at :math:`T = 0` it reduces to +:math:`\tan x = x`, :math:`x = ML/2EI`, so :math:`M_{cr} = 8.9868\,EI/L`. At large tension it +approaches the localised value :math:`2\sqrt{EI\,T}` of an infinite rod from above, reported in +the same paper. A bending-pinned end restrained in torsion transmits the torque about the +bisector of the connection direction and the tangent (a *semi-tangential* end, a +constant-velocity joint), for which :math:`\tan x = -x/3` and :math:`M_{cr} = 4.9113\,EI/L` at +zero tension, not the :math:`2\pi EI/L` of Greenhill's axial-torque hinge. OrcaFlex's +bending-free end with a twisting spring gives the same value (see :doc:`validation`). + +**Dynamics and loads.** The torsion force enters the force-blended residual like the internal +force, so a dynamic deck with torsion requires ``True alpha_force_blend`` (the default). Torsion +carries no inertia: the torque follows the imposed twist within the step, without a torsional +wave. This is accurate while the first torsional frequency of the line, +:math:`\sqrt{GJ/I_p}/(2L)` with :math:`I_p` the polar mass moment per length, is well above the +excitation; it is not for very long or torsionally soft lines. The moment of the torque on the +object at End A is :math:`M\,\partial\Theta/\partial\boldsymbol{\omega}_A`, the derivative with +respect to a rotation :math:`\boldsymbol{\omega}_A` of that object (for a clamped end, of its frame +and the end tangent together), so the virtual work of the body moment is exact; for a straight +line it is the torque along the line axis. The static body equilibrium uses the same moment and +its stiffness :math:`\mathbf{a}\mathbf{a}^{\mathsf T}/C`, :math:`\mathbf{a} = \partial\Theta/\partial\boldsymbol{\omega}_A`. + +**Limits.** Seabed friction does not resist twist, so the laid part of a line twists freely +(the friction torque of a cable on the seabed can be comparable with the twist torque near +touchdown). There is no torque–tension coupling of armoured cables or wire ropes, no +anisotropic or pre-twisted section, and no self-contact: a loop that closes on itself is not +resolved. The model is that of :ref:`Meier, Popp & Wall (2014) ` and +:ref:`Bergou et al. (2008) ` restricted to the isotropic, torque-free-span case; +it is not a new formulation. Constitutive laws ----------------- diff --git a/doc/troubleshooting.rst b/doc/troubleshooting.rst index f585611..18d5bf1 100644 --- a/doc/troubleshooting.rst +++ b/doc/troubleshooting.rst @@ -304,6 +304,58 @@ Model data - remove ``staggered bodyScheme``; on a rod end give the direction along the rod axis (either sense). +Torsion +~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 42 58 + + * - Message + - Cause and fix + * - ``END CONNECTIONS row needs 6 columns ..., or 10 or 11 with the torsion columns`` + - a torsion row is ``LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ + [Pretwist]``; see :doc:`driver_format`. + * - ``END CONNECTIONS torsional stiffness must be finite and non-negative, Free, or Rigid`` / + ``torsion reference normal (NxX NxY NxZ) must be non-zero`` / ``must not be parallel to + the direction Ez`` + - give ``Free``, ``Rigid`` or a stiffness in N·m/rad, and a reference normal that is not + along ``Ez``. + * - ``line is torsionally restrained at both ends: its LINE TYPES row ... must give an + explicit GJ > 0`` + - torsion has no ``EI/1.3`` default: use the 14-column ``LINE TYPES`` row with ``GJ`` (and + positive ``GAs``, ``Irt``, ``Irn``) for every section of the line. + * - ``torsional END CONNECTIONS ... require a finite-EI line`` / ``require a finite-EI line + with Fixed End B`` + - torsion is solved on cubic-Hermite lines whose End B is an anchor. + * - ``a torsional END CONNECTION on a rod end is not supported`` / ``on a body needs a Rigid6 + body`` / ``torsion is not combined with ATTACHMENTS in this build`` + - outside the torsion scope (:doc:`capabilities`). Move End A to a ``Fixed``, + ``Coupled``/``Vessel`` point or a Rigid6 body, or represent the modules as a smeared + buoyancy section. + * - ``torsion is not yet supported in coupled OpenFAST runs`` / ``torsion is not yet supported + in a deck that mixes EI = 0 and finite-EI lines without a BODY, nor in coupled OpenFAST or + FAST.Farm runs`` + - run the torsional line standalone (a mixed deck with a body runs on the multibody route, + which supports torsion), or set ``TorsStiffness Free`` in every ``END CONNECTIONS`` row. + * - ``a body holding a line restrained in torsion turns by more than 90 deg in one step even + after 6 step halvings`` + - the body turns faster than one step can follow the twist; reduce ``dtM``. + * - ``torsion in a dynamic run needs the force-blended generalised-alpha (OPTION + alpha_force_blend True)`` / ``modal analysis (OPTION nModes) of a line with torsion is not + yet supported`` + - remove ``False alpha_force_blend`` or ``nModes`` from a deck with torsion. + * - ``OUTPUT "Torq...": line is not torsionally restrained at both ends ..., so it carries + no torque`` + - a ``Torq``/``Twist`` channel needs a line with a torsional restraint at both ends. + * - ``motionFile: point has a non-zero roll column, but no line restrained in torsion at + both ends ... has its moving (non-Fixed) end there`` / ``the roll column of point must + be 0 at t = 0`` / ``gives the roll column (12th) on some rows only`` / ``the roll column + (12th, degrees) must be a finite number`` + - the roll column drives the frame of a torsional line's moving end, starts from 0 (put a + constant twist in ``Pretwist``), and appears on every row of the point or on none + (:doc:`file_formats`). + FAILURE and CONTROL sections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -385,6 +437,16 @@ but a few geometries are genuinely hard: exist: the surplus must fold or form another contact region. CableDyn rejects a resulting element-localized wad through the curvature safety check. This is not fixed by relaxing Newton tolerances; revise the physical support/contact model or geometry. +- **A twisted finite-EI line.** ``torsion continuation stalled at Phi = ... (target ...)`` + reports the imposed twist the static ramp reached: beyond it the line has no nearby static + equilibrium on the path, typically because a loop is forming (hockling), which needs a + dynamic analysis and is not resolved as self-contact. ``finite-EI torsion static solve ended on + an unstable equilibrium`` means the descent from a buckled (unstable) twisted state did not + find a stable one. ``the twist moved more than pi/2 from its committed value in one step`` + stops a dynamic step that would change the twist by more than a quarter turn (the unwrapping + of the twist cannot be trusted beyond it): reduce ``dtM`` or slow the imposed roll. + ``tangent turns by more than 120 degrees`` names an element or node pair where the twist of + the centreline is no longer reliable: refine the mesh there. - **A cold start at full load.** The static solve continues the load in stages; if you have hand-tuned a case into a bad basin, remove the tuning and let the default continuation run from the catenary/arch seed. @@ -403,6 +465,15 @@ Warnings and notes that do not stop a run :header-rows: 1 :widths: 42 58 + * - ``Note: line is torsionally restrained at one end only; ... no torsion is solved`` + - with the other end free to twist the line carries no torque, so the torsion columns of the + restrained end have no effect. Restrain both ends to solve torsion. + * - ``Note: line : torsion solved, imposed twist ... deg, torque ... N m ( twist + stage(s), buckling descent(s))`` + - the static twist stage of a torsional line. A non-zero descent count means the straight or + untwisted branch was unstable at the imposed twist and the solve moved to a buckled shape; + check the shape and the ``Twist`` channel. + * - Message - Cause and fix * - ``WARNING: line type ... sinks ... m into the seabed ...; kBot is soft for this line`` diff --git a/examples/README.md b/examples/README.md index 1c5ed75..cacac9a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -46,6 +46,7 @@ Copy a deck under a new name and keep every file it references at the same relat | `data/syrope/` | Syrope settings and original working-curve table used by `syrope_polyester_mooring.dat`. | | `data/lozon/` | Prescribed hang-off heave history for `lozon_gomex80_power_cable_motion.dat` and the script that generates it. | | `data/vessel/` | 6-DOF vessel-motion record for `lazy_wave_vessel_motion.dat`, the script that generates it, and the illustrative RAO table for `lazy_wave_vessel_rao.dat`. | +| `data/torsion/` | Hang-off roll history (motionFile roll column) for `torsion_lazy_wave_hangoff_twist.dat` and the script that generates it. | | `data/range_tdp/` | Fairlead surge history for `chain_range_tdp.dat` and the script that generates it. | | `moordynC_wavekin/` | A deck with the MoorDyn-C `wave_frequencies.txt` and `current_profile.txt` files it reads from its own folder. | | `plot_range_envelope.py` | Plots the tension envelope of a range-graph file (`python plot_range_envelope.py `). | @@ -92,3 +93,13 @@ The seven `lozon_*` decks take their dimensions and static properties from Lozon The dynamic stiffness and dashpots in the 200 m and 800 m mooring decks are taken from CableDyn's MoorDyn viscoelastic validation case and are labelled as such in each deck; do not cite them as values from the paper. + +## Torsion + +`torsion_lazy_wave_hangoff_twist.dat` is the Gulf of Mexico 80 m lazy-wave cable with an +illustrative torsional stiffness `GJ` of 50 kN·m², clamped and restrained in torsion at both +ends through the `END CONNECTIONS` torsion columns. The `motionFile` roll column twists the +hang-off by two turns over 60 s; the run then holds the twist for 30 s. The torque settles at +3.54 kN·m: the cable writhes 3.3 m out of its plane and takes up 30° of the 720° itself. The +deck grammar is in [doc/driver_format.md](../doc/driver_format.md) and the model in the +torsion section of [doc/theory.rst](../doc/theory.rst). diff --git a/examples/data/torsion/generate_hangoff_roll.py b/examples/data/torsion/generate_hangoff_roll.py new file mode 100644 index 0000000..d9e3d0c --- /dev/null +++ b/examples/data/torsion/generate_hangoff_roll.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +"""Generate the hang-off roll history of torsion_lazy_wave_hangoff_twist.dat. + +The hang-off point stays at its deck position; the 12th column rolls the line's End A frame +about its director from 0 to -720 deg (two turns of imposed twist, Phi = +720 deg) along a C2 +quintic ramp over the first 60 s, then holds it. +""" + +from __future__ import annotations + +from pathlib import Path + +DT = 0.1 +T_MAX = 90.0 +RAMP_DURATION = 60.0 +ROLL_END = -720.0 +POSITION = (5.0, 0.0, -14.0) +POINT_ID = 1 + + +def roll(time: float) -> float: + """C2 quintic ramp from 0 to ROLL_END over RAMP_DURATION [deg].""" + if time >= RAMP_DURATION: + return ROLL_END + tau = time / RAMP_DURATION + return ROLL_END * (10.0 * tau**3 - 15.0 * tau**4 + 6.0 * tau**5) + + +def main() -> None: + output = Path(__file__).with_name("hangoff_roll_2turns_60s_dt01.txt") + lines = [ + "# File: examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt", + "# Hang-off roll for torsion_lazy_wave_hangoff_twist.dat (generate_hangoff_roll.py).", + "# The point is held; the roll ramps from 0 to -720 deg over 60 s (C2 quintic) and holds.", + "# Columns: time(s) point_id x(m) y(m) z(m) vx vy vz(m/s) ax ay az(m/s2) roll(deg)", + ] + x, y, z = POSITION + for index in range(round(T_MAX / DT) + 1): + time = index * DT + lines.append(f"{time:.1f} {POINT_ID:d} {x:.4f} {y:.4f} {z:.4f} 0 0 0 0 0 0 {roll(time) + 0.0:.10e}") + output.write_text("\n".join(lines) + "\n", encoding="ascii", newline="\n") + + +if __name__ == "__main__": + main() diff --git a/examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt b/examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt new file mode 100644 index 0000000..4613491 --- /dev/null +++ b/examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt @@ -0,0 +1,905 @@ +# File: examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt +# Hang-off roll for torsion_lazy_wave_hangoff_twist.dat (generate_hangoff_roll.py). +# The point is held; the roll ramps from 0 to -720 deg over 60 s (C2 quintic) and holds. +# Columns: time(s) point_id x(m) y(m) z(m) vx vy vz(m/s) ax ay az(m/s2) roll(deg) +0.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 0.0000000000e+00 +0.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3250055556e-05 +0.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6533511111e-04 +0.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.9326350000e-04 +0.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1120568889e-03 +0.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1147569444e-03 +0.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0924320000e-03 +0.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1234183722e-02 +0.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6727153778e-02 +0.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3756530500e-02 +1.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2505555556e-02 +1.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3155530611e-02 +1.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5885824000e-02 +1.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0873877389e-02 +1.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.8295212444e-02 +1.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0832343750e-01 +1.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3113025422e-01 +1.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5688546428e-01 +1.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8575697600e-01 +1.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1791081106e-01 +2.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5351111111e-01 +2.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9272014450e-01 +2.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3569831289e-01 +2.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8260415794e-01 +2.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3359436800e-01 +2.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8882378472e-01 +2.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4844540978e-01 +2.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1261041150e-01 +2.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8146813156e-01 +2.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.5516609161e-01 +3.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.3385000000e-01 +3.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.1766375839e-01 +3.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0067494684e+00 +3.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1012474385e+00 +3.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2012961902e+00 +3.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3070324653e+00 +3.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4185912320e+00 +3.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5361056921e+00 +3.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6597072871e+00 +3.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7895257055e+00 +4.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9256888889e+00 +4.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0683230389e+00 +4.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2175526240e+00 +4.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3735003857e+00 +4.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5362873458e+00 +4.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7060328125e+00 +4.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8828543876e+00 +4.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0668679726e+00 +4.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2581877760e+00 +4.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4569263194e+00 +5.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6631944444e+00 +5.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8771013195e+00 +5.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0987544462e+00 +5.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3282596663e+00 +5.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5657211680e+00 +5.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8112414931e+00 +5.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0649215431e+00 +5.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3268605865e+00 +5.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5971562649e+00 +5.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8759045999e+00 +6.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1632000000e+00 +6.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4591352667e+00 +6.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7638016018e+00 +6.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0772886135e+00 +6.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.3996843236e+00 +6.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.7310751736e+00 +6.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.0715460320e+00 +6.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.4211802004e+00 +6.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.7800594204e+00 +6.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.1482638805e+00 +7.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.5258722222e+00 +7.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.9129615473e+00 +7.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0309607424e+01 +7.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0715883894e+01 +7.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1131863479e+01 +7.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1557617188e+01 +7.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1993214521e+01 +7.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2438723481e+01 +7.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2894210576e+01 +7.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3359740828e+01 +8.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3835377778e+01 +8.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4321183494e+01 +8.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4817218580e+01 +8.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5323542175e+01 +8.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5840211968e+01 +8.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6367284201e+01 +8.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6904813676e+01 +8.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7452853762e+01 +8.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8011456398e+01 +8.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8580672108e+01 +9.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9160550000e+01 +9.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9751137775e+01 +9.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0352481735e+01 +9.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0964626789e+01 +9.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1587616457e+01 +9.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2221492882e+01 +9.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2866296832e+01 +9.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3522067709e+01 +9.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4188843554e+01 +9.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4866661056e+01 +10.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5555555556e+01 +10.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6255561056e+01 +10.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6966710224e+01 +10.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7689034402e+01 +10.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8422563612e+01 +10.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9167326562e+01 +10.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9923350654e+01 +10.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0690661989e+01 +10.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1469285376e+01 +10.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2259244336e+01 +11.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3060561111e+01 +11.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3873256670e+01 +11.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4697350713e+01 +11.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.5532861683e+01 +11.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6379806768e+01 +11.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.7238201910e+01 +11.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8108061810e+01 +11.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8989399936e+01 +11.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.9882228532e+01 +11.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0786558617e+01 +12.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1702400000e+01 +12.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2629761283e+01 +12.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3568649868e+01 +12.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4519071964e+01 +12.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5481032590e+01 +12.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.6454535590e+01 +12.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7439583632e+01 +12.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8436178217e+01 +12.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9444319687e+01 +12.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0464007230e+01 +13.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1495238889e+01 +13.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2538011564e+01 +13.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3592321024e+01 +13.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4658161911e+01 +13.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5735527746e+01 +13.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6824410938e+01 +13.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7924802788e+01 +13.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9036693498e+01 +13.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0160072176e+01 +13.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1294926844e+01 +14.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2441244444e+01 +14.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3599010845e+01 +14.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4768210846e+01 +14.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5948828191e+01 +14.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7140845568e+01 +14.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8344244618e+01 +14.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9559005943e+01 +14.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0785109112e+01 +14.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2022532665e+01 +14.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.3271254125e+01 +15.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.4531250000e+01 +15.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.5802495792e+01 +15.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.7084966002e+01 +15.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.8378634139e+01 +15.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.9683472724e+01 +15.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.0999453299e+01 +15.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.2326546432e+01 +15.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.3664721725e+01 +15.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.5013947820e+01 +15.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.6374192405e+01 +16.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.7745422222e+01 +16.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -8.9127603072e+01 +16.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.0520699824e+01 +16.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.1924676419e+01 +16.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.3339495879e+01 +16.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.4765120313e+01 +16.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.6201510921e+01 +16.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.7648628006e+01 +16.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -9.9106430976e+01 +16.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0057487835e+02 +17.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0205392778e+02 +17.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0354353602e+02 +17.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0504365898e+02 +17.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0655425170e+02 +17.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0807526837e+02 +17.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.0960666233e+02 +17.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1114838608e+02 +17.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1270039129e+02 +17.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1426262880e+02 +17.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1583504863e+02 +18.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1741760000e+02 +18.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.1901023130e+02 +18.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2061289014e+02 +18.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2222552331e+02 +18.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2384807686e+02 +18.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2548049601e+02 +18.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2712272523e+02 +18.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.2877470823e+02 +18.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3043638795e+02 +18.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3210770658e+02 +19.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3378860556e+02 +19.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3547902558e+02 +19.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3717890662e+02 +19.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.3888818793e+02 +19.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4060680801e+02 +19.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4233470469e+02 +19.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4407181505e+02 +19.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4581807551e+02 +19.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4757342178e+02 +19.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.4933778886e+02 +20.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5111111111e+02 +20.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5289332219e+02 +20.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5468435511e+02 +20.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5648414221e+02 +20.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.5829261517e+02 +20.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6010970503e+02 +20.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6193534221e+02 +20.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6376945646e+02 +20.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6561197693e+02 +20.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6746283214e+02 +21.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.6932195000e+02 +21.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7118925781e+02 +21.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7306468227e+02 +21.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7494814949e+02 +21.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7683958499e+02 +21.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.7873891372e+02 +21.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8064606003e+02 +21.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8256094774e+02 +21.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8448350009e+02 +21.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8641363976e+02 +22.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.8835128889e+02 +22.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9029636909e+02 +22.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9224880142e+02 +22.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9420850644e+02 +22.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9617540415e+02 +22.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -1.9814941406e+02 +22.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0013045519e+02 +22.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0211844602e+02 +22.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0411330458e+02 +22.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0611494837e+02 +23.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.0812329444e+02 +23.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1013825937e+02 +23.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1215975925e+02 +23.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1418770972e+02 +23.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1622202597e+02 +23.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.1826262274e+02 +23.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2030941434e+02 +23.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2236231464e+02 +23.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2442123706e+02 +23.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2648609465e+02 +24.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.2855680000e+02 +24.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3063326532e+02 +24.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3271540240e+02 +24.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3480312266e+02 +24.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3689633712e+02 +24.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.3899495642e+02 +24.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4109889083e+02 +24.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4320805025e+02 +24.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4532234422e+02 +24.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4744168193e+02 +25.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.4956597222e+02 +25.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5169512360e+02 +25.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5382904422e+02 +25.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5596764194e+02 +25.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.5811082428e+02 +25.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6025849844e+02 +25.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6241057132e+02 +25.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6456694953e+02 +25.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6672753938e+02 +25.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.6889224688e+02 +26.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7106097778e+02 +26.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7323363754e+02 +26.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7541013138e+02 +26.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7759036422e+02 +26.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.7977424077e+02 +26.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8196166545e+02 +26.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8415254248e+02 +26.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8634677581e+02 +26.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.8854426920e+02 +26.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9074492616e+02 +27.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9294865000e+02 +27.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9515534383e+02 +27.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9736491054e+02 +27.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -2.9957725284e+02 +27.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0179227326e+02 +27.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0400987413e+02 +27.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0622995763e+02 +27.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.0845242576e+02 +27.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1067718035e+02 +27.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1290412311e+02 +28.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1513315556e+02 +28.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1736417911e+02 +28.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.1959709502e+02 +28.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2183180445e+02 +28.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2406820841e+02 +28.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2630620781e+02 +28.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.2854570345e+02 +28.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3078659604e+02 +28.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3302878618e+02 +28.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3527217439e+02 +29.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3751666111e+02 +29.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.3976214672e+02 +29.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4200853151e+02 +29.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4425571573e+02 +29.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4650359957e+02 +29.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.4875208316e+02 +29.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.5100106661e+02 +29.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.5325044999e+02 +29.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.5550013333e+02 +29.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.5775001667e+02 +30.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6000000000e+02 +30.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6224998333e+02 +30.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6449986667e+02 +30.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6674955001e+02 +30.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.6899893339e+02 +30.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.7124791684e+02 +30.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.7349640043e+02 +30.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.7574428427e+02 +30.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.7799146849e+02 +30.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8023785328e+02 +31.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8248333889e+02 +31.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8472782561e+02 +31.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8697121382e+02 +31.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.8921340396e+02 +31.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.9145429655e+02 +31.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.9369379219e+02 +31.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.9593179159e+02 +31.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -3.9816819555e+02 +31.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0040290498e+02 +31.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0263582089e+02 +32.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0486684444e+02 +32.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0709587689e+02 +32.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.0932281965e+02 +32.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1154757424e+02 +32.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1377004237e+02 +32.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1599012587e+02 +32.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.1820772674e+02 +32.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2042274716e+02 +32.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2263508946e+02 +32.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2484465617e+02 +33.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2705135000e+02 +33.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.2925507384e+02 +33.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3145573080e+02 +33.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3365322419e+02 +33.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3584745752e+02 +33.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.3803833455e+02 +33.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4022575923e+02 +33.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4240963578e+02 +33.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4458986862e+02 +33.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4676636246e+02 +34.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.4893902222e+02 +34.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5110775312e+02 +34.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5327246062e+02 +34.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5543305047e+02 +34.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5758942868e+02 +34.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.5974150156e+02 +34.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.6188917572e+02 +34.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.6403235806e+02 +34.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.6617095578e+02 +34.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.6830487640e+02 +35.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7043402778e+02 +35.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7255831807e+02 +35.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7467765578e+02 +35.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7679194975e+02 +35.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.7890110917e+02 +35.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8100504358e+02 +35.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8310366288e+02 +35.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8519687734e+02 +35.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8728459760e+02 +35.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.8936673468e+02 +36.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9144320000e+02 +36.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9351390535e+02 +36.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9557876294e+02 +36.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9763768536e+02 +36.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -4.9969058566e+02 +36.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0173737726e+02 +36.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0377797403e+02 +36.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0581229028e+02 +36.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0784024075e+02 +36.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.0986174063e+02 +37.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1187670556e+02 +37.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1388505163e+02 +37.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1588669542e+02 +37.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1788155398e+02 +37.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.1986954481e+02 +37.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2185058594e+02 +37.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2382459585e+02 +37.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2579149356e+02 +37.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2775119858e+02 +37.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.2970363091e+02 +38.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3164871111e+02 +38.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3358636024e+02 +38.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3551649991e+02 +38.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3743905226e+02 +38.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.3935393997e+02 +38.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4126108628e+02 +38.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4316041501e+02 +38.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4505185051e+02 +38.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4693531773e+02 +38.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.4881074219e+02 +39.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5067805000e+02 +39.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5253716786e+02 +39.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5438802307e+02 +39.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5623054354e+02 +39.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5806465779e+02 +39.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.5989029497e+02 +39.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6170738483e+02 +39.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6351585779e+02 +39.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6531564489e+02 +39.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6710667781e+02 +40.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.6888888889e+02 +40.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7066221114e+02 +40.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7242657822e+02 +40.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7418192449e+02 +40.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7592818495e+02 +40.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7766529531e+02 +40.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.7939319199e+02 +40.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8111181207e+02 +40.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8282109338e+02 +40.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8452097442e+02 +41.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8621139444e+02 +41.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8789229342e+02 +41.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.8956361205e+02 +41.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9122529177e+02 +41.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9287727477e+02 +41.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9451950399e+02 +41.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9615192314e+02 +41.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9777447669e+02 +41.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -5.9938710986e+02 +41.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0098976870e+02 +42.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0258240000e+02 +42.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0416495137e+02 +42.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0573737120e+02 +42.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0729960871e+02 +42.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.0885161392e+02 +42.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1039333767e+02 +42.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1192473163e+02 +42.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1344574830e+02 +42.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1495634102e+02 +42.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1645646398e+02 +43.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1794607222e+02 +43.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.1942512165e+02 +43.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2089356902e+02 +43.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2235137199e+02 +43.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2379848908e+02 +43.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2523487969e+02 +43.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2666050412e+02 +43.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2807532358e+02 +43.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.2947930018e+02 +43.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3087239693e+02 +44.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3225457778e+02 +44.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3362580759e+02 +44.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3498605218e+02 +44.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3633527827e+02 +44.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3767345357e+02 +44.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.3900054670e+02 +44.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4031652728e+02 +44.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4162136586e+02 +44.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4291503400e+02 +44.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4419750421e+02 +45.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4546875000e+02 +45.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4672874588e+02 +45.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4797746734e+02 +45.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.4921489089e+02 +45.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5044099406e+02 +45.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5165575538e+02 +45.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5285915443e+02 +45.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5405117181e+02 +45.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5523178915e+02 +45.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5640098916e+02 +46.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5755875556e+02 +46.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5870507316e+02 +46.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.5983992782e+02 +46.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6096330650e+02 +46.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6207519721e+02 +46.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6317558906e+02 +46.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6426447225e+02 +46.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6534183809e+02 +46.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6640767898e+02 +46.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6746198844e+02 +47.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6850476111e+02 +47.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.6953599277e+02 +47.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7055568031e+02 +47.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7156382178e+02 +47.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7256041637e+02 +47.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7354546441e+02 +47.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7451896741e+02 +47.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7548092804e+02 +47.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7643135013e+02 +47.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7737023872e+02 +48.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7829760000e+02 +48.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.7921344138e+02 +48.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8011777147e+02 +48.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8101060006e+02 +48.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8189193819e+02 +48.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8276179809e+02 +48.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8362019323e+02 +48.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8446713832e+02 +48.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8530264929e+02 +48.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8612674333e+02 +49.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8693943889e+02 +49.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8774075566e+02 +49.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8853071462e+02 +49.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.8930933801e+02 +49.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9007664935e+02 +49.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9083267344e+02 +49.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9157743639e+02 +49.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9231096560e+02 +49.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9303328978e+02 +49.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9374443894e+02 +50.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9444444444e+02 +50.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9513333894e+02 +50.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9581115645e+02 +50.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9647793229e+02 +50.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9713370317e+02 +50.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9777850712e+02 +50.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9841238354e+02 +50.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9903537321e+02 +50.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -6.9964751826e+02 +50.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0024886222e+02 +51.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0083945000e+02 +51.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0141932789e+02 +51.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0198854360e+02 +51.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0254714624e+02 +51.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0309518632e+02 +51.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0363271580e+02 +51.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0415978803e+02 +51.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0467645783e+02 +51.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0518278142e+02 +51.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0567881651e+02 +52.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0616462222e+02 +52.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0664025917e+02 +52.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0710578942e+02 +52.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0756127652e+02 +52.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0800678548e+02 +52.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0844238281e+02 +52.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0886813652e+02 +52.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0928411611e+02 +52.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.0969039258e+02 +52.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1008703845e+02 +53.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1047412778e+02 +53.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1085173612e+02 +53.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1121994058e+02 +53.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1157881980e+02 +53.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1192845397e+02 +53.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1226892483e+02 +53.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1260031568e+02 +53.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1292271139e+02 +53.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1323619840e+02 +53.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1354086473e+02 +54.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1383680000e+02 +54.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1412409540e+02 +54.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1440284374e+02 +54.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1467313941e+02 +54.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1493507846e+02 +54.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1518875851e+02 +54.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1543427883e+02 +54.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1567174033e+02 +54.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1590124555e+02 +54.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1612289868e+02 +55.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1633680556e+02 +55.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1654307368e+02 +55.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1674181222e+02 +55.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1693313203e+02 +55.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1711714561e+02 +55.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1729396719e+02 +55.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1746371265e+02 +55.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1762649961e+02 +55.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1778244738e+02 +55.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1793167696e+02 +56.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1807431111e+02 +56.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1821047429e+02 +56.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1834029271e+02 +56.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1846389431e+02 +56.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1858140877e+02 +56.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1869296753e+02 +56.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1879870381e+02 +56.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1889875256e+02 +56.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1899325053e+02 +56.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1908233624e+02 +57.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1916615000e+02 +57.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1924483391e+02 +57.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1931853187e+02 +57.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1938738959e+02 +57.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1945155459e+02 +57.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1951117622e+02 +57.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1956640563e+02 +57.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1961739584e+02 +57.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1966430169e+02 +57.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1970727986e+02 +58.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1974648889e+02 +58.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1978208919e+02 +58.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1981424302e+02 +58.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1984311454e+02 +58.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1986886975e+02 +58.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1989167656e+02 +58.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1991170479e+02 +58.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1992912612e+02 +58.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1994411418e+02 +58.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1995684447e+02 +59.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1996749444e+02 +59.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1997624347e+02 +59.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1998327285e+02 +59.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1998876582e+02 +59.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999290757e+02 +59.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999588524e+02 +59.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999788794e+02 +59.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999910674e+02 +59.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999973466e+02 +59.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.1999996675e+02 +60.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +60.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +61.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +62.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +63.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +64.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +65.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +66.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +67.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +68.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +69.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +70.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +71.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +72.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +73.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +74.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +75.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +76.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +77.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +78.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +79.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +80.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +81.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +82.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +83.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +84.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +85.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +86.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +87.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +88.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.1 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.2 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.3 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.4 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.5 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.6 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.7 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.8 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +89.9 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 +90.0 1 5.0000 0.0000 -14.0000 0 0 0 0 0 0 -7.2000000000e+02 diff --git a/examples/torsion_lazy_wave_hangoff_twist.dat b/examples/torsion_lazy_wave_hangoff_twist.dat new file mode 100644 index 0000000..e20300b --- /dev/null +++ b/examples/torsion_lazy_wave_hangoff_twist.dat @@ -0,0 +1,66 @@ +--------------------- CableDyn Input File ------------------------------------ +Lozon Gulf of Mexico 80 m lazy-wave power cable twisted two turns at the hang-off +-- Geometry and properties match lozon_gomex80_power_cable.dat, with an ILLUSTRATIVE torsional +-- stiffness GJ = 50 kN.m^2 (the paper gives none). Both ends are clamped along the untwisted +-- static end directions (FairDecl1 = 171.58 deg and AnchDecl1 = 89.71 deg of the same deck with +-- pinned ends) and restrained in torsion with the reference normal +y, so the untwisted cable +-- carries no torque. The motionFile holds the hang-off in place and rolls its frame about the line from 0 +-- to -720 deg over 60 s (roll column, smooth start), which imposes two turns of twist +-- (Phi = Pretwist B - Pretwist A - roll = +720 deg); the cable then holds the twist for 30 s. +-- Torsion is quasi-static (no torsional inertia): Torq1N1 follows the imposed twist, less the +-- part the cable absorbs by writhing out of its plane (Twist1 = Phi - Theta). +--------------------- LINE TYPES --------------------------------------------- +TypeName Diam MassDenInAir EA BA EI GAs GJ Irt Irn Cd_n Cd_t Ca_n Ca_t +(-) (m) (kg/m) (N) (-) (N-m2) (N) (N-m2) (kg-m) (kg-m) (-) (-) (-) (-) +bare 0.160 36.70 4.69e8 0.0 1.99e4 9.02e7 5.0e4 0.0587 0.1174 1.2 0.1 1.0 0.0 +buoy 0.290 59.53 4.69e8 0.0 1.99e4 9.02e7 5.0e4 0.3129 0.6258 1.2 0.1 1.0 0.0 +--------------------- POINTS ------------------------------------------------ +ID Type X Y Z +(-) (-) (m) (m) (m) +1 Coupled 5.0 0.0 -14.0 +2 Fixed 125.0 0.0 -80.0 +--------------------- LINES ------------------------------------------------- +ID NodeA NodeB Outputs +(-) (-) (-) (-) +1 1 2 r +--------------------- SECTIONS ---------------------------------------------- +LineID LineType Length NumSegs +(-) (-) (m) (-) +1 bare 68.114 56 +1 buoy 50.000 40 +1 bare 52.101 32 +--------------------- END CONNECTIONS --------------------------------------- +-- Ez points from End A (hang-off) to End B (anchor); Nx is the zero-twist reference normal. +LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist +(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg) +1 A Rigid 0.14641 0.0 -0.98922 Rigid 0.0 1.0 0.0 0.0 +1 B Rigid 0.99999 0.0 0.00503 Rigid 0.0 1.0 0.0 0.0 +--------------------- OPTIONS ----------------------------------------------- +9.80665 g - Gravitational acceleration (m/s^2) [default: 9.80665] +1025.0 rhoW - Water density (kg/m^3) [default: 1025] +80.0 WtrDpth - Water depth for the flat seabed (m) [default: absent standalone; host-owned OpenFAST] +1.0e5 kBot - Seabed penalty stiffness base (Pa/m) [default: 1.0e5] +1.0e4 cBot - Seabed normal damping base (Pa-s/m) [default: 1.0e4] +0.4 rhoInf - Generalised-alpha spectral radius {0<=rhoInf<=1} [default: 0.4] +False modified_newton - Tangent-reuse mode {False=full Newton; True=guarded reuse} [default: False] +0.1 dtM - CableDyn internal time step (s) [default: omitted standalone; 0.1 s target OpenFAST] +90.0 TMax - Standalone simulation duration (s): 60 s twist ramp, 30 s hold +data/torsion/hangoff_roll_2turns_60s_dt01.txt motionFile - Held hang-off with the roll history (12th column, deg) +False adaptive_mesh - Automatic finite-EI refinement {False; True} [default: False] +True tensile_safety - Refine and reject local axial compression in this tensile cable +dynamic_solver 1.0e-8 1.0e-14 30 12 - Relative tolerance, absolute tolerance, max iterations, backtracks [defaults shown] +none frictionMu - Seabed friction coefficient {none; value>=0} [default: none] +none current - Current model and velocity (m/s) {none; uniform; profile} [default: none] +none waves - Wave model, height, period, and direction (m, s, deg) {none; airy; jonswap} [default: none] +0 WaterKin - Water-kinematics source {0/none; file; SEASTATE coupled only} [default: no policy] +-- Complete option/default reference: cabledyn_options_reference.dat and doc/options.rst. +--------------------- OUTPUTS ----------------------------------------------- +"FairTen1" +"AnchTen1" +"Torq1N1" +"Twist1" +"Twist1N129" +"BendMom1N1" +"L1N58py" +"Curv1N89" +--------------------- need this line ---------------------------------------- diff --git a/python/cabledyn/builder.py b/python/cabledyn/builder.py index 8654b17..7166674 100644 --- a/python/cabledyn/builder.py +++ b/python/cabledyn/builder.py @@ -89,6 +89,8 @@ ] _END_CONNECTION_KEYWORDS = _PINNED_END_CONNECTIONS | _RIGID_END_CONNECTIONS +# TorsStiffness keywords of the optional END CONNECTIONS torsion columns +_TORSION_KEYWORDS = frozenset({"free", "zero"}) | _RIGID_END_CONNECTIONS Vec3 = tuple[float, float, float] """A three-component vector ``(x, y, z)``.""" @@ -107,7 +109,8 @@ ), ), ("line", re.compile(r"(tdp)([0-9]+)(s|x|y|z|lay|exc)", re.IGNORECASE)), - ("line", re.compile(r"(ten|curv|bendmom)([0-9]+)(n[0-9]+)", re.IGNORECASE)), + ("line", re.compile(r"(ten|curv|bendmom|torq)([0-9]+)(n[0-9]+)", re.IGNORECASE)), + ("line", re.compile(r"(twist)([0-9]+)(n[0-9]+|)", re.IGNORECASE)), ("line", re.compile(r"(l)([0-9]+)(n[0-9]+(?:[pva][xyz]|dec|azi))", re.IGNORECASE)), ("point", re.compile(r"(point|con)([0-9]+)(p[xyz])", re.IGNORECASE)), ("point", re.compile(r"(point)([0-9]+)(f[xyzh])", re.IGNORECASE)), @@ -543,12 +546,22 @@ class EndConnection: Rotational stiffness in N m/rad, ``Pinned``, or ``Rigid``. direction : tuple[float, float, float] Non-zero reference direction (End A to End B convention). + torsion_stiffness : float | str | None + Optional torsional restraint: ``Free``, ``Rigid`` or a stiffness in N m/rad; ``None`` + writes the six-column row (no torsion columns). + normal : tuple[float, float, float] | None + Zero-twist reference normal in the frame of ``direction`` (with ``torsion_stiffness``). + pretwist : float | None + Optional roll of the end frame about ``direction``, in degrees. """ line: Line end: str stiffness: float | str direction: Vec3 + torsion_stiffness: float | str | None = None + normal: Vec3 | None = None + pretwist: float | None = None @dataclass(eq=False) @@ -1906,6 +1919,10 @@ def add_end_connection( end: str, stiffness: float | str, direction: Sequence[float], + *, + torsion_stiffness: float | str | None = None, + normal: Sequence[float] | None = None, + pretwist: float | None = None, ) -> EndConnection: """Add an ``END CONNECTIONS`` row; see :class:`EndConnection`. @@ -1917,8 +1934,10 @@ def add_end_connection( Raises ------ ValueError - If ``end`` is not A or B, that line end already has a row, or - ``direction`` does not hold three values. + If ``end`` is not A or B, that line end already has a row, + ``direction`` or ``normal`` does not hold three values, or the + torsion columns are incomplete (``torsion_stiffness`` and ``normal`` + go together; ``pretwist`` needs them). """ target = self._line(line) letter = _end_letter(end) @@ -1926,8 +1945,26 @@ def add_end_connection( raise ValueError(f"line {target.id} End {letter} already has an end connection") if len(direction) != 3: raise ValueError(f"end-connection direction must be three numbers, got {direction!r}") + if (torsion_stiffness is None) != (normal is None): + raise ValueError("end-connection torsion_stiffness and normal must be given together") + if pretwist is not None and torsion_stiffness is None: + raise ValueError("end-connection pretwist needs torsion_stiffness and normal") + torsion_normal: Vec3 | None = None + if normal is not None: + if len(normal) != 3: + raise ValueError(f"end-connection normal must be three numbers, got {normal!r}") + nx, ny, nz = normal + torsion_normal = (nx, ny, nz) x, y, z = direction - item = EndConnection(target, letter, stiffness, (x, y, z)) + item = EndConnection( + target, + letter, + stiffness, + (x, y, z), + torsion_stiffness=torsion_stiffness, + normal=torsion_normal, + pretwist=pretwist, + ) self._end_connections.append(item) return item @@ -2682,7 +2719,21 @@ def _end_connection_table(self) -> tuple[str, str, list[list[str]]]: else _num(row.stiffness, what) ) direction = _vec(row.direction, 3, f"{what} direction") - rows.append([_int(row.line.id, what), _end_letter(row.end), stiffness, *direction]) + cells = [_int(row.line.id, what), _end_letter(row.end), stiffness, *direction] + if row.torsion_stiffness is not None: + cells.append( + _text(row.torsion_stiffness, what) + if isinstance(row.torsion_stiffness, str) + else _num(row.torsion_stiffness, what) + ) + cells.extend(_vec(row.normal, 3, f"{what} normal")) + if row.pretwist is not None: + cells.append(_num(row.pretwist, f"{what} pretwist")) + rows.append(cells) + if any(row.torsion_stiffness is not None for row in self._end_connections): + header = "LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist" + units = "(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)" + return header, units, rows header = "LineID End Stiffness EzX EzY EzZ" return header, "(-) (-) (N-m/rad) (-) (-) (-)", rows @@ -2890,7 +2941,19 @@ def load(self) -> None: t = row.tokens # Pinned/Free/Zero and Rigid/Infinity/Inf are keywords, not numbers stiffness: float | str = t[2] if t[2].lower() in _END_CONNECTION_KEYWORDS else _f(t[2]) - model.add_end_connection(_native_int(t[0]), t[1], stiffness, _f3(t[3:6])) + if len(t) >= 10: + torsion: float | str = t[6] if t[6].lower() in _TORSION_KEYWORDS else _f(t[6]) + model.add_end_connection( + _native_int(t[0]), + t[1], + stiffness, + _f3(t[3:6]), + torsion_stiffness=torsion, + normal=_f3(t[7:10]), + pretwist=_f(t[10]) if len(t) == 11 else None, + ) + else: + model.add_end_connection(_native_int(t[0]), t[1], stiffness, _f3(t[3:6])) for row in self.rows("EQUIVALENT BUOYANCY"): t = row.tokens model.add_equivalent_buoyancy(t[0], _f(t[1]), _f(t[2])) diff --git a/python/cabledyn/deck.py b/python/cabledyn/deck.py index 6ef7ea9..81b66ec 100644 --- a/python/cabledyn/deck.py +++ b/python/cabledyn/deck.py @@ -260,10 +260,22 @@ def add_section(self, *, line_id: int, line_type: str, length: float, num_segs: self._sections.append(f"{line} {line_type} {_num(length)} {segments}") def add_end_connection( - self, line_id: int, end: str, stiffness: float | str, direction: Sequence[float] + self, + line_id: int, + end: str, + stiffness: float | str, + direction: Sequence[float], + *, + torsion_stiffness: float | str | None = None, + normal: Sequence[float] | None = None, + pretwist: float | None = None, ) -> None: """Add a finite-EI line-end bending connection. + This writer has no torsion columns (``TorsStiffness NxX NxY NxZ Pretwist``) and + no ``GJ`` column, so a torsion argument is refused; build a deck with torsion with + ``cabledyn.builder.DeckModel.add_end_connection``. + Parameters ---------- line_id : int @@ -279,6 +291,8 @@ def add_end_connection( Three finite components of the non-zero end direction in the global frame, following CableDyn's End-A-to-End-B convention; normalized before writing. + torsion_stiffness, normal, pretwist : None + Not supported by this writer; any other value raises ``ValueError``. Raises ------ @@ -286,9 +300,16 @@ def add_end_connection( If ``line_id`` is not an integer. ValueError If ``line_id`` is not positive, ``end`` is not A or B, that end - already has a connection, ``stiffness`` is invalid, or - ``direction`` is not three finite components with a non-zero norm. + already has a connection, ``stiffness`` is invalid, + ``direction`` is not three finite components with a non-zero norm, + or a torsion argument is given. """ + if torsion_stiffness is not None or normal is not None or pretwist is not None: + raise ValueError( + "the simple deck writer has no torsion columns or GJ; build a deck with torsion " + "with cabledyn.builder.DeckModel (add_end_connection(..., torsion_stiffness=..., " + "normal=..., pretwist=...))" + ) key_id = _int(line_id, "line_id") if key_id < 1: raise ValueError("line_id must be positive") diff --git a/python/cabledyn/deck_file.py b/python/cabledyn/deck_file.py index dcefd40..d122ef3 100644 --- a/python/cabledyn/deck_file.py +++ b/python/cabledyn/deck_file.py @@ -125,7 +125,19 @@ "point": ("id", "type", "x", "y", "z", "mass", "vol", "cda", "ca"), "line": ("id", "nodea", "nodeb", "outputs"), "section": ("lineid", "linetype", "length", "numsegs"), - "end_connection": ("lineid", "end", "stiffness", "ezx", "ezy", "ezz"), + "end_connection": ( + "lineid", + "end", + "stiffness", + "ezx", + "ezy", + "ezz", + "torsstiffness", + "nxx", + "nxy", + "nxz", + "pretwist", + ), } _PATH_OPTIONS = { "bathymetryfile", @@ -300,6 +312,10 @@ "cpld": "coupled", } _RIGID_END_CONNECTIONS = frozenset({"rigid", "infinity", "inf"}) +# TorsStiffness keywords of an end free to twist (a numeric 0 is free too) +_FREE_TORSION_KEYWORDS = frozenset({"free", "zero"}) +# Native TORS_NX_PARALLEL_TOL: smallest |Nx x Ez| of unit vectors (about 0.06 degrees off Ez). +_TORSION_NORMAL_PARALLEL_TOL = 1.0e-3 _WATERKIN_FILENAME_LETTERS = frozenset("abcdfghijklmnopqrstuvwxyzABCDFGHIJKLMNOPQRSTUVWXYZ") # MoorDyn-C kinematics files the native reader opens by fixed name in the deck folder # (read_moordyn_c_kinematics): WaveKin 3, WaveKin 7 and Currents 1. @@ -552,6 +568,19 @@ def _table_float(token: str) -> float: return value +def _keyword_column_real(token: str) -> float | None: + """Mirror the native ``parse_real_token`` on a keyword column (END CONNECTIONS stiffnesses). + + The value of a plain real (``E``/``D`` exponent, no ``Q``; quotes removed as the + list-directed READ of a character column does), or ``None`` when the token is not a + number or is NaN, infinite or subnormal. + """ + value = _fortran_real_value(_strip_quotes(token)) + if value is None or not math.isfinite(value) or _is_subnormal(value): + return None + return value + + def _fortran_real_value(token: str) -> float | None: """Return what a native list-directed ``READ (token, *) real`` gives, or ``None``.""" match = _FORTRAN_REAL.fullmatch(token) @@ -629,7 +658,11 @@ def _native_positive_int(text: str, first: int, last: int) -> int: def _parse_line_node_channel(lowered: str) -> tuple[int, int, int, int]: """Mirror the native ``parse_line_node_channel``: (line, node, kind, component).""" last = len(lowered) - for keyword, code in (("ten", 3), ("curv", 5), ("bendmom", 6)): + if lowered.startswith("twist") and last > 5 and "n" not in lowered[5:]: + # Twist: the line's total twist (node 1) + line_id = _native_positive_int(lowered, 6, last) + return (line_id, 1, 11, 0) if line_id > 0 else (line_id, 0, 0, 0) + for keyword, code in (("torq", 9), ("twist", 10), ("ten", 3), ("curv", 5), ("bendmom", 6)): if lowered.startswith(keyword): prefix = len(keyword) separator = lowered.find("n", prefix) @@ -696,7 +729,7 @@ def _channel_identity_key(channel: str) -> str: if touchdown is not None and int(touchdown.group(1)) > 0: component = ("s", "x", "y", "z", "lay", "exc").index(touchdown.group(2)) + 1 return f"tdp:{int(touchdown.group(1))}:{component}" - if lowered.startswith(("ten", "curv", "bendmom", "l")): + if lowered.startswith(("ten", "curv", "bendmom", "l", "torq", "twist")): line_id, node_id, kind, component = _parse_line_node_channel(lowered) if line_id > 0 and node_id > 0 and kind > 0: return f"node:{line_id}:{node_id}:{kind}:{component}" @@ -792,7 +825,8 @@ def _text_columns(section: str, width: int) -> frozenset[int]: if section == "RODS": return frozenset({1, 2, 10}) if section == "END CONNECTIONS": - return frozenset({1, 2}) + # End, Stiffness and the optional TorsStiffness keyword + return frozenset({1, 2, 6}) if width >= 10 else frozenset({1, 2}) if section in {"POINTS", "SECTIONS", "BODIES"}: return frozenset({1}) if section in {"ROD TYPES", "EQUIVALENT BUOYANCY"}: @@ -2384,6 +2418,7 @@ def numeric(token: str, row: DeckRecord, field: str) -> float: # name -> (Diam, MassDenInAir, static EA, EI) as written in LINE TYPES line_type_scales: dict[str, tuple[float, float]] = {} # name -> (BA, max Cd/Ca) line_type_meta: dict[str, tuple[float, float, float, float]] = {} + line_type_gj: dict[str, float] = {} # name -> explicit GJ of a 14-column row, else 0 for row in self.line_types: for index in (1, 2, *range(5, len(row.tokens))): numeric(row.tokens[index], row, f"LINE TYPES column {index + 1}") @@ -2467,6 +2502,9 @@ def numeric(token: str, row: DeckRecord, field: str) -> float: ea_static, ei, ) + line_type_gj[row.tokens[0].lower()] = ( + numeric(row.tokens[7], row, "GJ") if len(row.tokens) == 14 else 0.0 + ) # Syrope keeps its BA_s on the working-curve model (native BA slot = 0) line_type_scales[row.tokens[0].lower()] = ( 0.0 if is_syrope else ba_values[0], @@ -2823,9 +2861,15 @@ def end_key(token: str) -> str: self._check_attachments(line_ids, section_types_by_line, line_type_meta, line_lengths) assigned_end_connections: set[tuple[str, str]] = set() + # line id -> {end: torsionally restrained} from the optional torsion columns + torsion_ends: dict[str, dict[str, bool]] = {} for row in self.end_connections: - if len(row.tokens) != 6: - raise DeckFormatError(f"{self._at(row)}: END CONNECTIONS row needs 6 fields") + if len(row.tokens) not in (6, 10, 11): + raise DeckFormatError( + f"{self._at(row)}: END CONNECTIONS row needs 6 fields " + "(LineID End Stiffness EzX EzY EzZ), or 10 or 11 with the torsion columns " + "(... TorsStiffness NxX NxY NxZ [Pretwist])" + ) try: line_id = str(_native_int(row.tokens[0])) except ValueError as exc: @@ -2849,16 +2893,17 @@ def end_key(token: str) -> str: ) assigned_end_connections.add(key) - stiffness_token = row.tokens[2].lower() + stiffness_token = _strip_quotes(row.tokens[2]).lower() if stiffness_token in _PINNED_END_CONNECTIONS: non_pinned = False elif stiffness_token in _RIGID_END_CONNECTIONS: non_pinned = True else: - stiffness = numeric(row.tokens[2], row, "END CONNECTIONS stiffness") - if stiffness < 0.0: + # native parse_real_token: a plain real (no q exponent), finite, not subnormal + stiffness = _keyword_column_real(row.tokens[2]) + if stiffness is None or stiffness < 0.0: raise DeckFormatError( - f"{self._at(row)}: END CONNECTIONS stiffness must be " + f"{self._at(row)}: END CONNECTIONS stiffness must be finite and " "non-negative, Pinned, or Rigid" ) non_pinned = stiffness > 0.0 @@ -2872,6 +2917,35 @@ def end_key(token: str) -> str: raise DeckFormatError( f"{self._at(row)}: END CONNECTIONS direction must be non-zero" ) + if len(row.tokens) > 6: + # native append_end_connection: TorsStiffness NxX NxY NxZ [Pretwist] + torsion_token = _strip_quotes(row.tokens[6]).lower() + if torsion_token in _FREE_TORSION_KEYWORDS: + restrained = False + elif torsion_token in _RIGID_END_CONNECTIONS: + restrained = True + else: + torsion_k = _keyword_column_real(row.tokens[6]) + if torsion_k is None or torsion_k < 0.0: + raise DeckFormatError( + f"{self._at(row)}: END CONNECTIONS torsional stiffness must be finite " + "and non-negative, Free, or Rigid" + ) + restrained = torsion_k > 0.0 + normal = [ + numeric( + row.tokens[index], + row, + f"END CONNECTIONS reference normal column {index + 1}", + ) + for index in range(7, 10) + ] + if len(row.tokens) == 11: + numeric(row.tokens[10], row, "END CONNECTIONS pretwist") + if restrained: + # a Free end has no torsion frame: its normal is not checked (native) + self._check_torsion_normal(row, normal, direction, norm) + torsion_ends.setdefault(line_id, {})[end] = restrained if non_pinned: has_finite_ei = any( line_type_meta[line_type][3] > 0.0 @@ -2952,6 +3026,18 @@ def end_key(token: str) -> str: ) for row in bodies } + torsion_lines = self._check_torsion_lines( + torsion_ends, + section_types_by_line, + line_type_meta, + line_type_gj, + normalized_line_endpoints, + point_types, + body_types, + option_by_key, + has_tmax, + mixed_route, + ) rod_types = self._rows("ROD TYPES") for row in rod_types: @@ -3605,6 +3691,35 @@ def is_body_kind(kind: str, pinned: bool) -> bool: f"{self._label}: OUTPUT {channel!r} references an unknown line" ) continue + if lowered.startswith(("torq", "twist")): + # native: TorqN, TwistN and Twist on a torsional line + line_number, node_id, kind, _ = _parse_line_node_channel(lowered) + line_id = str(line_number) + if ( + node_id == 0 + and line_id in line_ids + and re.search(r"[0-9]n0(?![0-9])", lowered) is not None + ): + raise DeckFormatError( + f"{self._label}: OUTPUT {channel!r}: node numbers start at 1 = End A " + "(MoorDyn numbers from N0)" + ) + if line_id not in line_ids or kind == 0 or node_id < 1: + raise DeckFormatError( + f"{self._label}: OUTPUT {channel!r}: bad torsion channel (use " + "TorqN, TwistN or Twist, with a known line id)" + ) + if node_id > line_nelems[line_id] + 1: + raise DeckFormatError( + f"{self._label}: OUTPUT {channel!r} node exceeds the line node count" + ) + if line_id not in torsion_lines: + raise DeckFormatError( + f"{self._label}: OUTPUT {channel!r}: line {line_id} is not torsionally " + "restrained at both ends (END CONNECTIONS TorsStiffness), so it carries " + "no torque" + ) + continue if lowered.startswith(("ten", "curv", "bendmom", "l")): # native parse_line_node_channel: Ten/Curv/BendMomN, LN... line_number, node_id, kind, _ = _parse_line_node_channel(lowered) @@ -3630,6 +3745,142 @@ def is_body_kind(kind: str, pinned: bool) -> bool: continue raise DeckFormatError(f"{self._label}: unsupported OUTPUT channel {channel!r}") + def _check_torsion_normal( + self, + row: DeckRecord, + normal: list[float], + direction: list[float], + direction_norm: float, + ) -> None: + """Native ``append_end_connection``: a restrained end's normal is non-zero, not along Ez.""" + normal_norm = math.sqrt(sum(value * value for value in normal)) + if not math.isfinite(normal_norm) or normal_norm <= math.sqrt(sys.float_info.min): + raise DeckFormatError( + f"{self._at(row)}: END CONNECTIONS torsion reference normal " + "(NxX NxY NxZ) must be non-zero" + ) + nx = [value / normal_norm for value in normal] + ez = [value / direction_norm for value in direction] + parallel = math.sqrt( + (nx[1] * ez[2] - nx[2] * ez[1]) ** 2 + + (nx[2] * ez[0] - nx[0] * ez[2]) ** 2 + + (nx[0] * ez[1] - nx[1] * ez[0]) ** 2 + ) + if parallel < _TORSION_NORMAL_PARALLEL_TOL: + raise DeckFormatError( + f"{self._at(row)}: END CONNECTIONS torsion reference normal " + "(NxX NxY NxZ) must not be parallel to the direction Ez" + ) + + def _check_torsion_lines( + self, + torsion_ends: dict[str, dict[str, bool]], + section_types_by_line: dict[str, list[str]], + line_type_meta: dict[str, tuple[float, float, float, float]], + line_type_gj: dict[str, float], + normalized_line_endpoints: dict[str, tuple[str, str]], + point_types: dict[str, str], + body_types: dict[str, str], + option_by_key: dict[str, OptionRecord], + has_tmax: bool, + aggregate_route: bool, + ) -> set[str]: + """Native ``check_torsion_line`` and the driver's torsion scope; the torsional lines. + + A line with a torsional END CONNECTIONS restraint (TorsStiffness Rigid or a + stiffness) must be finite-EI. With one restrained end it carries no torque and is + solved as without the columns. Restrained at both ends, torsion is solved: End B must + be Fixed, every section type needs an explicit GJ > 0, no ATTACHMENTS, End A neither a + rod end nor on a body other than Rigid6. A deck with torsion runs dynamics only with + the force-blended generalised-alpha and takes no modal analysis. The coupled routes + and a standalone mixed EI = 0 + finite-EI deck without bodies (``aggregate_route``, + run on the aggregate) refuse any torsion column, one restrained end included. + """ + attachment_lines = set() + for row in self._rows("ATTACHMENTS"): + try: + attachment_lines.add(str(_native_int(row.tokens[0]))) + except ValueError: + continue + torsion_lines: set[str] = set() + has_torsion_column = False + for line_id, ends in torsion_ends.items(): + if not any(ends.values()): + continue + has_torsion_column = True + if not any(line_type_meta[name][3] > 0.0 for name in section_types_by_line[line_id]): + raise DeckFormatError( + f"{self._label}: torsional END CONNECTIONS (TorsStiffness Rigid or a " + f"stiffness) require a finite-EI line; line {line_id} has EI = 0" + ) + # one restrained end only carries no torque: noted and ignored + if not (ends.get("a", False) and ends.get("b", False)): + continue + end_a, end_b = normalized_line_endpoints[line_id] + if point_types[end_b] != "fixed": + raise DeckFormatError( + f"{self._label}: torsional END CONNECTIONS require a finite-EI line with Fixed " + f"End B; line {line_id} has two moving ends" + ) + for name in section_types_by_line[line_id]: + if not line_type_gj[name] > 0.0: + raise DeckFormatError( + f"{self._label}: line {line_id} is torsionally restrained at both ends: " + f'its LINE TYPES row "{name}" must give an explicit GJ > 0 (the 14-column ' + "row; torsion has no EI/1.3 default)" + ) + if line_id in attachment_lines: + raise DeckFormatError( + f"{self._label}: line {line_id}: torsion is not combined with ATTACHMENTS " + "in this build" + ) + if _rod_point(point_types[end_a]) is not None: + raise DeckFormatError( + f"{self._label}: line {line_id}: a torsional END CONNECTION on a rod end is " + "not supported (a rod has no spin angle in this build)" + ) + body_id = _body_point_id(point_types[end_a]) + if body_id > 0 and body_types.get(str(body_id), "rigid6") != "rigid6": + raise DeckFormatError( + f"{self._label}: line {line_id}: a torsional END CONNECTION on a body needs " + "a Rigid6 body" + ) + torsion_lines.add(line_id) + # native CD_Init_Deck_Aggregate (deck_has_torsion_columns): the coupled routes and a + # standalone mixed EI = 0 + finite-EI deck without bodies refuse any restrained end + if has_torsion_column and (self.caller_driven or aggregate_route): + raise DeckFormatError( + f"{self._label}: torsion is not yet supported in a deck that mixes EI = 0 and " + "finite-EI lines without a BODY, nor in coupled OpenFAST or FAST.Farm runs (with a " + "BODY such a deck runs on the multibody route, which supports torsion); set " + "TorsStiffness Free in every END CONNECTIONS row" + ) + if not torsion_lines: + return torsion_lines + tmax_row = option_by_key.get("tmax") + dynamic = has_tmax and tmax_row is not None and _native_float(tmax_row.values[0]) > 0.0 + blend_row = option_by_key.get("alpha_force_blend") + force_blend = blend_row is None or blend_row.values[0].lower() in { + "true", + "t", + "yes", + "y", + "on", + "1", + } + if dynamic and not force_blend: + raise DeckFormatError( + f"{self._label}: torsion in a dynamic run needs the force-blended " + "generalised-alpha (OPTION alpha_force_blend True)" + ) + modes_row = option_by_key.get("n_modes") + if modes_row is not None and round(_native_float(modes_row.values[0])) > 0: + raise DeckFormatError( + f"{self._label}: modal analysis (OPTION nModes) of a line with torsion is not " + "yet supported" + ) + return torsion_lines + def _line_type_fields(self, record: DeckRecord) -> tuple[str, ...]: fields = _FIELDS["line_type"] if len(record.tokens) == 14: @@ -4023,7 +4274,9 @@ def set_end_connection(self, line_id: int, end: str, **changes: object) -> None: """Edit one ``END CONNECTIONS`` row selected by line id and end. Keyword names are column names, case-insensitive: ``lineid``, ``end``, - ``stiffness``, ``ezx``, ``ezy``, and ``ezz``. + ``stiffness``, ``ezx``, ``ezy``, ``ezz`` and, on a row that has the torsion + columns (10 or 11 values), ``torsstiffness``, ``nxx``, ``nxy``, ``nxz`` and + ``pretwist``. Parameters ---------- @@ -4033,7 +4286,9 @@ def set_end_connection(self, line_id: int, end: str, **changes: object) -> None: ``"A"`` or ``"B"``. **changes : object New column values: ``stiffness`` in N m/rad (or ``Pinned`` / - ``Rigid``) and dimensionless direction components. + ``Rigid``), dimensionless direction components, ``torsstiffness`` in + N m/rad (or ``Free`` / ``Rigid``), the reference normal components and + ``pretwist`` in degrees. Keywords are written unquoted. Raises ------ diff --git a/python/cabledyn/ranges.py b/python/cabledyn/ranges.py index 7d13904..a9d81c6 100644 --- a/python/cabledyn/ranges.py +++ b/python/cabledyn/ranges.py @@ -166,7 +166,8 @@ class RangeGraph: line (a per-line file whose name has no ``.Line.`` part). quantity : str ``"tension"``, ``"curvature"``, ``"bend_moment"``, for a range file - also ``"declination"`` and ``"clearance"``, or, for an element table, + also ``"declination"``, ``"clearance"``, ``"torque"`` and ``"twist"``, + or, for an element table, ``"axial_resultant"``. unit : str | None Unit of the quantity: ``N``, ``1/m``, ``N-m``, ``deg``, or ``m``. @@ -427,6 +428,8 @@ def static_range_graph(profile: StaticProfile, line_id: int, quantity: str) -> R "bend_moment": ("BendMoment", "N-m"), "declination": ("Declination", "deg"), "clearance": ("Clearance", "m"), + "torque": ("Torque", "N-m"), + "twist": ("Twist", "deg"), } _RANGE_TITLE = re.compile( r"CableDyn range graph \(line ([1-9][0-9]*); ([1-9][0-9]*) samples from t =\s*(\S+) s to " @@ -453,8 +456,9 @@ def read_range_graphs(source: str | os.PathLike[str] | OutputTable) -> dict[str, ``.Line.range.out`` is written by the driver for a line with the LINES ``Outputs`` flag ``r``: the minimum, maximum, and mean over the output times of the range window of the node tension, curvature, bend - moment, and declination, and the seabed clearance when the deck has a - seabed, at every node against its arc length. + moment, and declination, the seabed clearance when the deck has a + seabed, and the torque and twist from End A when the line is restrained + in torsion at both ends, at every node against its arc length. Parameters ---------- @@ -465,7 +469,8 @@ def read_range_graphs(source: str | os.PathLike[str] | OutputTable) -> dict[str, ------- dict[str, RangeGraph] ``"tension"``, ``"curvature"``, ``"bend_moment"``, ``"declination"``, - and, with a seabed, ``"clearance"``. + with a seabed ``"clearance"``, and with torsion ``"torque"`` and + ``"twist"``. Raises ------ @@ -504,7 +509,7 @@ def read_range_graph(source: str | os.PathLike[str] | OutputTable, quantity: str The range file ``.Line.range.out``, or its table. quantity : str ``"tension"``, ``"curvature"``, ``"bend_moment"``, ``"declination"``, - or ``"clearance"``. + ``"clearance"``, ``"torque"`` or ``"twist"``. Returns ------- @@ -516,7 +521,8 @@ def read_range_graph(source: str | os.PathLike[str] | OutputTable, quantity: str Raises ------ KeyError - If the file has no such quantity (clearance needs a seabed). + If the file has no such quantity (clearance needs a seabed, torque and + twist a line with torsion). ValueError If ``quantity`` is unknown or the table is not a range file. """ diff --git a/python/cabledyn/results.py b/python/cabledyn/results.py index a1fef66..adcfaa7 100644 --- a/python/cabledyn/results.py +++ b/python/cabledyn/results.py @@ -55,6 +55,8 @@ def _native_channel_unit(channel: str) -> str | None: (r"Ten[1-9][0-9]*N[1-9][0-9]*", "N"), (r"Curv[1-9][0-9]*N[1-9][0-9]*", "1/m"), (r"BendMom[1-9][0-9]*N[1-9][0-9]*", "N-m"), + (r"Torq[1-9][0-9]*N[1-9][0-9]*", "N-m"), + (r"Twist[1-9][0-9]*(?:N[1-9][0-9]*)?", "deg"), (r"L[1-9][0-9]*N[1-9][0-9]*p[xyz]", "m"), (r"L[1-9][0-9]*N[1-9][0-9]*v[xyz]", "m/s"), (r"L[1-9][0-9]*N[1-9][0-9]*a[xyz]", "m/s^2"), diff --git a/python/tests/test_checks_ranges.py b/python/tests/test_checks_ranges.py index fb7eac2..02fa91f 100644 --- a/python/tests/test_checks_ranges.py +++ b/python/tests/test_checks_ranges.py @@ -108,11 +108,14 @@ def test_range_graph_errors(files): element_range_graph(files["elements"], 1, "tension") -def _write_range_file(path, clearance=True): +def _write_range_file(path, clearance=True, torsion=False): names = ["Tension", "Curvature", "BendMoment", "Declination"] + ( ["Clearance"] if clearance else [] ) units = ["(N)", "(1/m)", "(N.m)", "(deg)", "(m)"][: len(names)] + if torsion: + names += ["Torque", "Twist"] + units += ["(N.m)", "(deg)"] header = ["Node", "ArcLength"] + [f"{n}{s}" for n in names for s in ("Min", "Max", "Mean")] unit_row = ["(-)", "(m)"] + [u for u in units for _ in range(3)] rows = [] @@ -149,6 +152,25 @@ def test_read_range_graphs_from_a_range_file(tmp_path): assert read_range_graph(path, "declination").unit == "deg" +def test_read_range_graphs_with_torsion(tmp_path): + path = _write_range_file(tmp_path / "tw.Line3.range.out", clearance=False, torsion=True) + graphs = read_range_graphs(path) + assert tuple(graphs) == ( + "tension", + "curvature", + "bend_moment", + "declination", + "torque", + "twist", + ) + assert graphs["torque"].unit == "N.m" + assert np.allclose(graphs["torque"].minimum, [51.0, 52.0, 53.0]) + twist = read_range_graph(path, "twist") + assert twist.unit == "deg" and np.allclose(twist.maximum, [63.0, 64.0, 65.0]) + with pytest.raises(KeyError, match="no torque"): + read_range_graph(_write_range_file(tmp_path / "plain.Line3.range.out"), "torque") + + def test_read_range_graph_errors(files, tmp_path): path = _write_range_file(tmp_path / "flat.Line3.range.out", clearance=False) assert "clearance" not in read_range_graphs(path) diff --git a/python/tests/test_deck_file.py b/python/tests/test_deck_file.py index 241e41b..5b25cc8 100644 --- a/python/tests/test_deck_file.py +++ b/python/tests/test_deck_file.py @@ -123,7 +123,7 @@ def test_end_connections_round_trip_edit_and_cli_inventory(tmp_path, capsys): ("row", "message"), [ ("1 C 2.0e4 -0.25 0.0 -0.97", "End must be A or B"), - ("1 A -1 -0.25 0.0 -0.97", "stiffness must be non-negative"), + ("1 A -1 -0.25 0.0 -0.97", "stiffness must be finite and non-negative, Pinned, or Rigid"), ("1 A nan -0.25 0.0 -0.97", "stiffness must be finite"), ("1 A 2.0e4 0 0 0", "direction must be non-zero"), ("9 A 2.0e4 -0.25 0.0 -0.97", "references an undefined line"), diff --git a/python/tests/test_deck_native_parity.py b/python/tests/test_deck_native_parity.py index 8da0362..14dc66e 100644 --- a/python/tests/test_deck_native_parity.py +++ b/python/tests/test_deck_native_parity.py @@ -10,7 +10,9 @@ import hashlib import json +import os import re +import subprocess from pathlib import Path import numpy as np @@ -757,3 +759,402 @@ def test_coupled_route_takes_no_motion_file_and_needs_a_line(): ) DeckFile.from_text(no_lines) _rejects(no_lines, "needs at least one line", caller_driven=True) + + +# --------------------------------------------------------------------------- torsion + +# A straight finite-EI line clamped at a Coupled and a Fixed point with the optional torsion +# columns of END CONNECTIONS (the pure-torsion deck of test_torsion_deck). +_TORSION = f"""\ +{_BAR} CableDyn Input File {_BAR} +in-memory fixture: a straight finite-EI line restrained in torsion at both ends +{_BAR} LINE TYPES {_BAR} +Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat +cab 0.2 32.2 1.0e7 0.0 1.0e6 1.0e8 5.0e4 1.0 1.0 0.0 0.0 0.0 0.0 +{_BAR} POINTS {_BAR} +ID Type X Y Z Mass Vol CdA Ca +1 Coupled 0.0 0.0 -50.0 0 0 0 0 +2 Fixed 100.0 0.0 -50.0 0 0 0 0 +{_BAR} LINES {_BAR} +ID NodeA NodeB Outputs +1 1 2 - +{_BAR} SECTIONS {_BAR} +LineID LineType Length NumSegs +1 cab 100.0 40 +{_BAR} END CONNECTIONS {_BAR} +LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist +1 A Rigid 1 0 0 Rigid 0 0 1 0 +1 B Rigid 1 0 0 Rigid 0 0 1 720 +{_BAR} OPTIONS {_BAR} +9.80665 g +1025.0 rhoW +50.0 WtrDpth +0.1 dtM +0.0 TMax +{_BAR} OUTPUTS {_BAR} +Torq1N1 +Twist1N41 +Twist1 +{_BAR} need this line {_BAR} +""" +_TORSION_ROW_A = "1 A Rigid 1 0 0 Rigid 0 0 1 0\n" + + +def test_torsion_end_connection_columns_are_accepted(): + deck = DeckFile.from_text(_TORSION) + assert len(deck.end_connections[0].tokens) == 10 + 1 + # ten columns (no pretwist), keywords and a finite stiffness, mixed with six-column rows + DeckFile.from_text(_edit(_TORSION, (_TORSION_ROW_A, "1 A Rigid 1 0 0 1.0e5 0 0 1\n"))) + DeckFile.from_text(_edit(_TORSION, (_TORSION_ROW_A, "1 A Rigid 1 0 0 Inf 0 1 1 -45\n"))) + six = _edit( + _TORSION, + (_TORSION_ROW_A, "1 A Rigid 1 0 0\n"), + ("Torq1N1\nTwist1N41\nTwist1\n", "FairTen1\n"), + ) + DeckFile.from_text(six) + + +@pytest.mark.parametrize( + ("row", "message"), + [ + ("1 A Rigid 1 0 0 Rigid 0 0\n", "or 10 or 11 with the torsion columns"), + ("1 A Rigid 1 0 0 Rigid 0 0 1 0 0\n", "or 10 or 11 with the torsion columns"), + ("1 A Rigid 1 0 0 Stiff 0 0 1 0\n", "torsional stiffness must be finite and non-negative"), + ("1 A Rigid 1 0 0 -1.0 0 0 1 0\n", "torsional stiffness must be finite and non-negative"), + ("1 A Rigid 1 0 0 Rigid 0 0 0 0\n", r"reference normal \(NxX NxY NxZ\) must be non-zero"), + ("1 A Rigid 1 0 0 Rigid 2 0 0 0\n", "must not be parallel to the direction Ez"), + ("1 A Rigid 1 0 0 Rigid 0 x 1 0\n", "reference normal column 9 must be numeric"), + ("1 A Rigid 1 0 0 Rigid 0 0 1 nan\n", "is not a finite number"), + ], +) +def test_torsion_end_connection_columns_follow_the_native_rules(row, message): + _rejects(_edit(_TORSION, (_TORSION_ROW_A, row)), message) + + +def test_torsion_line_rules_follow_the_native_checks(): + # torsion has no EI/1.3 default: the 10-column LINE TYPES row has no GJ + no_gj = _edit( + _TORSION, + ( + "Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat\n", + "Name Diam Mass EA BA EI Cdn Cdt Can Cat\n", + ), + ( + "cab 0.2 32.2 1.0e7 0.0 1.0e6 1.0e8 5.0e4 1.0 1.0 0.0 0.0 0.0 0.0", + "cab 0.2 32.2 1.0e7 0.0 1.0e6 0 0 0 0", + ), + ) + _rejects(no_gj, "must give an explicit GJ > 0") + ei0 = _edit( + _TORSION, + ("1.0e7 0.0 1.0e6 1.0e8", "1.0e7 0.0 0.0 1.0e8"), + ("1 A Rigid 1 0 0 Rigid", "1 A Pinned 1 0 0 Rigid"), + ("1 B Rigid 1 0 0 Rigid", "1 B Pinned 1 0 0 Rigid"), + ("Torq1N1\nTwist1N41\nTwist1\n", "FairTen1\n"), + ) + _rejects(ei0, "require a finite-EI line; line 1 has EI = 0") + # one restrained end is accepted (it carries no torque), but takes no torsion channel + one_end = _edit(_TORSION, (_TORSION_ROW_A, "1 A Rigid 1 0 0 Free 0 0 1 0\n")) + _rejects(one_end, "is not torsionally restrained at both ends") + DeckFile.from_text(_edit(one_end, ("Torq1N1\nTwist1N41\nTwist1\n", "FairTen1\n"))) + # the dynamic run needs the force blend; modal analysis and the coupled routes refuse torsion + dynamic = _edit(_TORSION, ("0.0 TMax\n", "10.0 TMax\n")) + DeckFile.from_text(dynamic) + _rejects( + _options(dynamic, "False alpha_force_blend"), "needs the force-blended generalised-alpha" + ) + _rejects(_options(_TORSION, "3 nModes"), "modal analysis .* with torsion is not yet supported") + _rejects(_TORSION, "nor in coupled OpenFAST or FAST.Farm runs", caller_driven=True) + # the coupled routes refuse one restrained end too (any torsion column), after the line rules + _rejects(one_end, "nor in coupled OpenFAST or FAST.Farm runs", caller_driven=True) + _rejects(ei0, "require a finite-EI line; line 1 has EI = 0", caller_driven=True) + + +# The pure-torsion deck dynamic (TMax > 0), for the rules that need a dynamic run. +_TORSION_DYN = _edit(_TORSION, ("0.0 TMax", "1.0 TMax")) +_NO_TORSION_OUTPUTS = ("Torq1N1\nTwist1N41\nTwist1\n", "FairTen1\n") + + +def _torsion_body(body_row: str) -> str: + return _edit( + _TORSION_DYN, + ( + f"{_BAR} POINTS {_BAR}", + f"{_BAR} BODIES {_BAR}\n" + "ID Type X Y Z Roll Pitch Yaw Mass Vol C33 C44 C55 CdA Ca Ixx Iyy Izz\n" + f"{body_row}\n{_BAR} POINTS {_BAR}", + ), + ("1 Coupled 0.0 0.0 -50.0", "1 Body1 0.0 0.0 0.0"), + ) + + +def _torsion_two_moving(row_a: str) -> str: + return _edit( + _TORSION_DYN, + ("2 Fixed 100.0 0.0 -50.0 0 0 0 0", "2 Free 100.0 0.0 -50.0 100 0 0 0"), + (_TORSION_ROW_A, row_a), + ("1 B Rigid 1 0 0 Rigid", "1 B Pinned 1 0 0 Rigid"), + ) + + +def test_torsion_scope_rules_follow_the_native_checks(): + # End A on a Rigid6 body is supported, on a Point3 body or a rod end it stops by name + DeckFile.from_text( + _torsion_body("1 Rigid6 0 0 -50 0 0 0 1.0e5 97.56 0 0 0 0 0 1.0e3 1.0e3 1.0e3") + ) + _rejects( + _torsion_body("1 Point3 0 0 -50 0 0 0 1.0e5 97.56 0 0 0 0 0"), + "a torsional END CONNECTION on a body needs a Rigid6 body", + ) + rod_end = _edit( + _TORSION_DYN, + ( + f"{_BAR} POINTS {_BAR}", + f"{_BAR} ROD TYPES {_BAR}\nName Diam Mass Cd Ca CdEnd CaEnd\n" + "spar 1.0 300.0 0.8 1.0 0.0 0.0\n" + f"{_BAR} RODS {_BAR}\nID RodType Type XA YA ZA XB YB ZB NumSegs Outputs\n" + f"1 spar Fixed 0 0 -50 0 0 -40 2 -\n{_BAR} POINTS {_BAR}", + ), + ("1 1 2 -", "1 R1A 2 -"), + ("1 Coupled 0.0 0.0 -50.0 0 0 0 0\n", ""), + ) + _rejects(rod_end, "a torsional END CONNECTION on a rod end is not supported") + attachments = _edit( + _TORSION, + ( + f"{_BAR} OPTIONS {_BAR}", + f"{_BAR} ATTACHMENTS {_BAR}\nLineID ArcLength Mass Volume CdA Ca\n" + f"1 50.0 10.0 0.0 0.0 0.0\n{_BAR} OPTIONS {_BAR}", + ), + ) + _rejects(attachments, "torsion is not combined with ATTACHMENTS") + # restrained at both ends, End B must be Fixed + _rejects( + _torsion_two_moving("1 A Pinned 1 0 0 Rigid 0 0 1 0\n"), + "torsional END CONNECTIONS require a finite-EI line with Fixed End B; line 1 has two " + "moving ends", + ) + # one restrained end only, or none, is ignored: the same verdict as the six-column rows + one_end = _edit(_torsion_two_moving("1 A Pinned 1 0 0 Free 0 0 1 0\n"), _NO_TORSION_OUTPUTS) + six = _edit( + _torsion_two_moving("1 A Pinned 1 0 0\n"), + ("1 B Pinned 1 0 0 Rigid 0 0 1 720", "1 B Pinned 1 0 0"), + _NO_TORSION_OUTPUTS, + ) + DeckFile.from_text(six) + DeckFile.from_text(one_end) + both_free = _edit( + _TORSION, + (_TORSION_ROW_A, "1 A Rigid 1 0 0 Free 0 0 1 0\n"), + ("1 B Rigid 1 0 0 Rigid 0 0 1 720", "1 B Rigid 1 0 0 Free 0 0 1 720"), + ) + _rejects(both_free, "is not torsionally restrained at both ends") + DeckFile.from_text(_edit(both_free, _NO_TORSION_OUTPUTS)) + + +def test_torsion_on_a_standalone_mixed_deck_is_refused(): + # a mixed EI = 0 + finite-EI deck without bodies runs on the aggregate, which has no torsion + gj_cable = _edit( + _MIXED, + ( + "Name Diam Mass EA BA EI Cdn Cdt Can Cat\n", + "Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat\n", + ), + ( + "chain 0.252 390.0 1.674e9 -1.0 0.0 1.37 0.64 1.0 0.0\n", + "chain 0.252 390.0 1.674e9 -1.0 0.0 0 0 0 0 1.37 0.64 1.0 0.0\n", + ), + ( + "cable 0.2 60.0 4.0e8 0.0 1.0e4 1.2 0.1 1.0 0.0\n", + "cable 0.2 60.0 4.0e8 0.0 1.0e4 1.0e8 1.0e4 1.0 1.0 1.2 0.1 1.0 0.0\n", + ), + ) + DeckFile.from_text(gj_cable) + for rows in ( + ("2 A Rigid -1 0 0 Rigid 0 0 1 0", "2 B Rigid -1 0 0 Rigid 0 0 1 90"), + ("2 A Rigid -1 0 0 Rigid 0 0 1 0", "2 B Rigid -1 0 0 Free 0 0 1 0"), + ): + text = _section( + gj_cable, + "END CONNECTIONS", + "LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist", + *rows, + ) + _rejects(text, "mixes EI = 0 and finite-EI lines without a BODY") + _rejects(_options(text, "10.0 TMax"), "mixes EI = 0 and finite-EI lines without a BODY") + + +@pytest.mark.parametrize( + ("stiffness", "accepted"), + [ + ("Inf", True), + ("Infinity", True), + ("RIGID", True), + ('"Rigid"', True), + ("Zero", True), + ("-0", True), + ("2.5d4", True), + ("+Inf", False), + ("1e-320", False), + ("1.0q5", False), + ("Pinned", False), + ], +) +def test_torsion_stiffness_keywords_and_numbers_follow_the_native_reader(stiffness, accepted): + text = _edit(_TORSION, (_TORSION_ROW_A, f"1 A Rigid 1 0 0 {stiffness} 0 1 1 -45\n")) + if stiffness.lower() in {"zero", "-0"}: + text = _edit(text, _NO_TORSION_OUTPUTS) + if accepted: + DeckFile.from_text(text) + else: + _rejects(text, "torsional stiffness must be finite and non-negative, Free, or Rigid") + + +@pytest.mark.parametrize("stiffness", ["1.0q5", "1e-320", "+Inf", "NaN"]) +def test_bending_stiffness_numbers_follow_the_native_reader(stiffness): + _rejects( + _edit(_TORSION, (_TORSION_ROW_A, f"1 A {stiffness} 1 0 0 Rigid 0 1 1 -45\n")), + "END CONNECTIONS stiffness must be finite and non-negative, Pinned, or Rigid", + ) + + +def test_builders_write_or_refuse_the_torsion_columns(): + from cabledyn.deck import DeckWriter + + # DeckModel writes a 10-column row (no pretwist) that reads back the same + model = DeckModel.from_text(_edit(_TORSION, (_TORSION_ROW_A, ""), _NO_TORSION_OUTPUTS)) + model.add_end_connection( + 1, "A", "Rigid", (1.0, 0.0, 0.0), torsion_stiffness=2.5e4, normal=(0, 1, 1) + ) + text = model.to_text() + row = next(r for r in DeckFile.from_text(text).end_connections if r.tokens[1] == "A") + assert len(row.tokens) == 10 and float(row.tokens[6]) == 2.5e4 + # DeckFile edits the torsion columns of a row in place, keywords unquoted + deck = DeckFile.from_text(_TORSION) + deck.set_end_connection(1, "B", torsstiffness="Infinity", pretwist=90.0) + row_b = next(r for r in deck.end_connections if r.tokens[1] == "B") + assert row_b.tokens[6] == "Infinity" and float(row_b.tokens[10]) == 90.0 + # the simple writer has no torsion columns and says so + with pytest.raises(ValueError, match="no torsion columns"): + DeckWriter().add_end_connection(1, "A", "Rigid", (1.0, 0.0, 0.0), torsion_stiffness="Rigid") + + +def test_a_free_torsion_end_takes_any_reference_normal(): + for normal in ("0 0 0", "1 0 0"): + DeckFile.from_text( + _edit( + _TORSION, + (_TORSION_ROW_A, f"1 A Rigid 1 0 0 Free {normal} 0\n"), + _NO_TORSION_OUTPUTS, + ) + ) + + +_PARITY_DRIVER = ( + os.environ.get("CABLEDYN_TEST_DRIVER", "").strip() + or os.environ.get("CABLEDYN_DRIVER", "").strip() +) + + +@pytest.mark.skipif( + not _PARITY_DRIVER or not Path(_PARITY_DRIVER).is_file(), + reason="set CABLEDYN_TEST_DRIVER to a built native driver to run the native torsion parity", +) +@pytest.mark.parametrize( + "row_a", + [ + "1 A Rigid 1 0 0 Inf 0 1 1 -45", + "1 A Rigid 1 0 0 Infinity 0 1 1 -45", + '1 A Rigid 1 0 0 "Rigid" 0 1 1 -45', + "1 A Rigid 1 0 0 +Inf 0 1 1 -45", + "1 A Rigid 1 0 0 1e-320 0 1 1 -45", + "1 A Rigid 1 0 0 1.0q5 0 1 1 -45", + "1 A 1.0q5 1 0 0 Rigid 0 1 1 -45", + "1 A Rigid 1 0 0 Rigid 0 0 1", + ], +) +def test_torsion_rows_get_the_native_verdict(row_a, tmp_path): + text = _edit(_TORSION, (_TORSION_ROW_A, f"{row_a}\n")) + try: + DeckFile.from_text(text) + python_accepts = True + except DeckFormatError: + python_accepts = False + deck = tmp_path / "torsion.dat" + deck.write_text(text, encoding="utf-8") + completed = subprocess.run( + [_PARITY_DRIVER, str(deck), str(tmp_path / "torsion")], + capture_output=True, + text=True, + cwd=tmp_path, + timeout=600, + check=False, + ) + # exit code 1 is an input refusal; 0 a run and 2 a solve failure after validation + assert python_accepts == (completed.returncode != 1), completed.stdout + completed.stderr + + +@pytest.mark.parametrize( + ("channel", "message"), + [ + ("Torq1N42", "node exceeds the line node count"), + ("Torq1N0", "node numbers start at 1"), + ("Torq2N1", "bad torsion channel"), + ("Twist0", "bad torsion channel"), + ("TwistN1", "bad torsion channel"), + ], +) +def test_torsion_channels_follow_the_native_rules(channel, message): + _rejects(_edit(_TORSION, ("Twist1\n", f"Twist1\n{channel}\n")), message) + + +def test_torsion_channels_are_one_identity_per_quantity(): + _rejects(_edit(_TORSION, ("Twist1\n", "Twist1\nTORQ01N1\n")), "duplicate") + DeckFile.from_text(_edit(_TORSION, ("Twist1\n", "Twist1\nTwist1N1\nTorq1N41\n"))) + + +def test_model_round_trips_the_torsion_columns(): + model = DeckModel.from_text(_TORSION) + row_a, row_b = model.end_connections + assert (row_a.torsion_stiffness, row_a.normal, row_a.pretwist) == ( + "Rigid", + (0.0, 0.0, 1.0), + 0.0, + ) + assert row_b.pretwist == 720.0 + text = model.to_text() + assert "TorsStiffness" in text and "Pretwist" in text + again = DeckModel.from_text(text) + assert [(r.torsion_stiffness, r.normal, r.pretwist) for r in again.end_connections] == [ + (r.torsion_stiffness, r.normal, r.pretwist) for r in model.end_connections + ] + DeckFile.from_text(text) + # a six-column row keeps six columns; the builder checks the torsion arguments + plain = DeckModel.from_text( + _edit( + _TORSION, + (_TORSION_ROW_A, "1 A Rigid 1 0 0\n"), + ("1 B Rigid 1 0 0 Rigid 0 0 1 720\n", ""), + ("Torq1N1\nTwist1N41\nTwist1\n", "FairTen1\n"), + ) + ) + assert "TorsStiffness" not in plain.to_text() + with pytest.raises(ValueError, match="must be given together"): + plain.add_end_connection(1, "B", "Rigid", (1.0, 0.0, 0.0), torsion_stiffness="Rigid") + with pytest.raises(ValueError, match="pretwist needs"): + plain.add_end_connection(1, "B", "Rigid", (1.0, 0.0, 0.0), pretwist=10.0) + with pytest.raises(ValueError, match="normal must be three numbers"): + plain.add_end_connection( + 1, "B", "Rigid", (1.0, 0.0, 0.0), torsion_stiffness=1.0e4, normal=(0.0, 1.0) + ) + added = plain.add_end_connection( + 1, + "B", + "Rigid", + (1.0, 0.0, 0.0), + torsion_stiffness=1.0e4, + normal=(0.0, 0.0, 1.0), + pretwist=90.0, + ) + assert added.torsion_stiffness == 1.0e4 + assert "TorsStiffness" in plain.to_text() diff --git a/python/tests/test_results.py b/python/tests/test_results.py index 070f91a..315862b 100644 --- a/python/tests/test_results.py +++ b/python/tests/test_results.py @@ -302,6 +302,9 @@ def test_time_must_be_strictly_increasing(tmp_path): ("Ten3N4", "N"), ("Curv3N4", "1/m"), ("BendMom3N4", "N-m"), + ("Torq3N4", "N-m"), + ("Twist3N4", "deg"), + ("Twist3", "deg"), ("L3N4px", "m"), ("L3N4vy", "m/s"), ("L3N4az", "m/s^2"), diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index 2a18209..e944de0 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -97,7 +97,8 @@ MODULE CableDyn_DeckDriver CD_HermiteCable_Dyn_Set_Friction_Anchors, & CD_HermiteCable_Dyn_Set_Friction_Axial, & CD_HermiteCable_Dyn_Max_Step_Rotation, CD_HermiteCable_Dyn_Energy, & - CD_HermiteCable_Attachment_Drag + CD_HermiteCable_Attachment_Drag, CD_HermiteCable_Dyn_Torsion_State + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Compliance, CD_HTORS_MAX_STEP USE CableDyn_EndConnection, ONLY: CD_ENDCONN_PINNED, CD_ENDCONN_FINITE, CD_ENDCONN_RIGID USE CableDyn_Vessel, ONLY: CD_VesselRAOType, CD_Vessel_Euler_DCM, CD_Vessel_Euler_Angular, & CD_Vessel_Point_Kinematics, CD_Vessel_RAO_Set, CD_Vessel_RAO_Eval, & @@ -108,7 +109,7 @@ MODULE CableDyn_DeckDriver CD_HFMF_Set_Tensile_Monitor, CD_HFMF_Get_Tensile_Diagnostics, & CD_HFMF_Get_Recovery_Diagnostics, & CD_HFMF_Set_Recovery_Max_Substeps, & - CD_HFMF_Set_EndConnection, CD_HFMF_Set_Attachments, & + CD_HFMF_Set_EndConnection, CD_HFMF_Set_Attachments, CD_HFMF_Set_Torsion, & CD_HFMF_Set_Held_Fluid, CD_HFMF_Set_Contact, CD_HFMF_NNodes, & CD_HFMF_Refresh_Acceleration, & CD_HFMF_GetNodePositions, CD_HFMF_UpdateStates, & @@ -132,7 +133,8 @@ MODULE CableDyn_DeckDriver CD_Range_Commit_Sample, CD_Range_Clearance, CD_Range_TDP_Evaluate, & CD_Range_TDP_Reference, CD_Range_Write_Files, CD_Is_TDP_Channel, & CD_Parse_TDP_Channel, CD_RANGE_OK, CD_RANGE_BADINPUT, CD_RQ_TENSION, & - CD_RQ_CURVATURE, CD_RQ_BEND, CD_RQ_DECLINATION, CD_RQ_CLEARANCE, CD_TDP_N + CD_RQ_CURVATURE, CD_RQ_BEND, CD_RQ_DECLINATION, CD_RQ_CLEARANCE, CD_TDP_N, & + CD_RQ_TORQUE, CD_RQ_TWIST USE CableDyn_Conventions, ONLY: CD_Body_Rotation USE CableDyn_RigidKinematics, ONLY: CD_Rigid_Advance_Newmark, CD_RIGID_OK, CD_RIGID_NEWMARK_GAMMA, & CD_RIGID_NEWMARK_BETA @@ -383,6 +385,12 @@ MODULE CableDyn_DeckDriver INTEGER :: deck_line = 0 ! deck file line of the row, for error messages END TYPE + ! END CONNECTIONS torsional restraint (TorsStiffness column) + INTEGER, PARAMETER :: TORS_FREE = 0, TORS_RIGID = 1, TORS_FINITE = 2 + ! Smallest |Nx x Ez| / |Nx| accepted for a reference normal (about 0.06 degrees off Ez). + REAL(wp), PARAMETER :: TORS_NX_PARALLEL_TOL = 1.0e-3_wp + REAL(wp), PARAMETER :: DEG2RAD_TORS = 3.14159265358979323846264338327950288_wp/180.0_wp + TYPE :: DeckLine INTEGER :: id = 0, nodeA = 0, nodeB = 0 CHARACTER(NAMELEN) :: outputs = '-' @@ -397,6 +405,14 @@ MODULE CableDyn_DeckDriver INTEGER :: endconn_mode(2) = CD_ENDCONN_PINNED REAL(wp) :: endconn_k(2) = CD_ZERO REAL(wp) :: endconn_ez_ref(3, 2) = CD_ZERO + ! Torsional end restraint of the optional END CONNECTIONS columns (End A, End B): mode + ! TORS_FREE (default), TORS_RIGID or TORS_FINITE with stiffness tors_k [N m/rad]; tors_nx the + ! unit zero-twist reference normal in the frame of Ez (perpendicular to Ez); tors_pretwist + ! the roll of the end frame about Ez [rad]. Torsion is active only with both ends restrained. + INTEGER :: tors_mode(2) = TORS_FREE + REAL(wp) :: tors_k(2) = CD_ZERO + REAL(wp) :: tors_nx(3, 2) = CD_ZERO + REAL(wp) :: tors_pretwist(2) = CD_ZERO INTEGER :: deck_line = 0 ! deck file line of the row, for error messages ! ATTACHMENTS (finite-EI lines): one column per discrete attachment -- arc length from ! End A [m], mass [kg], displaced volume [m^3], drag area CdA [m^2], added-mass Ca [-], @@ -410,6 +426,10 @@ MODULE CableDyn_DeckDriver INTEGER :: mode = CD_ENDCONN_PINNED REAL(wp) :: stiffness = CD_ZERO REAL(wp) :: ez(3) = CD_ZERO + INTEGER :: tors_mode = TORS_FREE + REAL(wp) :: tors_k = CD_ZERO + REAL(wp) :: nx(3) = CD_ZERO + REAL(wp) :: pretwist = CD_ZERO END TYPE TYPE :: DeckFailure @@ -639,6 +659,11 @@ MODULE CableDyn_DeckDriver ! angular velocity and acceleration (3,t); an end connection on a vessel point turns with R. LOGICAL :: has_rot = .FALSE. REAL(wp), ALLOCATABLE :: rot(:, :, :), omega(:, :), alpha(:, :) + ! Roll column of motionFile point rows (read only on a deck with a torsional line): roll(p, t) + ! [rad] of the line end frames at point p about their directors, added to the END + ! CONNECTIONS Pretwist of those ends; has_roll when some row gives one. + LOGICAL :: has_roll = .FALSE. + REAL(wp), ALLOCATABLE :: roll(:, :) END TYPE TYPE :: DriverProgress @@ -1530,6 +1555,10 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, REAL(wp), ALLOCATABLE :: w_bare_deck(:), l0_deck_att(:), w_bare(:) INTEGER, ALLOCATABLE :: att_node(:) REAL(wp) :: att_offset + ! Condensed torsion (both END CONNECTIONS rows torsionally restrained): solved as the last + ! static load stage on the final mesh, then installed in the cable. + LOGICAL :: tors_line + TYPE(CD_HermiteTorsionType) :: tors ErrStat = CD_DECKDRV_OK ErrMsg = '' @@ -1664,6 +1693,10 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, IF (endconn_mode(i) /= CD_ENDCONN_PINNED .AND. & ABS(endconn_d0_local(2, i)) > 64.0_wp*EPSILON(CD_ONE)) require_3d_static = .TRUE. END DO + ! A twisted line leaves the plane of its chord (the torque bends it out of plane), so a + ! torsional line is solved in 3D from the start. + tors_line = line_torsion_active(ln) + IF (tors_line) require_3d_static = .TRUE. ! A current with a component across the vertical plane of the chord pushes the line out ! of that plane, which the dynamics (endpoints held, every other DOF free) follow: the ! static equilibrium is then solved in 3D as well. @@ -2525,6 +2558,15 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, 'found, so a cross-flow or a lateral disturbance can move the line away from it' CALL report_note(note_text) END IF + IF (tors_line) THEN + CALL torsion_stage(es, em) + IF (es /= CD_DECKDRV_OK) THEN + ErrStat = es + IF (INDEX(em, 'CableDyn_DeckDriver: ') == 1) em = em(LEN('CableDyn_DeckDriver: ') + 1:) + ErrMsg = 'CableDyn_DeckDriver: line '//TRIM(int_to_str(ln%id))//': '//TRIM(em) + RETURN + END IF + END IF IF (contact_enabled) CALL anchor_overshoot_note() ! Safety is independent of the opt-in adaptive accuracy policy. Diagnose the ! equilibrium on the FINAL user/refined mesh and reject an element-localized fold @@ -2644,6 +2686,20 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, CALL fail(ErrStat, ErrMsg, 'finite-EI cable end-connection wiring failed: '//TRIM(em)); RETURN END IF END IF + IF (tors_line) THEN + ! The End A torsion frame (director -Ez, reference normal Nx, in the frame of Ez) is held + ! by End A's parent and turns with it in the dynamics, whatever the bending connection. + BLOCK + REAL(wp) :: tframe(3, 2) + tframe(:, 1) = -ln%endconn_ez_ref(:, 1)/NORM2(ln%endconn_ez_ref(:, 1)) + tframe(:, 2) = ln%tors_nx(:, 1) - DOT_PRODUCT(ln%tors_nx(:, 1), tframe(:, 1))*tframe(:, 1) + tframe(:, 2) = tframe(:, 2)/NORM2(tframe(:, 2)) + CALL CD_HFMF_Set_Torsion(cable, tors, es, em, coupled_frame_parent=tframe, parent_orientation=fairlead_dcm) + END BLOCK + IF (es /= CD_HFMF_OK) THEN + CALL fail(ErrStat, ErrMsg, 'finite-EI cable torsion wiring failed: '//TRIM(em)); RETURN + END IF + END IF IF (contact_enabled) THEN CALL CD_Nodal_Seabed_Stiffness(opts%kbot, diam, l0, contact_kn, es, em) IF (es /= CD_LINE_OK) THEN @@ -2730,6 +2786,85 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, END IF CONTAINS + SUBROUTINE torsion_stage(est, emt) + !! The condensed-torsion load stage of a line restrained in torsion at both ends: from the + !! converged untwisted state on the final mesh (l0_am), CD_HermiteCable_Static_Solve ramps + !! the imposed twist Phi = Pretwist(End B) - Pretwist(End A) with the same loads, contact, + !! end connections and current as the untwisted solve, tests every stage for stability and + !! descends from an unstable one. End frames are taken in the solve frame: internal end 1 + !! is deck End B (global axes), end 2 deck End A (its parent's axes, turned with it). + INTEGER, INTENT(OUT) :: est + CHARACTER(*), INTENT(OUT) :: emt + REAL(wp) :: nvec(3), dvec(3), tol_t + INTEGER :: k, attempt + CHARACTER(320) :: tnote + TYPE(CD_HermiteTorsionType) :: tors0 + est = CD_DECKDRV_OK + emt = '' + tors = CD_HermiteTorsionType() + tors%active = .TRUE. + tors%quadrature_order = opts%bending_quadrature_order + tors%gj = gj_in + IF (SIZE(l0_am) /= SIZE(l0)) CALL remap_elem_by_reference_arc(tors%gj, l0, l0_am) + DO k = 1, 2 + dvec = endconn_d0_local(:, k)/NORM2(endconn_d0_local(:, k)) + IF (k == 1) THEN + nvec = ln%tors_nx(:, 2) + ELSE + nvec = MATMUL(TRANSPOSE(fairlead_dcm), ln%tors_nx(:, 1)) + END IF + nvec = [frame_cs(1)*nvec(1) + frame_cs(2)*nvec(2), -frame_cs(2)*nvec(1) + frame_cs(1)*nvec(2), nvec(3)] + nvec = nvec - DOT_PRODUCT(nvec, dvec)*dvec + IF (NORM2(nvec) < TORS_NX_PARALLEL_TOL) THEN + est = CD_DECKDRV_BADINPUT + emt = 'torsion reference normal parallel to the end direction' + RETURN + END IF + tors%ends(:, 2*k - 1) = dvec + tors%ends(:, 2*k) = nvec/NORM2(nvec) + END DO + tors%end_compliance = CD_ZERO + IF (ln%tors_mode(2) == TORS_FINITE) tors%end_compliance(1) = CD_ONE/ln%tors_k(2) + IF (ln%tors_mode(1) == TORS_FINITE) tors%end_compliance(2) = CD_ONE/ln%tors_k(1) + tors%phi = ln%tors_pretwist(2) - ln%tors_pretwist(1) + ! Rotating a buckled shape about a common clamp axis is an exact symmetry only without + ! weight or current, with both ends clamped on one axis. + tors%zero_mode_allowed = .NOT. ANY(ABS(w_am) > CD_ZERO) .AND. .NOT. opts%has_current .AND. & + ALL(endconn_mode == CD_ENDCONN_RIGID) .AND. & + DOT_PRODUCT(tors%ends(:, 1), tors%ends(:, 3)) > CD_ONE - 1.0e-12_wp + ! As for the untwisted solve: a residual stall at the tight standalone tolerance is + ! repeated at the coupled static tolerance (a round-off floor of a stiff, light line). + ! Every attempt starts from the same fresh torsion state (no Theta carried over). + tors0 = tors + DO attempt = 1, 2 + tors = tors0 + tol_t = static_tol_target + IF (attempt == 2) tol_t = COUPLED_STATIC_TOL + IF (PRESENT(bathymetry) .AND. .NOT. bathymetry_flat) THEN + CALL hermite_static_torsion(l0, l0_am, ea_am, ei_am, w_am, diam, dry_b, q_static, curv, fixed_am, & + contact_enabled, contact_z, frame_cs, endconn_k, endconn_d0_local, & + endconn_mode, opts, tol_t, tors, est, emt, bathymetry=bathymetry, & + cur_obj=cur_obj) + ELSE + CALL hermite_static_torsion(l0, l0_am, ea_am, ei_am, w_am, diam, dry_b, q_static, curv, fixed_am, & + contact_enabled, contact_z, frame_cs, endconn_k, endconn_d0_local, & + endconn_mode, opts, tol_t, tors, est, emt, cur_obj=cur_obj) + END IF + IF (est == CD_DECKDRV_OK .OR. est == CD_DECKDRV_BADINPUT) EXIT + IF (static_tol_target >= COUPLED_STATIC_TOL) EXIT + END DO + IF (est == CD_DECKDRV_OK .AND. tol_t > static_tol_target) THEN + WRITE (tnote, '(A,I0,A,ES9.2,A,ES9.2)') 'line ', ln%id, ': torsion static residual floor above the '// & + 'tolerance ', static_tol_target, '; twisted equilibrium accepted at ', tol_t + CALL report_note(tnote) + END IF + IF (est /= CD_DECKDRV_OK) RETURN + WRITE (tnote, '(A,I0,A,ES12.5,A,ES12.5,A,I0,A,I0,A)') 'line ', ln%id, ': torsion solved, imposed twist ', & + tors%phi*45.0_wp/QUARTER_PI, ' deg, torque ', tors%torque, ' N m (', tors%ramp_steps, & + ' twist stage(s), ', tors%descents, ' buckling descent(s))' + CALL report_note(tnote) + END SUBROUTINE torsion_stage + SUBROUTINE bow_route(done, why) !! A line too long for its span whose anchor walk failed or reached an equilibrium the !! audit rejects (an arch facing the anchor that has looped through itself): trace the @@ -4518,6 +4653,76 @@ SUBROUTINE hermite_static_stable(l0_deck, l0, ea, ei, w, diam_deck, dry_deck, q, END IF END SUBROUTINE hermite_static_stable + SUBROUTINE hermite_static_torsion(l0_deck, l0, ea, ei, w, diam_deck, dry_deck, q, curv, fixed_dofs, & + contact_enabled, contact_z, frame_cs, endconn_k, endconn_d0, endconn_mode, opts, & + tol, tors, ErrStat, ErrMsg, bathymetry, cur_obj) + !! Torsion stage of a deck cable on its final mesh l0: CD_HermiteCable_Static_Solve from the + !! converged untwisted state q (one EI and buoyancy stage, a polish) with the torsion + !! description tors, under the same contact, dry-buoyancy, end-connection and current terms + !! as hermite_static_stable. On success q and curv hold the twisted equilibrium and tors its + !! accepted Theta, torque and stability report. + REAL(wp), INTENT(IN) :: l0_deck(:), l0(:), ea(:), ei(:), w(:), diam_deck(:), dry_deck(:) + REAL(wp), INTENT(INOUT) :: q(:), curv(:) + INTEGER, INTENT(IN) :: fixed_dofs(:) + LOGICAL, INTENT(IN) :: contact_enabled + REAL(wp), INTENT(IN) :: contact_z, frame_cs(2), endconn_k(2), endconn_d0(3, 2), tol + INTEGER, INTENT(IN) :: endconn_mode(2) + TYPE(DeckOptions), INTENT(IN) :: opts + TYPE(CD_HermiteTorsionType), INTENT(INOUT) :: tors + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + TYPE(CD_BathymetryType), INTENT(IN), OPTIONAL :: bathymetry + TYPE(CD_HermiteStaticCurrentType), INTENT(IN), OPTIONAL :: cur_obj + REAL(wp), ALLOCATABLE :: diam(:), dry(:), knnode(:), q_out(:) + REAL(wp) :: res + INTEGER :: i, ne, it, es + CHARACTER(1024) :: em + + ErrStat = CD_DECKDRV_OK + ErrMsg = '' + ne = SIZE(l0) + diam = diam_deck + dry = dry_deck + IF (ne /= SIZE(l0_deck)) THEN + CALL remap_elem_by_reference_arc(diam, l0_deck, l0) + CALL remap_elem_by_reference_arc(dry, l0_deck, l0) + END IF + ALLOCATE (q_out(SIZE(q))) + IF (contact_enabled) THEN + ALLOCATE (knnode(ne + 1)) + knnode = CD_ZERO + DO i = 1, ne + knnode(i) = knnode(i) + 0.5_wp*opts%kbot*diam(i)*l0(i) + knnode(i + 1) = knnode(i + 1) + 0.5_wp*opts%kbot*diam(i)*l0(i) + END DO + CALL CD_HermiteCable_Static_Solve(l0, ea, ei, w, q, fixed_dofs, contact_z, CD_ZERO, 1, 200, & + tol, 0.7_wp, q_out, curv, res, it, es, em, n_buoy_steps=1, & + contact_kn=knnode, bathymetry=bathymetry, contact_frame_cs=frame_cs, & + axial_quadrature_order=opts%axial_quadrature_order, & + bending_quadrature_order=opts%bending_quadrature_order, & + endconn_stiffness=endconn_k, endconn_direction=endconn_d0, & + endconn_mode=endconn_mode, waterline_z=CD_ZERO, dry_buoyancy=dry, & + water_weight=opts%rhoW*opts%g, current=cur_obj, torsion=tors) + ELSE + CALL CD_HermiteCable_Static_Solve(l0, ea, ei, w, q, fixed_dofs, contact_z, CD_ZERO, 1, 200, & + tol, 0.7_wp, q_out, curv, res, it, es, em, n_buoy_steps=1, & + axial_quadrature_order=opts%axial_quadrature_order, & + bending_quadrature_order=opts%bending_quadrature_order, & + endconn_stiffness=endconn_k, endconn_direction=endconn_d0, & + endconn_mode=endconn_mode, waterline_z=CD_ZERO, dry_buoyancy=dry, & + water_weight=opts%rhoW*opts%g, current=cur_obj, torsion=tors) + END IF + IF (es == CD_HCSTAT_BADINPUT) THEN + CALL fail(ErrStat, ErrMsg, 'finite-EI torsion static solve rejected the input: '//TRIM(em)); RETURN + ELSE IF (es /= CD_HCSTAT_OK) THEN + CALL fail_solve(ErrStat, ErrMsg, 'finite-EI torsion static solve failed: '//TRIM(em)); RETURN + END IF + IF (.NOT. tors%stable) THEN + CALL fail_solve(ErrStat, ErrMsg, 'finite-EI torsion static solve ended on an unstable equilibrium'); RETURN + END IF + q = q_out + END SUBROUTINE hermite_static_torsion + SUBROUTINE audit_hermite_static(l0, q, ea, ei, w, diam, endconn_mode, physical, message, compression_note) !! Physical-branch audit of a converged deck cable (CD_HermiteCable_Branch_Audit). !! A rotationally restrained end (Finite/Rigid connection) may turn the centreline @@ -4690,6 +4895,10 @@ SUBROUTINE CD_Init_Deck_HermiteCable(deck_path, dt, cable, coupled_node, ErrStat CALL parse_deck(deck_path, types, bodies, rod_types, rods, points, lines, sections, opts, channels, & ErrStat, ErrMsg, caller_driven=.TRUE.) IF (ErrStat /= CD_DECKDRV_OK) RETURN + IF (deck_has_torsion_columns(lines)) THEN + CALL fail(ErrStat, ErrMsg, 'torsion is not yet supported in coupled OpenFAST runs; set TorsStiffness '// & + 'Free in every END CONNECTIONS row'); RETURN + END IF IF (.NOT. deck_has_finite_ei(types, sections)) THEN CALL fail(ErrStat, ErrMsg, 'CD_Init_Deck_HermiteCable: deck has no finite-EI (EI>0) line; '// & 'EI=0 decks use CD_Init_Deck_System'); RETURN @@ -4840,6 +5049,14 @@ SUBROUTINE CD_Init_Deck_Aggregate(deck_path, dt, agg, ErrStat, ErrMsg, env_gravi ErrStat, ErrMsg, caller_driven=.TRUE., env_gravity=env_gravity, & env_rho_water=env_rho_water, env_wtrdpth=env_wtrdpth, allow_turbine=.TRUE., & failures=fails, controls=ctrls, run_tmax=run_tmax, host_coupled_objects=.TRUE.) + IF (ErrStat == CD_DECKDRV_OK) THEN + IF (deck_has_torsion_columns(lines)) THEN + CALL fail(ErrStat, ErrMsg, 'torsion is not yet supported in a deck that mixes EI = 0 and finite-EI '// & + 'lines without a BODY, nor in coupled OpenFAST or FAST.Farm runs (with a BODY such a deck '// & + 'runs on the multibody route, which supports torsion); set TorsStiffness Free in every END '// & + 'CONNECTIONS row'); RETURN + END IF + END IF IF (ErrStat /= CD_DECKDRV_OK) RETURN host_dcm = CD_ZERO host_dcm(1, 1) = CD_ONE @@ -5895,6 +6112,24 @@ SUBROUTINE run_deck_driver(deck_path, out_root, converged, ErrStat, ErrMsg) CALL parse_deck(deck_path, types, bodies, rod_types, rods, points, lines, sections, opts, channels, & ErrStat, ErrMsg, failures=fails) IF (ErrStat /= 0) RETURN + ! Torsion (both ends of a line torsionally restrained) runs on the standalone cubic-Hermite + ! route and the multibody march, in statics and dynamics (quasi-static torsion: no torsional + ! inertia). One restrained end carries no torque (the other end is free to twist), so it is + ! noted and ignored. + DO li = 1, SIZE(lines) + IF (COUNT(lines(li)%tors_mode /= TORS_FREE) == 1) WRITE (output_unit, '(A)') ' Note: line '// & + TRIM(int_to_str(lines(li)%id))//' is torsionally restrained at one end only; with the other end free '// & + 'to twist it carries no torque, so no torsion is solved.' + END DO + IF (ANY([(line_torsion_active(lines(li)), li=1, SIZE(lines))])) THEN + IF (opts%has_tmax .AND. opts%tmax > CD_ZERO .AND. .NOT. opts%alpha_force_blend) THEN + CALL fail(ErrStat, ErrMsg, 'torsion in a dynamic run needs the force-blended generalised-alpha (OPTION '// & + 'alpha_force_blend True)'); RETURN + END IF + IF (opts%n_modes > 0) THEN + CALL fail(ErrStat, ErrMsg, 'modal analysis (OPTION nModes) of a line with torsion is not yet supported'); RETURN + END IF + END IF ! A FAILURE deck is inherently dynamic (the trigger fires at a committed march ! step) and runs on the point-system object graph -- the only path with the ! reserve-point detach machinery. Every other branch fails closed by name. @@ -8412,7 +8647,8 @@ SUBROUTINE parse_deck(path, types, bodies, rod_types, rods, points, lines, secti CALL apply_syrope_ic(line, lines, nlin, ErrStat, ErrMsg) CASE (SEC_ENDCONN) IF (.NOT. has_numeric_token(line)) CYCLE - CALL list_safe_row(line, NO_TEXT_COLS, [2, 3], row, ErrStat, ErrMsg) + ! End, Stiffness and TorsStiffness take keywords (Rigid, Infinity, Inf, Free, ...) + CALL list_safe_row(line, NO_TEXT_COLS, [2, 3, 7], row, ErrStat, ErrMsg) IF (ErrStat == 0) CALL append_end_connection(row, endconns, nec, ErrStat, ErrMsg) CASE (SEC_TURBINES) IF (.NOT. has_numeric_token(line)) CYCLE @@ -10811,11 +11047,16 @@ END FUNCTION pinned_body_of_rod_type SUBROUTINE append_end_connection(line, endconns, nec, ErrStat, ErrMsg) !! END CONNECTIONS row: - !! LineID End BendingStiffness EzX EzY EzZ + !! LineID End BendingStiffness EzX EzY EzZ [TorsStiffness NxX NxY NxZ [Pretwist]] !! End is A or B. BendingStiffness is a non-negative number in N m/rad, !! Pinned/Free/Zero, or Rigid/Infinity. Ez follows the line End-A-to-End-B !! convention and is normalized here. Rigid selects the exact tangent-direction !! constraint; it is never converted to a large finite penalty stiffness. + !! The optional torsion columns (10 or 11 columns in all): TorsStiffness is Free/Zero/0 + !! (default), Rigid/Infinity/Inf, or a positive torsional stiffness in N m/rad; Nx is the + !! zero-twist reference normal in the frame of Ez (finite, non-zero, not parallel to Ez; + !! orthonormalised against Ez here); Pretwist [deg, default 0, any real value] rolls the end + !! frame about Ez (right-hand rule). CHARACTER(*), INTENT(IN) :: line TYPE(DeckEndConnection), ALLOCATABLE, INTENT(INOUT) :: endconns(:) INTEGER, INTENT(INOUT) :: nec @@ -10824,19 +11065,28 @@ SUBROUTINE append_end_connection(line, endconns, nec, ErrStat, ErrMsg) TYPE(DeckEndConnection), ALLOCATABLE :: grown(:) TYPE(DeckEndConnection) :: ec - CHARACTER(NAMELEN) :: end_token, stiffness_token - REAL(wp) :: ez_norm - INTEGER :: ios, istat + CHARACTER(NAMELEN) :: end_token, stiffness_token, tors_token + REAL(wp) :: ez_norm, nx_norm + INTEGER :: ios, istat, ntok LOGICAL :: ok ErrStat = 0 ErrMsg = '' - IF (count_tokens(line) /= 6) THEN + ntok = count_tokens(line) + IF (ntok /= 6 .AND. ntok /= 10 .AND. ntok /= 11) THEN CALL fail(ErrStat, ErrMsg, & - 'END CONNECTIONS row needs 6 columns: LineID End Stiffness EzX EzY EzZ') + 'END CONNECTIONS row needs 6 columns (LineID End Stiffness EzX EzY EzZ), or 10 or 11 with '// & + 'the torsion columns (... TorsStiffness NxX NxY NxZ [Pretwist])') RETURN END IF - READ (line, *, IOSTAT=ios) ec%line_id, end_token, stiffness_token, ec%ez + tors_token = 'free' + IF (ntok == 6) THEN + READ (line, *, IOSTAT=ios) ec%line_id, end_token, stiffness_token, ec%ez + ELSE IF (ntok == 10) THEN + READ (line, *, IOSTAT=ios) ec%line_id, end_token, stiffness_token, ec%ez, tors_token, ec%nx + ELSE + READ (line, *, IOSTAT=ios) ec%line_id, end_token, stiffness_token, ec%ez, tors_token, ec%nx, ec%pretwist + END IF IF (ios /= 0 .OR. ec%line_id < 1) THEN CALL fail(ErrStat, ErrMsg, 'malformed END CONNECTIONS row') RETURN @@ -10858,6 +11108,7 @@ SUBROUTINE append_end_connection(line, endconns, nec, ErrStat, ErrMsg) ec%mode = CD_ENDCONN_RIGID ec%stiffness = CD_ZERO CASE DEFAULT + ! parse_real_token rejects NaN, Inf and subnormal values (the row guard skips keyword columns) CALL parse_real_token(stiffness_token, ec%stiffness, ok) IF (.NOT. ok) ec%stiffness = -CD_ONE IF (.NOT. IEEE_IS_FINITE(ec%stiffness) .OR. ec%stiffness < CD_ZERO) THEN @@ -10881,6 +11132,47 @@ SUBROUTINE append_end_connection(line, endconns, nec, ErrStat, ErrMsg) RETURN END IF ec%ez = ec%ez/ez_norm + IF (ntok > 6) THEN + SELECT CASE (to_lower(TRIM(tors_token))) + CASE ('free', 'zero') + ec%tors_mode = TORS_FREE + CASE ('rigid', 'infinity', 'inf') + ec%tors_mode = TORS_RIGID + CASE DEFAULT + CALL parse_real_token(tors_token, ec%tors_k, ok) + IF (.NOT. ok) ec%tors_k = -CD_ONE + IF (.NOT. IEEE_IS_FINITE(ec%tors_k) .OR. ec%tors_k < CD_ZERO) THEN + CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS torsional stiffness must be finite and non-negative, '// & + 'Free, or Rigid') + RETURN + END IF + ec%tors_mode = MERGE(TORS_FINITE, TORS_FREE, ec%tors_k > CD_ZERO) + END SELECT + IF (.NOT. (ALL(IEEE_IS_FINITE(ec%nx)) .AND. IEEE_IS_FINITE(ec%pretwist))) THEN + CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS torsion reference normal and pretwist must be finite') + RETURN + END IF + IF (ec%tors_mode == TORS_FREE) THEN + ! a Free end has no torsion frame: its Nx and Pretwist are not used (6-column meaning) + ec%nx = CD_ZERO + ec%pretwist = CD_ZERO + ELSE + nx_norm = SQRT(DOT_PRODUCT(ec%nx, ec%nx)) + IF (.NOT. IEEE_IS_FINITE(nx_norm) .OR. nx_norm <= SQRT(TINY(CD_ONE))) THEN + CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS torsion reference normal (NxX NxY NxZ) must be non-zero') + RETURN + END IF + ec%nx = ec%nx/nx_norm + IF (NORM2(cross3(ec%nx, ec%ez)) < TORS_NX_PARALLEL_TOL) THEN + CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS torsion reference normal (NxX NxY NxZ) must not be '// & + 'parallel to the direction Ez') + RETURN + END IF + ec%nx = ec%nx - DOT_PRODUCT(ec%nx, ec%ez)*ec%ez + ec%nx = ec%nx/NORM2(ec%nx) + ec%pretwist = ec%pretwist*DEG2RAD_TORS + END IF + END IF ALLOCATE (grown(nec + 1), STAT=istat) IF (istat /= 0) THEN @@ -11018,6 +11310,10 @@ SUBROUTINE resolve_end_connections(endconns, lines, ErrStat, ErrMsg) lines(lidx)%endconn_mode(endconns(i)%end_index) = endconns(i)%mode lines(lidx)%endconn_k(endconns(i)%end_index) = endconns(i)%stiffness lines(lidx)%endconn_ez_ref(:, endconns(i)%end_index) = endconns(i)%ez + lines(lidx)%tors_mode(endconns(i)%end_index) = endconns(i)%tors_mode + lines(lidx)%tors_k(endconns(i)%end_index) = endconns(i)%tors_k + lines(lidx)%tors_nx(:, endconns(i)%end_index) = endconns(i)%nx + lines(lidx)%tors_pretwist(endconns(i)%end_index) = endconns(i)%pretwist END DO END SUBROUTINE resolve_end_connections @@ -12019,7 +12315,7 @@ FUNCTION channel_identity_key(ch) RESULT(key) CALL parse_point_channel(ch, a, b) IF (a > 0 .AND. b > 0) key = 'point:'//TRIM(int_to_str(a))//':'//TRIM(int_to_str(b)) ELSE IF (starts_with(lo, 'ten') .OR. starts_with(lo, 'curv') .OR. starts_with(lo, 'bendmom') .OR. & - starts_with(lo, 'l')) THEN + starts_with(lo, 'l') .OR. starts_with(lo, 'torq') .OR. starts_with(lo, 'twist')) THEN CALL parse_line_node_channel(ch, a, b, c, d) IF (a > 0 .AND. b > 0 .AND. c > 0) key = 'node:'//TRIM(int_to_str(a))//':'//TRIM(int_to_str(b))// & ':'//TRIM(int_to_str(c))//':'//TRIM(int_to_str(d)) @@ -12122,6 +12418,20 @@ SUBROUTINE normalize_stock_endpoint_order(points, lines, sections) ez_tmp = lines(i)%endconn_ez_ref(:, 1) lines(i)%endconn_ez_ref(:, 1) = -lines(i)%endconn_ez_ref(:, 2) lines(i)%endconn_ez_ref(:, 2) = -ez_tmp + ! torsion: the reference normals swap with their ends; a pretwist is a roll about Ez, + ! which is reversed, so its sign flips (Phi = pretwist B - pretwist A is unchanged) + mode_tmp = lines(i)%tors_mode(1) + lines(i)%tors_mode(1) = lines(i)%tors_mode(2) + lines(i)%tors_mode(2) = mode_tmp + k_tmp = lines(i)%tors_k(1) + lines(i)%tors_k(1) = lines(i)%tors_k(2) + lines(i)%tors_k(2) = k_tmp + ez_tmp = lines(i)%tors_nx(:, 1) + lines(i)%tors_nx(:, 1) = lines(i)%tors_nx(:, 2) + lines(i)%tors_nx(:, 2) = ez_tmp + k_tmp = lines(i)%tors_pretwist(1) + lines(i)%tors_pretwist(1) = -lines(i)%tors_pretwist(2) + lines(i)%tors_pretwist(2) = -k_tmp ! attachment arcs are measured from End A, which is now the other end IF (ALLOCATED(lines(i)%att)) & lines(i)%att(1, :) = SUM(sections%length, MASK=sections%line_id == lines(i)%id) - lines(i)%att(1, :) @@ -13037,6 +13347,10 @@ SUBROUTINE finalize_checks(types, bodies, rod_types, rods, points, lines, sectio DO i = 1, SIZE(lines) at_line = lines(i)%deck_line + IF (ANY(lines(i)%tors_mode /= TORS_FREE)) THEN + CALL check_torsion_line(lines(i), ErrStat, ErrMsg) + IF (ErrStat /= 0) RETURN + END IF IF (.NOT. ANY(lines(i)%endconn_mode /= CD_ENDCONN_PINNED)) CYCLE IF (.NOT. line_is_finite_ei(lines(i), types, sections)) THEN CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS requires a finite-EI line') @@ -13082,6 +13396,69 @@ SUBROUTINE finalize_checks(types, bodies, rod_types, rods, points, lines, sectio IF (ErrStat /= 0) RETURN END DO at_line = 0 + + CONTAINS + + SUBROUTINE check_torsion_line(ln, es_t, em_t) + !! A line with a torsional END CONNECTIONS restraint: a finite-EI (cubic-Hermite) line. With + !! both ends restrained (torsion active) End B must be Fixed, every section's LINE TYPES row + !! must give an explicit GJ, End A may not be a rod end, a body End A must be a Rigid6 body, + !! and ATTACHMENTS are not combined with it. + TYPE(DeckLine), INTENT(IN) :: ln + INTEGER, INTENT(OUT) :: es_t + CHARACTER(*), INTENT(OUT) :: em_t + INTEGER :: js, jp, jb + CHARACTER(32) :: lid + es_t = 0 + em_t = '' + lid = TRIM(int_to_str(ln%id)) + IF (.NOT. line_is_finite_ei(ln, types, sections)) THEN + CALL fail(es_t, em_t, 'torsional END CONNECTIONS (TorsStiffness Rigid or a stiffness) require a '// & + 'finite-EI line; line '//TRIM(lid)//' has EI = 0') + RETURN + END IF + ! one restrained end only carries no torque (noted and ignored): the rules below apply to a + ! line restrained at both ends + IF (.NOT. ALL(ln%tors_mode /= TORS_FREE)) RETURN + jp = find_point(points, ln%nodeB) + IF (jp < 1) RETURN + IF (TRIM(points(jp)%ptype) /= 'fixed') THEN + CALL fail(es_t, em_t, 'torsional END CONNECTIONS require a finite-EI line with Fixed End B; line '// & + TRIM(lid)//' has two moving ends') + RETURN + END IF + DO js = 1, SIZE(sections) + IF (sections(js)%line_id /= ln%id) CYCLE + IF (sections(js)%ltype < 1 .OR. sections(js)%ltype > SIZE(types)) CYCLE + IF (.NOT. (types(sections(js)%ltype)%gj > CD_ZERO)) THEN + CALL fail(es_t, em_t, 'line '//TRIM(lid)//' is torsionally restrained at both ends: its LINE TYPES '// & + 'row "'//TRIM(types(sections(js)%ltype)%name)//'" must give an explicit GJ > 0 (the '// & + '14-column row; torsion has no EI/1.3 default)') + RETURN + END IF + END DO + IF (ALLOCATED(ln%att)) THEN + CALL fail(es_t, em_t, 'line '//TRIM(lid)//': torsion is not combined with ATTACHMENTS in this build') + RETURN + END IF + jp = find_point(points, ln%nodeA) + IF (jp < 1) RETURN + IF (is_rod_point_type(points(jp)%ptype)) THEN + CALL fail(es_t, em_t, 'line '//TRIM(lid)//': a torsional END CONNECTION on a rod end is not supported '// & + '(a rod has no spin angle in this build)') + RETURN + END IF + IF (starts_with(TRIM(points(jp)%ptype), 'body')) THEN + jb = find_body(bodies, points(jp)%body_id) + IF (jb > 0) THEN + IF (TRIM(bodies(jb)%btype) /= 'rigid6') THEN + CALL fail(es_t, em_t, 'line '//TRIM(lid)//': a torsional END CONNECTION on a body needs a Rigid6 '// & + 'body') + RETURN + END IF + END IF + END IF + END SUBROUTINE check_torsion_line END SUBROUTINE finalize_checks ! --------------------------------------------------------------------------- ! @@ -15991,7 +16368,8 @@ SUBROUTINE write_multibody_dynamic_out(deck_path, out_root, channels, types, bod pc = points pa = find_point(points, cab_lines(c)%nodeA) IF (TRIM(pc(pa)%ptype) /= 'fixed') pc(pa)%ptype = 'coupled' - mb%cab_clamp(c) = cab_lines(c)%endconn_mode(1) /= CD_ENDCONN_PINNED .AND. TRIM(points(pa)%ptype) /= 'fixed' + mb%cab_clamp(c) = (cab_lines(c)%endconn_mode(1) /= CD_ENDCONN_PINNED .OR. line_torsion_active(cab_lines(c))) & + .AND. TRIM(points(pa)%ptype) /= 'fixed' IF (mb%cab_clamp(c)) THEN ! End A clamped or elastic on its object: the connection direction is stored in the object ! frame and turns with the object's orientation (the OpenFAST parent-frame contract) @@ -17537,18 +17915,24 @@ SUBROUTINE cable_load(kind, obj, f, m) END SUBROUTINE cable_load END SUBROUTINE mb_imp_group_residual - SUBROUTINE mb_imp_row_scales(mb, points, opts, rscale) + SUBROUTINE mb_imp_row_scales(mb, points, opts, bh2, rscale) !! The force (translation rows) and moment (rotation rows) scale of every junction row, from !! its own object: the larger of its weight, its buoyancy, the line reactions at its !! attachments (their magnitudes summed, so a junction whose line loads cancel keeps their !! scale) and the cable end forces on it (at least 1 N), times its size (at least 1 m) for - !! moments. + !! moments. A body holding a line restrained in torsion has its moment rows scaled by its + !! rotational step stiffness instead when that is smaller, I_min/(beta h^2) + sum 1/C (bh2 = + !! beta h^2) times TORS_ROT_REF, so that the junction tolerance bounds the rotation error + !! (JUNCTION_TOL TORS_ROT_REF rad): the weight of a heavy body with a small rotational + !! inertia would otherwise accept a moment residual far above the line torque. TYPE(MultibodyMarch), INTENT(IN) :: mb TYPE(DeckPoint), INTENT(IN) :: points(:) TYPE(DeckOptions), INTENT(IN) :: opts + REAL(wp), INTENT(IN) :: bh2 REAL(wp), INTENT(OUT) :: rscale(:) + REAL(wp), PARAMETER :: TORS_ROT_REF = 1.0e-2_wp INTEGER :: ib, ir, p, ip, k, o, base, bases(2) - REAL(wp) :: f, l, arms(3, 2) + REAL(wp) :: f, l, arms(3, 2), kt rscale = CD_ONE DO ib = 1, SIZE(mb%rigid) o = mb%jb_off(ib) @@ -17563,6 +17947,9 @@ SUBROUTINE mb_imp_row_scales(mb, points, opts, rscale) f = MAX(f, cable_force(1, ib)) rscale(o + 1:o + 3) = f rscale(o + 4:o + 6) = f*l + kt = torsion_stiffness(ib) + IF (kt > CD_ZERO) rscale(o + 4:o + 6) = MIN(f*l, MAX(CD_ONE, & + (MINVAL(mb%rigid(ib)%inertia)/bh2 + kt)*TORS_ROT_REF)) END DO DO ir = 1, SIZE(mb%rods) o = mb%jr_off(ir) @@ -17602,6 +17989,21 @@ REAL(wp) FUNCTION cable_force(kind, obj) RESULT(fc) IF (mb%cab_kind(cc) == kind .AND. mb%cab_obj(cc) == obj) fc = MAX(fc, NORM2(mb%cab_force(:, cc))) END DO END FUNCTION cable_force + + REAL(wp) FUNCTION torsion_stiffness(obj) RESULT(ks) + !! Summed torsional stiffness 1/C of the active torsional cables on body obj (0: none). + INTEGER, INTENT(IN) :: obj + INTEGER :: cc + REAL(wp) :: cmp + ks = CD_ZERO + IF (.NOT. ALLOCATED(mb%cables)) RETURN + DO cc = 1, SIZE(mb%cables) + IF (mb%cab_kind(cc) /= 1 .OR. mb%cab_obj(cc) /= obj) CYCLE + IF (.NOT. mb%cables(cc)%line%torsion%active) CYCLE + cmp = CD_HermiteTorsion_Compliance(mb%cables(cc)%line%torsion, mb%cables(cc)%line%l0) + IF (cmp > CD_ZERO) ks = ks + CD_ONE/cmp + END DO + END FUNCTION torsion_stiffness END SUBROUTINE mb_imp_row_scales RECURSIVE SUBROUTINE mb_step_implicit(mb, points, opts, time, dt, converged, stalled, n_iter, ErrStat, ErrMsg, & @@ -17633,7 +18035,7 @@ RECURSIVE SUBROUTINE mb_step_implicit(mb, points, opts, time, dt, converged, sta REAL(wp), PARAMETER :: JUNCTION_TOL = 1.0e-6_wp, PREDICTOR_TOL = 1.0e-9_wp CHARACTER(24) :: tstr INTEGER :: dep, it, g, c, ib, ir, p, o, es, lit, nj, gworst, it2 - LOGICAL :: lconv, lstall, first, ok, conv2, stall2, refresh, lines_stepped + LOGICAL :: lconv, lstall, first, ok, conv2, stall2, refresh, lines_stepped, tors_turn, lim INTEGER, PARAMETER :: TAN_MAX_AGE = 20 REAL(wp) :: rho, am, af, beta_na, gam_na, t_af, h, fac, dth, worst, tol_it, am_p, gam_p, beta_p REAL(wp), ALLOCATABLE :: x(:), xp(:), r(:), rp(:), jac(:, :), dx(:), kg(:, :), ga(:, :), rscale(:) @@ -17713,8 +18115,15 @@ RECURSIVE SUBROUTINE mb_step_implicit(mb, points, opts, time, dt, converged, sta first = .TRUE. lines_stepped = .FALSE. ok = .FALSE. + tors_turn = .FALSE. gworst = 0 DO it = 1, MAX_OUTER + ! a body holding a line restrained in torsion turns by less than pi/2 per step: the line + ! sees the body only through its orientation, which cannot tell a turn of psi from one of + ! psi - 2 pi, so a larger trial turn could settle one turn off. The trial is held inside + ! the limit; a step whose solution lies outside it does not converge and is halved. + CALL mb_torsion_turn_limit(mb, x, dt, beta_na, lim) + tors_turn = tors_turn .OR. lim CALL mb_imp_states(mb, x, dt, gam_na, beta_na, gam_p, beta_p, ErrStat, ErrMsg) IF (ErrStat /= CD_DECKDRV_OK) EXIT CALL mb_imp_scatter(mb, points, ErrStat, ErrMsg) @@ -17776,7 +18185,7 @@ RECURSIVE SUBROUTINE mb_step_implicit(mb, points, opts, time, dt, converged, sta END DO IF (ErrStat /= CD_DECKDRV_OK) EXIT IF (.NOT. ALL(IEEE_IS_FINITE(r))) EXIT - IF (it == 1) CALL mb_imp_row_scales(mb, points, opts, rscale) + IF (it == 1) CALL mb_imp_row_scales(mb, points, opts, beta_na*dt*dt, rscale) ! every row against its own object's force (moment) scale worst = CD_ZERO DO g = 1, mb%ng @@ -17874,7 +18283,12 @@ RECURSIVE SUBROUTINE mb_step_implicit(mb, points, opts, time, dt, converged, sta END DO mb%tan_age = HUGE(1) IF (dep >= MAX_DEPTH) THEN - IF (ErrStat == CD_DECKDRV_OK) THEN + IF (ErrStat == CD_DECKDRV_OK .AND. tors_turn) THEN + WRITE (tstr, '(ES12.5)') time + CALL fail_solve(ErrStat, ErrMsg, 'monolithic body/point step at t = '//TRIM(ADJUSTL(tstr))// & + ' s: a body holding a line restrained in torsion turns by more than 90 deg in one step '// & + 'even after '//TRIM(int_to_str(MAX_DEPTH))//' step halvings; reduce dtM') + ELSE IF (ErrStat == CD_DECKDRV_OK) THEN IF (gworst > 0) THEN SELECT CASE (mb%g_kind(gworst)) CASE (1) @@ -17945,6 +18359,38 @@ SUBROUTINE cable_columns(cc, gg) END SUBROUTINE cable_columns END SUBROUTINE mb_step_implicit + SUBROUTINE mb_torsion_turn_limit(mb, x, dt, beta_na, limited) + !! Keep every body that holds a cable restrained in torsion within a turn of TURN_FRAC + !! CD_HTORS_MAX_STEP over the step to the trial accelerations x: when the Newmark rotation + !! increment dt omega_n + dt^2 ((1/2 - beta) alpha_n + beta alpha_{n+1}) of + !! CD_Rigid_Advance_Newmark is larger, alpha_{n+1} is moved so that the increment keeps its + !! direction at that length (limited = .TRUE.). The cable sees the body only through its + !! orientation, which cannot tell a turn of psi from one of psi - 2 pi. + TYPE(MultibodyMarch), INTENT(IN) :: mb + REAL(wp), INTENT(INOUT) :: x(:) + REAL(wp), INTENT(IN) :: dt, beta_na + LOGICAL, INTENT(OUT) :: limited + REAL(wp), PARAMETER :: TURN_FRAC = 0.9_wp + INTEGER :: c, ib, o + REAL(wp) :: base(3), dpsi(3), s + limited = .FALSE. + IF (.NOT. ALLOCATED(mb%cables)) RETURN + DO c = 1, SIZE(mb%cables) + IF (mb%cab_kind(c) /= 1 .OR. .NOT. mb%cables(c)%line%torsion%active) CYCLE + ib = mb%cab_obj(c) + o = mb%jb_off(ib) + IF (o < 0) CYCLE + base = dt*mb%old_rigid(ib)%omega + dt*dt*(0.5_wp - beta_na)*mb%old_rigid(ib)%alpha + dpsi = base + dt*dt*beta_na*x(o + 4:o + 6) + IF (NORM2(dpsi) <= TURN_FRAC*CD_HTORS_MAX_STEP) CYCLE + ! non-finite trials are left to the residual's finite check + IF (.NOT. ALL(IEEE_IS_FINITE(dpsi))) CYCLE + s = TURN_FRAC*CD_HTORS_MAX_STEP/NORM2(dpsi) + x(o + 4:o + 6) = (s*dpsi - base)/(dt*dt*beta_na) + limited = .TRUE. + END DO + END SUBROUTINE mb_torsion_turn_limit + INTEGER FUNCTION mb_cable_group(mb, c) RESULT(g) !! The junction group that carries cable c's End A object (0: a held attachment or an object !! without junction coordinates). A rod pinned to a body belongs to the body's group. @@ -19300,7 +19746,7 @@ END SUBROUTINE rigid6_rod_loads SUBROUTINE solve_body_equilibrium_core(types, bodies, rod_types, rods, points, lines, sections, opts, ErrStat, & ErrMsg, bathymetry, points_only, jac_check_err, ext_pt, ext_fm, & - end_force_a, quiet, skipped) + end_force_a, quiet, skipped, ext_km, ext_rot) !! Static equilibrium of the free objects of a deck: free Rigid6 bodies (six DOFs: position !! and a rotation-vector increment on SO(3)), free rods (position and the two rotations !! normal to the axis; the rod is symmetric about it) and, jointly with them, the @@ -19367,6 +19813,11 @@ SUBROUTINE solve_body_equilibrium_core(types, bodies, rod_types, rods, points, l ! point ext_pt(e), the force ext_fm(1:3, e) at that point and the moment ext_fm(4:6, e). INTEGER, INTENT(IN), OPTIONAL :: ext_pt(:) REAL(wp), INTENT(IN), OPTIONAL :: ext_fm(:, :) + ! ... and a moment stiffness of the extra load (a finite-EI line's condensed torsion): with + ! the carrying body turned from the orientation ext_rot(:, :, e) by the rotation vector dw + ! (global axes), the moment ext_fm(4:6, e) becomes ext_fm(4:6, e) - ext_km(:, :, e) dw. The + ! central-difference own-load Jacobian carries it into the Newton step and the stability test. + REAL(wp), INTENT(IN), OPTIONAL :: ext_km(:, :, :), ext_rot(:, :, :) ! The end force every moving line exerts on its End A point at the solution (zero for a line ! that does not move), and quiet: no notes or completion line. REAL(wp), INTENT(OUT), OPTIONAL :: end_force_a(:, :) @@ -20034,6 +20485,10 @@ SUBROUTINE own_loads(xs, fk, mk, cen, sc_out, good) IF (kk == 0) CYCLE fk(:, kk) = fk(:, kk) + ext_fm(1:3, es_l) mk(:, kk) = mk(:, kk) + cross3(pts(ext_pt(es_l))%pos - cen(:, kk), ext_fm(1:3, es_l)) + ext_fm(4:6, es_l) + IF (PRESENT(ext_km) .AND. PRESENT(ext_rot)) THEN + IF (okind(kk) == OBJ_BODY) mk(:, kk) = mk(:, kk) - MATMUL(ext_km(:, :, es_l), & + CD_Log_SO3(MATMUL(bds(idx(kk))%rot_eq, TRANSPOSE(ext_rot(:, :, es_l))))) + END IF END DO END IF good = .TRUE. @@ -20800,7 +21255,12 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, !! repeat from the deck state until these loads change by at most 1e-7 of the force scale !! (moments of the force scale times 1 m), so the converged pose balances the Hermite cable !! exactly: at the fixed point the core's surrogate end force cancels. A deck without such a - !! line is the core solve unchanged. + !! line is the core solve unchanged. A line with condensed torsion also returns its torque, + !! which depends on the object's rotation as strongly as GJ/L: each pass then also gives the + !! core the torque's moment stiffness (1/C) a a^T about the previous pass's pose (a = dTheta/dw + !! of the cable at that pose, its shape held; C the line torsional compliance), so the passes + !! converge whatever the ratio of the object's own restoring stiffness to GJ/L (the passes + !! alone would need it far above GJ/L), to 1e-10 of the force scale. TYPE(DeckLineType), INTENT(IN) :: types(:) TYPE(DeckBody), INTENT(INOUT) :: bodies(:) TYPE(DeckRodType), INTENT(IN) :: rod_types(:) @@ -20815,15 +21275,16 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, LOGICAL, INTENT(IN), OPTIONAL :: points_only REAL(wp), INTENT(OUT), OPTIONAL :: jac_check_err INTEGER, PARAMETER :: MAX_PASS = 60 - REAL(wp), PARAMETER :: PASS_TOL = 1.0e-7_wp + REAL(wp), PARAMETER :: PASS_TOL = 1.0e-7_wp, PASS_TOL_TORSION = 1.0e-10_wp TYPE(DeckBody), ALLOCATABLE :: bodies0(:) TYPE(DeckRod), ALLOCATABLE :: rods0(:) TYPE(DeckPoint), ALLOCATABLE :: points0(:) INTEGER, ALLOCATABLE :: cl(:), ext_pt(:) - REAL(wp), ALLOCATABLE :: ext_fm(:, :), new_fm(:, :), fs(:, :) + REAL(wp), ALLOCATABLE :: ext_fm(:, :), new_fm(:, :), fs(:, :), ext_km(:, :, :), ext_rot(:, :, :) + REAL(wp), ALLOCATABLE :: new_km(:, :, :), new_rot(:, :, :) INTEGER :: j, pa, e, pass, ncl, eworst LOGICAL :: only_points, held - REAL(wp) :: scale, change + REAL(wp) :: scale, change, pass_tol_eff CHARACTER(24) :: b1 ErrStat = CD_DECKDRV_OK @@ -20834,7 +21295,7 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, ALLOCATE (cl(0)) IF (.NOT. only_points .AND. .NOT. PRESENT(jac_check_err) .AND. opts%body_ic_static) THEN DO j = 1, SIZE(lines) - IF (lines(j)%endconn_mode(1) == CD_ENDCONN_PINNED) CYCLE + IF (lines(j)%endconn_mode(1) == CD_ENDCONN_PINNED .AND. .NOT. line_torsion_active(lines(j))) CYCLE IF (.NOT. line_is_finite_ei(lines(j), types, sections)) CYCLE pa = find_point(points, lines(j)%nodeA) IF (pa < 1) CYCLE @@ -20863,23 +21324,41 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, rods0 = rods points0 = points ALLOCATE (ext_pt(ncl), ext_fm(6, ncl), new_fm(6, ncl), fs(3, SIZE(lines))) + ALLOCATE (ext_km(3, 3, ncl), ext_rot(3, 3, ncl), new_km(3, 3, ncl), new_rot(3, 3, ncl)) + pass_tol_eff = PASS_TOL DO e = 1, ncl ext_pt(e) = find_point(points, lines(cl(e))%nodeA) + IF (line_torsion_active(lines(cl(e)))) pass_tol_eff = PASS_TOL_TORSION END DO ext_fm = CD_ZERO + ext_km = CD_ZERO + ext_rot = CD_ZERO + DO e = 1, ncl + ext_rot(1, 1, e) = CD_ONE + ext_rot(2, 2, e) = CD_ONE + ext_rot(3, 3, e) = CD_ONE + END DO DO pass = 1, MAX_PASS bodies = bodies0 rods = rods0 points = points0 - CALL solve_body_equilibrium_core(types, bodies, rod_types, rods, points, lines, sections, opts, ErrStat, & - ErrMsg, bathymetry, ext_pt=ext_pt, ext_fm=ext_fm, end_force_a=fs, & - quiet=pass > 1, skipped=held) + ! the torque stiffness only with torsion: without it the core's arithmetic is exactly that + ! of a deck before torsion existed (no zero-stiffness correction, signed zeros kept) + IF (pass_tol_eff < PASS_TOL) THEN + CALL solve_body_equilibrium_core(types, bodies, rod_types, rods, points, lines, sections, opts, ErrStat, & + ErrMsg, bathymetry, ext_pt=ext_pt, ext_fm=ext_fm, end_force_a=fs, & + quiet=pass > 1, skipped=held, ext_km=ext_km, ext_rot=ext_rot) + ELSE + CALL solve_body_equilibrium_core(types, bodies, rod_types, rods, points, lines, sections, opts, ErrStat, & + ErrMsg, bathymetry, ext_pt=ext_pt, ext_fm=ext_fm, end_force_a=fs, & + quiet=pass > 1, skipped=held) + END IF IF (ErrStat /= CD_DECKDRV_OK) RETURN ! the objects stay at their deck poses (the core has already said why): nothing to balance IF (held) RETURN scale = CD_ONE DO e = 1, ncl - CALL hermite_end_loads(cl(e), new_fm(:, e)) + CALL hermite_end_loads(cl(e), new_fm(:, e), new_km(:, :, e), new_rot(:, :, e)) IF (ErrStat /= CD_DECKDRV_OK) RETURN new_fm(1:3, e) = new_fm(1:3, e) - fs(:, cl(e)) scale = MAX(scale, NORM2(fs(:, cl(e))), NORM2(new_fm(1:3, e) + fs(:, cl(e)))) @@ -20887,9 +21366,11 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, change = MAXVAL(ABS(new_fm - ext_fm)) eworst = MAXLOC(MAXVAL(ABS(new_fm - ext_fm), DIM=1), DIM=1) ext_fm = new_fm - IF (change <= PASS_TOL*scale) EXIT + ext_km = new_km + ext_rot = new_rot + IF (change <= pass_tol_eff*scale) EXIT END DO - IF (change > PASS_TOL*scale) THEN + IF (change > pass_tol_eff*scale) THEN WRITE (b1, '(ES10.3)') change/scale CALL fail_solve(ErrStat, ErrMsg, 'static body/rod equilibrium with the clamped or elastic END CONNECTION '// & 'of finite-EI LINE '//TRIM(int_to_str(lines(cl(eworst))%id))//' did not converge after '// & @@ -20903,11 +21384,14 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, CONTAINS - SUBROUTINE hermite_end_loads(jl, fm) + SUBROUTINE hermite_end_loads(jl, fm, km, rot) !! The cubic-Hermite static end force and connection moment of line jl on its End A object, - !! at the current (solved) object poses and point positions. + !! at the current (solved) object poses and point positions; for a line with condensed + !! torsion, km = (1/C) a a^T, the torque's moment stiffness against a rotation of the object + !! with the cable shape held (a = dTheta/dw: the End A frame turns, and a clamped end's + !! tangent with it), in global axes, and rot the object's orientation (km = 0 otherwise). INTEGER, INTENT(IN) :: jl - REAL(wp), INTENT(OUT) :: fm(6) + REAL(wp), INTENT(OUT) :: fm(6), km(3, 3), rot(3, 3) TYPE(CD_HFMF_ModuleType) :: cab TYPE(DeckLine) :: lnc TYPE(DeckPoint), ALLOCATABLE :: pc(:) @@ -20916,8 +21400,10 @@ SUBROUTINE hermite_end_loads(jl, fm) CHARACTER(512) :: note CHARACTER(300) :: em fm = CD_ZERO + km = CD_ZERO lnc = lines(jl) CALL clamp_object_frame(lnc, points, bodies, rods, rclamp, dclamp, ErrStat, ErrMsg, check_axis=.FALSE.) + rot = rclamp IF (ErrStat /= CD_DECKDRV_OK) RETURN lnc%endconn_ez_ref(:, 1) = dclamp pc = points @@ -20932,6 +21418,26 @@ SUBROUTINE hermite_end_loads(jl, fm) IF (es /= CD_HFMF_OK) CALL fail_solve(ErrStat, ErrMsg, 'finite-EI LINE '//TRIM(int_to_str(lnc%id))// & ' static end loads failed: '//TRIM(em)) END IF + IF (ErrStat == CD_DECKDRV_OK .AND. cab%line%torsion%active) THEN + BLOCK + REAL(wp) :: th, mt, eg(6), a(3), ag(3), cc + REAL(wp), ALLOCATABLE :: gq(:) + INTEGER :: nb + ALLOCATE (gq(cab%line%ndof)) + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em, gq, eg) + IF (es == CD_HCDYN_OK) THEN + ! End A is the cable's last node (internal end 2) + a = eg(4:6) + IF (cab%line%endconn_mode(2) == CD_ENDCONN_RIGID) THEN + nb = cab%line%ndof - 3 + a = a + cross3(cab%line%q(nb + 1:nb + 3), gq(nb + 1:nb + 3)) + END IF + ag = [cab%frame_c*a(1) - cab%frame_s*a(2), cab%frame_s*a(1) + cab%frame_c*a(2), a(3)] + cc = CD_HermiteTorsion_Compliance(cab%line%torsion, cab%line%l0) + km = SPREAD(ag, 2, 3)*SPREAD(ag, 1, 3)/cc + END IF + END BLOCK + END IF CALL CD_HFMF_End(cab) END SUBROUTINE hermite_end_loads END SUBROUTINE solve_body_equilibrium @@ -23095,7 +23601,10 @@ SUBROUTINE write_hermite_dynamic_out(out_root, channels, lines, points, opts, ca CALL fail(ErrStat, ErrMsg, 'Hermite standalone cable count does not match line count'); RETURN END IF IF (opts%has_motion_file) THEN - CALL read_motion_file(opts%motion_file, points, opts, motion, ErrStat, ErrMsg) + CALL read_motion_file(opts%motion_file, points, opts, motion, ErrStat, ErrMsg, & + with_roll=ANY([(line_torsion_active(lines(i)), i=1, SIZE(lines))])) + IF (ErrStat /= CD_DECKDRV_OK) RETURN + CALL check_motion_roll(ErrStat, ErrMsg) IF (ErrStat /= CD_DECKDRV_OK) RETURN ! Install row 1 without advancing the cable clock. The first pass makes the ! t=0 fluid sample use the prescribed geometry. The held-field setter refreshes @@ -23171,6 +23680,12 @@ SUBROUTINE write_hermite_dynamic_out(out_root, channels, lines, points, opts, ca u_orientation=TRANSPOSE(motion%rot(:, :, step + 1)), & u_angular_velocity=motion%omega(:, step + 1), & u_angular_acceleration=motion%alpha(:, step + 1)) + ELSE IF (motion%has_roll .AND. line_torsion_active(lines(i)) .AND. & + find_motion_point(motion, lines(i)%nodeA) > 0) THEN + ! imposed roll of End A about its director: Phi = Pretwist(B) - (Pretwist(A) + roll) + CALL CD_HFMF_UpdateStates(cables(i), up, uv, ua, es, em, & + u_twist=lines(i)%tors_pretwist(2) - lines(i)%tors_pretwist(1) - & + motion%roll(find_motion_point(motion, lines(i)%nodeA), step + 1)) ELSE CALL CD_HFMF_UpdateStates(cables(i), up, uv, ua, es, em) END IF @@ -23238,6 +23753,39 @@ SUBROUTINE write_hermite_dynamic_out(out_root, channels, lines, points, opts, ca converged = ErrStat == CD_DECKDRV_OK IF (converged) CALL range_write(ranges, out_root, ErrStat, ErrMsg) converged = ErrStat == CD_DECKDRV_OK + + CONTAINS + + SUBROUTINE check_motion_roll(est, emt) + !! The motionFile roll column drives a torsional line through its moving end only (internal + !! End A after the anchor-first swap: the user's End A, or End B of an anchor-first deck), + !! right-handed about the tangent pointing into the line, so Phi = Pretwist(B) - + !! Pretwist(A) - roll in either order; it starts from the static state (roll 0 at t = 0). + INTEGER, INTENT(OUT) :: est + CHARACTER(*), INTENT(OUT) :: emt + INTEGER :: ip, li + LOGICAL :: used + est = CD_DECKDRV_OK + emt = '' + IF (.NOT. motion%has_roll) RETURN + DO ip = 1, motion%n_point + IF (.NOT. ANY(ABS(motion%roll(ip, :)) > CD_ZERO)) CYCLE + used = .FALSE. + DO li = 1, SIZE(lines) + IF (lines(li)%nodeA == motion%point_ids(ip) .AND. line_torsion_active(lines(li))) used = .TRUE. + END DO + IF (.NOT. used) THEN + CALL fail(est, emt, 'motionFile: point '//TRIM(int_to_str(motion%point_ids(ip)))//' has a non-zero roll '// & + 'column, but no line restrained in torsion at both ends (END CONNECTIONS TorsStiffness) '// & + 'has its moving (non-Fixed) end there'); RETURN + END IF + IF (ABS(motion%roll(ip, 1)) > CD_ZERO) THEN + CALL fail(est, emt, 'motionFile: the roll column of point '//TRIM(int_to_str(motion%point_ids(ip)))// & + ' must be 0 at t = 0 (the static solve uses the END CONNECTIONS Pretwist, which takes a '// & + 'constant twist)'); RETURN + END IF + END DO + END SUBROUTINE check_motion_roll END SUBROUTINE write_hermite_dynamic_out SUBROUTINE write_hermite_static_profile(out_root, lines, cables, ErrStat, ErrMsg) @@ -26463,7 +27011,12 @@ SUBROUTINE prescribed_point_state(point, motion, time_index, q, v, a, ErrStat, E END SELECT END SUBROUTINE prescribed_point_state - SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include_rod_points, include_body_points) + SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include_rod_points, include_body_points, & + with_roll) + !! Prescribed point motion: rows "time point_id x y z vx vy vz ax ay az". with_roll (a deck + !! with a torsional line) also reads an optional 12th column, the roll [deg] of the line end + !! frames at that point about their directors (motion%roll, radians); a point gives it on + !! every row or on none, and without with_roll a 12th token stays commentary. CHARACTER(*), INTENT(IN) :: path TYPE(DeckPoint), INTENT(IN) :: points(:) TYPE(DeckOptions), INTENT(IN) :: opts @@ -26472,8 +27025,12 @@ SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include CHARACTER(*), INTENT(OUT) :: ErrMsg LOGICAL, INTENT(IN), OPTIONAL :: include_rod_points LOGICAL, INTENT(IN), OPTIONAL :: include_body_points + LOGICAL, INTENT(IN), OPTIONAL :: with_roll INTEGER :: unit, ios, ip, it, pid, nstep, lineno, row_status, ipt + LOGICAL :: read_roll, roll_ok + LOGICAL, ALLOCATABLE :: roll_seen(:, :) + CHARACTER(DECK_RECLEN) :: roll_tok REAL(wp) :: t, qv(3), vv(3), av(3), tol, row(11), trow(21), rq(3, 3), rp(3) CHARACTER(DECK_RECLEN) :: line CHARACTER(256) :: why @@ -26487,6 +27044,8 @@ SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include accept_body_points = .FALSE. IF (PRESENT(include_rod_points)) accept_rod_points = include_rod_points IF (PRESENT(include_body_points)) accept_body_points = include_body_points + read_roll = .FALSE. + IF (PRESENT(with_roll)) read_roll = with_roll CALL grid_step_count(opts%tmax, opts%dtM, nstep, grid_ok) IF (.NOT. grid_ok .OR. nstep < 0) THEN CALL fail(ErrStat, ErrMsg, 'motionFile needs finite dtM > 0 and TMax >= 0 with a representable step count') @@ -26518,6 +27077,13 @@ SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include motion%v = CD_ZERO motion%a = CD_ZERO seen = .FALSE. + ALLOCATE (roll_seen(MERGE(motion%n_point, 0, read_roll), MERGE(motion%n_time, 0, read_roll)), STAT=ios) + IF (ios == 0 .AND. read_roll) ALLOCATE (motion%roll(motion%n_point, motion%n_time), STAT=ios) + IF (ios /= 0) THEN + CALL fail(ErrStat, ErrMsg, 'cannot allocate the motionFile roll table'); RETURN + END IF + roll_seen = .FALSE. + IF (read_roll) motion%roll = CD_ZERO tol = 100.0_wp*EPSILON(CD_ONE)*MAX(CD_ONE, opts%tmax) IF (opts%motion_kind /= MOTION_POINT_ROWS) THEN IF (accept_rod_points .OR. accept_body_points) THEN @@ -26698,8 +27264,35 @@ SUBROUTINE read_motion_file(path, points, opts, motion, ErrStat, ErrMsg, include motion%v(:, ip, it) = vv motion%a(:, ip, it) = av seen(ip, it) = .TRUE. + IF (read_roll) THEN + ! the optional roll column: a 12th token shaped like a number must be one + CALL get_token(line, 12, roll_tok, roll_ok) + IF (roll_ok) roll_ok = numeric_token_shape(roll_tok) + IF (roll_ok) THEN + CALL parse_real_token(roll_tok, motion%roll(ip, it), roll_ok) + IF (roll_ok) roll_ok = IEEE_IS_FINITE(motion%roll(ip, it)) + IF (.NOT. roll_ok) THEN + CLOSE (unit) + CALL fail(ErrStat, ErrMsg, 'the roll column (12th, degrees) must be a finite number') + CALL add_row_context('motionFile', lineno, line, ErrMsg) + RETURN + END IF + motion%roll(ip, it) = motion%roll(ip, it)*QUARTER_PI/45.0_wp + roll_seen(ip, it) = .TRUE. + END IF + END IF END DO CLOSE (unit) + IF (read_roll) THEN + DO ip = 1, motion%n_point + IF (ANY(roll_seen(ip, :)) .AND. .NOT. ALL(roll_seen(ip, :))) THEN + CALL fail(ErrStat, ErrMsg, 'motionFile: point '//TRIM(int_to_str(motion%point_ids(ip)))//' gives the '// & + 'roll column (12th) on some rows only; give it on every row of the point or on none') + RETURN + END IF + END DO + motion%has_roll = ANY(roll_seen) + END IF IF (.NOT. ALL(seen)) THEN IF (accept_rod_points .OR. accept_body_points) THEN CALL fail(ErrStat, ErrMsg, 'motionFile must provide every eligible prescribed point at every dtM time') @@ -27350,6 +27943,26 @@ SUBROUTINE check_channel(ch, lines, points, sections, ErrStat, ErrMsg, bodies, r IF (comp < 1 .OR. comp > 3 .OR. point_index(points, pidx) == 0) THEN CALL fail(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": bad point id or component'); RETURN END IF + ELSE IF (starts_with(lo, 'torq') .OR. starts_with(lo, 'twist')) THEN + CALL parse_line_node_channel(ch, line_id, node, kind, comp) + li = find_line(lines, line_id) + IF (li /= 0 .AND. node == 0 .AND. names_node_zero(lo)) THEN + CALL fail(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": node numbers start at 1 = End A (MoorDyn '// & + 'numbers from N0)'); RETURN + END IF + IF (li == 0 .OR. kind == 0 .OR. node < 1) THEN + CALL fail(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": bad torsion channel (use TorqN, '// & + 'TwistN or Twist, with a known line id)'); RETURN + END IF + nelem = line_nelem_from_sections(lines(li)%id, sections) + IF (node > nelem + 1) THEN + CALL fail(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": node index exceeds line node count'); RETURN + END IF + IF (.NOT. line_torsion_active(lines(li))) THEN + CALL fail(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": line '//TRIM(int_to_str(line_id))//' is not '// & + 'torsionally restrained at both ends (END CONNECTIONS TorsStiffness), so it carries no '// & + 'torque'); RETURN + END IF ELSE IF (starts_with(lo, 'ten') .OR. starts_with(lo, 'curv') .OR. & starts_with(lo, 'bendmom') .OR. starts_with(lo, 'l')) THEN CALL parse_line_node_channel(ch, line_id, node, kind, comp) @@ -27370,7 +27983,7 @@ SUBROUTINE check_channel(ch, lines, points, sections, ErrStat, ErrMsg, bodies, r '" not supported (use FairTen, AnchTen, FairIncl, AnchIncl, '// & 'FairDecl, AnchDecl (FairAngle/AnchAngle aliases), '// & 'Point

p{x,y,z} (alias Con

p{x,y,z}), Point

F{x,y,z,H}, '// & - 'TenN, CurvN, BendMomN, '// & + 'TenN, CurvN, BendMomN, TorqN, TwistN, Twist, '// & 'LNp{x,y,z}, LNv{x,y,z}, LNa{x,y,z}, LNDec, LNAzi, '// & 'TDP{s,x,y,z,Lay,Exc}, or the Body/Rod channels)'); RETURN END IF @@ -27561,6 +28174,9 @@ SUBROUTINE parse_line_node_channel(ch, line_id, node_id, kind, comp) !! LNa{x,y,z} -> kind=4, comp=1..3 acceleration [m/s^2] !! LNDec -> kind=7, comp=0 declination [deg] !! LNAzi -> kind=8, comp=0 azimuth [deg] + !! TorqN -> kind=9, comp=0 line torque [N.m] + !! TwistN -> kind=10, comp=0 twist from End A [deg] + !! Twist -> kind=11, comp=0 total twist (node 1) [deg] CHARACTER(*), INTENT(IN) :: ch INTEGER, INTENT(OUT) :: line_id, node_id, kind, comp @@ -27573,8 +28189,27 @@ SUBROUTINE parse_line_node_channel(ch, line_id, node_id, kind, comp) kind = 0 comp = 0 last = LEN_TRIM(lo) - ! Scalar-per-node channels with a leading keyword: Ten / Curv / BendMom. - IF (starts_with(lo, 'ten')) THEN + ! Scalar-per-node channels with a leading keyword: Ten / Curv / BendMom / Torq / Twist, and + ! the line total Twist (kind 11, node 1). + IF (starts_with(lo, 'torq')) THEN + CALL parse_keyword_line_node(lo, 4, last, line_id, node_id) + IF (line_id > 0 .AND. node_id > 0) kind = 9 + RETURN + ELSE IF (starts_with(lo, 'twist')) THEN + IF (last > 5) THEN + IF (INDEX(lo(6:last), 'n') == 0) THEN + line_id = parse_positive_int(lo, 6, last) + IF (line_id > 0) THEN + node_id = 1 + kind = 11 + END IF + RETURN + END IF + END IF + CALL parse_keyword_line_node(lo, 5, last, line_id, node_id) + IF (line_id > 0 .AND. node_id > 0) kind = 10 + RETURN + ELSE IF (starts_with(lo, 'ten')) THEN CALL parse_keyword_line_node(lo, 3, last, line_id, node_id) IF (line_id > 0 .AND. node_id > 0) kind = 3 RETURN @@ -28206,6 +28841,13 @@ SUBROUTINE range_setup(rset, lines, channels, opts, nnodes, by_id, ErrStat, ErrM CALL CD_Range_Setup(rset, ids, nnodes, want_r, want_t, opts%range_start, opts%has_wtrdpth, -opts%wtrdpth, & es, em, bathymetry=bathymetry) CALL range_status(es, em, ErrStat, ErrMsg) + IF (ErrStat /= CD_DECKDRV_OK .OR. .NOT. ALLOCATED(rset%lines)) RETURN + ! a line with condensed torsion adds its torque and twist envelopes + DO i = 1, SIZE(lines) + k = i + IF (by_id) k = lines(i)%id + IF (k >= 1 .AND. k <= SIZE(rset%lines)) rset%lines(k)%has_torsion = line_torsion_active(lines(i)) + END DO END SUBROUTINE range_setup SUBROUTINE range_setup_planned(rset, lines, channels, opts, ErrStat, ErrMsg, models, fmodels, system, cables, & @@ -28450,6 +29092,17 @@ SUBROUTINE range_sample_cable(cable, rl, ErrStat, ErrMsg) rl%val(CD_RQ_CURVATURE, j) = curv rl%val(CD_RQ_BEND, j) = bend END DO + IF (rl%has_torsion .AND. cable%line%torsion%active) THEN + ! condensed torsion: the committed torque (uniform) and the cable's own twist from End A + ! (public node 1, the last internal node), as the TorqN and TwistN channels + rl%val(CD_RQ_TORQUE, :) = cable%line%torsion%torque + rl%val(CD_RQ_TWIST, 1) = CD_ZERO + DO j = 2, nn + inode = nn + 1 - j + rl%val(CD_RQ_TWIST, j) = rl%val(CD_RQ_TWIST, j - 1) + cable%line%torsion%torque* & + cable%line%l0(inode)/cable%line%torsion%gj(inode)*45.0_wp/QUARTER_PI + END DO + END IF END SUBROUTINE range_sample_cable SUBROUTINE aggregate_line_positions(system, cables, line_is_cable, line_obj_index, line_id, r, ErrStat, ErrMsg) @@ -29188,6 +29841,27 @@ SUBROUTINE eval_cable_channel(cable, ch, val, ErrStat, ErrMsg) IF (es /= CD_HCABLE_OK) THEN CALL fail_solve(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": '//TRIM(em)); RETURN END IF + CASE (9, 10, 11) ! condensed torsion: torque [N m], twist from End A to node J, total twist [deg] + BLOCK + REAL(wp) :: th, mt + INTEGER :: e + CALL CD_HermiteCable_Dyn_Torsion_State(cable%line, th, mt, es, em) + IF (es /= CD_HCDYN_OK) THEN + CALL fail_solve(ErrStat, ErrMsg, 'OUTPUT "'//TRIM(ch)//'": '//TRIM(em)); RETURN + END IF + IF (kind == 9) THEN + val = mt + ELSE IF (kind == 11) THEN + val = mt*CD_HermiteTorsion_Compliance(cable%line%torsion, cable%line%l0)*45.0_wp/QUARTER_PI + ELSE IF (cable%line%torsion%active) THEN + ! End A is the last internal node: the cable's own twist over the elements inode..ne + val = CD_ZERO + DO e = inode, nelem + val = val + cable%line%l0(e)/cable%line%torsion%gj(e) + END DO + val = mt*val*45.0_wp/QUARTER_PI + END IF + END BLOCK CASE (8) ! azimuth: local-frame azimuth plus the cable's frame heading, wrapped val = line_geom_value(kind, node, nnode, cable%line%q(1:6*nnode), 6) IF (.NOT. cable_frame_is_identity(cable)) THEN @@ -29948,6 +30622,22 @@ PURE FUNCTION ptfm_zyx_dcm(theta) RESULT(m) m(3, 3) = cx*cy END FUNCTION ptfm_zyx_dcm + PURE LOGICAL FUNCTION line_torsion_active(ln) RESULT(active) + !! Condensed torsion is solved on a line only when both ends declare a torsional restraint. + TYPE(DeckLine), INTENT(IN) :: ln + active = ALL(ln%tors_mode /= TORS_FREE) + END FUNCTION line_torsion_active + + PURE LOGICAL FUNCTION deck_has_torsion_columns(lines) RESULT(has) + !! Some END CONNECTIONS row declares a torsional restraint (TorsStiffness not Free). + TYPE(DeckLine), INTENT(IN) :: lines(:) + INTEGER :: i + has = .FALSE. + DO i = 1, SIZE(lines) + IF (ANY(lines(i)%tors_mode /= TORS_FREE)) has = .TRUE. + END DO + END FUNCTION deck_has_torsion_columns + LOGICAL FUNCTION line_is_finite_ei(ln, types, sections) RESULT(is_finite) !! A LINE is a finite-EI (bending) cable iff ANY of its sections has EI > 0. !! Factored from all_lines_have_finite_ei's per-line finite test so the OpenFAST diff --git a/src/CableDyn_HermiteCableDynamic.f90 b/src/CableDyn_HermiteCableDynamic.f90 index 2c665d4..638a66e 100644 --- a/src/CableDyn_HermiteCableDynamic.f90 +++ b/src/CableDyn_HermiteCableDynamic.f90 @@ -69,6 +69,10 @@ MODULE CableDyn_HermiteCableDynamic CD_Seabed_Friction_Mu_Dir, CD_FRICTION_STICK_SLIP USE CableDyn_Bathymetry, ONLY: CD_BathymetryType, CD_Bathymetry_Is_Initialized, & CD_Bathymetry_Floor_Gradient, CD_End_Bathymetry, CD_BATHY_OK + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Line, CD_HermiteTorsion_Unwrap, & + CD_HermiteTorsion_Compliance, CD_HermiteTorsion_Validate, CD_HTORS_OK, & + CD_HTORS_MAX_STEP, CD_HermiteTorsionWorkType, CD_HermiteTorsion_Work_Init, & + CD_HermiteTorsion_Work_End, CD_HermiteTorsion_Work_Add_Hessian USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: INT64 !$ USE OMP_LIB, ONLY: omp_get_max_threads, omp_in_parallel IMPLICIT NONE @@ -79,6 +83,10 @@ MODULE CableDyn_HermiteCableDynamic PUBLIC :: CD_HermiteCable_Dyn_Set_Drag PUBLIC :: CD_HermiteCable_Dyn_Set_Axial_Damping PUBLIC :: CD_HermiteCable_Dyn_Set_EndConnection + PUBLIC :: CD_HermiteCable_Dyn_Set_Torsion + PUBLIC :: CD_HermiteCable_Dyn_Torsion_State + PUBLIC :: CD_HermiteCable_Dyn_Set_Torsion_Drive + PUBLIC :: CD_HermiteCable_Dyn_Set_Torsion_Frames PUBLIC :: CD_HermiteCable_Dyn_EndConnection_Moment PUBLIC :: CD_HermiteCable_Dyn_Set_ModifiedNewton PUBLIC :: CD_HermiteCable_Dyn_Set_TangentReuse @@ -311,6 +319,33 @@ MODULE CableDyn_HermiteCableDynamic REAL(wp), ALLOCATABLE :: snap_q(:), snap_v(:), snap_a(:) REAL(wp) :: snap_t = CD_ZERO REAL(wp) :: snap_endconn_d0(3, 2) = CD_ZERO + ! Condensed torsion (CD_HermiteCable_Dyn_Set_Torsion): end frames, GJ, imposed twist Phi and + ! the accepted (unwrapped) Theta of the committed state; the residual, the tangent, the end + ! loads and the energy include E_t = (Phi - Theta)**2/(2 C). A step drives the end frames and + ! Phi to the target of CD_HermiteCable_Dyn_Set_Torsion_Drive (held without one): the step + ! keeps that target in tors_step_ends/tors_step_phi and the residual uses it (tors_use_step) + ! once the committed force F_n is evaluated; the commit makes it the committed frames and Phi, + ! with Theta at q_{n+1}. Allocation-free step scratch: tors_work (the kernel), tors_g + ! (dTheta/dq at the last tangent), tors_gq (at the last residual), tors_gk and tors_zk (tors_g + ! in the solve coordinates and K_eff^-1 of it, kept with the factor) and tors_den + ! (C/(1 - alpha_f) + tors_gk . tors_zk) of the Sherman-Morrison (bordered) solve. snap_* are + ! the frames, Phi and Theta of the last snapshot. + TYPE(CD_HermiteTorsionType) :: torsion + TYPE(CD_HermiteTorsionWorkType) :: tors_work + REAL(wp), ALLOCATABLE :: tors_g(:), tors_gq(:), tors_gk(:), tors_zk(:) + REAL(wp) :: tors_den = CD_ZERO + LOGICAL :: tors_use_step = .FALSE., tors_drive_set = .FALSE. + REAL(wp) :: tors_step_ends(3, 4) = CD_ZERO, tors_step_phi = CD_ZERO + REAL(wp) :: tors_drive_ends(3, 4) = CD_ZERO, tors_drive_phi = CD_ZERO + REAL(wp) :: snap_torsion_theta = CD_ZERO, snap_torsion_phi = CD_ZERO, snap_torsion_ends(3, 4) = CD_ZERO + ! Torsion query cache (allocation-free read-only queries): Theta, dTheta/d[w_1, w_2] and + ! dTheta/dr at both end nodes, evaluated at tors_c_q with the end frames tors_c_ends and + ! unwrapped against tors_c_prev. A query uses it only when the model's q, end frames and + ! accepted Theta are bitwise those, so it can never be stale; otherwise it re-evaluates. + REAL(wp), ALLOCATABLE :: tors_c_q(:) + REAL(wp) :: tors_c_ends(3, 4) = CD_ZERO, tors_c_prev = CD_ZERO, tors_c_theta = CD_ZERO + REAL(wp) :: tors_c_eg(6) = CD_ZERO, tors_c_gend(3, 2) = CD_ZERO + LOGICAL :: tors_c_valid = .FALSE. LOGICAL :: snap_valid = .FALSE. ! Adaptive-Newton warmup counters are step-history too: a staged step (aggregate ! stage-then-commit) that advances them and is then rolled back must restore them, or the @@ -2293,6 +2328,161 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i !! e.g. a moving fairlead/hang-off heave driving a lazy-wave cable. These DOFs are held at !! pres_q during the Newton solve and their prescribed q/v/a enter the free-DOF residual !! through the mass and stiffness coupling; all four arrays must be supplied together. + !! + !! CONDENSED TORSION (CD_HermiteCable_Dyn_Set_Torsion): the end frames and Phi move to the + !! target of CD_HermiteCable_Dyn_Set_Torsion_Drive (consumed by this call; held without one). + !! The torsion is quasi-static (no torsional inertia): it adds -M_t dTheta/dq to the residual, + !! -M_t d2Theta/dq2 to the tangent and the rank-one (1 - alpha_f) g g^T / C, solved by + !! Sherman-Morrison on the band factor with g frozen with the factor (so a reused factor stays + !! consistent). Theta is unwrapped against the committed value and may change by at most pi/2 + !! in a step (a larger change fails the evaluation, and the line search or the recovering step + !! cuts it). It needs the force-blended generalised-alpha. + TYPE(CD_HermiteCableDynType), INTENT(INOUT), TARGET :: model + REAL(wp), INTENT(IN) :: dt, tol + INTEGER, INTENT(IN) :: max_iter + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER, INTENT(OUT), OPTIONAL :: iters_out + REAL(wp), INTENT(OUT), OPTIONAL :: res_out + INTEGER, INTENT(IN), OPTIONAL :: pres_dofs(:) + REAL(wp), INTENT(IN), OPTIONAL :: pres_q(:), pres_v(:), pres_a(:) + REAL(wp), INTENT(OUT), OPTIONAL :: residual_history(:) + INTEGER, INTENT(OUT), OPTIONAL :: history_count + REAL(wp), INTENT(IN), OPTIONAL :: endconn_direction(3, 2) + REAL(wp), INTENT(IN), OPTIONAL :: endconn_direction_rate(3, 2), endconn_direction_acceleration(3, 2) + LOGICAL, INTENT(OUT), OPTIONAL :: tensile_failed + INTEGER :: iend + REAL(wp) :: dn, dvec(3), nvec(3) + + model%tors_use_step = .FALSE. + IF (model%torsion%active) THEN + IF (.NOT. model%force_blend) THEN + IF (PRESENT(iters_out)) iters_out = 0 + IF (PRESENT(res_out)) res_out = CD_ZERO + IF (PRESENT(history_count)) history_count = 0 + IF (PRESENT(residual_history)) residual_history = CD_ZERO + IF (PRESENT(tensile_failed)) tensile_failed = .FALSE. + model%tors_drive_set = .FALSE. + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Step: torsion needs the force-blended generalised-alpha (alpha_force_blend)' + RETURN + END IF + IF (model%tors_drive_set) THEN + model%tors_step_ends = model%tors_drive_ends + model%tors_step_phi = model%tors_drive_phi + ELSE + model%tors_step_ends = model%torsion%ends + model%tors_step_phi = model%torsion%phi + END IF + model%tors_drive_set = .FALSE. + ! A rigid end's director is its connection direction: take the step's exactly and keep the + ! reference normal perpendicular to it (the smallest correction). + IF (PRESENT(endconn_direction)) THEN + DO iend = 1, 2 + IF (model%endconn_mode(iend) /= CD_ENDCONN_RIGID) CYCLE + dn = NORM2(endconn_direction(:, iend)) + IF (.NOT. (dn > CD_ZERO) .OR. .NOT. CD_Is_Finite(dn)) CYCLE + dvec = endconn_direction(:, iend)/dn + nvec = model%tors_step_ends(:, 2*iend) + nvec = nvec - DOT_PRODUCT(nvec, dvec)*dvec + dn = NORM2(nvec) + IF (.NOT. (dn > 1.0e-6_wp)) CYCLE + model%tors_step_ends(:, 2*iend - 1) = dvec + model%tors_step_ends(:, 2*iend) = nvec/dn + END DO + END IF + END IF + CALL dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, res_out, pres_dofs, pres_q, pres_v, & + pres_a, residual_history, history_count, endconn_direction, endconn_direction_rate, & + endconn_direction_acceleration, tensile_failed) + model%tors_use_step = .FALSE. + END SUBROUTINE CD_HermiteCable_Dyn_Step + + SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Drive(model, ends, phi, ErrStat, ErrMsg) + !! Target end frames (columns d_1, n_1, d_2, n_2 in the solve frame, orthonormal per end) and + !! imposed twist Phi [rad, unwrapped] of the condensed torsion at the end of the next step + !! (CD_HermiteCable_Dyn_Step or _Step_Recovering, which interpolates them over its substeps). + !! A turning parent turns its end frame; an imposed roll history changes Phi. The next step + !! consumes the target; without one the frames and Phi are held. + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + REAL(wp), INTENT(IN) :: ends(3, 4), phi + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: k + ErrStat = CD_HCDYN_BADINPUT + IF (.NOT. model%initialized .OR. .NOT. model%torsion%active) THEN + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Drive: the model has no torsion' + RETURN + END IF + IF (.NOT. (CD_All_Finite(RESHAPE(ends, [12])) .AND. CD_Is_Finite(phi))) THEN + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Drive: non-finite end frame or imposed twist' + RETURN + END IF + DO k = 1, 3, 2 + IF (ABS(NORM2(ends(:, k)) - CD_ONE) > 1.0e-8_wp .OR. ABS(NORM2(ends(:, k + 1)) - CD_ONE) > 1.0e-8_wp .OR. & + ABS(DOT_PRODUCT(ends(:, k), ends(:, k + 1))) > 1.0e-8_wp) THEN + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Drive: each end director and reference normal must be orthonormal' + RETURN + END IF + END DO + model%tors_drive_ends = ends + model%tors_drive_phi = phi + model%tors_drive_set = .TRUE. + ErrStat = CD_HCDYN_OK + ErrMsg = '' + END SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Drive + + SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Frames(model, ends, ErrStat, ErrMsg, q_check) + !! Move the COMMITTED torsion end frames without a step (a host writing its boundary between + !! steps): Theta is re-evaluated at the committed state with the new frames, unwrapped + !! against its value (at most pi/2 away), and the torque follows. Fails closed, unchanged. + !! With q_check, only check that the frames would be accepted at the configuration q_check + !! (the state a host is about to write) and change nothing but scratch. + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + REAL(wp), INTENT(IN) :: ends(3, 4) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(IN), OPTIONAL :: q_check(:) + REAL(wp) :: th, drive_ends(3, 4), drive_phi + LOGICAL :: drive_was_set + drive_was_set = model%tors_drive_set + drive_ends = model%tors_drive_ends + drive_phi = model%tors_drive_phi + ! validate the frames (Set_Torsion_Drive), then put the drive target back as it was + CALL CD_HermiteCable_Dyn_Set_Torsion_Drive(model, ends, model%torsion%phi, ErrStat, ErrMsg) + model%tors_drive_set = drive_was_set + model%tors_drive_ends = drive_ends + model%tors_drive_phi = drive_phi + IF (ErrStat /= CD_HCDYN_OK) RETURN + IF (PRESENT(q_check)) THEN + IF (SIZE(q_check) /= model%ndof) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Frames: q_check must have ndof entries' + RETURN + END IF + CALL torsion_theta_at(model, q_check, ends, th, ErrStat, ErrMsg) + IF (ErrStat /= CD_HCDYN_OK) ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Frames: '//TRIM(ErrMsg) + RETURN + END IF + CALL torsion_theta_at(model, model%q, ends, th, ErrStat, ErrMsg) + IF (ErrStat /= CD_HCDYN_OK) THEN + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion_Frames: '//TRIM(ErrMsg) + RETURN + END IF + ! the written frames supersede a pending drive target + model%tors_drive_set = .FALSE. + model%torsion%ends = ends + model%torsion%theta = th + model%torsion%torque = (model%torsion%phi - th)/CD_HermiteTorsion_Compliance(model%torsion, model%l0) + model%fc_valid = .FALSE. + model%tr_valid = .FALSE. + END SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Frames + + SUBROUTINE dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, res_out, & + pres_dofs, pres_q, pres_v, pres_a, residual_history, history_count, & + endconn_direction, endconn_direction_rate, endconn_direction_acceleration, & + tensile_failed) + !! The generalised-alpha step of CD_HermiteCable_Dyn_Step (which sets up the torsion target). TYPE(CD_HermiteCableDynType), INTENT(INOUT), TARGET :: model REAL(wp), INTENT(IN) :: dt, tol INTEGER, INTENT(IN) :: max_iter @@ -2312,8 +2502,8 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i INTEGER :: ndof, it, es, i, np, ip, ls, gdof, nsolve_step, axial_element, axial_es INTEGER :: axial_worst_element, iv, attempt, n_attempt - REAL(wp) :: c_a, c_v, c_a2, am, af, bt, gm, rnorm, fscale, rnt, lambda, chosen, lam_ok, cw, t_alpha - REAL(wp) :: e_bal, e_ma + REAL(wp) :: c_a, c_v, c_a2, am, af, bt, gm, rnorm, fscale, rnt, lambda, chosen, lam_ok, cw, t_alpha, dth_lin + REAL(wp) :: e_bal, e_ma, tors_theta_new REAL(wp) :: axial_qe(12), axial_min REAL(wp) :: axial_threshold, axial_margin, axial_worst_margin, axial_worst_force, axial_worst_xi REAL(wp) :: axial_worst_threshold @@ -2558,6 +2748,9 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i model%fc_d0 = model%endconn_d0 model%fc_valid = .TRUE. END IF + ! Condensed torsion: F_n above used the committed end frames and Phi; every evaluation of the + ! step from here on uses the step's target (set by CD_HermiteCable_Dyn_Step). + model%tors_use_step = model%torsion%active ! Newton predictor. The standard Newmark guess q_n + dt v_n + dt^2 (1/2 - beta) a_n (the ! acceleration map's base a_{n+1} = 0) is a far better start than q_n when the state is ! moving. When the committed acceleration is the physical one, the constant-acceleration @@ -2757,6 +2950,17 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i IF (attempt < n_attempt) CYCLE predict RETURN END IF + IF (model%torsion%active) THEN + ! Rank-one torsion term (1 - af) g g^T / C of the effective tangent: g in the solve + ! coordinates (as the residual), its K^-1 image and the Sherman-Morrison denominator, + ! all kept with this factor. + CALL torsion_factor(es, em2) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = 'CD_HermiteCable_Dyn_Step: '//TRIM(em2) + IF (attempt < n_attempt) CYCLE predict + RETURN + END IF + END IF mn_factored = .TRUE. mn_fresh = .TRUE. factor_ok = .TRUE. @@ -2787,6 +2991,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i IF (attempt < n_attempt) CYCLE predict RETURN END IF + IF (model%torsion%active) CALL torsion_sm_correct() CALL transform_rigid_vector_to_global(model, step_rigid_basis, model%ws_dq) nsolve_step = nsolve_step + 1 IF (PRESENT(iters_out)) iters_out = nsolve_step @@ -2797,6 +3002,12 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i chosen = -CD_ONE lam_ok = -CD_ONE ! smallest step length with a finite eval lambda = CD_ONE + ! Condensed torsion: the linearised twist change of a trial stays within pi/4 (the + ! post-trial unwrap against the committed Theta cannot tell 3 pi/2 from -pi/2) + IF (model%torsion%active) THEN + dth_lin = ABS(DOT_PRODUCT(model%tors_g, model%ws_dq)) + IF (dth_lin > 0.5_wp*CD_HTORS_MAX_STEP) lambda = 0.5_wp*CD_HTORS_MAX_STEP/dth_lin + END IF DO ls = 1, 10 DO iv = 1, SIZE(model%ws_qtrial) model%ws_qtrial(iv) = model%ws_qk(iv) + lambda*model%ws_dq(iv) @@ -2966,6 +3177,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i CALL CD_Solve_Factored_Banded(model%ws_Keffb, KL_D, KU_D, model%ws_ipiv, model%ws_dq, es, em2, & factor_validated=.TRUE.) IF (es == CD_LINALG_OK) THEN + IF (model%torsion%active) CALL torsion_sm_correct() CALL transform_rigid_vector_to_global(model, step_rigid_basis, model%ws_dq) DO iv = 1, SIZE(model%ws_qtrial) model%ws_qtrial(iv) = model%ws_qk(iv) + model%ws_dq(iv) @@ -3081,6 +3293,17 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i END IF END IF + ! Condensed torsion: Theta at q_{n+1} with the step's end frames, unwrapped against the + ! committed value (within pi/2: every accepted evaluation already was), before anything is + ! committed so that a failure leaves the step-start state. + tors_theta_new = model%torsion%theta + IF (model%torsion%active) THEN + CALL torsion_theta_at(model, model%ws_qk, model%tors_step_ends, tors_theta_new, es, em2) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = 'CD_HermiteCable_Dyn_Step: '//TRIM(em2); RETURN + END IF + END IF + ! Adaptive modified Newton: this step CONVERGED (a non-converged step returned above and ! does not count). Accumulate the warmup's full-Newton solve counts and, at the end of ! the warmup, latch tangent reuse on for the rest of the run iff the warmup averaged more @@ -3123,6 +3346,20 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i IF (model%fr_active) CALL commit_friction_anchors(model) model%t = model%t + dt IF (has_endconn_motion) model%endconn_d0 = step_endconn_target + IF (model%torsion%active) THEN + model%torsion%ends = model%tors_step_ends + model%torsion%phi = model%tors_step_phi + model%torsion%theta = tors_theta_new + model%torsion%torque = (model%torsion%phi - tors_theta_new)/ & + CD_HermiteTorsion_Compliance(model%torsion, model%l0) + ! the read-only queries of the committed state (end loads, energy, channels) from the + ! cache; a failure here only leaves it invalid (a query then evaluates and reports it) + BLOCK + INTEGER :: es_tc + CHARACTER(200) :: em_tc + CALL torsion_cache_fill(model, es_tc, em_tc) + END BLOCK + END IF ! Carry the factor of this step's last iteration to the next step. model%tr_valid = model%tangent_reuse .AND. factor_ok IF (xs_start) THEN @@ -3433,7 +3670,84 @@ SUBROUTINE add_rigid_transport_tangent() END DO END SUBROUTINE add_rigid_transport_tangent - END SUBROUTINE CD_HermiteCable_Dyn_Step + SUBROUTINE torsion_factor(esx, emx) + !! After a factorisation of K_eff = B (band part): g = dTheta/dq at the tangent point in + !! the solve coordinates (rigid-end basis, held DOFs zeroed), z = B^-1 g and the + !! Sherman-Morrison denominator C/(1 - af) + g.z of (B + (1 - af) g g^T / C)^-1. + INTEGER, INTENT(OUT) :: esx + CHARACTER(*), INTENT(OUT) :: emx + INTEGER :: ii + REAL(wp) :: cr, gz + esx = CD_HCDYN_OK + emx = '' + DO ii = 1, ndof + model%tors_gk(ii) = model%tors_g(ii) + END DO + CALL transform_rigid_vector_to_local(model, step_rigid_basis, model%tors_gk) + DO ii = 1, ndof + IF (.NOT. model%solve_mask(ii)) model%tors_gk(ii) = CD_ZERO + model%tors_zk(ii) = model%tors_gk(ii) + END DO + CALL CD_Solve_Factored_Banded(model%ws_Keffb, KL_D, KU_D, model%ws_ipiv, model%tors_zk, esx, emx, & + factor_validated=.TRUE.) + IF (esx /= CD_LINALG_OK) THEN + esx = CD_HCDYN_NOCONVERGE + emx = 'torsion: tangent solve: '//TRIM(emx) + RETURN + END IF + esx = CD_HCDYN_OK + cr = CD_HermiteTorsion_Compliance(model%torsion, model%l0)/(CD_ONE - af) + gz = DOT_PRODUCT(model%tors_gk, model%tors_zk) + model%tors_den = cr + gz + IF (.NOT. (ABS(model%tors_den) > 1.0e-12_wp*(cr + ABS(gz))) .OR. .NOT. CD_Is_Finite(model%tors_den)) THEN + esx = CD_HCDYN_NOCONVERGE + emx = 'torsion: the bordered (Sherman-Morrison) tangent is singular' + END IF + END SUBROUTINE torsion_factor + + SUBROUTINE torsion_sm_correct() + !! ws_dq <- ws_dq - z (g . ws_dq)/den: the solve with B turned into one with + !! B + (1 - af) g g^T / C (Sherman-Morrison), with g, z and den of the current factor. + REAL(wp) :: mu + INTEGER :: ii + mu = DOT_PRODUCT(model%tors_gk, model%ws_dq)/model%tors_den + DO ii = 1, ndof + model%ws_dq(ii) = model%ws_dq(ii) - mu*model%tors_zk(ii) + END DO + END SUBROUTINE torsion_sm_correct + + END SUBROUTINE dyn_step_core + + SUBROUTINE torsion_theta_at(model, q_cfg, ends, theta, ErrStat, ErrMsg) + !! Theta at q_cfg with end frames ends, unwrapped against the committed Theta; fails when it + !! moved by more than pi/2 (allocation-free: the model's kernel workspace and tors_gq). + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + REAL(wp), INTENT(IN) :: q_cfg(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: theta + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp) :: raw + INTEGER :: es + CHARACTER(200) :: em + theta = model%torsion%theta + CALL CD_HermiteTorsion_Line(q_cfg, model%l0, ends, raw, model%tors_gq, es, em, & + quadrature_order=model%torsion%quadrature_order, work=model%tors_work) + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: '//TRIM(em) + RETURN + END IF + theta = CD_HermiteTorsion_Unwrap(raw, model%torsion%theta) + IF (ABS(theta - model%torsion%theta) > CD_HTORS_MAX_STEP) THEN + theta = model%torsion%theta + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: the twist moved more than pi/2 from its committed value in one step; a smaller '// & + 'step resolves it' + RETURN + END IF + ErrStat = CD_HCDYN_OK + ErrMsg = '' + END SUBROUTINE torsion_theta_at LOGICAL FUNCTION rigid_rays_valid(model, directions, state) RESULT(valid) TYPE(CD_HermiteCableDynType), INTENT(IN) :: model @@ -3506,6 +3820,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat REAL(wp) :: tensile_force_save, tensile_threshold_save, tensile_xi_save, tensile_time_save ! The step-rotation diagnostic must not keep the turn of a step that is rewound. REAL(wp) :: rotation_save + REAL(wp) :: tors_theta0, tors_phi0, tors_phi1, tors_ends0(3, 4), tors_ends1(3, 4), tors_sub(3, 4) CHARACTER(300) :: em ! Tensile-qualification failure flag reported by each step (classified by flag, not by ! message text, so a short caller buffer cannot change the recovery decision). @@ -3519,7 +3834,6 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat ErrMsg = 'CD_HermiteCable_Dyn_Step_Recovering: model not initialised' RETURN END IF - has_pres = PRESENT(pres_dofs) .AND. PRESENT(pres_q) .AND. PRESENT(pres_v) .AND. PRESENT(pres_a) ! Reject a partial prescribed-motion set here too (not just in the underlying step): otherwise a ! caller that omits e.g. pres_a makes has_pres false and the common-path call below silently holds @@ -3594,6 +3908,17 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat tensile_xi_save = model%tensile_worst_xi tensile_time_save = model%tensile_worst_time rotation_save = model%max_step_rotation + ! Condensed torsion: the interval-start frames, Phi and Theta, and the interval-end target + ! (the pending drive, consumed by the full step), for the rewinds and the substep targets. + tors_theta0 = model%torsion%theta + tors_phi0 = model%torsion%phi + tors_ends0 = model%torsion%ends + tors_phi1 = tors_phi0 + tors_ends1 = tors_ends0 + IF (model%torsion%active .AND. model%tors_drive_set) THEN + tors_phi1 = model%tors_drive_phi + tors_ends1 = model%tors_drive_ends + END IF ! Common path: one full step. It performs no allocation or snapshot. IF (has_pres) THEN IF (PRESENT(endconn_direction)) THEN @@ -3669,9 +3994,23 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat model%tensile_worst_xi = tensile_xi_save model%tensile_worst_time = tensile_time_save model%max_step_rotation = rotation_save + CALL restore_torsion() es = CD_HCDYN_OK DO k = 1, nsub tensile_fail = .FALSE. + IF (model%torsion%active) THEN + frac = REAL(k, wp)/REAL(nsub, wp) + IF (k == nsub) THEN + tors_sub = tors_ends1 + model%tors_drive_phi = tors_phi1 + ELSE + CALL interpolate_torsion_frames(tors_ends0, tors_ends1, frac, tors_sub, es, em) + IF (es /= CD_HCDYN_OK) EXIT + model%tors_drive_phi = tors_phi0 + frac*(tors_phi1 - tors_phi0) + END IF + model%tors_drive_ends = tors_sub + model%tors_drive_set = .TRUE. + END IF IF (PRESENT(endconn_direction)) THEN frac = REAL(k, wp)/REAL(nsub, wp) CALL interpolate_endconn_kinematics(model, d0_save, d0_rate_save, d0_acceleration_save, & @@ -3731,6 +4070,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat model%tensile_worst_xi = tensile_xi_save model%tensile_worst_time = tensile_time_save model%max_step_rotation = rotation_save + CALL restore_torsion() ErrStat = CD_HCDYN_NOCONVERGE ErrMsg = 'CD_HermiteCable_Dyn_Step_Recovering: tensile qualification failed during '// & 'internal subdivision: '//TRIM(em) @@ -3770,6 +4110,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat model%tensile_worst_xi = tensile_xi_save model%tensile_worst_time = tensile_time_save model%max_step_rotation = rotation_save + CALL restore_torsion() ErrStat = CD_HCDYN_NOCONVERGE BLOCK ! Format into a local buffer: a record longer than the caller's ErrMsg @@ -3789,6 +4130,18 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat END IF ErrMsg = wbuf END BLOCK + + CONTAINS + + SUBROUTINE restore_torsion() + !! Interval-start torsion state (frames, Phi, Theta) after a rewind; no pending drive. + IF (.NOT. model%torsion%active) RETURN + model%torsion%theta = tors_theta0 + model%torsion%phi = tors_phi0 + model%torsion%ends = tors_ends0 + model%torsion%torque = (tors_phi0 - tors_theta0)/CD_HermiteTorsion_Compliance(model%torsion, model%l0) + model%tors_drive_set = .FALSE. + END SUBROUTINE restore_torsion END SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering SUBROUTINE quintic_boundary_state(q0, v0, a0, q1, v1, a1, duration, u, q, v, a) @@ -3870,6 +4223,56 @@ SUBROUTINE interpolate_endconn_kinematics(model, d0, v0, a0, d1, v1, a1, duratio END IF END SUBROUTINE interpolate_endconn_kinematics + SUBROUTINE interpolate_torsion_frames(ends0, ends1, fraction, ends, ErrStat, ErrMsg) + !! Torsion end frames at a fraction of a recovery interval: each end frame (d, n, d x n) turns + !! from ends0 to ends1 about one fixed axis (the geodesic on SO(3)), by fraction of the angle. + REAL(wp), INTENT(IN) :: ends0(3, 4), ends1(3, 4), fraction + REAL(wp), INTENT(OUT) :: ends(3, 4) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp) :: f0(3, 3), f1(3, 3), rq(3, 3), w(3), ang, cth, sk(3, 3), rf(3, 3) + INTEGER :: k + ErrStat = CD_HCDYN_OK + ErrMsg = '' + ends = ends1 + DO k = 1, 3, 2 + f0(:, 1) = ends0(:, k) + f0(:, 2) = ends0(:, k + 1) + f0(:, 3) = tcross(ends0(:, k), ends0(:, k + 1)) + f1(:, 1) = ends1(:, k) + f1(:, 2) = ends1(:, k + 1) + f1(:, 3) = tcross(ends1(:, k), ends1(:, k + 1)) + rq = MATMUL(f1, TRANSPOSE(f0)) + cth = MAX(-CD_ONE, MIN(CD_ONE, 0.5_wp*(rq(1, 1) + rq(2, 2) + rq(3, 3) - CD_ONE))) + ang = ACOS(cth) + IF (ang > 3.0_wp) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'torsion: an end frame turns by nearly 180 degrees within one interval' + RETURN + END IF + w = 0.5_wp*[rq(3, 2) - rq(2, 3), rq(1, 3) - rq(3, 1), rq(2, 1) - rq(1, 2)] + IF (ang > 1.0e-12_wp) w = w*ang/SIN(ang) + w = fraction*w + ang = NORM2(w) + rf = CD_ZERO + rf(1, 1) = CD_ONE + rf(2, 2) = CD_ONE + rf(3, 3) = CD_ONE + IF (ang > CD_ZERO) THEN + sk = RESHAPE([CD_ZERO, w(3), -w(2), -w(3), CD_ZERO, w(1), w(2), -w(1), CD_ZERO], [3, 3])/ang + rf = rf + SIN(ang)*sk + (CD_ONE - COS(ang))*MATMUL(sk, sk) + END IF + ends(:, k) = MATMUL(rf, ends0(:, k)) + ends(:, k + 1) = MATMUL(rf, ends0(:, k + 1)) + END DO + CONTAINS + PURE FUNCTION tcross(a, b) RESULT(c) + REAL(wp), INTENT(IN) :: a(3), b(3) + REAL(wp) :: c(3) + c = [a(2)*b(3) - a(3)*b(2), a(3)*b(1) - a(1)*b(3), a(1)*b(2) - a(2)*b(1)] + END FUNCTION tcross + END SUBROUTINE interpolate_torsion_frames + LOGICAL FUNCTION temporal_increment_ok(model) RESULT(ok) !! Cheap accepted-step quality gate. Newton convergence proves equilibrium at its discrete !! iterate, but not that one coupling interval resolved the nonlinear trajectory. Reject a step @@ -3987,6 +4390,17 @@ SUBROUTINE CD_HermiteCable_Dyn_Energy(model, kinetic, strain, ErrStat, ErrMsg, c END IF strain = strain + connection_energy END IF + IF (model%torsion%active) THEN + BLOCK + REAL(wp) :: th, mt + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, ErrStat, em2) + IF (ErrStat /= CD_HCDYN_OK) THEN + ErrMsg = 'CD_HermiteCable_Dyn_Energy: '//TRIM(em2) + RETURN + END IF + strain = strain + 0.5_wp*CD_HermiteTorsion_Compliance(model%torsion, model%l0)*mt*mt + END BLOCK + END IF IF (PRESENT(connection)) connection = connection_energy END SUBROUTINE CD_HermiteCable_Dyn_Energy @@ -4227,6 +4641,17 @@ SUBROUTINE CD_HermiteCable_Dyn_End_Force(model, node, force, ErrStat, ErrMsg) END DO END IF force = -r3 + ! Condensed torsion: R carries -M_t dTheta/dq, so the end force gains +M_t dTheta/dr_end. + IF (model%torsion%active) THEN + BLOCK + REAL(wp) :: th, mt, gend(3, 2) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em2, end_dr=gend) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = 'CD_HermiteCable_Dyn_End_Force: '//TRIM(em2); RETURN + END IF + force = force + mt*gend(:, MERGE(1, 2, node == 1)) + END BLOCK + END IF ! An end resting on the seabed (at or inside the contact blend) does not carry the end ! node's weight: the floor does, as it does for the grounded nodes beside it. The ! downward part of the end force is left to the seabed, so a grounded end reports the @@ -4261,7 +4686,7 @@ SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment(model, node, moment, ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg INTEGER :: iend, base, es - REAL(wp) :: f_ec(3), k_ec(3, 3), direction(3) + REAL(wp) :: f_ec(3), k_ec(3, 3), direction(3), bend_moment(3) CHARACTER(200) :: em moment = CD_ZERO @@ -4281,7 +4706,31 @@ SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment(model, node, moment, ErrStat ErrMsg = 'CD_HermiteCable_Dyn_EndConnection_Moment: node must be a cable endpoint' RETURN END IF - IF (.NOT. model%has_endconn .OR. model%endconn_mode(iend) == CD_ENDCONN_PINNED) RETURN + IF (model%torsion%active) THEN + ! Torque of the condensed torsion on the support: the generalised moment + ! -dE_t/dw = M_t dTheta/dw of the end frame (its axial part is -+M_t d); the slaved + ! tangent of a rigid end adds its share through the constraint reaction below. + BLOCK + REAL(wp) :: th, mt, eg(6) + IF (.NOT. torsion_cache_matches(model)) THEN + CALL torsion_cache_fill(model, es, em) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es + ErrMsg = 'CD_HermiteCable_Dyn_EndConnection_Moment: '//TRIM(em) + RETURN + END IF + END IF + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, end_grad=eg) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es + ErrMsg = 'CD_HermiteCable_Dyn_EndConnection_Moment: '//TRIM(em) + RETURN + END IF + moment = mt*eg(3*iend - 2:3*iend) + END BLOCK + END IF + IF (.NOT. model%has_endconn) RETURN + IF (model%endconn_mode(iend) == CD_ENDCONN_PINNED) RETURN base = 6*(node - 1) + 3 IF (model%endconn_mode(iend) == CD_ENDCONN_FINITE) THEN @@ -4318,13 +4767,239 @@ SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment(model, node, moment, ErrStat direction = model%endconn_d0(:, iend) f_ec = f_ec - DOT_PRODUCT(f_ec, direction)*direction END IF - CALL CD_EndConn_Reaction_Moment(model%q(base + 1:base + 3), f_ec, moment, es, em) + CALL CD_EndConn_Reaction_Moment(model%q(base + 1:base + 3), f_ec, bend_moment, es, em) IF (es /= CD_ENDCONN_OK) THEN ErrStat = CD_HCDYN_NOCONVERGE ErrMsg = 'CD_HermiteCable_Dyn_EndConnection_Moment: '//TRIM(em) + moment = CD_ZERO + RETURN + END IF + ! without torsion the reaction moment is the result itself (no +0.0 added, so a -0.0 + ! component keeps its sign as before torsion existed) + IF (model%torsion%active) THEN + moment = moment + bend_moment + ELSE + moment = bend_moment END IF END SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment + SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) + !! Install the condensed torsion of a converged static solve (CD_HermiteCable_Static_Solve + !! with torsion): the end frames and GJ in the model's solve frame, Phi and the accepted + !! Theta. The residual, the support reaction, the end force, the connection moment (now + !! including the torque) and the energy then include E_t = (Phi - Theta)**2/(2 C); the + !! acceleration is recomputed from the complete load set. An inactive description removes + !! the torsion. The time step then carries the torsion (see CD_HermiteCable_Dyn_Step); its + !! scratch is sized here, so a step allocates nothing. + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + TYPE(CD_HermiteTorsionType), INTENT(IN) :: torsion + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + TYPE(CD_HermiteTorsionType) :: saved + REAL(wp) :: th, mt + INTEGER :: es + CHARACTER(200) :: em + ErrStat = CD_HCDYN_OK + ErrMsg = '' + IF (.NOT. model%initialized) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: model not initialised' + RETURN + END IF + saved = model%torsion + model%tors_c_valid = .FALSE. + IF (.NOT. torsion%active) THEN + model%torsion = CD_HermiteTorsionType() + ELSE + CALL CD_HermiteTorsion_Validate(torsion, model%l0, es, em) + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: '//TRIM(em) + RETURN + END IF + IF (.NOT. torsion%has_theta) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: the torsion state has no accepted Theta (solve the statics first)' + RETURN + END IF + IF (model%tors_work%nn /= model%nn) THEN + CALL CD_HermiteTorsion_Work_Init(model%tors_work, model%nn, es, em) + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: '//TRIM(em) + RETURN + END IF + END IF + IF (.NOT. ALLOCATED(model%tors_g)) ALLOCATE (model%tors_g(model%ndof), model%tors_gq(model%ndof), & + model%tors_gk(model%ndof), model%tors_zk(model%ndof)) + IF (.NOT. ALLOCATED(model%tors_c_q)) ALLOCATE (model%tors_c_q(model%ndof)) + model%tors_g = CD_ZERO + model%tors_gq = CD_ZERO + model%tors_gk = CD_ZERO + model%tors_zk = CD_ZERO + model%tors_den = CD_ONE + model%torsion = torsion + model%tors_c_valid = .FALSE. + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em) + IF (es /= CD_HCDYN_OK) THEN + model%torsion = saved + ErrStat = es + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: '//TRIM(em) + RETURN + END IF + model%torsion%theta = th + model%torsion%torque = mt + END IF + model%tors_use_step = .FALSE. + model%tors_drive_set = .FALSE. + model%tors_step_ends = model%torsion%ends + model%tors_step_phi = model%torsion%phi + model%snap_torsion_theta = model%torsion%theta + model%snap_torsion_phi = model%torsion%phi + model%snap_torsion_ends = model%torsion%ends + model%fc_valid = .FALSE. + model%tr_valid = .FALSE. + CALL CD_HermiteCable_Dyn_Recompute_Acceleration(model, es, em) + IF (es /= CD_HCDYN_OK) THEN + model%torsion = saved + ErrStat = es + ErrMsg = 'CD_HermiteCable_Dyn_Set_Torsion: '//TRIM(em) + END IF + END SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion + + SUBROUTINE CD_HermiteCable_Dyn_Torsion_State(model, theta, torque, ErrStat, ErrMsg, grad, end_grad, q_eval, & + end_dr) + !! Theta (unwrapped against the stored accepted value), the torque M_t = (Phi - Theta)/C and, + !! optionally, dTheta/dq, dTheta/d[w_1, w_2] and dTheta/dr at the first and last node (end_dr + !! columns) at the committed state (or at q_eval). Read only: the stored Theta is not + !! advanced. A change of more than pi/2 from the stored value fails closed (a 2 pi branch + !! could otherwise be lost). At the committed state without grad it reads the query cache + !! when that matches the state (no heap allocation); otherwise it evaluates the kernel. + TYPE(CD_HermiteCableDynType), INTENT(IN) :: model + REAL(wp), INTENT(OUT) :: theta, torque + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(OUT), OPTIONAL :: grad(:), end_grad(6) + REAL(wp), INTENT(IN), OPTIONAL :: q_eval(:) + REAL(wp), INTENT(OUT), OPTIONAL :: end_dr(3, 2) + REAL(wp), ALLOCATABLE :: g(:) + REAL(wp) :: raw, eg(6), c + INTEGER :: es, nb + CHARACTER(200) :: em + theta = CD_ZERO + torque = CD_ZERO + IF (PRESENT(grad)) grad = CD_ZERO + IF (PRESENT(end_grad)) end_grad = CD_ZERO + IF (PRESENT(end_dr)) end_dr = CD_ZERO + ErrStat = CD_HCDYN_OK + ErrMsg = '' + IF (.NOT. model%torsion%active) RETURN + IF (.NOT. (PRESENT(grad) .OR. PRESENT(q_eval))) THEN + IF (torsion_cache_matches(model)) THEN + theta = model%tors_c_theta + torque = (model%torsion%phi - theta)/CD_HermiteTorsion_Compliance(model%torsion, model%l0) + IF (PRESENT(end_grad)) end_grad = model%tors_c_eg + IF (PRESENT(end_dr)) end_dr = model%tors_c_gend + RETURN + END IF + END IF + ALLOCATE (g(model%ndof)) + IF (PRESENT(q_eval)) THEN + CALL CD_HermiteTorsion_Line(q_eval, model%l0, model%torsion%ends, raw, g, es, em, end_grad=eg, & + quadrature_order=model%torsion%quadrature_order) + ELSE + CALL CD_HermiteTorsion_Line(model%q, model%l0, model%torsion%ends, raw, g, es, em, end_grad=eg, & + quadrature_order=model%torsion%quadrature_order) + END IF + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: '//TRIM(em) + RETURN + END IF + theta = CD_HermiteTorsion_Unwrap(raw, model%torsion%theta) + IF (ABS(theta - model%torsion%theta) > CD_HTORS_MAX_STEP) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: the twist moved more than pi/2 from its accepted value' + theta = CD_ZERO + RETURN + END IF + c = CD_HermiteTorsion_Compliance(model%torsion, model%l0) + torque = (model%torsion%phi - theta)/c + IF (PRESENT(grad)) grad = g + IF (PRESENT(end_grad)) end_grad = eg + IF (PRESENT(end_dr)) THEN + nb = 6*(model%nn - 1) + end_dr(:, 1) = g(1:3) + end_dr(:, 2) = g(nb + 1:nb + 3) + END IF + END SUBROUTINE CD_HermiteCable_Dyn_Torsion_State + + LOGICAL FUNCTION torsion_cache_matches(model) RESULT(match) + !! Whether the torsion query cache holds the evaluation at the model's committed q, end + !! frames and accepted Theta (bitwise; no temporaries). + TYPE(CD_HermiteCableDynType), INTENT(IN) :: model + INTEGER :: i, j + match = .FALSE. + IF (.NOT. model%tors_c_valid .OR. .NOT. ALLOCATED(model%tors_c_q) .OR. .NOT. ALLOCATED(model%q)) RETURN + IF (SIZE(model%tors_c_q) /= SIZE(model%q)) RETURN + IF (.NOT. same_bits(model%tors_c_prev, model%torsion%theta)) RETURN + DO j = 1, 4 + DO i = 1, 3 + IF (.NOT. same_bits(model%tors_c_ends(i, j), model%torsion%ends(i, j))) RETURN + END DO + END DO + DO i = 1, SIZE(model%q) + IF (.NOT. same_bits(model%tors_c_q(i), model%q(i))) RETURN + END DO + match = .TRUE. + CONTAINS + PURE LOGICAL FUNCTION same_bits(a, b) RESULT(same) + REAL(wp), INTENT(IN) :: a, b + same = TRANSFER(a, 0_INT64) == TRANSFER(b, 0_INT64) + END FUNCTION same_bits + END FUNCTION torsion_cache_matches + + SUBROUTINE torsion_cache_fill(model, ErrStat, ErrMsg) + !! Evaluate the torsion query cache at the committed q and end frames, allocation-free (the + !! kernel workspace and tors_gq). On failure the cache is left invalid and the error named. + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp) :: raw, eg(6), th + INTEGER :: es, nb, i + CHARACTER(200) :: em + ErrStat = CD_HCDYN_OK + ErrMsg = '' + model%tors_c_valid = .FALSE. + IF (.NOT. model%torsion%active) RETURN + IF (.NOT. ALLOCATED(model%tors_c_q) .OR. .NOT. ALLOCATED(model%tors_gq)) RETURN + IF (SIZE(model%tors_c_q) /= SIZE(model%q) .OR. model%tors_work%nn /= model%nn) RETURN + CALL CD_HermiteTorsion_Line(model%q, model%l0, model%torsion%ends, raw, model%tors_gq, es, em, end_grad=eg, & + quadrature_order=model%torsion%quadrature_order, work=model%tors_work) + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: '//TRIM(em) + RETURN + END IF + th = CD_HermiteTorsion_Unwrap(raw, model%torsion%theta) + IF (ABS(th - model%torsion%theta) > CD_HTORS_MAX_STEP) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: the twist moved more than pi/2 from its accepted value' + RETURN + END IF + DO i = 1, SIZE(model%q) + model%tors_c_q(i) = model%q(i) + END DO + nb = 6*(model%nn - 1) + model%tors_c_ends = model%torsion%ends + model%tors_c_prev = model%torsion%theta + model%tors_c_theta = th + model%tors_c_eg = eg + model%tors_c_gend(:, 1) = model%tors_gq(1:3) + model%tors_c_gend(:, 2) = model%tors_gq(nb + 1:nb + 3) + model%tors_c_valid = .TRUE. + END SUBROUTINE torsion_cache_fill + SUBROUTINE CD_HermiteCable_Dyn_Snapshot(model, ErrStat, ErrMsg) !! Capture the committed step state (q, v, a, t) into the model-owned snapshot !! buffers (see the type note: the aggregate stage-then-commit contract). Buffers @@ -4347,6 +5022,9 @@ SUBROUTINE CD_HermiteCable_Dyn_Snapshot(model, ErrStat, ErrMsg) model%snap_a = model%a model%snap_t = model%t model%snap_endconn_d0 = model%endconn_d0 + model%snap_torsion_theta = model%torsion%theta + model%snap_torsion_phi = model%torsion%phi + model%snap_torsion_ends = model%torsion%ends IF (model%fr_active) model%snap_fr_anchor = model%fr_anchor model%snap_na_steps_done = model%na_steps_done model%snap_na_iter_sum = model%na_iter_sum @@ -4386,6 +5064,13 @@ SUBROUTINE CD_HermiteCable_Dyn_Restore(model, ErrStat, ErrMsg) model%a = model%snap_a model%t = model%snap_t model%endconn_d0 = model%snap_endconn_d0 + model%torsion%theta = model%snap_torsion_theta + model%torsion%phi = model%snap_torsion_phi + model%torsion%ends = model%snap_torsion_ends + IF (model%torsion%active) model%torsion%torque = (model%torsion%phi - model%torsion%theta)/ & + CD_HermiteTorsion_Compliance(model%torsion, model%l0) + model%tors_drive_set = .FALSE. + model%tors_use_step = .FALSE. IF (model%fr_active) model%fr_anchor = model%snap_fr_anchor model%na_steps_done = model%snap_na_steps_done model%na_iter_sum = model%snap_na_iter_sum @@ -4473,6 +5158,26 @@ SUBROUTINE CD_HermiteCable_Dyn_End(model) model%tr_valid = .FALSE. model%snap_t = CD_ZERO model%snap_endconn_d0 = CD_ZERO + model%torsion = CD_HermiteTorsionType() + CALL CD_HermiteTorsion_Work_End(model%tors_work) + IF (ALLOCATED(model%tors_g)) DEALLOCATE (model%tors_g, model%tors_gq, model%tors_gk, model%tors_zk) + IF (ALLOCATED(model%tors_c_q)) DEALLOCATE (model%tors_c_q) + model%tors_c_valid = .FALSE. + model%tors_c_ends = CD_ZERO + model%tors_c_prev = CD_ZERO + model%tors_c_theta = CD_ZERO + model%tors_c_eg = CD_ZERO + model%tors_c_gend = CD_ZERO + model%tors_den = CD_ZERO + model%tors_use_step = .FALSE. + model%tors_drive_set = .FALSE. + model%tors_step_ends = CD_ZERO + model%tors_step_phi = CD_ZERO + model%tors_drive_ends = CD_ZERO + model%tors_drive_phi = CD_ZERO + model%snap_torsion_theta = CD_ZERO + model%snap_torsion_phi = CD_ZERO + model%snap_torsion_ends = CD_ZERO model%snap_valid = .FALSE. model%require_tensile = .FALSE. model%tensile_mode = CD_HCDYN_TENSILE_OFF @@ -4913,8 +5618,76 @@ SUBROUTINE assemble_residual(model, q_cfg, v_cfg, t_eval, want_residual, want_ta IF (want_tangent) CALL scatter12_band(Kb, -fjq, e) END DO END IF + + ! Condensed torsion: R <- R - M_t dTheta/dq and Kb <- Kb - M_t d2Theta/dq2 at q_cfg (the + ! rank-one g g^T / C joins the solve by Sherman-Morrison; g is kept in tors_g). + IF (model%torsion%active .AND. (want_residual .OR. want_tangent)) THEN + CALL assemble_torsion(model, q_cfg, want_residual, want_tangent, es, em2) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = TRIM(em2); RETURN + END IF + END IF END SUBROUTINE assemble_residual + SUBROUTINE assemble_torsion(model, q_cfg, want_residual, want_tangent, ErrStat, ErrMsg) + !! Condensed-torsion part of assemble_residual, allocation-free: Theta at q_cfg (unwrapped + !! against the committed Theta, at most pi/2 away) with the committed end frames and Phi, or + !! the running step's target ones (tors_use_step); M_t = (Phi - Theta)/C; R -= M_t dTheta/dq + !! (want_residual); Kb -= M_t d2Theta/dq2 and tors_g = dTheta/dq (want_tangent). + TYPE(CD_HermiteCableDynType), INTENT(INOUT), TARGET :: model + REAL(wp), INTENT(IN) :: q_cfg(:) + LOGICAL, INTENT(IN) :: want_residual, want_tangent + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), POINTER :: g(:) + REAL(wp) :: raw, th, mt, phi + INTEGER :: es, i + CHARACTER(200) :: em + ErrStat = CD_HCDYN_OK + ErrMsg = '' + IF (want_tangent) THEN + g => model%tors_g + ELSE + g => model%tors_gq + END IF + IF (model%tors_use_step) THEN + phi = model%tors_step_phi + CALL CD_HermiteTorsion_Line(q_cfg, model%l0, model%tors_step_ends, raw, g, es, em, & + quadrature_order=model%torsion%quadrature_order, work=model%tors_work, & + keep_hessian=want_tangent) + ELSE + phi = model%torsion%phi + CALL CD_HermiteTorsion_Line(q_cfg, model%l0, model%torsion%ends, raw, g, es, em, & + quadrature_order=model%torsion%quadrature_order, work=model%tors_work, & + keep_hessian=want_tangent) + END IF + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: '//TRIM(em) + RETURN + END IF + th = CD_HermiteTorsion_Unwrap(raw, model%torsion%theta) + IF (ABS(th - model%torsion%theta) > CD_HTORS_MAX_STEP) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: the twist moved more than pi/2 from its accepted value' + RETURN + END IF + mt = (phi - th)/CD_HermiteTorsion_Compliance(model%torsion, model%l0) + IF (want_residual) THEN + DO i = 1, model%ndof + model%ws_R(i) = model%ws_R(i) - mt*g(i) + END DO + END IF + IF (want_tangent) THEN + CALL CD_HermiteTorsion_Work_Add_Hessian(model%tors_work, model%ws_Kb, -mt, es, em) + IF (es /= CD_HTORS_OK) THEN + ErrStat = CD_HCDYN_NOCONVERGE + ErrMsg = 'torsion: '//TRIM(em) + RETURN + END IF + END IF + END SUBROUTINE assemble_torsion + SUBROUTINE add_endconn_band(R, Kb, q_cfg, node, k_rot, d0, want_residual, want_tangent, & ErrStat, ErrMsg) !! Scatter one rotational end connection into the dynamic residual and band tangent. diff --git a/src/CableDyn_HermiteCableStatic.f90 b/src/CableDyn_HermiteCableStatic.f90 index 94fa296..ed10896 100644 --- a/src/CableDyn_HermiteCableStatic.f90 +++ b/src/CableDyn_HermiteCableStatic.f90 @@ -47,6 +47,12 @@ MODULE CableDyn_HermiteCableStatic CD_Bathymetry_Floor, CD_Bathymetry_Floor_Gradient, CD_BATHY_OK USE CableDyn_HermiteCableDynamic, ONLY: CD_HermiteCable_Drag_Element, CD_HCDYN_OK USE CableDyn_Hydro, ONLY: CD_Current_Profile_Velocity, CD_HYDRO_OK + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Line, CD_HermiteTorsion_Unwrap, & + CD_HermiteTorsion_Accept, CD_HermiteTorsion_Compliance, & + CD_HermiteTorsion_Validate, CD_HermiteTorsion_Bordered_Solve, & + CD_HermiteTorsion_Inertia, CD_HermiteTorsion_Lowest_Mode, CD_HTORS_OK, & + CD_HTORS_KBAND, CD_HTORS_MAX_STEP, CD_HTORS_ZERO_MODE_TOL, CD_HTORS_PI, & + CD_HTORS_UNRELIABLE !$ USE OMP_LIB, ONLY: omp_get_max_threads, omp_in_parallel IMPLICIT NONE PRIVATE @@ -264,7 +270,7 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ endconn_stiffness, endconn_direction, endconn_mode, & waterline_z, dry_buoyancy, merit_line_search, pseudo_transient, & energy_minimization, stable, water_weight, current, residual_out, & - tangent_out, friction, residual_unmet) + tangent_out, friction, residual_unmet, torsion) !! Solve the finite-EI static equilibrium of a Hermite bending-cable line. !! !! l0(ne), EA(ne), EI(ne), w(ne) : per-element rest length, axial/bending stiffness, @@ -360,6 +366,35 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ !! its equilibrium value (the capacity's dependence on !! the normal force is left out; see the final !! evaluation). + !! torsion (optional, inout) : condensed isotropic torsion of the line (uniform torque, + !! CableDyn_HermiteTorsion), used when torsion%active. + !! The loads and EI/buoyancy stages above run without it; + !! the imposed twist is then ramped as the last load + !! stage, from the twist of that untwisted equilibrium + !! (zero torque) to torsion%phi, in steps of at most + !! pi/4 halved on a failed stage. Each Newton step adds + !! E_t = (Phi - Theta)**2/(2 C) and solves the bordered + !! system [B g; g^T -C] by Sherman-Morrison on the band + !! factor (CD_HermiteTorsion_Bordered_Solve); Theta is + !! unwrapped against the last accepted iterate and every + !! accepted iterate changes it by at most pi/2 (longer + !! steps are cut). Each converged stage is tested for + !! stability (the inertia of the tangent on the free + !! DOFs); an unstable one (above a buckling onset) is + !! left by negative-curvature descent along the lowest + !! mode and energy minimisation, unless torsion%descend + !! is false. On a converged return torsion holds the + !! accepted Theta (has_theta), the torque and the + !! stability report; any other return leaves its Theta + !! state unchanged. A re-solve with has_theta needs the + !! untwisted equilibrium within pi/2 of the stored Theta + !! (otherwise its turn count is ambiguous and the solve + !! stops by name); start a new solve with has_theta + !! false and theta_hint instead. residual_history and + !! history_count cover the untwisted stages only; + !! iters_out and res_out include the twist stages. Not + !! combined with pseudo_transient, + !! equilibrate_linear_system or tangent_out. REAL(wp), INTENT(IN) :: l0(:), EA(:), EI(:), w(:), seed(:) INTEGER, INTENT(IN) :: fixed_dofs(:) REAL(wp), INTENT(IN) :: seabed_z, kn, tol, damping @@ -394,6 +429,7 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ ! NOCONVERGE, "Newton did not converge"): the state is a finite best iterate a caller ! may continue from. LOGICAL, INTENT(OUT), OPTIONAL :: residual_unmet + TYPE(CD_HermiteTorsionType), INTENT(INOUT), OPTIONAL :: torsion INTEGER :: ne, nn, ndof, c, it, es, i, b, nci, n_buoy, omp_threads, axial_order, bending_order REAL(wp) :: eifac, rnorm, fscale, bfac, rmerit, ptc_tau, ptc_rprev, pi_total, pi_abs, lm_mu @@ -429,6 +465,15 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ ! capacity held at its equilibrium value. LOGICAL :: stab_eval REAL(wp), ALLOCATABLE :: ab_energy(:, :), r_energy(:), q_energy(:), sym(:, :) + ! Condensed torsion (optional torsion argument): use_tors when active, tors_on once the + ! twist stage runs. tors_g is dTheta/dq at the last assembly (global DOFs), tors_gr the same + ! in the rigid-end basis with held DOFs zeroed, tors_h d2Theta/dq2 (band, 23 rows), + ! tors_v the lowest mode of an unstable tangent (descent direction). + LOGICAL :: use_tors, tors_on, tors_have_mode, tors_descending + REAL(wp) :: tors_c, tors_phi_cur, tors_theta_acc, tors_theta_eval, tors_mt, tors_lambda + REAL(wp), ALLOCATABLE :: tors_g(:), tors_gr(:), tors_h(:, :), tors_v(:), tors_rhs(:), tors_q(:), tors_gs(:) + REAL(wp), ALLOCATABLE :: tors_grs(:), tors_gt(:) + REAL(wp), ALLOCATABLE :: tors_rhs2(:, :) REAL(wp) :: contact_cs(2) ErrStat = CD_HCSTAT_OK @@ -635,6 +680,40 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ use_endconn = ANY(endconn_kind == CD_ENDCONN_FINITE) use_rigid = ANY(endconn_kind == CD_ENDCONN_RIGID) END IF + use_tors = .FALSE. + tors_on = .FALSE. + tors_have_mode = .FALSE. + tors_descending = .FALSE. + tors_c = CD_ZERO + tors_phi_cur = CD_ZERO + tors_theta_acc = CD_ZERO + tors_theta_eval = CD_ZERO + tors_mt = CD_ZERO + tors_lambda = CD_ZERO + IF (PRESENT(torsion)) use_tors = torsion%active + IF (use_tors) THEN + CALL CD_HermiteTorsion_Validate(torsion, l0, es, ErrMsg) + IF (es /= CD_HTORS_OK) THEN + CALL fail(TRIM(ErrMsg)); RETURN + END IF + tors_c = CD_HermiteTorsion_Compliance(torsion, l0) + IF (.NOT. (tors_c > CD_ZERO) .OR. .NOT. CD_Is_Finite(tors_c)) THEN + CALL fail('torsion: the line compliance must be finite and positive'); RETURN + END IF + IF (use_ptc .OR. use_equilibration) THEN + CALL fail('torsion is not combined with pseudo_transient or equilibrate_linear_system'); RETURN + END IF + ! the band holds only B of K = B + g g^T / C: no banded tangent can carry the rank-one term + IF (PRESENT(tangent_out)) THEN + CALL fail('torsion is not combined with tangent_out (the twisted tangent is not banded)'); RETURN + END IF + DO i = 1, 2 + IF (endconn_kind(i) /= CD_ENDCONN_RIGID) CYCLE + IF (NORM2(torsion%ends(:, 2*i - 1) - endconn_d0(:, i)) > 1.0e-8_wp) THEN + CALL fail('torsion: the director at a rigid end must be the rigid connection direction'); RETURN + END IF + END DO + END IF ALLOCATE (q(ndof), trib(nn), freemask(ndof), solve_mask(ndof), w_cur(ne)) ALLOCATE (R(ndof), dq(ndof), ab(LDAB_H, ndof), elem_f(12, ne), elem_K(12, 12, ne), & @@ -642,7 +721,15 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ IF (use_equilibration) ALLOCATE (ab_recovery(LDAB_H, ndof), scaled_residual(ndof)) ALLOCATE (q_sec1(ndof), q_sec2(ndof)) IF (use_backtracking) ALLOCATE (q_base(ndof)) - IF (use_energy) ALLOCATE (ab_energy(LDAB_H, ndof), r_energy(ndof), q_energy(ndof), sym(KL_H + 1, ndof)) + IF (use_energy .OR. use_tors) & + ALLOCATE (ab_energy(LDAB_H, ndof), r_energy(ndof), q_energy(ndof), sym(KL_H + 1, ndof)) + IF (use_tors) THEN + ALLOCATE (tors_g(ndof), tors_gr(ndof), tors_h(2*CD_HTORS_KBAND + 1, ndof), tors_v(ndof), tors_rhs(ndof), & + tors_q(ndof), tors_gs(ndof), tors_rhs2(ndof, 2), tors_grs(ndof), tors_gt(ndof)) + tors_g = CD_ZERO + tors_gr = CD_ZERO + tors_v = CD_ZERO + END IF q = seed q_sec1 = seed q_sec2 = seed @@ -787,6 +874,16 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ q_sec1 = q END DO + ! Condensed torsion: the imposed twist is the last load stage. + IF (use_tors) THEN + CALL torsion_continuation(es, ErrMsg) + IF (es /= CD_HCSTAT_OK) THEN + q_out = q + curv_out = CD_ZERO + ErrStat = es; RETURN + END IF + END IF + ! Evaluate the TRUE residual at the final q and full EI (eifac = 1, w_cur = w here): the ! last update inside the loop may have reached tolerance, and its post-update ! residual must drive the convergence verdict rather than the pre-update value. @@ -817,7 +914,38 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ ErrStat = es; RETURN END IF END IF - stable = tangent_positive_definite() + IF (use_tors) THEN + CALL torsion_stability(stable, es, ErrMsg) + IF (es /= CD_HCSTAT_OK) THEN + q_out = q + curv_out = CD_ZERO + ErrStat = es; RETURN + END IF + ELSE + stable = tangent_positive_definite() + END IF + END IF + IF (use_tors) THEN + ! the accepted Theta becomes state only on a converged return: a NOCONVERGE return + ! leaves the caller's torsion state (and its branch) as it was + IF (res_out < tol) THEN + torsion%theta = tors_theta_acc + torsion%has_theta = .TRUE. + torsion%torque = tors_mt + torsion%energy = pi_total + END IF + IF (.NOT. PRESENT(stable)) THEN + IF (torsion%check_stability) THEN + CALL torsion_stability(torsion%stable, es, ErrMsg) + IF (es /= CD_HCSTAT_OK) THEN + q_out = q + curv_out = CD_ZERO + ErrStat = es; RETURN + END IF + END IF + ELSE + torsion%stable = stable + END IF END IF IF (res_out >= tol) THEN @@ -1073,7 +1201,16 @@ RECURSIVE SUBROUTINE newton_step(rnorm, es, em, do_update) ! coordinates are homogeneous constraints. Transforming both rows and ! columns preserves symmetry and avoids the conditioning and arbitrary ! stiffness scale of a penalty approximation. + ! ---- condensed torsion: -M_t dTheta/dq in R, -M_t d2Theta/dq2 in the band ---- + IF (tors_on) THEN + CALL add_torsion(es2, em2) + IF (es2 /= CD_HCSTAT_OK) THEN + es = CD_HCSTAT_NOCONVERGE; em = TRIM(em2); RETURN + END IF + END IF + IF (use_rigid) CALL apply_rigid_system(R, ab) + IF (tors_on) CALL torsion_reduce() ! Dimensionally-consistent relative residual over the FREE DOFs. Translational (r) ! DOFs carry force residuals [N]; material-tangent (m) DOFs carry moment residuals @@ -1175,7 +1312,13 @@ RECURSIVE SUBROUTINE newton_step(rnorm, es, em, do_update) ab_recovery = ab merit = residual_merit(scaled_residual) END IF - IF (use_equilibration) THEN + IF (tors_on) THEN + tors_rhs = dq + CALL CD_HermiteTorsion_Bordered_Solve(ab, KL_H, KU_H, tors_gr, tors_c, tors_rhs, dq, es2, em2) + IF (es2 /= CD_HTORS_OK) THEN + es = CD_HCSTAT_NOCONVERGE; em = 'torsion: '//TRIM(em2); RETURN + END IF + ELSE IF (use_equilibration) THEN CALL CD_Solve_Banded_Refined(ab, KL_H, KU_H, dq, es2, em2) ELSE CALL CD_Solve_Banded(ab, KL_H, KU_H, dq, es2, em2) @@ -1208,8 +1351,16 @@ RECURSIVE SUBROUTINE newton_step(rnorm, es, em, do_update) IF (rnorm < R_TRUST .AND. dqnorm < DQ_TRUST) step_scale = CD_ONE IF (use_ptc) step_scale = CD_ONE IF (use_rigid) CALL limit_rigid_step(dq, step_scale) + IF (tors_on) CALL limit_torsion_step(tors_g, dq, step_scale) IF (.NOT. use_backtracking .OR. use_ptc) THEN - q = q + step_scale*dq + IF (tors_on) THEN + CALL torsion_take_step(step_scale, es2, em2) + IF (es2 /= CD_HCSTAT_OK) THEN + es = CD_HCSTAT_NOCONVERGE; em = TRIM(em2); RETURN + END IF + ELSE + q = q + step_scale*dq + END IF ELSE q_base = q merit_base = rmerit @@ -1226,6 +1377,7 @@ RECURSIVE SUBROUTINE newton_step(rnorm, es, em, do_update) accepted = trial_norm < rnorm END IF IF (accepted) THEN + IF (tors_on) tors_theta_acc = tors_theta_eval RETURN END IF END IF @@ -1297,6 +1449,10 @@ RECURSIVE SUBROUTINE energy_update(es, em) ab_energy = ab r_energy = R q_energy = q + IF (tors_on) THEN + tors_gs = tors_g + tors_grs = tors_gr + END IF pi0 = pi_total pi_abs0 = pi_abs merit0 = rmerit @@ -1307,16 +1463,23 @@ RECURSIVE SUBROUTINE energy_update(es, em) ! potential is only piecewise smooth, and on a structured floor the curvature of the ! bilinear grid is not differentiated, so near the solution its line search can ! collapse to round-off steps where the Newton iteration converges. - IF (merit0 < NEWTON_MERIT .AND. lm_mu <= NEWTON_MU) THEN + ! (Not during a torsional negative-curvature descent: a Newton step would return to the saddle.) + IF (merit0 < NEWTON_MERIT .AND. lm_mu <= NEWTON_MU .AND. .NOT. tors_descending) THEN dq = -r_energy DO j = 1, ndof IF (.NOT. solve_mask(j)) dq(j) = CD_ZERO END DO - CALL CD_Solve_Banded(ab, KL_H, KU_H, dq, es_trial, em_trial) + IF (tors_on) THEN + tors_rhs = dq + CALL CD_HermiteTorsion_Bordered_Solve(ab, KL_H, KU_H, tors_gr, tors_c, tors_rhs, dq, es_trial, em_trial) + ELSE + CALL CD_Solve_Banded(ab, KL_H, KU_H, dq, es_trial, em_trial) + END IF IF (es_trial == CD_LINALG_OK .AND. CD_All_Finite(dq)) THEN IF (use_rigid) CALL rigid_increment_to_global(dq) alpha = CD_ONE IF (use_rigid) CALL limit_rigid_step(dq, alpha) + IF (tors_on) CALL limit_torsion_step(tors_gs, dq, alpha) skip_tangent = .TRUE. ! Only a step that at least halves the merit (a full or half Newton step) is ! taken; a Newton direction that makes slower progress leaves the step to the @@ -1326,6 +1489,7 @@ RECURSIVE SUBROUTINE energy_update(es, em) CALL newton_step(trial_norm, es_trial, em_trial, .FALSE.) IF (es_trial == CD_HCSTAT_OK .AND. rmerit <= 0.5_wp*merit0) THEN skip_tangent = .FALSE. + IF (tors_on) tors_theta_acc = tors_theta_eval RETURN END IF alpha = 0.5_wp*alpha @@ -1363,7 +1527,17 @@ RECURSIVE SUBROUTINE energy_update(es, em) DO j = 1, ndof IF (.NOT. solve_mask(j)) dq(j) = CD_ZERO END DO - CALL dpbtrs('L', ndof, KL_H, 1, sym, KL_H + 1, dq, ndof, info) + IF (tors_on) THEN + ! (B_sym + mu D + g g^T / C) dq = -R by Sherman-Morrison on the Cholesky factor: the + ! rank-one term is positive semidefinite, so the direction still descends. + tors_rhs2(:, 1) = dq + tors_rhs2(:, 2) = tors_grs + CALL dpbtrs('L', ndof, KL_H, 2, sym, KL_H + 1, tors_rhs2, ndof, info) + IF (info == 0) dq = tors_rhs2(:, 1) - tors_rhs2(:, 2)*(DOT_PRODUCT(tors_grs, tors_rhs2(:, 1))/ & + (tors_c + DOT_PRODUCT(tors_grs, tors_rhs2(:, 2)))) + ELSE + CALL dpbtrs('L', ndof, KL_H, 1, sym, KL_H + 1, dq, ndof, info) + END IF IF (info /= 0 .OR. .NOT. CD_All_Finite(dq)) THEN lm_mu = MAX(4.0_wp*lm_mu, 1.0e-8_wp) IF (lm_mu > 1.0e12_wp) EXIT @@ -1373,6 +1547,7 @@ RECURSIVE SUBROUTINE energy_update(es, em) IF (use_rigid) CALL rigid_increment_to_global(dq) sc = CD_ONE IF (use_rigid) CALL limit_rigid_step(dq, sc) + IF (tors_on) CALL limit_torsion_step(tors_gs, dq, sc) alpha = sc skip_tangent = .TRUE. DO bt = 0, MAX_HALVINGS @@ -1389,6 +1564,7 @@ RECURSIVE SUBROUTINE energy_update(es, em) END IF IF (accepted) THEN skip_tangent = .FALSE. + IF (tors_on) tors_theta_acc = tors_theta_eval IF (bt == 0) THEN lm_mu = 0.25_wp*lm_mu IF (lm_mu < 1.0e-10_wp) lm_mu = CD_ZERO @@ -1406,6 +1582,439 @@ RECURSIVE SUBROUTINE energy_update(es, em) stalled = .TRUE. END SUBROUTINE energy_update + SUBROUTINE add_torsion(ecs, ecm) + !! Condensed torsion at the current q: Theta (unwrapped against the last accepted value; a + !! change above pi/2 is refused so that the caller cuts the step), M_t = (Phi - Theta)/C, + !! R <- R - M_t dTheta/dq, band <- band - M_t d2Theta/dq2, energy + (Phi - Theta)**2/(2 C). + INTEGER, INTENT(OUT) :: ecs + CHARACTER(*), INTENT(OUT) :: ecm + REAL(wp) :: raw, th, e_t + INTEGER :: ks + CHARACTER(200) :: km + ecs = CD_HCSTAT_OK + ecm = '' + IF (skip_tangent) THEN + CALL CD_HermiteTorsion_Line(q, l0, torsion%ends, raw, tors_g, ks, km, & + quadrature_order=torsion%quadrature_order) + ELSE + tors_h = CD_ZERO + CALL CD_HermiteTorsion_Line(q, l0, torsion%ends, raw, tors_g, ks, km, hband=tors_h, & + quadrature_order=torsion%quadrature_order) + END IF + IF (ks /= CD_HTORS_OK) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion: '//TRIM(km) + RETURN + END IF + th = CD_HermiteTorsion_Unwrap(raw, tors_theta_acc) + IF (ABS(th - tors_theta_acc) > CD_HTORS_MAX_STEP) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion: the twist changed by more than pi/2 from the last accepted state; cut the step' + RETURN + END IF + tors_theta_eval = th + tors_mt = (tors_phi_cur - th)/tors_c + R = R - tors_mt*tors_g + IF (.NOT. skip_tangent) ab(KL_H + 1:LDAB_H, :) = ab(KL_H + 1:LDAB_H, :) - tors_mt*tors_h + e_t = 0.5_wp*tors_c*tors_mt*tors_mt + pi_total = pi_total + e_t + pi_abs = pi_abs + e_t + END SUBROUTINE add_torsion + + SUBROUTINE torsion_reduce() + !! dTheta/dq in the solve coordinates (the rigid-end basis, as apply_rigid_system maps R), + !! with every held DOF zeroed. + INTEGER :: iend, base, j + tors_gr = tors_g + IF (use_rigid) THEN + DO iend = 1, 2 + IF (endconn_kind(iend) /= CD_ENDCONN_RIGID) CYCLE + base = MERGE(3, 6*(nn - 1) + 3, iend == 1) + tors_gr(base + 1:base + 3) = MATMUL(TRANSPOSE(rigid_basis(:, :, iend)), tors_g(base + 1:base + 3)) + END DO + END IF + DO j = 1, ndof + IF (.NOT. solve_mask(j)) tors_gr(j) = CD_ZERO + END DO + END SUBROUTINE torsion_reduce + + SUBROUTINE limit_torsion_step(grad, increment, scale) + !! Predictive twist-step limit: the linearised change grad . increment may not exceed half + !! the accepted limit (the acceptance test after the step remains in force). + REAL(wp), INTENT(IN) :: grad(:), increment(:) + REAL(wp), INTENT(INOUT) :: scale + REAL(wp) :: dth + dth = ABS(DOT_PRODUCT(grad, increment)) + IF (dth*scale > 0.5_wp*CD_HTORS_MAX_STEP) scale = 0.5_wp*CD_HTORS_MAX_STEP/dth + END SUBROUTINE limit_torsion_step + + SUBROUTINE torsion_take_step(scale, ecs, ecm) + !! q <- q + scale dq under the twist acceptance rule (CD_HermiteTorsion_Accept): the new + !! Theta, unwrapped against the last accepted value, may change by at most pi/2 and the + !! line must pass the fold guard; otherwise the step is halved, at most 30 times. + REAL(wp), INTENT(IN) :: scale + INTEGER, INTENT(OUT) :: ecs + CHARACTER(*), INTENT(OUT) :: ecm + REAL(wp) :: sfac, raw, th + INTEGER :: k, ks + CHARACTER(200) :: km + tors_q = q + sfac = scale + km = '' + DO k = 0, 30 + q = tors_q + sfac*dq + CALL CD_HermiteTorsion_Line(q, l0, torsion%ends, raw, tors_gt, ks, km, & + quadrature_order=torsion%quadrature_order) + IF (ks == CD_HTORS_OK) THEN + CALL CD_HermiteTorsion_Accept(tors_theta_acc, raw, th, ks, km) + IF (ks == CD_HTORS_OK) THEN + tors_theta_acc = th + ecs = CD_HCSTAT_OK + ecm = '' + RETURN + END IF + END IF + sfac = 0.5_wp*sfac + END DO + q = tors_q + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion: no admissible Newton step ('//TRIM(km)//')' + END SUBROUTINE torsion_take_step + + SUBROUTINE torsion_continuation(ecs, ecm) + !! The imposed-twist load stage. From the converged untwisted state (Theta_0 on the branch + !! of torsion%theta_hint on a first solve; on a re-solve with has_theta, within pi/2 of + !! torsion%theta, or the solve stops by name) Phi is ramped from Theta_0 + !! (zero torque) to torsion%phi in stages of at most pi/4; a failed stage is restored and + !! halved, at most MAX_CUTS times. Each converged stage is tested for stability and an + !! unstable one is left by descent (torsion_stage). + INTEGER, INTENT(OUT) :: ecs + CHARACTER(*), INTENT(OUT) :: ecm + INTEGER, PARAMETER :: MAX_CUTS = 10 + REAL(wp), ALLOCATABLE :: q_keep(:) + REAL(wp) :: raw, phi0, total, lam, lam_try, dlam, dlam_max, th_keep, res_untwisted + INTEGER :: ks, cuts, wios + LOGICAL :: ok + CHARACTER(200) :: km + CHARACTER(512) :: why + CHARACTER(1024) :: wbuf + ecs = CD_HCSTAT_OK + ecm = '' + res_untwisted = res_out + torsion%ramp_steps = 0 + torsion%descents = 0 + torsion%stable = .FALSE. + torsion%n_negative = 0 + torsion%lambda_min = CD_ZERO + CALL CD_HermiteTorsion_Line(q, l0, torsion%ends, raw, tors_g, ks, km, quadrature_order=torsion%quadrature_order) + IF (ks /= CD_HTORS_OK) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'CD_HermiteCable_Static_Solve: torsion at the untwisted equilibrium: '//TRIM(km) + RETURN + END IF + IF (torsion%has_theta) THEN + ! A re-solve: the untwisted state must lie within the step limit of the stored Theta, + ! or its branch is ambiguous (the line may have released more than half a turn of writhe) + CALL CD_HermiteTorsion_Accept(torsion%theta, raw, tors_theta_acc, ks, km) + IF (ks /= CD_HTORS_OK) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'CD_HermiteCable_Static_Solve: torsion: the untwisted equilibrium lies more than 90 deg of '// & + 'twist from the stored Theta, so its turn count is ambiguous; solve from a fresh torsion '// & + 'state (has_theta false) with theta_hint on the intended branch' + RETURN + END IF + ELSE + tors_theta_acc = CD_HermiteTorsion_Unwrap(raw, torsion%theta_hint) + END IF + phi0 = tors_theta_acc + total = torsion%phi - phi0 + tors_on = .TRUE. + dlam_max = CD_ONE/REAL(MAX(1, CEILING(ABS(total)/(0.25_wp*CD_HTORS_PI))), wp) + dlam = dlam_max + lam = CD_ZERO + cuts = 0 + ALLOCATE (q_keep(ndof)) + DO + lam_try = MIN(CD_ONE, lam + dlam) + IF (lam_try >= CD_ONE) THEN + tors_phi_cur = torsion%phi + ELSE + tors_phi_cur = phi0 + lam_try*total + END IF + q_keep = q + th_keep = tors_theta_acc + CALL torsion_stage(ok, why) + IF (ok) THEN + lam = lam_try + torsion%ramp_steps = torsion%ramp_steps + 1 + IF (lam >= CD_ONE) EXIT + dlam = MIN(dlam_max, 2.0_wp*dlam) + ELSE + q = q_keep + tors_theta_acc = th_keep + cuts = cuts + 1 + IF (cuts > MAX_CUTS) THEN + ecs = CD_HCSTAT_NOCONVERGE + wbuf = '' + IF (lam <= CD_ZERO .AND. res_untwisted >= tol) THEN + ! not a loop: the untwisted equilibrium itself never reached the tolerance + WRITE (wbuf, '(A,ES12.5,A,ES12.5,A)', IOSTAT=wios) & + 'CD_HermiteCable_Static_Solve: torsion continuation could not start: the untwisted '// & + 'equilibrium ends at ||R|| = ', res_untwisted, ' above tol = ', tol, & + ' (a residual floor of this line); a looser tolerance is needed' + ecm = wbuf + RETURN + END IF + WRITE (wbuf, '(A,ES12.5,A,ES12.5,A)', IOSTAT=wios) & + 'CD_HermiteCable_Static_Solve: torsion continuation stalled at Phi = ', tors_phi_cur, & + ' rad (target ', torsion%phi, ' rad): '//TRIM(why)// & + '; the twisted line has no nearby static equilibrium on this path (for example a loop '// & + 'forming), which needs a dynamic analysis' + ecm = wbuf + RETURN + END IF + dlam = 0.5_wp*dlam + END IF + END DO + END SUBROUTINE torsion_continuation + + SUBROUTINE torsion_stage(ok, why) + !! Newton to tol at the current Phi, then (torsion%check_stability) the stability test and, + !! for an unstable state with torsion%descend, the negative-curvature descent. + LOGICAL, INTENT(OUT) :: ok + CHARACTER(*), INTENT(OUT) :: why + INTEGER :: it2, ks, wios + REAL(wp) :: rn + LOGICAL :: st + CHARACTER(512) :: km + CHARACTER(1024) :: wbuf + ok = .FALSE. + why = '' + km = '' + rn = HUGE(CD_ONE) + stalled = .FALSE. + DO it2 = 1, max_iter + CALL newton_step(rn, ks, km, .TRUE.) + IF (stalled) THEN + why = 'the Newton iteration stalled' + EXIT + END IF + IF (ks /= CD_HCSTAT_OK) THEN + why = km + EXIT + END IF + iters_out = iters_out + 1 + res_out = rn + IF (rn < tol) THEN + ok = .TRUE. + EXIT + END IF + END DO + stalled = .FALSE. + IF (.NOT. ok) THEN + IF (LEN_TRIM(why) == 0) THEN + wbuf = '' + WRITE (wbuf, '(A,ES12.5)', IOSTAT=wios) 'Newton did not converge, ||R||=', rn + why = wbuf + END IF + RETURN + END IF + IF (.NOT. torsion%check_stability) RETURN + CALL torsion_stability(st, ks, km) + IF (ks /= CD_HCSTAT_OK) THEN + ok = .FALSE. + why = km + RETURN + END IF + torsion%stable = st + IF (st .OR. .NOT. torsion%descend) RETURN + CALL torsion_descent(ok, why) + END SUBROUTINE torsion_stage + + SUBROUTINE torsion_stability(st, ecs, ecm) + !! Stability of the state q with torsion: the inertia of the symmetric tangent + !! B + g g^T / C on the free DOFs (CD_HermiteTorsion_Inertia). A negative (or unreliable) + !! count is checked by the lowest Jacobi-scaled eigenvalue (CD_HermiteTorsion_Lowest_Mode), + !! whose mode is kept for the descent; with torsion%zero_mode_allowed one eigenvalue within + !! CD_HTORS_ZERO_MODE_TOL of zero is neutral. A seabed friction capacity is held at its + !! equilibrium value, as for the stable argument. + LOGICAL, INTENT(OUT) :: st + INTEGER, INTENT(OUT) :: ecs + CHARACTER(*), INTENT(OUT) :: ecm + REAL(wp) :: rn, ztol + INTEGER :: ks, kin, nb, nk + CHARACTER(512) :: km + st = .FALSE. + ecs = CD_HCSTAT_OK + ecm = '' + tors_have_mode = .FALSE. + IF (use_friction .AND. .NOT. fr_frozen) stab_eval = .TRUE. + CALL newton_step(rn, ks, km, .FALSE.) + stab_eval = .FALSE. + IF (ks /= CD_HCSTAT_OK) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion stability evaluation failed: '//TRIM(km) + RETURN + END IF + CALL CD_HermiteTorsion_Inertia(ab, KL_H, KU_H, solve_mask, tors_gr, tors_c, nb, nk, kin, km) + torsion%lambda_min = CD_ZERO + torsion%n_negative = nk + IF (kin /= CD_HTORS_OK .AND. kin /= CD_HTORS_UNRELIABLE) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion stability evaluation failed: '//TRIM(km) + RETURN + END IF + IF (kin == CD_HTORS_OK .AND. nk == 0) THEN + st = .TRUE. + RETURN + END IF + CALL CD_HermiteTorsion_Lowest_Mode(ab, KL_H, KU_H, solve_mask, tors_gr, tors_c, tors_lambda, tors_v, ks, km) + IF (ks /= CD_HTORS_OK) THEN + ecs = CD_HCSTAT_NOCONVERGE + ecm = 'torsion stability evaluation failed: '//TRIM(km) + RETURN + END IF + tors_have_mode = .TRUE. + torsion%lambda_min = tors_lambda + ! The zero-mode band is relative to the softest physical scale of the Jacobi-scaled + ! spectrum, the smallest EI/(EA Le^2) (bending against stretching), and at least + ! a round-off floor: an absolute 1e-8 could absorb a genuine soft bending mode of a stiff-EA + ! cable. + ztol = CD_HTORS_ZERO_MODE_TOL*MIN(CD_ONE, soft_scale()) + ztol = MAX(ztol, 1.0e3_wp*EPSILON(CD_ONE)) + IF (kin == CD_HTORS_OK) THEN + ! a reliable count of nk >= 1 negative eigenvalues: stable only as the allowed zero mode + st = torsion%zero_mode_allowed .AND. nk == 1 .AND. ABS(tors_lambda) <= ztol + ELSE + nk = MERGE(1, 0, tors_lambda < -ztol) + torsion%n_negative = nk + st = tors_lambda > ztol .OR. (torsion%zero_mode_allowed .AND. tors_lambda >= -ztol) + END IF + END SUBROUTINE torsion_stability + + REAL(wp) FUNCTION soft_scale() RESULT(sc) + !! min over elements of EI/(EA Le^2) (1 without an axially stiff element). + INTEGER :: e + sc = CD_ONE + DO e = 1, ne + IF (EA(e) > CD_ZERO .AND. EI(e) > CD_ZERO) sc = MIN(sc, EI(e)/(EA(e)*l0(e)*l0(e))) + END DO + END FUNCTION soft_scale + + SUBROUTINE torsion_descent(ok, why) + !! Leave an unstable twisted equilibrium: perturb along the lowest mode (both senses, a + !! decreasing amplitude) to a state of lower total energy, then minimise the energy + !! (energy_update without its Newton shortcut, which would return to the saddle) and test + !! the stability again; at most MAX_DESCENTS times. + LOGICAL, INTENT(OUT) :: ok + CHARACTER(*), INTENT(OUT) :: why + INTEGER, PARAMETER :: MAX_DESCENTS = 4 + REAL(wp) :: e0, amp, a, rn, vscale, th0 + INTEGER :: attempt, k, j, ks, it2 + LOGICAL :: saved_energy, st, moved + CHARACTER(512) :: km + ok = .FALSE. + why = '' + km = '' + IF (drag_now .OR. (use_friction .AND. .NOT. fr_frozen)) THEN + why = 'the twisted equilibrium is unstable, and its energy descent needs conservative loads '// & + '(no current drag or sliding seabed friction)' + RETURN + END IF + DO attempt = 1, MAX_DESCENTS + IF (.NOT. tors_have_mode) THEN + why = 'no unstable mode was found for the descent' + RETURN + END IF + skip_tangent = .TRUE. + CALL newton_step(rn, ks, km, .FALSE.) + skip_tangent = .FALSE. + IF (ks /= CD_HCSTAT_OK) THEN + why = km + RETURN + END IF + e0 = pi_total + tors_rhs = tors_v + IF (use_rigid) CALL rigid_increment_to_global(tors_rhs) + vscale = CD_ZERO + DO j = 1, ndof + IF (MOD(j - 1, 6) >= 3) THEN + vscale = MAX(vscale, ABS(tors_rhs(j))) + ELSE + vscale = MAX(vscale, ABS(tors_rhs(j))/trib((j - 1)/6 + 1)) + END IF + END DO + IF (.NOT. (vscale > CD_ZERO)) THEN + why = 'the unstable mode vanishes on the free DOFs' + RETURN + END IF + amp = 0.05_wp/vscale + ! predictive twist limit (the post-trial unwrap cannot tell 3 pi/2 from -pi/2); tors_g is + ! dTheta/dq at q from the evaluation above + a = CD_ONE + CALL limit_torsion_step(tors_g, amp*tors_rhs, a) + amp = a*amp + tors_q = q + th0 = tors_theta_acc + moved = .FALSE. + DO k = 0, 11 + a = amp*0.25_wp**(k/2) + IF (MOD(k, 2) == 1) a = -a + q = tors_q + a*tors_rhs + skip_tangent = .TRUE. + CALL newton_step(rn, ks, km, .FALSE.) + skip_tangent = .FALSE. + IF (ks == CD_HCSTAT_OK .AND. pi_total < e0) THEN + tors_theta_acc = tors_theta_eval + moved = .TRUE. + EXIT + END IF + END DO + IF (.NOT. moved) THEN + q = tors_q + tors_theta_acc = th0 + why = 'no energy-decreasing perturbation along the unstable mode' + RETURN + END IF + saved_energy = use_energy + use_energy = .TRUE. + tors_descending = .TRUE. + lm_mu = CD_ZERO + stalled = .FALSE. + ok = .FALSE. + DO it2 = 1, MAX(max_iter, 200) + CALL newton_step(rn, ks, km, .TRUE.) + IF (stalled .OR. ks /= CD_HCSTAT_OK) EXIT + iters_out = iters_out + 1 + res_out = rn + IF (rn < tol) THEN + ok = .TRUE. + EXIT + END IF + END DO + use_energy = saved_energy + tors_descending = .FALSE. + stalled = .FALSE. + lm_mu = CD_ZERO + IF (.NOT. ok) THEN + why = 'the energy descent from the unstable twisted state did not converge' + IF (LEN_TRIM(km) > 0) why = TRIM(why)//' ('//TRIM(km)//')' + RETURN + END IF + torsion%descents = torsion%descents + 1 + CALL torsion_stability(st, ks, km) + IF (ks /= CD_HCSTAT_OK) THEN + ok = .FALSE. + why = km + RETURN + END IF + torsion%stable = st + IF (st) RETURN + ok = .FALSE. + END DO + why = 'the descent did not reach a stable twisted equilibrium' + END SUBROUTINE torsion_descent + SUBROUTINE setup_friction(fr, ecs, ecm) !! Validate the friction and map its reference onto the nodes of this mesh; the !! spring stiffness is the nodal normal contact stiffness. mu <= 0 leaves it off. diff --git a/src/CableDyn_HermiteTorsion.f90 b/src/CableDyn_HermiteTorsion.f90 new file mode 100644 index 0000000..f554be5 --- /dev/null +++ b/src/CableDyn_HermiteTorsion.f90 @@ -0,0 +1,1521 @@ +! File: src/CableDyn_HermiteTorsion.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +MODULE CableDyn_HermiteTorsion + !! Parallel-transport twist of a cubic-Hermite cable line, for condensed isotropic torsion. + !! + !! The Hermite cable (CableDyn_HermiteCable) carries [r(3), m(3)] per node, m = dr/ds, and has + !! no twist coordinate. For an isotropic section without distributed torque the torque is + !! uniform along a line, and the twist field condenses to one scalar per line: + !! E_t = (Phi - Theta(q))**2 / (2 C), C = sum_e L_e / GJ_e, M_t = (Phi - Theta) / C, + !! where Phi is the imposed relative roll of the end frames and Theta(q) is the parallel- + !! transport (smallest-rotation) holonomy of the centreline: the angle about d_B, right-hand + !! rule, from the End-B reference normal n_B to the End-A normal n_A carried along the line. + !! Because the Hermite curve is C1, Theta is the sum of + !! * one element holonomy per element (12 element DOFs), + !! h_e = + integral_e t1 . (a x b) / (|a|**2 (1 + t1 . a/|a|)) ds, + !! a = r', b = r'', t1 = m1/|m1| (Gauss-Legendre quadrature, four points by default); + !! * the smallest-rotation chain d_A -> t_0 -> ... -> t_N -> d_B through the unit node + !! tangents t_k = m_k/|m_k|. + !! The value of the chain is evaluated by sequential composition. Its first and second + !! derivatives are formed as a sum of local terms: every link j (u_j -> u_j+1) is given a gauge + !! with a pole p_j = unit(u_j + u_j+1) frozen at the current configuration, so that the chain + !! equals, up to a locally constant multiple of 2 pi, + !! sum_links Omega(p_j, u_j, u_j+1) - sum_vertices Omega(p_j-1, p_j, u_j) + end terms, + !! with Omega(x, y, z) = 2 atan2(x . (y x z), 1 + x.y + y.z + z.x) the signed solid angle of a + !! geodesic triangle. Every term depends on at most two neighbouring tangents, so the Hessian + !! has the half-bandwidth 11 of the cable tangent. Gradient and Hessian are closed form. + !! + !! The end frames (d, n) enter through incremental rotations exp(w) about the current frame, + !! w = 0: the routine also returns dTheta/dw and the w-w and w-m second derivatives. + !! + !! Theta is known modulo 2 pi. CD_HermiteTorsion_Accept selects the branch nearest to the + !! previous accepted value and rejects changes larger than pi/2, so a 2 pi slip cannot pass + !! unnoticed. All routines fail closed with named status codes: folds (1 + t1 . t < 0.5 at a + !! Gauss point or across a chain link), non-finite values, and malformed input. + USE CableDyn_Precision, ONLY: wp, CD_ZERO, CD_ONE, CD_All_Finite, CD_Is_Finite + USE CableDyn_HermiteCable, ONLY: CD_HermiteCable_Gauss_Rule + USE CableDyn_Linalg, ONLY: CD_Solve_Dense_As_Banded, CD_LINALG_OK + IMPLICIT NONE + PRIVATE + + PUBLIC :: CD_HermiteTorsion_Element + PUBLIC :: CD_HermiteTorsion_Line + PUBLIC :: CD_HermiteTorsion_Fold_Check + PUBLIC :: CD_HermiteTorsion_Unwrap + PUBLIC :: CD_HermiteTorsion_Accept + PUBLIC :: CD_HermiteTorsionType + PUBLIC :: CD_HermiteTorsion_Compliance + PUBLIC :: CD_HermiteTorsion_Validate + PUBLIC :: CD_HermiteTorsion_Bordered_Solve + PUBLIC :: CD_HermiteTorsion_Inertia + PUBLIC :: CD_HermiteTorsion_Lowest_Mode + PUBLIC :: CD_HermiteTorsionWorkType + PUBLIC :: CD_HermiteTorsion_Work_Init + PUBLIC :: CD_HermiteTorsion_Work_End + PUBLIC :: CD_HermiteTorsion_Work_Add_Hessian + + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_OK = 0 + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_BADINPUT = 1 + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_FOLD = 2 + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_NONFINITE = 3 + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_STEP = 4 + !! The bordered step stayed above its backward-error bound on every path (all values finite). + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_ILLCOND = 5 + !! The inertia count is unreliable (vanishing pivot or pivot growth); compute the lowest mode. + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_UNRELIABLE = 6 + !! Largest system solved densely when the banded bordered step fails. + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_DENSE_MAX = 600 + !! Half-bandwidth of d2Theta/dq2 (equal to that of the cable tangent). + INTEGER, PARAMETER, PUBLIC :: CD_HTORS_KBAND = 11 + !! Fold guard: 1 + t1 . t at every Gauss point and 1 + u_j . u_j+1 on every chain link. + REAL(wp), PARAMETER, PUBLIC :: CD_HTORS_FOLD_MIN = 0.5_wp + REAL(wp), PARAMETER, PUBLIC :: CD_HTORS_PI = 3.14159265358979323846264338327950288_wp + !! Largest accepted change of Theta between two accepted states. + REAL(wp), PARAMETER, PUBLIC :: CD_HTORS_MAX_STEP = 0.5_wp*CD_HTORS_PI + + INTEGER, PARAMETER :: NG_DEFAULT = 4, NG_MAX = 6 + REAL(wp), PARAMETER :: TWO_PI = 2.0_wp*CD_HTORS_PI + REAL(wp), PARAMETER :: GEOM_TOL = 1.0e-12_wp + REAL(wp), PARAMETER :: FRAME_TOL = 1.0e-8_wp + !! Normwise backward error accepted for a bordered (Sherman-Morrison) step. + REAL(wp), PARAMETER, PUBLIC :: CD_HTORS_BACKWARD_TOL = 1.0e-10_wp + !! Scaled eigenvalue magnitude below which one exact zero mode (rotation of a buckled shape + !! about a common clamp axis) is accepted as neutral, when the caller allows it. + REAL(wp), PARAMETER, PUBLIC :: CD_HTORS_ZERO_MODE_TOL = 1.0e-8_wp + + TYPE :: CD_HermiteTorsionType + !! Condensed torsion of one line: inputs, the one persistent state value (the accepted, + !! unwrapped Theta) and the static stability report. Ends follow the solver's node order: + !! end 1 is node 1 and end 2 the last node (the kernel's A and B). + LOGICAL :: active = .FALSE. + !! Imposed relative roll of the end-2 frame with respect to the end-1 frame [rad]; real + !! valued, never wrapped (multiple turns are allowed). + REAL(wp) :: phi = CD_ZERO + !! Columns d_1, n_1, d_2, n_2 in the solve frame: unit end directors and unit reference + !! normals perpendicular to them. + REAL(wp) :: ends(3, 4) = CD_ZERO + !! Torsional end compliances 1/k_t [rad/(N m)] at ends 1 and 2 (0 for a rigid end). + REAL(wp) :: end_compliance(2) = CD_ZERO + !! Torsional stiffness GJ of every element [N m^2/rad] (the line compliance is the sum of + !! l0/GJ over the elements plus the end compliances). + REAL(wp), ALLOCATABLE :: gj(:) + INTEGER :: quadrature_order = 4 + !! State: the accepted Theta (unwrapped) once has_theta; before that theta_hint selects the + !! 2 pi branch of the first evaluation (0 for an untwisted start). + LOGICAL :: has_theta = .FALSE. + REAL(wp) :: theta = CD_ZERO + REAL(wp) :: theta_hint = CD_ZERO + !! Torque M_t = (Phi - Theta)/C at the accepted state [N m]; positive is a right-handed twist. + REAL(wp) :: torque = CD_ZERO + !! Static controls: test the stability of every converged twist stage, leave an unstable + !! state by negative-curvature descent, and accept one exact zero mode (coaxial clamped + !! ends without gravity or current). + LOGICAL :: check_stability = .TRUE. + LOGICAL :: descend = .TRUE. + LOGICAL :: zero_mode_allowed = .FALSE. + !! Static report at the returned state: stability, negative-eigenvalue count of the tangent + !! on the free DOFs, the lowest Jacobi-scaled eigenvalue when it was computed (0 otherwise), + !! the total potential energy, the twist-continuation stage count and the descents taken. + LOGICAL :: stable = .FALSE. + INTEGER :: n_negative = 0 + REAL(wp) :: lambda_min = CD_ZERO + REAL(wp) :: energy = CD_ZERO + INTEGER :: ramp_steps = 0 + INTEGER :: descents = 0 + END TYPE CD_HermiteTorsionType + + TYPE :: CD_HermiteTorsionWorkType + !! Scratch of CD_HermiteTorsion_Line for a line of nn nodes (CD_HermiteTorsion_Work_Init). + !! Passed as `work`, it makes the evaluation free of heap allocation (the dynamic step). + INTEGER :: nn = 0 + REAL(wp), ALLOCATABLE :: u(:, :), pole(:, :), gu(:, :), huu(:, :, :), huv(:, :, :), band(:, :) + !! band holds the Hessian of the last successful CD_HermiteTorsion_Line call with keep_hessian + LOGICAL :: hess_valid = .FALSE. + END TYPE CD_HermiteTorsionWorkType + +CONTAINS + + ! ------------------------------------------------------------------------------------------ + ! line torsion data + ! ------------------------------------------------------------------------------------------ + + PURE REAL(wp) FUNCTION CD_HermiteTorsion_Compliance(tors, l0) RESULT(c) + !! Line torsional compliance C = sum_e l0_e/GJ_e + 1/k_t1 + 1/k_t2 [rad/(N m)]. Meant for a + !! description that passed CD_HermiteTorsion_Validate: without one GJ per element it + !! returns NaN (so no caller can take the end compliances alone for the line's). + USE, INTRINSIC :: IEEE_ARITHMETIC, ONLY: IEEE_VALUE, IEEE_QUIET_NAN + TYPE(CD_HermiteTorsionType), INTENT(IN) :: tors + REAL(wp), INTENT(IN) :: l0(:) + c = IEEE_VALUE(c, IEEE_QUIET_NAN) + IF (.NOT. ALLOCATED(tors%gj)) RETURN + IF (SIZE(tors%gj) /= SIZE(l0)) RETURN + c = tors%end_compliance(1) + tors%end_compliance(2) + SUM(l0/tors%gj) + END FUNCTION CD_HermiteTorsion_Compliance + + PURE SUBROUTINE CD_HermiteTorsion_Validate(tors, l0, ErrStat, ErrMsg) + !! Fail-closed check of an active torsion description against a mesh of SIZE(l0) elements. + TYPE(CD_HermiteTorsionType), INTENT(IN) :: tors + REAL(wp), INTENT(IN) :: l0(:) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: k + ErrStat = CD_HTORS_BADINPUT + IF (.NOT. ALLOCATED(tors%gj)) THEN + ErrMsg = 'torsion: GJ per element is missing' + RETURN + END IF + IF (SIZE(tors%gj) /= SIZE(l0)) THEN + ErrMsg = 'torsion: GJ must have one entry per element' + RETURN + END IF + IF (.NOT. (CD_All_Finite(tors%gj) .AND. CD_All_Finite(tors%ends) .AND. CD_Is_Finite(tors%phi) .AND. & + CD_All_Finite(tors%end_compliance) .AND. CD_Is_Finite(tors%theta) .AND. & + CD_Is_Finite(tors%theta_hint))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'torsion: non-finite GJ, end frame, imposed twist, compliance or state' + RETURN + END IF + IF (ANY(.NOT. (tors%gj > CD_ZERO))) THEN + ErrMsg = 'torsion: GJ must be positive on every element' + RETURN + END IF + IF (ANY(tors%end_compliance < CD_ZERO)) THEN + ErrMsg = 'torsion: end compliances must be non-negative' + RETURN + END IF + IF (tors%quadrature_order < 1 .OR. tors%quadrature_order > NG_MAX) THEN + ErrMsg = 'torsion: quadrature order must lie in [1,6]' + RETURN + END IF + DO k = 1, 3, 2 + IF (ABS(NORM2(tors%ends(:, k)) - CD_ONE) > FRAME_TOL .OR. ABS(NORM2(tors%ends(:, k + 1)) - CD_ONE) > FRAME_TOL & + .OR. ABS(DOT_PRODUCT(tors%ends(:, k), tors%ends(:, k + 1))) > FRAME_TOL) THEN + ErrMsg = 'torsion: each end director and reference normal must be orthonormal' + RETURN + END IF + END DO + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE CD_HermiteTorsion_Validate + + ! ------------------------------------------------------------------------------------------ + ! bordered (Sherman-Morrison) solve and the inertia of B + g g^T / C + ! ------------------------------------------------------------------------------------------ + + SUBROUTINE CD_HermiteTorsion_Bordered_Solve(ab, kl, ku, g, compliance, rhs, x, ErrStat, ErrMsg, & + backward_error, shifted, dense) + !! Solve (B + g g^T / C) x = rhs, B a general band matrix in LAPACK DGBSV storage (ab, with + !! the 2 kl + ku + 1 rows of DGBTRF; not modified) and g a dense vector, through one band + !! factorization of B and two right-hand sides (the bordered system [B g; g^T -C]): + !! B y = rhs, B z = g, mu = g.y/(C + g.z), x = y - mu z. + !! The step is accepted only with a successful factorization, the pivot guard + !! min|U_ii| > 1e3 eps max|U_ii|, a denominator |C + g.z| > 1e-12 (C + |g.z|) and a normwise + !! backward error ||K x - rhs|| / (||K|| ||x|| + ||rhs||) <= CD_HTORS_BACKWARD_TOL (infinity + !! norms, K applied as the band product plus the rank-one term). Otherwise B is shifted by + !! sigma = 1e-10 max|diag B|, Sherman-Morrison is applied to the shifted matrix and two steps + !! of iterative refinement against the true K follow; the same backward-error test decides. + !! Sherman-Morrison loses accuracy when g^T B^-1 g / C is very large even if K is well + !! conditioned, so a line of at most CD_HTORS_DENSE_MAX DOFs is then solved densely (K + !! assembled, LU with partial pivoting, dense = .TRUE.) under the same test. A failure of + !! every path leaves x = 0 with ErrStat CD_HTORS_ILLCOND (CD_HTORS_NONFINITE only for a + !! non-finite result). B may be singular where K is not (the rank-one term is positive + !! semidefinite); at a straight state g = 0 and K = B. + REAL(wp), INTENT(IN) :: ab(:, :) + INTEGER, INTENT(IN) :: kl, ku + REAL(wp), INTENT(IN) :: g(:), compliance, rhs(:) + REAL(wp), INTENT(OUT) :: x(:) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(OUT), OPTIONAL :: backward_error + LOGICAL, INTENT(OUT), OPTIONAL :: shifted, dense + REAL(wp), ALLOCATABLE :: lu(:, :), rr(:, :), res(:), dx(:) + INTEGER, ALLOCATABLE :: ipiv(:) + REAL(wp) :: sigma, eta, knorm + INTEGER :: n, ldab, attempt, refine + LOGICAL :: ok, finite_fail + CHARACTER(160) :: why + + x = CD_ZERO + IF (PRESENT(backward_error)) backward_error = HUGE(CD_ONE) + IF (PRESENT(shifted)) shifted = .FALSE. + IF (PRESENT(dense)) dense = .FALSE. + n = SIZE(ab, 2) + ldab = SIZE(ab, 1) + ErrStat = CD_HTORS_BADINPUT + IF (kl < 0 .OR. ku < 0 .OR. ldab /= 2*kl + ku + 1 .OR. SIZE(g) /= n .OR. SIZE(rhs) /= n .OR. SIZE(x) /= n) THEN + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: inconsistent band storage or vector sizes' + RETURN + END IF + IF (.NOT. (compliance > CD_ZERO) .OR. .NOT. CD_Is_Finite(compliance)) THEN + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: the line compliance must be finite and positive' + RETURN + END IF + IF (.NOT. (CD_All_Finite(g) .AND. CD_All_Finite(rhs))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: non-finite coupling vector or right-hand side' + RETURN + END IF + ALLOCATE (lu(ldab, n), rr(n, 2), res(n), dx(n), ipiv(n)) + knorm = band_inf_norm(ab, kl, ku) + SUM(ABS(g))*MAXVAL(ABS(g))/compliance + sigma = CD_ZERO + why = '' + DO attempt = 1, 2 + IF (attempt == 2) THEN + sigma = 1.0e-10_wp*MAX(MAXVAL(ABS(ab(kl + ku + 1, :))), TINY(CD_ONE)) + IF (PRESENT(shifted)) shifted = .TRUE. + END IF + CALL sm_solve(sigma, rhs, x, ok, why) + IF (.NOT. ok) CYCLE + IF (attempt == 2) THEN + DO refine = 1, 2 + CALL bordered_residual(x, res) + CALL sm_solve(sigma, res, dx, ok, why) + IF (.NOT. ok) EXIT + x = x + dx + END DO + IF (.NOT. ok) CYCLE + END IF + CALL bordered_residual(x, res) + eta = MAXVAL(ABS(res))/MAX(knorm*MAXVAL(ABS(x)) + MAXVAL(ABS(rhs)), TINY(CD_ONE)) + IF (PRESENT(backward_error)) backward_error = eta + IF (CD_Is_Finite(eta) .AND. eta <= CD_HTORS_BACKWARD_TOL .AND. CD_All_Finite(x)) THEN + ErrStat = CD_HTORS_OK + ErrMsg = '' + RETURN + END IF + WRITE (why, '(A,ES9.2)') 'backward error ', eta + END DO + ! dense fallback for a small line: K assembled and factored with partial pivoting + IF (n <= CD_HTORS_DENSE_MAX) THEN + CALL dense_solve(ok) + IF (ok) THEN + CALL bordered_residual(x, res) + eta = MAXVAL(ABS(res))/MAX(knorm*MAXVAL(ABS(x)) + MAXVAL(ABS(rhs)), TINY(CD_ONE)) + IF (PRESENT(backward_error)) backward_error = eta + IF (CD_Is_Finite(eta) .AND. eta <= CD_HTORS_BACKWARD_TOL .AND. CD_All_Finite(x)) THEN + IF (PRESENT(dense)) dense = .TRUE. + ErrStat = CD_HTORS_OK + ErrMsg = '' + RETURN + END IF + WRITE (why, '(A,ES9.2)') 'dense backward error ', eta + END IF + END IF + finite_fail = CD_All_Finite(x) + x = CD_ZERO + IF (finite_fail) THEN + ErrStat = CD_HTORS_ILLCOND + IF (n <= CD_HTORS_DENSE_MAX) THEN + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: ill-conditioned bordered torsion step ('//TRIM(why)// & + '), also with a shifted factorization, refinement and a dense solve' + ELSE + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: ill-conditioned bordered torsion step ('//TRIM(why)// & + '), also with a shifted factorization and refinement' + END IF + ELSE + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: non-finite bordered torsion step ('//TRIM(why)//')' + END IF + + CONTAINS + + SUBROUTINE dense_solve(success) + !! x from the dense K = B + g g^T / C (LU with partial pivoting through the band solver + !! at full bandwidth); success on a regular factor and a finite x. + LOGICAL, INTENT(OUT) :: success + REAL(wp), ALLOCATABLE :: kd(:, :) + INTEGER :: i, j, info + CHARACTER(200) :: lmsg + ALLOCATE (kd(n, n)) + DO j = 1, n + DO i = 1, n + kd(i, j) = g(i)*g(j)/compliance + END DO + DO i = MAX(1, j - ku), MIN(n, j + kl) + kd(i, j) = kd(i, j) + ab(kl + ku + 1 + i - j, j) + END DO + END DO + x = rhs + CALL CD_Solve_Dense_As_Banded(kd, x, info, lmsg) + success = info == CD_LINALG_OK .AND. CD_All_Finite(x) + IF (.NOT. success) THEN + why = TRIM(why)//'; dense factor singular' + x = CD_ZERO + END IF + END SUBROUTINE dense_solve + + SUBROUTINE sm_solve(shift, b, sol, success, reason) + REAL(wp), INTENT(IN) :: shift, b(:) + REAL(wp), INTENT(OUT) :: sol(:) + LOGICAL, INTENT(OUT) :: success + CHARACTER(*), INTENT(OUT) :: reason + REAL(wp) :: umax, umin, den, mu + INTEGER :: info, j + EXTERNAL :: dgbtrf, dgbtrs + success = .FALSE. + reason = '' + sol = CD_ZERO + lu = ab + IF (shift > CD_ZERO) lu(kl + ku + 1, :) = lu(kl + ku + 1, :) + shift + CALL dgbtrf(n, n, kl, ku, lu, ldab, ipiv, info) + IF (info /= 0) THEN + reason = 'singular band factor' + RETURN + END IF + umax = CD_ZERO + umin = HUGE(CD_ONE) + DO j = 1, n + umax = MAX(umax, ABS(lu(kl + ku + 1, j))) + umin = MIN(umin, ABS(lu(kl + ku + 1, j))) + END DO + IF (.NOT. (umin > 1.0e3_wp*EPSILON(CD_ONE)*umax)) THEN + reason = 'pivot guard' + RETURN + END IF + rr(:, 1) = b + rr(:, 2) = g + CALL dgbtrs('N', n, kl, ku, 2, lu, ldab, ipiv, rr, n, info) + IF (info /= 0 .OR. .NOT. CD_All_Finite(RESHAPE(rr, [2*n]))) THEN + reason = 'band back-substitution' + RETURN + END IF + den = compliance + DOT_PRODUCT(g, rr(:, 2)) + IF (.NOT. (ABS(den) > 1.0e-12_wp*(compliance + ABS(DOT_PRODUCT(g, rr(:, 2)))))) THEN + reason = 'vanishing Sherman-Morrison denominator' + RETURN + END IF + mu = DOT_PRODUCT(g, rr(:, 1))/den + sol = rr(:, 1) - mu*rr(:, 2) + success = CD_All_Finite(sol) + IF (.NOT. success) reason = 'non-finite step' + END SUBROUTINE sm_solve + + SUBROUTINE bordered_residual(sol, r) + !! r = rhs - (B + g g^T / C) sol, with the unshifted B. + REAL(wp), INTENT(IN) :: sol(:) + REAL(wp), INTENT(OUT) :: r(:) + INTEGER :: i, j + r = rhs - g*(DOT_PRODUCT(g, sol)/compliance) + DO j = 1, n + DO i = MAX(1, j - ku), MIN(n, j + kl) + r(i) = r(i) - ab(kl + ku + 1 + i - j, j)*sol(j) + END DO + END DO + END SUBROUTINE bordered_residual + END SUBROUTINE CD_HermiteTorsion_Bordered_Solve + + PURE REAL(wp) FUNCTION band_inf_norm(ab, kl, ku) RESULT(v) + !! Infinity norm of a general band matrix in DGBSV storage. + REAL(wp), INTENT(IN) :: ab(:, :) + INTEGER, INTENT(IN) :: kl, ku + REAL(wp), ALLOCATABLE :: rows(:) + INTEGER :: n, i, j + n = SIZE(ab, 2) + ALLOCATE (rows(n)) + rows = CD_ZERO + DO j = 1, n + DO i = MAX(1, j - ku), MIN(n, j + kl) + rows(i) = rows(i) + ABS(ab(kl + ku + 1 + i - j, j)) + END DO + END DO + v = CD_ZERO + IF (n > 0) v = MAXVAL(rows) + END FUNCTION band_inf_norm + + PURE SUBROUTINE scaled_symmetric_band(ab, kl, ku, mask, g, s, gs, dsc) + !! Lower symmetric band s (kl + 1 rows, LAPACK DPBTRF 'L' layout) of D sym(B) D on the DOFs + !! where mask is true (identity rows elsewhere), D = diag(|B_jj|^-1/2) (1 for a zero + !! diagonal), and gs = D g with masked entries zeroed. A congruence: the inertia of + !! sym(B) + g g^T / C equals that of s + gs gs^T / C. + REAL(wp), INTENT(IN) :: ab(:, :) + INTEGER, INTENT(IN) :: kl, ku + LOGICAL, INTENT(IN) :: mask(:) + REAL(wp), INTENT(IN) :: g(:) + REAL(wp), INTENT(OUT) :: s(:, :), gs(:), dsc(:) + INTEGER :: n, i, j, kd + n = SIZE(ab, 2) + kd = MIN(kl, ku) + dsc = CD_ONE + DO j = 1, n + IF (.NOT. mask(j)) CYCLE + IF (ABS(ab(kl + ku + 1, j)) > CD_ZERO) dsc(j) = CD_ONE/SQRT(ABS(ab(kl + ku + 1, j))) + END DO + s = CD_ZERO + DO j = 1, n + IF (.NOT. mask(j)) THEN + s(1, j) = CD_ONE + CYCLE + END IF + DO i = j, MIN(n, j + kd) + IF (.NOT. mask(i)) CYCLE + s(1 + i - j, j) = 0.5_wp*(ab(kl + ku + 1 + i - j, j) + ab(kl + ku + 1 + j - i, i))*dsc(i)*dsc(j) + END DO + END DO + gs = CD_ZERO + WHERE (mask) gs = dsc*g + END SUBROUTINE scaled_symmetric_band + + SUBROUTINE CD_HermiteTorsion_Inertia(ab, kl, ku, mask, g, compliance, n_negative_b, n_negative, ErrStat, ErrMsg) + !! Number of negative eigenvalues of the symmetric part of K = B + g g^T / C on the DOFs + !! where mask is true (B in DGBSV storage). With the determinant lemma and interlacing, + !! neg(K) = neg(B) - [1 + g^T B^-1 g / C < 0]; + !! neg(B) is counted by a banded L D L^T factorization without pivoting of the Jacobi-scaled + !! symmetric band (Sylvester's law of inertia), which also gives B^-1 g. A banded Cholesky + !! success short-cuts the count (B positive definite, hence K). A vanishing pivot or a + !! multiplier growth above 1e8 makes the count unreliable: ErrStat = CD_HTORS_UNRELIABLE. The + !! band must be square (kl = ku) and g finite. + REAL(wp), INTENT(IN) :: ab(:, :) + INTEGER, INTENT(IN) :: kl, ku + LOGICAL, INTENT(IN) :: mask(:) + REAL(wp), INTENT(IN) :: g(:), compliance + INTEGER, INTENT(OUT) :: n_negative_b, n_negative + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), ALLOCATABLE :: s(:, :), gs(:), dsc(:), lb(:, :), d(:), tmp(:), w(:) + REAL(wp) :: acc, smax, gbg + INTEGER :: n, kd, i, j, k, info + EXTERNAL :: dpbtrf + + n_negative_b = 0 + n_negative = 0 + n = SIZE(ab, 2) + kd = MIN(kl, ku) + IF (SIZE(ab, 1) /= 2*kl + ku + 1 .OR. SIZE(mask) /= n .OR. SIZE(g) /= n .OR. & + .NOT. (compliance > CD_ZERO) .OR. kl /= ku .OR. kl < 0) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Inertia: inconsistent input (a square band, kl = ku, is required)' + RETURN + END IF + IF (.NOT. (CD_All_Finite(g) .AND. CD_Is_Finite(compliance))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Inertia: non-finite coupling vector or compliance' + RETURN + END IF + ALLOCATE (s(kd + 1, n), gs(n), dsc(n), lb(kd + 1, n), d(n), tmp(n), w(n)) + CALL scaled_symmetric_band(ab, kl, ku, mask, g, s, gs, dsc) + IF (.NOT. CD_All_Finite(RESHAPE(s, [SIZE(s)]))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Inertia: non-finite tangent' + RETURN + END IF + lb = s + CALL dpbtrf('L', n, kd, lb, kd + 1, info) + IF (info == 0) THEN + ErrStat = CD_HTORS_OK + ErrMsg = '' + RETURN + END IF + smax = MAXVAL(ABS(s)) + lb = CD_ZERO + DO j = 1, n + acc = s(1, j) + DO k = MAX(1, j - kd), j - 1 + tmp(k) = lb(1 + j - k, k)*d(k) + acc = acc - lb(1 + j - k, k)*tmp(k) + END DO + d(j) = acc + IF (.NOT. (ABS(d(j)) > 1.0e-14_wp*smax)) THEN + ErrStat = CD_HTORS_UNRELIABLE + ErrMsg = 'CD_HermiteTorsion_Inertia: vanishing pivot (count unreliable)' + RETURN + END IF + DO i = j + 1, MIN(n, j + kd) + acc = s(1 + i - j, j) + DO k = MAX(1, i - kd), j - 1 + acc = acc - lb(1 + i - k, k)*tmp(k) + END DO + lb(1 + i - j, j) = acc/d(j) + IF (ABS(lb(1 + i - j, j)) > 1.0e8_wp) THEN + ErrStat = CD_HTORS_UNRELIABLE + ErrMsg = 'CD_HermiteTorsion_Inertia: pivot growth (count unreliable)' + RETURN + END IF + END DO + END DO + n_negative_b = COUNT(d < CD_ZERO) + ! w = S^-1 gs by the factors: L y = gs, D, L^T w = y/D + w = gs + DO j = 1, n + DO i = j + 1, MIN(n, j + kd) + w(i) = w(i) - lb(1 + i - j, j)*w(j) + END DO + END DO + w = w/d + DO j = n, 1, -1 + DO i = j + 1, MIN(n, j + kd) + w(j) = w(j) - lb(1 + i - j, j)*w(i) + END DO + END DO + gbg = DOT_PRODUCT(gs, w) + n_negative = n_negative_b + IF (CD_ONE + gbg/compliance < CD_ZERO) n_negative = n_negative - 1 + IF (.NOT. CD_Is_Finite(gbg)) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Inertia: non-finite coupling term' + RETURN + END IF + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE CD_HermiteTorsion_Inertia + + SUBROUTINE CD_HermiteTorsion_Lowest_Mode(ab, kl, ku, mask, g, compliance, lambda, v, ErrStat, ErrMsg) + !! Lowest eigenvalue lambda of the Jacobi-scaled symmetric part of K = B + g g^T / C on the + !! DOFs where mask is true (the scaling of CD_HermiteTorsion_Inertia: diag(D sym(B) D) = +-1), + !! and the corresponding direction v in the unscaled DOFs (v^T K v has the sign of lambda; + !! max|v| = 1; zero where mask is false). Shift-invert power iteration on (S + sigma I + + !! gs gs^T / C)^-1, sigma just above the smallest shift that makes S + sigma I positive + !! definite (banded Cholesky, bisected), with Sherman-Morrison for the rank-one term. + REAL(wp), INTENT(IN) :: ab(:, :) + INTEGER, INTENT(IN) :: kl, ku + LOGICAL, INTENT(IN) :: mask(:) + REAL(wp), INTENT(IN) :: g(:), compliance + REAL(wp), INTENT(OUT) :: lambda, v(:) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER, PARAMETER :: MAX_POWER = 400 + REAL(wp), ALLOCATABLE :: s(:, :), gs(:), dsc(:), ch(:, :), u(:), y(:), z(:) + REAL(wp) :: sig_lo, sig_hi, sig, lam_old, den + INTEGER :: n, kd, k, it, info + LOGICAL :: pd + EXTERNAL :: dpbtrf, dpbtrs + + lambda = CD_ZERO + v = CD_ZERO + n = SIZE(ab, 2) + kd = MIN(kl, ku) + IF (SIZE(ab, 1) /= 2*kl + ku + 1 .OR. SIZE(mask) /= n .OR. SIZE(g) /= n .OR. SIZE(v) /= n .OR. & + .NOT. (compliance > CD_ZERO) .OR. .NOT. ANY(mask) .OR. kl /= ku .OR. kl < 0) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Lowest_Mode: inconsistent input (a square band, kl = ku, is required)' + RETURN + END IF + IF (.NOT. (CD_All_Finite(g) .AND. CD_Is_Finite(compliance))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Lowest_Mode: non-finite coupling vector or compliance' + RETURN + END IF + ALLOCATE (s(kd + 1, n), gs(n), dsc(n), ch(kd + 1, n), u(n), y(n), z(n)) + CALL scaled_symmetric_band(ab, kl, ku, mask, g, s, gs, dsc) + ! bracket the positive-definiteness shift of S + sig_lo = CD_ZERO + sig_hi = 1.0e-6_wp + DO k = 1, 60 + CALL shifted_cholesky(sig_hi, pd) + IF (pd) EXIT + sig_lo = sig_hi + sig_hi = 4.0_wp*sig_hi + END DO + IF (.NOT. pd) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Lowest_Mode: no positive definite shift found' + RETURN + END IF + DO k = 1, 40 + IF (sig_hi - sig_lo <= 1.0e-3_wp*sig_hi) EXIT + sig = 0.5_wp*(sig_lo + sig_hi) + CALL shifted_cholesky(sig, pd) + IF (pd) THEN + sig_hi = sig + ELSE + sig_lo = sig + END IF + END DO + sig = sig_hi + CALL shifted_cholesky(sig, pd) + ! z = (S + sigma I)^-1 gs for the Sherman-Morrison update + z = gs + CALL dpbtrs('L', n, kd, 1, ch, kd + 1, z, n, info) + den = compliance + DOT_PRODUCT(gs, z) + DO k = 1, n + u(k) = MERGE(CD_ONE + 0.5_wp*SIN(0.7_wp*k), CD_ZERO, mask(k)) + END DO + u = u/NORM2(u) + lam_old = HUGE(CD_ONE) + DO it = 1, MAX_POWER + y = u + CALL dpbtrs('L', n, kd, 1, ch, kd + 1, y, n, info) + y = y - z*(DOT_PRODUCT(gs, y)/den) + WHERE (.NOT. mask) y = CD_ZERO + IF (.NOT. CD_All_Finite(y) .OR. NORM2(y) <= TINY(CD_ONE)) EXIT + u = y/NORM2(y) + lambda = rayleigh(u) + IF (ABS(lambda - lam_old) <= 1.0e-13_wp*MAX(CD_ONE, ABS(lambda))) EXIT + lam_old = lambda + END DO + IF (.NOT. (CD_All_Finite(u) .AND. CD_Is_Finite(lambda))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Lowest_Mode: non-finite iterate' + lambda = CD_ZERO + RETURN + END IF + v = dsc*u + WHERE (.NOT. mask) v = CD_ZERO + IF (MAXVAL(ABS(v)) > CD_ZERO) v = v/MAXVAL(ABS(v)) + ErrStat = CD_HTORS_OK + ErrMsg = '' + + CONTAINS + + SUBROUTINE shifted_cholesky(shift, ok) + REAL(wp), INTENT(IN) :: shift + LOGICAL, INTENT(OUT) :: ok + INTEGER :: j, inf + ch = s + DO j = 1, n + IF (mask(j)) ch(1, j) = ch(1, j) + shift + END DO + CALL dpbtrf('L', n, kd, ch, kd + 1, inf) + ok = inf == 0 + END SUBROUTINE shifted_cholesky + + REAL(wp) FUNCTION rayleigh(x) RESULT(rq) + !! x^T (S + gs gs^T / C) x / x^T x over the masked DOFs. + REAL(wp), INTENT(IN) :: x(:) + REAL(wp) :: acc + INTEGER :: i, j + acc = CD_ZERO + DO j = 1, n + IF (.NOT. mask(j)) CYCLE + acc = acc + s(1, j)*x(j)*x(j) + DO i = j + 1, MIN(n, j + kd) + IF (mask(i)) acc = acc + 2.0_wp*s(1 + i - j, j)*x(i)*x(j) + END DO + END DO + acc = acc + DOT_PRODUCT(gs, x)**2/compliance + rq = acc/MAX(DOT_PRODUCT(x, x), TINY(CD_ONE)) + END FUNCTION rayleigh + END SUBROUTINE CD_HermiteTorsion_Lowest_Mode + + ! ------------------------------------------------------------------------------------------ + ! public routines + ! ------------------------------------------------------------------------------------------ + + PURE SUBROUTINE CD_HermiteTorsion_Element(qe, L, h, grad, ErrStat, ErrMsg, hess, fold_margin, quadrature_order) + !! Element holonomy h_e of one 12-DOF element [r1, m1, r2, m2] of reference length L, with + !! its closed-form gradient and (optionally) Hessian. fold_margin returns min(1 + t1 . t) + !! over the Gauss points; below CD_HTORS_FOLD_MIN the routine fails with CD_HTORS_FOLD. + REAL(wp), INTENT(IN) :: qe(12), L + REAL(wp), INTENT(OUT) :: h, grad(12) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(OUT), OPTIONAL :: hess(12, 12) + REAL(wp), INTENT(OUT), OPTIONAL :: fold_margin + INTEGER, INTENT(IN), OPTIONAL :: quadrature_order + + REAL(wp) :: hk(12, 12), margin + INTEGER :: order + CHARACTER(200) :: em + + order = NG_DEFAULT + IF (PRESENT(quadrature_order)) order = quadrature_order + h = CD_ZERO + grad = CD_ZERO + IF (PRESENT(hess)) hess = CD_ZERO + IF (PRESENT(fold_margin)) fold_margin = CD_ZERO + IF (order < 1 .OR. order > NG_MAX) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Element: quadrature order must lie in [1,6]' + RETURN + END IF + IF (.NOT. (CD_All_Finite(qe) .AND. CD_Is_Finite(L))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Element: non-finite element DOFs or length' + RETURN + END IF + IF (L <= GEOM_TOL) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Element: element length must be positive' + RETURN + END IF + CALL element_holonomy(qe, L, order, PRESENT(hess), h, grad, hk, margin, ErrStat, em) + IF (PRESENT(fold_margin)) fold_margin = margin + IF (ErrStat /= CD_HTORS_OK) THEN + ErrMsg = 'CD_HermiteTorsion_Element: '//em + h = CD_ZERO + grad = CD_ZERO + RETURN + END IF + IF (PRESENT(hess)) hess = hk + END SUBROUTINE CD_HermiteTorsion_Element + + PURE SUBROUTINE CD_HermiteTorsion_Fold_Check(q, Le, ends, margin, ErrStat, ErrMsg, quadrature_order) + !! Fold guard for one line: margin = min(1 + t1 . t) over every Gauss point of every element + !! and min(1 + u_j . u_j+1) over every chain link d_A -> t_0 -> ... -> t_N -> d_B. Fails with + !! CD_HTORS_FOLD (naming the element or link) when margin < CD_HTORS_FOLD_MIN, i.e. when the + !! tangent turns by more than 120 degrees inside one element, between two nodes, or across an + !! articulated end. The holonomy and its derivatives grow without bound as the margin -> 0. + REAL(wp), INTENT(IN) :: q(:), Le(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: margin + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER, INTENT(IN), OPTIONAL :: quadrature_order + INTEGER :: order + REAL(wp), ALLOCATABLE :: u(:, :) + + order = NG_DEFAULT + IF (PRESENT(quadrature_order)) order = quadrature_order + ALLOCATE (u(3, 0:MAX(SIZE(q)/6, 0) + 1)) + CALL line_fold_check(q, Le, ends, order, 'CD_HermiteTorsion_Fold_Check', margin, ErrStat, ErrMsg, u) + END SUBROUTINE CD_HermiteTorsion_Fold_Check + + PURE SUBROUTINE line_fold_check(q, Le, ends, order, who, margin, ErrStat, ErrMsg, u) + !! Validation and fold guard; u(3, 0:nn+1) receives the chain vertices (caller scratch). + REAL(wp), INTENT(IN) :: q(:), Le(:), ends(3, 4) + INTEGER, INTENT(IN) :: order + CHARACTER(*), INTENT(IN) :: who + REAL(wp), INTENT(OUT) :: margin + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(OUT) :: u(:, 0:) + + INTEGER :: nn, e, j, ig + REAL(wp) :: gp(NG_MAX), gw(NG_MAX), t1(3), a(3), b(3), rho, sp, val + CHARACTER(16) :: tag + + margin = CD_ZERO + u = CD_ZERO + CALL validate_line(q, Le, ends, order, who, ErrStat, ErrMsg) + IF (ErrStat /= CD_HTORS_OK) RETURN + nn = SIZE(q)/6 + CALL chain_vertices(q, ends, u) + CALL CD_HermiteCable_Gauss_Rule(order, gp, gw) + margin = HUGE(CD_ONE) + DO e = 1, nn - 1 + rho = NORM2(q(6*e - 2:6*e)) + t1 = q(6*e - 2:6*e)/rho + DO ig = 1, order + CALL element_ab(q(6*e - 5:6*e + 6), Le(e), gp(ig), a, b) + sp = NORM2(a) + IF (sp <= GEOM_TOL) THEN + ErrStat = CD_HTORS_BADINPUT + WRITE (tag, '(I0)') e + ErrMsg = who//': zero centreline tangent in element '//TRIM(tag) + RETURN + END IF + val = CD_ONE + DOT_PRODUCT(t1, a)/sp + IF (val < margin) margin = val + IF (val < CD_HTORS_FOLD_MIN) THEN + ErrStat = CD_HTORS_FOLD + WRITE (tag, '(I0)') e + ErrMsg = who//': tangent turns by more than 120 degrees inside element ' & + //TRIM(tag)//' (1 + t1.t < 0.5); refine the mesh' + RETURN + END IF + END DO + END DO + DO j = 0, nn + val = CD_ONE + DOT_PRODUCT(u(:, j), u(:, j + 1)) + IF (val < margin) margin = val + IF (val < CD_HTORS_FOLD_MIN) THEN + ErrStat = CD_HTORS_FOLD + IF (j == 0) THEN + ErrMsg = who//': End-A director and first node tangent differ by more than ' & + //'120 degrees (1 + d.t < 0.5)' + ELSE IF (j == nn) THEN + ErrMsg = who//': last node tangent and End-B director differ by more than ' & + //'120 degrees (1 + t.d < 0.5)' + ELSE + WRITE (tag, '(I0)') j + ErrMsg = who//': node tangents turn by more than 120 degrees across element ' & + //TRIM(tag)//' (1 + t1.t2 < 0.5); refine the mesh' + END IF + RETURN + END IF + END DO + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE line_fold_check + + PURE SUBROUTINE CD_HermiteTorsion_Work_Init(work, nn, ErrStat, ErrMsg) + !! Size the scratch of CD_HermiteTorsion_Line for a line of nn >= 2 nodes. + TYPE(CD_HermiteTorsionWorkType), INTENT(INOUT) :: work + INTEGER, INTENT(IN) :: nn + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + CALL CD_HermiteTorsion_Work_End(work) + IF (nn < 2) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Work_Init: a line needs at least two nodes' + RETURN + END IF + ALLOCATE (work%u(3, 0:nn + 1), work%pole(3, 0:nn), work%gu(3, 0:nn + 1), work%huu(3, 3, 0:nn + 1), & + work%huv(3, 3, 0:nn), work%band(2*CD_HTORS_KBAND + 1, 6*nn)) + work%nn = nn + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE CD_HermiteTorsion_Work_Init + + PURE SUBROUTINE CD_HermiteTorsion_Work_End(work) + !! Release the scratch of CD_HermiteTorsion_Work_Init. + TYPE(CD_HermiteTorsionWorkType), INTENT(INOUT) :: work + IF (ALLOCATED(work%u)) DEALLOCATE (work%u) + IF (ALLOCATED(work%pole)) DEALLOCATE (work%pole) + IF (ALLOCATED(work%gu)) DEALLOCATE (work%gu) + IF (ALLOCATED(work%huu)) DEALLOCATE (work%huu) + IF (ALLOCATED(work%huv)) DEALLOCATE (work%huv) + IF (ALLOCATED(work%band)) DEALLOCATE (work%band) + work%nn = 0 + work%hess_valid = .FALSE. + END SUBROUTINE CD_HermiteTorsion_Work_End + + PURE SUBROUTINE CD_HermiteTorsion_Work_Add_Hessian(work, hband, scale, ErrStat, ErrMsg) + !! Add scale * d2Theta/dq2, kept in `work` by the last CD_HermiteTorsion_Line call with + !! keep_hessian, to the general band hband (layout as for CD_HermiteTorsion_Line's hband). + !! This lets a caller scale the Hessian by a factor that depends on Theta (the torque). + !! Fails (hband unchanged) when the last call kept no Hessian or failed, or on a size mismatch. + TYPE(CD_HermiteTorsionWorkType), INTENT(IN) :: work + REAL(wp), INTENT(INOUT) :: hband(:, :) + REAL(wp), INTENT(IN) :: scale + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: n, i, jj, i0, row0 + n = 6*work%nn + ErrStat = CD_HTORS_BADINPUT + IF (.NOT. work%hess_valid) THEN + ErrMsg = 'CD_HermiteTorsion_Work_Add_Hessian: the workspace holds no Hessian of a successful evaluation' + RETURN + END IF + IF (n < 12 .OR. SIZE(hband, 2) /= n .OR. SIZE(hband, 1) < 2*CD_HTORS_KBAND + 1) THEN + ErrMsg = 'CD_HermiteTorsion_Work_Add_Hessian: hband does not match the workspace' + RETURN + END IF + ErrStat = CD_HTORS_OK + ErrMsg = '' + row0 = CD_HTORS_KBAND + 1 + i0 = SIZE(hband, 1) - 2*CD_HTORS_KBAND - 1 + DO jj = 1, n + DO i = MAX(1, jj - CD_HTORS_KBAND), MIN(n, jj + CD_HTORS_KBAND) + hband(i0 + row0 + i - jj, jj) = hband(i0 + row0 + i - jj, jj) + scale*work%band(row0 + i - jj, jj) + END DO + END DO + END SUBROUTINE CD_HermiteTorsion_Work_Add_Hessian + + PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, hband, band_scale, & + end_grad, end_hess, end_cross, fold_margin, quadrature_order, work, & + keep_hessian) + !! Theta, dTheta/dq and d2Theta/dq2 of one line. + !! + !! q(6 n) nodal DOFs [r_k, m_k], k = 1..n (n >= 2), as in the cable solvers + !! Le(n - 1) element reference lengths + !! ends(3, 4) columns d_A, n_A, d_B, n_B: unit end directors and unit reference + !! normals (n perpendicular to d). A clamped end has d = its end tangent. + !! theta_raw sum of the element holonomies plus the chain angle; only the chain angle + !! is wrapped to (-pi, pi], the holonomy sum is unbounded. Theta is defined + !! modulo 2 pi -- pass it through CD_HermiteTorsion_Accept + !! grad(6 n) dTheta/dq + !! hband optional, INOUT: band_scale * d2Theta/dq2 is ADDED in general-band + !! storage with half-bandwidth CD_HTORS_KBAND: entry (i, j) at + !! hband(SIZE(hband, 1) - KBAND + i - j, j). This is the LAPACK DGBSV + !! layout for SIZE(hband, 1) = 3 KBAND + 1 and the DGBMV layout for + !! 2 KBAND + 1. band_scale defaults to 1. + !! end_grad(6) dTheta/d[w_A, w_B], w the rotation vector of an incremental rotation + !! exp(w) of the end frame (d and n rotate together), at w = 0 + !! end_hess(3,3,2) d2Theta/dw_A2 and d2Theta/dw_B2 (same parametrisation) + !! end_cross(3,3,2) d2Theta/dw_A dm_1 and d2Theta/dw_B dm_n (rows w, columns m). Theta has + !! no other w coupling (no w_A-w_B, no w-r). + !! fold_margin min(1 + t1 . t) over Gauss points and chain links + !! work optional scratch sized for SIZE(q)/6 nodes (CD_HermiteTorsion_Work_Init): + !! the evaluation then allocates nothing + !! keep_hessian with work: compute d2Theta/dq2 into work (unscaled) for a later + !! CD_HermiteTorsion_Work_Add_Hessian, also without hband + !! + !! On any failure every output is zero and hband is left unchanged. + REAL(wp), INTENT(IN) :: q(:), Le(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: theta_raw, grad(:) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(INOUT), OPTIONAL :: hband(:, :) + REAL(wp), INTENT(IN), OPTIONAL :: band_scale + REAL(wp), INTENT(OUT), OPTIONAL :: end_grad(6), end_hess(3, 3, 2), end_cross(3, 3, 2) + REAL(wp), INTENT(OUT), OPTIONAL :: fold_margin + INTEGER, INTENT(IN), OPTIONAL :: quadrature_order + TYPE(CD_HermiteTorsionWorkType), INTENT(INOUT), OPTIONAL :: work + LOGICAL, INTENT(IN), OPTIONAL :: keep_hessian + TYPE(CD_HermiteTorsionWorkType) :: local + INTEGER :: nn + LOGICAL :: keep + + keep = .FALSE. + IF (PRESENT(keep_hessian)) keep = keep_hessian + IF (PRESENT(work)) THEN + IF (work%nn /= SIZE(q)/6 .OR. work%nn < 2) THEN + theta_raw = CD_ZERO + grad = CD_ZERO + IF (PRESENT(end_grad)) end_grad = CD_ZERO + IF (PRESENT(end_hess)) end_hess = CD_ZERO + IF (PRESENT(end_cross)) end_cross = CD_ZERO + IF (PRESENT(fold_margin)) fold_margin = CD_ZERO + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Line: the workspace is not sized for this line' + work%hess_valid = .FALSE. + RETURN + END IF + work%hess_valid = .FALSE. + CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, work%nn, work%u, work%pole, work%gu, work%huu, & + work%huv, work%band, keep, hband, band_scale, end_grad, end_hess, end_cross, fold_margin, & + quadrature_order) + work%hess_valid = keep .AND. ErrStat == CD_HTORS_OK + ELSE + nn = MAX(SIZE(q)/6, 2) + ALLOCATE (local%u(3, 0:nn + 1), local%pole(3, 0:nn), local%gu(3, 0:nn + 1), local%huu(3, 3, 0:nn + 1), & + local%huv(3, 3, 0:nn), local%band(2*CD_HTORS_KBAND + 1, 6*nn)) + CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, nn, local%u, local%pole, local%gu, local%huu, & + local%huv, local%band, .FALSE., hband, band_scale, end_grad, end_hess, end_cross, & + fold_margin, quadrature_order) + END IF + END SUBROUTINE CD_HermiteTorsion_Line + + PURE SUBROUTINE line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, nw, u, pole, gu, huu, huv, band, keep, & + hband, band_scale, end_grad, end_hess, end_cross, fold_margin, quadrature_order) + !! CD_HermiteTorsion_Line on caller scratch for nw nodes (the scratch is explicit-shape so + !! that no expression on it needs a heap temporary); keep: compute the Hessian into band + !! even without hband. A q of another node count fails validation before any use. + REAL(wp), INTENT(IN) :: q(:), Le(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: theta_raw, grad(:) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER, INTENT(IN) :: nw + REAL(wp), INTENT(INOUT) :: u(3, 0:nw + 1), pole(3, 0:nw), gu(3, 0:nw + 1), huu(3, 3, 0:nw + 1), huv(3, 3, 0:nw) + REAL(wp), INTENT(INOUT) :: band(2*CD_HTORS_KBAND + 1, 6*nw) + LOGICAL, INTENT(IN) :: keep + REAL(wp), INTENT(INOUT), OPTIONAL :: hband(:, :) + REAL(wp), INTENT(IN), OPTIONAL :: band_scale + REAL(wp), INTENT(OUT), OPTIONAL :: end_grad(6), end_hess(3, 3, 2), end_cross(3, 3, 2) + REAL(wp), INTENT(OUT), OPTIONAL :: fold_margin + INTEGER, INTENT(IN), OPTIONAL :: quadrature_order + + INTEGER :: order, nn, n, e, j, k, i0, i, jj, row0 + LOGICAL :: want_hess, finite + REAL(wp) :: scale, margin, he, ge(12), hk(12, 12), v(3), chi, sum_h, eg(6), eh(3, 3, 2), ec(3, 3, 2) + REAL(wp) :: om, g9(9), h9(9, 9), pj(3, 3), pk(3, 3), sd(3, 3), dmy + CHARACTER(200) :: em + + theta_raw = CD_ZERO + grad = CD_ZERO + IF (PRESENT(end_grad)) end_grad = CD_ZERO + IF (PRESENT(end_hess)) end_hess = CD_ZERO + IF (PRESENT(end_cross)) end_cross = CD_ZERO + IF (PRESENT(fold_margin)) fold_margin = CD_ZERO + order = NG_DEFAULT + IF (PRESENT(quadrature_order)) order = quadrature_order + scale = CD_ONE + IF (PRESENT(band_scale)) scale = band_scale + want_hess = PRESENT(hband) .OR. PRESENT(end_hess) .OR. PRESENT(end_cross) .OR. keep + + CALL line_fold_check(q, Le, ends, order, 'CD_HermiteTorsion_Line', margin, ErrStat, ErrMsg, u) + IF (ErrStat /= CD_HTORS_OK) RETURN + n = SIZE(q) + nn = n/6 + IF (SIZE(grad) /= n) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Line: grad must have SIZE(q) entries' + RETURN + END IF + IF (PRESENT(hband)) THEN + IF (SIZE(hband, 1) < 2*CD_HTORS_KBAND + 1 .OR. SIZE(hband, 2) /= n) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Line: hband must be at least (2*KBAND+1, SIZE(q))' + RETURN + END IF + END IF + IF (.NOT. CD_Is_Finite(scale)) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Line: non-finite band_scale' + RETURN + END IF + + row0 = CD_HTORS_KBAND + 1 ! diagonal row of the local band + IF (want_hess) band(:, 1:n) = CD_ZERO + CALL chain_vertices(q, ends, u) + + ! element holonomies + sum_h = CD_ZERO + DO e = 1, nn - 1 + i0 = 6*(e - 1) + CALL element_holonomy(q(i0 + 1:i0 + 12), Le(e), order, want_hess, he, ge, hk, dmy, ErrStat, em) + IF (ErrStat /= CD_HTORS_OK) THEN + ErrMsg = 'CD_HermiteTorsion_Line: '//em + grad = CD_ZERO + RETURN + END IF + sum_h = sum_h + he + grad(i0 + 1:i0 + 12) = grad(i0 + 1:i0 + 12) + ge + IF (want_hess) THEN + DO jj = 1, 12 + DO i = 1, 12 + band(row0 + i - jj, i0 + jj) = band(row0 + i - jj, i0 + jj) + hk(i, jj) + END DO + END DO + END IF + END DO + + ! smallest-rotation chain: value by sequential composition + v = ends(:, 2) + DO j = 0, nn + v = smallest_rotation(u(:, j), u(:, j + 1), v) + END DO + chi = ATAN2(DOT_PRODUCT(ends(:, 3), cross(ends(:, 4), v)), DOT_PRODUCT(ends(:, 4), v)) + theta_raw = sum_h + chi + + ! chain derivatives: local solid-angle terms with frozen poles, w.r.t. the raw unit vertices + gu = CD_ZERO + huu = CD_ZERO + huv = CD_ZERO + DO j = 0, nn + pole(:, j) = u(:, j) + u(:, j + 1) + pole(:, j) = pole(:, j)/NORM2(pole(:, j)) + END DO + DO j = 0, nn ! links: + Omega(p_j, u_j, u_j+1) + CALL solid_angle(pole(:, j), u(:, j), u(:, j + 1), want_hess, om, g9, h9) + gu(:, j) = gu(:, j) + g9(4:6) + gu(:, j + 1) = gu(:, j + 1) + g9(7:9) + IF (want_hess) THEN + huu(:, :, j) = huu(:, :, j) + h9(4:6, 4:6) + huu(:, :, j + 1) = huu(:, :, j + 1) + h9(7:9, 7:9) + huv(:, :, j) = huv(:, :, j) + h9(4:6, 7:9) + END IF + END DO + DO j = 1, nn ! interior vertices: - Omega(p_j-1, p_j, u_j) + CALL solid_angle(pole(:, j - 1), pole(:, j), u(:, j), want_hess, om, g9, h9) + gu(:, j) = gu(:, j) - g9(7:9) + IF (want_hess) huu(:, :, j) = huu(:, :, j) - h9(7:9, 7:9) + END DO + ! end vertices: gauge change to a pole at the frozen end director, plus the end-frame twist + CALL solid_angle(ends(:, 1), pole(:, 0), u(:, 0), want_hess, om, g9, h9) + gu(:, 0) = gu(:, 0) - g9(7:9) + IF (want_hess) huu(:, :, 0) = huu(:, :, 0) - h9(7:9, 7:9) + CALL solid_angle(pole(:, nn), ends(:, 3), u(:, nn + 1), want_hess, om, g9, h9) + gu(:, nn + 1) = gu(:, nn + 1) - g9(7:9) + IF (want_hess) huu(:, :, nn + 1) = huu(:, :, nn + 1) - h9(7:9, 7:9) + + ! chain to the node tangent handles m_k (u = m/|m|) + DO k = 1, nn + i0 = 6*(k - 1) + 3 + CALL normalisation_jacobian(q(i0 + 1:i0 + 3), pj) + grad(i0 + 1:i0 + 3) = grad(i0 + 1:i0 + 3) + MATMUL(pj, gu(:, k)) + IF (want_hess) THEN + sd = normalised_hessian(q(i0 + 1:i0 + 3), gu(:, k), huu(:, :, k)) + CALL add_block(band, row0, i0, i0, sd) + IF (k < nn) THEN + CALL normalisation_jacobian(q(i0 + 7:i0 + 9), pk) + sd = MATMUL(pj, MATMUL(huv(:, :, k), pk)) + CALL add_block(band, row0, i0, i0 + 6, sd) + sd = TRANSPOSE(sd) + CALL add_block(band, row0, i0 + 6, i0, sd) + END IF + END IF + END DO + + ! chain to the end-frame rotations: d(w) = exp(w) d, dd/dw = -S(d), second order 1/2 w x (w x d) + eg = CD_ZERO + eh = CD_ZERO + ec = CD_ZERO + eg(1:3) = cross(ends(:, 1), gu(:, 0)) + ends(:, 1) + eg(4:6) = cross(ends(:, 3), gu(:, nn + 1)) - ends(:, 3) + IF (want_hess) THEN + eh(:, :, 1) = rotation_hessian(ends(:, 1), gu(:, 0), huu(:, :, 0)) + eh(:, :, 2) = rotation_hessian(ends(:, 3), gu(:, nn + 1), huu(:, :, nn + 1)) + CALL normalisation_jacobian(q(4:6), pj) + sd = skew(ends(:, 1)) + ec(:, :, 1) = MATMUL(sd, MATMUL(huv(:, :, 0), pj)) + CALL normalisation_jacobian(q(n - 2:n), pk) + sd = skew(ends(:, 3)) + ec(:, :, 2) = MATMUL(sd, MATMUL(TRANSPOSE(huv(:, :, nn)), pk)) + END IF + + finite = CD_Is_Finite(theta_raw) .AND. CD_All_Finite(grad) .AND. CD_All_Finite(eg) + IF (want_hess) finite = finite .AND. CD_All_Finite(band(:, 1:n)) .AND. CD_All_Finite(RESHAPE(eh, [18])) & + .AND. CD_All_Finite(RESHAPE(ec, [18])) + IF (.NOT. finite) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Line: non-finite twist or derivative' + theta_raw = CD_ZERO + grad = CD_ZERO + RETURN + END IF + + IF (PRESENT(hband)) THEN + i0 = SIZE(hband, 1) - 2*CD_HTORS_KBAND - 1 + DO jj = 1, n + DO i = MAX(1, jj - CD_HTORS_KBAND), MIN(n, jj + CD_HTORS_KBAND) + hband(i0 + row0 + i - jj, jj) = hband(i0 + row0 + i - jj, jj) + scale*band(row0 + i - jj, jj) + END DO + END DO + END IF + IF (PRESENT(end_grad)) end_grad = eg + IF (PRESENT(end_hess)) end_hess = eh + IF (PRESENT(end_cross)) end_cross = ec + IF (PRESENT(fold_margin)) fold_margin = margin + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE line_core + + ELEMENTAL REAL(wp) FUNCTION CD_HermiteTorsion_Unwrap(theta_raw, theta_prev) RESULT(theta) + !! The 2 pi branch of theta_raw nearest to theta_prev: theta_raw + 2 pi round((prev - raw)/2 pi). + !! Non-finite input returns theta_raw unchanged (the caller's finiteness check rejects it). + REAL(wp), INTENT(IN) :: theta_raw, theta_prev + theta = theta_raw + IF (CD_Is_Finite(theta_raw) .AND. CD_Is_Finite(theta_prev)) & + theta = theta_raw + TWO_PI*ANINT((theta_prev - theta_raw)/TWO_PI) + END FUNCTION CD_HermiteTorsion_Unwrap + + PURE SUBROUTINE CD_HermiteTorsion_Accept(theta_prev, theta_raw, theta_new, ErrStat, ErrMsg, max_step) + !! Unwrap a trial value against the previous accepted Theta and enforce the step limit + !! |theta_new - theta_prev| <= max_step (default pi/2, a margin below the hard limit pi at + !! which the nearest branch becomes ambiguous). On rejection theta_new = theta_prev and + !! ErrStat = CD_HTORS_STEP: the caller must cut the step. Call it after every accepted + !! Newton iterate and every accepted time step or load stage. It sees only the wrapped trial + !! value, so it cannot tell a change of 3 pi/2 from one of -pi/2: callers also bound the + !! linearised change dTheta/dq . dq of every trial step before taking it (to pi/4 in the + !! static and dynamic Newton steps and the static descent perturbation). + REAL(wp), INTENT(IN) :: theta_prev, theta_raw + REAL(wp), INTENT(OUT) :: theta_new + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(IN), OPTIONAL :: max_step + REAL(wp) :: limit + + limit = CD_HTORS_MAX_STEP + IF (PRESENT(max_step)) limit = max_step + theta_new = theta_prev + IF (.NOT. (CD_Is_Finite(theta_prev) .AND. CD_Is_Finite(theta_raw) .AND. CD_Is_Finite(limit))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Accept: non-finite twist value or step limit' + RETURN + END IF + IF (limit <= CD_ZERO .OR. limit >= CD_HTORS_PI) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Accept: step limit must lie in (0, pi)' + RETURN + END IF + IF (ABS(CD_HermiteTorsion_Unwrap(theta_raw, theta_prev) - theta_prev) > limit) THEN + ErrStat = CD_HTORS_STEP + ErrMsg = 'CD_HermiteTorsion_Accept: twist change exceeds the step limit; cut the step' + RETURN + END IF + theta_new = CD_HermiteTorsion_Unwrap(theta_raw, theta_prev) + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE CD_HermiteTorsion_Accept + + ! ------------------------------------------------------------------------------------------ + ! element holonomy + ! ------------------------------------------------------------------------------------------ + + PURE SUBROUTINE element_ab(qe, L, xi, a, b) + !! a = dr/ds and b = d2r/ds2 of the cubic-Hermite element at xi in [0, 1]. + REAL(wp), INTENT(IN) :: qe(12), L, xi + REAL(wp), INTENT(OUT) :: a(3), b(3) + REAL(wp) :: dh(4), ddh(4) + INTEGER :: i + CALL shape_derivatives(L, xi, dh, ddh) + a = CD_ZERO + b = CD_ZERO + DO i = 1, 4 + a = a + dh(i)*qe(3*i - 2:3*i) + b = b + ddh(i)*qe(3*i - 2:3*i) + END DO + END SUBROUTINE element_ab + + PURE SUBROUTINE shape_derivatives(L, x, dh, ddh) + !! d/ds and d2/ds2 of the Hermite shape functions [H1, L H2, H3, L H4] at xi = x. + REAL(wp), INTENT(IN) :: L, x + REAL(wp), INTENT(OUT) :: dh(4), ddh(4) + dh = [-6.0_wp*x + 6.0_wp*x*x, L*(CD_ONE - 4.0_wp*x + 3.0_wp*x*x), 6.0_wp*x - 6.0_wp*x*x, & + L*(-2.0_wp*x + 3.0_wp*x*x)]/L + ddh = [-6.0_wp + 12.0_wp*x, L*(-4.0_wp + 6.0_wp*x), 6.0_wp - 12.0_wp*x, L*(-2.0_wp + 6.0_wp*x)]/(L*L) + END SUBROUTINE shape_derivatives + + PURE SUBROUTINE element_holonomy(qe, L, order, want_hess, h, grad, hess, margin, ErrStat, ErrMsg) + !! h_e = L sum_g w_g f(a_g, b_g, t1), f = t1.(a x b) / (|a|**2 + |a| (t1.a)), t1 = m1/|m1|. + !! f = N/D is differentiated in closed form in x = (a, b, t1) and chained to the 12 DOFs by + !! the constant maps a, b = sum_i dh_i q_i, sum_i ddh_i q_i and by t1 = m1/|m1|. + REAL(wp), INTENT(IN) :: qe(12), L + INTEGER, INTENT(IN) :: order + LOGICAL, INTENT(IN) :: want_hess + REAL(wp), INTENT(OUT) :: h, grad(12), hess(12, 12), margin + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + + REAL(wp) :: gp(NG_MAX), gw(NG_MAX), dh(4), ddh(4), m1(3), rho, t(3), a(3), b(3), s2, sp, ta + REAL(wp) :: num, den, f, gn(9), gd(9), hn(9, 9), hd(9, 9), gf(9), hf(9, 9), w, eye(3, 3) + REAL(wp) :: gt(3), htt(3, 3), xat(3, 3, 4), pm(3, 3), blk(3, 3) + INTEGER :: ig, i, j + + h = CD_ZERO + grad = CD_ZERO + hess = CD_ZERO + margin = CD_ZERO + m1 = qe(4:6) + rho = NORM2(m1) + IF (rho <= GEOM_TOL) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'element holonomy: zero tangent handle at the first node' + RETURN + END IF + t = m1/rho + eye = identity3() + gt = CD_ZERO + htt = CD_ZERO + xat = CD_ZERO + margin = HUGE(CD_ONE) + CALL CD_HermiteCable_Gauss_Rule(order, gp, gw) + DO ig = 1, order + CALL shape_derivatives(L, gp(ig), dh, ddh) + CALL element_ab(qe, L, gp(ig), a, b) + s2 = DOT_PRODUCT(a, a) + sp = SQRT(s2) + IF (sp <= GEOM_TOL) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'element holonomy: zero centreline tangent at a Gauss point' + RETURN + END IF + ta = DOT_PRODUCT(t, a) + margin = MIN(margin, CD_ONE + ta/sp) + IF (CD_ONE + ta/sp < CD_HTORS_FOLD_MIN) THEN + ErrStat = CD_HTORS_FOLD + ErrMsg = 'element holonomy: tangent turns by more than 120 degrees inside the element (1 + t1.t < 0.5)' + RETURN + END IF + num = DOT_PRODUCT(t, cross(a, b)) + den = s2 + sp*ta + f = num/den + w = L*gw(ig) + h = h + w*f + gn(1:3) = cross(b, t) + gn(4:6) = cross(t, a) + gn(7:9) = cross(a, b) + gd(1:3) = 2.0_wp*a + (ta/sp)*a + sp*t + gd(4:6) = CD_ZERO + gd(7:9) = sp*a + gf = (gn - f*gd)/den + DO i = 1, 4 + grad(3*i - 2:3*i) = grad(3*i - 2:3*i) + w*(dh(i)*gf(1:3) + ddh(i)*gf(4:6)) + END DO + gt = gt + w*gf(7:9) + IF (.NOT. want_hess) CYCLE + hn = CD_ZERO + hn(1:3, 4:6) = -skew(t) + hn(4:6, 1:3) = skew(t) + hn(4:6, 7:9) = -skew(a) + hn(7:9, 4:6) = skew(a) + hn(7:9, 1:3) = -skew(b) + hn(1:3, 7:9) = skew(b) + hd = CD_ZERO + hd(1:3, 1:3) = 2.0_wp*eye + ta*(eye/sp - outer(a, a)/(sp*s2)) + (outer(a, t) + outer(t, a))/sp + hd(1:3, 7:9) = outer(a, a)/sp + sp*eye + hd(7:9, 1:3) = TRANSPOSE(hd(1:3, 7:9)) + hf = (hn - f*hd - outer9(gf, gd) - outer9(gd, gf))/den + DO j = 1, 4 + DO i = 1, 4 + blk = dh(i)*dh(j)*hf(1:3, 1:3) + dh(i)*ddh(j)*hf(1:3, 4:6) + ddh(i)*dh(j)*hf(4:6, 1:3) & + + ddh(i)*ddh(j)*hf(4:6, 4:6) + hess(3*i - 2:3*i, 3*j - 2:3*j) = hess(3*i - 2:3*i, 3*j - 2:3*j) + w*blk + END DO + xat(:, :, j) = xat(:, :, j) + w*(dh(j)*hf(1:3, 7:9) + ddh(j)*hf(4:6, 7:9)) + END DO + htt = htt + w*hf(7:9, 7:9) + END DO + ! chain through t1 = m1/|m1| (DOF block 2) + CALL normalisation_jacobian(m1, pm) + grad(4:6) = grad(4:6) + MATMUL(pm, gt) + IF (want_hess) THEN + DO i = 1, 4 + blk = MATMUL(xat(:, :, i), pm) + hess(3*i - 2:3*i, 4:6) = hess(3*i - 2:3*i, 4:6) + blk + hess(4:6, 3*i - 2:3*i) = hess(4:6, 3*i - 2:3*i) + TRANSPOSE(blk) + END DO + hess(4:6, 4:6) = hess(4:6, 4:6) + normalised_hessian(m1, gt, htt) + END IF + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE element_holonomy + + ! ------------------------------------------------------------------------------------------ + ! chain helpers + ! ------------------------------------------------------------------------------------------ + + PURE SUBROUTINE validate_line(q, Le, ends, order, who, ErrStat, ErrMsg) + REAL(wp), INTENT(IN) :: q(:), Le(:), ends(3, 4) + INTEGER, INTENT(IN) :: order + CHARACTER(*), INTENT(IN) :: who + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: nn, k + + ErrStat = CD_HTORS_BADINPUT + IF (order < 1 .OR. order > NG_MAX) THEN + ErrMsg = who//': quadrature order must lie in [1,6]' + RETURN + END IF + nn = SIZE(q)/6 + IF (MOD(SIZE(q), 6) /= 0 .OR. nn < 2 .OR. SIZE(Le) /= nn - 1) THEN + ErrMsg = who//': q must hold 6 DOFs for each of n >= 2 nodes and Le n - 1 lengths' + RETURN + END IF + IF (.NOT. (CD_All_Finite(q) .AND. CD_All_Finite(Le) .AND. CD_All_Finite(ends))) THEN + ErrStat = CD_HTORS_NONFINITE + ErrMsg = who//': non-finite DOFs, lengths or end frames' + RETURN + END IF + IF (ANY(Le <= GEOM_TOL)) THEN + ErrMsg = who//': element lengths must be positive' + RETURN + END IF + DO k = 1, nn + IF (NORM2(q(6*k - 2:6*k)) <= GEOM_TOL) THEN + ErrMsg = who//': zero tangent handle m at a node' + RETURN + END IF + END DO + DO k = 1, 3, 2 + IF (ABS(NORM2(ends(:, k)) - CD_ONE) > FRAME_TOL .OR. ABS(NORM2(ends(:, k + 1)) - CD_ONE) > FRAME_TOL & + .OR. ABS(DOT_PRODUCT(ends(:, k), ends(:, k + 1))) > FRAME_TOL) THEN + IF (k == 1) THEN + ErrMsg = who//': End-A director and reference normal must be orthonormal' + ELSE + ErrMsg = who//': End-B director and reference normal must be orthonormal' + END IF + RETURN + END IF + END DO + ErrStat = CD_HTORS_OK + ErrMsg = '' + END SUBROUTINE validate_line + + PURE SUBROUTINE chain_vertices(q, ends, u) + !! u(:, 0) = d_A, u(:, k) = m_k/|m_k| (k = 1..n), u(:, n+1) = d_B. + REAL(wp), INTENT(IN) :: q(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: u(:, 0:) + INTEGER :: nn, k + nn = SIZE(q)/6 + u(:, 0) = ends(:, 1) + DO k = 1, nn + u(:, k) = q(6*k - 2:6*k)/NORM2(q(6*k - 2:6*k)) + END DO + u(:, nn + 1) = ends(:, 3) + END SUBROUTINE chain_vertices + + PURE FUNCTION smallest_rotation(a, b, v) RESULT(r) + !! Smallest rotation taking unit a to unit b, applied to v (requires 1 + a.b > 0). + REAL(wp), INTENT(IN) :: a(3), b(3), v(3) + REAL(wp) :: r(3), c, w(3) + c = DOT_PRODUCT(a, b) + w = cross(a, b) + r = c*v + cross(w, v) + w*DOT_PRODUCT(w, v)/(CD_ONE + c) + END FUNCTION smallest_rotation + + PURE SUBROUTINE solid_angle(x, y, z, want_hess, om, g, h) + !! Omega = 2 atan2(N, D), N = x.(y x z), D = 1 + x.y + y.z + z.x (signed solid angle of the + !! geodesic triangle x, y, z), with gradient g and Hessian h in the raw vectors (x, y, z). + !! Under the fold guard every triangle used here has D >= 1.5. + REAL(wp), INTENT(IN) :: x(3), y(3), z(3) + LOGICAL, INTENT(IN) :: want_hess + REAL(wp), INTENT(OUT) :: om, g(9), h(9, 9) + REAL(wp) :: nv, dv, r2, gn(9), gd(9), eye(3, 3) + + nv = DOT_PRODUCT(x, cross(y, z)) + dv = CD_ONE + DOT_PRODUCT(x, y) + DOT_PRODUCT(y, z) + DOT_PRODUCT(z, x) + om = 2.0_wp*ATAN2(nv, dv) + gn(1:3) = cross(y, z) + gn(4:6) = cross(z, x) + gn(7:9) = cross(x, y) + gd(1:3) = y + z + gd(4:6) = x + z + gd(7:9) = x + y + r2 = nv*nv + dv*dv + g = 2.0_wp*(dv*gn - nv*gd)/r2 + h = CD_ZERO + IF (.NOT. want_hess) RETURN + eye = identity3() + ! d2N: (x,y) = -S(z), (y,z) = -S(x), (z,x) = -S(y); d2D: identity off-diagonal blocks + h(1:3, 4:6) = -dv*skew(z) - nv*eye + h(4:6, 1:3) = dv*skew(z) - nv*eye + h(4:6, 7:9) = -dv*skew(x) - nv*eye + h(7:9, 4:6) = dv*skew(x) - nv*eye + h(7:9, 1:3) = -dv*skew(y) - nv*eye + h(1:3, 7:9) = dv*skew(y) - nv*eye + h = 2.0_wp*(h/r2 + ((nv*nv - dv*dv)*(outer9(gn, gd) + outer9(gd, gn)) & + - 2.0_wp*dv*nv*(outer9(gn, gn) - outer9(gd, gd)))/(r2*r2)) + END SUBROUTINE solid_angle + + PURE SUBROUTINE normalisation_jacobian(m, p) + !! d(m/|m|)/dm = (I - t t^T)/|m|. + REAL(wp), INTENT(IN) :: m(3) + REAL(wp), INTENT(OUT) :: p(3, 3) + REAL(wp) :: rho, t(3) + rho = NORM2(m) + t = m/rho + p = (identity3() - outer(t, t))/rho + END SUBROUTINE normalisation_jacobian + + PURE FUNCTION normalised_hessian(m, g, h) RESULT(hm) + !! Hessian in m of F(t), t = m/|m|, from g = dF/dt and h = d2F/dt2: + !! P h P - (g t^T + t g^T + (g.t)(I - 3 t t^T))/|m|**2, P = (I - t t^T)/|m|. + REAL(wp), INTENT(IN) :: m(3), g(3), h(3, 3) + REAL(wp) :: hm(3, 3), p(3, 3), rho, t(3), gt + rho = NORM2(m) + t = m/rho + CALL normalisation_jacobian(m, p) + gt = DOT_PRODUCT(g, t) + hm = MATMUL(p, MATMUL(h, p)) - (outer(g, t) + outer(t, g) + gt*(identity3() - 3.0_wp*outer(t, t)))/(rho*rho) + END FUNCTION normalised_hessian + + PURE FUNCTION rotation_hessian(d, g, h) RESULT(hw) + !! Hessian in w at w = 0 of F(exp(w) d), from g = dF/dd and h = d2F/dd2: + !! -S(d) h S(d) + (g d^T + d g^T)/2 - (g.d) I. + REAL(wp), INTENT(IN) :: d(3), g(3), h(3, 3) + REAL(wp) :: hw(3, 3), sd(3, 3) + sd = skew(d) + hw = -MATMUL(sd, MATMUL(h, sd)) + 0.5_wp*(outer(g, d) + outer(d, g)) - DOT_PRODUCT(g, d)*identity3() + END FUNCTION rotation_hessian + + PURE SUBROUTINE add_block(band, row0, i0, j0, blk) + !! Add a 3x3 block at global rows i0+1..i0+3, columns j0+1..j0+3 (local band, diagonal row0). + REAL(wp), INTENT(INOUT) :: band(:, :) + INTEGER, INTENT(IN) :: row0, i0, j0 + REAL(wp), INTENT(IN) :: blk(3, 3) + INTEGER :: i, j + DO j = 1, 3 + DO i = 1, 3 + band(row0 + i0 + i - j0 - j, j0 + j) = band(row0 + i0 + i - j0 - j, j0 + j) + blk(i, j) + END DO + END DO + END SUBROUTINE add_block + + ! ------------------------------------------------------------------------------------------ + ! small vector helpers + ! ------------------------------------------------------------------------------------------ + + PURE FUNCTION cross(a, b) RESULT(c) + REAL(wp), INTENT(IN) :: a(3), b(3) + REAL(wp) :: c(3) + c = [a(2)*b(3) - a(3)*b(2), a(3)*b(1) - a(1)*b(3), a(1)*b(2) - a(2)*b(1)] + END FUNCTION cross + + PURE FUNCTION skew(v) RESULT(s) + !! S(v) w = v x w. + REAL(wp), INTENT(IN) :: v(3) + REAL(wp) :: s(3, 3) + s = RESHAPE([CD_ZERO, v(3), -v(2), -v(3), CD_ZERO, v(1), v(2), -v(1), CD_ZERO], [3, 3]) + END FUNCTION skew + + PURE FUNCTION outer(a, b) RESULT(m) + REAL(wp), INTENT(IN) :: a(3), b(3) + REAL(wp) :: m(3, 3) + m = SPREAD(a, 2, 3)*SPREAD(b, 1, 3) + END FUNCTION outer + + PURE FUNCTION outer9(a, b) RESULT(m) + REAL(wp), INTENT(IN) :: a(9), b(9) + REAL(wp) :: m(9, 9) + m = SPREAD(a, 2, 9)*SPREAD(b, 1, 9) + END FUNCTION outer9 + + PURE FUNCTION identity3() RESULT(m) + REAL(wp) :: m(3, 3) + m = RESHAPE([CD_ONE, CD_ZERO, CD_ZERO, CD_ZERO, CD_ONE, CD_ZERO, CD_ZERO, CD_ZERO, CD_ONE], [3, 3]) + END FUNCTION identity3 + +END MODULE CableDyn_HermiteTorsion diff --git a/src/CableDyn_OpenFAST_HermiteFMF.f90 b/src/CableDyn_OpenFAST_HermiteFMF.f90 index e49509a..b7b9d00 100644 --- a/src/CableDyn_OpenFAST_HermiteFMF.f90 +++ b/src/CableDyn_OpenFAST_HermiteFMF.f90 @@ -39,7 +39,11 @@ MODULE CableDyn_OpenFAST_HermiteFMF CD_HermiteCable_Dyn_EndConnection_Moment, & CD_HermiteCable_Dyn_Curvature, & CD_HermiteCable_Dyn_Snapshot, CD_HermiteCable_Dyn_Restore, & - CD_HermiteCable_Dyn_End, CD_HCDYN_OK + CD_HermiteCable_Dyn_End, CD_HCDYN_OK, & + CD_HermiteCable_Dyn_Set_Torsion, CD_HermiteCable_Dyn_Set_Torsion_Drive, & + CD_HermiteCable_Dyn_Set_Torsion_Frames + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Compliance, CD_HermiteTorsion_Line, & + CD_HermiteTorsion_Unwrap, CD_HTORS_OK, CD_HTORS_MAX_STEP USE CableDyn_HermiteCable, ONLY: CD_HermiteCable_Shapes USE CableDyn_EndConnection, ONLY: CD_EndConn_Project, CD_ENDCONN_OK, CD_ENDCONN_BADINPUT, & CD_ENDCONN_PINNED, CD_ENDCONN_FINITE, CD_ENDCONN_RIGID @@ -63,6 +67,7 @@ MODULE CableDyn_OpenFAST_HermiteFMF PUBLIC :: CD_HFMF_Set_Contact PUBLIC :: CD_HFMF_Set_Attachments PUBLIC :: CD_HFMF_Set_EndConnection + PUBLIC :: CD_HFMF_Set_Torsion PUBLIC :: CD_HFMF_Refresh_Acceleration PUBLIC :: CD_HFMF_NNodes PUBLIC :: CD_HFMF_GetNodePositions @@ -120,6 +125,13 @@ MODULE CableDyn_OpenFAST_HermiteFMF CD_ZERO, CD_ZERO, CD_ONE], [3, 3]) REAL(wp) :: snap_parent_omega(3) = CD_ZERO REAL(wp) :: snap_parent_alpha(3) = CD_ZERO + ! Condensed torsion with a parent-attached coupled end (CD_HFMF_Set_Torsion with + ! coupled_frame_parent): that end's torsion frame (director, reference normal) in parent + ! axes, turned with the parent orientation at every step, also at a bending-pinned end. + ! tors_index is the coupled end (1 or 2). + LOGICAL :: tors_parent = .FALSE. + INTEGER :: tors_index = 0 + REAL(wp) :: tors_frame_parent(3, 2) = CD_ZERO LOGICAL :: initialized = .FALSE. END TYPE CD_HFMF_ModuleType @@ -381,6 +393,95 @@ SUBROUTINE CD_HFMF_Set_EndConnection(self, k_rot, d0_global, ErrStat, ErrMsg, & END IF END SUBROUTINE CD_HFMF_Set_EndConnection + SUBROUTINE CD_HFMF_Set_Torsion(self, torsion, ErrStat, ErrMsg, coupled_frame_parent, parent_orientation) + !! Install the condensed torsion of the cable's converged static solve (end frames and GJ in + !! the cable solve frame; see CD_HermiteCable_Dyn_Set_Torsion). CalcOutput then returns the + !! end force and connection moment with the torque, and UpdateStates carries the torsion. + !! coupled_frame_parent (columns: director, reference normal of the coupled end's torsion + !! frame in parent axes) with parent_orientation (the global-to-parent DCM at which the static + !! frame was taken) attach that frame to the parent: UpdateStates then turns it with + !! u_orientation, for any bending connection (also a pinned one). The parent orientation must + !! agree with that of a parent-relative end connection, and the frame with the static one. + TYPE(CD_HFMF_ModuleType), INTENT(INOUT) :: self + TYPE(CD_HermiteTorsionType), INTENT(IN) :: torsion + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp), INTENT(IN), OPTIONAL :: coupled_frame_parent(3, 2), parent_orientation(3, 3) + INTEGER :: es, ci + REAL(wp) :: dl(3), nl(3) + CHARACTER(300) :: em + ErrStat = CD_HFMF_OK + ErrMsg = '' + IF (.NOT. self%initialized) THEN + ErrStat = CD_HFMF_BADINPUT + ErrMsg = 'CD_HFMF_Set_Torsion: module not initialised' + RETURN + END IF + ErrStat = CD_HFMF_BADINPUT + IF (PRESENT(coupled_frame_parent) .NEQV. PRESENT(parent_orientation)) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: coupled_frame_parent and parent_orientation must be supplied together' + RETURN + END IF + ci = 0 + IF (self%coupled_node == 1) ci = 1 + IF (self%coupled_node == self%line%nn) ci = 2 + IF (PRESENT(coupled_frame_parent) .AND. torsion%active) THEN + IF (ci == 0) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: a parent torsion frame needs the coupled node at a line end' + RETURN + END IF + IF (.NOT. valid_parent_dcm(parent_orientation)) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: parent_orientation must be a finite proper orthogonal DCM' + RETURN + END IF + IF (.NOT. CD_All_Finite(RESHAPE(coupled_frame_parent, [6]))) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: the parent torsion frame must be finite' + RETURN + END IF + IF (ABS(NORM2(coupled_frame_parent(:, 1)) - CD_ONE) > 1.0e-8_wp .OR. & + ABS(NORM2(coupled_frame_parent(:, 2)) - CD_ONE) > 1.0e-8_wp .OR. & + ABS(DOT_PRODUCT(coupled_frame_parent(:, 1), coupled_frame_parent(:, 2))) > 1.0e-8_wp) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: the parent torsion frame must be orthonormal' + RETURN + END IF + IF (self%has_parent_endconn) THEN + IF (MAXVAL(ABS(parent_orientation - self%parent_dcm)) > 1.0e-10_wp) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: parent_orientation differs from the end connection''s' + RETURN + END IF + END IF + dl = frame_to_local(self, MATMUL(TRANSPOSE(parent_orientation), coupled_frame_parent(:, 1))) + nl = frame_to_local(self, MATMUL(TRANSPOSE(parent_orientation), coupled_frame_parent(:, 2))) + IF (NORM2(dl - torsion%ends(:, 2*ci - 1)) > 1.0e-8_wp .OR. NORM2(nl - torsion%ends(:, 2*ci)) > 1.0e-8_wp) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: the parent torsion frame differs from the static end frame' + RETURN + END IF + END IF + CALL CD_HermiteCable_Dyn_Set_Torsion(self%line, torsion, es, em) + IF (es /= CD_HCDYN_OK) THEN + ErrMsg = 'CD_HFMF_Set_Torsion: '//TRIM(em) + RETURN + END IF + ErrStat = CD_HFMF_OK + ErrMsg = '' + self%tors_parent = .FALSE. + self%tors_index = 0 + self%tors_frame_parent = CD_ZERO + IF (PRESENT(coupled_frame_parent) .AND. torsion%active) THEN + self%tors_parent = .TRUE. + self%tors_index = ci + self%tors_frame_parent = coupled_frame_parent + IF (.NOT. self%has_parent_endconn) THEN + self%parent_dcm = parent_orientation + self%snap_parent_dcm = parent_orientation + self%parent_omega = CD_ZERO + self%parent_alpha = CD_ZERO + self%snap_parent_omega = CD_ZERO + self%snap_parent_alpha = CD_ZERO + END IF + END IF + END SUBROUTINE CD_HFMF_Set_Torsion + SUBROUTINE CD_HFMF_Set_ModifiedNewton(self, enabled, ErrStat, ErrMsg) !! Pass-through to the model's modified-Newton (within-step tangent reuse) switch: !! one effective-tangent assembly + factorization per dynamic step, contraction-gated @@ -532,16 +633,20 @@ SUBROUTINE CD_HFMF_Set_Waves(self, height, period, direction_deg, depth, gravity END SUBROUTINE CD_HFMF_Set_Waves SUBROUTINE CD_HFMF_UpdateStates(self, u_pos, u_vel, u_acc, ErrStat, ErrMsg, u_orientation, & - u_angular_velocity, u_angular_acceleration) + u_angular_velocity, u_angular_acceleration, u_twist) !! Advance the cable one coupling step of dt with the coupled endpoint's kinematics !! at t_{n+1} prescribed to (u_pos, u_vel, u_acc) -- the FMF kinematics-in boundary. + !! With condensed torsion, a parent-attached torsion frame (CD_HFMF_Set_Torsion) turns with + !! the orientation, and u_twist [rad] sets the imposed twist Phi at t_{n+1} (held otherwise). TYPE(CD_HFMF_ModuleType), INTENT(INOUT) :: self REAL(wp), INTENT(IN) :: u_pos(3), u_vel(3), u_acc(3) INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg REAL(wp), INTENT(IN), OPTIONAL :: u_orientation(3, 3) REAL(wp), INTENT(IN), OPTIONAL :: u_angular_velocity(3), u_angular_acceleration(3) + REAL(wp), INTENT(IN), OPTIONAL :: u_twist INTEGER :: es + REAL(wp) :: tors_ends(3, 4), tors_phi REAL(wp) :: target_dcm(3, 3), target_d0(3, 2), target_omega(3), target_alpha(3) REAL(wp) :: target_d0_rate(3, 2), target_d0_acceleration(3, 2), omega_local(3), alpha_local(3) CHARACTER(300) :: em @@ -595,6 +700,28 @@ SUBROUTINE CD_HFMF_UpdateStates(self, u_pos, u_vel, u_acc, ErrStat, ErrMsg, u_or cross3(omega_local, target_d0_rate(:, self%parent_endconn_index)) END IF END IF + IF (PRESENT(u_twist) .AND. .NOT. self%line%torsion%active) THEN + ErrStat = CD_HFMF_BADINPUT + ErrMsg = 'CD_HFMF_UpdateStates: u_twist needs a cable with torsion' + RETURN + END IF + IF (self%line%torsion%active) THEN + ! Condensed torsion target at t_{n+1}: the parent-attached end frame turned with the + ! orientation, and the imposed twist. + tors_ends = self%line%torsion%ends + IF (self%tors_parent) THEN + tors_ends(:, 2*self%tors_index - 1) = frame_to_local(self, MATMUL(TRANSPOSE(target_dcm), & + self%tors_frame_parent(:, 1))) + tors_ends(:, 2*self%tors_index) = frame_to_local(self, MATMUL(TRANSPOSE(target_dcm), & + self%tors_frame_parent(:, 2))) + END IF + tors_phi = self%line%torsion%phi + IF (PRESENT(u_twist)) tors_phi = u_twist + CALL CD_HermiteCable_Dyn_Set_Torsion_Drive(self%line, tors_ends, tors_phi, es, em) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = CD_HFMF_BADINPUT; ErrMsg = 'CD_HFMF_UpdateStates: '//TRIM(em); RETURN + END IF + END IF ! Advance the finite-EI line over the prescribed interval. Internal substeps are used when the ! nominal interval is too large for the boundary-motion or environmental-load increment, while ! preserving the requested endpoint time. @@ -614,7 +741,7 @@ SUBROUTINE CD_HFMF_UpdateStates(self, u_pos, u_vel, u_acc, ErrStat, ErrMsg, u_or END IF IF (es /= CD_HCDYN_OK) THEN ErrStat = CD_HFMF_SOLVEFAIL; ErrMsg = 'CD_HFMF_UpdateStates: '//TRIM(em) - ELSE IF (self%has_parent_endconn) THEN + ELSE IF (self%has_parent_endconn .OR. self%tors_parent) THEN self%parent_dcm = target_dcm self%parent_omega = target_omega self%parent_alpha = target_alpha @@ -685,11 +812,75 @@ SUBROUTINE CD_HFMF_PreflightCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat u_orientation, u_angular_velocity, u_angular_acceleration) IF (ErrStat /= CD_HFMF_OK) THEN ErrMsg = 'CD_HFMF_PreflightCoupledKinematics: '//TRIM(em) - ELSE - ErrMsg = '' + RETURN + END IF + ErrMsg = '' + IF (self%tors_parent) THEN + ! the parent-attached torsion frame at the written state: the same acceptance as the + ! frame update of SetCoupledKinematics (fold guard, twist change within pi/2) + BLOCK + REAL(wp), ALLOCATABLE :: qc(:), g(:) + REAL(wp) :: ends(3, 4), raw, th + INTEGER :: ks, k + CHARACTER(200) :: km + ends = coupled_torsion_ends(self, target_dcm) + DO k = 1, 3, 2 + IF (ABS(NORM2(ends(:, k)) - CD_ONE) > 1.0e-8_wp .OR. ABS(NORM2(ends(:, k + 1)) - CD_ONE) > 1.0e-8_wp & + .OR. ABS(DOT_PRODUCT(ends(:, k), ends(:, k + 1))) > 1.0e-8_wp) THEN + ErrStat = CD_HFMF_SOLVEFAIL + ErrMsg = 'CD_HFMF_PreflightCoupledKinematics: each end director and reference normal must be orthonormal' + RETURN + END IF + END DO + ALLOCATE (qc(SIZE(self%line%q)), g(SIZE(self%line%q))) + CALL coupled_candidate_q(self, u_pos, tangent_q, qc) + CALL CD_HermiteTorsion_Line(qc, self%line%l0, ends, raw, g, ks, km, & + quadrature_order=self%line%torsion%quadrature_order) + IF (ks /= CD_HTORS_OK) THEN + ErrStat = CD_HFMF_SOLVEFAIL + ErrMsg = 'CD_HFMF_PreflightCoupledKinematics: torsion: '//TRIM(km) + RETURN + END IF + th = CD_HermiteTorsion_Unwrap(raw, self%line%torsion%theta) + IF (ABS(th - self%line%torsion%theta) > CD_HTORS_MAX_STEP) THEN + ErrStat = CD_HFMF_SOLVEFAIL + ErrMsg = 'CD_HFMF_PreflightCoupledKinematics: torsion: the twist would move more than pi/2 from its '// & + 'committed value' + END IF + END BLOCK END IF END SUBROUTINE CD_HFMF_PreflightCoupledKinematics + FUNCTION coupled_torsion_ends(self, target_dcm) RESULT(ends) + !! The torsion end frames with the parent-attached end turned to target_dcm (solve frame). + TYPE(CD_HFMF_ModuleType), INTENT(IN) :: self + REAL(wp), INTENT(IN) :: target_dcm(3, 3) + REAL(wp) :: ends(3, 4) + ends = self%line%torsion%ends + ends(:, 2*self%tors_index - 1) = frame_to_local(self, MATMUL(TRANSPOSE(target_dcm), self%tors_frame_parent(:, 1))) + ends(:, 2*self%tors_index) = frame_to_local(self, MATMUL(TRANSPOSE(target_dcm), self%tors_frame_parent(:, 2))) + END FUNCTION coupled_torsion_ends + + SUBROUTINE coupled_candidate_q(self, u_pos, tangent_q, qc) + !! The configuration SetCoupledKinematics writes: the committed q with the coupled node's + !! translation and, for a rigid parent connection, its transported tangent (built the same + !! way, in place, by SetCoupledKinematics). qc must not be part of self. + TYPE(CD_HFMF_ModuleType), INTENT(IN) :: self + REAL(wp), INTENT(IN) :: u_pos(3), tangent_q(3) + REAL(wp), INTENT(OUT) :: qc(:) + INTEGER :: base, i + DO i = 1, SIZE(qc) + qc(i) = self%line%q(i) + END DO + IF (self%has_parent_endconn) THEN + IF (self%line%endconn_mode(self%parent_endconn_index) == CD_ENDCONN_RIGID) THEN + base = MERGE(3, 6*(self%line%nn - 1) + 3, self%parent_endconn_index == 1) + qc(base + 1:base + 3) = tangent_q + END IF + END IF + qc(self%pdof) = frame_to_local(self, u_pos) + END SUBROUTINE coupled_candidate_q + SUBROUTINE CD_HFMF_SetCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat, ErrMsg, u_orientation, & u_angular_velocity, u_angular_acceleration) !! Direct-feedthrough transfer: write ONLY the coupled node's three translational (r, v, a) @@ -708,8 +899,8 @@ SUBROUTINE CD_HFMF_SetCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat, ErrM REAL(wp), INTENT(IN), OPTIONAL :: u_angular_velocity(3), u_angular_acceleration(3) REAL(wp) :: target_dcm(3, 3), d0_target(3, 2) REAL(wp) :: tangent_q(3), tangent_v(3), tangent_a(3) - REAL(wp) :: target_omega(3), target_alpha(3) - INTEGER :: base + REAL(wp) :: target_omega(3), target_alpha(3), tors_ends(3, 4) + INTEGER :: base, es CHARACTER(300) :: em CALL prepare_coupled_kinematics(self, u_pos, u_vel, u_acc, target_dcm, target_omega, target_alpha, & @@ -719,6 +910,28 @@ SUBROUTINE CD_HFMF_SetCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat, ErrM ErrMsg = 'CD_HFMF_SetCoupledKinematics: '//TRIM(em) RETURN END IF + ! A parent-attached torsion frame is checked at the state about to be written before + ! anything is written (the update is all or nothing) + IF (self%tors_parent) THEN + tors_ends = coupled_torsion_ends(self, target_dcm) + ! the candidate configuration in the line's trial scratch (as coupled_candidate_q builds it) + DO base = 1, SIZE(self%line%q) + self%line%ws_qtrial(base) = self%line%q(base) + END DO + IF (self%has_parent_endconn) THEN + IF (self%line%endconn_mode(self%parent_endconn_index) == CD_ENDCONN_RIGID) THEN + base = MERGE(3, 6*(self%line%nn - 1) + 3, self%parent_endconn_index == 1) + self%line%ws_qtrial(base + 1:base + 3) = tangent_q + END IF + END IF + self%line%ws_qtrial(self%pdof) = frame_to_local(self, u_pos) + CALL CD_HermiteCable_Dyn_Set_Torsion_Frames(self%line, tors_ends, es, em, q_check=self%line%ws_qtrial) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = CD_HFMF_SOLVEFAIL + ErrMsg = 'CD_HFMF_SetCoupledKinematics: '//TRIM(em) + RETURN + END IF + END IF IF (self%has_parent_endconn) THEN IF (self%line%endconn_mode(self%parent_endconn_index) == CD_ENDCONN_RIGID) THEN IF (self%parent_endconn_index == 1) THEN @@ -740,6 +953,21 @@ SUBROUTINE CD_HFMF_SetCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat, ErrM self%line%q(self%pdof) = frame_to_local(self, u_pos) self%line%v(self%pdof) = frame_to_local(self, u_vel) self%line%a(self%pdof) = frame_to_local(self, u_acc) + IF (self%tors_parent) THEN + ! the parent-attached torsion frame turns with the written orientation (committed state); + ! checked above at this very state, so it is accepted + CALL CD_HermiteCable_Dyn_Set_Torsion_Frames(self%line, tors_ends, es, em) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = CD_HFMF_SOLVEFAIL + ErrMsg = 'CD_HFMF_SetCoupledKinematics: '//TRIM(em) + RETURN + END IF + IF (.NOT. self%has_parent_endconn) THEN + self%parent_dcm = target_dcm + self%parent_omega = target_omega + self%parent_alpha = target_alpha + END IF + END IF ErrStat = CD_HFMF_OK ErrMsg = '' END SUBROUTINE CD_HFMF_SetCoupledKinematics @@ -1254,9 +1482,18 @@ INTEGER FUNCTION CD_HFMF_MirrorSize(self) RESULT(n) n = 0 IF (.NOT. self%initialized) RETURN IF (ALLOCATED(self%line%freemask)) n = 3*COUNT(self%line%freemask) - n = n + CD_HFMF_FrictionMirrorSize(self) + CD_HFMF_ForceMirrorSize(self) + n = n + CD_HFMF_FrictionMirrorSize(self) + CD_HFMF_ForceMirrorSize(self) + torsion_mirror_size(self) END FUNCTION CD_HFMF_MirrorSize + INTEGER FUNCTION torsion_mirror_size(self) RESULT(n) + !! Mirror slots of the condensed torsion, last: the accepted (unwrapped) Theta, Phi and the + !! four end-frame vectors (14 values); zero without torsion. + TYPE(CD_HFMF_ModuleType), INTENT(IN) :: self + n = 0 + IF (.NOT. self%initialized) RETURN + IF (self%line%torsion%active) n = 14 + END FUNCTION torsion_mirror_size + INTEGER FUNCTION CD_HFMF_ForceMirrorSize(self) RESULT(n) !! Mirror slots of the force-blended generalised-alpha integrator state beyond [v; q; a] of !! the free DOFs: a validity flag and the committed force F_n (ndof values), then the @@ -1326,6 +1563,11 @@ SUBROUTINE CD_HFMF_PackMirror(self, buf, ErrStat, ErrMsg) buf(k) = self%line%a(i) END DO END IF + IF (self%line%torsion%active) THEN + buf(k + 1) = self%line%torsion%theta + buf(k + 2) = self%line%torsion%phi + buf(k + 3:k + 14) = RESHAPE(self%line%torsion%ends, [12]) + END IF END SUBROUTINE CD_HFMF_PackMirror SUBROUTINE CD_HFMF_UnpackMirror(self, buf, t, ErrStat, ErrMsg) @@ -1357,6 +1599,21 @@ SUBROUTINE CD_HFMF_UnpackMirror(self, buf, t, ErrStat, ErrMsg) IF (.NOT. CD_All_Finite(buf)) THEN ErrStat = CD_HFMF_BADINPUT; ErrMsg = 'CD_HFMF_UnpackMirror: mirror must be finite'; RETURN END IF + IF (self%line%torsion%active) THEN + ! the torsion block (last 14 values) is checked before anything is written: each end's + ! director and reference normal must be orthonormal, as Set_Torsion_Drive requires + k = SIZE(buf) - 14 + DO i = 0, 1 + ASSOCIATE (dv => buf(k + 3 + 6*i:k + 5 + 6*i), nv => buf(k + 6 + 6*i:k + 8 + 6*i)) + IF (ABS(NORM2(dv) - CD_ONE) > 1.0e-8_wp .OR. ABS(NORM2(nv) - CD_ONE) > 1.0e-8_wp .OR. & + ABS(DOT_PRODUCT(dv, nv)) > 1.0e-8_wp) THEN + ErrStat = CD_HFMF_BADINPUT + ErrMsg = 'CD_HFMF_UnpackMirror: the torsion end frames must be orthonormal' + RETURN + END IF + END ASSOCIATE + END DO + END IF k = 0 DO i = 1, self%line%ndof IF (self%line%freemask(i)) THEN @@ -1394,6 +1651,30 @@ SUBROUTINE CD_HFMF_UnpackMirror(self, buf, t, ErrStat, ErrMsg) self%line%a(i) = buf(k) END DO END IF + IF (self%line%torsion%active) THEN + self%line%torsion%theta = buf(k + 1) + self%line%torsion%phi = buf(k + 2) + self%line%torsion%ends = RESHAPE(buf(k + 3:k + 14), [3, 4]) + self%line%torsion%torque = (self%line%torsion%phi - self%line%torsion%theta)/ & + CD_HermiteTorsion_Compliance(self%line%torsion, self%line%l0) + self%line%tors_drive_set = .FALSE. + ! A parent-attached torsion frame without a parent end connection: the parent orientation + ! is not in the mirror but follows from the restored end frame (its director and normal + ! in parent axes, tors_frame_parent, and in global axes), so a later step without + ! u_orientation holds it instead of turning the frame back to the static orientation. + IF (self%tors_parent .AND. .NOT. self%has_parent_endconn) THEN + BLOCK + REAL(wp) :: fp(3, 3), gg(3, 3) + fp(:, 1) = self%tors_frame_parent(:, 1) + fp(:, 2) = self%tors_frame_parent(:, 2) + fp(:, 3) = cross3(fp(:, 1), fp(:, 2)) + gg(:, 1) = frame_to_global(self, self%line%torsion%ends(:, 2*self%tors_index - 1)) + gg(:, 2) = frame_to_global(self, self%line%torsion%ends(:, 2*self%tors_index)) + gg(:, 3) = cross3(gg(:, 1), gg(:, 2)) + self%parent_dcm = MATMUL(fp, TRANSPOSE(gg)) + END BLOCK + END IF + END IF END SUBROUTINE CD_HFMF_UnpackMirror SUBROUTINE CD_HFMF_End(self) diff --git a/src/CableDyn_RangeOutput.f90 b/src/CableDyn_RangeOutput.f90 index 41e2aa4..24e3005 100644 --- a/src/CableDyn_RangeOutput.f90 +++ b/src/CableDyn_RangeOutput.f90 @@ -6,7 +6,8 @@ MODULE CableDyn_RangeOutput !! !! A route samples a line at an output row by filling the line's scratch arrays in !! public node order (End A -> End B): positions r(3, nn) in the global frame and the - !! node values of the Ten, Curv, BendMom and LNDec channels. This module then + !! node values of the Ten, Curv, BendMom and LNDec channels (and, on a line with + !! condensed torsion, of TorqN and TwistN). This module then !! adds the seabed clearance, keeps the range envelopes (minimum, maximum and running !! sum per node, allocation-free after setup) and evaluates the touchdown point. !! @@ -35,7 +36,8 @@ MODULE CableDyn_RangeOutput INTEGER, PARAMETER, PUBLIC :: CD_RANGE_OK = 0, CD_RANGE_BADINPUT = 1, CD_RANGE_SOLVEFAIL = 2 ! Node quantities of a sample (rows of CD_RangeLine%val). INTEGER, PARAMETER, PUBLIC :: CD_RQ_TENSION = 1, CD_RQ_CURVATURE = 2, CD_RQ_BEND = 3, & - CD_RQ_DECLINATION = 4, CD_RQ_CLEARANCE = 5, CD_RQ_N = 5 + CD_RQ_DECLINATION = 4, CD_RQ_CLEARANCE = 5, CD_RQ_TORQUE = 6, CD_RQ_TWIST = 7, & + CD_RQ_N = 7 ! Components of the TDP channels (TDPs, x, y, z, Lay, Exc). INTEGER, PARAMETER, PUBLIC :: CD_TDP_S = 1, CD_TDP_X = 2, CD_TDP_Y = 3, CD_TDP_Z = 4, CD_TDP_LAY = 5, & CD_TDP_EXC = 6, CD_TDP_N = 6 @@ -45,6 +47,9 @@ MODULE CableDyn_RangeOutput INTEGER :: nn = 0 LOGICAL :: want_range = .FALSE. LOGICAL :: want_tdp = .FALSE. + ! The line carries condensed torsion: the route also fills the torque and twist rows, and + ! the range file gets their columns (zero rows otherwise, and no columns). + LOGICAL :: has_torsion = .FALSE. ! Sample scratch filled by the route (public order End A -> End B). REAL(wp), ALLOCATABLE :: r(:, :) ! (3, nn) global node positions REAL(wp), ALLOCATABLE :: val(:, :) ! (CD_RQ_N, nn) node values @@ -419,25 +424,37 @@ SUBROUTINE CD_Range_Write_Files(set, out_root, ErrStat, ErrMsg) CHARACTER(*), INTENT(IN) :: out_root INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg - INTEGER :: il, unit, ios, j, k, nq, n + INTEGER :: il, unit, ios, j, k, nq, n, kq, rows(CD_RQ_N) CHARACTER(LEN(out_root) + 32) :: path CHARACTER(1) :: tab CHARACTER(16) :: id_text CHARACTER(*), PARAMETER :: NAMES(CD_RQ_N) = & - [CHARACTER(11) :: 'Tension', 'Curvature', 'BendMoment', 'Declination', 'Clearance'] - CHARACTER(*), PARAMETER :: UNITS(CD_RQ_N) = [CHARACTER(7) :: '(N)', '(1/m)', '(N.m)', '(deg)', '(m)'] + [CHARACTER(11) :: 'Tension', 'Curvature', 'BendMoment', 'Declination', 'Clearance', & + 'Torque', 'Twist'] + CHARACTER(*), PARAMETER :: UNITS(CD_RQ_N) = [CHARACTER(7) :: '(N)', '(1/m)', '(N.m)', '(deg)', '(m)', '(N.m)', & + '(deg)'] CHARACTER(*), PARAMETER :: STATS(3) = [CHARACTER(4) :: 'Min', 'Max', 'Mean'] ErrStat = CD_RANGE_OK ErrMsg = '' IF (.NOT. set%active) RETURN tab = CHAR(9) - nq = CD_RQ_N - IF (.NOT. set%has_floor) nq = CD_RQ_CLEARANCE - 1 DO il = 1, SIZE(set%lines) IF (.NOT. set%lines(il)%want_range) CYCLE n = set%lines(il)%nsample IF (n < 1) CYCLE + ! the quantities written: Tension .. Declination, Clearance with a seabed, Torque and Twist + ! on a line with torsion + nq = CD_RQ_DECLINATION + rows(1:nq) = [(k, k=1, CD_RQ_DECLINATION)] + IF (set%has_floor) THEN + nq = nq + 1 + rows(nq) = CD_RQ_CLEARANCE + END IF + IF (set%lines(il)%has_torsion) THEN + rows(nq + 1:nq + 2) = [CD_RQ_TORQUE, CD_RQ_TWIST] + nq = nq + 2 + END IF WRITE (id_text, '(I0)') set%lines(il)%line_id path = TRIM(out_root)//'.Line'//TRIM(id_text)//'.range.out' OPEN (NEWUNIT=unit, FILE=TRIM(path), STATUS='REPLACE', ACTION='WRITE', IOSTAT=ios) @@ -451,14 +468,16 @@ SUBROUTINE CD_Range_Write_Files(set, out_root, ErrStat, ErrMsg) ' s; public node order End A -> End B)' IF (ios /= 0) GOTO 910 WRITE (unit, '(A)', ADVANCE='NO', IOSTAT=ios) 'Node'//tab//'ArcLength' - DO k = 1, nq + DO kq = 1, nq + k = rows(kq) DO j = 1, 3 WRITE (unit, '(A)', ADVANCE='NO', IOSTAT=ios) tab//TRIM(NAMES(k))//TRIM(STATS(j)) END DO END DO WRITE (unit, '(A)', IOSTAT=ios) '' WRITE (unit, '(A)', ADVANCE='NO', IOSTAT=ios) '(-)'//tab//'(m)' - DO k = 1, nq + DO kq = 1, nq + k = rows(kq) DO j = 1, 3 WRITE (unit, '(A)', ADVANCE='NO', IOSTAT=ios) tab//TRIM(UNITS(k)) END DO @@ -467,7 +486,8 @@ SUBROUTINE CD_Range_Write_Files(set, out_root, ErrStat, ErrMsg) IF (ios /= 0) GOTO 910 DO j = 1, set%lines(il)%nn WRITE (unit, '(I0,A,ES15.7E3)', ADVANCE='NO', IOSTAT=ios) j, tab, set%lines(il)%arc(j) - DO k = 1, nq + DO kq = 1, nq + k = rows(kq) WRITE (unit, '(3(A,ES15.7E3))', ADVANCE='NO', IOSTAT=ios) tab, set%lines(il)%vmin(k, j), & tab, set%lines(il)%vmax(k, j), tab, set%lines(il)%vsum(k, j)/REAL(n, wp) END DO diff --git a/tests/data/torsion_analytic_refs.txt b/tests/data/torsion_analytic_refs.txt new file mode 100644 index 0000000..8e57b41 --- /dev/null +++ b/tests/data/torsion_analytic_refs.txt @@ -0,0 +1,25 @@ +# Condensed-torsion analytic references (EI = L = 1; T = T L^2/EI, M_cr = M_cr L/EI). +# clamped: clamped-clamped critical torque of a straight twisted rod under dead tension T, +# van der Heijden, Neukirch, Goss & Thompson (2003), Int. J. Mech. Sci. 45:161-196, eq. (33); +# T = 0 is tan(M L / 2 EI) = M L / 2 EI. +# pinned: bending-pinned ends with the torque transmitted semi-tangentially (a constant-velocity +# joint, a bending-pinned end restrained in torsion): tan(M L / 2 EI) = -M L / (6 EI) at +# T = 0; derived and checked against a 4x4 boundary determinant (not Greenhill's 2 pi). +# tag T M_cr +clamped -20.0 5.771135703321342 +clamped 0.0 8.986818915818128 +clamped 10.0 10.428856439276947 +clamped 50.0 15.589323932753878 +clamped 200.0 28.98273784835377 +pinned 0.0 4.91128772575888 +pinned 10.0 7.269274206916713 +# localised: the same clamped-clamped eq. (33) at large tension, where the onset approaches the +# infinite-rod value 2 sqrt(EI T) (Coyne 1990, as reported by van der Heijden et al. 2003). +localised 400.0 40.496233340246 +localised 1600.0 80.24651244000465 +# helix: Kirchhoff helix of radius a and pitch angle alpha (tangent to the plane normal to the axis) +# under a uniform torque M_t, isotropic EI = B: kappa = cos^2(alpha)/a, tau = sin(alpha) cos(alpha)/a, +# axial force F = kappa (M_t - B tau)/cos(alpha), axial couple K = B kappa cos(alpha) + M_t sin(alpha) +# (from eqs. (1)-(5) of van der Heijden et al. 2003, checked by integrating the Kirchhoff equations). +# tag a alpha_deg B M_t F K +helix 1.0 30.0 1.0 0.5 0.05801270189221935 0.8995190528383291 diff --git a/tests/data/torsion_oracle_reference.txt b/tests/data/torsion_oracle_reference.txt new file mode 100644 index 0000000..da34103 --- /dev/null +++ b/tests/data/torsion_oracle_reference.txt @@ -0,0 +1,1455 @@ +# Reference values for tests/test_hermite_torsion_kernel.f90. +# Generated by validation/scripts/hermite_torsion_oracle.py (JAX automatic differentiation, +# float64, 4-point Gauss-Legendre rule per element). Do not edit by hand. +# Layout: number of cases, then per case +# name (one line); n_nodes quadrature_order +# L_e (n_nodes - 1), q (6 n_nodes, node order [r, m]), d_A n_A d_B n_B (12), +# Theta (modulo 2 pi), dTheta/d[q, w_A, w_B] (6 n_nodes + 6), +# d2Theta/dq2 lower band: for column j = 1..n, rows i = j..min(j + 11, n), +# d2Theta/dw_A2, d2Theta/dw_B2, d2Theta/dw_A dm_0, d2Theta/dw_B dm_N (3x3 each, column-major). +# w_A, w_B: rotation vectors of incremental rotations exp(w) of the end frames, at w = 0. +4 +random_seed0_clamped +17 4 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.0 0.0 0.0 2.1570782939422584 +3.1874067295537865 -3.914965652090791 2.1195021167184045 1.416303461094133 +-2.508824673768325 4.42739671695572 1.207218662354192 -3.992643615423291 +5.4198083940752655 1.4496841076982871 -4.96392803002027 6.369548892463389 +-1.2056403168565557 -4.019784170764922 9.060625831909903 0.2082872339836996 +-6.957230064382481 6.17441536877443 -2.9224805666482023 -2.900781929034208 +12.236865105457134 -1.5687175168386394 -8.212654259042717 4.576219249983086 +-2.859060583411729 -1.4054360108922668 14.518905142286181 -2.9465700016899974 +-8.670294855412504 3.005886038095236 -1.5188528743510583 -0.15743215336375185 +15.99996558805522 -3.2888055789094897 -8.483032569708369 2.039403538817526 +0.4064238182137387 0.7016495084478176 17.18802252609693 -2.5422983482499 +-7.9197971194011165 1.968244299772964 1.820028355135734 1.0353381147664416 +18.70983960310864 -1.1792765087821593 -7.2201005897919845 2.757128950088474 +2.143327157011945 1.0708288803041144 20.984645409487424 0.13764657121395213 +-6.474780356485796 4.499843736338568 1.8522180361494083 1.3106169621739878 +24.028925878156485 0.9508114263339376 -5.59856837178418 5.3557028794839345 +0.82651047321183 1.6088910256010536 27.474069611389773 1.2296670493387998 +-4.413285730220472 5.329587685556454 0.16064335464608898 2.1433207381288115 +30.762808044519637 1.3063268104951 -2.800011930794789 4.834197906330568 +0.19630840523580856 2.9066670957670295 33.403213095932976 1.591466609096899 +-0.8374868210351464 3.8094465080629383 0.8279508170899271 3.5326280261311322 +35.1454071976225 2.2681378360011464 1.1539825206186456 2.2266455137918135 +1.4645806769633969 3.220304568287081 36.00708572303993 3.164827830072841 +2.697933145160493 0.7392347351728202 1.3677316155290136 1.7993586250228364 +36.16826615562556 3.8870297548042507 3.3447960260475003 -0.19275508119659013 +0.8221132624465451 0.15657974070261757 0.3929143177337766 0.5805898395063996 +-0.7131155426589256 0.39967862192536363 0.5905851135590672 0.7010465197248082 +-0.22444631053569816 0.9572784668145878 0.1823233135395058 0.6710951539281728 +0.016178953156992547 0.7411946679846436 -2.9183183789727605 -0.06793597627479353 +-0.07449462435734953 -0.10060127859362666 0.00046203624622580264 0.0024055606343104316 +0.0018725822192053182 0.015164836523137493 0.032560374776903614 0.028771848748443504 +0.00040935081744310903 -0.0010132101650361866 -0.00032521738748995943 0.016372006234976574 +0.01413300322489739 0.022860402554804385 -0.0014049788977866032 -0.0016424629146125774 +-0.001797530154912394 0.008098907138328618 -0.0022006964929160086 0.020951440344939634 +-0.001582287827459554 -0.0017941487131394943 -0.0011999613657527712 -0.009131895286366213 +-0.04041757044300798 0.0623113885649854 -0.0019090512573642325 -0.0038558801649215754 +0.004008685237563036 -0.08594323102255193 -0.17826404103147014 0.4107089739630609 +-0.006273469815031248 -0.008006612310121064 0.026041494857729663 0.019166412506377073 +0.16208706645992105 -0.12299906234405045 0.013052850016469386 0.019425847277678807 +-0.05993720941788866 0.0916174963016764 0.11721691489473565 -0.3564051872650854 +-0.0034532308775330305 -0.012267097656124663 0.02335012563892476 0.02819997466203676 +-0.007063171093987457 -0.05662591628587278 -0.0011580596413953781 -0.002675700591706836 +0.007047019212869286 0.004284428103699256 -0.026226519549469213 0.022733397814887854 +-0.0007206083061893961 -0.00032437598452860796 0.0018076676361954355 -0.008135638415691985 +-0.021420702240581586 0.03870764219954001 -0.00029061431401980996 0.00038995219368024095 +-0.00011585638772892634 -0.012066144743229314 -0.018216445176274784 0.031469847468481194 +-0.0002596273440884367 0.0005130623543134691 1.6059904298065808e-05 -0.018401057847528397 +-0.02088238471963741 0.03179673073443861 -0.00036365396737961415 5.892067698367448e-05 +0.00015433596940557515 -0.026952270928750015 -0.015991712658501078 0.03243456446312732 +-0.0010035559294764226 -0.0011482679255710102 0.0009311371041398064 -0.049775057291675015 +-0.008748869899851727 0.03679919442704106 -0.0038499524864722134 -0.0028126269957726713 +0.003276249627300311 -0.6526799214437679 -0.2086526240318778 0.39503491469078383 +-0.0801966894231729 -0.02819815042528609 0.04813297243136605 0.7481171317841221 +0.296582002338467 -0.4979489014858984 0.20054586545573766 0.06634394190980447 +-0.1147317352642509 0.3929143177337767 0.5805898395063998 -0.7131155426589256 +0.2244463105356982 -0.957278466814588 -0.1823233135395058 -0.03968002352298585 +-0.03504611958759245 -0.005655591412225592 -0.0024668558480699784 -0.037095654146943186 +-0.025508074516414243 0.03968002352298585 0.03504611958759245 0.005655591412225592 +-0.0017357355070371514 0.028566041757270472 0.013704387814465634 -0.03006697350888655 +0.0057739912151767905 0.03435997621797439 -0.00019473229366252223 0.025489718453249936 +0.035046119587592454 0.03006697350888655 -0.0057739912151767905 -0.033183452256561115 +-0.002743702720616493 -0.029972089181635117 0.0 0.0697469970318724 +0.02424416060503107 -0.02443333982116298 0.0026615881417324993 0.005655591412225601 +-0.00577399121517678 -0.0697469970318724 -0.013721363870904297 0.03019019300573402 +0.004479438227653644 0.0 0.0 -0.0015166826277596482 +-0.0016907023804929831 -7.524909209220062e-05 0.00246685584806998 -0.03435997621797439 +-0.024244160605031078 0.0004161275556943062 0.010776215221324329 0.006095306281045115 +0.0 0.0 0.0 -0.001963694249388419 +0.00013409825121386007 0.037095654146943186 0.00019473229366252375 0.024433339821162983 +-0.010026458425847474 0.0003665708876905438 -0.008609064217921939 0.0 +0.0 0.0 -1.0842021724855044e-19 0.003480376877148063 +0.025508074516414247 -0.02548971845324994 -0.0026615881417325045 -0.0059939565493154 +0.008498671107808851 -0.0007826984433848489 0.0 0.0 +0.0 2.439454888092385e-19 1.0977546996415732e-18 -0.07710825265824553 +-0.05040967442027201 -0.01702959017025251 0.00014194041389008617 -0.054675049877554345 +-0.0188594415994305 0.03742822913525968 0.015363554832679564 0.011373998758026917 +-0.002538329752474304 0.017145701178932778 -0.0028585947239154028 -0.03096652844914685 +0.01628442178616944 0.058607056050767575 0.003094343536024233 0.06187450774378286 +0.015363554832679562 0.0008995549402602956 -0.010510430570992648 -0.019734111248319627 +-0.00036037686563371665 -0.026133194423601687 0.0 0.10807478110739235 +0.017641431156119537 -0.06089785385359149 -0.003236283949914321 0.011373998758026915 +-0.010510430570992671 -0.03832778407551996 0.0015371775019265654 0.027301996797144474 +0.002898706618108019 0.0 0.0 -0.003488829984606062 +-0.0021351714766041537 -0.0007928542808788722 0.0015937950931470654 -0.02542360379420646 +-0.00392006728521524 0.00035750377441045797 0.007043308105189964 0.0005048627874166952 +0.0 0.0 0.0 -0.001111479329685359 +0.0007221105470990447 0.026109008120283873 -0.00035064081540773985 0.030707660847857466 +-0.00672513835989165 2.7245065267465085e-05 -0.0090978073535522 0.0 +0.0 0.0 1.7618285302889447e-19 0.004600309314291425 +0.005155053784964867 -0.03190241856214775 -0.0012431542777393237 -0.0002969488297477711 +0.008905935854942306 -0.0003847488396779247 0.0 0.0 +0.0 4.0657581468206416e-19 8.131516293641283e-19 -0.0640607854059598 +-0.02098489072380518 -0.02199925743456288 0.0011542960276326425 -0.03324724540339117 +0.00960376296321805 0.026632556270700127 0.005621335891125619 0.010625258676535961 +-0.0018399832223501896 0.013584922096530677 -0.012125050851293211 0.006085642291283051 +0.022192535891690342 0.03537708738479117 0.0008913149473349458 0.05388097995936074 +0.005621335891125623 -0.006985197231543346 -0.011682105320697694 -0.014456672341867248 +0.000333973444872067 -0.026943314981383942 0.0 0.05797514311467675 +-0.009454438231599731 -0.05363252851098675 -0.0020456109749675865 0.010625258676535963 +-0.011682105320697689 -0.019647359039156785 0.010703003513158437 0.0283949001591415 +0.0015060097774781235 0.0 0.0 -0.0022889461961108468 +-0.0006936826472989004 -0.0008083908887027863 0.001384033724841662 -0.01564297613647155 +0.007917260729673166 0.00019533598390260093 0.004639683707907303 -0.0027977418616529754 +0.0 0.0 0.0 0.00024932105702922227 +0.0007987041348122719 0.016101544224458395 -0.0005309380817012297 0.026330531713842274 +-0.004550877515654063 -4.724610485733866e-05 -0.008488226718321909 0.0 +0.0 0.0 3.6422416731934915e-20 0.002039625139081629 +-0.006745168239302648 -0.027747785535759052 -0.0008530956431404324 0.002949118674987766 +0.008325243614558213 -0.00014808987904526107 0.0 0.0 +0.0 5.319366908757006e-19 6.860966872759833e-19 -0.049195120871960346 +-0.011171435066660164 -0.01741054707722807 0.00042831667961088773 -0.026992257530777525 +0.027851183593103672 0.022562564601260222 0.0055500991755345445 0.0067852884006921075 +-0.0011965877028720386 0.01368135019344913 -0.02473279956969332 0.020695488520943345 +0.02264387164993619 0.026986341060161737 0.0007673523391557771 0.057880321213956754 +0.00555009917553454 -0.013710291289399998 -0.010961766329238499 -0.014010829928203763 +0.0004423831026648421 -0.03929509594268603 0.0 0.02849963235101701 +-0.02697251719329076 -0.05806569942150301 -0.0011956690187666656 0.006785288400692109 +-0.010961766329238481 -0.008852273311860226 0.02356921923852716 0.04067135852833552 +0.0007542046002071976 0.0 0.0 -0.0016241243399055002 +-0.0003401139454276144 -0.0006069247258090988 0.0014116665427393025 -0.01252966871829449 +0.016269513680132325 0.00019109842477499752 0.004199149499604928 -0.0061902218856974296 +0.0 0.0 0.0 0.0006696141151413314 +0.000770727746383966 0.01340733543424685 -0.0011013257840278444 0.02967079926236152 +-0.004089309220580997 -0.0001247025177510742 -0.010995421269795323 0.0 +0.0 0.0 1.0842021724855044e-19 0.0009545102247641615 +-0.01572613274181046 -0.03093700623257281 -0.0003103407587114582 0.006276382810162825 +0.010832233357497069 -6.639590702392446e-05 0.0 0.0 +0.0 3.2526065174565133e-19 1.6263032587282567e-19 -0.06585421696547558 +-0.03345943206615093 0.017277675641122824 -0.002439186536758327 -0.030419667103584954 +0.06350503765211055 0.04329165236421536 0.02790933289061638 -0.024062964041814933 +-0.000637412800283523 0.011657471422900745 -0.0570258601519918 0.06310339692538604 +0.006398116583929973 0.025122689266345196 0.004061434385533867 0.09512564179993752 +0.027909332890616385 -0.04939310563598604 0.004563649745308526 -0.011110893786248324 +0.00044108688190376034 -0.09732487708776556 0.0 0.002750820040089549 +-0.05588419703846039 -0.09903881438067348 -0.0016222478487755384 -0.024062964041814922 +0.004563649745308526 0.006101453271770677 0.053706711737584345 0.10002406624391336 +0.00019632591837976288 0.0 0.0 -0.002240441116875105 +-0.0010585098269442555 0.0003813065184340444 0.003635774239630365 -0.011111859338141437 +0.03231497779993323 0.0005189684576894253 0.004234306452945393 -0.014652105344863393 +0.0 0.0 0.0 0.002089567010894972 +0.0003900070814627887 0.016738316910135824 -0.004503817488198709 0.058367455852337956 +-0.003475549969359293 -0.000577614633935678 -0.024389677808087182 0.0 +0.0 0.0 -1.3010426069826053e-18 0.00015087410598013174 +-0.038772238082417226 -0.0558305458572515 0.0008680432485683413 0.01373918711606056 +0.024720630218646265 5.86461762462481e-05 0.0 0.0 +0.0 0.0 -4.404571325722362e-20 -0.33582003205231226 +-0.29758353802970705 0.5317086188481925 -0.027409996413241714 -0.025578541138473625 +0.1756519753540554 0.2925283796880969 0.2696742051390907 -0.5076456548063776 +-0.0045652237651852 -0.08249678805801806 0.003072688014260541 0.16428371986771106 +-0.14394636809740197 -0.026151457233205427 0.0026432610412796273 0.28089010963037503 +0.26967420513909063 -0.11489061423172502 0.13938271835209343 0.07221360952878236 +0.015282235857190313 -0.3192971103172913 0.0 0.17153631218460128 +-0.06986815764439326 -0.3037159245141023 0.02476673537196209 -0.5076456548063776 +0.13938271835209348 -0.17763776545637194 0.008524831777154754 0.29792349932403484 +-0.01071701209200511 0.0 0.0 -0.010477615488936526 +-0.009134996473180959 0.015466233153773906 0.028047409213525238 0.03726235101945374 +0.016161445906808958 0.0027289030996460427 -0.011822498888236624 -0.014212286534046596 +0.0 0.0 0.0 0.007181164312918705 +-0.004752398322477411 0.013921069715572859 -0.003084347923183385 0.20369185827018899 +0.016571556981122303 7.864385339683214e-05 -0.07708456909140482 0.0 +0.0 0.0 2.6020852139652106e-18 0.003296451176017831 +-0.11862611520206362 -0.18356523254260948 -0.024963061290341856 0.004929653121118219 +0.07752184567805716 -0.002807546953042874 0.0 0.0 +0.0 -2.168404344971009e-18 -1.0842021724855044e-18 -0.49104320238315213 +-0.4135606591538502 0.7716123638956403 0.005319023543987399 0.1960537045455249 +-0.12040378891772334 0.19851482269505522 0.14388645401475952 -0.26396670908926273 +-0.019413393092669304 -0.1030212574036006 0.16417133398121594 0.02253756969391678 +0.0015222668769361847 -0.18060854342670338 -0.01460231123037981 0.5921534309451523 +0.14388645401475955 0.09235304453780824 -0.1409049852290296 0.07101180160487658 +-0.011540624458894842 -0.15778177341848768 0.0 0.46850563268923534 +0.09358882813148828 -0.558398955606989 0.009283287686392402 -0.26396670908926256 +-0.14090498522902956 -0.2908678672328634 -0.10270085788229937 0.18094684983282305 +0.03095401755156415 0.0 0.0 -0.02868001545356889 +-0.023453120990307323 0.047234880293243874 -0.0007537997788022127 0.10839493389792099 +-0.10211365990864307 0.002575040531573992 -0.028692479659395035 0.033163725883827766 +0.0 0.0 0.0 -0.005243085626729479 +0.0027083565990177454 -0.1135569164875068 -0.0006799246268104926 0.2604754562829542 +0.032467193554360306 0.0007430349710284261 -0.06922569430030476 0.0 +0.0 0.0 -5.421010862427522e-19 0.0339231010802984 +0.11733110090346284 -0.27285632062786097 0.0014337244056127048 -0.040714676448877156 +0.0674593634785194 -0.003318075502602401 0.0 0.0 +0.0 -4.7704895589362195e-18 6.7220534694101275e-18 -0.21245606527720234 +-0.13308007288815138 0.24096206076695542 0.020473701831672672 0.15748517588763697 +-0.2659181652819861 0.013941242582147139 -0.010806381126608153 0.02300464832230731 +-0.00197126105826311 -0.037962111300400725 0.07558469904437307 -0.06085326556312405 +0.13140424046148824 -0.12245001946803924 0.013257203724341736 0.26138395453614793 +-0.010806381126608167 -0.03149977897468419 0.00950074476754136 0.03735706847875822 +0.0013372175353654598 -0.0867521648283954 0.0 0.2733093308403264 +0.19886487238484457 -0.2892126307803554 -0.03373090555601441 0.023004648322307277 +0.009500744767541348 0.017558536392537044 -0.07476794227034358 0.08877351225810029 +0.000634043522897652 0.0 0.0 -0.007522340045281135 +-0.003529762628475784 0.00606341427685741 -0.0010603087390033703 0.05143821786316266 +-0.09616401450254525 0.00026395770262382175 -0.014487615347171974 0.027772550379099835 +0.0 0.0 0.0 0.00010947266502183729 +0.0044374070573143885 -0.05446391848403636 -0.0017165792654468938 0.10826578094753239 +0.014288355520958168 -0.0003891725403043461 -0.03070784499478659 0.0 +0.0 0.0 -6.776263578034403e-20 0.007412867380259298 +0.10174683130077018 -0.10360218111766026 0.002776888004450255 -0.02726775705840539 +0.030776042481602393 0.00012521483768053 0.0 0.0 +0.0 -2.439454888092385e-19 -1.6263032587282567e-19 0.0031327933224302475 +0.028497552879840397 -0.06958004108639304 0.0032666831102720747 0.06660081327336403 +-0.13203137955283298 -0.017074035904577387 -0.017691171753232246 0.04657539276408574 +0.0005975216480311916 -0.01797302596874928 0.028923863569367256 0.046968852498947625 +-0.03465057530652888 -0.06493586522558252 -0.0013642747363185636 0.16439266991259166 +-0.01769117175323226 -0.015469073524263442 0.02514983053898752 0.02050048264437898 +0.0014301244462562456 -0.06380784792605426 0.0 -0.050101645821377876 +0.12694222683123638 -0.16815392501468507 -0.0019024083739535107 0.04657539276408574 +0.02514983053898754 0.03254310942884083 -0.03416995745840589 0.06034067595235561 +-0.0020276460942874365 0.0 0.0 0.0003083577826980732 +0.0018139847427433332 -0.003979253606784699 -0.001295422052008965 0.027578796746824293 +-0.05217428456089279 -0.00016143627095017944 -0.007669430359377722 0.014067781982125969 +0.0 0.0 0.0 0.002733512580988974 +-0.002285677001857192 -0.028638701972963313 2.7057200953102556e-05 0.07938041275658478 +0.007265031578667864 -0.00021123741452237852 -0.021793940546257044 0.0 +0.0 0.0 1.3446648037662018e-20 -0.0030418703636870435 +0.05644668050845991 -0.07764050508419626 0.0012683648510558605 -0.013018554792822804 +0.022346491247664812 0.0003726736854725635 0.0 0.0 +0.0 -1.1689054672109345e-19 -5.682135238198262e-19 0.03769543587272416 +0.01906979975125454 -0.07892125647882853 0.00039391053704453984 0.03368395591927566 +-0.04907924206887852 -0.020621399968146777 -0.0013786279980222945 0.032345863714742795 +0.001262045837391661 -0.014511576280369056 0.008548145843805172 0.014963404180545373 +-0.03518821166133686 -0.036946785894802645 -0.0016605799066536705 0.11980980394140406 +-0.0013786279980222988 0.0005056693437180675 0.010038381122349335 0.015469079446747427 +0.00011067416959760064 -0.05236183115080109 0.0 -0.05265884005326954 +0.05195255832150227 -0.11679625097586435 0.001266669369609129 0.03234586371474281 +0.010038381122349341 0.020115730624428712 -0.013101696327143517 0.050766950404029926 +-0.0013727200069892618 0.0 0.0 0.001708018596689986 +0.0008876941312929453 -0.0035241165385554614 -0.0009914321850757325 0.01644630325042367 +-0.01778260086309638 -0.00020179494864427678 -0.004868743201620978 0.004999590076285999 +0.0 0.0 0.0 0.0005659598676902254 +-0.0014804010448595832 -0.015710929950526375 0.00023045546039742438 0.056455575023508744 +0.004825269503614544 -8.493565239403387e-06 -0.016657048366564554 0.0 +0.0 0.0 1.5246593050577406e-20 -0.00227397846438021 +0.020155378499511274 -0.05600195601534979 0.000760976724678307 -0.004336728347167954 +0.016883814367469527 0.00021028851388368505 0.0 0.0 +0.0 1.4907779871675686e-19 7.873171244728722e-19 0.03140065967203254 +-0.009174100561308261 -0.04403694730713293 -0.0009843031208975825 0.028068610963964378 +-0.013803382076116658 -0.010779259703885762 0.010552728559330555 0.011691083592390135 +0.0007943965116550751 -0.016421877462944608 0.0012810382369735447 -0.0021470733089071865 +-0.014952478999382054 -0.030570768028058498 -0.00015716789268103336 0.09697374866972339 +0.010552728559330558 0.0016414039651891188 0.00491409787703272 0.015868388725484177 +-0.00011978608210446045 -0.043837520089120806 0.0 -0.029253586363125354 +0.01802136570210606 -0.09588001138704759 0.0011414710135786172 0.011691083592390135 +0.004914097877032724 0.00913785573869664 -0.0032111109654435082 0.043435408100173184 +-0.0006746104295506143 0.0 0.0 0.001396238851753078 +-0.00046750389144283554 -0.0018726269976920376 -0.00027774271649407787 0.01510168858131107 +-0.004919669374962542 -0.00012752384997501696 -0.004667963327043085 0.0013128581101900226 +0.0 0.0 0.0 -0.00014382492253570497 +-0.0005777149552968132 -0.013557034683595327 4.649372308343204e-05 0.04511306098301765 +0.004917873562127295 1.878868643267681e-05 -0.013828263172267428 0.0 +0.0 0.0 -6.098637220230962e-20 -0.0012524139292173784 +0.005255236232311487 -0.0446119175189223 0.00023124899341064528 -0.001023729207584597 +0.013960007261082201 0.00010873516354233852 0.0 0.0 +0.0 -1.8973538018496328e-19 1.2790197503539935e-19 0.010574551720246678 +-0.02883173551161733 -0.01099818007527388 -0.0011317718360381525 0.034114537015427125 +-0.0019183738349650907 0.00020470798363908407 0.018279006952286774 -0.0006929035171162543 +0.00033143346931318284 -0.023184090797496764 0.0009024310863518196 -0.00238681425280183 +-0.01388915203360873 -0.03572292034677082 7.519716481533204e-05 0.08555996627420015 +0.018279006952286795 0.000745410287612711 0.00897505415657601 0.02178096019815423 +-7.259091649954247e-06 -0.04171025491763631 0.0 -0.008187737467444845 +0.004466889457078664 -0.08637642355953028 0.0010565746712228202 -0.0006929035171162549 +0.008975054156575991 -0.0009501182712517952 -0.0013489574987671214 0.04121161534338114 +-0.0003241743776632286 0.0 0.0 0.0005175073587539252 +-0.001387667347015242 -0.00048196105033907605 0.00033737532438307726 0.019854531621286635 +-0.0012557784916351555 5.4250645193466555e-06 -0.006267922516081796 0.00033615647290719897 +0.0 0.0 0.0 -9.839393178188012e-05 +-0.0006197669610823609 -0.017692659552482514 4.458891728912841e-05 0.04294101545935711 +0.006715245112325715 1.345834907815918e-05 -0.013050170912573258 0.0 +0.0 0.0 5.421010862427522e-20 -0.00041911342697204 +0.0006373355979915458 -0.04172244618507935 -0.0003819642416722056 -0.00034545094206477143 +0.01327672401261701 -1.8883413597506397e-05 0.0 0.0 +0.0 -4.743384504624082e-20 1.2874900798265365e-19 -0.016610124498400377 +-0.04710637314577877 0.00929722840348988 -0.0016769862128750129 0.05183552428332542 +-0.0034846780615068046 0.01640541651476129 0.028827366193491993 -0.008604324886373626 +-0.0004462320605313052 -0.038690552913129256 0.007410620920380621 -0.006737723109586722 +-0.028180762345724746 -0.05398559108671283 -0.0005674486756748417 0.08581663271099604 +0.028827366193491986 0.005992312821974011 0.019205708189148733 0.03590329523463731 +-3.407628731874591e-05 -0.045710192199921035 0.0 0.0233478476079871 +0.0050851865168219015 -0.08788455127673805 0.0022444348885498544 -0.008604324886373624 +0.019205708189148726 -0.022397729336735304 -0.006592921019659701 0.04403179392266009 +0.00048030834785005175 0.0 0.0 -0.0006590187748515529 +-0.0020351476887834233 0.0004288073803158264 0.00134555274356183 0.03220463088855861 +-0.0037362290180547814 0.00016074620336861114 -0.010250475275926147 0.0015258251003387947 +0.0 0.0 0.0 -0.0002251256621245992 +-0.0011999789940818767 -0.028651433485828652 0.0005747077673247961 0.046672935933356904 +0.010806342672832453 6.822840999751861e-05 -0.013823926607342416 0.0 +0.0 0.0 -6.2341624917916505e-19 0.0008841444369761492 +0.002582246975154985 -0.04410637779335975 -0.0019202605108866256 -0.0016752436186599383 +0.014189943878411848 -0.00022897461336613182 0.0 0.0 +0.0 -9.486769009248164e-20 1.6940658945086007e-19 -0.055908955340423755 +-0.07141067951628934 0.014147446919275677 -0.002444498542155008 0.08802458552270964 +-0.0226424491528747 0.03950353882566246 0.04258331332279734 -0.005543122032902052 +-0.0017730726581009527 -0.07239703346740506 0.028884531978807443 -0.03359971555007534 +-0.047650656067133354 -0.09094835222043948 -0.0022952293834966426 0.09393009373916046 +0.04258331332279735 0.027607402728101332 0.028444947877984624 0.06801229641999597 +-0.0009145792546547188 -0.055634268851966415 0.0 0.08950867089049908 +0.02191468435905731 -0.09579380310766256 0.00473972792565165 -0.005543122032902052 +0.028444947877984645 -0.06711094155376378 -0.02747422412312062 0.052242066160810396 +0.0026876519127556706 0.0 0.0 -0.002058254165550642 +-0.0026273212616864773 0.0006064614060038744 0.0028907306026863134 0.055045056985802185 +-0.01532176333939761 0.000314444791495015 -0.01855336088431646 0.006729353104209768 +0.0 0.0 0.0 -0.001145301055871768 +-0.0017981088272767632 -0.04933403260958037 0.002329305670815393 0.05176200918500247 +0.019222730432758125 0.00022297583460896423 -0.015787850607481887 0.0 +0.0 0.0 1.5178830414797062e-18 0.0032035552214224085 +0.015231828232494082 -0.04821990153923942 -0.005220036273501706 -0.006777746530734879 +0.01619538212978991 -0.0005374206261039758 0.0 0.0 +0.0 -2.0599841277224584e-18 -1.0842021724855044e-18 -0.11276914844102819 +-0.11914762940693854 -0.024114707785878688 -0.003929093624398366 0.16338661526237158 +-0.08420506398004704 0.07326560961536573 0.0765643160841412 0.02965782981878074 +-0.0018550326770005367 -0.17022728659417807 0.11650915600971642 -0.10456548847701806 +-0.047125912243722604 -0.17160796146295357 -0.005615246079632584 0.11366599262188751 +0.07656431608414119 0.07695808574891673 0.018680964365737983 0.16752846658952025 +-0.0015509373333753799 -0.08017391345420385 0.0 0.21733463691804628 +0.0760280890627945 -0.11143625721797429 0.009544339704030961 0.029657829818780776 +0.018680964365737997 -0.15022369536428248 -0.11470846136015991 0.07646020120341128 +0.0034059700103758993 0.0 0.0 -0.004023860843460184 +-0.004079106327436721 -0.0007054570154641598 0.0057021662824993215 0.10359566504295759 +-0.048553864939673876 0.0008105294295906399 -0.04101488187356801 0.02709894662677413 +0.0 0.0 0.0 -0.0035518174500865918 +-0.0017525344380110944 -0.09098958179496651 0.0065298253342873135 0.059194191057163885 +0.042811043042920834 0.0008216536187621764 -0.021287324367216885 0.0 +0.0 0.0 3.0357660829594124e-18 0.0075756782935467795 +0.055320532001239604 -0.05803172376992109 -0.012231991616786626 -0.026202680824807617 +0.02148764824808692 -0.0016321830483528327 0.0 0.0 +0.0 -6.071532165918825e-18 0.0 -0.23456131723208767 +-1.2864928361756347 -0.7638511428556903 0.0023275555711071774 0.30600323978152877 +-0.5100983293056752 0.16129570761672193 1.2099285200914935 0.7341933130369096 +-0.05786000726201612 -0.36710718334140857 1.1890994948455953 -0.9877113259548156 +0.4413116437947425 -0.6354647609429962 -0.11996637634552994 0.24209619598387236 +1.2099285200914935 0.9107532402058989 -0.4599926081604805 0.6689520637628703 +0.11064355851651692 0.0007252022617299098 1.2222726431869035 0.3175258099030712 +-0.0951673388478885 0.11763882077442282 0.73419331303691 -0.4599926081604806 +-1.072048947822621 -1.0530411921442746 -0.15505255136303822 -0.05278355125450074 +-0.007869308295816141 -0.03969666432463673 -0.02280093322673245 -0.0004725228941066355 +0.46793629435347583 -0.2028173485429114 -0.006445783723306591 -0.0913010761419593 +0.25465317117200187 -0.030909809141146986 0.013175532968186174 -0.13577595318735072 +0.12151731367890536 0.018707137644477283 0.16371565595334436 0.027428398334374035 +-0.012468729997128995 0.03877911743696319 0.39358917329595877 -0.16192228252966853 +-0.12104479078479875 -0.21793865762747655 -0.01963692627337539 -0.020982614611067485 +-0.16129570761672193 -1.2099285200914935 -0.7341933130369096 0.05786000726201612 +0.36710718334140857 -1.1890994948455953 -0.9107532402058989 0.4599926081604805 +-0.6689520637628703 -0.11064355851651692 -0.0007252022617299098 1.072048947822621 +1.0530411921442746 0.15505255136303822 0.05278355125450074 0.08530452910813749 +-0.2457997214639234 -0.10482067041097554 -0.1798319028749777 0.1269000272518011 +0.09452737376684045 -8.326672684688674e-17 0.0 1.3877787807814457e-16 +5.551115123125783e-17 2.0816681711721685e-16 -1.6653345369377348e-16 5.551115123125783e-17 +-2.7755575615628914e-17 -1.1102230246251565e-16 1.6653345369377348e-16 1.3877787807814457e-17 +-1.6653345369377348e-16 -5.551115123125783e-17 1.734723475976807e-17 -8.326672684688674e-17 +4.163336342344337e-17 1.0408340855860843e-17 9.71445146547012e-17 0.0770151815149499 +-0.020776349212960815 0.025518768216935998 -0.02077634921296082 0.06037540868853769 +0.03770780771982608 0.02551876821693603 0.03770780771982609 0.04476057178131438 +-0.5528766583832341 -0.1250913848995657 -0.023824912583719424 -0.12509138489956553 +-0.0486828604769189 0.10161483936046713 -0.02382491258371943 0.1016148393604672 +-0.5628523822029707 +random_seed1_clamped +17 4 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.0 0.0 0.0 1.4109899382012159 +2.2871704949541516 2.0216435314300867 0.9069017520941036 1.615985788165432 +0.4451644109158214 1.8313694478230447 3.111745947606634 -0.45047793469546143 +2.339189426138023 3.577167149654298 -0.4262977186225645 2.9294067803304102 +3.2028473355858793 -2.200250220518743 4.549181798910462 5.448790498967067 +-1.9915251566831003 3.918728079947978 2.6000298000088007 -2.446817583320951 +7.299171307864915 6.899048665334774 -3.307670153510658 4.2017259791354125 +1.7561479354535516 -1.3341788210175132 9.911765939198094 7.791698748350801 +-3.595050509603362 3.974415299322273 1.0760536350477694 0.6281359306872484 +11.611689216244702 8.156573696474355 -2.6300251854238548 1.6893069508955412 +0.2115692895114657 2.3995468508411935 11.969402682857291 8.083885998973024 +-0.8443242552961532 -0.4470208941012666 -0.3954574354030288 2.846366225027567 +11.185599704077013 7.628508461639379 0.9129339326214916 -1.8502570258297082 +-1.055777610730816 2.364749961105136 10.037707862446888 6.792505722520622 +1.8144445292476563 -1.6812705175198526 -1.7342976880850467 0.4909946616092002 +9.500144829338495 5.592252209732971 1.4579381384846863 0.06225253598485779 +-2.2145036724266562 -1.5878188098600081 10.23956101418771 4.151826298704065 +0.027163004513150228 2.4999449926924995 -2.568083009062571 -3.087093129187055 +12.263896162559856 2.739636248379797 -1.8543614956564682 3.6196872832802645 +-1.8639156120323703 -2.6976194314227646 14.922727148548141 1.6978113283983745 +-3.472950304693688 4.276251714955349 -1.214199085581636 -2.094048455568745 +17.26088340688671 1.2848196041262536 -4.372682451084421 2.994657129981702 +-0.10730661973545805 -0.7833954378141385 18.52899899607292 1.5168064970308386 +-4.516362742874662 0.9077397788309146 0.7044510624086173 0.22308399299693796 +18.567251933854358 2.109573751245476 -4.196885151379007 -0.7571536664486264 +1.0165794646891444 0.6907644506730436 0.4195755774631256 0.6801188691681233 +0.6011610920526692 -0.31563473635318645 -0.5116340213523846 0.7991278629869204 +-0.5245037690428689 0.704216045419119 0.47851390537170607 -0.22114263570049134 +-0.6554184954770563 0.7221651684081914 -2.8789254988369506 -0.37818782999200795 +0.1987001698423762 0.00459893869678841 -0.0005847125472482478 0.00330387654614768 +-0.007905908563952974 0.25132874791432414 -0.14193182277772354 -0.07664788905235968 +-0.005579361225128587 -1.0566009129270304e-05 0.007287273175085398 0.1011827767084308 +-0.07463843929528076 0.016230162649429343 -0.005520814090392054 0.0038118743783406486 +0.000902020702911982 0.033702121803265796 -0.03945487610297613 0.01107241432096135 +-0.0008316171969052644 0.0009787917608481585 0.0008019265885858481 0.03638361580959571 +-0.09129906901296697 -0.0007010041741236858 0.0006909881218535932 -0.002861286573819552 +0.0006414487016422679 0.06728868541044684 -0.25500664781589416 -0.0039167195506057476 +0.0013678202125550487 -0.009205661460226028 -0.00048035279412815893 -0.006088366553756705 +0.005140859097091166 0.01310142397311169 -0.0033460511449440317 0.022201875206451893 +3.101021952756078e-05 0.00393024585938731 0.16793140551688068 0.028966463279307304 +0.002302678900910557 0.0012626456573808786 0.0019231419932708646 0.1690684780069229 +-0.032604449561199345 0.13625566519643403 0.011859395373919779 -0.008815388968928972 +0.01095104706963446 0.23293274650190615 -0.14336835081872573 0.38883010006097907 +-0.01736721149635584 0.004948173304877512 -0.0019910998300562144 -0.3992238497105193 +0.25059036068418655 -0.307851813410495 0.010326419817519333 -0.0034203602210938766 +-0.005814649328794913 -0.09579774377165495 0.09497121291212374 -0.14689710125742533 +0.008751370527482209 -0.006838783401726341 0.010968867490753487 -0.008945142579538014 +0.013144057074997703 -0.01979896824304321 0.0009571124194785244 -0.0003311190600911243 +0.0013408943321206665 0.010545927615681051 -0.028085459718174763 0.03975574195244434 +0.0008550455596140873 -0.001428636178988238 0.0028850279520084773 0.09971343702642671 +-0.263415355687991 0.45513682953848217 0.008402671726989025 -0.02332584267679577 +0.04261466284234702 0.2013694739433162 -0.6515759695467676 1.2537278956345368 +-0.03336377909471705 0.09307418282125557 -0.16838092532533455 -0.3192033239922267 +0.9909023752100439 -1.7918621396144219 0.01807816611921395 -0.051226991259760934 +0.09283933252494392 0.41957557746312557 0.680118869168123 0.6011610920526691 +0.5245037690428689 -0.7042160454191189 -0.47851390537170596 -0.342968304258758 +-0.22212314498201824 -0.08920652388297368 -0.018549414800339777 0.05801760867488594 +-0.12930257960043126 0.342968304258758 0.22212314498201824 0.08920652388297368 +-0.0242184698115605 -0.02487853851124882 0.12178373539415008 0.33132308712151004 +0.05009551613488036 -0.08223434182788245 0.020710298399460838 0.07003231615066208 +0.22212314498201824 -0.33132308712151004 -0.05009551613488036 -0.007424736298141263 +0.022293496128004332 -0.06410815064109765 0.0 0.011645217137247879 +0.1306135861809475 -0.07741364475152836 -0.002160883599121054 0.0892065238829737 +-0.050095516134880384 -0.011645217137247879 -0.14282038899940655 0.08361657120355184 +0.001924973683556153 0.0 0.0 -0.011721773177405078 +-0.007351140089681945 -0.004846140202959787 0.01854941480033978 0.08223434182788243 +-0.1306135861809475 0.002255276689223887 -0.010824666190538353 0.04057833097004393 +0.0 0.0 0.0 0.009787896093160095 +0.003432336363300866 -0.058017608674885965 -0.02071029839946084 0.07741364475152834 +0.013511192236197282 -0.0018285206815673721 -0.02288877220635305 0.0 +0.0 0.0 -1.3145951341386741e-18 0.0019338770842449986 +0.12930257960043123 -0.07003231615066208 0.0021608835991210494 -0.03888712759434537 +0.02215888205718188 -0.00042675600765650524 0.0 0.0 +0.0 -3.3881317890172014e-21 -3.5575383784680614e-20 -0.43995285590541544 +-0.26962217346945827 -0.08850924726323095 0.023251514789113158 -0.0006485390271504451 +-0.21051287314346154 0.0969845516466574 0.047499028487440045 -0.000697276619742733 +-0.008677826197104874 0.02777030301871234 0.058221378201664245 0.3941124535530154 +-0.0012721341818690765 0.03292424735209109 -0.023869952857235123 0.11908756078018665 +0.04749902848744005 -0.06278936643150534 0.05136765031674943 -0.036605508145542184 +0.007244054895401445 -0.05004431599880775 0.0 0.045840402352399924 +0.23105005670989615 -0.14227720226488155 0.000618438068121971 -0.0006972766197427469 +0.05136765031674946 -0.034195185215152045 -0.057210599447991665 0.042261131158295544 +0.0014337713017034326 0.0 0.0 -0.016256398255592358 +-0.009605994303555031 -0.001856712788279741 0.0009669550224473389 -0.025499511053949837 +-0.08822966771048962 0.0011477291853643993 0.0094248306049857 0.022913978233365873 +0.0 0.0 0.0 0.014694585026867771 +-0.0024656262243291103 0.025527077538399258 0.0015764567292307868 0.05866063106132968 +-0.008178821223553588 -0.0008632031788694343 -0.015589875536142283 0.0 +0.0 0.0 1.463672932855431e-18 0.001561813228724567 +0.08872913774931146 -0.05497941013908898 -0.002543411751678125 -0.022852790709762677 +0.016545645356958346 -0.00028452600649496516 0.0 0.0 +0.0 -3.7947076036992655e-19 6.301925127571995e-19 -0.11500635854861913 +-0.06186398032140123 -0.012644006902368386 0.00869533189357699 -0.05683510297035385 +-0.0962048193984991 0.018021806901961732 0.014364951833961186 0.013341283522111119 +-0.0016885005236206877 0.02568747543208687 0.028960496864793222 0.05226671198992029 +-0.06377100718503408 0.06446663494148434 -0.008384793124248828 0.08846728191777938 +0.014364951833961194 0.010522654441585054 0.01240335686828464 -0.027063161649527483 +4.2381887713359174e-05 -0.04020056127765317 0.0 0.06273964655869882 +0.0933866843556787 -0.080649837267212 -0.0003105387693281673 0.013341283522111134 +0.012403356868284655 -0.02854446134354678 -0.029758222334405157 0.03789721534682677 +0.0016461186359073275 0.0 0.0 -0.0042313311683717676 +-0.0022389781631933377 -0.000709483502129619 -1.7505696472122124e-05 -0.02786112679594216 +-0.03617608490768702 0.00023852098827791077 0.008802353243506821 0.010643614217173194 +0.0 0.0 0.0 0.0012662244372988718 +-0.0025721876502759955 0.02906479995164151 0.001140738228847382 0.03838870610891646 +-0.00840452628849459 0.00011371115093943029 -0.012024854298324297 0.0 +0.0 0.0 -1.5585406229479126e-19 0.0029651067310729 +0.03798344119683486 -0.038422965918971624 -0.0011232325323752644 -0.010317783394159896 +0.01235962034113623 -0.0003522321392173443 0.0 0.0 +0.0 2.168404344971009e-19 4.743384504624082e-20 -0.011169672329301973 +-0.03654366508356558 -0.03341458966101107 0.0026438361019080485 -0.049779553509883986 +-0.05413592929263977 -0.006852134572659758 0.022178713249604395 0.020073306138899946 +-0.0004320374767279733 0.019642370297679262 0.02271874761534502 -0.035181677484761066 +-0.010902212508005096 0.04811983037989966 -0.0015634086748848542 0.0814545338792606 +0.022178713249604402 0.024659023043176014 -0.0015011443602795434 -0.02024559544651716 +-0.0005871830563387533 -0.050273615119504254 0.0 0.04635134981406304 +0.052696538479758845 -0.07854761393151583 -0.0010804274270231956 0.020073306138899932 +-0.001501144360279553 -0.017806888470516258 -0.024022236538216474 0.04997735960702007 +0.0010192205330667263 0.0 0.0 -0.0004422178776239521 +-0.0017095530847704831 -0.0017195606065967834 -0.0009553355782873612 -0.021056668730372187 +-0.02293831614535368 -0.00011380276961830494 0.007050926587418032 0.007806258586909122 +0.0 0.0 0.0 -0.0018753669327474564 +-0.00041413735296204476 0.024092078077797112 0.0015210267871714933 0.04065039858468905 +-0.006365624974767439 0.0003632710173509914 -0.014473227619501036 0.0 +0.0 0.0 -2.168404344971009e-19 0.0023175848103714034 +0.02517543242784654 -0.04125397260160743 -0.0005656912088841306 -0.007237363751865683 +0.014439412583667391 -0.0002494682477326821 0.0 0.0 +0.0 -4.336808689942018e-19 3.7947076036992655e-19 0.06058304315318596 +-0.10281357678012114 -0.049890444340583685 0.004998626247882063 -0.04105895279615254 +-0.0499705258961828 -0.053730908580526204 0.08063486353051674 0.02981713820168374 +0.0007847507612935931 -0.002806557180478798 0.02312120702027759 -0.08445865045999995 +0.000753437196920996 0.028435150135542615 -0.0037177204969532558 0.11806734046372105 +0.08063486353051673 0.05979962741682393 0.0007477071633585474 0.0002531201018327843 +-0.0017868213760607859 -0.08070226711347178 0.0 0.02387560730681399 +0.048165172835502645 -0.11900634432015662 -0.0012809057509288006 0.029817138201683725 +0.0007477071633585648 -0.006068718836297734 -0.02642989263973794 0.08269449830624347 +0.0010020706147671962 0.0 0.0 0.0024894181925218034 +-0.0043244495661005035 -0.0024962004684870413 -0.00456658877115409 -0.008189554689025453 +-0.02414293629728617 -0.0006342213137724372 0.002928995226355988 0.008183704871273234 +0.0 0.0 0.0 -0.003797987818285888 +0.0002394956748428816 0.02141658249847326 0.004304903553292009 0.06902898471313654 +-0.0009668558790811971 0.00067750348394561 -0.023372123136158718 0.0 +0.0 0.0 -8.673617379884035e-19 0.0013085696257640902 +0.02725177828083778 -0.06779372534421682 0.0002616852178620771 -0.007579493722927577 +0.023578581679805196 -4.32821701731777e-05 0.0 0.0 +0.0 -3.0357660829594124e-18 8.131516293641283e-19 0.22574198406337465 +-0.37213818340867144 -0.014225238841384876 0.011226967945898036 0.02464756192880873 +-0.04659388426448024 -0.17201107548284844 0.2915033198781547 -0.015591899360298863 +0.009036633084710682 -0.1341575144547239 0.019175360637514273 -0.18569107050855987 +-0.20463856037449624 -0.06569305943665607 -0.00573498711396702 0.17279781200563105 +0.2915033198781548 0.12589144309173594 0.2038908532111377 0.10759621977995684 +-0.0081118805538351 -0.12682104395161392 0.0 -0.04005091355481479 +0.05686540486530175 -0.21815987304030593 -0.0054919808319310185 -0.015591899360298861 +0.20389085321113765 0.046119632391112525 -0.023661259543489806 0.12659156148719342 +-0.0009247525308755901 0.0 0.0 0.00862616169391606 +-0.013757482124805886 -0.0012530041470060842 -0.012011718707191633 0.06543993933482327 +-0.030435512225563812 -0.0012153632141345128 -0.023564393628269467 0.0070898082655450065 +0.0 0.0 0.0 -0.007440325201395759 +-0.0056032441714893595 -0.02184100474832993 0.00752180849002781 0.13546537473406245 +0.027929238908130524 0.0008419481083739343 -0.03460009934206196 0.0 +0.0 0.0 0.0 -0.0011858364925203188 +0.023472677244202645 -0.09209554489215928 0.004489910217163825 -0.007588141668912529 +0.0387907304255524 0.00037341510576057325 0.0 0.0 +0.0 2.981555974335137e-19 1.6263032587282567e-18 0.21092422953693032 +-0.3707709326922259 0.10549062360576059 -0.011800125866443648 0.30337672860874937 +-0.016832117014634333 -0.03891315405408189 0.07926761281407119 -0.08989872424546172 +0.005967361389171799 -0.18825862469915539 -0.011921388645600002 -0.09820019376829639 +-0.5671478094507957 -0.26688764689917965 0.013765441861168253 0.18044199717998974 +0.0792676128140713 -0.02769124932343956 0.363256956239658 0.16578630275979234 +-0.002103487895386609 -0.023903549967934812 0.0 -0.11272403576863398 +0.031007620341604272 -0.20877960488149022 -0.0019653159947245966 -0.08989872424546172 +0.363256956239658 0.06660440337752145 0.020364511893460804 -0.019986595656711126 +-0.003863873493785197 0.0 0.0 0.010222154121864457 +-0.019489783120777635 0.00556552320184083 0.002763492781732964 0.15929142711922284 +-0.007346360798114465 -0.0005994637136100558 -0.05198511751948153 -0.0031410801069935226 +0.0 0.0 0.0 -0.004928850375503207 +-0.02987572456254215 -0.16921921415402547 -0.005653561307333152 0.08218804339429678 +0.05453306266234019 -0.00016150058030916636 -0.0067966336866103 0.0 +0.0 0.0 -7.589415207398531e-18 -0.0052933037463612186 +-0.002343243622879937 -0.05362095322837582 0.0028900685256001883 0.0010661821403148424 +0.015350691555493831 0.0007609642939192382 0.0 0.0 +0.0 9.215718466126788e-19 1.0977546996415732e-18 -0.03773474577165913 +-0.02310147125155787 0.17635685115528432 -0.012582848042162729 0.3474239762614125 +0.04544516240840257 0.07664789982574102 -0.056166141562513316 -0.0864581269098226 +-0.0005086272083758749 -0.12210999598247305 -0.037385711506185484 0.11675100473512534 +-0.5408840214120888 -0.3179803893784919 0.007260546899679307 -0.02656515503547901 +-0.056166141562513344 -0.08905975541168579 0.17762706517243085 0.12641496581660355 +0.003060655672056725 0.06285325533915664 0.0 -0.07901625896346623 +-0.04559939799389408 0.06128422213240237 0.005322301142483427 -0.08645812690982262 +0.17762706517243085 0.01241185558594478 0.04590696493157602 -0.08570073724635309 +-0.0025520284636808494 0.0 0.0 -0.0022326956548346993 +0.0012117701569124635 0.008545819400079112 0.006615486652990925 0.1521940866186995 +0.025234886100433285 0.0010106838099596515 -0.044783178695823565 -0.012282318993715333 +0.0 0.0 0.0 0.006704452393238358 +-0.026533080959026983 -0.15916535156225708 -0.0051570590042927 -0.04129762647569124 +0.0434755752349911 -0.0011868686810176951 0.021248552289721334 0.0 +0.0 0.0 0.0 -0.004471756738403644 +-0.03352377376280258 0.050468705003413813 -0.0014584276486982292 0.01015566677137347 +-0.016354973316871437 0.0001761848710580521 0.0 0.0 +0.0 8.673617379884035e-19 -4.336808689942018e-19 -0.4889611487601232 +0.09721378717152276 0.1399650636624571 -0.03722269407816935 0.2403440405987936 +0.11348742350085039 0.4123132489343822 -0.04104764560900944 -0.0535069367526345 +-0.004878380149927329 -0.08232430478464588 -0.13710610950099758 0.3702425920479644 +-0.39323636213306457 -0.2455047485904144 0.022197811462326487 -0.1995636122622452 +-0.04104764560900945 -0.28118283663627863 0.2156092969606337 0.09579992506338418 +0.0020946486446210277 0.1647791580413677 0.0 0.11871855671215885 +-0.12386877385210412 0.19018537317173395 0.015024882615842856 -0.05350693675263453 +0.21560929696063377 -0.13113041229810363 0.15632373394994536 -0.17650340574929 +0.002783731505306343 0.0 0.0 -0.017214791812904317 +0.00511897917828345 0.006888409866165806 0.037731321286545216 0.11908978277381087 +0.07796180892052812 0.004543636165852218 -0.03179693910355048 -0.035182773532316226 +0.0 0.0 0.0 0.01372781238430515 +-0.015252521524260078 -0.11823404461632059 -0.025258467134383202 -0.10448463592538092 +0.0317545816668359 -0.0034540294738284634 0.04763065332006679 0.0 +0.0 0.0 -5.421010862427522e-18 0.0034869794285991565 +-0.0761017119946649 0.13671035692308858 -0.012472854152162018 0.03531591320596983 +-0.0427500636427333 -0.00108960669202373 0.0 0.0 +0.0 1.0842021724855044e-19 -4.336808689942018e-19 -0.9787545972032043 +-0.3239848918249886 -0.384061571797161 0.0006273562233446023 0.042574341312726166 +0.35566721454974426 0.5664413482688222 0.365032537433998 0.4375685085497955 +-0.06446633613446526 0.09941825207042207 -0.26946513709469894 1.204926533242403 +-0.6745016304687501 -0.11317721989109562 0.05106804494104287 -0.35937091347273287 +0.365032537433998 -0.9237436966061245 0.4588923335081165 -0.13889045530981467 +0.06314389699984681 0.022271531840028252 0.0 -0.22617193603919888 +-0.43210932281102943 0.32786181948911775 -0.05169540116438754 0.4375685085497955 +0.4588923335081162 0.3573023483373025 0.2077645500887394 -0.09375033662956347 +0.0013224391346184083 0.0 0.0 -0.05086847445716819 +-0.010112847477900443 -0.013509799870837377 0.0042510239265827025 0.017377294827711456 +0.2757855888610842 0.004114562959988105 0.02102113684333167 -0.07151492259009194 +0.0 0.0 0.0 0.0483865652614375 +-0.028096569396017297 0.03974996347191976 -0.053162693585663875 -0.15135841373982767 +-0.01736287392055211 -0.005845901784911567 0.03795254876904768 0.0 +0.0 0.0 -4.336808689942018e-18 0.0024819091957306565 +-0.21856110504874665 0.19459175543136512 0.04891166965908121 0.07524078818271449 +-0.031973817464449036 0.0017313388249234653 0.0 0.0 +0.0 3.469446951953614e-18 2.168404344971009e-18 -0.5123071184298934 +-0.47244937075147303 -0.4485695461658932 0.06698390951990199 -0.21255121512961675 +0.40866370690807613 -0.05413422983892884 0.10741683331747505 0.011001037616097678 +0.004406276498018386 0.057858639873637166 -0.06675493982697839 1.1094321156077374 +-0.4957717321346541 0.2515774005785203 -0.064299067889454 0.009399416660282857 +0.107416833317475 -0.18568841900161273 0.03687939862653762 -0.07855867155869065 +0.01886595588432299 -0.04773651885902325 0.0 -0.5971249971778441 +-0.3385999263545102 0.06709173152811695 -0.002684841630447946 0.01100103761609763 +0.03687939862653762 0.23982264884054155 0.060009721166068716 0.037668683953030344 +-0.023272232382341385 0.0 0.0 -0.009183853976622343 +-0.018967160870840757 -0.014827156804433 -0.002517573385436731 -0.11268694526870565 +0.1308353762657708 -0.0003192176801705142 0.029467193167588515 -0.03079393368802924 +0.0 0.0 0.0 0.040626900952477105 +-0.01638934640044168 0.1131329630591947 0.0011551708896072018 0.026658605101446506 +-0.026879636086560726 -0.0023547305952286764 -0.010023586360092047 0.0 +0.0 0.0 1.8431436932253575e-18 -0.03144304697585479 +-0.13919856981337717 -0.031670948500311116 0.0013624024958295271 0.03184772073724805 +0.011371672753654416 0.002673948275399206 0.0 0.0 +0.0 2.168404344971009e-18 -4.0115480381963664e-18 0.06612602947830401 +-0.13408357726236605 0.0063916793684791265 -0.004174630603461285 -0.10046023521825098 +0.10037458293555077 -0.011991799639375161 0.026666743944891 -0.017392716984576805 +0.0009483489931194377 0.035379033316546155 -0.02557948682086205 0.21704081767697797 +-0.03223189459792459 0.1192693671211052 -0.01818339363495457 0.08739033242547298 +0.026666743944890988 -0.031352398675365226 -0.004647504028613032 -0.03861672123486324 +0.0022246054599887382 -0.04459028013304855 0.0 -0.28316684715528195 +-0.09197886365008814 -0.07663383186020205 0.02235802423841586 -0.017392716984576805 +-0.004647504028613013 0.04334419831474038 0.027418085439959147 0.04485886637364121 +-0.003172954453108171 0.0 0.0 0.0022107466837554203 +-0.004478616329608964 0.0008395878736712006 -0.00023164589455710411 -0.040710695562414544 +0.03196914248401942 -0.00014452865198504678 0.012709910641081021 -0.009553944545353016 +0.0 0.0 0.0 0.007041308635303372 +-0.0006608867887547018 0.042601595344613816 -0.0006825622493684181 0.038965147907171704 +-0.01204226722120532 -0.00041490933421518264 -0.013241990534857655 0.0 +0.0 0.0 1.3552527156068805e-19 -0.009252055319058792 +-0.03361964310857237 -0.03965381356644973 0.0009142081439255237 0.009148509024805865 +0.013138844339471623 0.0005594379862002251 0.0 0.0 +0.0 4.0657581468206416e-19 -4.336808689942018e-19 0.018424426903145115 +-0.04822577894564467 0.03379274288018986 -0.0006165001361045644 -0.06730049382843745 +0.047076374427048026 -0.006432627263769954 0.021559035000753672 -0.016400025895613055 +0.00023164791395608787 0.02615639549238151 -0.015999360211940112 0.04725582313021838 +0.009813202141714526 0.06841155726788022 -0.0016887202898651335 0.09055105648307618 +0.021559035000753665 -0.015903424454853154 -0.005165698113101493 -0.02780837245742648 +0.0008625233255317637 -0.04903059863652722 0.0 -0.0656802500333635 +-0.047214071852393735 -0.0903604125232609 0.0023052204259696903 -0.016400025895613058 +-0.005165698113101484 0.022336051718623122 0.01716937941556259 0.049476284205753465 +-0.0010941712394878541 0.0 0.0 0.0007341891727642104 +-0.0022668957750906216 0.0016162741567648884 -0.0003318488570148721 -0.029794836033016976 +0.019795986412434588 -9.196266488152355e-05 0.00946790827206189 -0.006129949080392073 +0.0 0.0 0.0 0.0021904330080928444 +0.0005373823130289943 0.03192146051189129 -0.0005358851701236089 0.04550154614961968 +-0.008851770809831308 -0.00022631730692183102 -0.015118311257079946 0.0 +0.0 0.0 0.0 -0.0029246221808570515 +-0.021496887606185976 -0.04596077635002764 0.0008677340271384765 0.005660544214409922 +0.014970575650483452 0.0003182799718033551 0.0 0.0 +0.0 4.336808689942018e-19 -1.6805133673525319e-18 0.030303553998030038 +-0.07318400589078175 0.06653345817013626 0.0020229657997943183 -0.06090343678319292 +0.03633824396843052 -0.023870926734260084 0.05162497089002807 -0.0501334322745232 +-0.00024194327693908722 0.03704524381058287 -0.011956659465649624 0.03182623013386547 +0.014914675529115854 0.0541107367671751 -7.89373921265109e-05 0.12144600362838892 +0.0516249708900281 -0.015922805679012313 -0.00974897741601436 -0.03798286904569487 +0.0009411531289055895 -0.11976106664723964 0.0 -0.06212978413189552 +-0.02849280674647333 -0.12034357184574877 -0.0019440284076678053 -0.050133432274523194 +-0.009748977416014333 0.0397937324132724 0.011464088842794762 0.11990167568603183 +-0.0006992098519665009 0.0 0.0 0.0011326094277255396 +-0.003048577109228773 0.0026268604665818443 -0.002254613713750406 -0.02630236430974861 +0.01132342733091074 -0.0003575751350677202 0.011378290153305744 -0.004966529767790208 +0.0 0.0 0.0 0.0014734038442590423 +0.0006441327541354389 0.034747041290811416 -0.000783585933405252 0.07086728763999531 +-0.009972277515601986 -0.0001591231071384078 -0.03050909542164277 0.0 +0.0 0.0 3.469446951953614e-18 -0.002606013271984583 +-0.020338883756490417 -0.07241540499186169 0.0030381996471556608 0.0034787835005469195 +0.0302263921914958 0.0005166982422061305 0.0 0.0 +0.0 -3.469446951953614e-18 -6.938893903907228e-18 0.34915801611356034 +-0.4665848450780806 0.7798688464036531 0.03784073540637687 -0.12048274771437048 +0.07075997396668451 -0.3252870893793003 0.4149598741880525 -0.7297354141291299 +-0.016729402656217876 -0.03118288996402111 0.24754826285340933 -0.2604096891483542 +0.2518898010194721 0.03183072889007456 -0.0439786052109064 0.40165935204421005 +0.41495987418805236 0.27633249482736655 -0.24214082360345773 0.06598527726967315 +0.043738894286976075 -0.6358737478582868 0.0 -0.08874832696520613 +0.1049210652282497 -0.33430725736563793 0.006137869804529527 -0.7297354141291299 +-0.24214082360345773 0.048954594551933725 -0.34810849111134595 0.5751983376923675 +-0.027009491630758217 0.0 0.0 0.011534619447858406 +-0.016109530284144167 0.025262892188864457 -0.03759879212943777 0.006152140155620328 +-0.11638515407104444 -0.00623135475382295 0.003949609652352221 0.040493058005778806 +0.0 0.0 0.0 -0.006916036264484927 +0.007765812549219996 0.08343750390378764 0.043037452082000806 0.2144055816796061 +0.011687786963799095 0.009056327537718331 -0.14465659207831683 0.0 +0.0 0.0 0.0 -0.004618583183373454 +-0.058803314501034876 -0.2818982853969704 -0.005438659952563037 -0.072398501475463 +0.13044137832501074 -0.0028249727838953404 0.0 0.0 +0.0 6.938893903907228e-18 0.0 0.5442479904858996 +-0.05062474951717022 1.679526226438822 -0.04324458016734589 0.5894305222958421 +-1.3889914496954663 -0.21896090110659927 -0.3643351246708823 -0.9497908123096921 +0.06183201636541267 -0.513649037236004 1.00378229220141 -4.557471274864167 +2.9924583811736123 -0.4001079037208122 -0.18092620330033926 1.2685679955417395 +-0.36433512467088225 4.2811387800368 -2.7503175575701544 0.4460663471806452 +-0.37256701834448597 0.4647954149307736 4.013223284378266 1.1921699084234492 +-1.0669857114058576 0.22417078346768515 -0.9497908123096923 -2.7503175575701535 +-4.0621778789302 -0.6018784985395822 0.03782529834351642 0.3107350019790736 +0.0796265752404019 -0.07601939006540184 0.22051409036067376 0.05997398282356375 +0.33412262645113905 -0.8440614173121033 -0.006741455058556611 -0.13361435039217368 +0.2501326369988979 -0.24011581636037393 0.1695217667163889 -0.5582476323318211 +0.1371873090133634 0.49178737371349024 0.14252890441303404 0.03310323694894923 +-0.08144967724252994 0.16048924111997187 1.141443186842057 -0.632694247683453 +-0.1971612918369271 -0.2901841756183209 0.0365323798889087 -0.02636178189039251 +0.21896090110659927 0.3643351246708823 0.9497908123096921 -0.06183201636541267 +0.513649037236004 -1.00378229220141 -4.2811387800368 2.7503175575701544 +-0.4460663471806452 0.37256701834448597 -0.4647954149307736 4.0621778789302 +0.6018784985395822 -0.03782529834351642 -0.3107350019790736 -0.0016260831589915078 +0.02273152957686357 0.0036548106603494154 -0.11955032542596511 0.07437924943696672 +0.12117640858495665 -8.326672684688674e-17 -8.326672684688674e-17 -1.6653345369377348e-16 +0.0 1.3877787807814457e-16 0.0 9.71445146547012e-17 +-1.3877787807814457e-17 2.7755575615628914e-17 5.551115123125783e-17 0.0 +-1.1102230246251565e-16 5.551115123125783e-17 4.85722573273506e-17 5.551115123125783e-17 +-5.551115123125783e-17 5.551115123125783e-17 -6.938893903907228e-18 0.12250688173487695 +-0.04242787820835337 -0.03750225255242311 -0.042427878208353385 0.07990701739067266 +-0.060789976746091615 -0.037502252552423114 -0.060789976746091615 0.0949486589647062 +-0.25107887219575875 -0.1279349245872183 -0.08693161820990583 -0.12793492458721836 +-0.17459582278663321 0.11671725545343216 -0.08693161820990586 0.11671725545343214 +-0.2670562423226008 +random_seed2_clamped +17 4 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.0 0.0 0.0 -3.6447727384025526 +4.9033174665641255 0.570081281403931 -2.0156566324787435 3.4253354409208576 +0.014680392969503563 -2.466970974787889 6.159577817732747 -0.5739077590291796 +-2.463847906303318 6.831852897073422 -0.6064615036634324 1.0117646994543623 +4.985323875632146 -1.3847894007490036 -0.7131763126433182 9.475738011499386 +-1.6311965103583848 4.626446951153119 3.4514719604540334 -1.8774533637725768 +2.777130402145817 10.913959240058679 -2.7763918193264057 5.856015876557384 +1.1897858430465023 -1.6567034629298034 6.735465119540195 11.17972877734023 +-3.8243484631522087 5.455838171398983 -0.2957275886473031 -1.4070722395811543 +9.790248886031112 10.688912194151396 -4.667833730166021 3.4264309822440366 +-1.0407947205450785 -1.096428208050072 11.200374978869748 9.941749853955525 +-5.280050370214524 1.0070008837669115 -1.3018590929580818 -0.8533838133290437 +11.188226839879016 9.204579863382383 -5.654785676718779 -0.727665558850784 +-1.2257818995662115 -0.4158782686889228 10.701260214771843 8.365839156824883 +-5.775400552530161 -0.6023211518378421 -1.7212657172319483 0.0185606849322824 +10.76615169355086 7.053617697904098 -5.642294591265839 0.9249203334794562 +-2.7636956871308436 0.3992605222055327 11.834850024233397 4.941867612661987 +-5.337780187444012 2.363791827442628 -3.992284196368902 0.5201840624833727 +13.51742723731092 2.054440116259738 -5.06992120896916 2.710511389561867 +-5.055122250751779 0.24416034155967573 14.857566307129737 -1.128469881272099 +-5.139832814510563 1.3594873478895861 -5.024838814052263 -0.5549305120956095 +14.976444935750234 -3.8436376661905314 -5.8216685332693485 -1.0832287912193712 +-3.823078406202524 -1.7847379035234832 13.686509746370316 -5.414256886984885 +-7.205044309393928 -2.8837055231669506 -1.3784165305146665 -2.6747332929170393 +11.69186046550526 -5.610829230173248 -9.087638787235772 -3.354306042909719 +0.7177415280931374 -3.4345725158647844 -0.5939872226118298 0.799091776796214 +0.09290592893112075 0.05542465235822736 -0.07456285631280567 0.9956748909003609 +-0.6910176514561054 0.14786130387351334 -0.7075532772822928 -0.7083972774370032 +0.05615350712122672 0.7035766347440965 -2.4118582633154904 -0.038562386052785086 +-0.023301735650408724 -0.06456573033450709 -0.00010627300033573539 4.0985200434591994e-05 +-0.00354281269338471 -0.011494572857060875 -0.01410805066770774 -0.09937934130681679 +-0.0011351432618145092 -0.0013682045559063917 -0.003753400590029543 0.002829803586072044 +-0.009265555371640859 -0.036204661918975756 0.0010256618782865552 0.0013840586734125068 +0.0069872969624069775 0.024211898275748718 0.02453118345387382 0.10172948547160164 +-0.0002864626942798987 0.0004350664771184329 0.0002456506597204172 0.0093913078659775 +0.0169887221128782 0.04496548906493149 -0.00010421670038908426 -4.924335068480501e-05 +-0.0007996032086248842 -0.0014579889026660638 0.02121365980099949 -0.01191858311340284 +0.00016708468514424653 0.0008134151276099236 -0.00030578141282378013 -0.03168135537724297 +0.1482264541928432 -0.2466178173057207 -0.002584689999801496 0.012110531317364115 +-0.02154524876851062 -0.24319459888043904 0.505901680421087 -0.9224117054471054 +-0.011312348767935215 -0.020194177862980722 0.03490697115281538 0.022734839804793294 +-0.515048948653156 1.4478859512888778 -0.000765684394639865 0.023466130488709408 +-0.011295920876730592 0.16325117430049643 -0.10033033815806425 0.3183330203349477 +0.0038207379605544323 0.007971583009146424 -0.06589094688464908 0.10824445085468146 +-0.03936851602802818 -0.4442717447999951 -0.008803243010480064 0.001896885331935676 +0.019005944810831177 0.013534863718648177 -0.005980182806545978 -0.09987245015635775 +-0.0005957546736182137 0.0006817777709247977 0.0056384271664657315 0.021000134609420466 +0.008383671125080887 -0.0527098311616607 0.00111761502536354 0.0006605441199968708 +-0.0007817784616508117 0.07787004846641421 0.03211961360858462 -0.11666617094454389 +0.004473242082737021 0.001529942763762178 -0.0048804456296243914 0.07359837875978273 +0.0019345525937101288 -0.059842421441294646 -0.004291368847061827 -0.0037549202435144743 +0.008085188147349154 -0.07015255349334798 -0.0698410076337212 0.1123122105372037 +0.00037204375959608293 -0.0003580016026119553 -0.0027244350635727 -0.12012344467849301 +0.017944797660215648 0.1292343012328183 0.0034638274939586405 0.003250146449397162 +-0.003877514766008755 -0.5939872226118299 0.7990917767962141 0.09290592893112076 +0.6910176514561054 -0.1478613038735134 0.7075532772822929 0.017900337742736236 +-0.009522557967103256 0.01465027871724281 0.0007395416571552227 0.0014481251370740814 +-0.032574456990070354 -0.017900337742736236 0.009522557967103256 -0.01465027871724281 +0.0011723382211885935 0.0010917499552737877 0.03413282682850152 -0.01794455527050397 +-0.025135458441448377 -0.002676254753711694 -0.0009633580445627446 -0.024871039659445534 +0.009522557967103254 0.01794455527050397 0.025135458441448377 -0.0018559382941646648 +-0.0008756028933032366 0.016338086130530536 0.0 4.421752776773217e-05 +0.03475835184729067 0.02090939749558112 0.00022381638740752132 -0.014650278717242806 +0.02513545844144838 -4.421752776773217e-05 -0.03305021890614291 -0.017652487807130492 +-0.00029673532788535657 0.0 0.0 0.0008403740933580622 +-0.0003634247144549612 0.0004596828114313658 -0.0007395416571552243 0.002676254753711695 +-0.03475835184729067 -0.00018438401052662352 6.564269662682511e-06 0.010373780990824309 +0.0 0.0 0.0 -0.0007847361361833483 +-0.0008081298620035509 -0.001448125137074081 0.0009633580445627446 -0.020909397495581118 +0.00019832546565944282 0.00020101449571749864 0.006477985533837461 0.0 +0.0 0.0 -6.098637220230962e-20 -5.563795717471301e-05 +0.03257445699007036 0.024871039659445534 -0.00022381638740752116 -0.010647049133229983 +-0.005940742487857157 -1.6630485190876078e-05 0.0 0.0 +0.0 -2.168404344971009e-19 -4.336808689942018e-19 0.0237870659635196 +-0.032977548929474125 0.028331182096972274 -0.001411276385766205 -0.009049236772775275 +-0.07611724480860457 -0.005886728220783366 0.02345499096237087 -0.013680903379729463 +0.001071657853881209 0.01196798071130403 0.05589112880695138 -0.055977308616869614 +-0.1050327253971868 0.006059889499328659 -0.0020988055103776013 -0.0333997650099402 +0.02345499096237087 0.03803275334636565 0.07989726695573843 -0.013374655787802487 +-0.0012170616647425884 -0.0046867467709655036 0.0 0.032190242653350014 +0.07532644047304626 0.022031265323669592 0.0035100818961438045 -0.013680903379729452 +0.07989726695573846 -0.03214602512558228 -0.0523179032522956 -0.000405901603886245 +0.0001454038108613786 0.0 0.0 0.0010323942256382343 +-0.001185414590721016 0.0013127136286993483 0.00023893816457761129 -0.004203951205163994 +-0.04227622156690336 -1.821305736103752e-05 0.003148903217581233 0.014843169927170625 +0.0 0.0 0.0 -0.0020184758516309928 +-0.003823826757432449 0.007957486817501486 0.002974408403680837 -0.004378777516539106 +-0.002696938751142141 0.00036484203622003434 0.0019697223974025667 0.0 +0.0 0.0 1.6263032587282567e-19 0.000986081625992754 +0.041984417980103036 0.017061678879409667 -0.0032133465682584487 -0.014928866249069316 +-0.0004712749929842207 -0.00034662897885900327 0.0 0.0 +0.0 -6.505213034913027e-19 -1.4094628242311558e-18 -0.019882632307291606 +-0.056675212424778326 -0.03341020225723383 -0.0029216208646274614 -0.030066962459262093 +-0.11030161765984353 0.025769360528074974 0.03322022146240746 0.047091105636963296 +-0.0011702567036775934 0.013336154259019038 0.03266252306037309 -0.07821491846147044 +-0.15852090676769437 0.028439011835922436 -6.061359753182279e-05 0.02302630076800407 +0.03322022146240747 0.0401821651151048 0.07862363981195594 -0.01771851601583749 +-0.0031350282619372 -0.03843914887099348 0.0 0.09809755076876206 +0.09929490730760526 -0.022365786646938807 0.0029822344621592885 0.0470911056369633 +0.07862363981195594 -0.06595152564317978 -0.03630420993785982 0.025676617338426626 +0.0043052849656147925 0.0 0.0 -0.0008862383901942497 +-0.0025456801062534746 -0.0013262263565231233 0.0018499630107462538 -0.015064356048119948 +-0.046977004055309676 0.00020965647118413719 0.0052608766981446144 0.013580443670323229 +0.0 0.0 0.0 -0.0037584719219844395 +-0.007681008246098537 0.01809898174795806 0.0012776752622744103 0.02277168825082505 +-0.004643307059581164 0.00038868188772337017 -0.007295507316087248 0.0 +0.0 0.0 -4.607859233063394e-19 0.00464471031217867 +0.05441048885289216 -0.01833955399703857 -0.0031276382730206637 -0.012821843571361489 +0.0088319289088872 -0.0005983383589075108 0.0 0.0 +0.0 -7.589415207398531e-19 -1.0570971181733668e-18 -0.04371594458226972 +-0.0455861690734377 -0.0824670558604612 0.0005497763950969741 -0.02566918462868068 +-0.05247088314189512 0.017946584054194744 0.012365947611030242 0.035375950223497904 +-0.0011308742570929123 0.009966743573673066 0.008380400923097822 -0.047476843769255345 +-0.09186213418911773 0.029624757805771226 0.0032309853142975737 0.07221798912132602 +0.01236594761103024 0.007294678654150544 0.013238494377161793 -0.011875739774306414 +-0.000712160321706257 -0.03745254155995051 0.0 0.09119278835152507 +0.05377417541587957 -0.05914168082050715 -0.0037807617093945467 0.035375950223497904 +0.013238494377161788 -0.025241262708345286 -0.012945252592989832 0.034902807595911835 +0.00184303457879917 0.0 0.0 -0.002019863104185687 +-0.0019555897812038917 -0.0039579257144873635 0.0006204803085806176 -0.011906241789933731 +-0.017469965478019746 0.0002193280255873432 0.0037855947957091754 0.005069264470192548 +0.0 0.0 0.0 -0.0018677949027155446 +-0.0034882169752920517 0.012333030369661643 -9.595705236037429e-05 0.03346506348208052 +-0.0034671444599567278 0.00010907914875913814 -0.010754264924633212 0.0 +0.0 0.0 -1.4230153513872246e-19 0.0038876580069012326 +0.01980836008152203 -0.03377884025033255 -0.0005245232562202443 -0.004204329714989061 +0.01115031273641852 -0.0003284071743464825 0.0 0.0 +0.0 -6.505213034913027e-19 4.811147140404426e-19 -0.028793038690811595 +-0.014761966159613445 -0.055187012399047865 0.0007201178424930932 -0.019301507356662376 +-0.013583831243498146 0.01084645463661685 0.002396018548583203 0.01981106217554996 +-0.0004750584064573749 0.009690564293637162 -0.0008267028074614617 -0.007644166661932247 +-0.014077493132655258 0.021391209672230807 0.000758805508599981 0.07226831110143758 +0.002396018548583206 0.00034948800778170243 0.0008389987554934648 -0.01027851643489873 +-5.181489022800889e-05 -0.03867698249295984 0.0 0.036437205352743834 +0.01671944304818412 -0.06951434685470739 -0.0014789233510930757 0.019811062175549964 +0.000838998755493476 -0.011195942644398549 -0.0009432962148276567 0.03838077539077299 +0.0005268732966853837 0.0 0.0 -0.001407749806935226 +-0.0006882391572566218 -0.002678814599508899 0.00041075641459982037 -0.009515469897924393 +-0.0037741904551942887 0.0001682643505514492 0.0031349024600114293 0.0010360751625602695 +0.0 0.0 0.0 -0.00028458047512689463 +-0.0005447776840909429 0.009334763782989312 -4.664518689372365e-05 0.034611539258795565 +-0.0030636977678604044 1.029579603791102e-05 -0.011657868231966396 0.0 +0.0 0.0 -2.574980159653073e-19 0.0016923302820621164 +0.00520343032040033 -0.034815769541487095 -0.00036411122770609554 -0.0004136431156984173 +0.011693428188725331 -0.00017856014658935992 0.0 0.0 +0.0 -1.951563910473908e-18 8.944667923005412e-19 -0.026292273494661268 +0.007120052480507141 -0.05099182554779783 -0.0005168265843772651 -0.021889056441239292 +0.004321485527511321 0.015445818858044417 -0.009516071029090344 0.03118076337224787 +-0.0002882611290764463 0.0211813982164874 -0.017587024542824983 -0.0034565353449211725 +0.0027463458389754476 0.02453682240360262 -0.00035798464758561636 0.08879996160743793 +-0.00951607102909034 0.00310704733713947 -0.0035853445944689123 -0.02207760759503865 +0.0003195256427211763 -0.07306135760955956 0.0 0.029748808839582433 +-0.006958745594168019 -0.08778376875751412 0.0008748112319628802 0.03118076337224786 +-0.0035853445944689227 -0.018552866195183884 0.017211384330419605 0.07284737058332039 +-3.126451364472921e-05 0.0 0.0 -0.0013618926119334917 +0.0002600315082965358 -0.00255867657385514 0.000991884990834639 -0.01425830596870389 +0.007902041808995678 0.00023519064244628836 0.005421611030499593 -0.0032511780792838455 +0.0 0.0 0.0 -9.283842219253005e-05 +0.00012686949541729434 0.012198492147602128 0.0004097995378136255 0.049402993366741144 +-0.0057965240758022 8.160993210973961e-05 -0.019699377836114848 0.0 +0.0 0.0 -1.2197274440461925e-19 0.0014547310341260235 +-0.003494782720049868 -0.050122979114478106 -0.0014016845286482643 0.004281229187890809 +0.019572380281434713 -0.00031680057455602917 0.0 0.0 +0.0 2.3852447794681098e-18 4.7704895589362195e-18 -0.0943485912758337 +0.1712051901955154 -0.27314538018929807 -0.007405281468707877 -0.05596246225241374 +0.05204801103197858 0.07890277241778929 -0.16168911916642503 0.2419646168170502 +0.0008048282171635897 0.1299886931677273 -0.18653871665545305 -0.1781149801248915 +0.1014912473487715 0.09240557160303098 -0.02291105501768475 0.22238770535145916 +-0.16168911916642506 0.17500793278775204 -0.0979059027543026 -0.1412613218132307 +0.015268262218048969 -0.22956030007286626 0.0 0.2724635714007252 +-0.10174063834785313 -0.19693687577355998 0.030316336486392637 0.2419646168170502 +-0.0979059027543026 -0.2539107052055413 0.1971495307616783 0.21228743627819813 +-0.016073090435212593 0.0 0.0 -0.003559261836974612 +0.0057291296064810645 -0.00969384249876775 0.007693542597784327 -0.07032796400799231 +0.08452925401743351 0.0011345869655533178 0.02768707223229763 -0.03730579043663826 +0.0 0.0 0.0 -0.005281350247086627 +0.0030818327987196725 0.03478106403592634 0.022591529374963577 0.12408950519023962 +-0.03316185500046875 0.0041711911726885886 -0.058421048124082275 0.0 +0.0 0.0 4.553649124439119e-18 0.008840612084061228 +-0.03446098648915359 -0.14932634774189962 -0.03028507197274791 0.04484687913080766 +0.05370566692840076 -0.005305778138241908 0.0 0.0 +0.0 -1.5612511283791264e-17 1.734723475976807e-17 0.04383530414723466 +0.6644696184453086 0.1622781808077832 0.03766292774825636 -0.4659978997602766 +0.8823569551828316 -0.12273807656502395 -0.5027804992788836 -0.4042427976248334 +-0.024893553815720594 0.36695338662155386 -0.7590323574751648 -2.8148290712138433 +1.8657313905253716 0.48891535514486467 -0.17954707329786285 0.49603619936045795 +-0.5027804992788836 2.6398211384260915 -1.767825487771069 -0.2613747283151299 +-0.20059554875746582 0.3976963084118341 0.0 2.7709937670666083 +-0.6963852320254076 -0.2466318407876225 0.1418841455496064 -0.40424279762483306 +-1.767825487771069 -2.517083061861067 0.7078435692595682 -0.1283246354482395 +0.22548910257318663 0.0 0.0 -0.0068611176742008236 +0.03615384058559424 -0.027797102408290184 -0.038467755965419986 -0.34765403333163397 +0.49923570126372935 -0.0012044446717631267 0.09320113233870664 -0.19722042827667507 +0.0 0.0 0.0 -0.09636702811508147 +0.05864610774803296 0.3360092065925492 0.16427881107981382 0.034344404509424276 +-0.10140664126103321 0.009883242278432194 0.011923901877504028 0.0 +0.0 0.0 1.474514954580286e-17 0.10322814578928216 +-0.6958182385273783 -0.26647589928759174 -0.1258110551143939 0.20096807724773721 +-0.022547474651258753 -0.008678797606669036 0.0 0.0 +0.0 -5.204170427930421e-18 6.938893903907228e-18 0.6586707744621367 +0.7728647897005105 0.26975898640160145 0.04820294909378817 -0.4918343927365568 +1.30006628548782 -0.5359326978971128 -0.270084290421627 0.13448381122323197 +0.0416450313824499 0.046946520124608954 -0.500427954593785 -3.1187920609895388 +1.378630525607879 0.4276763819288183 0.19516260042190625 -0.8418312654080353 +-0.27008429042162707 0.4789709225634471 0.38919496216319005 -0.023543656062079775 +-0.041958432693853576 0.2833684338548935 0.0 2.4601212865274014 +-1.329764943822729 0.4408466127995908 -0.24336554951569467 0.1344838112232317 +0.38919496216319005 0.0569617753336657 0.5323639332539494 -0.24926829437068837 +0.00031340131140368674 0.0 0.0 0.033400987941615286 +0.020060737072562167 0.026944725950453233 -0.023309395278067552 -0.1663016536136883 +0.621921374563161 -0.0039162942677510165 0.0265535294630018 -0.1715546511419753 +0.0 0.0 0.0 -0.09671296384862399 +0.04555376330684777 0.1248810061150029 0.0054329483355594595 -0.31252197735135134 +-0.030631697515358353 0.005720506105733273 0.10090208554170188 0.0 +0.0 0.0 7.37257477290143e-18 0.06331197590700843 +-0.5410339280126553 0.44413495699620126 0.017876446942508024 0.170113657351776 +-0.09588489981700105 -0.001804211837982161 0.0 0.0 +0.0 -2.862293735361732e-17 -6.938893903907228e-18 0.546336454846062 +0.4179265021473684 -0.15365782217412774 -0.04828646712760344 -0.021822544324115383 +0.9530005796770602 -0.01040375694894908 -0.14784221172574133 0.01917401095089577 +0.003964943595442624 -0.017249981265135633 -0.28152835289493594 -0.6332712796758102 +-1.1407614456719157 -0.0019251503477304178 0.039324027602835876 -0.3747436790056795 +-0.14784221172574136 0.15430035711236315 0.7515664835087256 0.04894547361458086 +-0.01405688929609634 -0.11289286491856647 0.0 0.08693482482974837 +-0.9564691570023506 0.3095039454048011 0.008962439524767558 0.01917401095089588 +0.7515664835087252 -0.14389660016341407 0.2597325010040962 -0.02754119700904174 +0.010091945700653717 0.0 0.0 0.01980872429114391 +0.019418920719120762 -0.01720611637508878 0.006641435745153548 0.025468806409810203 +0.42410522374840126 -0.0008966859773195128 -0.010114991235407735 -0.1080154819653749 +0.0 0.0 0.0 -0.02286198031262993 +-0.06270316041850976 -0.025123975800493574 0.002634405091017697 -0.06023565103411266 +0.007025698239190631 0.001543735410134175 0.013185260793538961 0.0 +0.0 0.0 6.640738306473715e-19 0.003053256021485989 +-0.4525726250832752 0.09137524515078606 -0.009275840836171246 0.11064951676667348 +-1.665630439569299e-05 -0.0006470494328146906 0.0 0.0 +0.0 -3.821812658011403e-18 -2.168404344971009e-18 0.013586109939472691 +0.15013747509341305 0.009839502796553573 -0.0032579871203494396 0.035968793728572666 +0.41199254289625564 -0.0031823529905236126 -0.002295263367671727 -0.029013513747449343 +-0.00020262383929419607 -0.010459283136921554 -0.07530225905914165 -0.17506525367283815 +-0.8093859311551967 -0.06891940105999445 0.014075409944210292 0.17019892190500274 +-0.0022952633676717217 0.020764896560475 0.05781944764647115 0.011625690678510991 +-0.0018191742466115695 -0.04967498643827017 0.0 0.16147914373336547 +-0.3909634479501415 -0.024982296850137597 -0.010817422823860856 -0.02901351374744931 +0.05781944764647115 -0.017582543569951387 0.08127020023739415 0.03650340755559146 +0.0020217980859057656 0.0 0.0 4.3337476409039885e-05 +0.004131322162604601 0.0030181371052964198 -0.0007069564750931882 0.01997392744541358 +0.13123094694604534 1.486372621921524e-06 -0.004883142623169272 -0.03354119714365435 +0.0 0.0 0.0 -0.005854444698953862 +-0.025599137296835522 -0.01871881246343704 -1.8520648113952757e-05 0.05252349385917934 +0.004715249112316521 0.000300792645112913 -0.014274256818856008 0.0 +0.0 0.0 -1.5924219408380846e-19 0.00581110722254481 +-0.13046419000131979 -0.05730605698643627 0.000725477123207138 0.032735792197207736 +0.016290686938947536 -0.00030227901773483695 0.0 0.0 +0.0 -1.0842021724855044e-19 1.0842021724855044e-19 0.012688861899489009 +-0.003264630931681344 0.026514520899970813 0.000772344026355931 0.016245159958390026 +0.12818369917964484 -0.009506508908965396 0.005559894299353071 0.002498992847478532 +0.000403339090061083 -0.0034510058035265995 -0.043913831770177485 -0.029323505057824432 +-0.05179850070762792 -0.01818090198928628 0.0014109761480551727 0.08244766566955639 +0.005559894299353071 0.00855860849734943 -0.00602094693884323 0.0030911065458919863 +-0.00041971216533616905 -0.025273092998705483 0.0 0.01663464315833542 +-0.13515486829190382 -0.06741998170827028 -0.0021833201744111035 0.002498992847478511 +-0.006020946938843231 0.000947900411615968 0.0442767318584462 0.02482705268293666 +1.6373075275085346e-05 0.0 0.0 0.000624336983413958 +-0.0002222902577453835 0.000882910094205799 -0.0005697201870617347 0.0065552113107752875 +0.05388466805450969 -0.00010673670978695508 -0.001425114553471865 -0.01535017715300628 +0.0 0.0 0.0 -0.0011148965378062182 +-0.0015872538632950165 -0.005785876821468472 0.0004081980985563962 0.030916574152678802 +0.0015518297008053818 0.00010611692253665499 -0.008992849753795702 0.0 +0.0 0.0 -3.015437292225309e-19 0.0004905595543922586 +-0.05288144012050318 -0.032772679231286225 0.00016152208850533906 0.015379477790385381 +0.008894610123079799 6.197872503009073e-07 0.0 0.0 +0.0 -7.724940478959219e-19 -2.710505431213761e-19 0.024839048587857168 +-0.020513910957078232 -0.015799967876921003 0.0006064255445168901 0.002898532772460814 +0.08774541314633753 -0.015332539678891774 0.014954016657725163 0.01330097502944247 +0.0006140525250136228 0.0031093146268756523 -0.05194276144590428 -0.02554075916995365 +0.03677387693247568 -0.005006043445213916 -0.0009285692795871162 0.049412237277699264 +0.01495401665772516 0.016982150672604224 -0.03075292999363245 -0.0035379891802994703 +-0.00032038848015528316 -0.017481512687219248 0.0 0.000701710582096485 +-0.09007188844533368 -0.043980007682101974 0.0003221437350702262 0.013300975029442472 +-0.030752929993632435 -0.001649610993712453 0.051157223759463716 0.018562151960619735 +-0.00029366404485833917 0.0 0.0 0.0010333080868224927 +-0.0007940988139345231 -0.0006890600893555826 -0.0010097646345779733 0.0019149368993219285 +0.04579515658688747 -0.00016081199828149692 0.0006235382456840936 -0.014953204854770077 +0.0 0.0 0.0 -0.0009856583340320385 +0.0014992268486958404 0.0005524730310657838 0.0013482814449232848 0.019152954999165313 +-0.00025331420779803175 0.0002051678236807252 -0.006540514911394462 0.0 +0.0 0.0 -2.0328790734103208e-19 -4.764975279045576e-05 +-0.04383158137616005 -0.024139144278993784 -0.00033851681034531144 0.015246425325217031 +0.005802282336172805 -4.435582539923002e-05 0.0 0.0 +0.0 1.8973538018496328e-19 0.0 0.018249046366657716 +-0.08747352531656881 -0.03535802561094302 -0.001876448935630487 -0.02168709576647724 +0.11833745442925117 -0.002916506687765941 0.07251950865884364 0.022057050581500547 +0.002435644926770675 0.026195700275934427 -0.08046441059620722 -0.07981910104586043 +0.13659400300281632 0.009219085727799267 -0.004775203253963895 0.03627056011669298 +0.07251950865884363 0.0628369503732562 -0.10584107300918388 -0.02915997839901609 +-0.0020002210528837737 0.012478727642710005 0.0 0.061570054679202714 +-0.1197830373407718 -0.020768264166604072 0.006651652189594381 0.022057050581500547 +-0.10584107300918393 -0.05992044368549026 0.07717037001968197 -0.005075665110493715 +-0.00043542387388689936 0.0 0.0 0.001088994703813613 +-0.003000472227528571 -0.0015216006254300811 0.001262396410616862 -0.005681096547499797 +0.06862581358130809 0.00014919407537100063 0.006954105770775109 -0.02242870075077264 +0.0 0.0 0.0 -0.002908610048287412 +0.005135177236439994 0.018577781139601585 0.005095591734119179 0.0022061122059843516 +-0.005537345076620452 0.0005671597507991184 -0.0010668416531003713 0.0 +0.0 0.0 -1.4365678785432934e-18 0.001819615344473795 +-0.06639469298334688 -0.018789047429473738 -0.006357988144736042 0.022713868780826527 +-0.0008268444344459672 -0.0007163538261701248 0.0 0.0 +0.0 4.7704895589362195e-18 0.0 -0.14425876062182616 +-0.18021053837708703 -0.007347372070668781 -0.013761262970291111 -0.07723971889134687 +0.15444540630289022 0.1471752673095921 0.10769102971824339 -0.014709678510831766 +-0.006132606102221404 0.05121133306199304 -0.04706720889941615 -0.11259186087358547 +0.19736793969828464 0.07218212208899923 0.0015069862870106545 -0.04886605830086674 +0.10769102971824338 0.049754910500329264 -0.09152686668910076 -0.06837995249235894 +-0.005048972238468938 0.06947373979904485 0.0 0.2568506214954116 +-0.1479561499738832 0.045540499797050674 0.012254276683280453 -0.014709678510831756 +-0.09152686668910077 -0.19693017780992136 0.04784731644279157 -0.05324340689414568 +0.011181578340690345 0.0 0.0 -0.005304445012525014 +-0.0083481257422264 -0.00040147591609541967 0.011325618043520436 -0.04302214368998314 +0.07078577995420124 0.001154020979838099 0.017324401395261042 -0.018508948851125757 +0.0 0.0 0.0 -0.005588885440916505 +0.009348399142522477 0.05104401861541244 0.0004932347658731168 -0.040464834686556955 +-0.015561482499843191 0.0004648572774322347 0.014439328416723747 0.0 +0.0 0.0 -7.386127300057499e-19 0.010893330453441517 +-0.07398099570668303 0.036387330658156726 -0.011818852809393553 0.018379289375501727 +-0.016166812731128577 -0.0016188782572703429 0.0 0.0 +0.0 1.9922214919421144e-18 -4.445228907190568e-18 -0.26146891667369365 +-0.10585073217606072 0.02279352796459731 0.0020244063009696852 -0.10688930953281232 +0.06494744102398961 0.11429364936410157 -0.0018402975421826684 -0.008083849453765543 +-0.008122809078719824 0.0503588742251214 0.003461430333006694 -0.04954623474139158 +0.10679953743135866 0.12840151987807563 0.005149819379353181 -0.1319913173601851 +-0.0018402975421826628 -0.00020867575893768463 -0.015272670742257898 -0.05354892012322389 +0.00024125098762958611 0.05482666290675112 0.3110151514150853 -0.0656839226234916 +0.11633058892779936 -0.007174225680322873 -0.008083849453765547 -0.015272670742257896 +-0.1140849736051639 -0.0018972108442776392 -0.05255181200819123 0.007881558091090242 +-0.011640598739005359 -0.004029831709667379 0.0011349938813794798 0.004108199801251721 +-0.06002156738571668 0.017836606180700022 0.0011723601619767939 0.01717200983378009 +-0.002562609977064793 -0.001378784412776446 0.003528965022416364 0.055677976470819276 +-0.00010084714088424214 -0.06308718203365368 -0.017051496581981233 6.448341527480793e-05 +0.017900227686775072 0.013019383151781791 -0.01788023212457345 0.06251757756114025 +-0.004007352660367471 0.002371292261699699 -0.018301952852060207 -0.001236843577251609 +-0.11429364936410157 0.0018402975421826684 0.008083849453765543 0.008122809078719824 +-0.0503588742251214 -0.003461430333006694 0.00020867575893768463 0.015272670742257898 +0.05354892012322389 -0.00024125098762958611 -0.05482666290675112 0.1140849736051639 +0.0018972108442776392 0.05255181200819123 -0.007881558091090242 -0.00398994450235079 +0.0006108571606305846 0.00022315390698107642 -0.00018254040306441108 0.0005756262830125662 +0.004172484905415194 -8.326672684688674e-17 1.2836953722228372e-16 1.1102230246251565e-16 +5.551115123125783e-17 -7.632783294297951e-17 8.326672684688674e-17 1.4051260155412137e-16 +-9.540979117872439e-18 -6.938893903907228e-18 -1.942890293094024e-16 1.1102230246251565e-16 +-5.551115123125783e-17 0.0 -1.5959455978986625e-16 -2.7755575615628914e-17 +-2.7755575615628914e-17 5.551115123125783e-17 -2.498001805406602e-16 0.052735271974320784 +0.03867678956691126 0.004496733876043553 0.038676789566911264 0.029452874342695855 +-0.006049461884023709 0.004496733876043559 -0.006049461884023713 0.08078147215970771 +-0.05381932812431613 -0.010524467557897859 0.0503622064540425 -0.010524467557897847 +-0.10075257712844791 -0.010776311569683266 0.05036220645404248 -0.010776311569683259 +-0.05143721932212913 +random_seed0_articulated +17 4 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.625 0.625 0.625 0.625 +0.0 0.0 0.0 2.1570782939422584 +3.1874067295537865 -3.914965652090791 2.1195021167184045 1.416303461094133 +-2.508824673768325 4.42739671695572 1.207218662354192 -3.992643615423291 +5.4198083940752655 1.4496841076982871 -4.96392803002027 6.369548892463389 +-1.2056403168565557 -4.019784170764922 9.060625831909903 0.2082872339836996 +-6.957230064382481 6.17441536877443 -2.9224805666482023 -2.900781929034208 +12.236865105457134 -1.5687175168386394 -8.212654259042717 4.576219249983086 +-2.859060583411729 -1.4054360108922668 14.518905142286181 -2.9465700016899974 +-8.670294855412504 3.005886038095236 -1.5188528743510583 -0.15743215336375185 +15.99996558805522 -3.2888055789094897 -8.483032569708369 2.039403538817526 +0.4064238182137387 0.7016495084478176 17.18802252609693 -2.5422983482499 +-7.9197971194011165 1.968244299772964 1.820028355135734 1.0353381147664416 +18.70983960310864 -1.1792765087821593 -7.2201005897919845 2.757128950088474 +2.143327157011945 1.0708288803041144 20.984645409487424 0.13764657121395213 +-6.474780356485796 4.499843736338568 1.8522180361494083 1.3106169621739878 +24.028925878156485 0.9508114263339376 -5.59856837178418 5.3557028794839345 +0.82651047321183 1.6088910256010536 27.474069611389773 1.2296670493387998 +-4.413285730220472 5.329587685556454 0.16064335464608898 2.1433207381288115 +30.762808044519637 1.3063268104951 -2.800011930794789 4.834197906330568 +0.19630840523580856 2.9066670957670295 33.403213095932976 1.591466609096899 +-0.8374868210351464 3.8094465080629383 0.8279508170899271 3.5326280261311322 +35.1454071976225 2.2681378360011464 1.1539825206186456 2.2266455137918135 +1.4645806769633969 3.220304568287081 36.00708572303993 3.164827830072841 +2.697933145160493 0.7392347351728202 1.3677316155290136 1.7993586250228364 +36.16826615562556 3.8870297548042507 3.3447960260475003 -0.19275508119659013 +0.8221132624465451 0.15657974070261757 0.8004403633330706 0.5002752270831691 +-0.3301816498748916 0.5299989400031799 -0.847998304005088 0.0 +-0.1990074380419978 0.8955334711889901 0.3980148760839956 0.0 +0.4061384660534476 -0.913811548620257 2.8344282747214944 -0.06793597627479353 +-0.07449462435734953 -0.10060127859362666 0.016797881459910614 -0.04124869399409273 +-0.02466817882095746 0.015164836523137493 0.032560374776903614 0.028771848748443504 +0.00040935081744308127 -0.0010132101650362005 -0.0003252173874899872 0.016372006234976574 +0.01413300322489739 0.022860402554804385 -0.0014049788977866032 -0.0016424629146125844 +-0.00179753015491238 0.008098907138328618 -0.0022006964929160086 0.020951440344939634 +-0.001582287827459547 -0.0017941487131394943 -0.0011999613657527677 -0.009131895286366213 +-0.04041757044300798 0.0623113885649854 -0.0019090512573642499 -0.003855880164921586 +0.0040086852375630115 -0.08594323102255193 -0.17826404103147014 0.4107089739630609 +-0.0062734698150312546 -0.008006612310121106 0.02604149485772972 0.019166412506377073 +0.16208706645992105 -0.12299906234405045 0.013052850016469386 0.01942584727767882 +-0.05993720941788855 0.0916174963016764 0.11721691489473565 -0.3564051872650854 +-0.0034532308775330443 -0.01226709765612464 0.023350125638924732 0.02819997466203676 +-0.007063171093987457 -0.05662591628587278 -0.001158059641395363 -0.00267570059170686 +0.007047019212869293 0.004284428103699256 -0.026226519549469213 0.022733397814887854 +-0.0007206083061893995 -0.00032437598452857846 0.0018076676361954078 -0.008135638415691985 +-0.021420702240581586 0.03870764219954001 -0.000290614314019803 0.0003899521936802184 +-0.00011585638772893675 -0.012066144743229314 -0.018216445176274784 0.031469847468481194 +-0.0002596273440884358 0.0005130623543134621 1.6059904298064073e-05 -0.018401057847528397 +-0.02088238471963741 0.03179673073443861 -0.0003636539673796176 5.89206769836606e-05 +0.00015433596940558209 -0.026952270928750015 -0.015991712658501078 0.03243456446312732 +-0.0010035559294764122 -0.001148267925571031 0.0009311371041397995 -0.049775057291675015 +-0.008748869899851727 0.03679919442704106 -0.0038499524864722204 -0.0028126269957726643 +0.003276249627300304 -0.6526799214437679 -0.2086526240318778 0.39503491469078383 +-0.08019668942317296 -0.028198150425286186 0.04813297243136616 0.7481171317841221 +0.296582002338467 -0.4979489014858984 0.07214300608891205 0.035059600416661146 +-0.1085433965880912 0.6484154255184941 0.5872936128714038 -0.5668809145947206 +0.21446030115846787 -0.9383660141134509 -0.29391522294572425 -0.03968002352298585 +-0.03504611958759245 -0.005655591412225592 -0.0024668558480699784 -0.037095654146943186 +-0.025508074516414243 0.03968002352298585 0.03504611958759245 0.005655591412225592 +-0.0017357355070371514 0.028566041757270472 0.013704387814465634 -0.03006697350888655 +0.0057739912151767905 0.03435997621797439 -0.00019473229366252223 0.025489718453249936 +0.035046119587592454 0.03006697350888655 -0.0057739912151767905 -0.033183452256561115 +-0.002743702720616493 -0.029972089181635117 0.0 0.0697469970318724 +0.02424416060503107 -0.02443333982116298 0.0026615881417324993 0.005655591412225601 +-0.00577399121517678 -0.0697469970318724 -0.013721363870904297 0.03019019300573402 +0.004479438227653644 0.0 0.0 -0.004615256317906075 +0.0007118815061492026 0.004346243393760328 0.00246685584806998 -0.03435997621797439 +-0.024244160605031078 0.000416127555694304 0.010776215221324338 0.006095306281045116 +0.0 0.0 0.0 0.007322933306783178 +-0.002131932845384159 0.037095654146943186 0.00019473229366252375 0.024433339821162983 +-0.010026458425847479 0.00036657088769054616 -0.008609064217921944 0.0 +0.0 0.0 3.2526065174565133e-19 -0.0027076769888771043 +0.025508074516414247 -0.02548971845324994 -0.0026615881417325045 -0.005993956549315406 +0.008498671107808858 -0.0007826984433848523 0.0 0.0 +0.0 -1.5585406229479126e-19 -1.4399560103323106e-19 -0.07710825265824553 +-0.05040967442027201 -0.01702959017025251 0.00014194041389008617 -0.054675049877554345 +-0.0188594415994305 0.03742822913525968 0.015363554832679564 0.011373998758026917 +-0.002538329752474304 0.017145701178932778 -0.0028585947239154028 -0.03096652844914685 +0.01628442178616944 0.058607056050767575 0.003094343536024233 0.06187450774378286 +0.015363554832679562 0.0008995549402602956 -0.010510430570992648 -0.019734111248319627 +-0.00036037686563371665 -0.026133194423601687 0.0 0.10807478110739235 +0.017641431156119537 -0.06089785385359149 -0.003236283949914321 0.011373998758026915 +-0.010510430570992671 -0.03832778407551996 0.0015371775019265654 0.027301996797144474 +0.002898706618108019 0.0 0.0 -0.0034888299846060553 +-0.002135171476604151 -0.0007928542808788731 0.0015937950931470654 -0.02542360379420646 +-0.00392006728521524 0.00035750377441046057 0.007043308105189969 0.000504862787416698 +0.0 0.0 0.0 -0.001111479329685365 +0.0007221105470990416 0.026109008120283873 -0.00035064081540773985 0.030707660847857466 +-0.006725138359891655 2.724506526746484e-05 -0.009097807353552211 0.0 +0.0 0.0 -2.846030702774449e-19 0.004600309314291416 +0.005155053784964867 -0.03190241856214775 -0.0012431542777393237 -0.0002969488297477707 +0.008905935854942312 -0.000384748839677926 0.0 0.0 +0.0 -5.421010862427522e-19 -6.776263578034403e-19 -0.0640607854059598 +-0.02098489072380518 -0.02199925743456288 0.0011542960276326425 -0.03324724540339117 +0.00960376296321805 0.026632556270700127 0.005621335891125619 0.010625258676535961 +-0.0018399832223501896 0.013584922096530677 -0.012125050851293211 0.006085642291283051 +0.022192535891690342 0.03537708738479117 0.0008913149473349458 0.05388097995936074 +0.005621335891125623 -0.006985197231543346 -0.011682105320697694 -0.014456672341867248 +0.000333973444872067 -0.026943314981383942 0.0 0.05797514311467675 +-0.009454438231599731 -0.05363252851098675 -0.0020456109749675865 0.010625258676535963 +-0.011682105320697689 -0.019647359039156785 0.010703003513158437 0.0283949001591415 +0.0015060097774781235 0.0 0.0 -0.0022889461961108468 +-0.0006936826472988987 -0.0008083908887027873 0.001384033724841662 -0.01564297613647155 +0.007917260729673166 0.0001953359839026005 0.004639683707907302 -0.002797741861652976 +0.0 0.0 0.0 0.0002493210570292178 +0.0007987041348122715 0.016101544224458395 -0.0005309380817012297 0.026330531713842274 +-0.0045508775156540655 -4.724610485733877e-05 -0.008488226718321912 0.0 +0.0 0.0 4.743384504624082e-20 0.002039625139081626 +-0.006745168239302648 -0.027747785535759052 -0.0008530956431404324 0.0029491186749877668 +0.008325243614558213 -0.0001480898790452601 0.0 0.0 +0.0 -5.353248226647178e-19 -6.623797647528629e-19 -0.049195120871960346 +-0.011171435066660164 -0.01741054707722807 0.00042831667961088773 -0.026992257530777525 +0.027851183593103672 0.022562564601260222 0.0055500991755345445 0.0067852884006921075 +-0.0011965877028720386 0.01368135019344913 -0.02473279956969332 0.020695488520943345 +0.02264387164993619 0.026986341060161737 0.0007673523391557771 0.057880321213956754 +0.00555009917553454 -0.013710291289399998 -0.010961766329238499 -0.014010829928203763 +0.0004423831026648421 -0.03929509594268603 0.0 0.02849963235101701 +-0.02697251719329076 -0.05806569942150301 -0.0011956690187666656 0.006785288400692109 +-0.010961766329238481 -0.008852273311860226 0.02356921923852716 0.04067135852833552 +0.0007542046002071976 0.0 0.0 -0.0016241243399055007 +-0.00034011394542761495 -0.0006069247258090992 0.0014116665427393025 -0.01252966871829449 +0.016269513680132325 0.00019109842477499773 0.004199149499604929 -0.006190221885697431 +0.0 0.0 0.0 0.0006696141151413329 +0.000770727746383966 0.01340733543424685 -0.0011013257840278444 0.02967079926236152 +-0.004089309220581 -0.00012470251775107625 -0.010995421269795328 0.0 +0.0 0.0 -2.168404344971009e-19 0.0009545102247641658 +-0.01572613274181046 -0.03093700623257281 -0.0003103407587114582 0.006276382810162827 +0.010832233357497072 -6.639590702392539e-05 0.0 0.0 +0.0 -6.505213034913027e-19 -8.131516293641283e-19 -0.06585421696547558 +-0.03345943206615093 0.017277675641122824 -0.002439186536758327 -0.030419667103584954 +0.06350503765211055 0.04329165236421536 0.02790933289061638 -0.024062964041814933 +-0.000637412800283523 0.011657471422900745 -0.0570258601519918 0.06310339692538604 +0.006398116583929973 0.025122689266345196 0.004061434385533867 0.09512564179993752 +0.027909332890616385 -0.04939310563598604 0.004563649745308526 -0.011110893786248324 +0.00044108688190376034 -0.09732487708776556 0.0 0.002750820040089549 +-0.05588419703846039 -0.09903881438067348 -0.0016222478487755384 -0.024062964041814922 +0.004563649745308526 0.006101453271770677 0.053706711737584345 0.10002406624391336 +0.00019632591837976288 0.0 0.0 -0.0022404411168750983 +-0.0010585098269442516 0.00038130651843404676 0.003635774239630365 -0.011111859338141437 +0.03231497779993323 0.0005189684576894235 0.004234306452945393 -0.014652105344863406 +0.0 0.0 0.0 0.002089567010894965 +0.00039000708146278416 0.016738316910135824 -0.004503817488198709 0.058367455852337956 +-0.0034755499693592926 -0.0005776146339356763 -0.024389677808087186 0.0 +0.0 0.0 4.336808689942018e-19 0.00015087410598012453 +-0.038772238082417226 -0.0558305458572515 0.0008680432485683413 0.013739187116060574 +0.024720630218646292 5.864617624626263e-05 0.0 0.0 +0.0 -4.0657581468206416e-20 -4.404571325722362e-20 -0.33582003205231226 +-0.29758353802970705 0.5317086188481925 -0.027409996413241714 -0.025578541138473625 +0.1756519753540554 0.2925283796880969 0.2696742051390907 -0.5076456548063776 +-0.0045652237651852 -0.08249678805801806 0.003072688014260541 0.16428371986771106 +-0.14394636809740197 -0.026151457233205427 0.0026432610412796273 0.28089010963037503 +0.26967420513909063 -0.11489061423172502 0.13938271835209343 0.07221360952878236 +0.015282235857190313 -0.3192971103172913 0.0 0.17153631218460128 +-0.06986815764439326 -0.3037159245141023 0.02476673537196209 -0.5076456548063776 +0.13938271835209348 -0.17763776545637194 0.008524831777154754 0.29792349932403484 +-0.01071701209200511 0.0 0.0 -0.010477615488936508 +-0.009134996473180945 0.015466233153773871 0.028047409213525238 0.03726235101945374 +0.016161445906808958 0.0027289030996460358 -0.011822498888236626 -0.014212286534046596 +0.0 0.0 0.0 0.007181164312918684 +-0.004752398322477395 0.013921069715572859 -0.003084347923183385 0.20369185827018899 +0.0165715569811223 7.864385339684515e-05 -0.07708456909140485 0.0 +0.0 0.0 7.589415207398531e-19 0.003296451176017818 +-0.11862611520206362 -0.18356523254260948 -0.024963061290341856 0.004929653121118226 +0.07752184567805716 -0.002807546953042886 0.0 0.0 +0.0 -1.4094628242311558e-18 8.673617379884035e-19 -0.49104320238315213 +-0.4135606591538502 0.7716123638956403 0.005319023543987399 0.1960537045455249 +-0.12040378891772334 0.19851482269505522 0.14388645401475952 -0.26396670908926273 +-0.019413393092669304 -0.1030212574036006 0.16417133398121594 0.02253756969391678 +0.0015222668769361847 -0.18060854342670338 -0.01460231123037981 0.5921534309451523 +0.14388645401475955 0.09235304453780824 -0.1409049852290296 0.07101180160487658 +-0.011540624458894842 -0.15778177341848768 0.0 0.46850563268923534 +0.09358882813148828 -0.558398955606989 0.009283287686392402 -0.26396670908926256 +-0.14090498522902956 -0.2908678672328634 -0.10270085788229937 0.18094684983282305 +0.03095401755156415 0.0 0.0 -0.028680015453568884 +-0.02345312099030733 0.04723488029324382 -0.0007537997788022127 0.10839493389792099 +-0.10211365990864307 0.0025750405315739886 -0.028692479659395045 0.033163725883827766 +0.0 0.0 0.0 -0.005243085626729508 +0.002708356599017783 -0.1135569164875068 -0.0006799246268104926 0.2604754562829542 +0.03246719355436033 0.0007430349710284279 -0.06922569430030479 0.0 +0.0 0.0 -2.927345865710862e-18 0.03392310108029838 +0.11733110090346284 -0.27285632062786097 0.0014337244056127048 -0.04071467644887718 +0.06745936347851943 -0.0033180755026024304 0.0 0.0 +0.0 -3.3610267347050637e-18 1.7889335846010823e-18 -0.21245606527720234 +-0.13308007288815138 0.24096206076695542 0.020473701831672672 0.15748517588763697 +-0.2659181652819861 0.013941242582147139 -0.010806381126608153 0.02300464832230731 +-0.00197126105826311 -0.037962111300400725 0.07558469904437307 -0.06085326556312405 +0.13140424046148824 -0.12245001946803924 0.013257203724341736 0.26138395453614793 +-0.010806381126608167 -0.03149977897468419 0.00950074476754136 0.03735706847875822 +0.0013372175353654598 -0.0867521648283954 0.0 0.2733093308403264 +0.19886487238484457 -0.2892126307803554 -0.03373090555601441 0.023004648322307277 +0.009500744767541348 0.017558536392537044 -0.07476794227034358 0.08877351225810029 +0.000634043522897652 0.0 0.0 -0.0075223400452811215 +-0.003529762628475777 0.0060634142768573995 -0.0010603087390033703 0.05143821786316266 +-0.09616401450254525 0.0002639577026238187 -0.01448761534717197 0.027772550379099842 +0.0 0.0 0.0 0.00010947266502183035 +0.004437407057314376 -0.05446391848403636 -0.0017165792654468938 0.10826578094753239 +0.01428835552095817 -0.00038917254030434133 -0.0307078449947866 0.0 +0.0 0.0 -2.0328790734103208e-20 0.0074128673802593015 +0.10174683130077018 -0.10360218111766026 0.002776888004450255 -0.02726775705840539 +0.030776042481602393 0.00012521483768053693 0.0 0.0 +0.0 5.692061405548898e-19 -1.748276003132876e-18 0.0031327933224302475 +0.028497552879840397 -0.06958004108639304 0.0032666831102720747 0.06660081327336403 +-0.13203137955283298 -0.017074035904577387 -0.017691171753232246 0.04657539276408574 +0.0005975216480311916 -0.01797302596874928 0.028923863569367256 0.046968852498947625 +-0.03465057530652888 -0.06493586522558252 -0.0013642747363185636 0.16439266991259166 +-0.01769117175323226 -0.015469073524263442 0.02514983053898752 0.02050048264437898 +0.0014301244462562456 -0.06380784792605426 0.0 -0.050101645821377876 +0.12694222683123638 -0.16815392501468507 -0.0019024083739535107 0.04657539276408574 +0.02514983053898754 0.03254310942884083 -0.03416995745840589 0.06034067595235561 +-0.0020276460942874365 0.0 0.0 0.00030835778269806087 +0.001813984742743334 -0.003979253606784689 -0.001295422052008965 0.027578796746824293 +-0.05217428456089279 -0.00016143627095018247 -0.007669430359377723 0.01406778198212598 +0.0 0.0 0.0 0.002733512580988973 +-0.002285677001857182 -0.028638701972963313 2.7057200953102556e-05 0.07938041275658478 +0.0072650315786678635 -0.0002112374145223769 -0.021793940546257048 0.0 +0.0 0.0 -1.3891340334970526e-19 -0.003041870363687035 +0.05644668050845991 -0.07764050508419626 0.0012683648510558605 -0.013018554792822807 +0.022346491247664826 0.00037267368547255915 0.0 0.0 +0.0 4.743384504624082e-20 -2.998496633280223e-19 0.03769543587272416 +0.01906979975125454 -0.07892125647882853 0.00039391053704453984 0.03368395591927566 +-0.04907924206887852 -0.020621399968146777 -0.0013786279980222945 0.032345863714742795 +0.001262045837391661 -0.014511576280369056 0.008548145843805172 0.014963404180545373 +-0.03518821166133686 -0.036946785894802645 -0.0016605799066536705 0.11980980394140406 +-0.0013786279980222988 0.0005056693437180675 0.010038381122349335 0.015469079446747427 +0.00011067416959760064 -0.05236183115080109 0.0 -0.05265884005326954 +0.05195255832150227 -0.11679625097586435 0.001266669369609129 0.03234586371474281 +0.010038381122349341 0.020115730624428712 -0.013101696327143517 0.050766950404029926 +-0.0013727200069892618 0.0 0.0 0.0017080185966899886 +0.0008876941312929399 -0.0035241165385554597 -0.0009914321850757325 0.01644630325042367 +-0.01778260086309638 -0.00020179494864427786 -0.00486874320162098 0.004999590076286005 +0.0 0.0 0.0 0.0005659598676902165 +-0.0014804010448595806 -0.015710929950526375 0.00023045546039742438 0.056455575023508744 +0.004825269503614545 -8.493565239402926e-06 -0.016657048366564554 0.0 +0.0 0.0 -1.1858461261560205e-20 -0.0022739784643802012 +0.020155378499511274 -0.05600195601534979 0.000760976724678307 -0.004336728347167952 +0.016883814367469527 0.00021028851388367941 0.0 0.0 +0.0 6.098637220230962e-20 5.166900978251232e-20 0.03140065967203254 +-0.009174100561308261 -0.04403694730713293 -0.0009843031208975825 0.028068610963964378 +-0.013803382076116658 -0.010779259703885762 0.010552728559330555 0.011691083592390135 +0.0007943965116550751 -0.016421877462944608 0.0012810382369735447 -0.0021470733089071865 +-0.014952478999382054 -0.030570768028058498 -0.00015716789268103336 0.09697374866972339 +0.010552728559330558 0.0016414039651891188 0.00491409787703272 0.015868388725484177 +-0.00011978608210446045 -0.043837520089120806 0.0 -0.029253586363125354 +0.01802136570210606 -0.09588001138704759 0.0011414710135786172 0.011691083592390135 +0.004914097877032724 0.00913785573869664 -0.0032111109654435082 0.043435408100173184 +-0.0006746104295506143 0.0 0.0 0.0013962388517530762 +-0.00046750389144283445 -0.0018726269976920367 -0.00027774271649407787 0.01510168858131107 +-0.004919669374962542 -0.0001275238499750175 -0.004667963327043086 0.0013128581101900243 +0.0 0.0 0.0 -0.00014382492253569836 +-0.0005777149552968117 -0.013557034683595327 4.649372308343204e-05 0.04511306098301765 +0.004917873562127292 1.8788686432677447e-05 -0.013828263172267426 0.0 +0.0 0.0 0.0 -0.00125241392921738 +0.005255236232311487 -0.0446119175189223 0.00023124899341064528 -0.001023729207584599 +0.0139600072610822 0.00010873516354234362 0.0 0.0 +0.0 1.6263032587282567e-19 -6.20451633863775e-19 0.010574551720246678 +-0.02883173551161733 -0.01099818007527388 -0.0011317718360381525 0.034114537015427125 +-0.0019183738349650907 0.00020470798363908407 0.018279006952286774 -0.0006929035171162543 +0.00033143346931318284 -0.023184090797496764 0.0009024310863518196 -0.00238681425280183 +-0.01388915203360873 -0.03572292034677082 7.519716481533204e-05 0.08555996627420015 +0.018279006952286795 0.000745410287612711 0.00897505415657601 0.02178096019815423 +-7.259091649954247e-06 -0.04171025491763631 0.0 -0.008187737467444845 +0.004466889457078664 -0.08637642355953028 0.0010565746712228202 -0.0006929035171162549 +0.008975054156575991 -0.0009501182712517952 -0.0013489574987671214 0.04121161534338114 +-0.0003241743776632286 0.0 0.0 0.0005175073587539243 +-0.001387667347015243 -0.00048196105033907475 0.00033737532438307726 0.019854531621286635 +-0.0012557784916351555 5.425064519347198e-06 -0.006267922516081796 0.0003361564729071979 +0.0 0.0 0.0 -9.839393178187887e-05 +-0.0006197669610823644 -0.017692659552482514 4.458891728912841e-05 0.04294101545935711 +0.006715245112325715 1.3458349078154992e-05 -0.01305017091257326 0.0 +0.0 0.0 -1.2197274440461925e-19 -0.0004191134269720474 +0.0006373355979915458 -0.04172244618507935 -0.0003819642416722056 -0.000345450942064775 +0.013276724012617008 -1.8883413597499892e-05 0.0 0.0 +0.0 5.421010862427522e-20 -6.776263578034403e-21 -0.016610124498400377 +-0.04710637314577877 0.00929722840348988 -0.0016769862128750129 0.05183552428332542 +-0.0034846780615068046 0.01640541651476129 0.028827366193491993 -0.008604324886373626 +-0.0004462320605313052 -0.038690552913129256 0.007410620920380621 -0.006737723109586722 +-0.028180762345724746 -0.05398559108671283 -0.0005674486756748417 0.08581663271099604 +0.028827366193491986 0.005992312821974011 0.019205708189148733 0.03590329523463731 +-3.407628731874591e-05 -0.045710192199921035 0.0 0.0233478476079871 +0.0050851865168219015 -0.08788455127673805 0.0022444348885498544 -0.008604324886373624 +0.019205708189148726 -0.022397729336735304 -0.006592921019659701 0.04403179392266009 +0.00048030834785005175 0.0 0.0 -0.0006590187748515505 +-0.0020351476887834224 0.00042880738031582377 0.00134555274356183 0.03220463088855861 +-0.0037362290180547814 0.00016074620336860984 -0.01025047527592615 0.001525825100338797 +0.0 0.0 0.0 -0.00022512566212459084 +-0.0011999789940818676 -0.028651433485828652 0.0005747077673247961 0.046672935933356904 +0.01080634267283246 6.822840999752051e-05 -0.01382392660734242 0.0 +0.0 0.0 -4.87890977618477e-19 0.000884144436976154 +0.002582246975154985 -0.04410637779335975 -0.0019202605108866256 -0.0016752436186599357 +0.014189943878411852 -0.0002289746133661355 0.0 0.0 +0.0 9.486769009248164e-20 1.8295911660692887e-19 -0.055908955340423755 +-0.07141067951628934 0.014147446919275677 -0.002444498542155008 0.08802458552270964 +-0.0226424491528747 0.03950353882566246 0.04258331332279734 -0.005543122032902052 +-0.0017730726581009527 -0.07239703346740506 0.028884531978807443 -0.03359971555007534 +-0.047650656067133354 -0.09094835222043948 -0.0022952293834966426 0.09393009373916046 +0.04258331332279735 0.027607402728101332 0.028444947877984624 0.06801229641999597 +-0.0009145792546547188 -0.055634268851966415 0.0 0.08950867089049908 +0.02191468435905731 -0.09579380310766256 0.00473972792565165 -0.005543122032902052 +0.028444947877984645 -0.06711094155376378 -0.02747422412312062 0.052242066160810396 +0.0026876519127556706 0.0 0.0 -0.002058254165550648 +-0.002627321261686465 0.0006064614060038768 0.0028907306026863134 0.055045056985802185 +-0.01532176333939761 0.0003144447914950174 -0.01855336088431647 0.006729353104209771 +0.0 0.0 0.0 -0.001145301055871761 +-0.0017981088272767624 -0.04933403260958037 0.002329305670815393 0.05176200918500247 +0.01922273043275813 0.00022297583460896032 -0.01578785060748189 0.0 +0.0 0.0 -4.336808689942018e-18 0.0032035552214224137 +0.015231828232494082 -0.04821990153923942 -0.005220036273501706 -0.006777746530734877 +0.016195382129789913 -0.0005374206261039766 0.0 0.0 +0.0 1.951563910473908e-18 0.0 -0.11276914844102819 +-0.11914762940693854 -0.024114707785878688 -0.003929093624398366 0.16338661526237158 +-0.08420506398004704 0.07326560961536573 0.0765643160841412 0.02965782981878074 +-0.0018550326770005367 -0.17022728659417807 0.11650915600971642 -0.10456548847701806 +-0.047125912243722604 -0.17160796146295357 -0.005615246079632584 0.11366599262188751 +0.07656431608414119 0.07695808574891673 0.018680964365737983 0.16752846658952025 +-0.0015509373333753799 -0.08017391345420385 0.0 0.21733463691804628 +0.0760280890627945 -0.11143625721797429 0.009544339704030961 0.029657829818780776 +0.018680964365737997 -0.15022369536428248 -0.11470846136015991 0.07646020120341128 +0.0034059700103758993 0.0 0.0 -0.004023860843460196 +-0.004079106327436723 -0.0007054570154641516 0.0057021662824993215 0.10359566504295759 +-0.048553864939673876 0.0008105294295906651 -0.04101488187356801 0.027098946626774124 +0.0 0.0 0.0 -0.0035518174500865883 +-0.0017525344380111014 -0.09098958179496651 0.0065298253342873135 0.059194191057163885 +0.04281104304292085 0.0008216536187621607 -0.021287324367216878 0.0 +0.0 0.0 3.2526065174565133e-19 0.0075756782935467865 +0.055320532001239604 -0.05803172376992109 -0.012231991616786626 -0.026202680824807638 +0.02148764824808693 -0.0016321830483528362 0.0 0.0 +0.0 8.673617379884035e-18 1.3010426069826053e-18 -0.23456131723208767 +-1.2864928361756347 -0.7638511428556903 0.0023275555711071774 0.30600323978152877 +-0.5100983293056752 0.16129570761672193 1.2099285200914935 0.7341933130369096 +-0.05786000726201612 -0.36710718334140857 1.1890994948455953 -0.9877113259548156 +0.4413116437947425 -0.6354647609429962 -0.11996637634552994 0.24209619598387236 +1.2099285200914935 0.9107532402058989 -0.4599926081604805 0.6689520637628703 +0.11064355851651692 0.0007252022617299098 1.2222726431869035 0.3175258099030712 +-0.0951673388478885 0.11763882077442282 0.73419331303691 -0.4599926081604806 +-1.072048947822621 -1.0530411921442746 -0.15505255136303822 -0.05278355125450074 +-0.007869308295816169 -0.039696664324636685 -0.022800933226732444 -0.0004725228941066355 +0.46793629435347583 -0.2028173485429114 -0.006445783723306648 -0.09130107614195934 +0.25465317117200204 -0.03090980914114698 0.013175532968186226 -0.13577595318735072 +0.12151731367890536 0.018707137644477283 0.16371565595334428 0.02742839833437402 +-0.012468729997129065 0.03877911743696309 0.39358917329595877 -0.16192228252966853 +-0.12104479078479875 -0.21793865762747655 -0.01963692627337539 -0.02098261461106747 +-0.16129570761672193 -1.2099285200914935 -0.7341933130369096 0.05786000726201612 +0.36710718334140857 -1.1890994948455953 -0.9107532402058989 0.4599926081604805 +-0.6689520637628703 -0.11064355851651692 -0.0007252022617299098 1.072048947822621 +1.0530411921442746 0.15505255136303822 0.05278355125450074 0.019681926033839833 +-0.11208141411102865 -0.0676372183879752 -0.11077763196408125 0.12874440851530627 +0.09109570593024119 0.05815177850097644 -0.051364356756826636 -0.07265920637130441 +-0.05136435675682685 -0.14075048788358685 0.025142700848733907 -0.07265920637130434 +0.025142700848733768 0.08259870938261006 0.023649119237166748 -0.04885712261390143 +-0.016775295742923968 -0.04885712261390135 -0.02521114976949526 -0.0014548191683897496 +-0.016775295742924034 -0.0014548191683896872 0.0015620305323283123 0.053516053218401013 +-0.04991041435245781 0.05411404666209682 -0.023369653312295017 0.06489343558913888 +0.041669682256767884 0.010459792033693668 0.02533383704308309 0.06374167317726201 +-0.562508089542378 -0.11579517536366449 -0.020714900202943795 -0.12198351403982419 +-0.06997595222963239 0.09645413549676084 -0.05199924169608701 0.22485699486358607 +-0.5319278592911123 diff --git a/tests/data/torsion_orcaflex_refs.txt b/tests/data/torsion_orcaflex_refs.txt new file mode 100644 index 0000000..d12cc02 --- /dev/null +++ b/tests/data/torsion_orcaflex_refs.txt @@ -0,0 +1,41 @@ +# OrcaFlex torsion reference values (summary values only) for tests/test_torsion_orcaflex.f90. +# OrcaFlex 11.6d through OrcFxAPI; written by +# validation/scripts/orcaflex_torsion_reference.py --refs from its JSON summary +# (script SHA-256 65b6f529fec69320...). Line IncludeTorsion Yes, statics Catenary then +# Full statics; StaticsTolerance 1e-09 (pure), 1e-08 (onsets, lazy wave); +# StaticsMaxIterations 2000; TensionTorqueCoupling 0; seabed friction 0. +# Units SI (N, m, N.m); OrcaFlex kN and kN.m are converted. See VALIDATION.md (torsion). +# +# pure: straight neutrally buoyant line, L 10.0 m, 20 segments, EI 1.0 kN.m^2, +# GJ 10.0 kN.m^2, both ends encastre; gamma reached by continuation. +# tag gamma_A_deg gamma_B_deg torque_N.m +pure 0.0 90.0 1.570796326795e+03 +pure 0.0 450.0 7.853981633974e+03 +pure 0.0 1800.0 3.141592653590e+04 +pure 0.0 -90.0 -1.570796326795e+03 +pure 30.0 120.0 1.570796326795e+03 +# series: GJ 10 kN.m^2 over 4 m then 25 kN.m^2 over 6 m, gamma_B 90 deg +# tag torque_N.m +series 2.454369260617e+03 +# clamped: Greenhill onset of the clamped-clamped line (case b, L 10 m, EI 1 kN.m^2), M_cr L/EI: +# OrcaFlex at 200 segments and the Richardson extrapolation of its three finest meshes +# tag T_L^2/EI n200 extrapolated +clamped 0.0 8.9870465524 8.9868186152 +clamped 10.0 10.4292160777 10.4288576773 +clamped 50.0 15.5899281305 15.5893530030 +clamped 200.0 28.9839761262 28.9830105566 +# pinned: bending-free ends with a finite twisting spring (1e4 kN.m/deg), M_cr L/EI as above +# tag T_L^2/EI n200 extrapolated +pinned 0.0 4.9113380432 4.9112670694 +pinned 10.0 7.2693679810 7.2692660363 +# lazywave: Lozon et al. (2025) GoMex 80 m (geometry as CableDyn validation/scripts/orcaflex_lozon_cables.py), GJ 50.0 kN.m^2 (illustrative), +# anchor End A, hang-off End B, segment 0.375 m, both ends encastre at the untwisted static +# orientation; the declinations (deg) of that orientation at the anchor and the hang-off: +orientation 87.3991775607 8.3968483696 +# tag turns torque_hangoff_N.m geometric_twist_turns peak_interior_curvature_1/m max_abs_Y_m T_hangoff_N +lazywave 0.0 -3.468577321e-14 1.879313946e-17 9.684766870e-02 4.146545931e-16 9.317615555e+03 +lazywave 1.0 1.765406378e+03 4.348309981e-02 9.721875420e-02 1.671157022e+00 9.326347366e+03 +lazywave 2.0 3.536055812e+03 8.412545341e-02 9.849395059e-02 3.222997787e+00 9.351197525e+03 +lazywave 3.0 5.313974260e+03 1.208293740e-01 1.013180989e-01 4.591115354e+00 9.389005537e+03 +lazywave 4.0 7.097221947e+03 1.546458532e-01 1.070380124e-01 5.770929912e+00 9.436854046e+03 +lazywave 5.0 8.879082825e+03 1.892137215e-01 1.196371754e-01 6.788001258e+00 9.493106351e+03 diff --git a/tests/test_hermite_torsion_dynamic.f90 b/tests/test_hermite_torsion_dynamic.f90 new file mode 100644 index 0000000..d6b1f92 --- /dev/null +++ b/tests/test_hermite_torsion_dynamic.f90 @@ -0,0 +1,526 @@ +! File: tests/test_hermite_torsion_dynamic.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +PROGRAM test_hermite_torsion_dynamic + !! Dynamic gates of condensed torsion on the cubic-Hermite cable, on a 10 m line + !! clamped (Rigid bending connections) at both ends with both ends restrained in torsion: + !! S step response: an imposed-twist step (u_twist) on a straight line gives the quasi-static + !! torque GJ Phi / L at the very first step and every later one, with the centreline + !! unchanged. Condensed torsion has no torsional inertia, so the torque follows the imposed twist + !! without a torsional wave or overshoot: the dynamic and quasi-static answers coincide; + !! T turning parent: the coupled end's torsion frame turns with the parent orientation about + !! the line axis through 1.5 turns while the imposed twist follows it, so Theta passes + !! several 2 pi branches (unwrapped, no slip) and the torque stays constant; turning the + !! parent alone changes the torque by (roll)/C; + !! E energy: free vibration of a bowed, twisted line (rho_inf = 1, no drag): kinetic + + !! axial/bending + torsional energy (the torsional share exchanges with the bending) + !! stays within 0.5 % of the vibration energy over ten periods; + !! R restart: a moving, rolling run restored from a snapshot, and one rebuilt from the + !! checkpoint mirror into a fresh module, continue bit-identically (Theta and torque + !! included) after the frame has turned through more than one turn; + !! A the non-stepping boundary write and the mirror reload are all or nothing, and a reload + !! recovers a parent-attached frame's orientation; + !! B the bordered (Sherman-Morrison) step converges as fast as the line without torsion + !! on the same swaying, rolling motion with bending-pinned torsional ends (in such steps + !! the rank-one term is small beside the inertia; it carries weight near buckling), and + !! a model with torsion refuses the configuration blend. + USE CableDyn_Precision, ONLY: wp + USE CableDyn_EndConnection, ONLY: CD_ENDCONN_RIGID + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Line, CD_HermiteTorsion_Unwrap, & + CD_HTORS_OK + USE CableDyn_HermiteCableDynamic, ONLY: CD_HermiteCableDynType, CD_HermiteCable_Dyn_Init, & + CD_HermiteCable_Dyn_Step, CD_HermiteCable_Dyn_Energy, & + CD_HermiteCable_Dyn_End, CD_HermiteCable_Dyn_Set_EndConnection, & + CD_HermiteCable_Dyn_Set_Torsion, CD_HermiteCable_Dyn_Set_TangentReuse, & + CD_HermiteCable_Dyn_Set_ForceBlend, CD_HermiteCable_Dyn_Torsion_State, & + CD_HermiteCable_Dyn_Reset_Profile, CD_HermiteCable_Dyn_Get_Profile, & + CD_HermiteCable_Dyn_Disable_Profile, CD_HCDYN_OK + USE CableDyn_OpenFAST_HermiteFMF, ONLY: CD_HFMF_ModuleType, CD_HFMF_Init, CD_HFMF_Set_EndConnection, & + CD_HFMF_Set_Torsion, CD_HFMF_UpdateStates, CD_HFMF_CalcOutput, & + CD_HFMF_Snapshot, CD_HFMF_Restore, CD_HFMF_MirrorSize, & + CD_HFMF_PackMirror, CD_HFMF_UnpackMirror, CD_HFMF_SetCoupledKinematics, & + CD_HFMF_End, CD_HFMF_OK, CD_HFMF_PreflightCoupledKinematics + IMPLICIT NONE + + REAL(wp), PARAMETER :: PI = 3.14159265358979323846_wp + REAL(wp), PARAMETER :: LL = 10.0_wp, EA = 1.0e5_wp, EI = 100.0_wp, GJ = 80.0_wp, RHOA = 1.0_wp + INTEGER, PARAMETER :: NE = 20, NN = NE + 1, NDOF = 6*NN + INTEGER :: nfail = 0 + + CALL check_step_response() + CALL check_turning_parent() + CALL check_energy() + CALL check_restart() + CALL check_boundary_and_mirror() + CALL check_bordered_and_blend() + + IF (nfail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', nfail, ' assertion(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: Hermite torsion dynamics (step, turning parent, energy, restart, bordered step)' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(cond, label) + LOGICAL, INTENT(IN) :: cond + CHARACTER(*), INTENT(IN) :: label + IF (.NOT. cond) THEN + nfail = nfail + 1 + WRITE (*, '(A)') 'FAIL: '//label + END IF + END SUBROUTINE require + + FUNCTION roll_x(psi) RESULT(r) + !! Global-to-parent DCM of a parent rolled by psi about +x (the transpose of the rotation). + REAL(wp), INTENT(IN) :: psi + REAL(wp) :: r(3, 3) + r = 0.0_wp + r(1, 1) = 1.0_wp + r(2, 2) = COS(psi) + r(3, 3) = COS(psi) + r(2, 3) = SIN(psi) + r(3, 2) = -SIN(psi) + END FUNCTION roll_x + + SUBROUTINE line_seed(bow_y, bow_z, q) + !! Straight line along +x (node 1 at the origin), optionally bowed by y = bow_y sin^2(pi s/L), + !! z = bow_z sin^2(2 pi s/L) (end tangents stay along +x, as the clamps require). + REAL(wp), INTENT(IN) :: bow_y, bow_z + REAL(wp), INTENT(OUT) :: q(NDOF) + INTEGER :: k + REAL(wp) :: s + q = 0.0_wp + DO k = 1, NN + s = LL*REAL(k - 1, wp)/REAL(NE, wp) + q(6*k - 5) = s + q(6*k - 4) = bow_y*SIN(PI*s/LL)**2 + q(6*k - 3) = bow_z*SIN(2.0_wp*PI*s/LL)**2 + q(6*k - 2) = 1.0_wp + q(6*k - 1) = bow_y*(PI/LL)*SIN(2.0_wp*PI*s/LL) + q(6*k) = bow_z*(2.0_wp*PI/LL)*SIN(4.0_wp*PI*s/LL) + END DO + END SUBROUTINE line_seed + + SUBROUTINE torsion_of(q, phi, tors, gj_line) + !! Condensed torsion of the line: both ends clamped along +x with reference normal +z, GJ on + !! every element, imposed twist phi, Theta of the seed q (branch nearest 0). + REAL(wp), INTENT(IN) :: q(NDOF), phi + TYPE(CD_HermiteTorsionType), INTENT(OUT) :: tors + REAL(wp), INTENT(IN), OPTIONAL :: gj_line + REAL(wp) :: g(NDOF), raw, l0(NE) + INTEGER :: es + CHARACTER(200) :: em + l0 = LL/REAL(NE, wp) + tors%active = .TRUE. + tors%phi = phi + tors%ends = RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, & + 0.0_wp, 0.0_wp, 1.0_wp], [3, 4]) + ALLOCATE (tors%gj(NE)) + tors%gj = GJ + IF (PRESENT(gj_line)) tors%gj = gj_line + CALL CD_HermiteTorsion_Line(q, l0, tors%ends, raw, g, es, em) + CALL require(es == CD_HTORS_OK, 'seed twist evaluates: '//TRIM(em)) + tors%theta = CD_HermiteTorsion_Unwrap(raw, 0.0_wp) + tors%has_theta = .TRUE. + END SUBROUTINE torsion_of + + SUBROUTINE build_cable(cab, q, phi, dt, tol, rho_inf, pinned, gj_line) + !! HFMF cable on seed q: node NN coupled (driven), node 1 held; Rigid bending connections + !! along +x at both ends (bending-pinned with pinned), the coupled one in parent axes + !! (identity attitude); torsion with the coupled end's frame attached to the parent. + TYPE(CD_HFMF_ModuleType), INTENT(INOUT) :: cab + REAL(wp), INTENT(IN) :: q(NDOF), phi, dt, tol, rho_inf + LOGICAL, INTENT(IN), OPTIONAL :: pinned + REAL(wp), INTENT(IN), OPTIONAL :: gj_line + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: l0(NE), eye(3, 3), frame(3, 2) + INTEGER :: es + LOGICAL :: rigid + CHARACTER(300) :: em + l0 = LL/REAL(NE, wp) + eye = roll_x(0.0_wp) + rigid = .TRUE. + IF (PRESENT(pinned)) rigid = .NOT. pinned + CALL CD_HFMF_Init(cab, l0, [(EA, es=1, NE)], [(EI, es=1, NE)], [(RHOA, es=1, NE)], [(0.0_wp, es=1, NE)], q, & + [1, 2, 3, NDOF - 5, NDOF - 4, NDOF - 3], 0.0_wp, 0.0_wp, rho_inf, dt, NN, es, em, & + max_iter=60, tol=tol) + CALL require(es == CD_HFMF_OK, 'cable init: '//TRIM(em)) + IF (rigid) THEN + CALL CD_HFMF_Set_EndConnection(cab, [0.0_wp, 0.0_wp], RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), es, em, & + coupled_d0_parent=[1.0_wp, 0.0_wp, 0.0_wp], parent_orientation=eye, & + connection_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID]) + CALL require(es == CD_HFMF_OK, 'cable end connections: '//TRIM(em)) + END IF + CALL torsion_of(q, phi, tors, gj_line) + frame(:, 1) = [1.0_wp, 0.0_wp, 0.0_wp] + frame(:, 2) = [0.0_wp, 0.0_wp, 1.0_wp] + CALL CD_HFMF_Set_Torsion(cab, tors, es, em, coupled_frame_parent=frame, parent_orientation=eye) + CALL require(es == CD_HFMF_OK, 'cable torsion: '//TRIM(em)) + END SUBROUTINE build_cable + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_step_response() + TYPE(CD_HFMF_ModuleType) :: cab + REAL(wp) :: q(NDOF), xa(3), phi, worst, bent, th, mt, f(3), m(3), mref + INTEGER :: k, es + CHARACTER(300) :: em + CALL line_seed(0.0_wp, 0.0_wp, q) + CALL build_cable(cab, q, 1.0_wp, 0.01_wp, 1.0e-8_wp, 0.8_wp) + xa = q(NDOF - 5:NDOF - 3) + worst = 0.0_wp + bent = 0.0_wp + DO k = 1, 40 + phi = 1.0_wp + IF (k >= 3) phi = 3.0_wp ! step of 2 rad at t = 0.03 s + CALL CD_HFMF_UpdateStates(cab, xa, [0.0_wp, 0.0_wp, 0.0_wp], [0.0_wp, 0.0_wp, 0.0_wp], es, em, u_twist=phi) + CALL require(es == CD_HFMF_OK, 'step-response step: '//TRIM(em)) + IF (es /= CD_HFMF_OK) EXIT + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) + mref = GJ*phi/LL + worst = nan_max_abs([worst, ABS(mt - mref)/mref]) + bent = nan_max_abs([bent, nan_max_abs(cab%line%q(2:NDOF:6)), nan_max_abs(cab%line%q(3:NDOF:6))]) + IF (k == 3) THEN + CALL CD_HFMF_CalcOutput(cab, f, es, em, y_moment=m) + CALL require(es == CD_HFMF_OK .AND. ABS(m(1) + mref) <= 1.0e-9_wp*mref .AND. & + NORM2(m(2:3)) <= 1.0e-9_wp*mref, 'S: the coupled clamp carries -M_t along the axis') + END IF + END DO + WRITE (*, '(A,ES10.3,A,ES10.3,A)') 'S step response: torque vs GJ Phi/L ', worst, & + ' (every step, quasi-static from the first), transverse motion ', bent, ' m' + CALL require(worst <= 1.0e-9_wp, 'S: torque equals the quasi-static GJ Phi / L at every step') + CALL require(bent <= 1.0e-12_wp, 'S: a straight twisted line below the buckling torque stays straight') + CALL CD_HFMF_End(cab) + END SUBROUTINE check_step_response + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_turning_parent() + TYPE(CD_HFMF_ModuleType) :: cab + REAL(wp) :: q(NDOF), xa(3), psi, th, mt, th0, err_th, err_mt + INTEGER :: k, es + INTEGER, PARAMETER :: NSTEP = 120 + CHARACTER(300) :: em + CALL line_seed(0.0_wp, 0.0_wp, q) + CALL build_cable(cab, q, 2.0_wp, 0.01_wp, 1.0e-8_wp, 0.8_wp) + xa = q(NDOF - 5:NDOF - 3) + th0 = cab%line%torsion%theta + err_th = 0.0_wp + err_mt = 0.0_wp + ! 1.5 turns of the parent about the line axis; the imposed twist follows the roll + DO k = 1, NSTEP + psi = 3.0_wp*PI*REAL(k, wp)/REAL(NSTEP, wp) + CALL CD_HFMF_UpdateStates(cab, xa, [0.0_wp, 0.0_wp, 0.0_wp], [0.0_wp, 0.0_wp, 0.0_wp], es, em, & + u_orientation=roll_x(psi), u_twist=2.0_wp - psi) + CALL require(es == CD_HFMF_OK, 'T: turning-parent step: '//TRIM(em)) + IF (es /= CD_HFMF_OK) EXIT + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) + err_th = nan_max_abs([err_th, ABS(th - (th0 - psi))]) + err_mt = nan_max_abs([err_mt, ABS(mt - GJ*2.0_wp/LL)]) + END DO + WRITE (*, '(A,F9.4,A,ES10.3,A,ES10.3)') 'T turning parent: Theta after 1.5 turns ', cab%line%torsion%theta, & + ' rad, Theta error ', err_th, ', torque error ', err_mt + CALL require(err_th <= 1.0e-10_wp, 'T: Theta follows the parent roll through 1.5 turns (unwrapped)') + CALL require(err_mt <= 1.0e-9_wp, 'T: the torque stays GJ Phi_0 / L when the twist follows the roll') + ! the parent alone turns on by 0.3 rad: the torque grows by 0.3/C + CALL CD_HFMF_UpdateStates(cab, xa, [0.0_wp, 0.0_wp, 0.0_wp], [0.0_wp, 0.0_wp, 0.0_wp], es, em, & + u_orientation=roll_x(3.0_wp*PI + 0.3_wp)) + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) + WRITE (*, '(A,ES14.6,A,ES14.6)') 'T parent roll alone: torque ', mt, ' expected ', GJ*(2.0_wp + 0.3_wp)/LL + CALL require(es == CD_HFMF_OK .AND. ABS(mt - GJ*(2.0_wp + 0.3_wp)/LL) <= 1.0e-9_wp, & + 'T: a parent roll alone changes the torque by roll/C (the frame turn adds to the imposed twist)') + CALL CD_HFMF_End(cab) + END SUBROUTINE check_turning_parent + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE energy_run(phi, band, e_vib, t_exchange) + !! Free vibration of the bowed line clamped at both ends (no gravity, no fluid), rho_inf = 1, + !! ten periods of the first bending mode; band = (max - min) of kinetic + elastic (with the + !! torsional share) over the run, relative to the vibration energy above the straight + !! twisted state; t_exchange = the swing of the torsional energy alone, same scale. + REAL(wp), INTENT(IN) :: phi + REAL(wp), INTENT(OUT) :: band, e_vib, t_exchange + TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: q(NDOF), l0(NE), ke, se, e0, emax, emin, t1, dt, th, mt, et, etmin, etmax, e_straight + INTEGER :: k, es, nstep + CHARACTER(300) :: em + l0 = LL/REAL(NE, wp) + CALL line_seed(0.05_wp, 0.03_wp, q) + CALL CD_HermiteCable_Dyn_Init(m, l0, [(EA, k=1, NE)], [(EI, k=1, NE)], [(RHOA, k=1, NE)], [(0.0_wp, k=1, NE)], q, & + [1, 2, 3, NDOF - 5, NDOF - 4, NDOF - 3], 0.0_wp, 0.0_wp, 1.0_wp, es, em) + CALL require(es == CD_HCDYN_OK, 'E: init: '//TRIM(em)) + CALL CD_HermiteCable_Dyn_Set_EndConnection(m, [0.0_wp, 0.0_wp], & + RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], [3, 2]), & + es, em, connection_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID]) + CALL require(es == CD_HCDYN_OK, 'E: end connections: '//TRIM(em)) + IF (phi > 0.0_wp) THEN + CALL torsion_of(q, phi, tors) + CALL CD_HermiteCable_Dyn_Set_Torsion(m, tors, es, em) + CALL require(es == CD_HCDYN_OK, 'E: torsion: '//TRIM(em)) + END IF + t1 = 2.0_wp*PI/(22.373_wp*SQRT(EI/(RHOA*LL**4))) + dt = t1/400.0_wp + nstep = 4000 + e_straight = 0.5_wp*(LL/GJ)*(GJ*phi/LL)**2 + CALL CD_HermiteCable_Dyn_Energy(m, ke, se, es, em) + e0 = ke + se + e_vib = e0 - e_straight + emax = e0 + emin = e0 + etmax = -HUGE(1.0_wp) + etmin = HUGE(1.0_wp) + DO k = 1, nstep + CALL CD_HermiteCable_Dyn_Step(m, dt, 40, 1.0e-10_wp, es, em) + IF (es /= CD_HCDYN_OK) THEN + CALL require(.FALSE., 'E: step: '//TRIM(em)) + EXIT + END IF + CALL CD_HermiteCable_Dyn_Energy(m, ke, se, es, em) + emax = nan_max_abs([emax, ke + se]) + emin = MIN(emin, ke + se) + IF (phi > 0.0_wp) THEN + CALL CD_HermiteCable_Dyn_Torsion_State(m, th, mt, es, em) + et = 0.5_wp*(LL/GJ)*mt*mt + etmax = nan_max_abs([etmax, et]) + etmin = MIN(etmin, et) + END IF + END DO + band = (emax - emin)/e_vib + t_exchange = 0.0_wp + IF (phi > 0.0_wp) t_exchange = (etmax - etmin)/e_vib + CALL CD_HermiteCable_Dyn_End(m) + END SUBROUTINE energy_run + + SUBROUTINE check_energy() + REAL(wp) :: band0, band1, ev0, ev1, tx0, tx1 + CALL energy_run(0.0_wp, band0, ev0, tx0) + CALL energy_run(4.0_wp, band1, ev1, tx1) + WRITE (*, '(A,ES10.3,A,ES10.3,A,ES10.3,A)') 'E energy band over 10 periods (rho_inf = 1): twisted ', band1, & + ', untwisted ', band0, ' of the vibration energy; torsional share swings by ', tx1, ' of it' + CALL require(band1 <= 5.0e-3_wp, 'E: kinetic + elastic + torsional energy conserved within 0.5 % (rho_inf = 1)') + CALL require(tx1 >= 1.0e-4_wp, 'E: the torsional energy exchanges with the bending (coupling active)') + END SUBROUTINE check_energy + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE drive(k, x, v, a, dcm, w, al, phi) + !! Coupled-end drive of the restart runs: a lateral sway with a roll of the parent about the + !! line axis (3 rad/s, more than a turn by step 250) and an imposed twist following it. + INTEGER, INTENT(IN) :: k + REAL(wp), INTENT(OUT) :: x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi + REAL(wp), PARAMETER :: DT = 0.01_wp, AMP = 0.05_wp, OM = 2.0_wp, ROLL_RATE = 3.0_wp + REAL(wp) :: t + t = DT*REAL(k, wp) + x = [LL, AMP*SIN(OM*t), 0.0_wp] + v = [0.0_wp, AMP*OM*COS(OM*t), 0.0_wp] + a = [0.0_wp, -AMP*OM*OM*SIN(OM*t), 0.0_wp] + dcm = roll_x(ROLL_RATE*t) + w = [ROLL_RATE, 0.0_wp, 0.0_wp] + al = 0.0_wp + phi = 1.0_wp - ROLL_RATE*t + 0.5_wp*SIN(OM*t) + END SUBROUTINE drive + + SUBROUTINE march(cab, k0, k1, ok) + TYPE(CD_HFMF_ModuleType), INTENT(INOUT) :: cab + INTEGER, INTENT(IN) :: k0, k1 + LOGICAL, INTENT(OUT) :: ok + REAL(wp) :: x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi + INTEGER :: k, es + CHARACTER(300) :: em + ok = .TRUE. + DO k = k0 + 1, k1 + CALL drive(k, x, v, a, dcm, w, al, phi) + CALL CD_HFMF_UpdateStates(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al, u_twist=phi) + IF (es /= CD_HFMF_OK) THEN + WRITE (*, '(A,I0,A)') ' restart march step ', k, ': '//TRIM(em) + ok = .FALSE. + RETURN + END IF + END DO + END SUBROUTINE march + + SUBROUTINE check_restart() + INTEGER, PARAMETER :: K_SNAP = 250, K_END = 400 + TYPE(CD_HFMF_ModuleType) :: cab, fresh + REAL(wp) :: q(NDOF), qa(NDOF), va(NDOF), tha, mta, th, mt, x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi + REAL(wp) :: t_snap + REAL(wp), ALLOCATABLE :: buf(:) + LOGICAL :: ok + INTEGER :: es + CHARACTER(300) :: em + CALL line_seed(0.0_wp, 0.0_wp, q) + CALL build_cable(cab, q, 1.0_wp, 0.01_wp, 1.0e-6_wp, 0.8_wp) + ! a restart is exact with cross-step factor reuse off (the factor's age is not state) + CALL CD_HermiteCable_Dyn_Set_TangentReuse(cab%line, .FALSE., es, em) + CALL march(cab, 0, K_SNAP, ok) + CALL require(ok, 'R: march to the snapshot') + CALL require(ABS(cab%line%torsion%theta) > 2.0_wp*PI, 'R: Theta has passed more than one turn at the snapshot') + t_snap = cab%line%t + CALL CD_HFMF_Snapshot(cab, es, em) + CALL require(es == CD_HFMF_OK, 'R: snapshot: '//TRIM(em)) + ALLOCATE (buf(CD_HFMF_MirrorSize(cab))) + CALL CD_HFMF_PackMirror(cab, buf, es, em) + CALL require(es == CD_HFMF_OK, 'R: pack mirror: '//TRIM(em)) + CALL march(cab, K_SNAP, K_END, ok) + CALL require(ok, 'R: uninterrupted march') + qa = cab%line%q + va = cab%line%v + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, tha, mta, es, em) + ! (1) restore the snapshot and re-run + CALL CD_HFMF_Restore(cab, es, em) + CALL require(es == CD_HFMF_OK, 'R: restore: '//TRIM(em)) + CALL march(cab, K_SNAP, K_END, ok) + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) + CALL require(ok .AND. nan_max_abs(cab%line%q - qa) <= 0.0_wp .AND. nan_max_abs(cab%line%v - va) <= 0.0_wp .AND. & + .NOT. (ABS(th - tha) > 0.0_wp) .AND. .NOT. (ABS(mt - mta) > 0.0_wp), & + 'R: snapshot/restore continues bit-identically (state, Theta, torque)') + ! (2) a fresh module from the same static state, reloaded from the mirror + CALL build_cable(fresh, q, 1.0_wp, 0.01_wp, 1.0e-6_wp, 0.8_wp) + CALL CD_HermiteCable_Dyn_Set_TangentReuse(fresh%line, .FALSE., es, em) + CALL drive(K_SNAP, x, v, a, dcm, w, al, phi) + CALL CD_HFMF_UnpackMirror(fresh, buf, t_snap, es, em) + CALL require(es == CD_HFMF_OK, 'R: unpack mirror: '//TRIM(em)) + CALL CD_HFMF_SetCoupledKinematics(fresh, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al) + CALL require(es == CD_HFMF_OK, 'R: re-prescribe the coupled end after the reload: '//TRIM(em)) + CALL march(fresh, K_SNAP, K_END, ok) + CALL CD_HermiteCable_Dyn_Torsion_State(fresh%line, th, mt, es, em) + WRITE (*, '(A,F10.4,A,ES10.3,A,ES10.3)') 'R restart: Theta ', th, ' rad, |dq| (mirror) ', & + nan_max_abs(fresh%line%q - qa), ', |dTheta| ', ABS(th - tha) + CALL require(ok .AND. nan_max_abs(fresh%line%q - qa) <= 0.0_wp .AND. nan_max_abs(fresh%line%v - va) <= 0.0_wp & + .AND. .NOT. (ABS(th - tha) > 0.0_wp), & + 'R: a checkpoint-mirror restart continues bit-identically (state, Theta)') + CALL CD_HFMF_End(cab) + CALL CD_HFMF_End(fresh) + END SUBROUTINE check_restart + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_boundary_and_mirror() + !! A the non-stepping boundary write is all or nothing: a coupled-end move together with a + !! parent roll of 2 rad (beyond the pi/2 twist step) is refused by the preflight and by + !! SetCoupledKinematics, and the cable state (q, v, a, frames, Theta, parent orientation) + !! is left exactly as it was; a mirror whose torsion frames are not orthonormal is refused + !! before anything is written; and a cable whose torsion frame turns with its parent but + !! has no parent end connection recovers the parent orientation from the restored frame, + !! so a restarted step without an orientation input holds it, as the uninterrupted run does. + TYPE(CD_HFMF_ModuleType) :: cab, fresh + REAL(wp) :: q(NDOF), x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi, q0(NDOF), v0(NDOF), a0(NDOF) + REAL(wp) :: ends0(3, 4), th0, pd0(3, 3), th, mt, tha, mta, t_snap + REAL(wp), ALLOCATABLE :: buf(:), bad(:) + LOGICAL :: ok, same + INTEGER :: es, k + CHARACTER(300) :: em + CALL line_seed(0.0_wp, 0.0_wp, q) + CALL build_cable(cab, q, 1.0_wp, 0.01_wp, 1.0e-6_wp, 0.8_wp, pinned=.TRUE.) + CALL march(cab, 0, 60, ok) + CALL require(ok, 'A: march before the boundary write') + q0 = cab%line%q + v0 = cab%line%v + a0 = cab%line%a + ends0 = cab%line%torsion%ends + th0 = cab%line%torsion%theta + pd0 = cab%parent_dcm + CALL drive(60, x, v, a, dcm, w, al, phi) + x(2) = x(2) + 0.01_wp + dcm = roll_x(3.0_wp*0.6_wp + 2.0_wp) + CALL CD_HFMF_PreflightCoupledKinematics(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al) + CALL require(es /= CD_HFMF_OK .AND. INDEX(em, 'pi/2') > 0, 'A: the preflight refuses a 2 rad frame turn: '//TRIM(em)) + CALL CD_HFMF_SetCoupledKinematics(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al) + same = nan_max_abs(cab%line%q - q0) <= 0.0_wp .AND. nan_max_abs(cab%line%v - v0) <= 0.0_wp .AND. & + nan_max_abs(cab%line%a - a0) <= 0.0_wp .AND. nan_max_abs(cab%line%torsion%ends - ends0) <= 0.0_wp .AND. & + .NOT. (ABS(cab%line%torsion%theta - th0) > 0.0_wp) .AND. nan_max_abs(cab%parent_dcm - pd0) <= 0.0_wp + CALL require(es /= CD_HFMF_OK .AND. same, 'A: a refused boundary write leaves the cable unchanged') + ! a corrupt mirror: the torsion block (last 14 values) with a non-unit reference normal + ALLOCATE (buf(CD_HFMF_MirrorSize(cab))) + CALL CD_HFMF_PackMirror(cab, buf, es, em) + bad = buf + k = SIZE(bad) - 14 + bad(k + 6:k + 8) = 1.1_wp*bad(k + 6:k + 8) + CALL CD_HFMF_UnpackMirror(cab, bad, cab%line%t, es, em) + CALL require(es /= CD_HFMF_OK .AND. INDEX(em, 'orthonormal') > 0 .AND. nan_max_abs(cab%line%q - q0) <= 0.0_wp, & + 'A: a mirror with non-orthonormal torsion frames is refused before any write') + ! restart without an orientation input after the reload + t_snap = cab%line%t + CALL drive(61, x, v, a, dcm, w, al, phi) + CALL CD_HFMF_UpdateStates(cab, x, v, a, es, em, u_twist=phi) + CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, tha, mta, es, em) + CALL build_cable(fresh, q, 1.0_wp, 0.01_wp, 1.0e-6_wp, 0.8_wp, pinned=.TRUE.) + CALL CD_HFMF_UnpackMirror(fresh, buf, t_snap, es, em) + CALL require(es == CD_HFMF_OK, 'A: unpack: '//TRIM(em)) + CALL CD_HFMF_UpdateStates(fresh, x, v, a, es, em, u_twist=phi) + CALL CD_HermiteCable_Dyn_Torsion_State(fresh%line, th, mt, es, em) + WRITE (*, '(A,ES12.5,A,ES12.5,A,ES10.3)') 'A restart without an orientation input: torque ', mt, & + ' (uninterrupted ', mta, '), parent orientation error ', nan_max_abs(fresh%parent_dcm - cab%parent_dcm) + CALL require(es == CD_HFMF_OK .AND. ABS(mt - mta) <= 1.0e-9_wp*ABS(mta) .AND. & + nan_max_abs(fresh%parent_dcm - cab%parent_dcm) <= 1.0e-12_wp, & + 'A: the restored parent orientation holds the frame (no turn back to the static orientation)') + CALL CD_HFMF_End(cab) + CALL CD_HFMF_End(fresh) + END SUBROUTINE check_boundary_and_mirror + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_bordered_and_blend() + !! The same swaying, rolling drive on a bowed line with and without torsion (a twist of about + !! half the buckling twist): the bordered step keeps the Newton solve count of the untwisted + !! line (a missing rank-one term would cost iterations), and torsion refuses the + !! configuration blend. + TYPE(CD_HFMF_ModuleType) :: cab + TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: q(NDOF), x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi, l0(NE), tt(6) + INTEGER :: k, es, pass, nst, nres, nsol(2), nam, nal, ntan + CHARACTER(300) :: em + CALL line_seed(0.02_wp, 0.01_wp, q) + l0 = LL/REAL(NE, wp) + DO pass = 1, 2 + CALL build_cable(cab, q, 3.0_wp, 0.01_wp, 1.0e-8_wp, 0.8_wp, pinned=.TRUE.) + IF (pass == 1) THEN + tors%active = .FALSE. + CALL CD_HermiteCable_Dyn_Set_Torsion(cab%line, tors, es, em) + END IF + CALL CD_HermiteCable_Dyn_Set_TangentReuse(cab%line, .FALSE., es, em) + CALL CD_HermiteCable_Dyn_Reset_Profile() + DO k = 1, 80 + CALL drive(k, x, v, a, dcm, w, al, phi) + x(2) = 10.0_wp*x(2) + v(2) = 10.0_wp*v(2) + a(2) = 10.0_wp*a(2) + IF (pass == 1) THEN + CALL CD_HFMF_UpdateStates(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al) + ELSE + CALL CD_HFMF_UpdateStates(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=w, & + u_angular_acceleration=al, u_twist=phi + 2.0_wp) + END IF + CALL require(es == CD_HFMF_OK, 'B: step: '//TRIM(em)) + IF (es /= CD_HFMF_OK) EXIT + END DO + CALL CD_HermiteCable_Dyn_Get_Profile(nst, nres, nsol(pass), nam, nal, tt(1), tt(2), tt(3), tt(4), tt(5), & + ntan, tt(6)) + CALL CD_HermiteCable_Dyn_Disable_Profile() + CALL CD_HFMF_End(cab) + END DO + WRITE (*, '(A,I0,A,I0,A)') 'B bordered step: ', nsol(2), ' Newton solves with torsion, ', nsol(1), & + ' without (80 steps)' + CALL require(nsol(2) <= nsol(1) + 80, 'B: the bordered step needs at most one more solve per step') + ! the configuration blend is refused by name + CALL CD_HermiteCable_Dyn_Init(m, l0, [(EA, k=1, NE)], [(EI, k=1, NE)], [(RHOA, k=1, NE)], [(0.0_wp, k=1, NE)], q, & + [1, 2, 3, NDOF - 5, NDOF - 4, NDOF - 3], 0.0_wp, 0.0_wp, 0.8_wp, es, em) + CALL CD_HermiteCable_Dyn_Set_EndConnection(m, [0.0_wp, 0.0_wp], & + RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], [3, 2]), & + es, em, connection_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID]) + CALL torsion_of(q, 1.0_wp, tors) + CALL CD_HermiteCable_Dyn_Set_Torsion(m, tors, es, em) + CALL CD_HermiteCable_Dyn_Set_ForceBlend(m, .FALSE., es, em) + CALL CD_HermiteCable_Dyn_Step(m, 0.01_wp, 30, 1.0e-8_wp, es, em) + CALL require(es /= CD_HCDYN_OK .AND. INDEX(em, 'force-blended') > 0, & + 'B: torsion refuses the configuration blend by name (got: '//TRIM(em)//')') + CALL CD_HermiteCable_Dyn_End(m) + END SUBROUTINE check_bordered_and_blend + +END PROGRAM test_hermite_torsion_dynamic diff --git a/tests/test_hermite_torsion_kernel.f90 b/tests/test_hermite_torsion_kernel.f90 new file mode 100644 index 0000000..c3a9e51 --- /dev/null +++ b/tests/test_hermite_torsion_kernel.f90 @@ -0,0 +1,904 @@ +! File: tests/test_hermite_torsion_kernel.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +PROGRAM test_hermite_torsion_kernel + !! Gates for the parallel-transport twist kernel of the Hermite cable (CableDyn_HermiteTorsion): + !! O Theta, dTheta/d[q, w_A, w_B] and every second derivative against automatic- + !! differentiation reference values (tests/data/torsion_oracle_reference.txt) on random + !! large-deformation lines with clamped and articulated ends, + !! F gradient and Hessian against central finite differences, including the end rotations, + !! B the DOF Hessian has half-bandwidth exactly 11, in DGBSV storage, and accumulates with + !! band_scale, + !! R invariance under a rigid rotation of the line and both end frames, + !! P Theta = 0 for planar curves (also a 1.5-turn circle), + !! H helix with Frenet end normals: Theta = -tau * length, + !! U 2 pi unwrapping through more than three turns, and the pi/2 step limit, + !! G fold guard (element and articulated end) and fail-closed input checks, + !! E the element routine against finite differences and against the line assembly, + !! W the allocation-free workspace evaluation (bit-identical, kept Hessian, size checks), + !! L the bordered solve on its direct, shifted and dense paths and its named failure, the + !! inertia count's input checks and unreliable count, and the compliance of a bad input. + !! Argument 1: path of the reference data file. + USE CableDyn_Precision, ONLY: wp + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsion_Element, CD_HermiteTorsion_Line, CD_HermiteTorsion_Fold_Check, & + CD_HermiteTorsion_Unwrap, CD_HermiteTorsion_Accept, CD_HTORS_OK, & + CD_HTORS_BADINPUT, CD_HTORS_FOLD, CD_HTORS_NONFINITE, CD_HTORS_STEP, & + CD_HTORS_KBAND, CD_HTORS_PI, CD_HermiteTorsionWorkType, & + CD_HermiteTorsion_Work_Init, CD_HermiteTorsion_Work_End, & + CD_HermiteTorsion_Work_Add_Hessian, CD_HermiteTorsion_Bordered_Solve, & + CD_HermiteTorsion_Inertia, CD_HermiteTorsion_Compliance, CD_HermiteTorsionType, & + CD_HTORS_ILLCOND, CD_HTORS_UNRELIABLE, CD_HTORS_DENSE_MAX + USE, INTRINSIC :: IEEE_ARITHMETIC, ONLY: IEEE_IS_NAN, IEEE_VALUE, IEEE_QUIET_NAN + IMPLICIT NONE + + INTEGER, PARAMETER :: KB = CD_HTORS_KBAND, LDAB = 3*KB + 1 + REAL(wp), PARAMETER :: TWO_PI = 2.0_wp*CD_HTORS_PI + INTEGER :: nfail + CHARACTER(512) :: ref_path + + nfail = 0 + IF (COMMAND_ARGUMENT_COUNT() < 1) THEN + WRITE (*, '(A)') 'usage: test_hermite_torsion_kernel ' + ERROR STOP 2 + END IF + CALL GET_COMMAND_ARGUMENT(1, ref_path) + + CALL check_oracle_reference() + CALL check_finite_differences() + CALL check_band_layout() + CALL check_workspace() + CALL check_rigid_rotation() + CALL check_planar() + CALL check_helix_and_unwrap() + CALL check_accept_rule() + CALL check_fold_and_inputs() + CALL check_element_routine() + CALL check_linear_algebra() + + IF (nfail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', nfail, ' assertion(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: Hermite torsion kernel matches the reference and analytic cases' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(ok, label) + LOGICAL, INTENT(IN) :: ok + CHARACTER(*), INTENT(IN) :: label + IF (.NOT. ok) THEN + nfail = nfail + 1 + WRITE (*, '(A)') 'FAIL: '//label + END IF + END SUBROUTINE require + + REAL(wp) FUNCTION wrap(x) + REAL(wp), INTENT(IN) :: x + wrap = x - TWO_PI*ANINT(x/TWO_PI) + END FUNCTION wrap + + ! --------------------------------------------------------------------------------------- + ! geometry helpers + ! --------------------------------------------------------------------------------------- + + FUNCTION cross(a, b) RESULT(c) + REAL(wp), INTENT(IN) :: a(3), b(3) + REAL(wp) :: c(3) + c = [a(2)*b(3) - a(3)*b(2), a(3)*b(1) - a(1)*b(3), a(1)*b(2) - a(2)*b(1)] + END FUNCTION cross + + FUNCTION rotate(w, v) RESULT(r) + !! exp(w) v (Rodrigues). + REAL(wp), INTENT(IN) :: w(3), v(3) + REAL(wp) :: r(3), th, k(3) + th = NORM2(w) + IF (th < 1.0e-300_wp) THEN + r = v + RETURN + END IF + k = w/th + r = v*COS(th) + cross(k, v)*SIN(th) + k*DOT_PRODUCT(k, v)*(1.0_wp - COS(th)) + END FUNCTION rotate + + SUBROUTINE random_line(nel, seed, q, le) + !! Smooth random 3D line (several radians of tangent turning) with random nodal stretch. + INTEGER, INTENT(IN) :: nel, seed + REAL(wp), ALLOCATABLE, INTENT(OUT) :: q(:), le(:) + REAL(wp) :: ca(3, 3), cb(3, 3), s, w, stretch, total + INTEGER :: k, md, i + total = 6.0_wp + ALLOCATE (q(6*(nel + 1)), le(nel)) + DO md = 1, 3 + DO i = 1, 3 + ca(i, md) = 0.9_wp*SIN(1.7_wp*i + 2.3_wp*md + 0.37_wp*seed)/md + cb(i, md) = 0.9_wp*COS(0.6_wp*i - 1.1_wp*md + 0.53_wp*seed)/md + END DO + END DO + DO k = 0, nel + s = total*k/nel + q(6*k + 1:6*k + 6) = 0.0_wp + q(6*k + 1) = s + q(6*k + 4) = 1.0_wp + DO md = 1, 3 + w = CD_HTORS_PI*md/total + q(6*k + 1:6*k + 3) = q(6*k + 1:6*k + 3) + (ca(:, md)*SIN(w*s) + cb(:, md)*(COS(w*s) - 1.0_wp))*total/CD_HTORS_PI + q(6*k + 4:6*k + 6) = q(6*k + 4:6*k + 6) + (ca(:, md)*w*COS(w*s) - cb(:, md)*w*SIN(w*s))*total/CD_HTORS_PI + END DO + stretch = 1.0_wp + 0.08_wp*SIN(3.1_wp*k + seed) + q(6*k + 4:6*k + 6) = stretch*q(6*k + 4:6*k + 6) + END DO + le = total/nel + END SUBROUTINE random_line + + FUNCTION clamped_ends(q) RESULT(ends) + REAL(wp), INTENT(IN) :: q(:) + REAL(wp) :: ends(3, 4), ta(3), tb(3), sd(3) + INTEGER :: n + n = SIZE(q) + ta = q(4:6)/NORM2(q(4:6)) + tb = q(n - 2:n)/NORM2(q(n - 2:n)) + sd = [0.0_wp, 0.0_wp, 1.0_wp] + IF (ABS(ta(3)) > 0.9_wp) sd = [0.0_wp, 1.0_wp, 0.0_wp] + ends(:, 1) = ta + ends(:, 2) = (sd - DOT_PRODUCT(sd, ta)*ta)/NORM2(sd - DOT_PRODUCT(sd, ta)*ta) + ends(:, 3) = tb + ends(:, 4) = (ends(:, 2) - DOT_PRODUCT(ends(:, 2), tb)*tb)/NORM2(ends(:, 2) - DOT_PRODUCT(ends(:, 2), tb)*tb) + END FUNCTION clamped_ends + + FUNCTION articulated_ends(q) RESULT(ends) + !! End directors 30-40 degrees off the end tangents, normals perpendicular to them. + REAL(wp), INTENT(IN) :: q(:) + REAL(wp) :: ends(3, 4) + ends = clamped_ends(q) + ends(:, 1:2) = RESHAPE([rotate([0.3_wp, -0.4_wp, 0.2_wp], ends(:, 1)), & + rotate([0.3_wp, -0.4_wp, 0.2_wp], ends(:, 2))], [3, 2]) + ends(:, 3:4) = RESHAPE([rotate([-0.5_wp, 0.1_wp, 0.35_wp], ends(:, 3)), & + rotate([-0.5_wp, 0.1_wp, 0.35_wp], ends(:, 4))], [3, 2]) + END FUNCTION articulated_ends + + FUNCTION rotated_ends(ends, wa, wb) RESULT(r) + REAL(wp), INTENT(IN) :: ends(3, 4), wa(3), wb(3) + REAL(wp) :: r(3, 4) + r(:, 1) = rotate(wa, ends(:, 1)) + r(:, 2) = rotate(wa, ends(:, 2)) + r(:, 3) = rotate(wb, ends(:, 3)) + r(:, 4) = rotate(wb, ends(:, 4)) + END FUNCTION rotated_ends + + SUBROUTINE evaluate(q, le, ends, th, g, hb, eg, eh, ec, es) + REAL(wp), INTENT(IN) :: q(:), le(:), ends(3, 4) + REAL(wp), INTENT(OUT) :: th, g(:), hb(:, :), eg(6), eh(3, 3, 2), ec(3, 3, 2) + INTEGER, INTENT(OUT) :: es + CHARACTER(200) :: em + hb = 0.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, hband=hb, end_grad=eg, end_hess=eh, end_cross=ec) + IF (es /= CD_HTORS_OK) WRITE (*, '(A)') ' kernel: '//TRIM(em) + END SUBROUTINE evaluate + + REAL(wp) FUNCTION theta_of(q, le, ends) RESULT(th) + REAL(wp), INTENT(IN) :: q(:), le(:), ends(3, 4) + REAL(wp), ALLOCATABLE :: g(:) + INTEGER :: es + CHARACTER(200) :: em + ALLOCATE (g(SIZE(q))) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em) + IF (es /= CD_HTORS_OK) th = HUGE(1.0_wp) + END FUNCTION theta_of + + FUNCTION band_to_dense(hb, n) RESULT(h) + REAL(wp), INTENT(IN) :: hb(:, :) + INTEGER, INTENT(IN) :: n + REAL(wp) :: h(n, n) + INTEGER :: i, j + h = 0.0_wp + DO j = 1, n + DO i = MAX(1, j - KB), MIN(n, j + KB) + h(i, j) = hb(SIZE(hb, 1) - KB + i - j, j) + END DO + END DO + END FUNCTION band_to_dense + + ! --------------------------------------------------------------------------------------- + ! O: automatic-differentiation reference + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_oracle_reference() + INTEGER :: unit, ios, ncase, ic, nn, ng, n, i, j, es, nb + CHARACTER(256) :: line, name + REAL(wp), ALLOCATABLE :: le(:), q(:), g_ref(:), band_ref(:), g(:), hb(:, :), h(:, :) + REAL(wp) :: ends(3, 4), th_ref, th, eg(6), eh(3, 3, 2), ec(3, 3, 2), blocks(9, 4), href(3, 3, 4) + REAL(wp) :: err_g, err_h, err_e, scale_h + + OPEN (NEWUNIT=unit, FILE=TRIM(ref_path), STATUS='OLD', ACTION='READ', IOSTAT=ios) + CALL require(ios == 0, 'O: open reference data '//TRIM(ref_path)) + IF (ios /= 0) RETURN + DO + READ (unit, '(A)') line + IF (line(1:1) /= '#') EXIT + END DO + READ (line, *) ncase + CALL require(ncase == 4, 'O: four reference cases') + DO ic = 1, ncase + READ (unit, '(A)') name + READ (unit, *) nn, ng + n = 6*nn + ALLOCATE (le(nn - 1), q(n), g_ref(n + 6), g(n), hb(LDAB, n)) + nb = 0 + DO j = 1, n + nb = nb + MIN(n, j + KB) - j + 1 + END DO + ALLOCATE (band_ref(nb)) + READ (unit, *) le, q, ends, th_ref, g_ref, band_ref, blocks + CALL evaluate(q, le, ends, th, g, hb, eg, eh, ec, es) + CALL require(es == CD_HTORS_OK .AND. ng == 4, 'O: evaluate '//TRIM(name)) + h = band_to_dense(hb, n) + err_h = 0.0_wp + scale_h = MAXVAL(ABS(band_ref)) + nb = 0 + DO j = 1, n + DO i = j, MIN(n, j + KB) + nb = nb + 1 + err_h = nan_max_abs([err_h, ABS(h(i, j) - band_ref(nb)), ABS(h(j, i) - band_ref(nb))]) + END DO + END DO + err_h = err_h/scale_h + err_g = nan_max_abs([g, eg] - g_ref)/MAXVAL(ABS(g_ref)) + href = RESHAPE(blocks, [3, 3, 4]) + err_e = MAX(nan_max_abs(eh(:, :, 1) - href(:, :, 1)), nan_max_abs(eh(:, :, 2) - href(:, :, 2)), & + nan_max_abs(ec(:, :, 1) - href(:, :, 3)), nan_max_abs(ec(:, :, 2) - href(:, :, 4)))/scale_h + WRITE (*, '(A,A,ES9.2,A,ES9.2,A,ES9.2,A,ES9.2)') ' O ', TRIM(name)//': Theta err ', & + ABS(wrap(th - th_ref)), ', grad rel ', err_g, ', Hessian rel ', err_h, ', end blocks rel ', err_e + CALL require(ABS(wrap(th - th_ref)) <= 1.0e-12_wp*MAX(1.0_wp, ABS(th_ref)), 'O: Theta '//TRIM(name)) + CALL require(err_g <= 1.0e-12_wp, 'O: gradient '//TRIM(name)) + CALL require(err_h <= 1.0e-12_wp, 'O: DOF Hessian '//TRIM(name)) + CALL require(err_e <= 1.0e-12_wp, 'O: end-rotation Hessian blocks '//TRIM(name)) + DEALLOCATE (le, q, g_ref, g, hb, band_ref, h) + END DO + CLOSE (unit) + END SUBROUTINE check_oracle_reference + + ! --------------------------------------------------------------------------------------- + ! F: finite differences (independent of the reference data) + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_finite_differences() + REAL(wp), ALLOCATABLE :: q(:), le(:), g(:), hb(:, :), h(:, :), gp(:), gm(:), hfd(:, :), dq(:) + REAL(wp) :: ends(3, 4), th, eg(6), eh(3, 3, 2), ec(3, 3, 2), egfd(6), ehfd(3, 3, 2), ecfd(3, 3, 2) + REAL(wp) :: tp, tm, step, wa(3), wb(3), w6(6), e_i(3), e_j(3), t4(4), dum + REAL(wp) :: hbx(LDAB, 1), egx(6), ehx(3, 3, 2), ecx(3, 3, 2) + INTEGER :: n, i, j, k, es, iend + CHARACTER(16) :: tag + CHARACTER(200) :: em + + DO iend = 1, 2 + CALL random_line(10, 3 + iend, q, le) + IF (iend == 1) THEN + ends = clamped_ends(q) + tag = 'clamped' + ELSE + ends = articulated_ends(q) + tag = 'articulated' + END IF + n = SIZE(q) + ALLOCATE (g(n), hb(LDAB, n), gp(n), gm(n), hfd(n, n), dq(n)) + CALL evaluate(q, le, ends, th, g, hb, eg, eh, ec, es) + CALL require(es == CD_HTORS_OK, 'F: evaluate '//TRIM(tag)) + h = band_to_dense(hb, n) + step = 1.0e-6_wp + DO i = 1, n + dq = 0.0_wp + dq(i) = step + tp = theta_of(q + dq, le, ends) + tm = theta_of(q - dq, le, ends) + gp(i) = wrap(tp - tm)/(2.0_wp*step) + END DO + CALL require(nan_max_abs(gp - g) <= 1.0e-7_wp*MAXVAL(ABS(g)), 'F: gradient vs FD '//TRIM(tag)) + step = 1.0e-6_wp + DO i = 1, n + dq = 0.0_wp + dq(i) = step + CALL CD_HermiteTorsion_Line(q + dq, le, ends, dum, gp, es, em) + CALL CD_HermiteTorsion_Line(q - dq, le, ends, dum, gm, es, em) + hfd(:, i) = (gp - gm)/(2.0_wp*step) + END DO + WRITE (*, '(A,ES9.2)') ' F '//TRIM(tag)//': Hessian vs FD rel ', nan_max_abs(hfd - h)/MAXVAL(ABS(h)) + CALL require(nan_max_abs(hfd - h) <= 1.0e-6_wp*MAXVAL(ABS(h)), 'F: Hessian vs FD '//TRIM(tag)) + ! B: half-bandwidth exactly 11 -- nothing outside the band, something on its edge + dum = 0.0_wp + tp = 0.0_wp + DO j = 1, n + DO i = 1, n + IF (ABS(i - j) > KB) dum = nan_max_abs([dum, ABS(hfd(i, j))]) + IF (ABS(i - j) == KB) tp = nan_max_abs([tp, ABS(h(i, j))]) + END DO + END DO + CALL require(dum <= 1.0e-7_wp*MAXVAL(ABS(h)), 'B: FD Hessian vanishes outside the band '//TRIM(tag)) + CALL require(tp >= 1.0e-3_wp*MAXVAL(ABS(h)), 'B: Hessian reaches the band edge 11 '//TRIM(tag)) + + ! end rotations: gradient and Hessians by differences of exp(w)-rotated end frames + step = 1.0e-6_wp + DO k = 1, 6 + w6 = 0.0_wp + w6(k) = step + wa = w6(1:3) + wb = w6(4:6) + tp = theta_of(q, le, rotated_ends(ends, wa, wb)) + tm = theta_of(q, le, rotated_ends(ends, -wa, -wb)) + egfd(k) = wrap(tp - tm)/(2.0_wp*step) + CALL CD_HermiteTorsion_Line(q, le, rotated_ends(ends, wa, wb), dum, gp, es, em) + CALL CD_HermiteTorsion_Line(q, le, rotated_ends(ends, -wa, -wb), dum, gm, es, em) + IF (k <= 3) ecfd(k, :, 1) = (gp(4:6) - gm(4:6))/(2.0_wp*step) + IF (k > 3) ecfd(k - 3, :, 2) = (gp(n - 2:n) - gm(n - 2:n))/(2.0_wp*step) + END DO + step = 1.0e-4_wp + DO k = 1, 2 + DO j = 1, 3 + DO i = 1, 3 + e_i = 0.0_wp + e_j = 0.0_wp + e_i(i) = step + e_j(j) = step + IF (k == 1) THEN + t4 = [theta_of(q, le, rotated_ends(ends, e_i + e_j, [0.0_wp, 0.0_wp, 0.0_wp])), & + theta_of(q, le, rotated_ends(ends, e_i - e_j, [0.0_wp, 0.0_wp, 0.0_wp])), & + theta_of(q, le, rotated_ends(ends, -e_i + e_j, [0.0_wp, 0.0_wp, 0.0_wp])), & + theta_of(q, le, rotated_ends(ends, -e_i - e_j, [0.0_wp, 0.0_wp, 0.0_wp]))] + ELSE + t4 = [theta_of(q, le, rotated_ends(ends, [0.0_wp, 0.0_wp, 0.0_wp], e_i + e_j)), & + theta_of(q, le, rotated_ends(ends, [0.0_wp, 0.0_wp, 0.0_wp], e_i - e_j)), & + theta_of(q, le, rotated_ends(ends, [0.0_wp, 0.0_wp, 0.0_wp], -e_i + e_j)), & + theta_of(q, le, rotated_ends(ends, [0.0_wp, 0.0_wp, 0.0_wp], -e_i - e_j))] + END IF + ehfd(i, j, k) = (wrap(t4(1) - t4(2)) - wrap(t4(3) - t4(4)))/(4.0_wp*step*step) + END DO + END DO + END DO + WRITE (*, '(A,3ES9.2)') ' F '//TRIM(tag)//': end grad / end Hessian / cross vs FD ', & + nan_max_abs(egfd - eg), nan_max_abs(ehfd - eh), nan_max_abs(ecfd - ec) + CALL require(nan_max_abs(egfd - eg) <= 1.0e-8_wp, 'F: end-rotation gradient '//TRIM(tag)) + CALL require(nan_max_abs(ehfd - eh) <= 1.0e-6_wp*MAX(1.0_wp, MAXVAL(ABS(eh))), & + 'F: end-rotation Hessian '//TRIM(tag)) + CALL require(nan_max_abs(ecfd - ec) <= 1.0e-6_wp*MAX(1.0_wp, MAXVAL(ABS(ec))), & + 'F: end-rotation / tangent cross Hessian '//TRIM(tag)) + ! a rotation of an end frame about its own director changes Theta by exactly that angle + CALL require(ABS(DOT_PRODUCT(eg(1:3), ends(:, 1)) - 1.0_wp) <= 1.0e-12_wp .AND. & + ABS(DOT_PRODUCT(eg(4:6), ends(:, 3)) + 1.0_wp) <= 1.0e-12_wp, 'F: end roll enters with unit weight') + DEALLOCATE (g, hb, gp, gm, hfd, dq, h) + END DO + ! the optional end outputs need no band + CALL random_line(4, 1, q, le) + ends = clamped_ends(q) + ALLOCATE (g(SIZE(q))) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, end_grad=egx, end_hess=ehx, end_cross=ecx) + CALL require(es == CD_HTORS_OK .AND. ALL(ABS(egx) < 1.0e3_wp), 'F: end outputs without band') + hbx = 0.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, hband=hbx) + CALL require(es == CD_HTORS_BADINPUT, 'F: undersized band rejected') + END SUBROUTINE check_finite_differences + + ! --------------------------------------------------------------------------------------- + ! B: band layout and accumulation + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_band_layout() + REAL(wp), ALLOCATABLE :: q(:), le(:), g(:), h34(:, :), h23(:, :), hacc(:, :) + REAL(wp) :: ends(3, 4), th + INTEGER :: n, es + CHARACTER(200) :: em + CALL random_line(6, 2, q, le) + ends = articulated_ends(q) + n = SIZE(q) + ALLOCATE (g(n), h34(LDAB, n), h23(2*KB + 1, n), hacc(LDAB, n)) + h34 = 0.0_wp + h23 = 0.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, hband=h34) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, hband=h23) + CALL require(nan_max_abs(band_to_dense(h34, n) - band_to_dense(h23, n)) <= 0.0_wp, & + 'B: DGBSV and DGBMV layouts carry the same Hessian') + CALL require(nan_max_abs(h34(1:KB, :)) <= 0.0_wp, 'B: DGBSV fill rows untouched') + CALL require(nan_max_abs(band_to_dense(h34, n) - TRANSPOSE(band_to_dense(h34, n))) <= & + 1.0e-14_wp*MAXVAL(ABS(h34)), 'B: Hessian symmetric') + hacc = 1.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, hband=hacc, band_scale=-2.5_wp) + CALL require(nan_max_abs(hacc - 1.0_wp + 2.5_wp*h34) <= 1.0e-13_wp*MAXVAL(ABS(h34)), & + 'B: band_scale accumulates into the band') + END SUBROUTINE check_band_layout + + ! --------------------------------------------------------------------------------------- + ! W: workspace variant (no allocation in the evaluation) + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_workspace() + !! The workspace evaluation returns bit-identical values to the allocating one; keep_hessian + !! with CD_HermiteTorsion_Work_Add_Hessian equals hband with band_scale; a workspace sized for + !! another line, or released, is refused by name. + REAL(wp), ALLOCATABLE :: q(:), le(:), g1(:), g2(:), h1(:, :), h2(:, :) + REAL(wp) :: ends(3, 4), th1, th2, eg1(6), eg2(6) + TYPE(CD_HermiteTorsionWorkType) :: work + INTEGER :: n, es, k + CHARACTER(200) :: em + CALL random_line(16, 2, q, le) + n = SIZE(q) + ALLOCATE (g1(n), g2(n), h1(LDAB, n), h2(LDAB, n)) + CALL CD_HermiteTorsion_Work_Init(work, n/6, es, em) + CALL require(es == CD_HTORS_OK, 'W: workspace sized') + DO k = 1, 2 + IF (k == 1) THEN + ends = clamped_ends(q) + ELSE + ends = articulated_ends(q) + END IF + h1 = 0.0_wp + h2 = 0.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th1, g1, es, em, hband=h1, band_scale=-3.0_wp, end_grad=eg1) + CALL CD_HermiteTorsion_Line(q, le, ends, th2, g2, es, em, end_grad=eg2, work=work, keep_hessian=.TRUE.) + CALL require(es == CD_HTORS_OK, 'W: workspace evaluation succeeds '//TRIM(em)) + CALL CD_HermiteTorsion_Work_Add_Hessian(work, h2, -3.0_wp, es, em) + CALL require(es == CD_HTORS_OK, 'W: the kept Hessian is added '//TRIM(em)) + CALL require(.NOT. (ABS(th1 - th2) > 0.0_wp) .AND. nan_max_abs(g1 - g2) <= 0.0_wp .AND. & + nan_max_abs(eg1 - eg2) <= 0.0_wp, 'W: workspace Theta and gradients bit-identical') + CALL require(nan_max_abs(h1 - h2) <= 0.0_wp, 'W: kept Hessian scaled later equals band_scale') + h2 = 0.0_wp + CALL CD_HermiteTorsion_Line(q, le, ends, th2, g2, es, em, hband=h2, band_scale=-3.0_wp, work=work) + CALL require(nan_max_abs(h1 - h2) <= 0.0_wp, 'W: workspace hband path bit-identical') + ! that call kept no Hessian: adding one now fails and leaves the band unchanged + h1 = h2 + CALL CD_HermiteTorsion_Work_Add_Hessian(work, h2, 1.0_wp, es, em) + CALL require(es == CD_HTORS_BADINPUT .AND. INDEX(em, 'no Hessian') > 0 .AND. nan_max_abs(h1 - h2) <= 0.0_wp, & + 'W: adding a Hessian that was not kept is refused') + CALL CD_HermiteTorsion_Line(q, le, ends, th2, g2, es, em, work=work, keep_hessian=.TRUE.) + CALL CD_HermiteTorsion_Work_Add_Hessian(work, h2(:, 1:n - 6), 1.0_wp, es, em) + CALL require(es == CD_HTORS_BADINPUT .AND. INDEX(em, 'does not match') > 0, & + 'W: a band of another size is refused') + END DO + CALL CD_HermiteTorsion_Work_Init(work, n/6 + 1, es, em) + CALL CD_HermiteTorsion_Line(q, le, ends, th2, g2, es, em, work=work) + CALL require(es == CD_HTORS_BADINPUT .AND. INDEX(em, 'workspace is not sized') > 0 .AND. & + nan_max_abs(g2) <= 0.0_wp, 'W: a workspace of another size is refused') + CALL CD_HermiteTorsion_Work_End(work) + CALL CD_HermiteTorsion_Line(q, le, ends, th2, g2, es, em, work=work) + CALL require(es == CD_HTORS_BADINPUT, 'W: a released workspace is refused') + CALL CD_HermiteTorsion_Work_Init(work, 1, es, em) + CALL require(es == CD_HTORS_BADINPUT, 'W: a one-node workspace is refused') + END SUBROUTINE check_workspace + + ! --------------------------------------------------------------------------------------- + ! R: rigid rotation of everything + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_rigid_rotation() + REAL(wp), ALLOCATABLE :: q(:), le(:), q2(:), g(:), g2(:), grot(:) + REAL(wp) :: ends(3, 4), ends2(3, 4), th, th2, w(3), err_t, err_g, err_gen, eg(6), gen(3) + INTEGER :: n, k, ir, iend, es + CHARACTER(200) :: em + err_t = 0.0_wp + err_g = 0.0_wp + err_gen = 0.0_wp + DO iend = 1, 2 + CALL random_line(12, 7, q, le) + ends = clamped_ends(q) + IF (iend == 2) ends = articulated_ends(q) + n = SIZE(q) + ALLOCATE (q2(n), g(n), g2(n), grot(n)) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em, end_grad=eg) + ! infinitesimal form: the rotation generator annihilates Theta, + ! sum_k (r_k x dTheta/dr_k + m_k x dTheta/dm_k) + dTheta/dw_A + dTheta/dw_B = 0 + gen = eg(1:3) + eg(4:6) + DO k = 0, n/3 - 1 + gen = gen + cross(q(3*k + 1:3*k + 3), g(3*k + 1:3*k + 3)) + END DO + err_gen = nan_max_abs([err_gen, nan_max_abs(gen)/MAXVAL(ABS(g))]) + DO ir = 1, 5 + w = [0.7_wp*ir, -1.3_wp + 0.4_wp*ir, 2.1_wp - 0.9_wp*ir] + DO k = 0, n/3 - 1 + q2(3*k + 1:3*k + 3) = rotate(w, q(3*k + 1:3*k + 3)) + grot(3*k + 1:3*k + 3) = rotate(w, g(3*k + 1:3*k + 3)) + END DO + DO k = 1, 4 + ends2(:, k) = rotate(w, ends(:, k)) + END DO + CALL CD_HermiteTorsion_Line(q2, le, ends2, th2, g2, es, em) + err_t = nan_max_abs([err_t, ABS(wrap(th2 - th))]) + err_g = nan_max_abs([err_g, nan_max_abs(g2 - grot)/MAXVAL(ABS(g))]) + END DO + DEALLOCATE (q2, g, g2, grot) + END DO + WRITE (*, '(A,3ES9.2)') ' R rigid rotation: max |dTheta|, gradient covariance rel, generator ', & + err_t, err_g, err_gen + CALL require(err_gen <= 1.0e-13_wp, 'R: rotation generator annihilates Theta (end gradients included)') + CALL require(err_t <= 1.0e-13_wp, 'R: Theta invariant under rigid rotation') + CALL require(err_g <= 1.0e-12_wp, 'R: gradient rotates with the line') + END SUBROUTINE check_rigid_rotation + + ! --------------------------------------------------------------------------------------- + ! P: planar curves + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_planar() + REAL(wp), ALLOCATABLE :: q(:), le(:), g(:), hb(:, :) + REAL(wp) :: ends(3, 4), th, s, rc, ez(3), eg(6), eh(3, 3, 2), ec(3, 3, 2) + INTEGER :: k, n, es, nc + CHARACTER(200) :: em + ez = [0.0_wp, 0.0_wp, 1.0_wp] + CALL random_line(14, 5, q, le) + n = SIZE(q) + DO k = 0, n/6 - 1 + q(6*k + 3) = 0.0_wp + q(6*k + 6) = 0.0_wp + END DO + ends(:, 1) = q(4:6)/NORM2(q(4:6)) + ends(:, 2) = ez + ends(:, 3) = q(n - 2:n)/NORM2(q(n - 2:n)) + ends(:, 4) = ez + ALLOCATE (g(n), hb(LDAB, n)) + CALL evaluate(q, le, ends, th, g, hb, eg, eh, ec, es) + CALL require(es == CD_HTORS_OK .AND. ABS(th) <= 1.0e-14_wp, 'P: random planar curve has Theta = 0') + ! out-of-plane gradient only: in-plane perturbations keep Theta = 0 + DO k = 0, n/6 - 1 + CALL require(ABS(g(6*k + 1)) + ABS(g(6*k + 2)) + ABS(g(6*k + 4)) + ABS(g(6*k + 5)) <= 1.0e-13_wp, & + 'P: in-plane gradient vanishes') + END DO + DEALLOCATE (q, le, g, hb) + ! 1.5-turn circle, 540 degrees of tangent turning + rc = 2.0_wp + nc = 36 + ALLOCATE (q(6*(nc + 1)), le(nc), g(6*(nc + 1))) + DO k = 0, nc + s = 3.0_wp*CD_HTORS_PI*rc*k/nc + q(6*k + 1:6*k + 6) = [rc*SIN(s/rc), rc*(1.0_wp - COS(s/rc)), 0.0_wp, COS(s/rc), SIN(s/rc), 0.0_wp] + END DO + le = 3.0_wp*CD_HTORS_PI*rc/nc + n = SIZE(q) + ends(:, 1) = q(4:6) + ends(:, 2) = ez + ends(:, 3) = q(n - 2:n) + ends(:, 4) = ez + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em) + CALL require(es == CD_HTORS_OK .AND. ABS(th) <= 1.0e-14_wp, 'P: 1.5-turn circle has Theta = 0') + END SUBROUTINE check_planar + + ! --------------------------------------------------------------------------------------- + ! H, U: helix closed form and multi-turn unwrapping + ! --------------------------------------------------------------------------------------- + + SUBROUTINE helix(nel, ell, q, le, ends, expected) + !! r = (R cos(s/k), R sin(s/k), c s/k), k = sqrt(R^2 + c^2); clamped with Frenet normals. + INTEGER, INTENT(IN) :: nel + REAL(wp), INTENT(IN) :: ell + REAL(wp), ALLOCATABLE, INTENT(OUT) :: q(:), le(:) + REAL(wp), INTENT(OUT) :: ends(3, 4), expected + REAL(wp), PARAMETER :: rh = 1.0_wp, ch = 0.5_wp + REAL(wp) :: kh, s + INTEGER :: k + kh = SQRT(rh*rh + ch*ch) + ALLOCATE (q(6*(nel + 1)), le(nel)) + DO k = 0, nel + s = ell*k/nel + q(6*k + 1:6*k + 6) = [rh*COS(s/kh), rh*SIN(s/kh), ch*s/kh, -rh*SIN(s/kh)/kh, rh*COS(s/kh)/kh, ch/kh] + END DO + le = ell/nel + ends(:, 1) = q(4:6) + ends(:, 2) = [-1.0_wp, 0.0_wp, 0.0_wp] + ends(:, 3) = q(6*nel + 4:6*nel + 6) + ends(:, 4) = [-COS(ell/kh), -SIN(ell/kh), 0.0_wp] + expected = -ch/(kh*kh)*ell + END SUBROUTINE helix + + SUBROUTINE check_helix_and_unwrap() + REAL(wp), ALLOCATABLE :: q(:), le(:), g(:) + REAL(wp) :: ends(3, 4), expected, th, raw, raw_prev, prev, err, err8, ell + INTEGER :: es, i, jumps, nsweep + CHARACTER(200) :: em + ! H: one turn of a helix, 12 and 48 elements (fourth-order quadrature convergence) + CALL helix(12, 7.0_wp, q, le, ends, expected) + ALLOCATE (g(SIZE(q))) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em) + err8 = ABS(wrap(th - expected)) + DEALLOCATE (q, le, g) + CALL helix(48, 7.0_wp, q, le, ends, expected) + ALLOCATE (g(SIZE(q))) + CALL CD_HermiteTorsion_Line(q, le, ends, th, g, es, em) + err = ABS(wrap(th - expected)) + DEALLOCATE (q, le, g) + WRITE (*, '(A,2ES9.2)') ' H helix Theta = -tau l: error at 12 / 48 elements ', err8, err + CALL require(es == CD_HTORS_OK .AND. err <= 2.0e-6_wp, 'H: helix Theta = -tau * length') + CALL require(err8 > 16.0_wp*err, 'H: quadrature error converges at high order') + + ! U: sweep the helix length through more than three turns of Theta, one accepted step at a time + nsweep = 400 + prev = 0.0_wp + jumps = 0 + err = 0.0_wp + DO i = 1, nsweep + ell = 0.05_wp + (50.0_wp - 0.05_wp)*REAL(i - 1, wp)/(nsweep - 1) + CALL helix(96, ell, q, le, ends, expected) + ALLOCATE (g(SIZE(q))) + CALL CD_HermiteTorsion_Line(q, le, ends, raw, g, es, em) + IF (i == 1) THEN + prev = raw + ELSE + IF (ABS(raw - raw_prev) > CD_HTORS_PI) jumps = jumps + 1 + CALL CD_HermiteTorsion_Accept(prev, raw, th, es, em) + CALL require(es == CD_HTORS_OK, 'U: sweep step accepted') + prev = th + END IF + raw_prev = raw + err = nan_max_abs([err, ABS(prev - expected)]) + DEALLOCATE (q, le, g) + END DO + WRITE (*, '(A,F8.3,A,I0,A,ES9.2)') ' U unwrap: final Theta ', prev, ' rad, raw branch jumps ', jumps, & + ', max error vs -tau l ', err + CALL require(prev < -3.0_wp*CD_HTORS_PI, 'U: sweep passes more than 1.5 turns') + CALL require(jumps >= 3, 'U: raw value wraps at least three times') + CALL require(err <= 2.0e-3_wp, 'U: unwrapped Theta follows the multi-turn closed form') + END SUBROUTINE check_helix_and_unwrap + + SUBROUTINE check_accept_rule() + REAL(wp) :: th + INTEGER :: es + CHARACTER(200) :: em + CALL require(ABS(CD_HermiteTorsion_Unwrap(3.1_wp, -3.1_wp) - (3.1_wp - TWO_PI)) <= 1.0e-15_wp, & + 'U: nearest branch across +-pi') + CALL require(ABS(CD_HermiteTorsion_Unwrap(0.2_wp, 25.0_wp) - (0.2_wp + 4.0_wp*TWO_PI)) <= 1.0e-13_wp, & + 'U: nearest branch several turns away') + CALL CD_HermiteTorsion_Accept(10.0_wp, 10.0_wp - TWO_PI + 1.4_wp, th, es, em) + CALL require(es == CD_HTORS_OK .AND. ABS(th - 11.4_wp) <= 1.0e-13_wp, 'U: step below pi/2 accepted') + CALL CD_HermiteTorsion_Accept(10.0_wp, 10.0_wp + 1.7_wp, th, es, em) + CALL require(es == CD_HTORS_STEP .AND. ABS(th - 10.0_wp) <= 0.0_wp .AND. INDEX(em, 'step limit') > 0, & + 'U: step above pi/2 rejected by name, previous value kept') + CALL CD_HermiteTorsion_Accept(10.0_wp, 10.0_wp + 1.7_wp, th, es, em, max_step=2.0_wp) + CALL require(es == CD_HTORS_OK, 'U: wider explicit step limit') + CALL CD_HermiteTorsion_Accept(10.0_wp, 10.0_wp, th, es, em, max_step=4.0_wp) + CALL require(es == CD_HTORS_BADINPUT, 'U: step limit at or above pi rejected') + CALL CD_HermiteTorsion_Accept(10.0_wp, quiet_nan(), th, es, em) + CALL require(es == CD_HTORS_NONFINITE .AND. ABS(th - 10.0_wp) <= 0.0_wp, 'U: non-finite trial rejected') + END SUBROUTINE check_accept_rule + + REAL(wp) FUNCTION quiet_nan() RESULT(x) + USE, INTRINSIC :: IEEE_ARITHMETIC, ONLY: IEEE_VALUE, IEEE_QUIET_NAN + x = IEEE_VALUE(1.0_wp, IEEE_QUIET_NAN) + END FUNCTION quiet_nan + + ! --------------------------------------------------------------------------------------- + ! G: fold guard and input checks + ! --------------------------------------------------------------------------------------- + + SUBROUTINE arc_element(psi_deg, qe) + !! One element of unit length turning by psi in the xy-plane, with a small z-lift. + REAL(wp), INTENT(IN) :: psi_deg + REAL(wp), INTENT(OUT) :: qe(12) + REAL(wp) :: kap + kap = psi_deg*CD_HTORS_PI/180.0_wp + qe(1:6) = [0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp] + qe(7:12) = [SIN(kap)/kap, (1.0_wp - COS(kap))/kap, 0.05_wp, COS(kap), SIN(kap), 0.1_wp] + END SUBROUTINE arc_element + + SUBROUTINE check_fold_and_inputs() + REAL(wp) :: qe(12), le(1), ends(3, 4), th, g(12), margin, hb(LDAB, 12), bad(12) + INTEGER :: es + CHARACTER(200) :: em + CALL arc_element(100.0_wp, qe) + le = 1.0_wp + ends = clamped_ends(qe) + CALL CD_HermiteTorsion_Fold_Check(qe, le, ends, margin, es, em) + CALL require(es == CD_HTORS_OK .AND. margin > 0.5_wp .AND. margin < 1.0_wp, 'G: 100-degree element accepted') + CALL arc_element(135.0_wp, qe) + ends = clamped_ends(qe) + hb = 7.0_wp + CALL CD_HermiteTorsion_Line(qe, le, ends, th, g, es, em, hband=hb) + CALL require(es == CD_HTORS_FOLD .AND. INDEX(em, '120 degrees inside element 1') > 0, & + 'G: 135-degree element fails closed by name') + CALL require(ALL(ABS(hb - 7.0_wp) <= 0.0_wp) .AND. ALL(ABS(g) <= 0.0_wp), 'G: failed call leaves outputs clean') + ! articulated End A director 150 degrees from the first tangent + CALL arc_element(40.0_wp, qe) + ends = clamped_ends(qe) + ends(:, 1) = [COS(2.618_wp), SIN(2.618_wp), 0.0_wp] + ends(:, 2) = [0.0_wp, 0.0_wp, 1.0_wp] + CALL CD_HermiteTorsion_Fold_Check(qe, le, ends, margin, es, em) + CALL require(es == CD_HTORS_FOLD .AND. INDEX(em, 'End-A director') > 0, 'G: folded articulated end by name') + ! malformed input + ends = clamped_ends(qe) + ends(:, 2) = ends(:, 2) + 1.0e-3_wp*ends(:, 1) + CALL CD_HermiteTorsion_Line(qe, le, ends, th, g, es, em) + CALL require(es == CD_HTORS_BADINPUT .AND. INDEX(em, 'End-A') > 0, 'G: non-orthonormal end frame rejected') + ends = clamped_ends(qe) + bad = qe + bad(5) = quiet_nan() + CALL CD_HermiteTorsion_Line(bad, le, ends, th, g, es, em) + CALL require(es == CD_HTORS_NONFINITE, 'G: NaN DOF rejected') + bad = qe + bad(10:12) = 0.0_wp + CALL CD_HermiteTorsion_Line(bad, le, ends, th, g, es, em) + CALL require(es == CD_HTORS_BADINPUT, 'G: zero tangent handle rejected') + CALL CD_HermiteTorsion_Line(qe(1:6), le(1:0), ends, th, g(1:6), es, em) + CALL require(es == CD_HTORS_BADINPUT, 'G: single-node line rejected') + CALL CD_HermiteTorsion_Line(qe, -le, ends, th, g, es, em) + CALL require(es == CD_HTORS_BADINPUT, 'G: negative length rejected') + CALL CD_HermiteTorsion_Line(qe, le, ends, th, g, es, em, quadrature_order=7) + CALL require(es == CD_HTORS_BADINPUT, 'G: unsupported quadrature rejected') + CALL CD_HermiteTorsion_Line(qe, le, ends, th, g(1:6), es, em) + CALL require(es == CD_HTORS_BADINPUT, 'G: gradient size checked') + CALL CD_HermiteTorsion_Line(qe, le, ends, th, g, es, em, hband=hb, band_scale=quiet_nan()) + CALL require(es == CD_HTORS_NONFINITE, 'G: non-finite band scale rejected') + END SUBROUTINE check_fold_and_inputs + + ! --------------------------------------------------------------------------------------- + ! E: element routine + ! --------------------------------------------------------------------------------------- + + SUBROUTINE check_element_routine() + REAL(wp) :: qe(12), h, g(12), hk(12, 12), hp, hm, gp(12), gm(12), gfd(12), hfd(12, 12), dq(12), margin + REAL(wp) :: ends(3, 4), th, gl(12), hb(LDAB, 12), le(1), eg(6), eh(3, 3, 2), ec(3, 3, 2), h6 + INTEGER :: es, i + CHARACTER(200) :: em + CALL arc_element(70.0_wp, qe) + qe(4:6) = 1.07_wp*qe(4:6) + qe(10:12) = 0.93_wp*qe(10:12) + [0.0_wp, 0.0_wp, 0.2_wp] + CALL CD_HermiteTorsion_Element(qe, 1.3_wp, h, g, es, em, hess=hk, fold_margin=margin) + CALL require(es == CD_HTORS_OK .AND. margin > 0.5_wp, 'E: element evaluates') + DO i = 1, 12 + dq = 0.0_wp + dq(i) = 1.0e-6_wp + CALL CD_HermiteTorsion_Element(qe + dq, 1.3_wp, hp, gp, es, em) + CALL CD_HermiteTorsion_Element(qe - dq, 1.3_wp, hm, gm, es, em) + gfd(i) = (hp - hm)/2.0e-6_wp + hfd(:, i) = (gp - gm)/2.0e-6_wp + END DO + CALL require(nan_max_abs(gfd - g) <= 1.0e-8_wp*MAXVAL(ABS(g)), 'E: element gradient vs FD') + CALL require(nan_max_abs(hfd - hk) <= 1.0e-6_wp*MAXVAL(ABS(hk)), 'E: element Hessian vs FD') + CALL require(nan_max_abs(hk - TRANSPOSE(hk)) <= 1.0e-14_wp*MAXVAL(ABS(hk)), 'E: element Hessian symmetric') + ! the line of one element = element holonomy + chain terms; clamped ends make the end links trivial + le = 1.3_wp + ends = clamped_ends(qe) + hb = 0.0_wp + CALL evaluate(qe, le, ends, th, gl, hb, eg, eh, ec, es) + CALL require(es == CD_HTORS_OK, 'E: one-element line evaluates') + CALL require(nan_max_abs(gl(1:3) - g(1:3)) + nan_max_abs(gl(7:9) - g(7:9)) <= 1.0e-14_wp*MAXVAL(ABS(g)), & + 'E: position gradient of the line is the element holonomy gradient') + CALL require(nan_max_abs(band_to_dense(hb, 12) - hk) > 1.0e-6_wp*MAXVAL(ABS(hk)), & + 'E: chain terms add to the tangent-handle block') + ! sixth-order quadrature changes h_e only by the quadrature error (about 1e-3 for one coarse, + ! 70-degree, distorted element) + CALL CD_HermiteTorsion_Element(qe, 1.3_wp, h6, g, es, em, quadrature_order=6) + WRITE (*, '(A,2ES12.4)') ' E element holonomy, 4- and 6-point quadrature ', h, h6 + CALL require(es == CD_HTORS_OK .AND. ABS(h6 - h) <= 5.0e-3_wp*ABS(h), & + 'E: quadrature order 6 consistent') + CALL CD_HermiteTorsion_Element(qe, 0.0_wp, h, g, es, em) + CALL require(es == CD_HTORS_BADINPUT, 'E: zero length rejected') + CALL CD_HermiteTorsion_Element(qe, 1.3_wp, h, g, es, em, quadrature_order=0) + CALL require(es == CD_HTORS_BADINPUT, 'E: quadrature order 0 rejected') + CALL arc_element(140.0_wp, qe) + CALL CD_HermiteTorsion_Element(qe, 1.0_wp, h, g, es, em, fold_margin=margin) + CALL require(es == CD_HTORS_FOLD .AND. margin < 0.5_wp, 'E: element fold reported with its margin') + END SUBROUTINE check_element_routine + + ! --------------------------------------------------------------------------------------- + SUBROUTINE check_linear_algebra() + !! L (B + g g^T / C) x = r on a 6x6 band (kl = ku = 2): + !! - B regular: the direct Sherman-Morrison step; + !! - B exactly singular, K regular: the shifted factorization with refinement; + !! - C = 1e-8 with B nearly singular (lambda ~ 1e-10): g^T B^-1 g / C ~ 1e18 defeats both + !! Sherman-Morrison paths although K is well conditioned, so the dense fallback solves it; + !! - that case in a line beyond CD_HTORS_DENSE_MAX DOFs: named CD_HTORS_ILLCOND with x = 0. + !! The inertia count refuses a non-square band and a NaN coupling vector, and reports a + !! vanishing pivot as unreliable; the compliance of a description without GJ is NaN. + INTEGER, PARAMETER :: N = 6, KLB = 2, LD = 2*KLB + KLB + 1 + REAL(wp) :: dense(N, N), g(N), r(N), x(N), xt(N), ab(LD, N), be, err + INTEGER :: es, i, nb, nk + LOGICAL :: sh, dn, mask(N) + CHARACTER(256) :: em + TYPE(CD_HermiteTorsionType) :: tors + g = [1.0_wp, 0.1_wp, 0.2_wp, -0.1_wp, 0.05_wp, 0.0_wp] + r = [1.0_wp, -2.0_wp, 0.5_wp, 1.0_wp, 0.0_wp, 3.0_wp] + ! direct path + CALL la_case(1.0_wp, 0.5_wp, 1.0_wp, g, r, x, xt, es, em, be, sh, dn) + err = nan_max_abs(x - xt)/MAXVAL(ABS(xt)) + CALL require(es == CD_HTORS_OK .AND. .NOT. sh .AND. .NOT. dn .AND. err <= 1.0e-13_wp, & + 'L: direct bordered step '//TRIM(em)) + ! B exactly singular (first pivot zero), K regular: shifted path + CALL la_case(0.0_wp, 0.0_wp, 1.0_wp, g, r, x, xt, es, em, be, sh, dn) + err = nan_max_abs(x - xt)/MAXVAL(ABS(xt)) + CALL require(es == CD_HTORS_OK .AND. sh .AND. .NOT. dn .AND. err <= 1.0e-8_wp, & + 'L: shifted bordered step for a singular B '//TRIM(em)) + ! g^T B^-1 g / C huge: dense fallback + CALL la_case(1.0e-10_wp + 0.09_wp, 0.3_wp, 1.0e-8_wp, g, r, x, xt, es, em, be, sh, dn) + err = nan_max_abs(x - xt)/MAXVAL(ABS(xt)) + WRITE (*, '(A,I0,A,L1,A,L1,A,ES10.3,A,ES10.3)') 'bordered solve, g^T B^-1 g / C ~ 1e18: status ', es, & + ', shifted ', sh, ', dense ', dn, ', backward error ', be, ', error vs dense LU ', err + CALL require(es == CD_HTORS_OK .AND. dn .AND. err <= 1.0e-6_wp .AND. be <= 1.0e-10_wp, & + 'L: dense fallback when Sherman-Morrison fails') + ! the same system embedded in a line too large for the dense fallback: named, x = 0 + BLOCK + INTEGER, PARAMETER :: NL = CD_HTORS_DENSE_MAX + 2 + REAL(wp), ALLOCATABLE :: abl(:, :), gl(:), rl(:), xl(:) + ALLOCATE (abl(LD, NL), gl(NL), rl(NL), xl(NL)) + abl = 0.0_wp + abl(2*KLB + 1, :) = 1.0_wp + abl(2*KLB + 1, 1) = 1.0e-10_wp + 0.09_wp + abl(2*KLB, 2) = 0.3_wp + abl(2*KLB + 2, 1) = 0.3_wp + gl = 0.0_wp + gl(1:N) = g + rl = 0.0_wp + rl(1:N) = r + CALL CD_HermiteTorsion_Bordered_Solve(abl, KLB, KLB, gl, 1.0e-8_wp, rl, xl, es, em, backward_error=be, & + shifted=sh, dense=dn) + CALL require(es == CD_HTORS_ILLCOND .AND. INDEX(em, 'ill-conditioned') > 0 .AND. nan_max_abs(xl) <= 0.0_wp & + .AND. .NOT. dn, 'L: a failed bordered step beyond the dense size is named ill-conditioned, x = 0') + END BLOCK + ! a band with a zero pivot for the inertia checks: B = diag(0, 0, 1, 1, 1, 1) + dense = 0.0_wp + DO i = 3, N + dense(i, i) = 1.0_wp + END DO + CALL la_band(dense, KLB, ab) + g = 0.0_wp + g(1) = 1.0_wp + ! inertia input checks and the unreliable count + mask = .TRUE. + CALL CD_HermiteTorsion_Inertia(ab(2:, :), KLB - 1, KLB, mask, g, 1.0_wp, nb, nk, es, em) + CALL require(es == CD_HTORS_BADINPUT .AND. INDEX(em, 'kl = ku') > 0, 'L: inertia refuses a non-square band') + g(2) = IEEE_VALUE(1.0_wp, IEEE_QUIET_NAN) + CALL CD_HermiteTorsion_Inertia(ab, KLB, KLB, mask, g, 1.0_wp, nb, nk, es, em) + CALL require(es == CD_HTORS_NONFINITE, 'L: inertia refuses a NaN coupling vector') + g(2) = 0.0_wp + CALL CD_HermiteTorsion_Inertia(ab, KLB, KLB, mask, g, 1.0_wp, nb, nk, es, em) + CALL require(es == CD_HTORS_UNRELIABLE, 'L: a vanishing pivot makes the count unreliable') + ! compliance without GJ + CALL require(IEEE_IS_NAN(CD_HermiteTorsion_Compliance(tors, [1.0_wp, 1.0_wp])), & + 'L: the compliance of a description without GJ is NaN') + ALLOCATE (tors%gj(3)) + tors%gj = 1.0_wp + CALL require(IEEE_IS_NAN(CD_HermiteTorsion_Compliance(tors, [1.0_wp, 1.0_wp])), & + 'L: the compliance with a GJ count that does not match the mesh is NaN') + tors%end_compliance = [0.5_wp, 0.25_wp] + CALL require(ABS(CD_HermiteTorsion_Compliance(tors, [1.0_wp, 2.0_wp, 3.0_wp]) - 6.75_wp) <= 1.0e-15_wp, & + 'L: compliance = sum L/GJ + end compliances') + END SUBROUTINE check_linear_algebra + + SUBROUTINE la_band(dense, kl, ab) + !! General band storage (DGBSV, kl = ku) of a dense square matrix. + REAL(wp), INTENT(IN) :: dense(:, :) + INTEGER, INTENT(IN) :: kl + REAL(wp), INTENT(OUT) :: ab(:, :) + INTEGER :: i, j, n + n = SIZE(dense, 2) + ab = 0.0_wp + DO j = 1, n + DO i = MAX(1, j - kl), MIN(n, j + kl) + ab(2*kl + 1 + i - j, j) = dense(i, j) + END DO + END DO + END SUBROUTINE la_band + + SUBROUTINE la_case(b11, b12, cc, g, r, x, xt, es, em, be, sh, dn) + !! B = identity with the leading 2x2 block [b11 b12; b12 1]: the bordered solve of + !! (B + g g^T / cc) x = r, and the reference xt from a dense LU of the same K. + REAL(wp), INTENT(IN) :: b11, b12, cc, g(:), r(:) + REAL(wp), INTENT(OUT) :: x(:), xt(:), be + INTEGER, INTENT(OUT) :: es + CHARACTER(*), INTENT(OUT) :: em + LOGICAL, INTENT(OUT) :: sh, dn + INTEGER, PARAMETER :: KL2 = 2 + REAL(wp) :: dense(SIZE(g), SIZE(g)), kk(SIZE(g), SIZE(g)), ab(3*KL2 + 1, SIZE(g)) + INTEGER :: i, n, info, ipiv(SIZE(g)) + EXTERNAL :: dgesv + n = SIZE(g) + dense = 0.0_wp + DO i = 1, n + dense(i, i) = 1.0_wp + END DO + dense(1, 1) = b11 + dense(1, 2) = b12 + dense(2, 1) = b12 + CALL la_band(dense, KL2, ab) + CALL CD_HermiteTorsion_Bordered_Solve(ab, KL2, KL2, g, cc, r, x, es, em, backward_error=be, shifted=sh, & + dense=dn) + kk = dense + SPREAD(g, 2, n)*SPREAD(g, 1, n)/cc + xt = r + CALL dgesv(n, 1, kk, n, ipiv, xt, n, info) + END SUBROUTINE la_case + +END PROGRAM test_hermite_torsion_kernel diff --git a/tests/test_hermite_torsion_static.f90 b/tests/test_hermite_torsion_static.f90 new file mode 100644 index 0000000..41f5b65 --- /dev/null +++ b/tests/test_hermite_torsion_static.f90 @@ -0,0 +1,559 @@ +! File: tests/test_hermite_torsion_static.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +PROGRAM test_hermite_torsion_static + !! Gates of the condensed-torsion static solve (CD_HermiteCable_Static_Solve with torsion): + !! T pure torsion of a straight multi-section rod (with and without torsional end springs): + !! torque = Phi / C to round-off, centreline unchanged; + !! G clamped-clamped Greenhill onset (sliding end, dead tension T) from the stability + !! report, bisected on the imposed twist: N = 32 within 1e-4 of van der Heijden et al. + !! (2003) eq. (33) for T L^2/EI in {-20, 0, 10, 50, 200}, observed order of the N = 8, + !! 16, 32 sequence; + !! P bending-pinned, torsion-restrained ends (semi-tangential, constant-velocity joint): + !! 4.9112877 and 7.2692742 EI/L at T L^2/EI = 0 and 10; + !! B above the onset the straight saddle is reported unstable, and the descent reaches a + !! stable buckled state of lower energy with a relaxed torque; below it the straight + !! state is stable; + !! M more than two turns of imposed twist through the continuation: the torque law, the + !! Theta continuity between successive solves and agreement with a single solve; + !! F fail-closed inputs (missing GJ, a director off the rigid direction, a rejected + !! solver mode or tangent output); + !! R a re-solve from a stored Theta: within pi/2 it continues, beyond it stops by name. + !! Argument 1: tests/data/torsion_analytic_refs.txt. + USE CableDyn_Precision, ONLY: wp + USE CableDyn_HermiteCableStatic, ONLY: CD_HermiteCable_Static_Solve, CD_HCSTAT_OK, CD_HCSTAT_BADINPUT + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HTORS_PI + USE CableDyn_EndConnection, ONLY: CD_ENDCONN_PINNED, CD_ENDCONN_RIGID + IMPLICIT NONE + + INTEGER :: nfail = 0 + CHARACTER(512) :: ref_path + REAL(wp) :: ref_clamped(5, 2), ref_pinned(2, 2) + + IF (COMMAND_ARGUMENT_COUNT() < 1) THEN + WRITE (*, '(A)') 'usage: test_hermite_torsion_static ' + ERROR STOP 2 + END IF + CALL GET_COMMAND_ARGUMENT(1, ref_path) + CALL read_refs() + + CALL check_pure_torsion() + CALL check_greenhill_clamped() + CALL check_greenhill_pinned() + CALL check_post_buckling() + CALL check_multi_turn() + CALL check_fail_closed() + CALL check_resolve() + + IF (nfail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', nfail, ' torsion statics assertion(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: condensed-torsion statics (pure torsion, Greenhill onsets, post-buckling, multi-turn)' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(ok, label) + LOGICAL, INTENT(IN) :: ok + CHARACTER(*), INTENT(IN) :: label + IF (.NOT. ok) THEN + nfail = nfail + 1 + WRITE (*, '(A)') 'FAIL: '//label + END IF + END SUBROUTINE require + + SUBROUTINE read_refs() + !! "clamped T M_cr" (five rows) and "pinned T M_cr" (two rows), EI = L = 1. + INTEGER :: u, ios, nc, np + CHARACTER(256) :: line + CHARACTER(16) :: tag + REAL(wp) :: t, m + nc = 0 + np = 0 + OPEN (NEWUNIT=u, FILE=ref_path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) THEN + WRITE (*, '(A)') 'cannot open '//TRIM(ref_path) + ERROR STOP 2 + END IF + DO + READ (u, '(A)', IOSTAT=ios) line + IF (ios /= 0) EXIT + IF (LEN_TRIM(line) == 0 .OR. line(1:1) == '#') CYCLE + READ (line, *, IOSTAT=ios) tag, t, m + IF (ios /= 0) CYCLE + IF (TRIM(tag) == 'clamped' .AND. nc < 5) THEN + nc = nc + 1 + ref_clamped(nc, :) = [t, m] + ELSE IF (TRIM(tag) == 'pinned' .AND. np < 2) THEN + np = np + 1 + ref_pinned(np, :) = [t, m] + END IF + END DO + CLOSE (u) + IF (nc /= 5 .OR. np /= 2) THEN + WRITE (*, '(A)') 'reference file incomplete: '//TRIM(ref_path) + ERROR STOP 2 + END IF + END SUBROUTINE read_refs + + ! ------------------------------------------------------------------------------------------ + ! a straight rod along +x, node 1 held, the last node sliding along x under a dead tension + ! ------------------------------------------------------------------------------------------ + + SUBROUTINE straight_rod(ne, length, seed, l0) + INTEGER, INTENT(IN) :: ne + REAL(wp), INTENT(IN) :: length + REAL(wp), ALLOCATABLE, INTENT(OUT) :: seed(:), l0(:) + INTEGER :: k + ALLOCATE (seed(6*(ne + 1)), l0(ne)) + l0 = length/ne + seed = 0.0_wp + DO k = 0, ne + seed(6*k + 1) = length*k/ne + seed(6*k + 4) = 1.0_wp + END DO + END SUBROUTINE straight_rod + + SUBROUTINE straight_torsion(ne, gj, phi, tors) + INTEGER, INTENT(IN) :: ne + REAL(wp), INTENT(IN) :: gj, phi + TYPE(CD_HermiteTorsionType), INTENT(OUT) :: tors + tors%active = .TRUE. + ALLOCATE (tors%gj(ne)) + tors%gj = gj + tors%phi = phi + tors%ends(:, 1) = [1.0_wp, 0.0_wp, 0.0_wp] + tors%ends(:, 2) = [0.0_wp, 0.0_wp, 1.0_wp] + tors%ends(:, 3) = [1.0_wp, 0.0_wp, 0.0_wp] + tors%ends(:, 4) = [0.0_wp, 0.0_wp, 1.0_wp] + END SUBROUTINE straight_torsion + + SUBROUTINE solve_rod(ne, ea, ei, tension, mode, tors, q, es, em, wline) + !! Weightless (or w = wline) straight rod, ends in connection mode `mode` (both), node 1 + !! held, last node free along x with the dead load `tension`. + INTEGER, INTENT(IN) :: ne, mode + REAL(wp), INTENT(IN) :: ea, ei, tension + TYPE(CD_HermiteTorsionType), INTENT(INOUT) :: tors + REAL(wp), ALLOCATABLE, INTENT(OUT) :: q(:) + INTEGER, INTENT(OUT) :: es + CHARACTER(*), INTENT(OUT) :: em + REAL(wp), INTENT(IN), OPTIONAL :: wline + REAL(wp), ALLOCATABLE :: seed(:), l0(:), f(:), curv(:), wv(:) + REAL(wp) :: res + INTEGER :: nd, it + CALL straight_rod(ne, 1.0_wp, seed, l0) + nd = SIZE(seed) + ALLOCATE (f(nd), q(nd), curv(ne + 1), wv(ne)) + wv = 0.0_wp + IF (PRESENT(wline)) wv = wline + f = 0.0_wp + f(nd - 5) = tension + CALL CD_HermiteCable_Static_Solve(l0, [(ea, it=1, ne)], [(ei, it=1, ne)], wv, seed, [1, 2, 3, nd - 4, nd - 3], & + -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-7_wp, 1.0_wp, q, curv, res, it, es, em, & + f_nodal=f, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[mode, mode], torsion=tors) + END SUBROUTINE solve_rod + + REAL(wp) FUNCTION onset(ne, tension, mode, mguess) RESULT(mcr) + !! Critical torque (EI = L = GJ = 1, so M = Phi on the straight state) bisected on the + !! stability report of the straight state (no descent), bracketed around mguess. + INTEGER, INTENT(IN) :: ne, mode + REAL(wp), INTENT(IN) :: tension, mguess + REAL(wp) :: lo, hi, mid + INTEGER :: k + LOGICAL :: st, failed + lo = 0.8_wp*mguess + hi = 1.2_wp*mguess + mcr = -1.0_wp + IF (.NOT. stable_at(ne, tension, mode, lo, failed)) RETURN + IF (stable_at(ne, tension, mode, hi, failed)) RETURN + DO k = 1, 60 + mid = 0.5_wp*(lo + hi) + st = stable_at(ne, tension, mode, mid, failed) + IF (failed) RETURN + IF (st) THEN + lo = mid + ELSE + hi = mid + END IF + IF (hi - lo <= 1.0e-9_wp*mguess) EXIT + END DO + mcr = 0.5_wp*(lo + hi) + END FUNCTION onset + + LOGICAL FUNCTION stable_at(ne, tension, mode, m, failed) RESULT(stab) + INTEGER, INTENT(IN) :: ne, mode + REAL(wp), INTENT(IN) :: tension, m + LOGICAL, INTENT(OUT) :: failed + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: q(:) + INTEGER :: es + CHARACTER(512) :: em + CALL straight_torsion(ne, 1.0_wp, m, tors) + tors%descend = .FALSE. + CALL solve_rod(ne, 1.0e7_wp, 1.0_wp, tension, mode, tors, q, es, em) + failed = es /= CD_HCSTAT_OK + IF (failed) WRITE (*, '(A)') ' solve failed: '//TRIM(em) + stab = tors%stable .AND. .NOT. failed + END FUNCTION stable_at + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_pure_torsion() + !! 10 x 0.2 m, GJ alternating 1e4 / 2.5e4, EI 1e6 (far below its Greenhill onset), clamped + !! straight ends; Phi = 0.5, 2.5, 10 rad. M_t = Phi / C exactly; with torsional end springs + !! k = 1e4 at both ends C gains 2e-4. + REAL(wp), PARAMETER :: PHIS(3) = [0.5_wp, 2.5_wp, 10.0_wp] + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: seed(:), l0(:), q(:), curv(:), f(:) + REAL(wp) :: res, c, err, drift + INTEGER :: ip, isp, it, es, nd, e + CHARACTER(512) :: em + CALL straight_rod(10, 2.0_wp, seed, l0) + nd = SIZE(seed) + ALLOCATE (q(nd), curv(11), f(nd)) + f = 0.0_wp + f(nd - 5) = 100.0_wp + DO isp = 0, 1 + DO ip = 1, 3 + tors = CD_HermiteTorsionType() + tors%active = .TRUE. + ALLOCATE (tors%gj(10)) + DO e = 1, 10 + tors%gj(e) = MERGE(1.0e4_wp, 2.5e4_wp, MOD(e, 2) == 1) + END DO + IF (isp == 1) tors%end_compliance = 1.0e-4_wp + tors%phi = PHIS(ip) + tors%ends(:, 1) = [1.0_wp, 0.0_wp, 0.0_wp] + tors%ends(:, 2) = [0.0_wp, 1.0_wp, 0.0_wp] + tors%ends(:, 3) = [1.0_wp, 0.0_wp, 0.0_wp] + tors%ends(:, 4) = [0.0_wp, 1.0_wp, 0.0_wp] + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e9_wp, it=1, 10)], [(1.0e6_wp, it=1, 10)], [(0.0_wp, it=1, 10)], & + seed, [1, 2, 3, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-7_wp, & + 1.0_wp, q, curv, res, it, es, em, f_nodal=f, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + CALL require(es == CD_HCSTAT_OK, 'T: pure torsion solve converges: '//TRIM(em)) + c = 5.0_wp*0.2_wp/1.0e4_wp + 5.0_wp*0.2_wp/2.5e4_wp + 2.0_wp*isp*1.0e-4_wp + err = ABS(tors%torque - PHIS(ip)/c)/(PHIS(ip)/c) + drift = nan_max_abs([nan_max_abs(q(2::6)), nan_max_abs(q(3::6)), nan_max_abs(q(5::6)), & + nan_max_abs(q(6::6))])/2.0_wp + WRITE (*, '(A,I0,A,F5.1,A,ES12.5,A,ES9.2,A,ES9.2)') 'pure torsion (springs ', isp, ') Phi=', PHIS(ip), & + ' rad: M_t=', tors%torque, ' N m, rel. error ', err, ', lateral drift/L ', drift + CALL require(err <= 1.0e-12_wp, 'T: M_t = Phi/C to round-off') + CALL require(drift <= 1.0e-12_wp, 'T: the straight centreline stays straight') + CALL require(ABS(tors%theta) <= 1.0e-12_wp, 'T: Theta = 0 on the straight rod') + CALL require(tors%stable, 'T: the twisted straight rod is stable below its onset') + END DO + END DO + END SUBROUTINE check_pure_torsion + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_greenhill_clamped() + INTEGER, PARAMETER :: NES(3) = [8, 16, 32] + REAL(wp) :: m(3), err(3), order + INTEGER :: it, k + DO it = 1, 5 + DO k = 1, 3 + m(k) = onset(NES(k), ref_clamped(it, 1), CD_ENDCONN_RIGID, ref_clamped(it, 2)) + err(k) = ABS(m(k) - ref_clamped(it, 2))/ref_clamped(it, 2) + END DO + order = LOG(ABS(m(1) - ref_clamped(it, 2))/MAX(ABS(m(2) - ref_clamped(it, 2)), TINY(1.0_wp)))/LOG(2.0_wp) + WRITE (*, '(A,F7.1,A,3F12.7,A,F11.8,A,ES9.2,A,F5.2)') 'Greenhill clamped T L^2/EI=', ref_clamped(it, 1), & + ': M_cr L/EI (N=8,16,32) =', m, ' ref ', ref_clamped(it, 2), ' rel.err(N=32) ', err(3), ' order ', order + CALL require(ALL(m > 0.0_wp), 'G: onset bracketed for every mesh') + CALL require(err(3) <= 1.0e-4_wp, 'G: N = 32 onset within 1e-4 of eq. (33)') + IF (ref_clamped(it, 1) <= 50.0_wp) & + CALL require(order >= 3.5_wp .AND. order <= 4.5_wp, 'G: observed order between 3.5 and 4.5 (T <= 50)') + END DO + END SUBROUTINE check_greenhill_clamped + + SUBROUTINE check_greenhill_pinned() + REAL(wp) :: m, err + INTEGER :: it + DO it = 1, 2 + m = onset(32, ref_pinned(it, 1), CD_ENDCONN_PINNED, ref_pinned(it, 2)) + err = ABS(m - ref_pinned(it, 2))/ref_pinned(it, 2) + WRITE (*, '(A,F6.1,A,F12.7,A,F11.8,A,ES9.2)') 'Greenhill pinned (semi-tangential) T L^2/EI=', & + ref_pinned(it, 1), ': M_cr L/EI(N=32) =', m, ' ref ', ref_pinned(it, 2), ' rel.err ', err + CALL require(err <= 1.0e-4_wp, 'P: pinned constant-velocity-joint onset within 1e-4') + END DO + END SUBROUTINE check_greenhill_pinned + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_post_buckling() + !! Clamped at both ends with both positions held (EI = L = 1, N = 16, EA = 1e4, prestressed + !! to T L^2/EI = 10 by the end separation). 0.9 M_cr: stable straight. 1.05 M_cr: the straight + !! saddle is reported unstable without descent; with descent the solve ends stable (the + !! rotation about the clamp axis is the one allowed zero mode), buckled, lower in energy and + !! with a relaxed torque. + REAL(wp), PARAMETER :: MCR = 10.42885644_wp + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: q(:) + REAL(wp) :: e_straight, m_straight, amp + INTEGER :: es + CHARACTER(512) :: em + CALL straight_torsion(16, 1.0_wp, 0.9_wp*MCR, tors) + tors%zero_mode_allowed = .TRUE. + CALL fixed_rod(tors, q, es, em) + CALL require(es == CD_HCSTAT_OK .AND. tors%stable, 'B: 0.9 M_cr straight state stable: '//TRIM(em)) + CALL require(tors%descents == 0, 'B: no descent below the onset') + CALL straight_torsion(16, 1.0_wp, 1.05_wp*MCR, tors) + tors%descend = .FALSE. + CALL fixed_rod(tors, q, es, em) + CALL require(es == CD_HCSTAT_OK, 'B: the straight saddle converges without descent: '//TRIM(em)) + CALL require(.NOT. tors%stable .AND. tors%n_negative >= 1, 'B: the straight saddle is reported unstable') + WRITE (*, '(A,I0,A,ES12.5)') 'post-buckling 1.05 M_cr straight: negative eigenvalues ', tors%n_negative, & + ', lowest scaled eigenvalue ', tors%lambda_min + e_straight = tors%energy + m_straight = tors%torque + CALL straight_torsion(16, 1.0_wp, 1.05_wp*MCR, tors) + tors%zero_mode_allowed = .TRUE. + CALL fixed_rod(tors, q, es, em) + CALL require(es == CD_HCSTAT_OK, 'B: the descent converges: '//TRIM(em)) + amp = nan_max_abs([nan_max_abs(q(2::6)), nan_max_abs(q(3::6))]) + WRITE (*, '(A,ES12.5,A,F9.5,A,F9.5,A,ES10.3,A,I0,A,ES11.3)') 'post-buckling: E - E_straight = ', & + tors%energy - e_straight, ', M_t ', tors%torque, ' (straight ', m_straight, '), lateral amplitude/L ', amp, & + ', descents ', tors%descents, ', Theta ', tors%theta + CALL require(tors%stable, 'B: the descent ends on a stable state') + CALL require(tors%descents >= 1, 'B: a descent was taken') + CALL require(tors%energy < e_straight, 'B: the buckled state has lower energy than the straight saddle') + CALL require(tors%torque < m_straight, 'B: the torque relaxes on the buckled branch') + CALL require(amp > 1.0e-3_wp .AND. amp < 0.1_wp, 'B: a buckled state near the onset (amplitude 1e-3 to 0.1 L)') + END SUBROUTINE check_post_buckling + + SUBROUTINE fixed_rod(tors, q, es, em) + !! Straight weightless rod, EI = L = 1, EA = 1e4, both positions held 1.001 apart (T = 10), + !! both ends clamped along +x. + TYPE(CD_HermiteTorsionType), INTENT(INOUT) :: tors + REAL(wp), ALLOCATABLE, INTENT(OUT) :: q(:) + INTEGER, INTENT(OUT) :: es + CHARACTER(*), INTENT(OUT) :: em + REAL(wp), ALLOCATABLE :: seed(:), l0(:), curv(:) + REAL(wp) :: res + INTEGER :: nd, it, ne + ne = SIZE(tors%gj) + CALL straight_rod(ne, 1.0_wp, seed, l0) + seed(1::6) = 1.001_wp*seed(1::6) + nd = SIZE(seed) + ALLOCATE (q(nd), curv(ne + 1)) + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e4_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(0.0_wp, it=1, ne)], seed, & + [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-9_wp, & + 1.0_wp, q, curv, res, it, es, em, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + END SUBROUTINE fixed_rod + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_multi_turn() + !! (a) Straight clamped rod, GJ/EI = 0.2, Phi = 3 turns in one solve (at most pi/4 per twist + !! stage): M_t = Phi/C. (b) A sagging line clamped at both ends with gravity, Phi swept by + !! 2.5 turns in half-turn solves from the warm state: Theta continuous between solves (also + !! across the +-pi cut of the raw holonomy), M_t C = Phi - Theta, and one direct solve to + !! the end reaches the same state. + TYPE(CD_HermiteTorsionType) :: tors, tors_direct + REAL(wp), ALLOCATABLE :: q(:), seed(:), l0(:), q_direct(:) + REAL(wp) :: phi, theta_prev, c, dmax, offset + INTEGER :: k, es, ne, pass + CHARACTER(512) :: em + CALL straight_torsion(16, 0.2_wp, 6.0_wp*CD_HTORS_PI, tors) + CALL solve_rod(16, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + CALL require(es == CD_HCSTAT_OK, 'M: three-turn straight solve converges: '//TRIM(em)) + WRITE (*, '(A,I0,A,ES12.5)') 'multi-turn straight: twist stages ', tors%ramp_steps, ', rel. torque error ', & + ABS(tors%torque - 0.2_wp*6.0_wp*CD_HTORS_PI)/(0.2_wp*6.0_wp*CD_HTORS_PI) + CALL require(tors%ramp_steps >= 24, 'M: at most pi/4 per twist stage') + CALL require(ABS(tors%torque - 0.2_wp*6.0_wp*CD_HTORS_PI) <= 1.0e-11_wp*0.2_wp*6.0_wp*CD_HTORS_PI, & + 'M: three-turn torque law') + ! (b) sagging clamped line, ends 0.9 L apart, w = 5 EI/L^3, GJ = 0.3 EI. Pass 2 rolls the + ! End-2 reference normal by -(pi - 1e-6) about its director, so Theta starts just below pi + ! and the twisted solves carry it across the +-pi branch cut of the raw holonomy. + ne = 24 + DEALLOCATE (q) + ALLOCATE (seed(6*(ne + 1)), l0(ne), q(6*(ne + 1)), q_direct(6*(ne + 1))) + l0 = 1.0_wp/ne + c = 1.0_wp/0.3_wp + DO pass = 1, 2 + DO k = 0, ne + seed(6*k + 1:6*k + 6) = [0.9_wp*k/ne, 0.0_wp, -0.2_wp*SIN(CD_HTORS_PI*k/ne), & + 0.9_wp, 0.0_wp, -0.2_wp*CD_HTORS_PI*COS(CD_HTORS_PI*k/ne)] + END DO + tors = CD_HermiteTorsionType() + tors%active = .TRUE. + ALLOCATE (tors%gj(ne)) + tors%gj = 0.3_wp + tors%ends(:, 1) = [1.0_wp, 0.0_wp, -1.0_wp]/SQRT(2.0_wp) + tors%ends(:, 2) = [0.0_wp, 1.0_wp, 0.0_wp] + tors%ends(:, 3) = [1.0_wp, 0.0_wp, 1.0_wp]/SQRT(2.0_wp) + offset = 0.0_wp + IF (pass == 2) offset = CD_HTORS_PI - 1.0e-6_wp + ! n_2 rolled by -offset about d_2 (Theta grows by offset) + tors%ends(:, 4) = COS(offset)*[0.0_wp, 1.0_wp, 0.0_wp] - SIN(offset)*[-1.0_wp, 0.0_wp, 1.0_wp]/SQRT(2.0_wp) + theta_prev = offset + DO k = 0, 5 + phi = offset + k*CD_HTORS_PI + tors%phi = phi + CALL sag_solve(tors, l0, seed, q, es, em) + CALL require(es == CD_HCSTAT_OK, 'M: sagging-line twist solve converges: '//TRIM(em)) + IF (es /= CD_HCSTAT_OK) RETURN + WRITE (*, '(A,I0,A,F6.2,A,ES16.8,A,ES12.5,A,L1,A,ES10.3)') 'multi-turn sag (pass ', pass, '): Phi/2pi=', & + phi/(2.0_wp*CD_HTORS_PI), ' Theta=', tors%theta, ' M_t=', tors%torque, ' stable ', tors%stable, & + ' max|y| ', nan_max_abs(q(2::6)) + CALL require(ABS(tors%theta - theta_prev) < 1.0e-3_wp, 'M: Theta continuous between solves') + CALL require(ABS(tors%torque*c - (phi - tors%theta)) <= 1.0e-10_wp*MAX(1.0_wp, phi), & + 'M: M_t C = Phi - Theta') + CALL require(tors%stable, 'M: the twisted sagging line is stable') + theta_prev = tors%theta + seed = q + END DO + IF (pass == 2) CALL require(tors%theta > CD_HTORS_PI, 'M: Theta carried across pi without a 2 pi slip') + tors_direct = tors + tors_direct%has_theta = .FALSE. + tors_direct%theta = 0.0_wp + tors_direct%theta_hint = offset + DO k = 0, ne + seed(6*k + 1:6*k + 6) = [0.9_wp*k/ne, 0.0_wp, -0.2_wp*SIN(CD_HTORS_PI*k/ne), & + 0.9_wp, 0.0_wp, -0.2_wp*CD_HTORS_PI*COS(CD_HTORS_PI*k/ne)] + END DO + CALL sag_solve(tors_direct, l0, seed, q_direct, es, em) + CALL require(es == CD_HCSTAT_OK, 'M: direct 2.5-turn solve converges: '//TRIM(em)) + dmax = nan_max_abs(q_direct - q) + WRITE (*, '(A,ES10.3,A,ES10.3)') 'multi-turn sag: direct vs swept state ', dmax, ', Theta difference ', & + ABS(tors_direct%theta - tors%theta) + CALL require(dmax <= 1.0e-7_wp .AND. ABS(tors_direct%theta - tors%theta) <= 1.0e-8_wp, & + 'M: the direct solve reaches the swept state') + END DO + END SUBROUTINE check_multi_turn + + SUBROUTINE sag_solve(t, l0, s0, qo, es2, em2) + !! Sagging line (EA 1e5, EI 1, w 5) clamped at both ends along t's end directors. + TYPE(CD_HermiteTorsionType), INTENT(INOUT) :: t + REAL(wp), INTENT(IN) :: l0(:), s0(:) + REAL(wp), INTENT(OUT) :: qo(:) + INTEGER, INTENT(OUT) :: es2 + CHARACTER(*), INTENT(OUT) :: em2 + REAL(wp), ALLOCATABLE :: curv(:) + REAL(wp) :: res + INTEGER :: it, ne, nd + ne = SIZE(l0) + nd = SIZE(s0) + ALLOCATE (curv(ne + 1)) + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e5_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(5.0_wp, it=1, ne)], & + s0, [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 4, 100, & + 1.0e-9_wp, 1.0_wp, qo, curv, res, it, es2, em2, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([t%ends(:, 1), t%ends(:, 3)], [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=t) + END SUBROUTINE sag_solve + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_fail_closed() + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: q(:), seed(:), l0(:), curv(:) + REAL(wp) :: res + INTEGER :: es, it, nd + CHARACTER(512) :: em + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + DEALLOCATE (tors%gj) + CALL solve_rod(8, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'GJ') > 0, 'F: missing GJ is rejected by name') + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + tors%gj(3) = 0.0_wp + CALL solve_rod(8, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'GJ must be positive') > 0, 'F: zero GJ is rejected') + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + tors%ends(:, 1) = [0.0_wp, 1.0_wp, 0.0_wp] + tors%ends(:, 2) = [0.0_wp, 0.0_wp, 1.0_wp] + CALL solve_rod(8, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'rigid connection direction') > 0, & + 'F: a director off the rigid direction is rejected') + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + tors%ends(:, 2) = [1.0_wp, 0.0_wp, 0.0_wp] + CALL solve_rod(8, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'orthonormal') > 0, & + 'F: a reference normal along the director is rejected') + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + CALL straight_rod(8, 1.0_wp, seed, l0) + nd = SIZE(seed) + ALLOCATE (curv(9)) + DEALLOCATE (q) + ALLOCATE (q(nd)) + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e7_wp, it=1, 8)], [(1.0_wp, it=1, 8)], [(0.0_wp, it=1, 8)], seed, & + [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 20, 1.0e-10_wp, & + 1.0_wp, q, curv, res, it, es, em, pseudo_transient=.TRUE., torsion=tors) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'pseudo_transient') > 0, & + 'F: pseudo-transient continuation with torsion is rejected') + BLOCK + REAL(wp) :: kband(34, 6*9), fres(6*9) + CALL straight_torsion(8, 1.0_wp, 1.0_wp, tors) + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e7_wp, it=1, 8)], [(1.0_wp, it=1, 8)], [(0.0_wp, it=1, 8)], seed, & + [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 20, 1.0e-10_wp, & + 1.0_wp, q, curv, res, it, es, em, residual_out=fres, tangent_out=kband, & + torsion=tors) + CALL require(es == CD_HCSTAT_BADINPUT .AND. INDEX(em, 'tangent_out') > 0, & + 'F: a banded tangent_out with torsion is rejected (it cannot hold g g^T / C)') + END BLOCK + END SUBROUTINE check_fail_closed + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_resolve() + !! R re-solve with a stored Theta (has_theta): the untwisted straight rod has Theta = 0, so a + !! stored value within pi/2 continues on its branch (torque Phi / C), while one farther away + !! (2 and 3.5 rad: the branch of 3.5 would be 2 pi, a torque one turn off) stops by name and + !! leaves the stored state as it was. + REAL(wp), PARAMETER :: STORED(4) = [0.0_wp, 1.0_wp, 2.0_wp, 3.5_wp] + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: q(:) + INTEGER :: k, es + LOGICAL :: near + CHARACTER(512) :: em + DO k = 1, SIZE(STORED) + CALL straight_torsion(10, 1.0_wp, 1.0_wp, tors) + tors%has_theta = .TRUE. + tors%theta = STORED(k) + CALL solve_rod(10, 1.0e7_wp, 1.0_wp, 1.0_wp, CD_ENDCONN_RIGID, tors, q, es, em) + near = STORED(k) <= 0.5_wp*CD_HTORS_PI + WRITE (*, '(A,F5.2,A,I0,A,F10.6,A,F10.6)') 're-solve from stored Theta ', STORED(k), ': status ', es, & + ', Theta ', tors%theta, ', torque ', tors%torque + IF (near) THEN + CALL require(es == CD_HCSTAT_OK .AND. ABS(tors%torque - 1.0_wp) <= 1.0e-10_wp, & + 'R: a stored Theta within pi/2 continues on its branch: '//TRIM(em)) + ELSE + CALL require(es /= CD_HCSTAT_OK .AND. INDEX(em, 'ambiguous') > 0, & + 'R: a stored Theta beyond pi/2 of the untwisted state stops by name') + CALL require(tors%has_theta .AND. ABS(tors%theta - STORED(k)) <= 0.0_wp, & + 'R: the stopped re-solve leaves the stored Theta unchanged') + END IF + END DO + ! the same rod at a tolerance below its untwisted residual floor: the stop names the floor + BLOCK + REAL(wp), ALLOCATABLE :: seed(:), l0(:), f(:), curv(:) + REAL(wp) :: res + INTEGER :: it, nd + CALL straight_rod(10, 1.0_wp, seed, l0) + nd = SIZE(seed) + ALLOCATE (f(nd), curv(11)) + DEALLOCATE (q) + ALLOCATE (q(nd)) + f = 0.0_wp + f(nd - 5) = 1.0_wp + CALL straight_torsion(10, 1.0_wp, 1.0_wp, tors) + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e7_wp, it=1, 10)], [(1.0_wp, it=1, 10)], [(0.0_wp, it=1, 10)], & + seed, [1, 2, 3, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-9_wp, & + 1.0_wp, q, curv, res, it, es, em, f_nodal=f, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + WRITE (*, '(A,I0,A)') 'tolerance below the residual floor: status ', es, ' ('//TRIM(em)//')' + CALL require(es /= CD_HCSTAT_OK .AND. INDEX(em, 'residual floor') > 0 .AND. .NOT. tors%has_theta, & + 'R: a tolerance below the untwisted residual floor is reported as such') + END BLOCK + END SUBROUTINE check_resolve + +END PROGRAM test_hermite_torsion_static diff --git a/tests/test_step_allocations.f90 b/tests/test_step_allocations.f90 index 9bce06b..13e1644 100644 --- a/tests/test_step_allocations.f90 +++ b/tests/test_step_allocations.f90 @@ -16,9 +16,24 @@ PROGRAM test_step_allocations !! too: the envelope accumulation, the touchdown evaluation, and the EI = 0 and Hermite line !! samples with their FairTen/AnchTen end-force recovery. !! + !! With the single argument "torsion" the gate runs on a finite-EI cable with condensed torsion + !! instead (CD_HFMF_UpdateStates): a line clamped at both ends and restrained in torsion, its + !! coupled end swaying while the parent rolls about the line axis and the imposed twist + !! changes, so every step evaluates the twist kernel, its Hessian and the bordered solve. The + !! committed-state outputs (CD_HFMF_SetCoupledKinematics, CD_HFMF_CalcOutput with the connection + !! moment, both end forces, the energy and the torque query) are gated as well. + !! !! Usage: test_step_allocations [ranges] + !! test_step_allocations torsion USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_INT, C_LONG_LONG USE CableDyn_Precision, ONLY: wp + USE CableDyn_EndConnection, ONLY: CD_ENDCONN_RIGID + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType + USE CableDyn_OpenFAST_HermiteFMF, ONLY: CD_HFMF_ModuleType, CD_HFMF_Init, CD_HFMF_Set_EndConnection, & + CD_HFMF_Set_Torsion, CD_HFMF_UpdateStates, CD_HFMF_CalcOutput, CD_HFMF_End, & + CD_HFMF_SetCoupledKinematics + USE CableDyn_HermiteCableDynamic, ONLY: CD_HermiteCable_Dyn_End_Force, CD_HermiteCable_Dyn_Energy, & + CD_HermiteCable_Dyn_Torsion_State USE CableDyn_OpenFAST_Aggregate, ONLY: CD_AGG_ModuleType, CD_AGG_Init_From_Deck, CD_AGG_NMovingPoints, & CD_AGG_NFluidNodes, CD_AGG_NumChannels, CD_AGG_GetMovingPointMesh, & CD_AGG_Snapshot, CD_AGG_SetFluidFields, CD_AGG_Step_Moving, & @@ -47,6 +62,12 @@ END SUBROUTINE cd_test_allocation_counting INTEGER(C_LONG_LONG) :: n_step_alloc, n_output_alloc LOGICAL :: ranges + IF (COMMAND_ARGUMENT_COUNT() == 1) THEN + CALL GET_COMMAND_ARGUMENT(1, arg) + IF (TRIM(arg) /= 'torsion') ERROR STOP 'a single argument must be "torsion"' + CALL torsion_gate() + STOP + END IF IF (COMMAND_ARGUMENT_COUNT() /= 3 .AND. COMMAND_ARGUMENT_COUNT() /= 4) THEN PRINT '(A)', 'usage: test_step_allocations [ranges]' ERROR STOP 2 @@ -148,6 +169,102 @@ SUBROUTINE march(first_step, count, counted) END DO END SUBROUTINE march + SUBROUTINE torsion_gate() + !! Zero heap allocation in CD_HFMF_UpdateStates of a cable with condensed torsion. + INTEGER, PARAMETER :: NE = 20, NN = NE + 1, NDOF = 6*NN, N_WARM = 40, N_COUNT = 200 + REAL(wp), PARAMETER :: LL = 10.0_wp, DTT = 0.01_wp + TYPE(CD_HFMF_ModuleType) :: cab + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: q(NDOF), l0(NE), eye(3, 3), frame(3, 2), t, x(3), v(3), a(3), dcm(3, 3), f(3), m(3), psi + REAL(wp) :: fe(3), ke, se, th, mt + INTEGER :: k + INTEGER(C_LONG_LONG) :: c0, n_alloc, n_out, n_energy + n_out = 0 + n_energy = 0 + q = 0.0_wp + DO k = 1, NN + q(6*k - 5) = LL*REAL(k - 1, wp)/REAL(NE, wp) + q(6*k - 2) = 1.0_wp + END DO + l0 = LL/REAL(NE, wp) + eye = 0.0_wp + eye(1, 1) = 1.0_wp + eye(2, 2) = 1.0_wp + eye(3, 3) = 1.0_wp + CALL CD_HFMF_Init(cab, l0, [(1.0e5_wp, k=1, NE)], [(100.0_wp, k=1, NE)], [(1.0_wp, k=1, NE)], & + [(0.0_wp, k=1, NE)], q, [1, 2, 3, NDOF - 5, NDOF - 4, NDOF - 3], 0.0_wp, 0.0_wp, 0.8_wp, & + DTT, NN, es, em, max_iter=60, tol=1.0e-8_wp) + CALL check('torsion cable init') + CALL CD_HFMF_Set_EndConnection(cab, [0.0_wp, 0.0_wp], RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), es, em, & + coupled_d0_parent=[1.0_wp, 0.0_wp, 0.0_wp], parent_orientation=eye, & + connection_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID]) + CALL check('torsion cable end connections') + tors%active = .TRUE. + tors%phi = 2.0_wp + tors%ends = RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, & + 0.0_wp, 0.0_wp, 1.0_wp], [3, 4]) + ALLOCATE (tors%gj(NE)) + tors%gj = 80.0_wp + tors%has_theta = .TRUE. + frame(:, 1) = [1.0_wp, 0.0_wp, 0.0_wp] + frame(:, 2) = [0.0_wp, 0.0_wp, 1.0_wp] + CALL CD_HFMF_Set_Torsion(cab, tors, es, em, coupled_frame_parent=frame, parent_orientation=eye) + CALL check('torsion cable torsion') + n_alloc = 0 + DO k = 1, N_WARM + N_COUNT + t = DTT*REAL(k, wp) + x = [LL, 0.05_wp*SIN(2.0_wp*t), 0.0_wp] + v = [0.0_wp, 0.1_wp*COS(2.0_wp*t), 0.0_wp] + a = [0.0_wp, -0.2_wp*SIN(2.0_wp*t), 0.0_wp] + psi = 3.0_wp*t + dcm = eye + dcm(2, 2) = COS(psi) + dcm(3, 3) = COS(psi) + dcm(2, 3) = SIN(psi) + dcm(3, 2) = -SIN(psi) + c0 = cd_test_allocation_count() + IF (k > N_WARM) CALL cd_test_allocation_counting(1_C_INT) + CALL CD_HFMF_UpdateStates(cab, x, v, a, es, em, u_orientation=dcm, u_angular_velocity=[3.0_wp, 0.0_wp, 0.0_wp], & + u_angular_acceleration=[0.0_wp, 0.0_wp, 0.0_wp], u_twist=2.0_wp - psi + 0.3_wp*SIN(t)) + CALL cd_test_allocation_counting(0_C_INT) + CALL check('torsion cable step') + n_alloc = n_alloc + (cd_test_allocation_count() - c0) + ! the outputs of the committed state and the non-stepping boundary write: the loads and + ! connection moment, the end forces, the energy and the twist and torque queries + c0 = cd_test_allocation_count() + IF (k > N_WARM) CALL cd_test_allocation_counting(1_C_INT) + CALL CD_HFMF_SetCoupledKinematics(cab, x, v, a, es, em, u_orientation=dcm, & + u_angular_velocity=[3.0_wp, 0.0_wp, 0.0_wp], & + u_angular_acceleration=[0.0_wp, 0.0_wp, 0.0_wp]) + IF (es == 0) CALL CD_HFMF_CalcOutput(cab, f, es, em, y_moment=m) + IF (es == 0) CALL CD_HermiteCable_Dyn_End_Force(cab%line, 1, fe, es, em) + IF (es == 0) CALL CD_HermiteCable_Dyn_End_Force(cab%line, NN, fe, es, em) + IF (es == 0) CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) + CALL cd_test_allocation_counting(0_C_INT) + CALL check('torsion cable output') + n_out = n_out + (cd_test_allocation_count() - c0) + ! the energy diagnostic (not a per-step output; its mass product uses a run-time-sized + ! temporary): only its torsion part is required to be allocation-free, so the count of + ! the call may not exceed that of the same call without torsion (one temporary) + c0 = cd_test_allocation_count() + IF (k > N_WARM) CALL cd_test_allocation_counting(1_C_INT) + CALL CD_HermiteCable_Dyn_Energy(cab%line, ke, se, es, em) + CALL cd_test_allocation_counting(0_C_INT) + CALL check('torsion cable energy') + IF (k > N_WARM) n_energy = MAX(n_energy, cd_test_allocation_count() - c0) + END DO + CALL CD_HFMF_End(cab) + PRINT '(A,I0,A,I0,A,I0,A)', 'torsion cable: ', N_COUNT, ' counted steps, ', n_alloc, & + ' heap allocations in the steps, ', n_out, ' in the outputs' + PRINT '(A,I0)', 'torsion cable energy diagnostic: heap allocations per call ', n_energy + IF (n_alloc /= 0 .OR. n_out /= 0 .OR. n_energy > 1) THEN + PRINT '(A)', 'FAIL: the per-step advance or the outputs of a cable with torsion allocated after warm-up' + ERROR STOP 1 + END IF + PRINT '(A)', 'PASS: no heap allocation in the per-step advance or outputs of a cable with torsion after warm-up' + END SUBROUTINE torsion_gate + SUBROUTINE check(stage) CHARACTER(*), INTENT(IN) :: stage diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 new file mode 100644 index 0000000..ada5465 --- /dev/null +++ b/tests/test_torsion_deck.f90 @@ -0,0 +1,1017 @@ +! File: tests/test_torsion_deck.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +!> Deck-level gates of condensed torsion (END CONNECTIONS columns TorsStiffness NxX NxY NxZ +!> [Pretwist], the standalone static route, the Torq/Twist channels and the body torque): +!> 1. pure torsion of a straight neutral line clamped at two Fixed points, two turns of +!> pretwist: Torq = GJ Phi / L at every node, Twist = 720 deg, TwistN linear from +!> End A; torsional end springs add their compliance; +!> 2. backward compatibility: the same deck with Free torsion columns (any normal), or with one +!> end restrained only, writes the same .out bytes as the 6-column rows; Rigid, Infinity, +!> Inf and a quoted "Rigid" are one restraint; +!> 3. a pretwist sweep to three turns on a sagging clamped line: the torque grows in equal +!> steps (no 2 pi slip) and Twist follows the pretwist; +!> 4. body torque return: a massive Rigid6 body (attitude rolled, pitched and yawed) holding a +!> twisted straight taut line: the connection moment is M_t along the line axis (1e-8), the body +!> net wrench equals the line end loads (output precision), and the sign follows the end +!> that is twisted; +!> 5. body static equilibrium: a moored body whose clamped cable is twisted rolls against the +!> torque and its static net moment vanishes; +!> 6. named errors for malformed rows, missing GJ, an EI = 0 line, a channel on an unrestrained +!> line, the configuration blend in a dynamic run, roll-column misuse, the coupled entries; +!> 7. dynamics driven at End A: a motionFile roll column (a step) and a vessel rolling about the +!> line axis give Twist = -roll and the quasi-static torque at every step; +!> 8. a body rolling about a twisted line oscillates at sqrt(GJ / (L I)), the connection moment +!> being the torque along the line axis at every step; +!> 9. static body/cable torsion coupling with no yaw restoring and with a yaw stiffness of the +!> order of GJ/L: the passes converge and the body moment balances to 1e-8; +!> 10. gate 8 with a bending-pinned End A on the turning body (torsion restrained, the +!> semi-tangential end): the same torsional pendulum and connection moment; +!> 11. gate 8 from rest at Omega dt = 2 and 5: the body roll follows the closed form of the +!> march's trapezoidal body integrator at every step (unconditionally stable). +PROGRAM test_torsion_deck + USE CableDyn_Precision, ONLY: wp + USE CableDyn_Conventions, ONLY: CD_Body_Rotation + USE CableDyn_DeckDriver, ONLY: CD_Run_Deck_Driver, CD_DECKDRV_OK, CD_Multibody_Probe_Arm, CD_Multibody_Probe_Get, & + CD_Init_Deck_Aggregate, CD_DeckAggregateType, CD_End_Deck_Aggregate, & + CD_Init_Deck_HermiteCable + USE CableDyn_OpenFAST_HermiteFMF, ONLY: CD_HFMF_ModuleType, CD_HFMF_End + USE, INTRINSIC :: IEEE_ARITHMETIC, ONLY: IEEE_IS_FINITE + IMPLICIT NONE + + REAL(wp), PARAMETER :: PI = 3.14159265358979323846_wp, RHOW = 1025.0_wp + INTEGER :: n_fail = 0, only + CHARACTER(160) :: neutral_type + CHARACTER(16) :: arg + CHARACTER(8) :: no_rows(0) + + ! neutral 0.2 m section: mass = rhoW pi d^2 / 4; EA 1e7, EI 1e6, GAs 1e8, GJ 5e4 + WRITE (neutral_type, '(A,ES24.16,A)') 'cab 0.2 ', RHOW*0.25_wp*PI*0.04_wp, & + ' 1.0e7 0.0 1.0e6 1.0e8 5.0e4 1.0 1.0 0.0 0.0 0.0 0.0' + + ! optional argument: run only gate 1..9 (diagnostics) + only = 0 + IF (COMMAND_ARGUMENT_COUNT() > 0) THEN + CALL GET_COMMAND_ARGUMENT(1, arg) + READ (arg, *) only + END IF + IF (only == 0 .OR. only == 1) CALL check_pure_torsion() + IF (only == 0 .OR. only == 2) CALL check_backward_compatible() + IF (only == 0 .OR. only == 2) CALL check_rigid_keywords() + IF (only == 0 .OR. only == 3) CALL check_sweep() + IF (only == 0 .OR. only == 4) CALL check_body_torque() + IF (only == 0 .OR. only == 5) CALL check_body_statics() + IF (only == 0 .OR. only == 6) CALL check_errors() + IF (only == 0 .OR. only == 7) CALL check_dynamic_roll() + IF (only == 0 .OR. only == 8) CALL check_body_roll_dynamics('Rigid') + IF (only == 0 .OR. only == 10) CALL check_body_roll_dynamics('Pinned') + IF (only == 0 .OR. only == 9) CALL check_body_yaw_statics() + IF (only == 0 .OR. only == 11) CALL check_body_roll_large_step(2.0_wp) + IF (only == 0 .OR. only == 11) CALL check_body_roll_large_step(5.0_wp) + IF (n_fail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', n_fail, ' torsion deck gate(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: torsion deck columns, statics, dynamics, channels, body torque and refusals' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(cond, msg) + LOGICAL, INTENT(IN) :: cond + CHARACTER(*), INTENT(IN) :: msg + IF (.NOT. cond) THEN + n_fail = n_fail + 1 + WRITE (*, '(A,A)') 'FAILED: ', msg + END IF + END SUBROUTINE require + + SUBROUTINE write_deck(path, types, bodies, points, lines, sections, endconns, options, outs) + CHARACTER(*), INTENT(IN) :: path + CHARACTER(*), INTENT(IN) :: types(:), bodies(:), points(:), lines(:), sections(:), endconns(:), options(:), & + outs(:) + INTEGER :: u, i + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE') + WRITE (u, '(A)') 'torsion deck gate' + WRITE (u, '(A)') '--- LINE TYPES ---' + IF (INDEX(types(1), 'TEN') > 0) THEN + WRITE (u, '(A)') 'Name Diam Mass EA BA EI Cdn Cdt Can Cat' + WRITE (u, '(A)') '(-) (m) (kg/m) (N) (-) (Nm2) (-) (-) (-) (-)' + ELSE + WRITE (u, '(A)') 'Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat' + WRITE (u, '(A)') '(-) (m) (kg/m) (N) (-) (Nm2) (N) (Nm2) (kgm) (kgm) (-) (-) (-) (-)' + END IF + DO i = 1, SIZE(types) + IF (INDEX(types(i), 'TEN') > 0) THEN + WRITE (u, '(A)') TRIM(types(i) (1:INDEX(types(i), 'TEN') - 1)) + ELSE + WRITE (u, '(A)') TRIM(types(i)) + END IF + END DO + IF (SIZE(bodies) > 0) THEN + WRITE (u, '(A)') '--- BODIES ---' + WRITE (u, '(A)') 'ID Type X Y Z Roll Pitch Yaw Mass Vol C33 C44 C55 CdA Ca Ixx Iyy Izz' + WRITE (u, '(A)') '(-) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m3) (N/m) (Nm) (Nm) (m2) (-) (kgm2) (kgm2) (kgm2)' + DO i = 1, SIZE(bodies) + WRITE (u, '(A)') TRIM(bodies(i)) + END DO + END IF + WRITE (u, '(A)') '--- POINTS ---' + WRITE (u, '(A)') 'ID Type X Y Z Mass Vol CdA Ca' + WRITE (u, '(A)') '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)' + DO i = 1, SIZE(points) + WRITE (u, '(A)') TRIM(points(i)) + END DO + WRITE (u, '(A)') '--- LINES ---' + WRITE (u, '(A)') 'ID NodeA NodeB Outputs' + WRITE (u, '(A)') '(-) (-) (-) (-)' + DO i = 1, SIZE(lines) + WRITE (u, '(A)') TRIM(lines(i)) + END DO + WRITE (u, '(A)') '--- SECTIONS ---' + WRITE (u, '(A)') 'LineID LineType Length NumSegs' + WRITE (u, '(A)') '(-) (-) (m) (-)' + DO i = 1, SIZE(sections) + WRITE (u, '(A)') TRIM(sections(i)) + END DO + IF (SIZE(endconns) > 0) THEN + WRITE (u, '(A)') '--- END CONNECTIONS ---' + WRITE (u, '(A)') 'LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist' + WRITE (u, '(A)') '(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)' + DO i = 1, SIZE(endconns) + WRITE (u, '(A)') TRIM(endconns(i)) + END DO + END IF + WRITE (u, '(A)') '--- OPTIONS ---' + WRITE (u, '(A)') '9.80665 g' + WRITE (u, '(A)') '1025.0 rhoW' + DO i = 1, SIZE(options) + WRITE (u, '(A)') TRIM(options(i)) + END DO + WRITE (u, '(A)') '--- OUTPUTS ---' + DO i = 1, SIZE(outs) + WRITE (u, '(A)') TRIM(outs(i)) + END DO + WRITE (u, '(A)') '--- END ---' + CLOSE (u) + END SUBROUTINE write_deck + + SUBROUTINE run_deck(path, root, ok, em_out) + CHARACTER(*), INTENT(IN) :: path, root + LOGICAL, INTENT(OUT) :: ok + CHARACTER(*), INTENT(OUT) :: em_out + LOGICAL :: conv + INTEGER :: es + CALL CD_Run_Deck_Driver(path, root, conv, es, em_out) + ok = es == CD_DECKDRV_OK .AND. conv + END SUBROUTINE run_deck + + SUBROUTINE read_row(path, row, vals, ok) + !! Data row `row` (0 = first) of a driver .out file. + CHARACTER(*), INTENT(IN) :: path + INTEGER, INTENT(IN) :: row + REAL(wp), INTENT(OUT) :: vals(:) + LOGICAL, INTENT(OUT) :: ok + INTEGER :: u, ios, i + CHARACTER(2048) :: buf + ok = .FALSE. + vals = 0.0_wp + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) RETURN + READ (u, '(A)', IOSTAT=ios) buf + READ (u, '(A)', IOSTAT=ios) buf + DO i = 0, row + READ (u, *, IOSTAT=ios) vals + IF (ios /= 0) THEN + CLOSE (u) + RETURN + END IF + END DO + CLOSE (u) + ok = ALL(IEEE_IS_FINITE(vals)) + END SUBROUTINE read_row + + LOGICAL FUNCTION same_file(a, b) RESULT(same) + !! Byte-identical text files. + CHARACTER(*), INTENT(IN) :: a, b + INTEGER :: ua, ub, ia, ib + CHARACTER(4096) :: la, lb + same = .FALSE. + OPEN (NEWUNIT=ua, FILE=a, STATUS='OLD', ACTION='READ', IOSTAT=ia) + IF (ia /= 0) RETURN + OPEN (NEWUNIT=ub, FILE=b, STATUS='OLD', ACTION='READ', IOSTAT=ib) + IF (ib /= 0) THEN + CLOSE (ua) + RETURN + END IF + DO + READ (ua, '(A)', IOSTAT=ia) la + READ (ub, '(A)', IOSTAT=ib) lb + IF (ia /= 0 .OR. ib /= 0) EXIT + IF (la /= lb) THEN + CLOSE (ua) + CLOSE (ub) + RETURN + END IF + END DO + same = ia /= 0 .AND. ib /= 0 + CLOSE (ua) + CLOSE (ub) + END FUNCTION same_file + + FUNCTION cross(a, b) RESULT(c) + REAL(wp), INTENT(IN) :: a(3), b(3) + REAL(wp) :: c(3) + c = [a(2)*b(3) - a(3)*b(2), a(3)*b(1) - a(1)*b(3), a(1)*b(2) - a(2)*b(1)] + END FUNCTION cross + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE straight_deck(path, conn_a, conn_b, outs, types) + !! A 100 m neutral line along +x between two Fixed points, 40 elements, TMax 0. + CHARACTER(*), INTENT(IN) :: path, conn_a, conn_b, outs(:) + CHARACTER(*), INTENT(IN), OPTIONAL :: types(:) + CHARACTER(160) :: trow(1) + CHARACTER(96) :: ec(2) + trow(1) = neutral_type + IF (PRESENT(types)) trow(1) = types(1) + ec(1) = '1 A '//conn_a + ec(2) = '1 B '//conn_b + CALL write_deck(path, trow, no_rows, & + [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0'], & + [CHARACTER(16) :: '1 1 2 -'], [CHARACTER(24) :: '1 cab 100.0 40'], ec, & + [CHARACTER(24) :: '0.1 dtM', '0.0 TMax'], outs) + END SUBROUTINE straight_deck + + SUBROUTINE check_pure_torsion() + CHARACTER(16), PARAMETER :: OUTS(8) = [CHARACTER(16) :: 'Torq1N1', 'Torq1N21', 'Torq1N41', 'Twist1N1', & + 'Twist1N21', 'Twist1N41', 'Twist1', 'FairTen1'] + REAL(wp) :: v(9), m_ref, c + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: isp + DO isp = 0, 1 + IF (isp == 0) THEN + CALL straight_deck('tdeck_pure.dat', 'Rigid 1 0 0 Rigid 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + c = 100.0_wp/5.0e4_wp + ELSE + CALL straight_deck('tdeck_pure.dat', 'Rigid 1 0 0 1.0e5 0 0 1 0', 'Rigid 1 0 0 1.0e5 0 0 1 720', OUTS) + c = 100.0_wp/5.0e4_wp + 2.0e-5_wp + END IF + CALL run_deck('tdeck_pure.dat', 'tdeck_pure', ok, em) + CALL require(ok, 'pure torsion deck runs: '//TRIM(em)) + IF (.NOT. ok) CYCLE + CALL read_row('tdeck_pure.out', 0, v, ok) + CALL require(ok, 'pure torsion output readable') + m_ref = 4.0_wp*PI/c + WRITE (*, '(A,I0,A,3ES15.7,A,ES15.7,A,4F10.4)') 'deck pure torsion (springs ', isp, '): Torq ', v(2:4), & + ' ref ', m_ref, '; Twist N1/N21/N41/total ', v(5:8) + CALL require(nan_max_abs(v(2:4) - m_ref) <= 1.0e-6_wp*m_ref, 'Torq = GJ Phi / C at every node') + CALL require(ABS(v(8) - 720.0_wp) <= 1.0e-5_wp, 'Twist = 720 deg') + CALL require(ABS(v(5)) <= 1.0e-9_wp, 'TwistN1 = 0 at End A') + CALL require(ABS(v(7) - m_ref*100.0_wp/5.0e4_wp*180.0_wp/PI) <= 1.0e-5_wp, 'TwistN41 = M L / GJ') + CALL require(ABS(v(6) - 0.5_wp*v(7)) <= 1.0e-5_wp, 'TwistN linear along a uniform line') + END DO + END SUBROUTINE check_pure_torsion + + SUBROUTINE check_backward_compatible() + !! 6-column rows (bending connections only) vs the same with Free torsion columns, and + !! with one end restrained in torsion: identical .out files. + CHARACTER(16), PARAMETER :: OUTS(4) = [CHARACTER(16) :: 'FairTen1', 'AnchTen1', 'BendMom1N1', 'Curv1N20'] + CHARACTER(160) :: t10(1) + LOGICAL :: ok + CHARACTER(512) :: em + ! a sagging line so that the bending ends matter; 10-column type row (no GJ) + t10(1) = 'cab 0.2 60.0 1.0e9 0.0 1.0e5 0.0 0.0 0.0 0.0TEN' + CALL straight_deck('tdeck_bc0.dat', 'Rigid 1 0 -0.3', 'Rigid 1 0 0.3', OUTS, t10) + CALL run_deck('tdeck_bc0.dat', 'tdeck_bc0', ok, em) + CALL require(ok, 'six-column deck runs: '//TRIM(em)) + CALL straight_deck('tdeck_bc1.dat', 'Rigid 1 0 -0.3 Free 0 1 0', 'Rigid 1 0 0.3 Free 0 1 0 45', OUTS, t10) + CALL run_deck('tdeck_bc1.dat', 'tdeck_bc1', ok, em) + CALL require(ok, 'Free torsion columns run: '//TRIM(em)) + CALL require(same_file('tdeck_bc0.out', 'tdeck_bc1.out'), 'Free torsion columns: identical .out') + CALL straight_deck('tdeck_bc2.dat', 'Rigid 1 0 -0.3 Rigid 0 1 0', 'Rigid 1 0 0.3 Free 0 1 0 45', OUTS, t10) + CALL run_deck('tdeck_bc2.dat', 'tdeck_bc2', ok, em) + CALL require(ok, 'one-end torsion runs: '//TRIM(em)) + CALL require(same_file('tdeck_bc0.out', 'tdeck_bc2.out'), 'one restrained end: identical .out') + CALL require(same_file('tdeck_bc0.static.out', 'tdeck_bc2.static.out'), 'one restrained end: identical profile') + ! a Free end has no torsion frame: a zero normal, or one along Ez, is not checked + CALL straight_deck('tdeck_bc3.dat', 'Rigid 1 0 -0.3 Free 0 0 0', 'Rigid 1 0 0.3 Zero 1 0 0.3 45', OUTS, t10) + CALL run_deck('tdeck_bc3.dat', 'tdeck_bc3', ok, em) + CALL require(ok, 'Free torsion columns with an unused normal run: '//TRIM(em)) + CALL require(same_file('tdeck_bc0.out', 'tdeck_bc3.out'), 'Free torsion columns, unused normal: identical .out') + END SUBROUTINE check_backward_compatible + + SUBROUTINE check_rigid_keywords() + !! The TorsStiffness spellings Rigid, Infinity, Inf and a quoted "Rigid" are the same + !! restraint: the pure-torsion deck writes the same .out with each. + CHARACTER(16), PARAMETER :: OUTS(2) = [CHARACTER(16) :: 'Torq1N1', 'Twist1'] + CHARACTER(12), PARAMETER :: WORDS(3) = [CHARACTER(12) :: 'Infinity', 'Inf', '"Rigid"'] + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: k + CALL straight_deck('tdeck_kw0.dat', 'Rigid 1 0 0 Rigid 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL run_deck('tdeck_kw0.dat', 'tdeck_kw0', ok, em) + CALL require(ok, 'keyword reference deck runs: '//TRIM(em)) + DO k = 1, SIZE(WORDS) + CALL straight_deck('tdeck_kw1.dat', 'Rigid 1 0 0 '//TRIM(WORDS(k))//' 0 0 1 0', & + 'Rigid 1 0 0 '//TRIM(WORDS(k))//' 0 0 1 720', OUTS) + CALL run_deck('tdeck_kw1.dat', 'tdeck_kw1', ok, em) + CALL require(ok, 'TorsStiffness '//TRIM(WORDS(k))//' is accepted: '//TRIM(em)) + CALL require(same_file('tdeck_kw0.out', 'tdeck_kw1.out'), 'TorsStiffness '//TRIM(WORDS(k))//' = Rigid') + END DO + END SUBROUTINE check_rigid_keywords + + SUBROUTINE check_sweep() + !! Sagging heavy line (EI 1e4, GJ 1e4) clamped at both ends; pretwist 0..1080 deg in 90 deg + !! steps (separate runs, each from the untwisted state). + REAL(wp) :: v(4), tq(0:12), tw(0:12), dstep + LOGICAL :: ok + CHARACTER(512) :: em + CHARACTER(96) :: cb + CHARACTER(160) :: trow(1) + INTEGER :: k + trow(1) = 'cab 0.2 60.0 1.0e9 0.0 1.0e4 1.0e8 1.0e4 1.0 1.0 0.0 0.0 0.0 0.0' + DO k = 0, 12 + WRITE (cb, '(A,F8.1)') 'Rigid 1 0 0.4 Rigid 0 1 0 ', 90.0_wp*k + CALL straight_deck('tdeck_sweep.dat', 'Rigid 1 0 -0.4 Rigid 0 1 0 0', TRIM(cb), & + [CHARACTER(16) :: 'Torq1N1', 'Torq1N21', 'Twist1'], trow) + CALL run_deck('tdeck_sweep.dat', 'tdeck_sweep', ok, em) + CALL require(ok, 'sweep deck runs: '//TRIM(em)) + IF (.NOT. ok) RETURN + CALL read_row('tdeck_sweep.out', 0, v, ok) + tq(k) = v(2) + tw(k) = v(4) + CALL require(ABS(v(2) - v(3)) <= 1.0e-6_wp*MAX(1.0_wp, ABS(v(2))), 'sweep: torque uniform along the line') + END DO + dstep = (tq(12) - tq(0))/12.0_wp + WRITE (*, '(A,ES12.5,A,ES12.5,A,F10.3,A)') 'deck sweep: torque step ', dstep, ' N m (max deviation ', & + MAXVAL(ABS(tq(1:12) - tq(0:11) - dstep)), '), Twist1 at 3 turns ', tw(12), ' deg' + CALL require(dstep > 0.0_wp, 'sweep: torque grows with the pretwist') + CALL require(MAXVAL(ABS(tq(1:12) - tq(0:11) - dstep)) <= 0.05_wp*dstep, 'sweep: equal torque steps, no slip') + CALL require(ABS(tw(12) - 1080.0_wp) <= 0.01_wp*1080.0_wp, 'sweep: Twist follows the pretwist') + END SUBROUTINE check_sweep + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE body_deck(path, rot, pre_a, pre_b, outs) + !! Massive neutral Rigid6 body (attitude rot) holding a 20 m neutral line, stretched by 1e-4, + !! from its point off (body frame) along dglob = rot dbody to a Fixed anchor; both ends Rigid + !! in bending and torsion, normals nbody (body) / rot nbody (anchor). + CHARACTER(*), INTENT(IN) :: path, outs(:) + REAL(wp), INTENT(IN) :: rot(3) + REAL(wp), INTENT(IN) :: pre_a, pre_b + REAL(wp), PARAMETER :: MB = 1.0e9_wp + REAL(wp) :: r(3, 3), dbody(3), nbody(3), off(3), ra(3), anchor(3), dg(3), ng(3) + CHARACTER(256) :: brow, pa, pb, ca, cb + r = CD_Body_Rotation(rot(1), rot(2), rot(3)) + dbody = [0.8_wp, 0.0_wp, -0.6_wp] + nbody = [0.0_wp, 1.0_wp, 0.0_wp] + off = [1.5_wp, 0.5_wp, -0.5_wp] + ra = [0.0_wp, 0.0_wp, -60.0_wp] + MATMUL(r, off) + dg = MATMUL(r, dbody) + ng = MATMUL(r, nbody) + anchor = ra + 20.002_wp*dg + WRITE (brow, '(A,3F8.2,A,ES24.16,A,ES24.16,A)') '1 Rigid6 0 0 -60 ', rot, ' ', MB, ' ', MB/RHOW, & + ' 0 0 0 0 0 1.0e12 1.0e12 1.0e12' + WRITE (pa, '(A,3ES24.16,A)') '1 Body1 ', off, ' 0 0 0 0' + WRITE (pb, '(A,3ES24.16,A)') '2 Fixed ', anchor, ' 0 0 0 0' + WRITE (ca, '(A,3ES24.16,A,3ES24.16,F9.2)') '1 A Rigid ', dbody, ' Rigid ', nbody, pre_a + WRITE (cb, '(A,3ES24.16,A,3ES24.16,F9.2)') '1 B Rigid ', dg, ' Rigid ', ng, pre_b + CALL write_deck(path, [neutral_type], [brow], [pa, pb], [CHARACTER(16) :: '1 1 2 -'], & + [CHARACTER(24) :: '1 cab 20.0 20'], [ca, cb], & + [CHARACTER(24) :: '200.0 WtrDpth', 'moordyn bodyWetting', '0.01 dtM', '0.0 TMax', 'deck bodyIC'], & + outs) + END SUBROUTINE body_deck + + SUBROUTINE check_body_torque() + CHARACTER(12), PARAMETER :: OUTS(7) = [CHARACTER(12) :: 'Body1Fx', 'Body1Fy', 'Body1Fz', 'Body1Mx', 'Body1My', & + 'Body1Mz', 'Torq1N1'] + REAL(wp), PARAMETER :: ROT(3) = [10.0_wp, -20.0_wp, 35.0_wp] + REAL(wp), ALLOCATABLE :: rec(:, :) + REAL(wp) :: r(3, 3), dg(3), off(3), m_ref, ma(3), fa(3), wrench(8), mnet(3), e_axis, ew + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: isign + r = CD_Body_Rotation(ROT(1), ROT(2), ROT(3)) + dg = MATMUL(r, [0.8_wp, 0.0_wp, -0.6_wp]) + off = [1.5_wp, 0.5_wp, -0.5_wp] + DO isign = 1, -1, -2 + ! +: one turn at End B (Phi = +2 pi); -: one turn at End A (Phi = -2 pi) + IF (isign == 1) THEN + CALL body_deck('tdeck_body.dat', ROT, 0.0_wp, 360.0_wp, OUTS) + ELSE + CALL body_deck('tdeck_body.dat', ROT, 360.0_wp, 0.0_wp, OUTS) + END IF + m_ref = isign*5.0e4_wp*2.0_wp*PI/20.0_wp + CALL CD_Multibody_Probe_Arm([0.0_wp, 0.0_wp, 0.0_wp], [0.0_wp, 0.0_wp, 0.0_wp]) + CALL run_deck('tdeck_body.dat', 'tdeck_body', ok, em) + CALL CD_Multibody_Probe_Get(rec) + ok = ok .AND. ALLOCATED(rec) + CALL require(ok, 'body torque deck runs and is probed: '//TRIM(em)) + IF (.NOT. ok) RETURN + ma = rec(24:26, 0) + fa = rec(27:29, 0) + e_axis = NORM2(ma - m_ref*dg)/ABS(m_ref) + CALL read_row('tdeck_body.out', 0, wrench, ok) + CALL require(ok, 'body torque output readable') + mnet = cross(MATMUL(r, off), fa) + ma + ew = nan_max_abs([NORM2(wrench(2:4) - fa)/MAX(NORM2(fa), ABS(m_ref)/20.0_wp), & + NORM2(wrench(5:7) - mnet)/NORM2(mnet)]) + WRITE (*, '(A,I2,A,3ES14.6,A,ES10.3,A,ES10.3,A,ES14.6)') 'body torque (sign', isign, '): moment ', ma, & + ' axis error ', e_axis, ', net wrench vs line loads ', ew, ', Torq ', wrench(8) + CALL require(e_axis <= 1.0e-8_wp, 'body: connection moment = M_t along the line axis (1e-8)') + CALL require(ew <= 1.0e-6_wp, 'body: net wrench equals the line end loads (output precision)') + CALL require(ABS(wrench(8) - m_ref) <= 1.0e-6_wp*ABS(m_ref), 'body: Torq channel = M_t') + CALL require(NORM2(fa - DOT_PRODUCT(fa, dg)*dg) <= 1.0e-6_wp*NORM2(fa), & + 'body: the straight twisted line pulls along its axis only') + END DO + END SUBROUTINE check_body_torque + + SUBROUTINE moored_deck(path, pretwist) + !! A buoyant body (C44 = C55 = 2e6 N m/rad) on three EI = 0 legs, with a finite-EI cable + !! clamped at its keel point (End A) and at an anchor on the seabed (End B), twisted by + !! pretwist at End B; bodyIC static, TMax 0. + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(IN) :: pretwist + CHARACTER(96) :: ca, cb + ca = '4 A Rigid -0.7071067811865476 0.0 -0.7071067811865476 Rigid 0.0 1.0 0.0 0' + WRITE (cb, '(A,F8.1)') '4 B Rigid -1.0 0.0 0.0 Rigid 0.0 1.0 0.0 ', pretwist + CALL write_deck(path, [CHARACTER(96) :: 'poly 0.12 15.0 5.0e7 -1.0 0.0 1.0e6 1.0 1.0 1.0 1.2 0.2 1.0 0.0', & + 'cab 0.2 60.0 5.0e8 -1.0 2.0e4 1.0e8 1.0e4 1.0 1.0 1.2 0.0 1.0 0.0'], & + [CHARACTER(96) :: '1 Rigid6 0 0 -20 0 0 0 2.0e4 40.0 0.0 2.0e6 2.0e6 8.0 0.5 3.5e4 3.5e4 3.5e4'], & + [CHARACTER(48) :: '1 Body1 1.5 0.0 -2.0 0 0 0 0', '2 Body1 -0.75 1.299 -2.0 0 0 0 0', & + '3 Body1 -0.75 -1.299 -2.0 0 0 0 0', '4 Fixed 40.0 0.0 -100.0 0 0 0 0', & + '5 Fixed -20.0 34.641 -100.0 0 0 0 0', '6 Fixed -20.0 -34.641 -100.0 0 0 0 0', & + '7 Body1 0.0 0.0 -3.0 0 0 0 0', '8 Fixed -40.0 0.0 -100.0 0 0 0 0'], & + [CHARACTER(16) :: '1 1 4 -', '2 2 5 -', '3 3 6 -', '4 7 8 -'], & + [CHARACTER(24) :: '1 poly 86.85 20', '2 poly 86.85 20', '3 poly 86.85 20', '4 cab 100.0 50'], & + [CHARACTER(96) :: ca, cb], & + [CHARACTER(24) :: '100.0 WtrDpth', '1.0e5 kBot', '1.0e4 cBot', '0.05 dtM', '0.0 TMax', & + 'static bodyIC'], & + [CHARACTER(12) :: 'Body1Rx', 'Body1Ry', 'Body1Rz', 'Body1Mx', 'Body1My', 'Body1Mz', 'Torq4N1']) + END SUBROUTINE moored_deck + + SUBROUTINE check_body_statics() + REAL(wp) :: v0(8), v1(8) + LOGICAL :: ok, ok1 + CHARACTER(512) :: em + CALL moored_deck('tdeck_moor0.dat', 0.0_wp) + CALL run_deck('tdeck_moor0.dat', 'tdeck_moor0', ok, em) + CALL require(ok, 'moored untwisted deck runs: '//TRIM(em)) + CALL moored_deck('tdeck_moor1.dat', 720.0_wp) + CALL run_deck('tdeck_moor1.dat', 'tdeck_moor1', ok1, em) + CALL require(ok1, 'moored twisted deck runs: '//TRIM(em)) + IF (.NOT. (ok .AND. ok1)) RETURN + CALL read_row('tdeck_moor0.out', 0, v0, ok) + CALL read_row('tdeck_moor1.out', 0, v1, ok1) + CALL require(ok .AND. ok1, 'moored outputs readable') + WRITE (*, '(A,3F11.6,A,3F11.6,A,3ES11.3,A,ES12.5)') 'moored body: attitude untwisted ', v0(2:4), & + ' deg, twisted ', v1(2:4), ' deg; static net moment ', v1(5:7), ' N m; Torq ', v1(8) + CALL require(v1(8) > 1.0e3_wp, 'moored: the twisted cable carries torque') + CALL require(nan_max_abs(v1(2:4) - v0(2:4)) > 1.0e-3_wp, 'moored: the torque turns the body in statics') + CALL require(nan_max_abs(v1(5:7)) <= 1.0e-4_wp*v1(8), 'moored: the static body moment balances the torque') + END SUBROUTINE check_body_statics + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE roll_deck(path, opt_motion, point_a) + !! The straight 100 m neutral line of straight_deck, End A on point_a (Coupled or Vessel), + !! both ends clamped and restrained in torsion with no pretwist, dynamic: dtM 0.05, TMax 0.5. + CHARACTER(*), INTENT(IN) :: path, opt_motion, point_a + CHARACTER(48) :: prow(2) + prow(1) = '1 '//point_a//' 0.0 0.0 -50.0 0 0 0 0' + prow(2) = '2 Fixed 100.0 0.0 -50.0 0 0 0 0' + CALL write_deck(path, [neutral_type], no_rows, prow, & + [CHARACTER(16) :: '1 1 2 r'], [CHARACTER(24) :: '1 cab 100.0 40'], & + [CHARACTER(96) :: '1 A Rigid 1 0 0 Rigid 0 0 1 0', '1 B Rigid 1 0 0 Rigid 0 0 1 0'], & + [CHARACTER(48) :: '0.05 dtM', '0.5 TMax', opt_motion], & + [CHARACTER(16) :: 'Torq1N1', 'Torq1N41', 'Twist1']) + END SUBROUTINE roll_deck + + SUBROUTINE check_dynamic_roll() + !! 7a. motionFile roll column: End A rolls 30 deg about Ez at t = 0.05 s (a step): from that + !! first step on, Twist1 = -30 deg and Torq = GJ Twist / L at both ends (quasi-static + !! torsion: no torsional inertia, the torque follows the roll at once). + !! 7b. vesselMotion: the vessel rolls about the line axis at 60 deg/s: the End A frame turns + !! with it and Twist1 = -roll(t), Torq = GJ Twist / L at every step (a frame rotation and + !! an imposed roll of the same end are the same twist). + REAL(wp), PARAMETER :: GJ = 5.0e4_wp, L = 100.0_wp + REAL(wp) :: v(4), roll, werr_a, werr_b, terr + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: u, k + ! 7a + OPEN (NEWUNIT=u, FILE='tdeck_roll_motion.txt', STATUS='REPLACE', ACTION='WRITE') + DO k = 0, 10 + roll = MERGE(0.0_wp, 30.0_wp, k == 0) + WRITE (u, '(F6.2,A,F6.1)') 0.05_wp*k, ' 1 0.0 0.0 -50.0 0 0 0 0 0 0 ', roll + END DO + CLOSE (u) + CALL roll_deck('tdeck_roll_a.dat', 'tdeck_roll_motion.txt motionFile', 'Coupled') + CALL run_deck('tdeck_roll_a.dat', 'tdeck_roll_a', ok, em) + CALL require(ok, '7a: the motionFile roll deck runs: '//TRIM(em)) + werr_a = 0.0_wp + terr = 0.0_wp + DO k = 0, 10 + CALL read_row('tdeck_roll_a.out', k, v, ok) + CALL require(ok, '7a: output row readable') + IF (.NOT. ok) EXIT + roll = MERGE(0.0_wp, 30.0_wp, k == 0) + werr_a = nan_max_abs([werr_a, ABS(v(4) + roll)]) + terr = nan_max_abs([terr, ABS(v(2) - GJ*(-roll*PI/180.0_wp)/L), ABS(v(3) - v(2))]) + END DO + WRITE (*, '(A,ES10.3,A,ES10.3,A)') 'roll column (step): Twist1 error ', werr_a, ' deg, torque error ', terr, ' N m' + CALL require(werr_a <= 1.0e-6_wp, '7a: Twist1 = -roll from the first step on') + CALL require(terr <= 1.0e-6_wp*GJ, '7a: Torq = GJ Twist / L at both ends at every step (quasi-static)') + CALL check_roll_range('tdeck_roll_a.Line1.range.out', GJ*(-30.0_wp*PI/180.0_wp)/L) + ! 7b + OPEN (NEWUNIT=u, FILE='tdeck_roll_vessel.txt', STATUS='REPLACE', ACTION='WRITE') + DO k = 0, 10 + WRITE (u, '(F6.2,A,F10.4,A,ES24.16,A)') 0.05_wp*k, ' 0 0 -50 ', 60.0_wp*0.05_wp*k, ' 0 0 0 0 0 ', & + 60.0_wp*PI/180.0_wp, ' 0 0 0 0 0 0 0 0' + END DO + CLOSE (u) + CALL roll_deck('tdeck_roll_b.dat', 'tdeck_roll_vessel.txt vesselMotion', 'Vessel') + CALL insert_option('tdeck_roll_b.dat', '0|0|-50 vesselRef') + CALL run_deck('tdeck_roll_b.dat', 'tdeck_roll_b', ok, em) + CALL require(ok, '7b: the vessel-roll deck runs: '//TRIM(em)) + werr_b = 0.0_wp + terr = 0.0_wp + DO k = 0, 10 + CALL read_row('tdeck_roll_b.out', k, v, ok) + CALL require(ok, '7b: output row readable') + IF (.NOT. ok) EXIT + roll = 60.0_wp*0.05_wp*k + werr_b = nan_max_abs([werr_b, ABS(v(4) + roll)]) + terr = nan_max_abs([terr, ABS(v(2) - GJ*(v(4)*PI/180.0_wp)/L)]) + END DO + WRITE (*, '(A,ES10.3,A,ES10.3,A)') 'vessel roll: Twist1 error ', werr_b, ' deg, torque error ', terr, ' N m' + CALL require(werr_b <= 1.0e-6_wp, '7b: the End A frame turns with the vessel: Twist1 = -roll(t)') + CALL require(terr <= 1.0e-6_wp*GJ, '7b: Torq = GJ Twist / L under vessel roll') + END SUBROUTINE check_dynamic_roll + + SUBROUTINE check_roll_range(path, m_step) + !! 7c. The range graph of a torsional line carries the torque and twist envelopes: over the + !! roll step, Torque spans [m_step, 0] at every node and the twist from End A spans + !! [-30, 0] deg at End B and is 0 at End A. + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(IN) :: m_step + CHARACTER(2048) :: head + CHARACTER(64) :: names(40) + REAL(wp) :: row(40) + INTEGER :: u, ios, k, ncol, itq, itw, nrow + LOGICAL :: ok + ok = .FALSE. + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + CALL require(ios == 0, '7c: the range file of the torsional line exists') + IF (ios /= 0) RETURN + READ (u, '(A)') head + READ (u, '(A)') head + ncol = 0 + names = '' + READ (head, *, IOSTAT=ios) names + DO k = 1, SIZE(names) + IF (LEN_TRIM(names(k)) > 0) ncol = k + END DO + itq = 0 + itw = 0 + DO k = 1, ncol + IF (TRIM(names(k)) == 'TorqueMin') itq = k + IF (TRIM(names(k)) == 'TwistMin') itw = k + END DO + CALL require(itq > 0 .AND. itw > 0, '7c: the range file has Torque and Twist columns') + IF (itq == 0 .OR. itw == 0) THEN + CLOSE (u) + RETURN + END IF + READ (u, '(A)') head + nrow = 0 + DO + READ (u, *, IOSTAT=ios) row(1:ncol) + IF (ios /= 0) EXIT + nrow = nrow + 1 + ok = ABS(row(itq) - m_step) <= 1.0e-6_wp*ABS(m_step) .AND. ABS(row(itq + 1)) <= 1.0e-6_wp*ABS(m_step) + CALL require(ok, '7c: Torque envelope [M_step, 0] at every node') + IF (nrow == 1) CALL require(ABS(row(itw)) + ABS(row(itw + 1)) <= 1.0e-9_wp, '7c: no twist at End A') + IF (nrow == 41) CALL require(ABS(row(itw) + 30.0_wp) <= 1.0e-6_wp .AND. ABS(row(itw + 1)) <= 1.0e-9_wp, & + '7c: twist envelope [-30, 0] deg at End B') + END DO + CLOSE (u) + CALL require(nrow == 41, '7c: one range row per node') + WRITE (*, '(A,I0,A)') 'roll range graph: ', nrow, ' nodes with Torque and Twist envelopes' + END SUBROUTINE check_roll_range + + SUBROUTINE insert_option(path, row) + !! Insert an OPTIONS row (just after the section header) of a deck written by write_deck. + CHARACTER(*), INTENT(IN) :: path, row + CHARACTER(512) :: lines(400) + INTEGER :: u, n, ios, k + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ') + n = 0 + DO + READ (u, '(A)', IOSTAT=ios) lines(n + 1) + IF (ios /= 0) EXIT + n = n + 1 + END DO + CLOSE (u) + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE') + DO k = 1, n + WRITE (u, '(A)') TRIM(lines(k)) + IF (INDEX(lines(k), '--- OPTIONS ---') > 0) WRITE (u, '(A)') TRIM(row) + END DO + CLOSE (u) + END SUBROUTINE insert_option + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_body_roll_dynamics(bend) + !! 8. A Rigid6 body (translation massive, rotational inertia I) holding a straight, taut line + !! twisted by one turn at its reference point, released with a small roll rate w0 about the + !! line axis: the line's torsion is the only roll stiffness, k = GJ/L, and nothing else holds + !! the twist, so the body is a torsional pendulum about the untwisted state: the torque is + !! M0 cos(Omega t) - sqrt(k I) w0 sin(Omega t), Omega = sqrt(k / I) (no torsional inertia in + !! the line), while the body turns through two turns and back; at every step the connection + !! moment on the body is the torque along the line axis. bend is End A's bending + !! connection: Rigid (gate 8) or Pinned (gate 10; the torsion frame still turns with the body). + CHARACTER(*), INTENT(IN) :: bend + REAL(wp), PARAMETER :: MB = 1.0e9_wp, IB = 1.0e3_wp, GJ = 5.0e4_wp, LN = 20.0_wp, W0 = 0.05_wp + INTEGER, PARAMETER :: NSTEP = 400 + REAL(wp), ALLOCATABLE :: rec(:, :) + REAL(wp) :: dg(3), anchor(3), v(2), m0, omega, amp, t, err_axis, err_wave, mt, dk(3), rk(3, 3), kt + CHARACTER(256) :: brow, pb, cb, ca + CHARACTER(64) :: root + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: k + root = 'tdeck_broll_'//bend + ca = '1 A '//bend//' 0.8 0 -0.6 Rigid 0 1 0 0' + dg = [0.8_wp, 0.0_wp, -0.6_wp] + anchor = [0.0_wp, 0.0_wp, -60.0_wp] + (LN*1.0001_wp)*dg + WRITE (brow, '(A,ES24.16,A,ES24.16,A,3ES12.4)') '1 Rigid6 0 0 -60 0 0 0 ', MB, ' ', MB/RHOW, & + ' 0 0 0 0 0 ', IB, IB, IB + WRITE (pb, '(A,3ES24.16,A)') '2 Fixed ', anchor, ' 0 0 0 0' + WRITE (cb, '(A,3ES24.16,A)') '1 B Rigid ', dg, ' Rigid 0 1 0 360' + CALL write_deck(TRIM(root)//'.dat', [neutral_type], [brow], & + [CHARACTER(96) :: '1 Body1 0 0 0 0 0 0 0', pb], [CHARACTER(16) :: '1 1 2 -'], & + [CHARACTER(24) :: '1 cab 20.0 20'], & + [ca, cb], & + [CHARACTER(24) :: '200.0 WtrDpth', 'moordyn bodyWetting', '0.01 dtM', '4.0 TMax', 'deck bodyIC'], & + [CHARACTER(12) :: 'Torq1N1']) + CALL CD_Multibody_Probe_Arm([0.0_wp, 0.0_wp, 0.0_wp], W0*dg) + CALL run_deck(TRIM(root)//'.dat', TRIM(root), ok, em) + CALL CD_Multibody_Probe_Get(rec) + ok = ok .AND. ALLOCATED(rec) + CALL require(ok, '8/10: the rolling-body deck ('//bend//' End A) runs and is probed: '//TRIM(em)) + IF (.NOT. ok) RETURN + kt = GJ/LN + m0 = kt*2.0_wp*PI + omega = SQRT(kt/IB) + amp = SQRT(kt*IB)*W0 + err_axis = 0.0_wp + err_wave = 0.0_wp + DO k = 0, NSTEP + CALL read_row(TRIM(root)//'.out', k, v, ok) + IF (.NOT. ok) EXIT + t = v(1) + mt = v(2) + rk = RESHAPE(rec(8:16, k), [3, 3]) + dk = MATMUL(rk, dg) + err_axis = nan_max_abs([err_axis, NORM2(rec(24:26, k) - mt*dk)/m0]) + err_wave = nan_max_abs([err_wave, ABS(mt - (m0*COS(omega*t) - amp*SIN(omega*t)))/m0]) + END DO + CALL require(ok, '8/10: all output rows readable') + WRITE (*, '(A,A,A,ES10.3,A,ES10.3,A,F8.4,A)') 'body roll dynamics (', bend, ' End A): moment-axis error ', & + err_axis, ', torque vs M0 cos(Omega t) - sqrt(k I) w0 sin(Omega t) ', err_wave, ' of M0 (Omega = ', omega, & + ' rad/s)' + CALL require(err_axis <= 1.0e-6_wp, '8/10: the connection moment on the body is the torque along the axis') + CALL require(err_wave <= 1.0e-2_wp, '8/10: the body swings as a torsional pendulum at sqrt(GJ/(L I))') + END SUBROUTINE check_body_roll_dynamics + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_body_roll_large_step(wdt) + !! 11. Gate 8's torsional pendulum stepped coarsely, Omega dt = wdt (2 and 5): a Rigid6 body + !! (translation massive, rotational inertia I = k (dt/wdt)^2) starts at rest on a straight + !! line twisted by 30 deg. The monolithic march carries the body inertia with am = af, + !! beta = 1/4, gamma = 1/2, which on the linear pendulum is the trapezoidal rule whatever + !! the dissipation: unconditionally stable, x_{n+1} with the closed form below and + !! bounded within [0, 60] deg. At these steps the body turns by up to 56 deg per step, + !! so the junction must converge the moment balance to the rotation (not to the body + !! weight) and keep each trial turn inside the torsion step limit. + REAL(wp), INTENT(IN) :: wdt + REAL(wp), PARAMETER :: MB = 1.0e9_wp, GJ = 5.0e4_wp, LN = 20.0_wp, DT = 0.01_wp, XEQ = 30.0_wp + INTEGER, PARAMETER :: NSTEP = 20 + REAL(wp), ALLOCATABLE :: rec(:, :) + REAL(wp) :: dg(3), anchor(3), kt, ib, x, v, a, xp, a1, err, rk(3, 3), w(3), ang, s, th + CHARACTER(256) :: brow, pb, cb, ca + CHARACTER(24) :: opt_dt, opt_t + CHARACTER(64) :: root + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: k + WRITE (root, '(A,I0)') 'tdeck_bigstep_', NINT(wdt) + kt = GJ/LN + ib = kt*(DT/wdt)**2 + dg = [0.8_wp, 0.0_wp, -0.6_wp] + anchor = [0.0_wp, 0.0_wp, -60.0_wp] + (LN*1.0001_wp)*dg + WRITE (brow, '(A,ES24.16,A,ES24.16,A,3ES24.16)') '1 Rigid6 0 0 -60 0 0 0 ', MB, ' ', MB/RHOW, & + ' 0 0 0 0 0 ', ib, ib, ib + WRITE (pb, '(A,3ES24.16,A)') '2 Fixed ', anchor, ' 0 0 0 0' + ca = '1 A Rigid 0.8 0 -0.6 Rigid 0 1 0 0' + WRITE (cb, '(A,3ES24.16,A)') '1 B Rigid ', dg, ' Rigid 0 1 0 30' + WRITE (opt_dt, '(ES12.5,A)') DT, ' dtM' + WRITE (opt_t, '(ES12.5,A)') NSTEP*DT, ' TMax' + CALL write_deck(TRIM(root)//'.dat', [neutral_type], [brow], & + [CHARACTER(96) :: '1 Body1 0 0 0 0 0 0 0', pb], [CHARACTER(16) :: '1 1 2 -'], & + [CHARACTER(24) :: '1 cab 20.0 20'], [ca, cb], & + [CHARACTER(24) :: '200.0 WtrDpth', 'moordyn bodyWetting', opt_dt, opt_t, 'deck bodyIC'], & + [CHARACTER(12) :: 'Torq1N1']) + CALL CD_Multibody_Probe_Arm([0.0_wp, 0.0_wp, 0.0_wp], [0.0_wp, 0.0_wp, 0.0_wp]) + CALL run_deck(TRIM(root)//'.dat', TRIM(root), ok, em) + CALL CD_Multibody_Probe_Get(rec) + ok = ok .AND. ALLOCATED(rec) + CALL require(ok, '11: the coarse-step rolling-body deck runs and is probed: '//TRIM(em)) + IF (.NOT. ok) RETURN + ok = UBOUND(rec, 2) >= NSTEP + CALL require(ok, '11: every step is recorded') + IF (.NOT. ok) RETURN + ! the march's own closed form in steps of dt (h = 1): a = wdt^2 (XEQ - x), from rest + x = 0.0_wp + v = 0.0_wp + a = wdt*wdt*XEQ + err = 0.0_wp + DO k = 1, NSTEP + xp = x + v + 0.25_wp*a + a1 = wdt*wdt*(XEQ - xp)/(1.0_wp + 0.25_wp*wdt*wdt) + x = xp + 0.25_wp*a1 + v = v + 0.5_wp*(a + a1) + a = a1 + rk = RESHAPE(rec(8:16, k), [3, 3]) + ang = ACOS(MAX(-1.0_wp, MIN(1.0_wp, 0.5_wp*(rk(1, 1) + rk(2, 2) + rk(3, 3) - 1.0_wp)))) + w = [rk(3, 2) - rk(2, 3), rk(1, 3) - rk(3, 1), rk(2, 1) - rk(1, 2)] + s = NORM2(w) + th = 0.0_wp + IF (s > 1.0e-12_wp) th = DOT_PRODUCT(w/s, dg)*ang*180.0_wp/PI + err = nan_max_abs([err, th - x]) + END DO + WRITE (*, '(A,F4.1,A,ES10.3,A)') 'coarse-step body roll (Omega dt = ', wdt, & + '): largest body roll error against the trapezoidal closed form ', err, ' deg' + CALL require(err <= 1.0e-4_wp, '11: the coarse-step torsional pendulum follows the march''s closed form') + END SUBROUTINE check_body_roll_large_step + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE yaw_deck(path, legs) + !! A buoyant Rigid6 body (no yaw restoring) held down by a vertical twisted finite-EI cable + !! clamped at its keel and at an anchor below, both ends restrained in torsion with 90 deg + !! of pretwist at the anchor, TMax 0, bodyIC static; legs > 0 adds two vertical EI = 0 legs + !! at +-legs m from the keel (yaw stiffness ~ 2 T legs**2 / L). + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(IN) :: legs + CHARACTER(64) :: pts(6) + CHARACTER(16) :: lns(3) + CHARACTER(24) :: secs(3) + INTEGER :: np + pts(1) = '1 Body1 0.0 0.0 -2.0 0 0 0 0' + pts(2) = '2 Fixed 0.0 0.0 -100.0 0 0 0 0' + lns(1) = '1 1 2 -' + secs(1) = '1 cab 77.97 40' + np = 2 + IF (legs > 0.0_wp) THEN + WRITE (pts(3), '(A,F6.2,A)') '3 Body1 ', legs, ' 0.0 -2.0 0 0 0 0' + WRITE (pts(4), '(A,F6.2,A)') '4 Body1 ', -legs, ' 0.0 -2.0 0 0 0 0' + WRITE (pts(5), '(A,F6.2,A)') '5 Fixed ', legs, ' 0.0 -100.0 0 0 0 0' + WRITE (pts(6), '(A,F6.2,A)') '6 Fixed ', -legs, ' 0.0 -100.0 0 0 0 0' + lns(2) = '2 3 5 -' + lns(3) = '3 4 6 -' + secs(2) = '2 poly 77.9 20' + secs(3) = '3 poly 77.9 20' + np = 6 + END IF + CALL write_deck(path, [CHARACTER(96) :: 'poly 0.12 15.0 5.0e7 -1.0 0.0 1.0e6 1.0 1.0 1.0 1.2 0.2 1.0 0.0', & + 'cab 0.2 60.0 5.0e8 -1.0 2.0e4 1.0e8 1.0e4 1.0 1.0 1.2 0.0 1.0 0.0'], & + [CHARACTER(96) :: '1 Rigid6 0 0 -20 0 0 0 2.0e4 40.0 0.0 2.0e6 2.0e6 8.0 0.5 3.5e4 3.5e4 3.5e4'], & + pts(1:np), lns(1:np/2), secs(1:np/2), & + [CHARACTER(96) :: '1 A Rigid 0 0 -1 Rigid 1 0 0 0', '1 B Rigid 0 0 -1 Rigid 1 0 0 90'], & + [CHARACTER(24) :: '100.0 WtrDpth', '1.0e5 kBot', '1.0e4 cBot', '0.05 dtM', '0.0 TMax', & + 'static bodyIC'], & + [CHARACTER(12) :: 'Body1Rz', 'Body1Mx', 'Body1My', 'Body1Mz', 'Torq1N1', 'Twist1']) + END SUBROUTINE yaw_deck + + SUBROUTINE check_body_yaw_statics() + !! 9. Static body/cable torsion coupling for any ratio of the body's own restoring to GJ/L: + !! (a) no yaw restoring at all (0 << GJ/L): the body yaws until the cable carries no torque; + !! (b) two legs whose yaw stiffness is of the order of GJ/L: the twist is shared. In both the + !! passes converge, Twist1 = 90 deg + body yaw (a straight vertical line: the yaw turns the + !! End A frame about the line), and the static net moment on the body vanishes to 1e-8 of + !! the untwisted torque GJ (pi/2) / L. + REAL(wp), PARAMETER :: M_REF = 1.0e4_wp*0.5_wp*PI/78.0_wp + REAL(wp) :: v(7), share + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: icase + DO icase = 1, 2 + IF (icase == 1) THEN + CALL yaw_deck('tdeck_yaw.dat', 0.0_wp) + ELSE + CALL yaw_deck('tdeck_yaw.dat', 0.3_wp) + END IF + CALL run_deck('tdeck_yaw.dat', 'tdeck_yaw', ok, em) + CALL require(ok, '9: the yawing-body deck runs: '//TRIM(em)) + IF (.NOT. ok) CYCLE + CALL read_row('tdeck_yaw.out', 0, v, ok) + CALL require(ok, '9: output readable') + share = v(7)/90.0_wp + WRITE (*, '(A,I0,A,F10.5,A,F10.5,A,ES10.3,A,ES11.3,A,F7.4)') 'body yaw statics (', icase, '): yaw ', v(2), & + ' deg, Twist1 ', v(7), ' deg, |net moment| ', NORM2(v(3:5))/M_REF, ' of GJ Phi/L, Torq ', v(6), & + ', cable share of the twist ', share + CALL require(ABS(v(7) - (90.0_wp + v(2))) <= 1.0e-5_wp, '9: Twist1 = 90 deg + body yaw') + CALL require(NORM2(v(3:5)) <= 1.0e-8_wp*M_REF, '9: static net moment on the body vanishes (1e-8)') + IF (icase == 1) THEN + CALL require(ABS(v(6)) <= 1.0e-8_wp*M_REF, '9a: without yaw restoring the cable ends untwisted') + ELSE + CALL require(share > 0.1_wp .AND. share < 0.9_wp, '9b: legs and cable share the twist (comparable stiffness)') + END IF + END DO + END SUBROUTINE check_body_yaw_statics + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE expect_error(label, path, root, needle) + CHARACTER(*), INTENT(IN) :: label, path, root, needle + LOGICAL :: ok + CHARACTER(512) :: em + CALL run_deck(path, root, ok, em) + CALL require(.NOT. ok .AND. INDEX(em, needle) > 0, label//' (got: '//TRIM(em)//')') + END SUBROUTINE expect_error + + SUBROUTINE check_one_end_two_moving() + !! One restrained end is ignored before the torsion rules: on a line with two moving ends + !! (Coupled to Free, dynamic) the one-end row gets exactly the verdict of the six-column rows, + !! not the torsion rule "two moving ends". + LOGICAL :: ok0, ok1 + CHARACTER(512) :: em0, em1 + CHARACTER(48), PARAMETER :: PTS(2) = [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', & + '2 Free 100.0 0.0 -50.0 100 0 0 0'] + CALL write_deck('tdeck_om0.dat', [neutral_type], no_rows, PTS, [CHARACTER(16) :: '1 1 2 -'], & + [CHARACTER(24) :: '1 cab 100.0 40'], [CHARACTER(96) :: '1 A Pinned 1 0 0', '1 B Pinned 1 0 0'], & + [CHARACTER(24) :: '0.1 dtM', '1.0 TMax'], [CHARACTER(16) :: 'FairTen1']) + CALL write_deck('tdeck_om1.dat', [neutral_type], no_rows, PTS, [CHARACTER(16) :: '1 1 2 -'], & + [CHARACTER(24) :: '1 cab 100.0 40'], & + [CHARACTER(96) :: '1 A Pinned 1 0 0 Free 0 0 1 0', '1 B Pinned 1 0 0 Rigid 0 0 1 0'], & + [CHARACTER(24) :: '0.1 dtM', '1.0 TMax'], [CHARACTER(16) :: 'FairTen1']) + CALL run_deck('tdeck_om0.dat', 'tdeck_om0', ok0, em0) + CALL run_deck('tdeck_om1.dat', 'tdeck_om1', ok1, em1) + CALL require((ok0 .EQV. ok1) .AND. TRIM(em0) == TRIM(em1) .AND. INDEX(em1, 'two moving ends') == 0, & + 'one restrained end on a line with two moving ends: the six-column verdict (got: '//TRIM(em1)//')') + END SUBROUTINE check_one_end_two_moving + + SUBROUTINE roll_error_deck(path, roll0, every_row) + !! The roll deck with a motionFile whose roll column starts at roll0 deg and, unless + !! every_row, is missing on the last row. + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(IN) :: roll0 + LOGICAL, INTENT(IN) :: every_row + INTEGER :: u, k + OPEN (NEWUNIT=u, FILE=path//'.motion', STATUS='REPLACE', ACTION='WRITE') + DO k = 0, 10 + IF (k == 10 .AND. .NOT. every_row) THEN + WRITE (u, '(F6.2,A)') 0.05_wp*k, ' 1 0.0 0.0 -50.0 0 0 0 0 0 0' + ELSE + WRITE (u, '(F6.2,A,F6.1)') 0.05_wp*k, ' 1 0.0 0.0 -50.0 0 0 0 0 0 0 ', MERGE(roll0, 10.0_wp, k == 0) + END IF + END DO + CLOSE (u) + CALL roll_deck(path, path//'.motion motionFile', 'Coupled') + END SUBROUTINE roll_error_deck + + SUBROUTINE check_errors() + CHARACTER(16), PARAMETER :: OUTS(1) = [CHARACTER(16) :: 'Torq1N1'] + CHARACTER(160) :: t10(1), tei0(1) + TYPE(CD_DeckAggregateType) :: agg + TYPE(CD_HFMF_ModuleType) :: cab + INTEGER :: es, node + CHARACTER(512) :: em + CALL straight_deck('tdeck_e1.dat', 'Rigid 1 0 0 Rigid 0 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a 9-column row is named', 'tdeck_e1.dat', 'tdeck_e1', 'or 10 or 11 with the torsion columns') + CALL straight_deck('tdeck_e2.dat', 'Rigid 1 0 0 Stiff 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a bad TorsStiffness token is named', 'tdeck_e2.dat', 'tdeck_e2', 'torsional stiffness') + CALL straight_deck('tdeck_e3.dat', 'Rigid 1 0 0 -1.0 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a negative torsional stiffness is named', 'tdeck_e3.dat', 'tdeck_e3', 'torsional stiffness') + CALL straight_deck('tdeck_e4.dat', 'Rigid 1 0 0 Rigid 0 0 0 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a null reference normal is named', 'tdeck_e4.dat', 'tdeck_e4', 'must be non-zero') + CALL straight_deck('tdeck_e5.dat', 'Rigid 1 0 0 Rigid 2 0 0 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a normal along Ez is named', 'tdeck_e5.dat', 'tdeck_e5', 'must not be parallel') + CALL straight_deck('tdeck_e6.dat', 'Rigid 1 0 0 Rigid 0 NaN 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS) + CALL expect_error('a non-finite normal is named', 'tdeck_e6.dat', 'tdeck_e6', 'is not a finite number') + ! a twist far beyond the buckling onset: the failed stage is named once, with its line + t10(1) = 'cab 0.2 32.2013246 1.0e7 0.0 1.0e6 1.0e8 5.0e4 1.0 1.0 0.0 0.0 0.0 0.0' + CALL straight_deck('tdeck_e15.dat', 'Rigid 1 0 0 Rigid 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 36000', OUTS, t10) + CALL expect_error('a failed torsion stage is named without a doubled prefix', 'tdeck_e15.dat', 'tdeck_e15', & + 'CableDyn_DeckDriver: line 1: finite-EI torsion static solve failed') + CALL check_one_end_two_moving() + t10(1) = 'cab 0.2 32.2 1.0e9 0.0 1.0e6 0.0 0.0 0.0 0.0TEN' + CALL straight_deck('tdeck_e7.dat', 'Rigid 1 0 0 Rigid 0 0 1 0', 'Rigid 1 0 0 Rigid 0 0 1 720', OUTS, t10) + CALL expect_error('a torsional line without GJ is named', 'tdeck_e7.dat', 'tdeck_e7', 'must give an explicit GJ') + tei0(1) = 'cab 0.2 60.0 1.0e9 0.0 0.0 0.0 0.0 0.0 0.0TEN' + CALL straight_deck('tdeck_e8.dat', 'Pinned 1 0 0 Rigid 0 0 1 0', 'Pinned 1 0 0 Free 0 0 1 0', & + [CHARACTER(16) :: 'FairTen1'], tei0) + CALL expect_error('torsion on an EI = 0 line is named', 'tdeck_e8.dat', 'tdeck_e8', 'require a finite-EI line') + CALL straight_deck('tdeck_e9.dat', 'Rigid 1 0 0 Rigid 0 0 1 0', 'Rigid 1 0 0 Free 0 0 1 720', OUTS) + CALL expect_error('a torque channel on a line without torsion is named', 'tdeck_e9.dat', 'tdeck_e9', & + 'carries no torque') + CALL write_deck('tdeck_e10.dat', [neutral_type], no_rows, & + [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0'], & + [CHARACTER(16) :: '1 1 2 -'], [CHARACTER(24) :: '1 cab 100.0 40'], & + [CHARACTER(96) :: '1 A Rigid 1 0 0 Rigid 0 0 1 0', '1 B Rigid 1 0 0 Rigid 0 0 1 90'], & + [CHARACTER(32) :: '0.1 dtM', '1.0 TMax', 'False alpha_force_blend'], OUTS) + CALL expect_error('the configuration blend is refused with torsion', 'tdeck_e10.dat', 'tdeck_e10', & + 'force-blended generalised-alpha') + CALL roll_error_deck('tdeck_e13.dat', 5.0_wp, .TRUE.) + CALL expect_error('a roll not starting from 0 is named', 'tdeck_e13.dat', 'tdeck_e13', 'must be 0 at t = 0') + CALL roll_error_deck('tdeck_e14.dat', 0.0_wp, .FALSE.) + CALL expect_error('a roll column on some rows only is named', 'tdeck_e14.dat', 'tdeck_e14', 'on some rows only') + CALL write_deck('tdeck_e11.dat', [neutral_type], no_rows, & + [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0'], & + [CHARACTER(16) :: '1 1 2 -'], [CHARACTER(24) :: '1 cab 100.0 40'], & + [CHARACTER(96) :: '1 A Rigid 1 0 0 Rigid 0 0 1 0', '1 B Rigid 1 0 0 Rigid 0 0 1 90'], & + [CHARACTER(24) :: '0.1 dtM', '0.0 TMax', '3 nModes'], OUTS) + CALL expect_error('modal analysis with torsion is refused', 'tdeck_e11.dat', 'tdeck_e11', 'modal analysis') + ! coupled entries + CALL write_deck('tdeck_e12.dat', [neutral_type], no_rows, & + [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0'], & + [CHARACTER(16) :: '1 1 2 -'], [CHARACTER(24) :: '1 cab 100.0 40'], & + [CHARACTER(96) :: '1 A Rigid 1 0 0 Rigid 0 0 1 0', '1 B Rigid 1 0 0 Rigid 0 0 1 90'], & + [CHARACTER(24) :: '0.1 dtM'], [CHARACTER(16) :: 'FairTen1']) + CALL CD_Init_Deck_Aggregate('tdeck_e12.dat', 0.1_wp, agg, es, em) + CALL require(es /= CD_DECKDRV_OK .AND. INDEX(em, 'nor in coupled OpenFAST or FAST.Farm runs') > 0, & + 'the coupled aggregate refuses torsion by name (got: '//TRIM(em)//')') + CALL CD_End_Deck_Aggregate(agg) + CALL CD_Init_Deck_HermiteCable('tdeck_e12.dat', 0.1_wp, cab, node, es, em) + CALL require(es /= CD_DECKDRV_OK .AND. INDEX(em, 'not yet supported in coupled OpenFAST runs') > 0, & + 'the coupled single-cable entry refuses torsion by name (got: '//TRIM(em)//')') + CALL CD_HFMF_End(cab) + CALL check_scope_refusals() + END SUBROUTINE check_errors + + SUBROUTINE write_lines(path, rows) + CHARACTER(*), INTENT(IN) :: path, rows(:) + INTEGER :: u, i + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE') + DO i = 1, SIZE(rows) + WRITE (u, '(A)') TRIM(rows(i)) + END DO + CLOSE (u) + END SUBROUTINE write_lines + + SUBROUTINE check_scope_refusals() + !! 6 (continued). Torsion outside its scope stops by name: End A on a rod end, End A on a + !! Point3 body, and a torsional line carrying ATTACHMENTS. + CHARACTER(120) :: tail(9), ty(4) + ty(1) = '--- LINE TYPES ---' + ty(2) = 'Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat' + ty(3) = '(-) (m) (kg/m) (N) (-) (Nm2) (N) (Nm2) (kgm) (kgm) (-) (-) (-) (-)' + ty(4) = 'cab 0.2 60.0 1.0e9 0.0 1.0e5 1.0e8 1.0e4 1.0 1.0 1.2 0.0 1.0 0.0' + tail(1) = '--- SECTIONS ---' + tail(2) = 'LineID LineType Length NumSegs' + tail(3) = '(-) (-) (m) (-)' + tail(4) = '1 cab 100.0 40' + tail(5) = '--- OPTIONS ---' + tail(6) = '0.1 dtM' + tail(7) = '1.0 TMax' + tail(8) = '--- OUTPUTS ---' + tail(9) = '--- END ---' + ! End A on a rod end + CALL write_lines('tdeck_s1.dat', [CHARACTER(120) :: 'torsion scope', ty, '--- ROD TYPES ---', & + 'Name Diam Mass Cd Ca CdEnd CaEnd', '(-) (m) (kg/m) (-) (-) (-) (-)', & + 'arm 0.6 400.0 1.0 1.0 0.0 0.0', '--- RODS ---', & + 'ID RodType Attachment XA YA ZA XB YB ZB NumSegs Outputs', & + '(-) (-) (-) (m) (m) (m) (m) (m) (m) (-) (-)', '1 arm Fixed 0 0 -40 0 0 -50 4 -', & + '--- POINTS ---', 'ID Type X Y Z Mass Vol CdA Ca', '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)', & + '2 Fixed 100.0 0.0 -50.0 0 0 0 0', '--- LINES ---', 'ID NodeA NodeB Outputs', '(-) (-) (-) (-)', & + '1 R1B 2 -', tail(1:4), '--- END CONNECTIONS ---', & + 'LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist', & + '(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)', '1 A Rigid 0 0 -1 Rigid 1 0 0 0', & + '1 B Rigid 1 0 0 Rigid 0 0 1 0', tail(5:9)]) + CALL expect_error('a torsional end on a rod is named', 'tdeck_s1.dat', 'tdeck_s1', 'on a rod end is not supported') + ! End A on a Point3 body + CALL write_lines('tdeck_s2.dat', [CHARACTER(120) :: 'torsion scope', ty, '--- BODIES ---', & + 'ID Type X Y Z Roll Pitch Yaw Mass Vol C33 C44 C55 CdA Ca', & + '(-) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m3) (N/m) (Nm) (Nm) (m2) (-)', & + '1 Point3 0 0 -50 0 0 0 1000.0 1.0 0 0 0 0 0', & + '--- POINTS ---', 'ID Type X Y Z Mass Vol CdA Ca', '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)', & + '1 Body1 0 0 0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0', '--- LINES ---', & + 'ID NodeA NodeB Outputs', '(-) (-) (-) (-)', '1 1 2 -', tail(1:4), '--- END CONNECTIONS ---', & + 'LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist', & + '(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)', '1 A Pinned 1 0 0 Rigid 0 0 1 0', & + '1 B Rigid 1 0 0 Rigid 0 0 1 0', tail(5:9)]) + CALL expect_error('a torsional end on a Point3 body is named', 'tdeck_s2.dat', 'tdeck_s2', 'needs a Rigid6 body') + ! a torsional line with ATTACHMENTS + CALL write_lines('tdeck_s3.dat', [CHARACTER(120) :: 'torsion scope', ty, '--- POINTS ---', & + 'ID Type X Y Z Mass Vol CdA Ca', '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)', & + '1 Coupled 0.0 0.0 -50.0 0 0 0 0', '2 Fixed 100.0 0.0 -50.0 0 0 0 0', '--- LINES ---', & + 'ID NodeA NodeB Outputs', '(-) (-) (-) (-)', '1 1 2 -', tail(1:4), '--- ATTACHMENTS ---', & + 'LineID ArcLength Mass Volume CdA Ca', '(-) (m) (kg) (m3) (m2) (-)', '1 50.0 100.0 0.1 0.1 1.0', & + '--- END CONNECTIONS ---', 'LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist', & + '(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)', '1 A Rigid 1 0 0 Rigid 0 0 1 0', & + '1 B Rigid 1 0 0 Rigid 0 0 1 0', tail(5:9)]) + CALL expect_error('a torsional line with ATTACHMENTS is named', 'tdeck_s3.dat', 'tdeck_s3', & + 'not combined with ATTACHMENTS') + END SUBROUTINE check_scope_refusals + +END PROGRAM test_torsion_deck diff --git a/tests/test_torsion_orcaflex.f90 b/tests/test_torsion_orcaflex.f90 new file mode 100644 index 0000000..059b4de --- /dev/null +++ b/tests/test_torsion_orcaflex.f90 @@ -0,0 +1,491 @@ +! File: tests/test_torsion_orcaflex.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +!> Torsion compared with OrcaFlex 11.6d summary values (tests/data/torsion_orcaflex_refs.txt, +!> regenerated by validation/scripts/orcaflex_torsion_reference.py): +!> a. pure torsion of a straight clamped line through the deck (L 10 m, GJ 10 kN.m^2): the +!> torque at End A and End B for gamma_B = 90, 450, 1800 and -90 deg, gamma_A 30 / gamma_B +!> 120 deg (only the difference matters), and two sections in series (GJ 10 / 25 kN.m^2); +!> b. Greenhill onset of the clamped-clamped line at T L^2/EI = 0, 10, 50 and 200, and of +!> bending-pinned, torsion-restrained ends at 0 and 10 (the semi-tangential end), N = 32, +!> against OrcaFlex extrapolated to zero segment length; +!> c. the Lozon et al. (2025) Gulf of Mexico 80 m lazy-wave cable with an illustrative +!> GJ = 50 kN.m^2, clamped at both ends in its untwisted static orientation and twisted +!> 0 to 5 turns at the hang-off through the deck Pretwist: hang-off torque, geometric twist +!> Theta = Phi - M L / GJ, peak interior curvature, out-of-plane offset and hang-off tension; +!> at zero twist the deck also reproduces the same cable without torsion columns. +!> Argument 1: tests/data/torsion_orcaflex_refs.txt. +PROGRAM test_torsion_orcaflex + USE CableDyn_Precision, ONLY: wp + USE CableDyn_DeckDriver, ONLY: CD_Run_Deck_Driver, CD_DECKDRV_OK + USE CableDyn_HermiteCableStatic, ONLY: CD_HermiteCable_Static_Solve, CD_HCSTAT_OK + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType + USE CableDyn_EndConnection, ONLY: CD_ENDCONN_PINNED, CD_ENDCONN_RIGID + USE, INTRINSIC :: IEEE_ARITHMETIC, ONLY: IEEE_IS_FINITE + IMPLICIT NONE + + REAL(wp), PARAMETER :: PI = 3.14159265358979323846_wp, RHOW = 1025.0_wp + INTEGER :: n_fail = 0, npure = 0, nclamp = 0, npin = 0, nlw = 0 + REAL(wp) :: pure(3, 8), series_ref = 0.0_wp, clamp(3, 8), pin(3, 4), orient(2) = 0.0_wp, lw(6, 8) + CHARACTER(512) :: ref_path + + IF (COMMAND_ARGUMENT_COUNT() < 1) THEN + WRITE (*, '(A)') 'usage: test_torsion_orcaflex ' + ERROR STOP 2 + END IF + CALL GET_COMMAND_ARGUMENT(1, ref_path) + CALL read_refs() + + CALL check_pure() + CALL check_onsets() + CALL check_lazy_wave() + + IF (n_fail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', n_fail, ' torsion OrcaFlex comparison(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: torsion agrees with the OrcaFlex 11.6d summary values' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(cond, msg) + LOGICAL, INTENT(IN) :: cond + CHARACTER(*), INTENT(IN) :: msg + IF (.NOT. cond) THEN + n_fail = n_fail + 1 + WRITE (*, '(A,A)') 'FAILED: ', msg + END IF + END SUBROUTINE require + + SUBROUTINE read_refs() + INTEGER :: u, ios + CHARACTER(512) :: line + CHARACTER(16) :: tag + REAL(wp) :: v(6) + OPEN (NEWUNIT=u, FILE=ref_path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) THEN + WRITE (*, '(A)') 'cannot open '//TRIM(ref_path) + ERROR STOP 2 + END IF + DO + READ (u, '(A)', IOSTAT=ios) line + IF (ios /= 0) EXIT + IF (LEN_TRIM(line) == 0 .OR. line(1:1) == '#') CYCLE + READ (line, *, IOSTAT=ios) tag + IF (ios /= 0) CYCLE + v = 0.0_wp + SELECT CASE (TRIM(tag)) + CASE ('pure') + READ (line, *, IOSTAT=ios) tag, v(1:3) + IF (ios == 0 .AND. npure < SIZE(pure, 2)) THEN + npure = npure + 1 + pure(:, npure) = v(1:3) + END IF + CASE ('series') + READ (line, *, IOSTAT=ios) tag, series_ref + CASE ('clamped') + READ (line, *, IOSTAT=ios) tag, v(1:3) + IF (ios == 0 .AND. nclamp < SIZE(clamp, 2)) THEN + nclamp = nclamp + 1 + clamp(:, nclamp) = v(1:3) + END IF + CASE ('pinned') + READ (line, *, IOSTAT=ios) tag, v(1:3) + IF (ios == 0 .AND. npin < SIZE(pin, 2)) THEN + npin = npin + 1 + pin(:, npin) = v(1:3) + END IF + CASE ('orientation') + READ (line, *, IOSTAT=ios) tag, orient + CASE ('lazywave') + READ (line, *, IOSTAT=ios) tag, v + IF (ios == 0 .AND. nlw < SIZE(lw, 2)) THEN + nlw = nlw + 1 + lw(:, nlw) = v + END IF + END SELECT + END DO + CLOSE (u) + IF (npure < 5 .OR. nclamp < 4 .OR. npin < 2 .OR. nlw < 6 .OR. series_ref <= 0.0_wp .OR. orient(1) <= 0.0_wp) THEN + WRITE (*, '(A)') 'reference file incomplete: '//TRIM(ref_path) + ERROR STOP 2 + END IF + END SUBROUTINE read_refs + + SUBROUTINE write_deck(path, types, points, sections, endconns, options, outs) + !! A one-line deck: line 1 from point 1 (End A) to point 2 (End B). + CHARACTER(*), INTENT(IN) :: path, types(:), points(:), sections(:), endconns(:), options(:), outs(:) + INTEGER :: u, i + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE') + WRITE (u, '(A)') 'torsion OrcaFlex comparison deck' + WRITE (u, '(A)') '--- LINE TYPES ---' + WRITE (u, '(A)') 'Name Diam Mass EA BA EI GAs GJ Irt Irn Cdn Cdt Can Cat' + WRITE (u, '(A)') '(-) (m) (kg/m) (N) (-) (Nm2) (N) (Nm2) (kgm) (kgm) (-) (-) (-) (-)' + DO i = 1, SIZE(types) + WRITE (u, '(A)') TRIM(types(i)) + END DO + WRITE (u, '(A)') '--- POINTS ---' + WRITE (u, '(A)') 'ID Type X Y Z Mass Vol CdA Ca' + WRITE (u, '(A)') '(-) (-) (m) (m) (m) (kg) (m3) (m2) (-)' + DO i = 1, SIZE(points) + WRITE (u, '(A)') TRIM(points(i)) + END DO + WRITE (u, '(A)') '--- LINES ---' + WRITE (u, '(A)') 'ID NodeA NodeB Outputs' + WRITE (u, '(A)') '(-) (-) (-) (-)' + WRITE (u, '(A)') '1 1 2 -' + WRITE (u, '(A)') '--- SECTIONS ---' + WRITE (u, '(A)') 'LineID LineType Length NumSegs' + WRITE (u, '(A)') '(-) (-) (m) (-)' + DO i = 1, SIZE(sections) + WRITE (u, '(A)') TRIM(sections(i)) + END DO + WRITE (u, '(A)') '--- END CONNECTIONS ---' + WRITE (u, '(A)') 'LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist' + WRITE (u, '(A)') '(-) (-) (N-m/rad) (-) (-) (-) (N-m/rad) (-) (-) (-) (deg)' + DO i = 1, SIZE(endconns) + WRITE (u, '(A)') TRIM(endconns(i)) + END DO + WRITE (u, '(A)') '--- OPTIONS ---' + WRITE (u, '(A)') '9.80665 g' + WRITE (u, '(A)') '1025.0 rhoW' + DO i = 1, SIZE(options) + WRITE (u, '(A)') TRIM(options(i)) + END DO + WRITE (u, '(A)') '--- OUTPUTS ---' + DO i = 1, SIZE(outs) + WRITE (u, '(A)') TRIM(outs(i)) + END DO + WRITE (u, '(A)') '--- END ---' + CLOSE (u) + END SUBROUTINE write_deck + + SUBROUTINE run_deck(path, root, ok, em_out) + CHARACTER(*), INTENT(IN) :: path, root + LOGICAL, INTENT(OUT) :: ok + CHARACTER(*), INTENT(OUT) :: em_out + LOGICAL :: conv + INTEGER :: es + CALL CD_Run_Deck_Driver(path, root, conv, es, em_out) + ok = es == CD_DECKDRV_OK .AND. conv + END SUBROUTINE run_deck + + SUBROUTINE read_row(path, vals, ok) + !! The first data row of a driver .out file. + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(OUT) :: vals(:) + LOGICAL, INTENT(OUT) :: ok + INTEGER :: u, ios + CHARACTER(2048) :: buf + ok = .FALSE. + vals = 0.0_wp + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) RETURN + READ (u, '(A)', IOSTAT=ios) buf + READ (u, '(A)', IOSTAT=ios) buf + READ (u, *, IOSTAT=ios) vals + CLOSE (u) + ok = ios == 0 .AND. ALL(IEEE_IS_FINITE(vals)) + END SUBROUTINE read_row + + SUBROUTINE read_profile(path, prof, n, ok) + !! The rows of a .static.out file: LineID Node ArcLength X Y Z Tension Curvature BendMoment + !! Declination Inclination Azimuth (prof(1:12, 1:n)). + CHARACTER(*), INTENT(IN) :: path + REAL(wp), ALLOCATABLE, INTENT(OUT) :: prof(:, :) + INTEGER, INTENT(OUT) :: n + LOGICAL, INTENT(OUT) :: ok + INTEGER :: u, ios + REAL(wp) :: row(12) + CHARACTER(2048) :: buf + ALLOCATE (prof(12, 4096)) + n = 0 + ok = .FALSE. + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) RETURN + READ (u, '(A)', IOSTAT=ios) buf + READ (u, '(A)', IOSTAT=ios) buf + READ (u, '(A)', IOSTAT=ios) buf + DO + READ (u, *, IOSTAT=ios) row + IF (ios /= 0) EXIT + IF (n >= SIZE(prof, 2)) EXIT + n = n + 1 + prof(:, n) = row + END DO + CLOSE (u) + ok = n > 0 .AND. ALL(IEEE_IS_FINITE(prof(:, 1:n))) + END SUBROUTINE read_profile + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_pure() + !! a. Straight neutral 0.1 m line along +x, End A Coupled at x = 0, End B Fixed at x = 10, + !! 20 elements, EA 1e8 N, GJ 1e4 N.m^2, both ends clamped and restrained in torsion with + !! the pretwists gamma_A and gamma_B. Output precision is 8 digits. OrcaFlex's EI of + !! 1 kN.m^2 puts every case but -90/90 deg above the Greenhill onset (0.90 kN.m): OrcaFlex + !! statics stays on the straight branch there, whereas CableDyn's stability check leaves + !! it for the buckled shape. The torque of the straight branch does not depend on EI, so + !! the deck uses EI = 1e6 N.m^2, which keeps the straight line stable up to five turns. + CHARACTER(160) :: t(2), ec(2) + CHARACTER(24) :: secs(2) + REAL(wp) :: v(3), mass, err + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: k, ns + mass = RHOW*0.25_wp*PI*0.01_wp + WRITE (t(1), '(A,ES24.16,A)') 'rodA 0.1 ', mass, ' 1.0e8 0.0 1.0e6 1.0e8 1.0e4 1.0 1.0 0 0 0 0' + WRITE (t(2), '(A,ES24.16,A)') 'rodB 0.1 ', mass, ' 1.0e8 0.0 1.0e6 1.0e8 2.5e4 1.0 1.0 0 0 0 0' + DO k = 1, npure + 1 + IF (k <= npure) THEN + WRITE (ec(1), '(A,F10.3)') '1 A Rigid 1 0 0 Rigid 0 0 1 ', pure(1, k) + WRITE (ec(2), '(A,F10.3)') '1 B Rigid 1 0 0 Rigid 0 0 1 ', pure(2, k) + secs(1) = '1 rodA 10.0 20' + ns = 1 + ELSE + ec(1) = '1 A Rigid 1 0 0 Rigid 0 0 1 0' + ec(2) = '1 B Rigid 1 0 0 Rigid 0 0 1 90' + secs(1) = '1 rodA 4.0 8' + secs(2) = '1 rodB 6.0 12' + ns = 2 + END IF + CALL write_deck('tofx_pure.dat', t, [CHARACTER(48) :: '1 Coupled 0.0 0.0 -50.0 0 0 0 0', & + '2 Fixed 10.0 0.0 -50.0 0 0 0 0'], secs(1:ns), ec, & + [CHARACTER(24) :: '0.1 dtM', '0.0 TMax'], [CHARACTER(16) :: 'Torq1N1', 'Torq1N21']) + CALL run_deck('tofx_pure.dat', 'tofx_pure', ok, em) + CALL require(ok, 'a: pure torsion deck runs: '//TRIM(em)) + IF (.NOT. ok) CYCLE + CALL read_row('tofx_pure.out', v, ok) + CALL require(ok, 'a: output readable') + IF (k <= npure) THEN + err = nan_max_abs(v(2:3) - pure(3, k))/ABS(pure(3, k)) + WRITE (*, '(A,2F8.1,A,2ES16.8,A,ES16.8,A,ES9.2)') 'a: gamma_A, gamma_B ', pure(1:2, k), ' deg: Torq ', & + v(2:3), ' OrcaFlex ', pure(3, k), ' rel. ', err + ELSE + err = nan_max_abs(v(2:3) - series_ref)/series_ref + WRITE (*, '(A,2ES16.8,A,ES16.8,A,ES9.2)') 'a: two sections in series: Torq ', v(2:3), ' OrcaFlex ', & + series_ref, ' rel. ', err + END IF + CALL require(err <= 1.0e-7_wp, 'a: torque equals OrcaFlex to output precision') + END DO + END SUBROUTINE check_pure + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_onsets() + !! b. EI = L = GJ = 1, EA = 1e7 (EA L^2/EI of the OrcaFlex model), N = 32; onset bisected on + !! the stability report of the straight state (sliding End B under the dead tension). Compared with OrcaFlex + !! extrapolated to zero segment length (its 200-segment value is listed alongside). + REAL(wp) :: m, err, err200 + INTEGER :: k + DO k = 1, nclamp + m = onset(32, clamp(1, k), CD_ENDCONN_RIGID, clamp(3, k)) + err = (m - clamp(3, k))/clamp(3, k) + err200 = (clamp(2, k) - clamp(3, k))/clamp(3, k) + WRITE (*, '(A,F6.1,A,F12.7,A,F12.7,A,ES10.2,A,ES10.2,A)') 'b: clamped T L^2/EI = ', clamp(1, k), & + ': CableDyn (N = 32) ', m, ', OrcaFlex extrapolated ', clamp(3, k), ', rel. ', err, & + ' (OrcaFlex 200 segments ', err200, ')' + CALL require(m > 0.0_wp .AND. ABS(err) <= 2.0e-4_wp, 'b: clamped onset within 2e-4 of OrcaFlex extrapolated') + END DO + DO k = 1, npin + m = onset(32, pin(1, k), CD_ENDCONN_PINNED, pin(3, k)) + err = (m - pin(3, k))/pin(3, k) + err200 = (pin(2, k) - pin(3, k))/pin(3, k) + WRITE (*, '(A,F6.1,A,F12.7,A,F12.7,A,ES10.2,A,ES10.2,A)') 'b: pinned (semi-tangential) T L^2/EI = ', & + pin(1, k), ': CableDyn (N = 32) ', m, ', OrcaFlex extrapolated ', pin(3, k), ', rel. ', err, & + ' (OrcaFlex 200 segments ', err200, ')' + CALL require(m > 0.0_wp .AND. ABS(err) <= 2.0e-4_wp, 'b: pinned onset within 2e-4 of OrcaFlex extrapolated') + END DO + END SUBROUTINE check_onsets + + REAL(wp) FUNCTION onset(ne, tension, mode, mguess) RESULT(mcr) + INTEGER, INTENT(IN) :: ne, mode + REAL(wp), INTENT(IN) :: tension, mguess + REAL(wp) :: lo, hi, mid + INTEGER :: k + LOGICAL :: failed + lo = 0.9_wp*mguess + hi = 1.1_wp*mguess + mcr = -1.0_wp + IF (.NOT. stable_at(ne, tension, mode, lo, failed)) RETURN + IF (stable_at(ne, tension, mode, hi, failed)) RETURN + DO k = 1, 60 + mid = 0.5_wp*(lo + hi) + IF (stable_at(ne, tension, mode, mid, failed)) THEN + lo = mid + ELSE + hi = mid + END IF + IF (failed) RETURN + IF (hi - lo <= 1.0e-8_wp*mguess) EXIT + END DO + mcr = 0.5_wp*(lo + hi) + END FUNCTION onset + + LOGICAL FUNCTION stable_at(ne, tension, mode, m, failed) RESULT(stab) + INTEGER, INTENT(IN) :: ne, mode + REAL(wp), INTENT(IN) :: tension, m + LOGICAL, INTENT(OUT) :: failed + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: seed(:), l0(:), q(:), f(:), curv(:) + REAL(wp) :: res + INTEGER :: nd, it, es, k + CHARACTER(512) :: em + nd = 6*(ne + 1) + ALLOCATE (seed(nd), l0(ne), q(nd), f(nd), curv(ne + 1)) + l0 = 1.0_wp/ne + seed = 0.0_wp + DO k = 0, ne + seed(6*k + 1) = REAL(k, wp)/ne + seed(6*k + 4) = 1.0_wp + END DO + f = 0.0_wp + f(nd - 5) = tension + tors%active = .TRUE. + ALLOCATE (tors%gj(ne)) + tors%gj = 1.0_wp + tors%phi = m + tors%ends = RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, & + 0.0_wp, 0.0_wp, 1.0_wp], [3, 4]) + tors%descend = .FALSE. + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e7_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(0.0_wp, it=1, ne)], seed, & + [1, 2, 3, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-7_wp, 1.0_wp, q, & + curv, res, it, es, em, f_nodal=f, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[mode, mode], torsion=tors) + failed = es /= CD_HCSTAT_OK + IF (failed) WRITE (*, '(A)') ' solve failed: '//TRIM(em) + stab = tors%stable .AND. .NOT. failed + END FUNCTION stable_at + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE lazy_wave_deck(path, turns, with_torsion) + !! The Gulf of Mexico 80 m cable of the OrcaFlex case, hang-off End A (Coupled, held) and + !! anchor End B (Fixed), 0.375 m elements, both ends Rigid along the untwisted OrcaFlex end + !! orientation (End A to End B), reference normals +y, the twist at the hang-off. OrcaFlex + !! twists its End B (the hang-off) by gamma_B = +turns about its A->B tangent; here the + !! hang-off is End A and its Ez points the other way, so Pretwist(A) = -360 turns and + !! Phi = Pretwist(B) - Pretwist(A) = +360 turns: the same physical twist and torque sign. + CHARACTER(*), INTENT(IN) :: path + REAL(wp), INTENT(IN) :: turns + LOGICAL, INTENT(IN) :: with_torsion + REAL(wp) :: da, db + CHARACTER(200) :: t(2), ec(2) + da = orient(2)*PI/180.0_wp + db = orient(1)*PI/180.0_wp + ! bare 0.16 m, 36.7 kg/m; buoyancy section 0.29 m, 59.53 kg/m; EA 469 MN, EI 19.9 kN.m^2, + ! GJ 50 kN.m^2; GAs and the rotary inertias of the circular-section closure (not used here) + t(1) = 'bare 0.160 36.70 4.69e8 0.0 1.99e4 9.0192e7 5.0e4 0.058720 0.117440 1.2 0.1 1.0 0.0' + t(2) = 'buoy 0.290 59.53 4.69e8 0.0 1.99e4 9.0192e7 5.0e4 0.312904 0.625808 1.2 0.1 1.0 0.0' + IF (with_torsion) THEN + WRITE (ec(1), '(A,ES24.16,A,ES24.16,A,F10.2)') '1 A Rigid ', SIN(da), ' 0.0 ', -COS(da), ' Rigid 0 1 0 ', & + -360.0_wp*turns + WRITE (ec(2), '(A,ES24.16,A,ES24.16,A)') '1 B Rigid ', SIN(db), ' 0.0 ', -COS(db), ' Rigid 0 1 0 0' + ELSE + WRITE (ec(1), '(A,ES24.16,A,ES24.16)') '1 A Rigid ', SIN(da), ' 0.0 ', -COS(da) + WRITE (ec(2), '(A,ES24.16,A,ES24.16)') '1 B Rigid ', SIN(db), ' 0.0 ', -COS(db) + END IF + CALL write_deck(path, t, [CHARACTER(48) :: '1 Coupled 5.0 0.0 -14.0 0 0 0 0', '2 Fixed 125.0 0.0 -80.0 0 0 0 0'], & + [CHARACTER(24) :: '1 bare 68.114 182', '1 buoy 50.0 133', '1 bare 52.101 139'], ec, & + [CHARACTER(24) :: '80.0 WtrDpth', '1.0e5 kBot', '1.0e4 cBot', '0.05 dtM', '0.0 TMax'], & + [CHARACTER(16) :: 'FairTen1']) + IF (with_torsion) CALL add_outputs(path) + END SUBROUTINE lazy_wave_deck + + SUBROUTINE add_outputs(path) + !! Append the torsion channels to the OUTPUTS of a deck written by write_deck. + CHARACTER(*), INTENT(IN) :: path + CHARACTER(512) :: lines(200) + INTEGER :: u, n, ios, k + OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ') + n = 0 + DO + READ (u, '(A)', IOSTAT=ios) lines(n + 1) + IF (ios /= 0) EXIT + n = n + 1 + END DO + CLOSE (u) + OPEN (NEWUNIT=u, FILE=path, STATUS='REPLACE', ACTION='WRITE') + DO k = 1, n + IF (TRIM(lines(k)) == '--- END ---') THEN + WRITE (u, '(A)') 'Torq1N1' + WRITE (u, '(A)') 'Torq1N455' + WRITE (u, '(A)') 'Twist1' + END IF + WRITE (u, '(A)') TRIM(lines(k)) + END DO + CLOSE (u) + END SUBROUTINE add_outputs + + SUBROUTINE check_lazy_wave() + REAL(wp), ALLOCATABLE :: p0(:, :), p1(:, :) + REAL(wp) :: v(5), tq, th, kap, ymax, ten, etq, eth, ekap, ey, eten, dpos, dten, worst(5) + INTEGER :: k, n0, n1, jlo, jhi + LOGICAL :: ok + CHARACTER(512) :: em + CHARACTER(32) :: root + ! zero twist: the same cable without torsion columns (V3 on the lazy wave) + CALL lazy_wave_deck('tofx_lw_ref.dat', 0.0_wp, .FALSE.) + CALL run_deck('tofx_lw_ref.dat', 'tofx_lw_ref', ok, em) + CALL require(ok, 'c: the cable without torsion runs: '//TRIM(em)) + IF (.NOT. ok) RETURN + CALL read_profile('tofx_lw_ref.static.out', p0, n0, ok) + CALL require(ok .AND. n0 == 455, 'c: profile without torsion readable') + worst = 0.0_wp + DO k = 1, nlw + WRITE (root, '(A,I0)') 'tofx_lw', k + CALL lazy_wave_deck(TRIM(root)//'.dat', lw(1, k), .TRUE.) + CALL run_deck(TRIM(root)//'.dat', TRIM(root), ok, em) + CALL require(ok, 'c: twisted lazy-wave deck runs: '//TRIM(em)) + IF (.NOT. ok) CYCLE + CALL read_row(TRIM(root)//'.out', v, ok) + CALL read_profile(TRIM(root)//'.static.out', p1, n1, ok) + CALL require(ok .AND. n1 == 455, 'c: outputs readable') + IF (.NOT. ok) CYCLE + tq = v(3) + th = lw(1, k) - v(5)/360.0_wp + jlo = 1 + NINT(0.03_wp*454) + jhi = 1 + NINT(0.97_wp*454) + kap = MAXVAL(p1(8, jlo:jhi)) + ymax = nan_max_abs(p1(5, 1:n1)) + ten = p1(7, 1) + IF (lw(1, k) < 0.5_wp) THEN + etq = ABS(tq) + eth = ABS(th) + dpos = nan_max_abs(p1(4:6, 1:n1) - p0(4:6, 1:n0)) + dten = nan_max_abs(p1(7, 1:n1) - p0(7, 1:n0))/MAXVAL(p0(7, 1:n0)) + WRITE (*, '(A,ES10.3,A,ES10.3,A,ES10.3,A,ES10.3)') 'c: 0 turns: torque ', tq, ' N m, Theta ', th, & + ' turns; vs no torsion columns: position ', dpos, ' m, tension ', dten + CALL require(etq <= 1.0e-6_wp .AND. eth <= 1.0e-9_wp, 'c: no torque and no twist at zero pretwist') + CALL require(dpos <= 1.0e-6_wp .AND. dten <= 1.0e-6_wp, 'c: zero twist reproduces the cable without torsion') + ELSE + etq = ABS(tq/lw(2, k) - 1.0_wp) + eth = ABS(th - lw(3, k))/lw(1, k) + worst(1) = nan_max_abs([worst(1), etq]) + worst(2) = nan_max_abs([worst(2), eth]) + END IF + ekap = ABS(kap/lw(4, k) - 1.0_wp) + ey = ABS(ymax - lw(5, k)) + eten = ABS(ten/lw(6, k) - 1.0_wp) + worst(3) = nan_max_abs([worst(3), ekap]) + worst(4) = nan_max_abs([worst(4), ey]) + worst(5) = nan_max_abs([worst(5), eten]) + WRITE (*, '(A,F4.1,A,2F11.3,A,2F9.5,A,2F9.6,A,2F7.3,A,2F10.1)') 'c: turns ', lw(1, k), & + ': torque (CableDyn, OrcaFlex) ', tq, lw(2, k), ' N m; Theta ', th, lw(3, k), ' turns; peak curvature ', & + kap, lw(4, k), ' 1/m; max|Y| ', ymax, lw(5, k), ' m; hang-off tension ', ten, lw(6, k) + CALL require(ABS(v(3) - v(4)) <= 1.0e-6_wp*MAX(1.0_wp, ABS(v(3))), 'c: torque uniform (End A = End B)') + IF (lw(1, k) > 0.5_wp) THEN + CALL require(etq <= 1.0e-2_wp, 'c: hang-off torque within 1 % of OrcaFlex') + CALL require(eth <= 1.0e-3_wp, 'c: geometric twist Theta within 1e-3 turn per imposed turn') + END IF + CALL require(ekap <= 1.0e-2_wp, 'c: peak interior curvature within 1 % of OrcaFlex') + CALL require(ey <= 0.1_wp, 'c: out-of-plane offset within 0.1 m of OrcaFlex') + CALL require(eten <= 5.0e-3_wp, 'c: hang-off tension within 0.5 % of OrcaFlex') + END DO + WRITE (*, '(A,ES9.2,A,ES9.2,A,ES9.2,A,F6.3,A,ES9.2)') 'c: largest differences: torque ', worst(1), & + ', Theta per turn ', worst(2), ', peak curvature ', worst(3), ', max|Y| ', worst(4), ' m, hang-off tension ', & + worst(5) + END SUBROUTINE check_lazy_wave + +END PROGRAM test_torsion_orcaflex diff --git a/tests/test_torsion_validation.f90 b/tests/test_torsion_validation.f90 new file mode 100644 index 0000000..a8c2a39 --- /dev/null +++ b/tests/test_torsion_validation.f90 @@ -0,0 +1,727 @@ +! File: tests/test_torsion_validation.f90 +! SPDX-License-Identifier: Apache-2.0 +! Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +PROGRAM test_torsion_validation + !! Validation gates of condensed torsion on the cubic-Hermite cable that complement the kernel, + !! statics, dynamics and deck tests: + !! Z zero-twist parity (V3): a sagging line clamped at both ends with out-of-plane end + !! directions (a three-dimensional shape with a non-zero holonomy Theta_0), restrained in + !! torsion with Phi = Theta_0, reproduces the solve without torsion to solver tolerance + !! and carries no torque; + !! H Kirchhoff helix (V5): a = 1, pitch angle 30 deg, EI = 1, M_t = 0.5, four turns on 96 + !! elements, clamped on the exact helix with Phi = Theta_helix + M_t C: the line stays on + !! the helix and the end reaction is the wrench of the helical equilibrium, F = 0.058013 + !! along the axis and K = 0.899519 about it (van der Heijden et al. 2003, eqs. 1-5); + !! L localised (hockling) onset (V6, static form): clamped-clamped critical torque at + !! T L^2/EI = 400 and 1600 against eq. (33), approaching 2 sqrt(EI T) from above; + !! C cross-check against the Cosserat rod path (V7): pure torsion, and a rod clamped with + !! its far end displaced and rotated (bent and twisted), solved by both formulations: + !! shape and torque within 1 %; + !! S the rank-one (Sherman-Morrison) term of the bordered Newton step (WP3 review): near the + !! Greenhill onset and on the post-buckled branch, Newton with the bordered tangent + !! B + g g^T / C converges quadratically, while the same iteration without g g^T / C + !! converges slowly or not at all, and B alone is indefinite on the post-buckled branch. + !! Argument 1: tests/data/torsion_analytic_refs.txt. + USE CableDyn_Precision, ONLY: wp + USE CableDyn_HermiteCable, ONLY: CD_HermiteCable_Element + USE CableDyn_HermiteCableStatic, ONLY: CD_HermiteCable_Static_Solve, CD_HCSTAT_OK + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Line, CD_HermiteTorsion_Unwrap, & + CD_HermiteTorsion_Bordered_Solve, CD_HermiteTorsion_Inertia, CD_HTORS_OK, & + CD_HTORS_KBAND, CD_HTORS_PI + USE CableDyn_EndConnection, ONLY: CD_ENDCONN_RIGID + USE CableDyn_CosseratStatic, ONLY: CosseratSolverConfig, CD_Static_Cosserat_Solve, CD_COS_OK + USE CableDyn_Cosserat, ONLY: CD_Reference_Frame, CD_Cosserat_Strains + USE CableDyn_SO3, ONLY: CD_Exp_SO3 + IMPLICIT NONE + + REAL(wp), PARAMETER :: PI = CD_HTORS_PI + INTEGER, PARAMETER :: KB = CD_HTORS_KBAND, LDAB = 3*KB + 1 + INTEGER :: nfail = 0 + CHARACTER(512) :: ref_path + REAL(wp) :: ref_local(2, 2), ref_helix(6) + + IF (COMMAND_ARGUMENT_COUNT() < 1) THEN + WRITE (*, '(A)') 'usage: test_torsion_validation ' + ERROR STOP 2 + END IF + CALL GET_COMMAND_ARGUMENT(1, ref_path) + CALL read_refs() + + CALL check_zero_twist_parity() + CALL check_helix() + CALL check_localised_onset() + CALL check_cosserat() + CALL check_bordered_term() + + IF (nfail > 0) THEN + WRITE (*, '(A,I0,A)') 'FAIL: ', nfail, ' torsion validation assertion(s) failed' + ERROR STOP 1 + END IF + WRITE (*, '(A)') 'PASS: torsion validation (zero-twist parity, helix, localised onset, Cosserat, bordered step)' + +CONTAINS + + INCLUDE 'nan_max_abs.inc' + + SUBROUTINE require(ok, label) + LOGICAL, INTENT(IN) :: ok + CHARACTER(*), INTENT(IN) :: label + IF (.NOT. ok) THEN + nfail = nfail + 1 + WRITE (*, '(A)') 'FAIL: '//label + END IF + END SUBROUTINE require + + SUBROUTINE read_refs() + !! "localised T M_cr" (two rows, EI = L = 1) and "helix a alpha_deg B M_t F K". + INTEGER :: u, ios, nl, nh + CHARACTER(256) :: line + CHARACTER(16) :: tag + REAL(wp) :: v(6) + nl = 0 + nh = 0 + OPEN (NEWUNIT=u, FILE=ref_path, STATUS='OLD', ACTION='READ', IOSTAT=ios) + IF (ios /= 0) THEN + WRITE (*, '(A)') 'cannot open '//TRIM(ref_path) + ERROR STOP 2 + END IF + DO + READ (u, '(A)', IOSTAT=ios) line + IF (ios /= 0) EXIT + IF (LEN_TRIM(line) == 0 .OR. line(1:1) == '#') CYCLE + READ (line, *, IOSTAT=ios) tag + IF (ios /= 0) CYCLE + IF (TRIM(tag) == 'localised' .AND. nl < 2) THEN + READ (line, *, IOSTAT=ios) tag, v(1:2) + IF (ios /= 0) CYCLE + nl = nl + 1 + ref_local(nl, :) = v(1:2) + ELSE IF (TRIM(tag) == 'helix' .AND. nh < 1) THEN + READ (line, *, IOSTAT=ios) tag, v + IF (ios /= 0) CYCLE + nh = nh + 1 + ref_helix = v + END IF + END DO + CLOSE (u) + IF (nl /= 2 .OR. nh /= 1) THEN + WRITE (*, '(A)') 'reference file incomplete (localised, helix rows): '//TRIM(ref_path) + ERROR STOP 2 + END IF + END SUBROUTINE read_refs + + ! ------------------------------------------------------------------------------------------ + ! the condensed energy of a line in the test's own assembly (no loads, both end nodes held) + ! ------------------------------------------------------------------------------------------ + + SUBROUTINE line_energy(q, l0, ea, ei, ends, c, phi, theta_ref, e_tot, theta, mt, ok, r, ab, g) + !! E = sum_e U_e(q) + (Phi - Theta)^2 / (2 C), Theta unwrapped against theta_ref. Optional: + !! r = dE/dq, ab = sum_e K_e - M_t d2Theta/dq2 in DGBSV band storage (the band part B of the + !! tangent), g = dTheta/dq; the DOFs of both end nodes are held (rows and columns of B set + !! to the identity, r and g zeroed there). + REAL(wp), INTENT(IN) :: q(:), l0(:), ea, ei, ends(3, 4), c, phi, theta_ref + REAL(wp), INTENT(OUT) :: e_tot, theta, mt + LOGICAL, INTENT(OUT) :: ok + REAL(wp), INTENT(OUT), OPTIONAL :: r(:), ab(:, :), g(:) + REAL(wp) :: fe(12), ke(12, 12), ee, raw + REAL(wp), ALLOCATABLE :: gq(:) + INTEGER :: e, i, j, ii, jj, nd, es + CHARACTER(256) :: em + nd = SIZE(q) + ALLOCATE (gq(nd)) + e_tot = 0.0_wp + IF (PRESENT(r)) r = 0.0_wp + IF (PRESENT(ab)) ab = 0.0_wp + ok = .FALSE. + DO e = 1, SIZE(l0) + CALL CD_HermiteCable_Element(q(6*e - 5:6*e + 6), l0(e), ea, ei, ee, fe, ke, es, em) + IF (es /= 0) RETURN + e_tot = e_tot + ee + IF (PRESENT(r)) r(6*e - 5:6*e + 6) = r(6*e - 5:6*e + 6) + fe + IF (PRESENT(ab)) THEN + DO jj = 1, 12 + j = 6*(e - 1) + jj + DO ii = 1, 12 + i = 6*(e - 1) + ii + ab(2*KB + 1 + i - j, j) = ab(2*KB + 1 + i - j, j) + ke(ii, jj) + END DO + END DO + END IF + END DO + CALL CD_HermiteTorsion_Line(q, l0, ends, raw, gq, es, em) + IF (es /= CD_HTORS_OK) RETURN + theta = CD_HermiteTorsion_Unwrap(raw, theta_ref) + mt = (phi - theta)/c + e_tot = e_tot + 0.5_wp*(phi - theta)**2/c + IF (PRESENT(r)) r = r - mt*gq + IF (PRESENT(ab)) THEN + CALL CD_HermiteTorsion_Line(q, l0, ends, raw, gq, es, em, hband=ab, band_scale=-mt) + IF (es /= CD_HTORS_OK) RETURN + END IF + IF (PRESENT(g)) g = gq + DO i = 1, nd + IF (i > 6 .AND. i <= nd - 6) CYCLE + IF (PRESENT(r)) r(i) = 0.0_wp + IF (PRESENT(g)) g(i) = 0.0_wp + IF (PRESENT(ab)) THEN + DO j = MAX(1, i - KB), MIN(nd, i + KB) + ab(2*KB + 1 + i - j, j) = 0.0_wp + ab(2*KB + 1 + j - i, i) = 0.0_wp + END DO + ab(2*KB + 1, i) = 1.0_wp + END IF + END DO + ok = ABS(e_tot) < HUGE(1.0_wp) + END SUBROUTINE line_energy + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_zero_twist_parity() + !! V3: sagging line (EA 1e5, EI 1, w 5, L = 1, 24 elements), ends 0.9 apart along x and + !! clamped along out-of-plane directions; solved without torsion, then with both ends + !! restrained and Phi = Theta_0, the holonomy of that untwisted equilibrium. + INTEGER, PARAMETER :: NE = 24, ND = 6*(NE + 1) + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: seed(ND), q0(ND), q1(ND), l0(NE), gq(ND), ends(3, 4), raw, dq, da(3), db(3) + INTEGER :: k, es + CHARACTER(512) :: em + l0 = 1.0_wp/NE + da = [1.0_wp, 1.2_wp, -1.0_wp]/NORM2([1.0_wp, 1.2_wp, -1.0_wp]) + db = [0.2_wp, 1.0_wp, 1.0_wp]/NORM2([0.2_wp, 1.0_wp, 1.0_wp]) + DO k = 0, NE + seed(6*k + 1:6*k + 6) = [0.9_wp*k/NE, 0.0_wp, -0.2_wp*SIN(PI*k/NE), 0.9_wp, 0.0_wp, -0.2_wp*PI*COS(PI*k/NE)] + END DO + CALL sag(seed, da, db, q0, es, em) + CALL require(es == CD_HCSTAT_OK, 'Z: untwisted 3D sag solve converges: '//TRIM(em)) + IF (es /= CD_HCSTAT_OK) RETURN + ends(:, 1) = da + ends(:, 2) = unit_perp([0.0_wp, 1.0_wp, 0.0_wp], da) + ends(:, 3) = db + ends(:, 4) = unit_perp([0.0_wp, 1.0_wp, 0.0_wp], db) + CALL CD_HermiteTorsion_Line(q0, l0, ends, raw, gq, es, em) + CALL require(es == CD_HTORS_OK, 'Z: holonomy of the untwisted state') + tors%active = .TRUE. + ALLOCATE (tors%gj(NE)) + tors%gj = 0.3_wp + tors%ends = ends + tors%phi = raw + tors%theta_hint = raw + CALL sag(seed, da, db, q1, es, em, tors) + CALL require(es == CD_HCSTAT_OK, 'Z: twist-restrained solve with Phi = Theta_0 converges: '//TRIM(em)) + IF (es /= CD_HCSTAT_OK) RETURN + dq = nan_max_abs(q1 - q0) + WRITE (*, '(A,F9.5,A,ES10.3,A,ES10.3,A,L1)') 'zero-twist parity (3D sag): Theta_0 = ', raw, ' rad, |q - q_0| = ', & + dq, ', torque ', tors%torque, ', stable ', tors%stable + CALL require(ABS(raw) > 1.0e-2_wp, 'Z: the untwisted 3D line has a non-zero holonomy') + CALL require(dq <= 1.0e-8_wp, 'Z: the restrained untwisted line equals the line without torsion (1e-8)') + CALL require(ABS(tors%torque) <= 1.0e-8_wp, 'Z: no torque at Phi = Theta_0') + CALL require(tors%stable, 'Z: stable') + END SUBROUTINE check_zero_twist_parity + + SUBROUTINE sag(s0, d1, d2, qo, es2, em2, t) + !! The V3 sagging line (EA 1e5, EI 1, w 5, unit length) clamped along d1 and d2. + REAL(wp), INTENT(IN) :: s0(:), d1(3), d2(3) + REAL(wp), INTENT(OUT) :: qo(:) + INTEGER, INTENT(OUT) :: es2 + CHARACTER(*), INTENT(OUT) :: em2 + TYPE(CD_HermiteTorsionType), INTENT(INOUT), OPTIONAL :: t + REAL(wp), ALLOCATABLE :: l0(:), curv(:) + REAL(wp) :: res + INTEGER :: ne, nd, it + nd = SIZE(s0) + ne = nd/6 - 1 + ALLOCATE (l0(ne), curv(ne + 1)) + l0 = 1.0_wp/ne + IF (PRESENT(t)) THEN + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e5_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(5.0_wp, it=1, ne)], & + s0, [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 4, 100, & + 1.0e-9_wp, 1.0_wp, qo, curv, res, it, es2, em2, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([d1, d2], [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=t) + ELSE + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e5_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(5.0_wp, it=1, ne)], & + s0, [1, 2, 3, nd - 5, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 4, 100, & + 1.0e-9_wp, 1.0_wp, qo, curv, res, it, es2, em2, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([d1, d2], [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID]) + END IF + END SUBROUTINE sag + + FUNCTION unit_perp(v, d) RESULT(n) + REAL(wp), INTENT(IN) :: v(3), d(3) + REAL(wp) :: n(3) + n = v - DOT_PRODUCT(v, d)*d + n = n/NORM2(n) + END FUNCTION unit_perp + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_helix() + !! V5: helix r(s) = (a cos p, a sin p, s sin(alpha)), p = s cos(alpha)/a, four turns, 96 + !! elements (13.0 deg of tangent turn per element), EA 1e5 (EA/F > 1e6), GJ = 1, both ends + !! clamped with the Frenet normals as reference normals. Theta_helix = -tau l (oracle), and + !! Phi = Theta_helix + M_t C. The end reaction is measured as the derivative of the discrete + !! total energy: dE/dr_1 (force) and dE/dw (moment of a rigid rotation of the End-1 frame + !! and its clamped tangent about r_1). + INTEGER, PARAMETER :: NE = 96, ND = 6*(NE + 1) + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: a, alpha, bb, mt0, fref, kref, tau, ell, seed(ND), q(ND), l0(NE), curv(NE + 1), res, c + REAL(wp) :: s, p, t0(3), t1(3), n0(3), n1(3), drift, f1(3), mom(3), mexp(3), ferr, merr, h + REAL(wp) :: ep, emn, theta, mt, ends(3, 4), qp(ND), endsp(3, 4), rot(3, 3), w(3) + INTEGER :: k, it, es, ic + LOGICAL :: ok + CHARACTER(512) :: em + a = ref_helix(1) + alpha = ref_helix(2)*PI/180.0_wp + bb = ref_helix(3) + mt0 = ref_helix(4) + fref = ref_helix(5) + kref = ref_helix(6) + tau = SIN(alpha)*COS(alpha)/a + ell = 4.0_wp*2.0_wp*PI*a/COS(alpha) + l0 = ell/NE + DO k = 0, NE + s = ell*k/NE + p = s*COS(alpha)/a + seed(6*k + 1:6*k + 3) = [a*COS(p), a*SIN(p), s*SIN(alpha)] + seed(6*k + 4:6*k + 6) = [-SIN(p)*COS(alpha), COS(p)*COS(alpha), SIN(alpha)] + END DO + t0 = seed(4:6) + t1 = seed(ND - 2:ND) + n0 = [-1.0_wp, 0.0_wp, 0.0_wp] + p = ell*COS(alpha)/a + n1 = [-COS(p), -SIN(p), 0.0_wp] + c = ell/1.0_wp + tors%active = .TRUE. + ALLOCATE (tors%gj(NE)) + tors%gj = 1.0_wp + tors%ends(:, 1) = t0 + tors%ends(:, 2) = n0 + tors%ends(:, 3) = t1 + tors%ends(:, 4) = n1 + tors%theta_hint = -tau*ell + tors%phi = -tau*ell + mt0*c + tors%descend = .FALSE. + ends = tors%ends + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e5_wp, it=1, NE)], [(bb, it=1, NE)], [(0.0_wp, it=1, NE)], seed, & + [1, 2, 3, ND - 5, ND - 4, ND - 3], -1.0e6_wp, 0.0_wp, 1, 100, 1.0e-8_wp, & + 1.0_wp, q, curv, res, it, es, em, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([t0, t1], [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + CALL require(es == CD_HCSTAT_OK, 'H: clamped helix solve converges: '//TRIM(em)) + IF (es /= CD_HCSTAT_OK) RETURN + drift = 0.0_wp + DO k = 0, NE + drift = nan_max_abs([drift, ABS(NORM2(q(6*k + 1:6*k + 2)) - a)]) + END DO + ! end reaction: derivatives of the discrete total energy at the solution + h = 1.0e-6_wp + DO ic = 1, 3 + qp = q + qp(ic) = q(ic) + h + CALL line_energy(qp, l0, 1.0e5_wp, bb, ends, c, tors%phi, tors%theta, ep, theta, mt, ok) + qp(ic) = q(ic) - h + CALL line_energy(qp, l0, 1.0e5_wp, bb, ends, c, tors%phi, tors%theta, emn, theta, mt, ok) + f1(ic) = (ep - emn)/(2.0_wp*h) + w = 0.0_wp + w(ic) = h + rot = CD_Exp_SO3(w) + qp = q + qp(4:6) = MATMUL(rot, q(4:6)) + endsp = ends + endsp(:, 1) = MATMUL(rot, ends(:, 1)) + endsp(:, 2) = MATMUL(rot, ends(:, 2)) + CALL line_energy(qp, l0, 1.0e5_wp, bb, endsp, c, tors%phi, tors%theta, ep, theta, mt, ok) + rot = CD_Exp_SO3(-w) + qp(4:6) = MATMUL(rot, q(4:6)) + endsp(:, 1) = MATMUL(rot, ends(:, 1)) + endsp(:, 2) = MATMUL(rot, ends(:, 2)) + CALL line_energy(qp, l0, 1.0e5_wp, bb, endsp, c, tors%phi, tors%theta, emn, theta, mt, ok) + mom(ic) = (ep - emn)/(2.0_wp*h) + END DO + ! the helical wrench (F e_z, K e_z) about the axis, reduced to the end point r_1 = (a, 0, 0) + mexp = [0.0_wp, a*fref, kref] + ferr = MIN(NORM2(f1 - fref*[0.0_wp, 0.0_wp, 1.0_wp]), NORM2(f1 + fref*[0.0_wp, 0.0_wp, 1.0_wp]))/fref + merr = MIN(NORM2(mom - mexp), NORM2(mom + mexp))/kref + WRITE (*, '(A,I0,A,ES10.3,A,F10.7,A,3F11.7,A,3F11.7,A,L1)') 'helix (', NE, ' elements): radius drift/a ', & + drift/a, ', M_t ', tors%torque, ', end force ', f1, ', end moment ', mom, ', stable ', tors%stable + WRITE (*, '(A,F10.7,A,F10.7,A,ES10.3,A,ES10.3)') 'helix: reference F = ', fref, ', K = ', kref, & + '; force error/F ', ferr, ', moment error/K ', merr + CALL require(drift <= 5.0e-4_wp*a, 'H: the line stays on the helix (radius drift <= 5e-4 a)') + CALL require(ABS(tors%torque - mt0) <= 1.0e-4_wp*mt0, 'H: M_t = 0.5') + CALL require(ferr <= 5.0e-3_wp, 'H: end force = F e_z within 0.5 %') + CALL require(merr <= 5.0e-3_wp, 'H: end moment = K e_z + lever within 0.5 % of K') + END SUBROUTINE check_helix + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_localised_onset() + !! V6 (static form): clamped-clamped, sliding End 2 under the dead tension T, onset bisected + !! on the stability report of the straight state, N = 64 and 128 elements. + INTEGER, PARAMETER :: NES(2) = [64, 128] + REAL(wp) :: m(2), err(2), infinite + INTEGER :: it, k + DO it = 1, 2 + DO k = 1, 2 + m(k) = onset(NES(k), ref_local(it, 1), ref_local(it, 2)) + err(k) = ABS(m(k) - ref_local(it, 2))/ref_local(it, 2) + END DO + infinite = 2.0_wp*SQRT(ref_local(it, 1)) + WRITE (*, '(A,F7.1,A,2F12.6,A,F12.6,A,2ES10.2,A,F8.5)') 'localised onset T L^2/EI=', ref_local(it, 1), & + ': M_cr L/EI (N=64,128) =', m, ' eq.(33) ', ref_local(it, 2), ' rel.err ', err, & + '; ratio to 2 sqrt(EI T) ', m(2)/infinite + CALL require(ALL(m > 0.0_wp), 'L: onset bracketed') + CALL require(err(2) <= 1.0e-3_wp, 'L: N = 128 onset within 1e-3 of eq. (33)') + CALL require(err(2) <= err(1), 'L: refinement does not move the onset away') + CALL require(m(2) > infinite, 'L: the finite clamped rod buckles above 2 sqrt(EI T)') + END DO + END SUBROUTINE check_localised_onset + + REAL(wp) FUNCTION onset(ne, tension, mguess) RESULT(mcr) + INTEGER, INTENT(IN) :: ne + REAL(wp), INTENT(IN) :: tension, mguess + REAL(wp) :: lo, hi, mid + INTEGER :: k + LOGICAL :: failed + lo = 0.9_wp*mguess + hi = 1.1_wp*mguess + mcr = -1.0_wp + IF (.NOT. stable_at(ne, tension, lo, failed)) RETURN + IF (stable_at(ne, tension, hi, failed)) RETURN + DO k = 1, 60 + mid = 0.5_wp*(lo + hi) + IF (stable_at(ne, tension, mid, failed)) THEN + lo = mid + ELSE + hi = mid + END IF + IF (failed) RETURN + IF (hi - lo <= 1.0e-8_wp*mguess) EXIT + END DO + mcr = 0.5_wp*(lo + hi) + END FUNCTION onset + + LOGICAL FUNCTION stable_at(ne, tension, m, failed) RESULT(stab) + !! Straight weightless rod along +x, EI = L = GJ = 1, EA = 1e9 (T/EA <= 1.6e-6), node 1 held, the last node + !! sliding along x under the dead tension; both ends clamped along +x; Phi = m (no descent). + INTEGER, INTENT(IN) :: ne + REAL(wp), INTENT(IN) :: tension, m + LOGICAL, INTENT(OUT) :: failed + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp), ALLOCATABLE :: seed(:), l0(:), q(:), f(:), curv(:) + REAL(wp) :: res + INTEGER :: nd, it, es, k + CHARACTER(512) :: em + nd = 6*(ne + 1) + ALLOCATE (seed(nd), l0(ne), q(nd), f(nd), curv(ne + 1)) + l0 = 1.0_wp/ne + seed = 0.0_wp + DO k = 0, ne + seed(6*k + 1) = REAL(k, wp)/ne + seed(6*k + 4) = 1.0_wp + END DO + f = 0.0_wp + f(nd - 5) = tension + tors%active = .TRUE. + ALLOCATE (tors%gj(ne)) + tors%gj = 1.0_wp + tors%phi = m + tors%ends = RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, & + 0.0_wp, 0.0_wp, 1.0_wp], [3, 4]) + tors%descend = .FALSE. + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e9_wp, it=1, ne)], [(1.0_wp, it=1, ne)], [(0.0_wp, it=1, ne)], seed, & + [1, 2, 3, nd - 4, nd - 3], -1.0e6_wp, 0.0_wp, 1, 60, 1.0e-7_wp, 1.0_wp, q, & + curv, res, it, es, em, f_nodal=f, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + failed = es /= CD_HCSTAT_OK + IF (failed) WRITE (*, '(A)') ' solve failed: '//TRIM(em) + stab = tors%stable .AND. .NOT. failed + END FUNCTION stable_at + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_cosserat() + !! V7: a rod of unit length along +z, EA 1e6, EI 1, GJ 0.7 (Cosserat GAs 1e6, reduced shear, + !! 128 linear elements; Hermite 16 elements), End A clamped at the origin with frame + !! (d, n) = (e_z, e_x). (1) End B at (0, 0, 1) turned by 2.5 rad about e_z; (2) End B at + !! (0.15, -0.1, 0.98363) (chord = L) with the frame R_B = exp([0.25, -0.3, 1.2]). The Hermite line is + !! clamped with d_B = R_B e_z, n_B = R_B e_x and Phi = 0 (the Cosserat reference is the + !! untwisted straight rod). Compared: node positions at equal arc length and the torque + !! (Hermite M_t; Cosserat GJ times the material twist curvature, mean over the elements). + REAL(wp), PARAMETER :: THB(3, 2) = RESHAPE([0.0_wp, 0.0_wp, 2.5_wp, 0.25_wp, -0.3_wp, 1.2_wp], [3, 2]) + REAL(wp), PARAMETER :: RB(3, 2) = RESHAPE([0.0_wp, 0.0_wp, 1.0_wp, 0.15_wp, -0.1_wp, 0.98362593_wp], [3, 2]) + INTEGER :: icase + REAL(wp) :: mt_h, mt_c, dpos + REAL(wp), ALLOCATABLE :: rh(:, :), rc(:, :) + LOGICAL :: ok_h, ok_c + DO icase = 1, 2 + CALL hermite_rod(RB(:, icase), THB(:, icase), rh, mt_h, ok_h) + CALL cosserat_rod(RB(:, icase), THB(:, icase), rc, mt_c, ok_c) + CALL require(ok_h .AND. ok_c, 'C: both formulations converge') + IF (.NOT. (ok_h .AND. ok_c)) CYCLE + dpos = nan_max_abs(rh - rc(:, 1::8)) + WRITE (*, '(A,I0,A,ES10.3,A,F11.7,A,F11.7,A,ES10.3)') 'Cosserat cross-check ', icase, & + ': max position difference/L ', dpos, ', torque Hermite ', mt_h, ' Cosserat ', mt_c, ' rel. ', & + ABS(mt_h - mt_c)/ABS(mt_c) + CALL require(dpos <= 1.0e-2_wp, 'C: shapes agree within 1 % of L') + CALL require(ABS(mt_h - mt_c) <= 1.0e-2_wp*ABS(mt_c), 'C: torques agree within 1 %') + IF (icase == 1) CALL require(ABS(mt_h - 0.7_wp*2.5_wp) <= 1.0e-10_wp, 'C: pure torsion GJ Phi / L (Hermite)') + END DO + END SUBROUTINE check_cosserat + + SUBROUTINE hermite_rod(rb, thb, rpos, mt, ok) + REAL(wp), INTENT(IN) :: rb(3), thb(3) + REAL(wp), ALLOCATABLE, INTENT(OUT) :: rpos(:, :) + REAL(wp), INTENT(OUT) :: mt + LOGICAL, INTENT(OUT) :: ok + INTEGER, PARAMETER :: NE = 16, ND = 6*(NE + 1) + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: seed(ND), q(ND), l0(NE), curv(NE + 1), res, rmat(3, 3), dbv(3) + INTEGER :: k, it, es + CHARACTER(512) :: em + rmat = CD_Exp_SO3(thb) + dbv = rmat(:, 3) + l0 = 1.0_wp/NE + DO k = 0, NE + seed(6*k + 1:6*k + 3) = rb*REAL(k, wp)/NE + seed(6*k + 4:6*k + 6) = rb + END DO + tors%active = .TRUE. + ALLOCATE (tors%gj(NE)) + tors%gj = 0.7_wp + tors%ends(:, 1) = [0.0_wp, 0.0_wp, 1.0_wp] + tors%ends(:, 2) = [1.0_wp, 0.0_wp, 0.0_wp] + tors%ends(:, 3) = dbv + tors%ends(:, 4) = rmat(:, 1) + tors%phi = 0.0_wp + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e6_wp, it=1, NE)], [(1.0_wp, it=1, NE)], [(0.0_wp, it=1, NE)], seed, & + [1, 2, 3, ND - 5, ND - 4, ND - 3], -1.0e6_wp, 0.0_wp, 4, 200, 1.0e-8_wp, & + 1.0_wp, q, curv, res, it, es, em, endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([0.0_wp, 0.0_wp, 1.0_wp, dbv], [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + ok = es == CD_HCSTAT_OK + IF (.NOT. ok) WRITE (*, '(A)') ' Hermite solve: '//TRIM(em) + ALLOCATE (rpos(3, NE + 1)) + DO k = 0, NE + rpos(:, k + 1) = q(6*k + 1:6*k + 3) + END DO + mt = tors%torque + END SUBROUTINE hermite_rod + + SUBROUTINE cosserat_rod(rb, thb, rpos, mt, ok) + !! Linear-Lagrange Cosserat rod clamped at both ends (position and rotation vector), the End + !! B pose reached in ten increments (each solve seeded with the previous one). + REAL(wp), INTENT(IN) :: rb(3), thb(3) + REAL(wp), ALLOCATABLE, INTENT(OUT) :: rpos(:, :) + REAL(wp), INTENT(OUT) :: mt + LOGICAL, INTENT(OUT) :: ok + INTEGER, PARAMETER :: NE = 128, NN = NE + 1, ND = 6*NN, NSTEP = 20 + REAL(wp) :: nodes(3, NN), q0(ND), q(ND), fext(ND), lam0(3, 3), len0, gam(3), kap(3), lam, zb(3) + INTEGER :: conn(2, NE), fixed(12), k, istep, it, es + LOGICAL :: conv, stalled + TYPE(CosseratSolverConfig) :: cfg + CHARACTER(160) :: em + DO k = 1, NN + nodes(:, k) = [0.0_wp, 0.0_wp, REAL(k - 1, wp)/NE] + END DO + DO k = 1, NE + conn(:, k) = [k, k + 1] + END DO + fixed = [1, 2, 3, 4, 5, 6, ND - 5, ND - 4, ND - 3, ND - 2, ND - 1, ND] + fext = 0.0_wp + cfg%abs_tol = 1.0e-5_wp + cfg%max_iter = 200 + q = 0.0_wp + DO k = 1, NN + q(6*k - 5:6*k - 3) = nodes(:, k) + END DO + zb = [0.0_wp, 0.0_wp, 1.0_wp] + ok = .TRUE. + DO istep = 1, NSTEP + lam = REAL(istep, wp)/NSTEP + q0 = q + ! interior seed: previous state plus the end increment spread linearly; the end moves on + ! the sphere |r_B| = |rb| (no compressive intermediate chord) + DO k = 2, NN + q0(6*k - 5:6*k - 3) = q(6*k - 5:6*k - 3) + (REAL(k - 1, wp)/NE)*(end_at(zb, rb, lam) - & + end_at(zb, rb, lam - 1.0_wp/NSTEP)) + q0(6*k - 2:6*k) = q(6*k - 2:6*k) + (REAL(k - 1, wp)/NE)*thb/NSTEP + END DO + q0(ND - 5:ND - 3) = end_at(zb, rb, lam) + q0(ND - 2:ND) = lam*thb + CALL CD_Static_Cosserat_Solve(nodes, conn, [(1.0e6_wp, it=1, NE)], [(1.0e6_wp, it=1, NE)], & + [(1.0_wp, it=1, NE)], [(0.7_wp, it=1, NE)], q0, fext, fixed, .TRUE., cfg, q, & + conv, stalled, it, es, em) + IF (es /= CD_COS_OK .OR. .NOT. conv) THEN + ok = .FALSE. + WRITE (*, '(A,I0,A)') ' Cosserat solve, increment ', istep, ': '//TRIM(em) + EXIT + END IF + END DO + ALLOCATE (rpos(3, NN)) + mt = 0.0_wp + DO k = 1, NN + rpos(:, k) = q(6*k - 5:6*k - 3) + END DO + DO k = 1, NE + CALL CD_Reference_Frame(nodes(:, k), nodes(:, k + 1), lam0, len0) + CALL CD_Cosserat_Strains(q(6*k - 5:6*k + 6), lam0, len0, 0.0_wp, gam, kap) + mt = mt + 0.7_wp*kap(3)/NE + END DO + END SUBROUTINE cosserat_rod + + FUNCTION end_at(za, zb, f) RESULT(p) + !! The point at fraction f between za and zb, moved onto the sphere of radius |zb|. + REAL(wp), INTENT(IN) :: za(3), zb(3), f + REAL(wp) :: p(3) + p = za + f*(zb - za) + p = p*NORM2(zb)/NORM2(p) + END FUNCTION end_at + + ! ------------------------------------------------------------------------------------------ + SUBROUTINE check_bordered_term() + !! The rank-one term of the bordered Newton step on post-buckled states. A weightless rod + !! (EI = L = 1, GJ = 2.5 EI, the ratio of the lazy-wave reference cable, EA = 1e4, 16 + !! elements), clamped along +x at both ends, node 1 held and End B sliding along x under the + !! dead tension T L^2/EI = 10 with its lateral position held at y = 0.002 L (a small + !! imperfection that removes the rotation symmetry about the clamp axis). The production + !! static solve takes it to M = 1.05 and 1.20 M_cr (M_cr = 10.4289 EI/L, eq. (33)), where it + !! buckles out of plane and the torque relaxes. At each state, with B = sum K_e - + !! M_t d2Theta/dq2 and g = dTheta/dq on the free DOFs, rho = g^T B^-1 g / C is the error + !! amplification of a Newton iteration that drops g g^T / C (its error is multiplied by + !! -rho along B^-1 g): such an iteration converges only linearly, at the rate rho. From a + !! 1e-6 perturbation of every free DOF, the test's own Newton iteration (both end nodes held + !! at the solution) runs (a) with the bordered tangent through + !! CD_HermiteTorsion_Bordered_Solve: quadratic convergence back to the production state, and + !! (b) with B alone: linear convergence at the predicted rate rho. + REAL(wp), PARAMETER :: MCR = 10.42885644_wp, OFS = 2.0e-3_wp, GJ = 2.5_wp, FRACS(2) = [1.05_wp, 1.20_wp] + INTEGER, PARAMETER :: NE = 16, ND = 6*(NE + 1), MAXIT = 30 + TYPE(CD_HermiteTorsionType) :: tors + REAL(wp) :: seed(ND), qs(ND), l0(NE), curv(NE + 1), res, ends(3, 4), q0(ND), hist(MAXIT, 2), sk, fdead(ND) + REAL(wp) :: ab(LDAB, ND), abc(LDAB, ND), g(ND), z(ND), e, th, mt, qfin(ND), dfin, rho, amp, rate + INTEGER :: k, it, es, ic, nit(2), nneg_b, nneg_k, info, ipiv(ND), nr + LOGICAL :: conv(2), ok, mask(ND) + CHARACTER(512) :: em + EXTERNAL :: dgbsv + l0 = 1.0_wp/NE + fdead = 0.0_wp + fdead(ND - 5) = 10.0_wp + ends = RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, & + 0.0_wp, 0.0_wp, 1.0_wp], [3, 4]) + DO ic = 1, 2 + DO k = 0, NE + sk = REAL(k, wp)/NE + seed(6*k + 1:6*k + 6) = [1.001_wp*sk, OFS*(3.0_wp*sk**2 - 2.0_wp*sk**3), 0.0_wp, & + 1.001_wp, 6.0_wp*OFS*sk*(1.0_wp - sk), 0.0_wp] + END DO + tors = CD_HermiteTorsionType() + tors%active = .TRUE. + ALLOCATE (tors%gj(NE)) + tors%gj = GJ + tors%ends = ends + tors%phi = FRACS(ic)*MCR/GJ + CALL CD_HermiteCable_Static_Solve(l0, [(1.0e4_wp, it=1, NE)], [(1.0_wp, it=1, NE)], [(0.0_wp, it=1, NE)], & + seed, [1, 2, 3, ND - 4, ND - 3], -1.0e6_wp, 0.0_wp, 1, 100, & + 1.0e-9_wp, 1.0_wp, qs, curv, res, it, es, em, f_nodal=fdead, & + endconn_stiffness=[0.0_wp, 0.0_wp], & + endconn_direction=RESHAPE([1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 0.0_wp, 0.0_wp], & + [3, 2]), & + endconn_mode=[CD_ENDCONN_RIGID, CD_ENDCONN_RIGID], torsion=tors) + CALL require(es == CD_HCSTAT_OK .AND. tors%stable, 'S: the production solve reaches a stable twisted state: '// & + TRIM(em)) + IF (es /= CD_HCSTAT_OK) CYCLE + ! inertia of B and of K = B + g g^T / C at the solution (free DOFs), and rho + CALL line_energy(qs, l0, 1.0e4_wp, 1.0_wp, ends, 1.0_wp/GJ, tors%phi, tors%theta, e, th, mt, ok, ab=ab, g=g) + mask = .TRUE. + mask(1:6) = .FALSE. + mask(ND - 5:ND) = .FALSE. + CALL CD_HermiteTorsion_Inertia(ab, KB, KB, mask, g, 1.0_wp/GJ, nneg_b, nneg_k, es, em) + CALL require(es == CD_HTORS_OK, 'S: inertia count: '//TRIM(em)) + abc = ab + z = g + CALL dgbsv(ND, KB, KB, 1, abc, LDAB, ipiv, z, ND, info) + rho = DOT_PRODUCT(g, z)*GJ + IF (info /= 0) rho = HUGE(1.0_wp) + ! the perturbed start and the two iterations + q0 = qs + DO k = 7, ND - 6 + q0(k) = qs(k) + 1.0e-6_wp*SIN(1.7_wp*k) + END DO + CALL newton(q0, l0, ends, tors%phi, tors%theta, GJ, .TRUE., nit(1), hist(:, 1), conv(1), qfin) + dfin = nan_max_abs(qfin - qs) + CALL newton(q0, l0, ends, tors%phi, tors%theta, GJ, .FALSE., nit(2), hist(:, 2), conv(2), qfin) + ! observed linear rate of (b): geometric mean of the residual ratios after the first two + ! iterations, above the round-off floor + rate = 0.0_wp + nr = 0 + DO k = 3, MIN(nit(2), MAXIT - 1) + IF (hist(k + 1, 2) <= 1.0e-9_wp) EXIT + rate = rate + LOG(hist(k + 1, 2)/hist(k, 2)) + nr = nr + 1 + END DO + IF (nr > 0) rate = EXP(rate/nr) + amp = nan_max_abs(qs(3::6)) + WRITE (*, '(A,F5.2,A,F9.5,A,ES10.3,A,I0,A,I0)') 'bordered step at M/M_cr = ', FRACS(ic), ': M_t L/EI ', & + tors%torque, ', out-of-plane amplitude/L ', amp, '; negative eigenvalues of B ', nneg_b, ', of B + g g^T/C ', & + nneg_k + WRITE (*, '(A,I0,A,6ES9.1)') ' bordered tangent: ', nit(1), ' iterations, |R|: ', hist(1:MIN(6, nit(1) + 1), 1) + WRITE (*, '(A,I0,A,6ES9.1)') ' B alone: ', nit(2), ' iterations, |R|: ', hist(1:6, 2) + WRITE (*, '(A,ES10.3,A,ES10.3)') ' rho = g^T B^-1 g / C = ', rho, ', observed rate without g g^T/C ', rate + CALL require(conv(1) .AND. nit(1) <= 4, 'S: the bordered Newton step converges within 4 iterations') + CALL require(dfin <= 1.0e-8_wp, 'S: and returns to the production solution') + CALL require(rho >= 0.05_wp, 'S: the rank-one term carries weight on the buckled branch (rho >= 0.05)') + CALL require(conv(2) .AND. nit(2) >= nit(1) + 2, 'S: without the rank-one term the iteration slows down') + CALL require(nr >= 1 .AND. ABS(rate/rho - 1.0_wp) <= 0.25_wp, & + 'S: without the rank-one term it converges linearly at the predicted rate rho (25 %)') + CALL require(nneg_b == 0 .AND. nneg_k == 0, 'S: both tangents positive definite at this stable state') + END DO + END SUBROUTINE check_bordered_term + + SUBROUTINE newton(qstart, l0, ends, phi, theta0, gjn, bordered, niter, rh, converged, qout) + !! Newton iteration of the S gate (EA 1e4, EI = GJ = 1, both end nodes held), with the + !! bordered tangent (CD_HermiteTorsion_Bordered_Solve) or with its band part B alone. + REAL(wp), INTENT(IN) :: qstart(:), l0(:), ends(3, 4), phi, theta0, gjn + LOGICAL, INTENT(IN) :: bordered + INTEGER, INTENT(OUT) :: niter + REAL(wp), INTENT(OUT) :: rh(:), qout(:) + LOGICAL, INTENT(OUT) :: converged + REAL(wp), ALLOCATABLE :: qn(:), r(:), abn(:, :), gn(:), dq(:) + REAL(wp) :: en, thn, mtn, thref + INTEGER :: iter, info, nd, maxit, es + INTEGER, ALLOCATABLE :: ipiv(:) + LOGICAL :: okn + CHARACTER(256) :: em + EXTERNAL :: dgbsv + nd = SIZE(qstart) + maxit = SIZE(rh) + ALLOCATE (qn(nd), r(nd), abn(LDAB, nd), gn(nd), dq(nd), ipiv(nd)) + qn = qstart + rh = 0.0_wp + converged = .FALSE. + thref = theta0 + niter = maxit + DO iter = 0, maxit - 1 + CALL line_energy(qn, l0, 1.0e4_wp, 1.0_wp, ends, SUM(l0)/gjn, phi, thref, en, thn, mtn, okn, r=r, ab=abn, g=gn) + IF (.NOT. okn) EXIT + thref = thn + rh(iter + 1) = nan_max_abs(r) + IF (.NOT. (rh(iter + 1) < 1.0e3_wp)) EXIT + IF (rh(iter + 1) <= 1.0e-10_wp) THEN + converged = .TRUE. + niter = iter + EXIT + END IF + IF (bordered) THEN + CALL CD_HermiteTorsion_Bordered_Solve(abn, KB, KB, gn, SUM(l0)/gjn, -r, dq, es, em) + IF (es /= CD_HTORS_OK) EXIT + ELSE + dq = -r + CALL dgbsv(nd, KB, KB, 1, abn, LDAB, ipiv, dq, nd, info) + IF (info /= 0) EXIT + END IF + qn = qn + dq + END DO + qout = qn + END SUBROUTINE newton + +END PROGRAM test_torsion_validation diff --git a/validation/README.md b/validation/README.md index f88403a..8c3fc6e 100644 --- a/validation/README.md +++ b/validation/README.md @@ -35,6 +35,7 @@ licence holders. | V-D1b buoy release (bodies suite) | `orcaflex_vd1b_reference.py ref --nseg 320 --dt 0.00025 --out DIR` | `bodies/references/V-D1b/orcaflex.json` (`summary`: statistics, dominant tension harmonic, snap maxima over t = 0.5-120 s), scored by `bodies_score.py` | time series in `DIR/*.npz` | | L3-6c irregular sea | `orcaflex_lazywave_irregular_reference.py` | `REF_*` values in `tests/test_l3_lazywave_irregular.f90` | printed; the sea is CableDyn's seeded synthesis, so no wave table is needed | | L3-6c, the article's sea | `orcaflex_lazywave_irregular_reference.py --orcaflex-jonswap TABLE.dat` | `ART_*` values in the same test | OrcaFlex's 78-component table in `TABLE.dat`; run `test_l3_lazywave_irregular OUT TABLE.dat` | +| T-2, T-5, T-6 torsion | `orcaflex_torsion_reference.py --refs tests/data/torsion_orcaflex_refs.txt` | `tests/data/torsion_orcaflex_refs.txt` (torques, buckling onsets, twist, peak curvature, offset, tension), checked by `test_torsion_orcaflex` | `build/validation/orcaflex_torsion_summary.json`: summary values and every setting with OrcaFlex's read-back | | L-fig 800 m curvature overlay | `curvature_profile_overlay.py` | `tests/data/lfig_800m_curvature_summary.csv` | `build/validation/lfig_800m_curvature_profiles.csv` | | L2-2 dynamic chain | `l2_2_orcaflex_summary.py /tests/Mooring/QuasiStatic` | `tests/data/l2_2_dynamic_refs/orcaflex_summary.txt` | the OrcaFlex series distributed with MoorDyn-C (BSD-3-Clause) | | 800 m segment-length study | see `performance/ORCAFLEX_MESH_CONVERGENCE.md` | `performance/orcaflex_mesh_convergence.csv` | rerun of the listed meshes | diff --git a/validation/scripts/hermite_torsion_oracle.py b/validation/scripts/hermite_torsion_oracle.py new file mode 100644 index 0000000..6359041 --- /dev/null +++ b/validation/scripts/hermite_torsion_oracle.py @@ -0,0 +1,266 @@ +# File: validation/scripts/hermite_torsion_oracle.py +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +"""Automatic-differentiation reference for the parallel-transport twist of a Hermite cable. + +The cubic-Hermite cable stores, at every node, a position r and a tangent handle m = dr/ds, +with the 12 element DOFs ordered [r1, m1, r2, m2]. For condensed isotropic torsion, the +twist of the centreline between two end frames (d_A, n_A) and (d_B, n_B) is the +parallel-transport (smallest-rotation, Bishop) holonomy + + Theta = angle about d_B from n_B to PT(n_A) (right-hand rule, modulo 2 pi), + +where PT carries n_A from d_A through the node tangents t_k = m_k / |m_k| to d_B. Because the +Hermite curve is C1, PT is the product of the smallest rotations along the chain +d_A -> t_0 -> ... -> t_N -> d_B and of the element holonomies + + h_e = + integral_e t1 . (a x b) / (|a|^2 (1 + t1 . a/|a|)) ds, a = r', b = r'', + +where t1 is the tangent at the first node of element e. This script evaluates Theta in that +form with JAX (float64), differentiates it twice with respect to the nodal DOFs and to small +rotations of the two end frames, and writes the reference values used by the Fortran test +``tests/test_hermite_torsion_kernel.f90``: + + python validation/scripts/hermite_torsion_oracle.py --out tests/data/torsion_oracle_reference.txt + +``--out`` is required (``--help`` only prints the usage). Requires JAX and NumPy. + +The end-frame derivatives are taken with respect to the rotation vector w of an +incremental rotation exp(w) applied to both end directors at w = 0. Before writing, +the script checks the analytic structure the kernel relies on: Theta agrees with a +brute-force parallel transport (fine smallest-rotation steps, no h_e formula), the +DOF Hessian has half-bandwidth 11, and the end rotations couple only to the adjacent +end-node tangent. +""" +from __future__ import annotations + +import os + +import numpy as np +import jax +import jax.numpy as jnp +from jax import lax + +jax.config.update("jax_enable_x64", True) + +TWO_PI = 2.0 * np.pi +KBAND = 11 +QUADRATURE_ORDER = 4 + + +# ------------------------------------------------------------------------------------------ +# kinematics and Theta +# ------------------------------------------------------------------------------------------ +def unit(v): + return v / jnp.linalg.norm(v, axis=-1, keepdims=True) + + +def smallest_rotation(a, b, v): + """Smallest rotation taking unit a to unit b, applied to v (singular at b = -a).""" + c = jnp.dot(a, b) + w = jnp.cross(a, b) + return c * v + jnp.cross(w, v) + w * jnp.dot(w, v) / (1.0 + c) + + +def signed_angle(u, v, axis): + return jnp.arctan2(jnp.dot(axis, jnp.cross(u, v)), jnp.dot(u, v)) + + +def gauss01(order): + x, w = np.polynomial.legendre.leggauss(order) + return 0.5 * (x + 1.0), 0.5 * w + + +def element_ab(qe, length, xi): + """a = dr/ds and b = d2r/ds2 of one element at the parametric points xi.""" + x = jnp.asarray(xi) + dh = jnp.stack([-6 * x + 6 * x**2, length * (1 - 4 * x + 3 * x**2), 6 * x - 6 * x**2, + length * (-2 * x + 3 * x**2)], 1) + ddh = jnp.stack([-6 + 12 * x, length * (-4 + 6 * x), 6 - 12 * x, length * (-2 + 6 * x)], 1) + nodal = qe.reshape(4, 3) + return (dh / length) @ nodal, (ddh / length**2) @ nodal + + +def element_holonomy(qe, length, order=QUADRATURE_ORDER): + xi, w = gauss01(order) + a, b = element_ab(qe, length, xi) + t1 = unit(qe[3:6]) + s2 = jnp.sum(a * a, 1) + num = jnp.cross(a, b) @ t1 + den = s2 * (1.0 + (a @ t1) / jnp.sqrt(s2)) + return length * jnp.sum(jnp.asarray(w) * num / den) + + +def element_dofs(q): + n_el = q.size // 6 - 1 + return q[6 * np.arange(n_el)[:, None] + np.arange(12)[None, :]] + + +def theta(q, lengths, ends, order=QUADRATURE_ORDER): + """Theta modulo 2 pi: sum of element holonomies plus the smallest-rotation chain.""" + d_a, n_a, d_b, n_b = ends + hol = jnp.sum(jax.vmap(element_holonomy, (0, 0, None))(element_dofs(q), lengths, order)) + chain = jnp.concatenate([d_a[None], unit(q.reshape(-1, 6)[:, 3:]), d_b[None]]) + + def step(v, uu): + return smallest_rotation(uu[0], uu[1], v), None + + v, _ = lax.scan(step, n_a, (chain[:-1], chain[1:])) + return hol + signed_angle(n_b, v, d_b) + + +def rotate(w, v): + """exp(w) v (rotation-vector map); differentiable at w = 0.""" + k = jnp.array([[0.0, -w[2], w[1]], [w[2], 0.0, -w[0]], [-w[1], w[0], 0.0]]) + return jax.scipy.linalg.expm(k) @ v + + +def theta_with_end_rotations(x, lengths, ends): + """Theta of x = [q, w_A, w_B]: q nodal DOFs, w_A / w_B incremental end-frame rotations.""" + n = x.size - 6 + w_a, w_b = x[n:n + 3], x[n + 3:] + d_a, n_a, d_b, n_b = ends + rotated = (rotate(w_a, d_a), rotate(w_a, n_a), rotate(w_b, d_b), rotate(w_b, n_b)) + return theta(x[:n], lengths, rotated) + + +def brute_force_theta(q, lengths, ends, samples=1500): + """Independent parallel transport by smallest-rotation steps between closely sampled + tangents (no h_e formula); Richardson-extrapolated in the step count.""" + + def sr(a, b, v): + c, w = a @ b, np.cross(a, b) + return c * v + np.cross(w, v) + w * (w @ v) / (1.0 + c) + + def run(ns): + d_a, n_a, d_b, n_b = (np.asarray(e) for e in ends) + v, prev = n_a.copy(), d_a + xi = np.linspace(0.0, 1.0, ns + 1) + for e, qe in enumerate(np.asarray(element_dofs(q))): + a = np.asarray(element_ab(jnp.asarray(qe), float(lengths[e]), xi)[0]) + for tk in a / np.linalg.norm(a, axis=1, keepdims=True): + v = sr(prev, tk, v) + v = v - (v @ tk) * tk + v /= np.linalg.norm(v) + prev = tk + v = sr(prev, d_b, v) + return float(np.arctan2(d_b @ np.cross(n_b, v), n_b @ v)) + + t1, t2 = run(samples), run(2 * samples) + d = (t2 - t1 + np.pi) % TWO_PI - np.pi + return t1 + d + d / 3.0 + + +# ------------------------------------------------------------------------------------------ +# configurations +# ------------------------------------------------------------------------------------------ +def random_curve(n_el, length=10.0, n_modes=4, amp=1.5, seed=0, stretch_amp=0.1): + """Smooth random large-deformation 3D curve sampled at the nodes, with random nodal + stretch so that |m| differs from 1.""" + rng = np.random.default_rng(seed) + k = np.arange(1, n_modes + 1) + a_c = rng.normal(size=(3, n_modes)) * amp / k + b_c = rng.normal(size=(3, n_modes)) * amp / k + w = np.pi * k / length + s = np.linspace(0.0, length, n_el + 1) + r = np.array([[si, 0.0, 0.0] + (a_c * np.sin(w * si) + b_c * (np.cos(w * si) - 1.0)).sum(1) * length / np.pi + for si in s]) + dr = np.array([[1.0, 0.0, 0.0] + (a_c * w * np.cos(w * si) - b_c * w * np.sin(w * si)).sum(1) * length / np.pi + for si in s]) + stretch = 1.0 + stretch_amp * rng.uniform(-1, 1, size=s.size) + return np.hstack([r, dr * stretch[:, None]]).ravel(), np.diff(s) + + +def clamped_ends(q): + """End directors along the end tangents; n_A from a fixed seed, n_B its projection.""" + nodes = q.reshape(-1, 6) + t_a = nodes[0, 3:] / np.linalg.norm(nodes[0, 3:]) + t_b = nodes[-1, 3:] / np.linalg.norm(nodes[-1, 3:]) + seed = np.array([0.0, 0.0, 1.0]) if abs(t_a[2]) < 0.9 else np.array([0.0, 1.0, 0.0]) + n_a = seed - (seed @ t_a) * t_a + n_a /= np.linalg.norm(n_a) + n_b = n_a - (n_a @ t_b) * t_b + return t_a, n_a, t_b, n_b / np.linalg.norm(n_b) + + +def articulated_ends(): + """End directors that differ from the end tangents (articulated ends).""" + d_a = np.array([0.8, 0.5, -0.33]) / np.linalg.norm([0.8, 0.5, -0.33]) + d_b = np.array([-0.2, 0.9, 0.4]) / np.linalg.norm([-0.2, 0.9, 0.4]) + n_a = np.cross(d_a, [0.0, 0.0, 1.0]) + n_b = np.cross(d_b, [1.0, 0.0, 0.0]) + return d_a, n_a / np.linalg.norm(n_a), d_b, n_b / np.linalg.norm(n_b) + + +def cases(): + out = [] + for seed in (0, 1, 2): + q, le = random_curve(16, seed=seed) + out.append((f"random_seed{seed}_clamped", q, le, clamped_ends(q))) + q, le = random_curve(16, seed=0) + out.append(("random_seed0_articulated", q, le, articulated_ends())) + return out + + +# ------------------------------------------------------------------------------------------ +# export +# ------------------------------------------------------------------------------------------ +def fmt(values): + return "\n".join(" ".join(repr(float(v)) for v in values[i:i + 4]) for i in range(0, len(values), 4)) + + +def main(path): + blocks = [] + for name, q, le, ends in cases(): + n = q.size + x0 = jnp.concatenate([jnp.asarray(q), jnp.zeros(6)]) + e = tuple(jnp.asarray(v) for v in ends) + lj = jnp.asarray(le) + th = float(theta(jnp.asarray(q), lj, e)) + grad = np.asarray(jax.grad(theta_with_end_rotations)(x0, lj, e)) + hess = np.asarray(jax.hessian(theta_with_end_rotations)(x0, lj, e)) + bf = brute_force_theta(q, le, ends) + scale = np.abs(hess).max() + i, j = np.indices((n, n)) + off_band = np.abs(hess[:n, :n][np.abs(i - j) > KBAND]).max() / scale + mask = np.ones((n + 6, n + 6), bool) # entries the kernel may fill + mask[:n, :n] = np.abs(i - j) <= KBAND + mask[n:n + 3, n:n + 3] = mask[n + 3:, n + 3:] = True + mask[n:n + 3, 3:6] = mask[3:6, n:n + 3] = True + mask[n + 3:, n - 3:n] = mask[n - 3:n, n + 3:] = True + outside = np.abs(hess[~mask]).max() / scale + assert off_band < 1e-14 and outside < 1e-14, (name, off_band, outside) + err_bf = abs((th - bf + np.pi) % TWO_PI - np.pi) + print(f"{name}: Theta = {th:+.15f}, brute-force PT difference {err_bf:.1e}, " + f"off-band {off_band:.1e}, outside end blocks {outside:.1e}") + band = [hess[r, c] for c in range(n) for r in range(c, min(c + KBAND, n - 1) + 1)] + end_blocks = [hess[n:n + 3, n:n + 3], hess[n + 3:, n + 3:], hess[n:n + 3, 3:6], hess[n + 3:, n - 3:n]] + values = (list(le) + list(q) + [v for d in ends for v in d] + [th] + list(grad) + band + + [v for blk in end_blocks for v in blk.ravel(order="F")]) + blocks.append(f"{name}\n{n // 6} {QUADRATURE_ORDER}\n{fmt(values)}") + header = f"""# Reference values for tests/test_hermite_torsion_kernel.f90. +# Generated by validation/scripts/hermite_torsion_oracle.py (JAX automatic differentiation, +# float64, {QUADRATURE_ORDER}-point Gauss-Legendre rule per element). Do not edit by hand. +# Layout: number of cases, then per case +# name (one line); n_nodes quadrature_order +# L_e (n_nodes - 1), q (6 n_nodes, node order [r, m]), d_A n_A d_B n_B (12), +# Theta (modulo 2 pi), dTheta/d[q, w_A, w_B] (6 n_nodes + 6), +# d2Theta/dq2 lower band: for column j = 1..n, rows i = j..min(j + 11, n), +# d2Theta/dw_A2, d2Theta/dw_B2, d2Theta/dw_A dm_0, d2Theta/dw_B dm_N (3x3 each, column-major). +# w_A, w_B: rotation vectors of incremental rotations exp(w) of the end frames, at w = 0. +""" + with open(path, "w", encoding="ascii", newline="\n") as fh: + fh.write(header + f"{len(blocks)}\n" + "\n".join(blocks) + "\n") + print(f"wrote {path}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--out", + required=True, + help="output path (the committed reference is tests/data/torsion_oracle_reference.txt)", + ) + main(os.path.normpath(parser.parse_args().out)) diff --git a/validation/scripts/orcaflex_torsion_reference.py b/validation/scripts/orcaflex_torsion_reference.py new file mode 100644 index 0000000..d942f15 --- /dev/null +++ b/validation/scripts/orcaflex_torsion_reference.py @@ -0,0 +1,782 @@ +# File: validation/scripts/orcaflex_torsion_reference.py +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University +"""OrcaFlex references for the CableDyn torsion validation (provenance tool, not built by CMake). + + python validation/scripts/orcaflex_torsion_reference.py [a] [b] [c] [--out DIR] [--refs FILE] + python validation/scripts/orcaflex_torsion_reference.py --summary JSON --refs FILE + +Runs with OrcaFlex 11.6d through OrcFxAPI 11.6d (about 5 min for all three cases). The run +writes only DIR/orcaflex_torsion_summary.json (default DIR: build/validation): the OrcaFlex +version, every data item the script sets together with the value OrcaFlex reads back, the +SHA-256 of this script and the summary values of each case. No OrcaFlex model, simulation or +raw result file is written. ``--refs FILE`` turns that summary (or, with ``--summary``, an +existing one, without running OrcaFlex) into the compact table that +``tests/test_torsion_orcaflex.f90`` checks, ``tests/data/torsion_orcaflex_refs.txt`` (SI units). +The pure-torsion case of that test runs the deck with a larger EI than case a (see the test): +the straight-branch torque does not depend on EI. + +OrcaFlex units: m, te, kN, s; torque kN.m. + +Conventions established with the API: + * Line data names: IncludeTorsion, StaticsStep1/2, End{A,B}{Azimuth,Declination,Gamma}, + End{A,B}{x,y}BendingStiffness, End{A,B}TwistingStiffness; line type GJ, TensionTorqueCoupling. + * With torsion included an end must be either encastre (every end stiffness Infinity) or have + every stiffness finite; a bending-free end with an infinite twisting stiffness is rejected. + * Imposed end twist = End B gamma (right-handed about the end Ez axis). A fresh statics run + maps gamma_B - gamma_A into (-180, 180] deg; multi-turn twist is reached by continuation in + steps below 180 deg with UseCalculatedPositions(SetLinesToUserSpecifiedStartingShape=True). + * 'Torque' is + GJ (phi_B - phi_A) / L for a right-handed rotation phi about the A->B + tangent; 'Twist' is the twist rate in deg/m; end stiffnesses are in kN.m/deg. + * A full modal analysis raises 'static position is an unstable equilibrium' past a buckling + onset; that flag is the bisection detector of case b. + +Cases: + a pure torsion of a straight clamped-clamped neutrally buoyant line (L 10 m, GJ 10 kN.m^2). + b Greenhill onset, clamped-clamped, dead-tension prestress by end separation, found by + bisection on the modal stability flag and cross-checked by extrapolating the lowest lateral + f^2 to zero; 25 to 400 segments; compared with van der Heijden et al. (2003) eq. (33) at the + measured tension. Also bending-free ends with a finite twisting spring (semi-tangential). + c Lozon et al. (2025) Gulf of Mexico 80 m lazy-wave cable with an illustrative + GJ = 50 kN.m^2, both ends clamped in their untwisted static orientation; hang-off gamma + 0 -> 5 turns in quarter turns by continuation; segments 0.75 m and 0.375 m. +""" + +from __future__ import annotations + +import argparse +import cmath +import hashlib +import json +import math +import os +import time +import traceback + +# OrcFxAPI is imported only to run OrcaFlex (_load_orcaflex), so --summary works without it +ofx = None + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +SUMMARY_NAME = "orcaflex_torsion_summary.json" +RHO_W = 1.025 # te/m^3 (OrcaFlex default sea water density) +STATICS_TOL = {"a": 1e-9, "b": 1e-8, "c": 1e-8} # General.StaticsTolerance (default 1e-6); +# the tightest value each case converges with (b: 1e-9 fails with EA/EI = 1e5) +STATICS_MAXIT = 2000 # General.StaticsMaxIterations (default 400) +SETTINGS: dict = {} # "case/object.dataname" -> {"set": v, "readback": v} + + +def inf(): + return ofx.OrcinaInfinity() + + +def setv(case, obj, name, value): + """Set a data item, read it back, fail if it did not take effect, record it.""" + setattr(obj, name, value) + rb = getattr(obj, name) + if not isinstance(rb, (str, int, float)): + rb = list(rb) # IndexedDataItem (per-section data) + if isinstance(value, (list, tuple)): + ok = len(rb) == len(value) and all(_same(a, b) for a, b in zip(value, rb)) + else: + ok = _same(value, rb) + if not ok: + raise RuntimeError(f"{obj.name}.{name}: set {value!r} but read back {rb!r}") + key = f"{case}/{obj.name}.{name}" + entry = {"set": _js(value), "readback": _js(rb)} + prev = SETTINGS.setdefault(key, []) + if entry not in prev: + prev.append(entry) + return rb + + +def _same(a, b): + if isinstance(a, str) or isinstance(b, str): + return str(a) == str(b) + if a >= 1e306 and b >= 1e306: # OrcaFlex 'Infinity' + return True + return abs(a - b) <= 1e-12 * max(1.0, abs(a)) + + +def _js(v): + if isinstance(v, (list, tuple)): + return [_js(x) for x in v] + if isinstance(v, float) and v >= 1e306: + return "Infinity" + return v + + +def new_model(case, depth): + m = ofx.Model() + setv(case, m.general, "StaticsTolerance", STATICS_TOL[case]) + setv(case, m.general, "StaticsMaxIterations", STATICS_MAXIT) + setv(case, m.environment, "WaterDepth", depth) + setv(case, m.environment, "Density", RHO_W) + return m + + +def line_type(case, m, name, od, mass_te_per_m, ea_kN, ei_kNm2, gj_kNm2): + lt = m.CreateObject(ofx.ObjectType.LineType, name) + setv(case, lt, "OD", od) + setv(case, lt, "ID", 0.0) + setv(case, lt, "MassPerUnitLength", mass_te_per_m) + setv(case, lt, "EA", ea_kN) + setv(case, lt, "EIx", ei_kNm2) # EIy left '~' (= EIx, isotropic) + setv(case, lt, "GJ", gj_kNm2) + setv(case, lt, "TensionTorqueCoupling", 0.0) + setv(case, lt, "SeabedLateralFrictionCoefficient", 0.0) # axial '~' = lateral + return lt + + +def end_conn(case, line, ab, *, x, y, z, azimuth, declination, gamma=0.0, conn="Fixed", clamped=True): + setv(case, line, f"End{ab}Connection", conn) + setv(case, line, f"End{ab}X", x) + setv(case, line, f"End{ab}Y", y) + setv(case, line, f"End{ab}Z", z) + setv(case, line, f"End{ab}Azimuth", azimuth) + setv(case, line, f"End{ab}Declination", declination) + setv(case, line, f"End{ab}Gamma", gamma) + if clamped: # encastre: every connection stiffness Infinity (y '~' follows x) + setv(case, line, f"End{ab}xBendingStiffness", inf()) + setv(case, line, f"End{ab}TwistingStiffness", inf()) + + +def straight_model(case, L, n_seg, ei, gj, ea, od=0.1, depth=100.0, sep_extra=0.0, sections=None): + """Neutrally buoyant straight line along global X at mid-depth: weightless in water. + + sections: optional [(GJ, length)], each a separate line type, for the series check.""" + m = new_model(case, depth) + mass = RHO_W * math.pi / 4 * od**2 # neutral buoyancy + line = m.CreateObject(ofx.ObjectType.Line, "rod") + if sections is None: + line_type(case, m, "lt0", od, mass, ea, ei, gj) + sections = [("lt0", L)] + else: + names = [] + for i, (g, ln_) in enumerate(sections): + line_type(case, m, f"lt{i}", od, mass, ea, ei, g) + names.append((f"lt{i}", ln_)) + sections = names + setv(case, line, "IncludeTorsion", "Yes") + setv(case, line, "StaticsStep1", "Catenary") + setv(case, line, "StaticsStep2", "Full statics") + setv(case, line, "NumberOfSections", len(sections)) + setv(case, line, "LineType", [s[0] for s in sections]) + setv(case, line, "Length", [s[1] for s in sections]) + setv(case, line, "TargetSegmentLength", [L / n_seg] * len(sections)) + zc = -depth / 2 + # Ez of both end fittings along +X (A -> B): azimuth 0, declination 90. + end_conn(case, line, "A", x=0.0, y=0.0, z=zc, azimuth=0.0, declination=90.0) + end_conn(case, line, "B", x=L + sep_extra, y=0.0, z=zc, azimuth=0.0, declination=90.0) + return m, line + + +def rg(line, var): + r = line.RangeGraph(var) + return list(r.X), list(r.Mean) + + +def statics(m): + m.CalculateStatics() + + +def commit(m): + """Make the converged static state the starting shape of the next solve (this keeps the + multi-turn twist; it resets the model, so read results before calling it).""" + if m.state == ofx.ModelState.InStaticState: + m.UseCalculatedPositions(True) + + +def set_gamma_continued(m, line, ab, target, step=90.0): + """Walk End gamma to target in steps <= step deg, each started from the previous state.""" + commit(m) + cur = getattr(line, f"End{ab}Gamma") + n = max(1, math.ceil(abs(target - cur) / step)) + for k in range(1, n + 1): + commit(m) + line.SetData(f"End{ab}Gamma", -1, cur + (target - cur) * k / n) + statics(m) + + +# ------------------------------------------------------------------------------------------ +def case_a(): + L, gj, ei, ea, n = 10.0, 10.0, 1.0, 1.0e5, 20 + rows = [] + m, line = straight_model("a", L, n, ei, gj, ea) + # gamma_B values reached by continuation from 0 (steps <= 90 deg) + for gam in (90.0, 450.0, 1800.0, -90.0): + set_gamma_continued(m, line, "B", gam) + s, tq = rg(line, "Torque") + _, tw = rg(line, "Twist") + _, yy = rg(line, "Y") + _, zz = rg(line, "Z") + exp = gj * math.radians(gam) / L + rows.append( + { + "gamma_A_deg": 0.0, + "gamma_B_deg": gam, + "expected_GJ_Phi_over_L_kNm": exp, + "torque_min_kNm": min(tq), + "torque_max_kNm": max(tq), + "torque_rel_err_max": max(abs(t / exp - 1) for t in tq), + "twist_rate_deg_per_m_min": min(tw), + "twist_rate_deg_per_m_max": max(tw), + "Gamma_result_endB_deg": line.StaticResult("Gamma", ofx.oeEndB), + "max_lateral_m": max(math.hypot(a, b + 50.0) for a, b in zip(yy, zz)), + "bend_moment_endB_kNm": line.StaticResult("Bend moment", ofx.oeEndB), + } + ) + print("case a", rows[-1], flush=True) + # fresh statics (no continuation) at 450 deg: OrcaFlex takes the twist modulo one turn + m2, line2 = straight_model("a", L, n, ei, gj, ea) + setv("a", line2, "EndBGamma", 450.0) + m2.CalculateStatics() + fresh450 = rg(line2, "Torque")[1][n // 2] + # only the end gamma difference matters + m3, line3 = straight_model("a", L, n, ei, gj, ea) + setv("a", line3, "EndAGamma", 30.0) + setv("a", line3, "EndBGamma", 120.0) + m3.CalculateStatics() + diff90 = rg(line3, "Torque")[1][n // 2] + # series compliance: two sections GJ 10 / 25 kN.m^2, 4 m + 6 m, gamma_B = 90 deg + secs = [(10.0, 4.0), (25.0, 6.0)] + m4, line4 = straight_model("a", L, n, ei, gj, ea, sections=secs) + setv("a", line4, "EndBGamma", 90.0) + m4.CalculateStatics() + _, tq4 = rg(line4, "Torque") + _, tw4 = rg(line4, "Twist") + exp4 = math.radians(90.0) / sum(l_ / g for g, l_ in secs) + return { + "L_m": L, + "GJ_kNm2": gj, + "EI_kNm2": ei, + "EA_kN": ea, + "segments": n, + "continuation_rows": rows, + "fresh_statics_gamma450": { + "torque_kNm": fresh450, + "note": "fresh statics returns the 90 deg solution: gamma is taken modulo one turn unless continued", + }, + "gammaA30_gammaB120": {"torque_kNm": diff90, "expected_kNm": gj * math.radians(90.0) / L}, + "series_two_sections": { + "sections_GJ_len": secs, + "gamma_B_deg": 90.0, + "expected_kNm": exp4, + "torque_min_kNm": min(tq4), + "torque_max_kNm": max(tq4), + "twist_rate_deg_per_m_range": [min(tw4), max(tw4)], + }, + } + + +# ------------------------------------------------------------------------------------------ +def eq33(M, T, B=1.0, L=1.0): + """van der Heijden et al. (2003) eq. (33) residual, clamped-clamped, m = ML/(2 pi B), + t = TL^2/(4 pi^2 B).""" + m = M * L / (2 * math.pi * B) + t = T * L * L / (4 * math.pi**2 * B) + a = cmath.sqrt(m * m - 4 * t) + if abs(a) < 1e-12: + sinc = math.pi + else: + sinc = cmath.sin(math.pi * a) / a + return (cmath.cos(math.pi * a) - math.cos(math.pi * m) - 2 * math.pi * t * sinc).real + + +def eq33_root(T, B, L, guess): + """Root of eq. (33) in M bracketed around guess (first critical torque).""" + t = T * L * L / (4 * math.pi**2 * B) + if abs(t) < 1e-9: # eq. (33) degenerates at t = 0 to tan(pi m) = pi m (paper, p. 172) + lo, hi = 4.4, 4.6 + for _ in range(200): + mid = 0.5 * (lo + hi) + if math.tan(mid) - mid < 0: + lo = mid + else: + hi = mid + return 2 * B / L * 0.5 * (lo + hi) + # first sign change of the residual nearest the guess + xs = [guess * (0.8 + 0.4 * i / 4000) for i in range(4001)] + fs = [eq33(x, T, B, L) for x in xs] + cands = [i for i in range(4000) if fs[i] * fs[i + 1] <= 0] + if not cands: + raise RuntimeError("eq33 bracket failed") + i = min(cands, key=lambda j: abs(xs[j] - guess)) + lo, hi, flo = xs[i], xs[i + 1], fs[i] + for _ in range(200): + mid = 0.5 * (lo + hi) + fm = eq33(mid, T, B, L) + if flo * fm <= 0: + hi = mid + else: + lo, flo = mid, fm + if hi - lo < 1e-14 * guess: + break + return 0.5 * (lo + hi) + + +def lowest_period(line): + """(stable, lowest period). Unstable equilibrium -> (False, None).""" + try: + md = ofx.Modes(line, ofx.ModalAnalysisSpecification(calculateShapes=False)) + return True, float(list(md.period)[0]) + except ofx.DLLError as exc: + if "unstable equilibrium" in str(exc): + return False, None + raise + + +def onset_one(n, Tn, gj=10.0, L=10.0, ei=1.0, ea=1.0e5, rel_tol=2e-7): + ref_nom = {0: 8.98681892, 10: 10.42885644, 50: 15.58932393, 200: 28.98273785}[Tn] + T = Tn * ei / L**2 + sep = T * L / ea + m, line = straight_model("b", L, n, ei, gj, ea, sep_extra=sep) + phi_ref = ref_nom * ei / L * L / gj # rad + t0 = time.time() + + def at(gdeg): + set_gamma_continued(m, line, "B", gdeg, step=60.0) + st, per = lowest_period(line) + tq = rg(line, "Torque")[1] + return st, per, tq[len(tq) // 2] + + # bracket: walk up from 0.9 phi_ref in 1 % steps + g_lo = math.degrees(0.9 * phi_ref) + st, per, _ = at(g_lo) + if not st: + raise RuntimeError(f"n={n} T={Tn}: unstable already at 0.9 x closed form") + g_hi = None + g = g_lo + while g_hi is None: + g += math.degrees(0.01 * phi_ref) + st, per, _ = at(g) + if st: + g_lo = g + else: + g_hi = g + if g > math.degrees(1.2 * phi_ref): + raise RuntimeError("no instability found up to 1.2 x closed form") + # bisection on the modal stability flag + while (g_hi - g_lo) > rel_tol * g_hi: + gm = 0.5 * (g_lo + g_hi) + st, per, _ = at(gm) + if st: + g_lo = gm + else: + g_hi = gm + st, per, M_lo = at(g_lo) + _, te = rg(line, "Effective tension") + T_meas = te[len(te) // 2] + # cross-check: quadratic extrapolation of f1^2 = 1/P1^2 to zero from the stable side + pts = [] + for frac in (0.97, 0.98, 0.99): + st2, per2, M2 = at(g_lo * frac) + pts.append((M2, 1.0 / per2**2)) + M_extrap = _quad_root(pts) + M_eq33 = eq33_root(T_meas, ei, L, ref_nom * ei / L) + return { + "segments": n, + "T_nominal_kN": T, + "T_measured_mid_kN": T_meas, + "TL2_over_EI_measured": T_meas * L * L / ei, + "gamma_onset_deg": [g_lo, g_hi], + "M_onset_kNm": M_lo, + "M_onset_L_over_EI": M_lo * L / ei, + "M_onset_f2_extrapolated_kNm": M_extrap, + "eq33_at_measured_T_kNm": M_eq33, + "eq33_at_nominal_T_kNm": ref_nom * ei / L, + "rel_err_vs_eq33": M_lo / M_eq33 - 1, + "lowest_period_at_onset_s": per, + "seconds": round(time.time() - t0, 1), + } + + +PINNED_REF = { # analytic_refs.json B_greenhill.pinned (M_cr L/EI) + 0: {"semi_tangential": 4.91128772575888, "axial_greenhill": 6.283185307179586}, + 10: {"semi_tangential": 7.269274206916713, "axial_greenhill": 8.915066887262116}, +} + + +def onset_pinned(n, Tn, kt_deg, gj=10.0, L=10.0, ei=1.0, ea=1.0e5, rel_tol=2e-7): + """Bending-free ends (x/y bending stiffness 0) with a FINITE twisting spring kt_deg + [kN.m/deg] -- the only torsionally restrained 'pinned' end OrcaFlex accepts with torsion on. + Onset by bisection on the modal stability flag, in applied torque M (gamma_B is set from the + series compliance L/GJ + 2/kt).""" + T = Tn * ei / L**2 + m, line = straight_model("b", L, n, ei, gj, ea, sep_extra=T * L / ea) + for ab in "AB": + setv("b", line, f"End{ab}xBendingStiffness", 0.0) + setv("b", line, f"End{ab}TwistingStiffness", kt_deg) + comp = L / gj + 2.0 / math.degrees(kt_deg) # kt_deg * 180/pi = kN.m/rad + t0 = time.time() + + def at(Mk): + set_gamma_continued(m, line, "B", math.degrees(Mk * comp), step=30.0) + st, _ = lowest_period(line) + return st, rg(line, "Torque")[1][n // 2] + + lo, hi, Mk = 0.3 * ei / L, None, 0.3 * ei / L + while hi is None: + Mk += 0.1 * ei / L + st, _ = at(Mk) + if st: + lo = Mk + else: + hi = Mk + if Mk > 2.0 * PINNED_REF[Tn]["axial_greenhill"] * ei / L: + raise RuntimeError("no instability found") + while hi - lo > rel_tol * hi: + mid = 0.5 * (lo + hi) + st, _ = at(mid) + if st: + lo = mid + else: + hi = mid + _, Mt = at(lo) + ref = PINNED_REF[Tn] + return { + "segments": n, + "TL2_over_EI": Tn, + "twisting_stiffness_kNm_per_deg": kt_deg, + "M_onset_kNm": Mt, + "M_onset_L_over_EI": Mt * L / ei, + "rel_err_vs_semi_tangential": Mt * L / ei / ref["semi_tangential"] - 1, + "rel_err_vs_axial_greenhill": Mt * L / ei / ref["axial_greenhill"] - 1, + "seconds": round(time.time() - t0, 1), + } + + +def _quad_root(pts): + """Root of the quadratic through 3 points (x, y) nearest the last x.""" + (x0, y0), (x1, y1), (x2, y2) = pts + d01 = (y1 - y0) / (x1 - x0) + d12 = (y2 - y1) / (x2 - x1) + a = (d12 - d01) / (x2 - x0) + b = d12 - a * (x1 + x2) # y = a x^2 + b x + c + c = y2 - a * x2 * x2 - b * x2 + if abs(a) < 1e-300: + return -c / b + disc = b * b - 4 * a * c + r = [(-b + s * math.sqrt(max(disc, 0.0))) / (2 * a) for s in (1, -1)] + return min(r, key=lambda z: abs(z - x2)) + + +def _richardson(rows): + """Observed order and extrapolation from the three finest meshes (h ~ 1/n).""" + r = sorted(rows, key=lambda q: q["segments"])[-3:] + f1, f2, f3 = (q["M_onset_kNm"] for q in r) + ratio = r[1]["segments"] / r[0]["segments"] + if (f2 - f3) == 0 or (f1 - f2) / (f2 - f3) <= 0: + return {"order": None, "extrapolated_kNm": f3} + p = math.log((f1 - f2) / (f2 - f3)) / math.log(ratio) + ext = f3 + (f3 - f2) / (ratio**p - 1) + return {"order": p, "extrapolated_kNm": ext, "from_segments": [q["segments"] for q in r]} + + +def case_b(): + out = { + "L_m": 10.0, + "EI_kNm2": 1.0, + "EA_kN": 1.0e5, + "detector": "bisection on OrcaFlex full modal analysis stability flag ('static position " + "is an unstable equilibrium'); cross-check by quadratic extrapolation of " + "the lowest f^2 from 97/98/99 % of onset", + "no_imperfection": "straight line; OrcaFlex statics stays on the straight branch past " + "onset, so stability is read from the modal analysis", + "tensions": {}, + } + plan = {0: (25, 50, 100, 200), 10: (25, 50, 100, 200, 400), 50: (25, 50, 100, 200), 200: (25, 50, 100, 200)} + for Tn, meshes in plan.items(): + rows = [] + for n in meshes: + r = onset_one(n, Tn) + rows.append(r) + print("case b", Tn, r, flush=True) + rich = _richardson(rows) + Mext = rich["extrapolated_kNm"] + rich["rel_err_vs_eq33"] = Mext / rows[-1]["eq33_at_measured_T_kNm"] - 1 + out["tensions"][f"TL2_over_EI={Tn}"] = {"GJ_kNm2": 10.0, "rows": rows, "richardson": rich} + # bending-free ends with a finite twisting spring: which pinned-end torque does OrcaFlex apply? + pin = { + "note": "End x/y bending stiffness 0, twisting stiffness finite (kN.m/deg); OrcaFlex " + "rejects bending-free + torsionally rigid when torsion is included", + "closed_forms_M_L_over_EI": PINNED_REF, + "tensions": {}, + } + for Tn in (0, 10): + rows = [onset_pinned(n, Tn, 1.0e4) for n in (50, 100, 200)] + for r in rows: + print("case b pinned", r, flush=True) + rich = _richardson(rows) + rich["rel_err_vs_semi_tangential"] = rich["extrapolated_kNm"] * 10.0 / PINNED_REF[Tn]["semi_tangential"] - 1 + pin["tensions"][f"TL2_over_EI={Tn}"] = {"rows": rows, "richardson": rich} + r = onset_pinned(100, 0, 1.0e2) + print("case b pinned kt=1e2", r, flush=True) + pin["spring_independence_T0_n100_kt=1e2"] = r + out["pinned_bendfree_finite_twist"] = pin + # GJ independence: GJ = EI = 1 (onset twist 10.4 rad, needs multi-turn continuation) + r = onset_one(100, 10, gj=1.0) + out["GJ_independence_TL2_over_EI=10_GJ=1_n100"] = r + print("case b GJ=1", r, flush=True) + return out + + +# ------------------------------------------------------------------------------------------ +def lazy_wave(seg): + case = "c" + depth, ho, anc = 80.0, (5.0, -14.0), (125.0, -80.0) + sections = [("bare", 68.114), ("buoy", 50.0), ("bare", 52.101)] # hang-off -> anchor + gj = 50.0 # kN.m^2, ILLUSTRATIVE + m = new_model(case, depth) + line_type(case, m, "bare", 0.16, 36.7e-3, 4.69e5, 19.9, gj) + line_type(case, m, "buoy", 0.29, 59.53e-3, 4.69e5, 19.9, gj) + line = m.CreateObject(ofx.ObjectType.Line, "cable") + secs = list(reversed(sections)) # End A = anchor, End B = hang-off + setv(case, line, "NumberOfSections", len(secs)) + setv(case, line, "LineType", [s[0] for s in secs]) + setv(case, line, "Length", [s[1] for s in secs]) + setv(case, line, "TargetSegmentLength", [seg] * len(secs)) + setv(case, line, "StaticsStep1", "Catenary") + setv(case, line, "StaticsStep2", "Full statics") + # pass 1: no torsion, default pinned ends (bending stiffness 0) -> natural end orientations + setv(case, line, "IncludeTorsion", "No") + setv(case, line, "EndAConnection", "Fixed") + setv(case, line, "EndBConnection", "Fixed") + for ab, xyz in (("A", anc), ("B", ho)): + setv(case, line, f"End{ab}X", xyz[0]) + setv(case, line, f"End{ab}Y", 0.0) + setv(case, line, f"End{ab}Z", xyz[1]) + m.CalculateStatics() + pass1 = { + "tension_hangoff_kN": line.StaticResult("Effective tension", ofx.oeEndB), + "max_curvature_per_m": max(rg(line, "Curvature")[1]), + } + m.UseStaticLineEndOrientations() + orient = {ab: {k: getattr(line, f"End{ab}{k}") for k in ("Azimuth", "Declination", "Gamma")} for ab in "AB"} + # pass 2: torsion on, both ends encastre at the untwisted static orientation + setv(case, line, "IncludeTorsion", "Yes") + for ab in "AB": + setv(case, line, f"End{ab}xBendingStiffness", inf()) + setv(case, line, f"End{ab}TwistingStiffness", inf()) + Ltot = sum(s[1] for s in secs) + rows = [] + for k in range(0, 21): + turns = 0.25 * k + target = orient["B"]["Gamma"] + 360.0 * turns + try: + set_gamma_continued(m, line, "B", target, step=90.0) + except Exception as exc: # noqa: BLE001 + rows.append({"turns_at_hangoff": turns, "statics_ok": False, "msg": str(exc)[:300]}) + print("case c", seg, rows[-1], flush=True) + break + s, tq = rg(line, "Torque") + _, tw = rg(line, "Twist") + _, kk = rg(line, "Curvature") + _, yy = rg(line, "Y") + _, zz = rg(line, "Z") + _, te = rg(line, "Effective tension") + MB = line.StaticResult("Torque", ofx.oeEndB) + phi = math.radians(360.0 * turns) + lo, hi = 0.03 * Ltot, 0.97 * Ltot + kin = [abs(kk[i]) for i in range(len(kk)) if lo <= s[i] <= hi] + ipk = max(range(len(kk)), key=lambda i: abs(kk[i])) + row = { + "turns_at_hangoff": turns, + "statics_ok": True, + "torque_hangoff_kNm": MB, + "torque_anchor_kNm": line.StaticResult("Torque", ofx.oeEndA), + "torque_min_kNm": min(tq), + "torque_max_kNm": max(tq), + "twist_rate_deg_per_m_min": min(tw), + "twist_rate_deg_per_m_max": max(tw), + "GJ_Phi_over_L_kNm": gj * phi / Ltot, + "geometric_twist_turns": (phi - MB * Ltot / gj) / (2 * math.pi), + "eff_tension_hangoff_kN": line.StaticResult("Effective tension", ofx.oeEndB), + "eff_tension_anchor_kN": line.StaticResult("Effective tension", ofx.oeEndA), + "eff_tension_min_kN": min(te), + "max_curvature_per_m": abs(kk[ipk]), + "arc_of_max_curvature_m": s[ipk], + "max_curvature_interior_per_m": max(kin), + "max_abs_Y_m": max(abs(v) for v in yy), + "min_Z_m": min(zz), + "bend_moment_hangoff_kNm": line.StaticResult("Bend moment", ofx.oeEndB), + "bend_moment_anchor_kNm": line.StaticResult("Bend moment", ofx.oeEndA), + } + rows.append(row) + print("case c", seg, row, flush=True) + return { + "segment_m": seg, + "pass1_no_torsion_pinned": pass1, + "untwisted_end_orientation_deg": orient, + "line_length_m": Ltot, + "rows": rows, + } + + +def case_c(): + res = { + "site": "Lozon et al. (2025) GoMex 80 m (geometry as CableDyn validation/scripts/orcaflex_lozon_cables.py)", + "GJ_kNm2_illustrative": 50.0, + "EI_kNm2": 19.9, + "seabed_friction": 0.0, + "end_A": "anchor (125, 0, -80)", + "end_B": "hang-off (5, 0, -14)", + "meshes": {}, + } + for seg in (0.75, 0.375): + res["meshes"][str(seg)] = lazy_wave(seg) + a, b = (res["meshes"][k]["rows"] for k in ("0.75", "0.375")) + conv = [] + for ra, rb in zip(a, b): + if ra.get("statics_ok") and rb.get("statics_ok"): + conv.append( + { + "turns": ra["turns_at_hangoff"], + "torque_rel_diff": (ra["torque_hangoff_kNm"] - rb["torque_hangoff_kNm"]) + / (abs(rb["torque_hangoff_kNm"]) + 1e-30), + "peak_curv_rel_diff": ra["max_curvature_interior_per_m"] / rb["max_curvature_interior_per_m"] - 1, + } + ) + res["mesh_0.75_vs_0.375"] = conv + return res + + +def _load_orcaflex() -> None: + """Import OrcFxAPI as the module-level ofx (needed only to run OrcaFlex).""" + global ofx + if ofx is None: + import OrcFxAPI + + ofx = OrcFxAPI + + +def run(cases: list[str], out_dir: str) -> str: + """Run the OrcaFlex cases and write (update) the JSON summary; return its path.""" + _load_orcaflex() + os.makedirs(out_dir, exist_ok=True) + out = os.path.join(out_dir, SUMMARY_NAME) + with open(os.path.abspath(__file__), "rb") as fh: + sha = hashlib.sha256(fh.read()).hexdigest() + summary: dict = {} + if os.path.exists(out): # keep cases not rerun in this invocation + with open(out, encoding="utf-8") as fh: + summary = json.load(fh) + summary.update( + { + "OrcaFlex_version": ofx.DLLVersion(), + "script": os.path.basename(__file__), + "script_sha256": sha, + "units": "m, te, kN, s; torque kN.m", + "statics": f"Line IncludeTorsion=Yes, StaticsStep1=Catenary, StaticsStep2=Full " + f"statics; General StaticsTolerance={STATICS_TOL}, " + f"StaticsMaxIterations={STATICS_MAXIT}", + "density_te_m3": RHO_W, + } + ) + for c in cases: + SETTINGS.clear() + t0 = time.time() + try: + summary["case_" + c] = {"a": case_a, "b": case_b, "c": case_c}[c]() + except Exception: # noqa: BLE001 + summary["case_" + c] = {"error": traceback.format_exc()} + print(summary["case_" + c]["error"], flush=True) + summary["case_" + c]["settings_set_and_read_back"] = dict(SETTINGS) + summary["case_" + c]["script_sha256"] = sha + summary["case_" + c]["wall_seconds"] = round(time.time() - t0, 1) + with open(out, "w", encoding="utf-8") as fh: + json.dump(summary, fh, indent=1) + print("wrote", out) + return out + + +def write_refs(summary_path: str, refs_path: str) -> None: + """The compact SI reference table of tests/test_torsion_orcaflex.f90 from a JSON summary.""" + with open(summary_path, encoding="utf-8") as fh: + s = json.load(fh) + a, b, c = s["case_a"], s["case_b"], s["case_c"] + lines = [ + "# OrcaFlex torsion reference values (summary values only) for tests/test_torsion_orcaflex.f90.", + f"# OrcaFlex {s['OrcaFlex_version']} through OrcFxAPI; written by", + "# validation/scripts/orcaflex_torsion_reference.py --refs from its JSON summary", + f"# (script SHA-256 {s['script_sha256'][:16]}...). Line IncludeTorsion Yes, statics Catenary then", + f"# Full statics; StaticsTolerance {STATICS_TOL['a']:g} (pure), {STATICS_TOL['b']:g} (onsets, lazy wave);", + f"# StaticsMaxIterations {STATICS_MAXIT}; TensionTorqueCoupling 0; seabed friction 0.", + "# Units SI (N, m, N.m); OrcaFlex kN and kN.m are converted. See VALIDATION.md (torsion).", + "#", + f"# pure: straight neutrally buoyant line, L {a['L_m']} m, {a['segments']} segments, EI {a['EI_kNm2']} kN.m^2,", + f"# GJ {a['GJ_kNm2']} kN.m^2, both ends encastre; gamma reached by continuation.", + "# tag gamma_A_deg gamma_B_deg torque_N.m", + ] + for r in a["continuation_rows"]: + lines.append( + f"pure {r['gamma_A_deg']:.1f} {r['gamma_B_deg']:.1f} " + f"{1e3 * 0.5 * (r['torque_min_kNm'] + r['torque_max_kNm']):.12e}" + ) + lines.append(f"pure 30.0 120.0 {1e3 * a['gammaA30_gammaB120']['torque_kNm']:.12e}") + ser = a["series_two_sections"] + lines += [ + "# series: GJ 10 kN.m^2 over 4 m then 25 kN.m^2 over 6 m, gamma_B 90 deg", + "# tag torque_N.m", + f"series {1e3 * 0.5 * (ser['torque_min_kNm'] + ser['torque_max_kNm']):.12e}", + ] + lines += [ + "# clamped: Greenhill onset of the clamped-clamped line (case b, L 10 m, EI 1 kN.m^2), M_cr L/EI:", + "# OrcaFlex at 200 segments and the Richardson extrapolation of its three finest meshes", + "# tag T_L^2/EI n200 extrapolated", + ] + for key, v in b["tensions"].items(): + tn = float(key.split("=")[1]) + n200 = [r for r in v["rows"] if r["segments"] == 200][0]["M_onset_L_over_EI"] + lines.append(f"clamped {tn:.1f} {n200:.10f} {10.0 * v['richardson']['extrapolated_kNm']:.10f}") + pin = b["pinned_bendfree_finite_twist"] + lines += [ + "# pinned: bending-free ends with a finite twisting spring (1e4 kN.m/deg), M_cr L/EI as above", + "# tag T_L^2/EI n200 extrapolated", + ] + for key, v in pin["tensions"].items(): + tn = float(key.split("=")[1]) + n200 = [r for r in v["rows"] if r["segments"] == 200][0]["M_onset_L_over_EI"] + lines.append(f"pinned {tn:.1f} {n200:.10f} {10.0 * v['richardson']['extrapolated_kNm']:.10f}") + m = c["meshes"]["0.375"] + orient = m["untwisted_end_orientation_deg"] + lines += [ + f"# lazywave: {c['site']}, GJ {c['GJ_kNm2_illustrative']} kN.m^2 (illustrative),", + "# anchor End A, hang-off End B, segment 0.375 m, both ends encastre at the untwisted static", + "# orientation; the declinations (deg) of that orientation at the anchor and the hang-off:", + f"orientation {orient['A']['Declination']:.10f} {orient['B']['Declination']:.10f}", + "# tag turns torque_hangoff_N.m geometric_twist_turns peak_interior_curvature_1/m max_abs_Y_m" + " T_hangoff_N", + ] + for r in m["rows"]: + if not r.get("statics_ok") or abs(r["turns_at_hangoff"] - round(r["turns_at_hangoff"])) > 1e-9: + continue + lines.append( + f"lazywave {r['turns_at_hangoff']:.1f} {1e3 * r['torque_hangoff_kNm']:.9e} " + f"{r['geometric_twist_turns']:.9e} {r['max_curvature_interior_per_m']:.9e} " + f"{r['max_abs_Y_m']:.9e} {1e3 * r['eff_tension_hangoff_kN']:.9e}" + ) + with open(refs_path, "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(lines) + "\n") + print("wrote", refs_path) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("cases", nargs="*", help="cases to run: a, b and/or c (default: all)") + parser.add_argument( + "--out", default=os.path.join(REPO, "build", "validation"), help="directory of the JSON summary" + ) + parser.add_argument("--refs", help="write the compact reference table to this path") + parser.add_argument("--summary", help="write --refs from this existing JSON summary instead of running OrcaFlex") + args = parser.parse_args() + if any(c not in ("a", "b", "c") for c in args.cases): + parser.error("cases are a, b and c") + summary = args.summary + if summary is None: + summary = run(args.cases or ["a", "b", "c"], args.out) + if args.refs: + write_refs(summary, args.refs) + + +if __name__ == "__main__": + main()