From b7bd81d6d832797db0db0d699a1f04c7ec4ec34d Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 11:41:02 +0900 Subject: [PATCH 01/26] Add a parallel-transport twist kernel for the Hermite cable New module CableDyn_HermiteTorsion for condensed isotropic torsion: for one line it returns the parallel-transport holonomy Theta between the two end frames, its gradient and its Hessian in the existing general-band layout (half-bandwidth 11), plus the derivatives with respect to incremental end frame rotations. Element holonomies and smallest-rotation chain terms are differentiated in closed form; the chain derivatives use per-link gauges frozen at the current configuration. Also provides the 2 pi unwrap rule with a pi/2 step limit and a fold guard (1 + t1.t >= 0.5) that fail closed with named status codes. The module is not called by any solver yet, so existing results are unchanged. --- CMakeLists.txt | 1 + src/CableDyn_HermiteTorsion.f90 | 769 ++++++++++++++++++++++++++++++++ 2 files changed, 770 insertions(+) create mode 100644 src/CableDyn_HermiteTorsion.f90 diff --git a/CMakeLists.txt b/CMakeLists.txt index ca4e0cf..6dd643f 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 diff --git a/src/CableDyn_HermiteTorsion.f90 b/src/CableDyn_HermiteTorsion.f90 new file mode 100644 index 0000000..2bdad98 --- /dev/null +++ b/src/CableDyn_HermiteTorsion.f90 @@ -0,0 +1,769 @@ +! 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 + IMPLICIT NONE + PRIVATE + + PUBLIC :: CD_HermiteTorsion_Element + PUBLIC :: CD_HermiteTorsion_Line + PUBLIC :: CD_HermiteTorsion_Fold_Check + PUBLIC :: CD_HermiteTorsion_Unwrap + PUBLIC :: CD_HermiteTorsion_Accept + + 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 + !! 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 + +CONTAINS + + ! ------------------------------------------------------------------------------------------ + ! 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 + + order = NG_DEFAULT + IF (PRESENT(quadrature_order)) order = quadrature_order + CALL line_fold_check(q, Le, ends, order, 'CD_HermiteTorsion_Fold_Check', margin, ErrStat, ErrMsg) + END SUBROUTINE CD_HermiteTorsion_Fold_Check + + PURE SUBROUTINE line_fold_check(q, Le, ends, order, who, margin, ErrStat, ErrMsg) + 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 + + INTEGER :: nn, e, j, ig + REAL(wp) :: gp(NG_MAX), gw(NG_MAX), t1(3), a(3), b(3), rho, sp, val + REAL(wp), ALLOCATABLE :: u(:, :) + CHARACTER(16) :: tag + + margin = CD_ZERO + CALL validate_line(q, Le, ends, order, who, ErrStat, ErrMsg) + IF (ErrStat /= CD_HTORS_OK) RETURN + nn = SIZE(q)/6 + ALLOCATE (u(3, 0:nn + 1)) + 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_Line(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, hband, band_scale, & + end_grad, end_hess, end_cross, fold_margin, quadrature_order) + !! 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 element holonomies + chain angle in (-pi, pi]; 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 + !! + !! 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 + + INTEGER :: order, nn, n, e, j, k, i0, i, jj, row0 + LOGICAL :: want_hess + 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 + REAL(wp), ALLOCATABLE :: u(:, :), pole(:, :), gu(:, :), huu(:, :, :), huv(:, :, :), band(:, :) + 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) + + CALL line_fold_check(q, Le, ends, order, 'CD_HermiteTorsion_Line', margin, ErrStat, ErrMsg) + 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 + + ALLOCATE (u(3, 0:nn + 1), pole(3, 0:nn), gu(3, 0:nn + 1), huu(3, 3, 0:nn + 1), huv(3, 3, 0:nn)) + ALLOCATE (band(2*CD_HTORS_KBAND + 1, n)) + row0 = CD_HTORS_KBAND + 1 ! diagonal row of the local band + band = 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 + + IF (.NOT. (CD_Is_Finite(theta_raw) .AND. CD_All_Finite(grad) .AND. CD_All_Finite(band) .AND. & + CD_All_Finite(eg) .AND. CD_All_Finite(RESHAPE(eh, [18])) .AND. CD_All_Finite(RESHAPE(ec, [18])))) 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 CD_HermiteTorsion_Line + + 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. + 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 From 37fe5b592f924a200cdeb9daa4e1c91dc0f3fbdc Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 11:41:02 +0900 Subject: [PATCH 02/26] Gate the twist kernel against automatic-differentiation reference values tests/test_hermite_torsion_kernel.f90 checks Theta, its gradient and every second derivative against reference values from validation/scripts/hermite_torsion_oracle.py (JAX, four random large-deformation lines with clamped and articulated ends), and against finite differences. It also checks the band edge, rigid-rotation invariance, planar curves, the helix closed form, unwrapping through more than three turns, the step limit, the fold guard and malformed input. --- CMakeLists.txt | 7 + tests/data/torsion_oracle_reference.txt | 1455 ++++++++++++++++++ tests/test_hermite_torsion_kernel.f90 | 709 +++++++++ validation/scripts/hermite_torsion_oracle.py | 261 ++++ 4 files changed, 2432 insertions(+) create mode 100644 tests/data/torsion_oracle_reference.txt create mode 100644 tests/test_hermite_torsion_kernel.f90 create mode 100644 validation/scripts/hermite_torsion_oracle.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dd643f..a6e285c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -563,6 +563,12 @@ 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_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) @@ -1958,6 +1964,7 @@ if(CABLEDYN_ENABLE_COVERAGE) test_robust_init test_line test_hermite_cable + test_hermite_torsion_kernel test_hermite_cable_static test_hermite_sequenced test_hermite_resolution 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/test_hermite_torsion_kernel.f90 b/tests/test_hermite_torsion_kernel.f90 new file mode 100644 index 0000000..55ef652 --- /dev/null +++ b/tests/test_hermite_torsion_kernel.f90 @@ -0,0 +1,709 @@ +! 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. + !! 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 + 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_rigid_rotation() + CALL check_planar() + CALL check_helix_and_unwrap() + CALL check_accept_rule() + CALL check_fold_and_inputs() + CALL check_element_routine() + + 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 = MAX(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 = MAX(dum, ABS(hfd(i, j))) + IF (ABS(i - j) == KB) tp = MAX(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 + + ! --------------------------------------------------------------------------------------- + ! 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 = MAX(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 = MAX(err_t, ABS(wrap(th2 - th))) + err_g = MAX(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 = MAX(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 + +END PROGRAM test_hermite_torsion_kernel diff --git a/validation/scripts/hermite_torsion_oracle.py b/validation/scripts/hermite_torsion_oracle.py new file mode 100644 index 0000000..fb39fbb --- /dev/null +++ b/validation/scripts/hermite_torsion_oracle.py @@ -0,0 +1,261 @@ +# 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 [output] + +The default output is ``tests/data/torsion_oracle_reference.txt``. 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 sys + +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__": + here = os.path.dirname(os.path.abspath(__file__)) + default = os.path.join(here, "..", "..", "tests", "data", "torsion_oracle_reference.txt") + main(os.path.normpath(sys.argv[1] if len(sys.argv) > 1 else default)) From 818c788783452f0317f5bce872b3bacb1779b83d Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 12:24:42 +0900 Subject: [PATCH 03/26] Solve condensed torsion in the Hermite static equilibrium CD_HermiteCable_Static_Solve takes an optional torsion description (CD_HermiteTorsionType: GJ per element, end frames, end compliances, imposed twist Phi, the accepted Theta). Without it nothing changes. With it, the imposed twist is the last load stage: Phi is ramped from the twist of the untwisted equilibrium (zero torque) in stages of at most pi/4, halved on a failed stage. Each Newton step adds E_t = (Phi - Theta)^2/(2C) and solves the bordered system by Sherman-Morrison on the band factor, with pivot, denominator and backward-error guards and a shifted, refined fallback. Theta is unwrapped against the last accepted iterate and may change by at most pi/2 per accepted step. Every converged stage is tested for stability by the inertia of B + g g^T/C (banded L D L^T and the determinant lemma); an unstable state above a buckling onset is left by descent along the lowest mode and energy minimisation, and one exact zero mode can be allowed for coaxial clamped ends. The energy step handles the rank-one term through the same update. tests/test_hermite_torsion_static.f90 gates pure torsion (exact torque), the clamped Greenhill onsets of van der Heijden et al. (2003) eq. (33) at N = 32 within 1e-5 with fourth-order convergence, the semi-tangential pinned onsets, post-buckling descent, multi-turn twist across the +-pi branch of the holonomy and fail-closed inputs. --- CMakeLists.txt | 7 + src/CableDyn_HermiteCableStatic.f90 | 558 +++++++++++++++++++++++++- src/CableDyn_HermiteTorsion.f90 | 532 ++++++++++++++++++++++++ tests/data/torsion_analytic_refs.txt | 15 + tests/test_hermite_torsion_static.f90 | 490 ++++++++++++++++++++++ 5 files changed, 1594 insertions(+), 8 deletions(-) create mode 100644 tests/data/torsion_analytic_refs.txt create mode 100644 tests/test_hermite_torsion_static.f90 diff --git a/CMakeLists.txt b/CMakeLists.txt index a6e285c..2d9a24b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -569,6 +569,12 @@ 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_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) @@ -1965,6 +1971,7 @@ if(CABLEDYN_ENABLE_COVERAGE) test_line test_hermite_cable test_hermite_torsion_kernel + test_hermite_torsion_static test_hermite_cable_static test_hermite_sequenced test_hermite_resolution diff --git a/src/CableDyn_HermiteCableStatic.f90 b/src/CableDyn_HermiteCableStatic.f90 index 94fa296..178a730 100644 --- a/src/CableDyn_HermiteCableStatic.f90 +++ b/src/CableDyn_HermiteCableStatic.f90 @@ -47,6 +47,11 @@ 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 !$ USE OMP_LIB, ONLY: omp_get_max_threads, omp_in_parallel IMPLICIT NONE PRIVATE @@ -264,7 +269,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 +365,26 @@ 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 (Route A, + !! 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 return torsion holds the accepted Theta, + !! the torque and the stability report. Not combined + !! with pseudo_transient or equilibrate_linear_system. 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 +419,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 +455,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 +670,36 @@ 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 + 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 +707,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 +860,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 +900,34 @@ 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 + torsion%theta = tors_theta_acc + torsion%has_theta = .TRUE. + torsion%torque = tors_mt + torsion%energy = pi_total + 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 +1183,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 +1294,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 +1333,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 +1359,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 +1431,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 +1445,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 +1471,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 +1509,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 +1529,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 +1546,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 +1564,390 @@ 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, or of torsion%theta_hint on a first solve) 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, ref, phi0, total, lam, lam_try, dlam, dlam_max, th_keep + INTEGER :: ks, cuts, wios + LOGICAL :: ok + CHARACTER(200) :: km + CHARACTER(512) :: why + CHARACTER(1024) :: wbuf + ecs = CD_HCSTAT_OK + ecm = '' + 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 + ref = torsion%theta_hint + IF (torsion%has_theta) ref = torsion%theta + tors_theta_acc = CD_HermiteTorsion_Unwrap(raw, ref) + 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 = '' + 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 + 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. 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 + IF (kin /= CD_HTORS_OK) THEN + nk = MERGE(1, 0, tors_lambda < -CD_HTORS_ZERO_MODE_TOL) + torsion%n_negative = nk + END IF + st = tors_lambda > CD_HTORS_ZERO_MODE_TOL .OR. & + (torsion%zero_mode_allowed .AND. nk <= 1 .AND. tors_lambda >= -CD_HTORS_ZERO_MODE_TOL) + END SUBROUTINE torsion_stability + + 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 + 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 index 2bdad98..5b8de57 100644 --- a/src/CableDyn_HermiteTorsion.f90 +++ b/src/CableDyn_HermiteTorsion.f90 @@ -43,6 +43,12 @@ MODULE CableDyn_HermiteTorsion 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 INTEGER, PARAMETER, PUBLIC :: CD_HTORS_OK = 0 INTEGER, PARAMETER, PUBLIC :: CD_HTORS_BADINPUT = 1 @@ -61,9 +67,535 @@ MODULE CableDyn_HermiteTorsion 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 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)]. + TYPE(CD_HermiteTorsionType), INTENT(IN) :: tors + REAL(wp), INTENT(IN) :: l0(:) + c = tors%end_compliance(1) + tors%end_compliance(2) + IF (ALLOCATED(tors%gj)) THEN + IF (SIZE(tors%gj) == SIZE(l0)) c = c + SUM(l0/tors%gj) + END IF + 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) + !! 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. + !! A failure of both leaves x = 0 with a named error. 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 + REAL(wp), ALLOCATABLE :: lu(:, :), rr(:, :), res(:), dx(:) + INTEGER, ALLOCATABLE :: ipiv(:) + REAL(wp) :: sigma, eta, knorm + INTEGER :: n, ldab, attempt, refine + LOGICAL :: ok + CHARACTER(160) :: why + + x = CD_ZERO + IF (PRESENT(backward_error)) backward_error = HUGE(CD_ONE) + IF (PRESENT(shifted)) shifted = .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 + x = CD_ZERO + ErrStat = CD_HTORS_NONFINITE + ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: the bordered torsion step failed ('//TRIM(why)// & + '), also with a shifted factorization and refinement' + + CONTAINS + + 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_STEP. + 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)) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Inertia: inconsistent input' + 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_STEP + 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_STEP + 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)) THEN + ErrStat = CD_HTORS_BADINPUT + ErrMsg = 'CD_HermiteTorsion_Lowest_Mode: inconsistent input' + 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 ! ------------------------------------------------------------------------------------------ diff --git a/tests/data/torsion_analytic_refs.txt b/tests/data/torsion_analytic_refs.txt new file mode 100644 index 0000000..0d20abe --- /dev/null +++ b/tests/data/torsion_analytic_refs.txt @@ -0,0 +1,15 @@ +# Route A 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, the Route A pinned end with torsion restrained): 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 diff --git a/tests/test_hermite_torsion_static.f90 b/tests/test_hermite_torsion_static.f90 new file mode 100644 index 0000000..7cb7f20 --- /dev/null +++ b/tests/test_hermite_torsion_static.f90 @@ -0,0 +1,490 @@ +! 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). + !! 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() + + 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 = MAX(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 = MAX(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') + END SUBROUTINE check_fail_closed + +END PROGRAM test_hermite_torsion_static From 88f3457bd4a4ba0a7d2cb1719fc2bb42ad25e73d Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 12:52:13 +0900 Subject: [PATCH 04/26] Carry condensed torsion in the Hermite cable model and its end loads CD_HermiteCable_Dyn_Set_Torsion (and CD_HFMF_Set_Torsion) installs the torsion of a converged static solve: end frames, GJ, the imposed twist and the accepted Theta, which now passes through snapshot, restore and end. The residual, the support reaction, the end force, the energy and the connection moment include it. The moment adds M_t dTheta/dw of the end frame, so a torsionally restrained end returns its torque to the support, also at a bending-pinned end; at a rigid end the slaved tangent adds its share through the constraint reaction. CD_HermiteCable_Dyn_Torsion_State reports Theta and the torque without advancing the stored state. The time step refuses a model with torsion (statics only in this build). --- src/CableDyn_HermiteCableDynamic.f90 | 193 ++++++++++++++++++++++++++- src/CableDyn_OpenFAST_HermiteFMF.f90 | 30 ++++- 2 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/CableDyn_HermiteCableDynamic.f90 b/src/CableDyn_HermiteCableDynamic.f90 index 2c665d4..d6db854 100644 --- a/src/CableDyn_HermiteCableDynamic.f90 +++ b/src/CableDyn_HermiteCableDynamic.f90 @@ -69,6 +69,9 @@ 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 USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: INT64 !$ USE OMP_LIB, ONLY: omp_get_max_threads, omp_in_parallel IMPLICIT NONE @@ -79,6 +82,8 @@ 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_EndConnection_Moment PUBLIC :: CD_HermiteCable_Dyn_Set_ModifiedNewton PUBLIC :: CD_HermiteCable_Dyn_Set_TangentReuse @@ -311,6 +316,12 @@ 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 and the + ! accepted Theta of the static solve. The residual, the end loads and the energy include it; + ! the time step does not yet (a model with torsion refuses to step). snap_torsion_theta is + ! the Theta of the last snapshot. + TYPE(CD_HermiteTorsionType) :: torsion + REAL(wp) :: snap_torsion_theta = CD_ZERO 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 @@ -2356,6 +2367,11 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i ErrStat = CD_HCDYN_OK ErrMsg = '' nsolve_step = 0 + IF (model%torsion%active) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Step: torsion is supported in statics only in this build' + RETURN + END IF ! Clock scratch is defined even when profiling is off, purely so -Wmaybe-uninitialized ! can prove it (every read sits under the same prof_enabled guard as its write). pt0 = 0_INT64; pc0 = 0_INT64; pcr = 1_INT64 @@ -3519,6 +3535,11 @@ SUBROUTINE CD_HermiteCable_Dyn_Step_Recovering(model, dt, max_iter, tol, ErrStat ErrMsg = 'CD_HermiteCable_Dyn_Step_Recovering: model not initialised' RETURN END IF + IF (model%torsion%active) THEN + ErrStat = CD_HCDYN_BADINPUT + ErrMsg = 'CD_HermiteCable_Dyn_Step_Recovering: torsion is supported in statics only in this build' + 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 @@ -3987,6 +4008,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, gq(model%ndof), eg(6) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, ErrStat, em2, gq, eg) + 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 +4259,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, gq(model%ndof), eg(6) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em2, gq, eg) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = 'CD_HermiteCable_Dyn_End_Force: '//TRIM(em2); RETURN + END IF + force = force + mt*gq(6*(node - 1) + 1:6*(node - 1) + 3) + 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 +4304,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 +4324,23 @@ 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, gq(model%ndof), eg(6) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, gq, 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 +4377,125 @@ 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 + moment = moment + bend_moment 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 refuses a model with torsion (statics only in this build). + 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, eg(6) + REAL(wp), ALLOCATABLE :: gq(:) + 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 + 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 + model%torsion = torsion + ALLOCATE (gq(model%ndof)) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, gq, eg) + 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%snap_torsion_theta = model%torsion%theta + 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) + !! Theta (unwrapped against the stored accepted value), the torque M_t = (Phi - Theta)/C and, + !! optionally, dTheta/dq and dTheta/d[w_1, w_2] 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). + 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), ALLOCATABLE :: g(:) + REAL(wp) :: raw, eg(6), c + INTEGER :: es + CHARACTER(200) :: em + theta = CD_ZERO + torque = CD_ZERO + IF (PRESENT(grad)) grad = CD_ZERO + IF (PRESENT(end_grad)) end_grad = CD_ZERO + ErrStat = CD_HCDYN_OK + ErrMsg = '' + IF (.NOT. model%torsion%active) RETURN + 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 + END SUBROUTINE CD_HermiteCable_Dyn_Torsion_State + 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 +4518,7 @@ 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 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 +4558,7 @@ 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 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 +4646,8 @@ 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() + model%snap_torsion_theta = CD_ZERO model%snap_valid = .FALSE. model%require_tensile = .FALSE. model%tensile_mode = CD_HCDYN_TENSILE_OFF @@ -4913,6 +5088,18 @@ 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 (statics only in this build): R <- R - M_t dTheta/dq at q_cfg. + IF (model%torsion%active .AND. want_residual) THEN + BLOCK + REAL(wp) :: th, mt, gq(model%ndof) + CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em2, gq, q_eval=q_cfg) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = es; ErrMsg = TRIM(em2); RETURN + END IF + R = R - mt*gq + END BLOCK + END IF END SUBROUTINE assemble_residual SUBROUTINE add_endconn_band(R, Kb, q_cfg, node, k_rot, d0, want_residual, want_tangent, & diff --git a/src/CableDyn_OpenFAST_HermiteFMF.f90 b/src/CableDyn_OpenFAST_HermiteFMF.f90 index e49509a..3503d4c 100644 --- a/src/CableDyn_OpenFAST_HermiteFMF.f90 +++ b/src/CableDyn_OpenFAST_HermiteFMF.f90 @@ -39,7 +39,9 @@ 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 + USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType 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 +65,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 @@ -381,6 +384,31 @@ 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) + !! 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. A cable with torsion does not step in + !! this build (statics only). + TYPE(CD_HFMF_ModuleType), INTENT(INOUT) :: self + TYPE(CD_HermiteTorsionType), INTENT(IN) :: torsion + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + INTEGER :: es + 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 + CALL CD_HermiteCable_Dyn_Set_Torsion(self%line, torsion, es, em) + IF (es /= CD_HCDYN_OK) THEN + ErrStat = CD_HFMF_BADINPUT + ErrMsg = 'CD_HFMF_Set_Torsion: '//TRIM(em) + 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 From f3f4c46b5144561eff1fd49ac3216e106b809e2a Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 12:52:13 +0900 Subject: [PATCH 05/26] Read torsional END CONNECTIONS and solve deck torsion in statics END CONNECTIONS rows take optional columns TorsStiffness NxX NxY NxZ [Pretwist]: Free (default), Rigid or a torsional stiffness, the zero-twist reference normal in the frame of Ez, and a roll of the end frame about Ez in degrees. Six-column rows parse as before. Torsion is solved on a line only when both ends are restrained; it needs a finite-EI line with a Fixed End B and an explicit GJ on every section, and is refused on rod ends, non-Rigid6 bodies and with ATTACHMENTS. Malformed rows fail with named errors. On the standalone cubic-Hermite route (TMax 0) a torsional line is solved in 3D, then the imposed twist Phi = Pretwist(B) - Pretwist(A) is ramped on the final mesh with stability checks and buckling descent; the torque is returned to a Rigid6 body at End A, in the output loads and in the body static equilibrium. New channels TorqN (N m), TwistN (cable twist from End A, deg) and Twist (Phi - Theta, deg) are accepted only on torsional lines. Dynamic decks (TMax > 0), modal analysis, the coupled OpenFAST and FAST.Farm entries and the mixed aggregate route refuse torsion by name. tests/test_torsion_deck.f90 gates the torque law and twist channels, byte-identical output with Free or one-end torsion columns, a three-turn pretwist sweep, the body torque return, the moored-body statics and every named refusal. --- CMakeLists.txt | 6 + src/CableDyn_DeckDriver.f90 | 457 +++++++++++++++++++++++++++++++-- tests/test_torsion_deck.f90 | 498 ++++++++++++++++++++++++++++++++++++ 3 files changed, 946 insertions(+), 15 deletions(-) create mode 100644 tests/test_torsion_deck.f90 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d9a24b..caae2c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1663,6 +1663,11 @@ 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") + 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) @@ -1932,6 +1937,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) diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index 2a18209..d1bab94 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 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, & @@ -383,6 +384,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 +404,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 +425,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 @@ -1530,6 +1549,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 +1687,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 +2552,14 @@ 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 + 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 +2679,12 @@ 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 + CALL CD_HFMF_Set_Torsion(cable, tors, es, em) + 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 +2771,81 @@ 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 + 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). + DO attempt = 1, 2 + 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 +4634,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 +4876,11 @@ 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 (nor on the mixed '// & + 'EI = 0 + finite-EI aggregate route or in FAST.Farm); set TorsStiffness Free in the END '// & + 'CONNECTIONS rows'); 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 +5031,13 @@ 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 coupled OpenFAST runs (nor on the mixed '// & + 'EI = 0 + finite-EI aggregate route or in FAST.Farm); set TorsStiffness Free in the END '// & + 'CONNECTIONS rows'); RETURN + END IF + END IF IF (ErrStat /= CD_DECKDRV_OK) RETURN host_dcm = CD_ZERO host_dcm(1, 1) = CD_ONE @@ -5895,6 +6093,23 @@ 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 in statics only in this build: the + ! standalone cubic-Hermite route with TMax = 0, with or without bodies. 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) THEN + CALL fail(ErrStat, ErrMsg, 'torsion is supported in statics only in this build: a deck with torsional '// & + 'END CONNECTIONS at both ends of a line needs TMax 0'); 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. @@ -10811,11 +11026,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 +11044,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 @@ -10881,6 +11110,41 @@ 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 + 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 ALLOCATE (grown(nec + 1), STAT=istat) IF (istat /= 0) THEN @@ -11018,6 +11282,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 +12287,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 +12390,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 +13319,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 +13368,67 @@ 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 + !! a Fixed End B. With both ends restrained (torsion active) 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 + 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 + IF (.NOT. ALL(ln%tors_mode /= TORS_FREE)) RETURN + 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 +16338,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) @@ -20834,7 +21182,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 @@ -27350,6 +27698,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 +27738,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 +27929,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 +27944,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 @@ -29188,6 +29578,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 +30359,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/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 new file mode 100644 index 0000000..713c9a0 --- /dev/null +++ b/tests/test_torsion_deck.f90 @@ -0,0 +1,498 @@ +! 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, or with one end +!> restrained only, writes the same .out bytes as the 6-column rows; +!> 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, a dynamic deck, the coupled entries. +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..6 (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 == 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 (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, 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') + END SUBROUTINE check_backward_compatible + + 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 = MAX(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 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_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') + 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(24) :: '0.1 dtM', '1.0 TMax'], OUTS) + CALL expect_error('a dynamic torsion deck is refused', 'tdeck_e10.dat', 'tdeck_e10', & + 'torsion is supported in statics only in this build') + 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, 'not yet supported in coupled OpenFAST 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) + END SUBROUTINE check_errors + +END PROGRAM test_torsion_deck From 9b481303212c0e8418f4dcd11f8442a8f9f39463 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 12:52:30 +0900 Subject: [PATCH 06/26] Record static torsion in the changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c8b3c..a07a65e 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 + +- Static torsion of finite-EI lines (condensed isotropic torsion): optional `END CONNECTIONS` + columns `TorsStiffness NxX NxY NxZ [Pretwist]` restrain the twist at a line end (`Free`, + `Rigid` or a stiffness) with a reference normal and a pretwist in degrees. A line restrained + at both ends needs an explicit `GJ` and is solved with its imposed twist, a stability check + and buckling descent; the torque is returned to a Rigid6 body at End A. New channels + `TorqN`, `TwistN` and `Twist`. Statics only (`TMax 0`) in the standalone + driver; dynamic, modal and coupled OpenFAST/FAST.Farm runs with torsion stop with an error. + ### Fixed - The static solve of a taut, neutrally buoyant finite-EI line (mass per length equal to From b0e292e0bda7910fee20c9756fb55e8de2f0e154 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 13:27:55 +0900 Subject: [PATCH 07/26] Add an allocation-free workspace to the twist kernel CD_HermiteTorsion_Line takes an optional workspace (CD_HermiteTorsion_Work_Init) for its chain vertices, gauges and Hessian band, so a dynamic step can evaluate the twist without heap allocation. With keep_hessian the unscaled Hessian stays in the workspace and CD_HermiteTorsion_Work_Add_Hessian adds it with a factor that depends on Theta (the torque). The kernel test checks that the workspace path is bit-identical to the allocating one and that a workspace of the wrong size is refused. --- src/CableDyn_HermiteTorsion.f90 | 147 +++++++++++++++++++++++--- tests/test_hermite_torsion_kernel.f90 | 56 +++++++++- 2 files changed, 186 insertions(+), 17 deletions(-) diff --git a/src/CableDyn_HermiteTorsion.f90 b/src/CableDyn_HermiteTorsion.f90 index 5b8de57..3f717c1 100644 --- a/src/CableDyn_HermiteTorsion.f90 +++ b/src/CableDyn_HermiteTorsion.f90 @@ -49,6 +49,10 @@ MODULE CableDyn_HermiteTorsion 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 @@ -114,6 +118,13 @@ MODULE CableDyn_HermiteTorsion 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(:, :) + END TYPE CD_HermiteTorsionWorkType + CONTAINS ! ------------------------------------------------------------------------------------------ @@ -660,30 +671,33 @@ PURE SUBROUTINE CD_HermiteTorsion_Fold_Check(q, Le, ends, margin, ErrStat, ErrMs 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 - CALL line_fold_check(q, Le, ends, order, 'CD_HermiteTorsion_Fold_Check', margin, ErrStat, ErrMsg) + 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) + 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 - REAL(wp), ALLOCATABLE :: u(:, :) 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 - ALLOCATE (u(3, 0:nn + 1)) CALL chain_vertices(q, ends, u) CALL CD_HermiteCable_Gauss_Rule(order, gp, gw) margin = HUGE(CD_ONE) @@ -733,8 +747,59 @@ PURE SUBROUTINE line_fold_check(q, Le, ends, order, who, margin, ErrStat, ErrMsg 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 + END SUBROUTINE CD_HermiteTorsion_Work_End + + PURE SUBROUTINE CD_HermiteTorsion_Work_Add_Hessian(work, hband, scale) + !! 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). + TYPE(CD_HermiteTorsionWorkType), INTENT(IN) :: work + REAL(wp), INTENT(INOUT) :: hband(:, :) + REAL(wp), INTENT(IN) :: scale + INTEGER :: n, i, jj, i0, row0 + n = 6*work%nn + IF (n < 12 .OR. SIZE(hband, 2) /= n .OR. SIZE(hband, 1) < 2*CD_HTORS_KBAND + 1) RETURN + 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) + 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 @@ -755,6 +820,10 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er !! 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) @@ -766,12 +835,60 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er 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' + RETURN + END IF + CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, 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) + 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, MAX(SIZE(q), 1))) + CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, 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, 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 u(3,0:nn+1), pole(3,0:nn), gu(3,0:nn+1), + !! huu(3,3,0:nn+1), huv(3,3,0:nn), band(2 KBAND + 1, >= 6 nn); keep: compute the Hessian + !! into band even without hband. + 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) :: u(:, 0:), pole(:, 0:), gu(:, 0:), huu(:, :, 0:), huv(:, :, 0:), band(:, :) + 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 + 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 - REAL(wp), ALLOCATABLE :: u(:, :), pole(:, :), gu(:, :), huu(:, :, :), huv(:, :, :), band(:, :) CHARACTER(200) :: em theta_raw = CD_ZERO @@ -784,9 +901,9 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er 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) + 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) + 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 @@ -808,10 +925,8 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er RETURN END IF - ALLOCATE (u(3, 0:nn + 1), pole(3, 0:nn), gu(3, 0:nn + 1), huu(3, 3, 0:nn + 1), huv(3, 3, 0:nn)) - ALLOCATE (band(2*CD_HTORS_KBAND + 1, n)) row0 = CD_HTORS_KBAND + 1 ! diagonal row of the local band - band = CD_ZERO + IF (want_hess) band(:, 1:n) = CD_ZERO CALL chain_vertices(q, ends, u) ! element holonomies @@ -909,8 +1024,10 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er ec(:, :, 2) = MATMUL(sd, MATMUL(TRANSPOSE(huv(:, :, nn)), pk)) END IF - IF (.NOT. (CD_Is_Finite(theta_raw) .AND. CD_All_Finite(grad) .AND. CD_All_Finite(band) .AND. & - CD_All_Finite(eg) .AND. CD_All_Finite(RESHAPE(eh, [18])) .AND. CD_All_Finite(RESHAPE(ec, [18])))) THEN + 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 @@ -932,7 +1049,7 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er IF (PRESENT(fold_margin)) fold_margin = margin ErrStat = CD_HTORS_OK ErrMsg = '' - END SUBROUTINE CD_HermiteTorsion_Line + 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). diff --git a/tests/test_hermite_torsion_kernel.f90 b/tests/test_hermite_torsion_kernel.f90 index 55ef652..5562d88 100644 --- a/tests/test_hermite_torsion_kernel.f90 +++ b/tests/test_hermite_torsion_kernel.f90 @@ -14,13 +14,16 @@ PROGRAM test_hermite_torsion_kernel !! 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. + !! E the element routine against finite differences and against the line assembly, + !! W the allocation-free workspace evaluation (bit-identical, kept Hessian, size checks). !! 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_HTORS_KBAND, CD_HTORS_PI, CD_HermiteTorsionWorkType, & + CD_HermiteTorsion_Work_Init, CD_HermiteTorsion_Work_End, & + CD_HermiteTorsion_Work_Add_Hessian IMPLICIT NONE INTEGER, PARAMETER :: KB = CD_HTORS_KBAND, LDAB = 3*KB + 1 @@ -38,6 +41,7 @@ PROGRAM test_hermite_torsion_kernel 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() @@ -396,6 +400,54 @@ SUBROUTINE check_band_layout() '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) + 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') + 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 ! --------------------------------------------------------------------------------------- From fd67ad46abc7c8a98e930efbeecfd58a3402d59f Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 14:27:16 +0900 Subject: [PATCH 08/26] Step condensed torsion in the Hermite cable dynamics The generalised-alpha step now carries condensed torsion instead of refusing it. The residual adds -M_t dTheta/dq and the tangent -M_t d2Theta/dq2; the rank-one (1 - alpha_f) g g^T / C joins the solve by Sherman-Morrison on the band factor, with g, K^-1 g and the denominator kept with the factor so that a reused factor stays consistent. Theta is unwrapped against the committed value; a step that would move it by more than pi/2 fails its evaluation, so the line search or the recovering step cuts it. Torsion is quasi-static (no torsional inertia) and needs the force-blended generalised-alpha. CD_HermiteCable_Dyn_Set_Torsion_Drive sets the end frames and Phi at the end of the next step (the recovering step interpolates the frames on SO(3) and Phi linearly over its substeps); the commit advances them with Theta. CD_HermiteCable_Dyn_Set_Torsion_Frames moves the committed frames between steps. Snapshot and restore carry Theta, Phi and the frames. The kernel evaluates in a model-owned, explicit-shape workspace, so a step allocates nothing. CD_HFMF_Set_Torsion can attach the coupled end's torsion frame to its parent: UpdateStates and SetCoupledKinematics turn it with the parent orientation (also at a bending-pinned end), and UpdateStates takes the imposed twist u_twist. The checkpoint mirror gains Theta, Phi and the frames when torsion is active (unchanged otherwise). tests/test_hermite_torsion_dynamic.f90 gates the step response against the quasi-static torque, a parent turning 1.5 turns, energy conservation at rho_inf = 1, bit-identical snapshot and mirror restarts past one turn, and the bordered step; test_step_allocations gains a torsion mode. --- CMakeLists.txt | 11 +- src/CableDyn_HermiteCableDynamic.f90 | 489 +++++++++++++++++++++++-- src/CableDyn_HermiteTorsion.f90 | 18 +- src/CableDyn_OpenFAST_HermiteFMF.f90 | 163 ++++++++- tests/test_hermite_torsion_dynamic.f90 | 457 +++++++++++++++++++++++ tests/test_step_allocations.f90 | 86 +++++ 6 files changed, 1175 insertions(+), 49 deletions(-) create mode 100644 tests/test_hermite_torsion_dynamic.f90 diff --git a/CMakeLists.txt b/CMakeLists.txt index caae2c6..4c1be16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -575,6 +575,11 @@ 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_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) @@ -1193,6 +1198,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) @@ -1666,7 +1674,7 @@ set_tests_properties(clamped_cable_ends PROPERTIES LABELS "fortran;driver;integr 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") +set_tests_properties(torsion_deck PROPERTIES LABELS "fortran;driver;integration;static;dynamic") add_executable(test_rod_hydro tests/test_rod_hydro.f90) target_link_libraries(test_rod_hydro PRIVATE cabledyn_core) @@ -1978,6 +1986,7 @@ if(CABLEDYN_ENABLE_COVERAGE) 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/src/CableDyn_HermiteCableDynamic.f90 b/src/CableDyn_HermiteCableDynamic.f90 index d6db854..9376e08 100644 --- a/src/CableDyn_HermiteCableDynamic.f90 +++ b/src/CableDyn_HermiteCableDynamic.f90 @@ -71,7 +71,8 @@ MODULE CableDyn_HermiteCableDynamic 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_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 @@ -84,6 +85,8 @@ MODULE CableDyn_HermiteCableDynamic 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 @@ -316,12 +319,25 @@ 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 and the - ! accepted Theta of the static solve. The residual, the end loads and the energy include it; - ! the time step does not yet (a model with torsion refuses to step). snap_torsion_theta is - ! the Theta of the last snapshot. + ! 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 - REAL(wp) :: snap_torsion_theta = CD_ZERO + 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 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 @@ -2304,6 +2320,139 @@ 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) + !! 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. + TYPE(CD_HermiteCableDynType), INTENT(INOUT) :: model + REAL(wp), INTENT(IN) :: ends(3, 4) + INTEGER, INTENT(OUT) :: ErrStat + CHARACTER(*), INTENT(OUT) :: ErrMsg + REAL(wp) :: th + CALL CD_HermiteCable_Dyn_Set_Torsion_Drive(model, ends, model%torsion%phi, ErrStat, ErrMsg) + model%tors_drive_set = .FALSE. + IF (ErrStat /= CD_HCDYN_OK) RETURN + 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 + 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 @@ -2324,7 +2473,7 @@ 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) :: 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 @@ -2367,11 +2516,6 @@ SUBROUTINE CD_HermiteCable_Dyn_Step(model, dt, max_iter, tol, ErrStat, ErrMsg, i ErrStat = CD_HCDYN_OK ErrMsg = '' nsolve_step = 0 - IF (model%torsion%active) THEN - ErrStat = CD_HCDYN_BADINPUT - ErrMsg = 'CD_HermiteCable_Dyn_Step: torsion is supported in statics only in this build' - RETURN - END IF ! Clock scratch is defined even when profiling is off, purely so -Wmaybe-uninitialized ! can prove it (every read sits under the same prof_enabled guard as its write). pt0 = 0_INT64; pc0 = 0_INT64; pcr = 1_INT64 @@ -2574,6 +2718,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 @@ -2773,6 +2920,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. @@ -2803,6 +2961,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 @@ -2982,6 +3141,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) @@ -3097,6 +3257,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 @@ -3139,6 +3310,13 @@ 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) + 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 @@ -3449,7 +3627,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 @@ -3522,6 +3777,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). @@ -3535,12 +3791,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 - IF (model%torsion%active) THEN - ErrStat = CD_HCDYN_BADINPUT - ErrMsg = 'CD_HermiteCable_Dyn_Step_Recovering: torsion is supported in statics only in this build' - 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 @@ -3615,6 +3865,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 @@ -3690,9 +3951,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, & @@ -3752,6 +4027,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) @@ -3791,6 +4067,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 @@ -3810,6 +4087,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) @@ -3891,6 +4180,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 @@ -4393,7 +4732,8 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) !! 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 refuses a model with torsion (statics only in this build). + !! 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 @@ -4425,6 +4765,21 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) 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)) + 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 ALLOCATE (gq(model%ndof)) CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, gq, eg) @@ -4437,7 +4792,13 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) 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) @@ -4519,6 +4880,8 @@ SUBROUTINE CD_HermiteCable_Dyn_Snapshot(model, ErrStat, ErrMsg) 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 @@ -4559,6 +4922,12 @@ SUBROUTINE CD_HermiteCable_Dyn_Restore(model, ErrStat, ErrMsg) 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 @@ -4647,7 +5016,18 @@ SUBROUTINE CD_HermiteCable_Dyn_End(model) 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) + 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 @@ -5089,19 +5469,68 @@ SUBROUTINE assemble_residual(model, q_cfg, v_cfg, t_eval, want_residual, want_ta END DO END IF - ! Condensed torsion (statics only in this build): R <- R - M_t dTheta/dq at q_cfg. - IF (model%torsion%active .AND. want_residual) THEN - BLOCK - REAL(wp) :: th, mt, gq(model%ndof) - CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em2, gq, q_eval=q_cfg) - IF (es /= CD_HCDYN_OK) THEN - ErrStat = es; ErrMsg = TRIM(em2); RETURN - END IF - R = R - mt*gq - END BLOCK + ! 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) CALL CD_HermiteTorsion_Work_Add_Hessian(model%tors_work, model%ws_Kb, -mt) + 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_HermiteTorsion.f90 b/src/CableDyn_HermiteTorsion.f90 index 3f717c1..72c97c1 100644 --- a/src/CableDyn_HermiteTorsion.f90 +++ b/src/CableDyn_HermiteTorsion.f90 @@ -855,29 +855,31 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er ErrMsg = 'CD_HermiteTorsion_Line: the workspace is not sized for this line' RETURN END IF - CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, work%u, work%pole, work%gu, work%huu, & + 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) 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, MAX(SIZE(q), 1))) - CALL line_core(q, Le, ends, theta_raw, grad, ErrStat, ErrMsg, local%u, local%pole, local%gu, local%huu, & + 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, u, pole, gu, huu, huv, band, keep, & + 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 u(3,0:nn+1), pole(3,0:nn), gu(3,0:nn+1), - !! huu(3,3,0:nn+1), huv(3,3,0:nn), band(2 KBAND + 1, >= 6 nn); keep: compute the Hessian - !! into band even without hband. + !! 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 - REAL(wp), INTENT(INOUT) :: u(:, 0:), pole(:, 0:), gu(:, 0:), huu(:, :, 0:), huv(:, :, 0:), band(:, :) + 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 diff --git a/src/CableDyn_OpenFAST_HermiteFMF.f90 b/src/CableDyn_OpenFAST_HermiteFMF.f90 index 3503d4c..ec04a41 100644 --- a/src/CableDyn_OpenFAST_HermiteFMF.f90 +++ b/src/CableDyn_OpenFAST_HermiteFMF.f90 @@ -40,8 +40,9 @@ MODULE CableDyn_OpenFAST_HermiteFMF CD_HermiteCable_Dyn_Curvature, & CD_HermiteCable_Dyn_Snapshot, CD_HermiteCable_Dyn_Restore, & CD_HermiteCable_Dyn_End, CD_HCDYN_OK, & - CD_HermiteCable_Dyn_Set_Torsion - USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType + 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 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 @@ -123,6 +124,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 @@ -384,16 +392,22 @@ 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) + 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. A cable with torsion does not step in - !! this build (statics only). + !! 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 - INTEGER :: es + 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 = '' @@ -402,10 +416,68 @@ SUBROUTINE CD_HFMF_Set_Torsion(self, torsion, ErrStat, ErrMsg) 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 - ErrStat = CD_HFMF_BADINPUT 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 @@ -560,16 +632,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 @@ -623,6 +699,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. @@ -642,7 +740,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 @@ -768,6 +866,29 @@ 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) + BLOCK + REAL(wp) :: tors_ends(3, 4) + INTEGER :: es + tors_ends = self%line%torsion%ends + 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))) + 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 + END BLOCK + 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 @@ -1282,9 +1403,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 @@ -1354,6 +1484,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) @@ -1422,6 +1557,14 @@ 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. + END IF END SUBROUTINE CD_HFMF_UnpackMirror SUBROUTINE CD_HFMF_End(self) diff --git a/tests/test_hermite_torsion_dynamic.f90 b/tests/test_hermite_torsion_dynamic.f90 new file mode 100644 index 0000000..33820c3 --- /dev/null +++ b/tests/test_hermite_torsion_dynamic.f90 @@ -0,0 +1,457 @@ +! 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 (Route A) 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. Route A 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; + !! 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 + 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_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 = MAX(worst, ABS(mt - mref)/mref) + bent = MAX(bent, MAXVAL(ABS(cab%line%q(2:NDOF:6))), MAXVAL(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 = MAX(err_th, ABS(th - (th0 - psi))) + err_mt = MAX(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 = MAX(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 = MAX(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_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_step_allocations.f90 b/tests/test_step_allocations.f90 index 9bce06b..d903c7a 100644 --- a/tests/test_step_allocations.f90 +++ b/tests/test_step_allocations.f90 @@ -16,9 +16,19 @@ 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. + !! !! 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 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 +57,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 +164,76 @@ 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 + INTEGER :: k + INTEGER(C_LONG_LONG) :: c0, n_alloc + 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) + CALL CD_HFMF_CalcOutput(cab, f, es, em, y_moment=m) + CALL check('torsion cable output') + END DO + CALL CD_HFMF_End(cab) + PRINT '(A,I0,A,I0,A)', 'torsion cable: ', N_COUNT, ' counted steps, ', n_alloc, ' heap allocations in the steps' + IF (n_alloc /= 0) THEN + PRINT '(A)', 'FAIL: the per-step advance 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 of a cable with torsion after warm-up' + END SUBROUTINE torsion_gate + SUBROUTINE check(stage) CHARACTER(*), INTENT(IN) :: stage From 6706b736c85b49d945215b7d67f08beb7c56673b Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 14:27:52 +0900 Subject: [PATCH 09/26] Run deck torsion in dynamics with turning ends and a motionFile roll A deck with torsional END CONNECTIONS at both ends of a line now runs dynamically on the standalone cubic-Hermite route and the multibody march (it needs the force-blended generalised-alpha; modal analysis and the coupled OpenFAST and FAST.Farm entries still refuse torsion by name). End A's torsion frame (-Ez, Nx) is attached to its parent, so a turning body or vessel turns it, also at a bending-pinned end. motionFile point rows take an optional 12th column on a deck with a torsional line: the roll of the line end frames at that point about Ez in degrees, added to the End A pretwist (Phi = Pretwist(B) - Pretwist(A) - roll), given on every row of a point or on none, starting at 0 and only at the End A point of a torsional line (named errors otherwise). Torsion has no inertia here, so the torque follows the roll at once. Range graphs of a torsional line add Torque and Twist (from End A) envelopes; other lines' range files are unchanged. tests/test_torsion_deck.f90 gains a roll-column step and a vessel rolling about the line axis (Twist = -roll, quasi-static torque at every step), the range envelopes, a body swinging as a torsional pendulum through two turns at sqrt(GJ / (L I)) with the connection moment along the axis, and the refusals of the configuration blend and of roll-column misuse. --- src/CableDyn_DeckDriver.f90 | 138 +++++++++++++++++-- src/CableDyn_RangeOutput.f90 | 40 ++++-- tests/test_torsion_deck.f90 | 249 ++++++++++++++++++++++++++++++++++- 3 files changed, 401 insertions(+), 26 deletions(-) diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index d1bab94..f72db11 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -133,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 @@ -658,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 @@ -2680,7 +2686,15 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, END IF END IF IF (tors_line) THEN - CALL CD_HFMF_Set_Torsion(cable, tors, es, em) + ! 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 @@ -6093,18 +6107,19 @@ 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 in statics only in this build: the - ! standalone cubic-Hermite route with TMax = 0, with or without bodies. One restrained end - ! carries no torque (the other end is free to twist), so it is noted and ignored. + ! 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) THEN - CALL fail(ErrStat, ErrMsg, 'torsion is supported in statics only in this build: a deck with torsional '// & - 'END CONNECTIONS at both ends of a line needs TMax 0'); RETURN + 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 @@ -23443,7 +23458,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 @@ -23519,6 +23537,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 @@ -23586,6 +23610,37 @@ 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 End A point only, and + !! starts from the static state (roll 0 at t = 0; a constant twist is the Pretwist). + 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 End A 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) @@ -26811,7 +26866,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 @@ -26820,8 +26880,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 @@ -26835,6 +26899,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') @@ -26866,6 +26932,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 @@ -27046,8 +27119,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') @@ -28596,6 +28696,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, & @@ -28840,6 +28947,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) 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/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 713c9a0..7d459d5 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -17,7 +17,11 @@ !> 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, a dynamic deck, the coupled entries. +!> 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; PROGRAM test_torsion_deck USE CableDyn_Precision, ONLY: wp USE CableDyn_Conventions, ONLY: CD_Body_Rotation @@ -38,7 +42,7 @@ PROGRAM test_torsion_deck 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..6 (diagnostics) + ! optional argument: run only gate 1..8 (diagnostics) only = 0 IF (COMMAND_ARGUMENT_COUNT() > 0) THEN CALL GET_COMMAND_ARGUMENT(1, arg) @@ -50,11 +54,13 @@ PROGRAM test_torsion_deck 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() 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, channels, body torque and refusals' + WRITE (*, '(A)') 'PASS: torsion deck columns, statics, dynamics, channels, body torque and refusals' CONTAINS @@ -428,6 +434,214 @@ SUBROUTINE check_body_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 = MAX(werr_a, ABS(v(4) + roll)) + terr = MAX(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 = MAX(werr_b, ABS(v(4) + roll)) + terr = MAX(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() + !! 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. + 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 + LOGICAL :: ok + CHARACTER(512) :: em + INTEGER :: k + 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('tdeck_broll.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'], & + [CHARACTER(256) :: '1 A Rigid 0.8 0 -0.6 Rigid 0 1 0 0', 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('tdeck_broll.dat', 'tdeck_broll', ok, em) + CALL CD_Multibody_Probe_Get(rec) + ok = ok .AND. ALLOCATED(rec) + CALL require(ok, '8: the rolling-body deck 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('tdeck_broll.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 = MAX(err_axis, NORM2(rec(24:26, k) - mt*dk)/m0) + err_wave = MAX(err_wave, ABS(mt - (m0*COS(omega*t) - amp*SIN(omega*t)))/m0) + END DO + CALL require(ok, '8: all output rows readable') + WRITE (*, '(A,ES10.3,A,ES10.3,A,F8.4,A)') 'body roll dynamics: 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: the connection moment on the body is the torque along the axis') + CALL require(err_wave <= 1.0e-2_wp, '8: the body swings as a torsional pendulum at sqrt(GJ/(L I))') + END SUBROUTINE check_body_roll_dynamics + + ! ------------------------------------------------------------------------------------------ SUBROUTINE expect_error(label, path, root, needle) CHARACTER(*), INTENT(IN) :: label, path, root, needle @@ -437,6 +651,25 @@ SUBROUTINE expect_error(label, path, root, needle) CALL require(.NOT. ok .AND. INDEX(em, needle) > 0, label//' (got: '//TRIM(em)//')') END SUBROUTINE expect_error + 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) @@ -470,9 +703,13 @@ SUBROUTINE check_errors() [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', '1.0 TMax'], OUTS) - CALL expect_error('a dynamic torsion deck is refused', 'tdeck_e10.dat', 'tdeck_e10', & - 'torsion is supported in statics only in this build') + [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'], & From 137938bc608864446a2a0a45f71b7df054cb98cb Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 14:27:59 +0900 Subject: [PATCH 10/26] Couple the cable torque into the static body equilibrium The static body solve returned a line's torque as a fixed load each pass, which converged only when the body's own restoring stiffness was far above GJ/L. Each pass now also gives the body solve the torque's moment stiffness (1/C) a a^T about the previous pass's pose (a = dTheta/dw of the cable with its shape held: the End A frame turns, a clamped end's tangent with it). The own-load Jacobian carries it into the Newton step and the stability test, so the passes converge whatever the ratio of the body's restoring stiffness to GJ/L, to 1e-10 of the force scale. Gate 9 of tests/test_torsion_deck.f90: a buoyant body held by a vertical twisted cable with no yaw restoring at all yaws until the cable is untwisted, and with two legs whose yaw stiffness is of the order of GJ/L the two share the twist; Twist = 90 deg + yaw, and the static net moment on the body is below 1e-11 of the torque, in three passes. --- src/CableDyn_DeckDriver.f90 | 76 +++++++++++++++++++++++++++++------ tests/test_torsion_deck.f90 | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 13 deletions(-) diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index f72db11..463d487 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -19663,7 +19663,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 @@ -19730,6 +19730,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(:, :) @@ -20397,6 +20402,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. @@ -21163,7 +21172,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(:) @@ -21178,15 +21192,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 @@ -21226,23 +21241,33 @@ 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) + quiet=pass > 1, skipped=held, ext_km=ext_km, ext_rot=ext_rot) 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)))) @@ -21250,9 +21275,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 '// & @@ -21266,11 +21293,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(:) @@ -21279,8 +21309,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 @@ -21295,6 +21327,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 diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 7d459d5..3eec3a6 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -22,6 +22,8 @@ !> 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. PROGRAM test_torsion_deck USE CableDyn_Precision, ONLY: wp USE CableDyn_Conventions, ONLY: CD_Body_Rotation @@ -42,7 +44,7 @@ PROGRAM test_torsion_deck 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..8 (diagnostics) + ! optional argument: run only gate 1..9 (diagnostics) only = 0 IF (COMMAND_ARGUMENT_COUNT() > 0) THEN CALL GET_COMMAND_ARGUMENT(1, arg) @@ -56,6 +58,7 @@ PROGRAM test_torsion_deck 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() + IF (only == 0 .OR. only == 9) CALL check_body_yaw_statics() IF (n_fail > 0) THEN WRITE (*, '(A,I0,A)') 'FAIL: ', n_fail, ' torsion deck gate(s) failed' ERROR STOP 1 @@ -641,6 +644,80 @@ SUBROUTINE check_body_roll_dynamics() CALL require(err_wave <= 1.0e-2_wp, '8: the body swings as a torsional pendulum at sqrt(GJ/(L I))') END SUBROUTINE check_body_roll_dynamics + ! ------------------------------------------------------------------------------------------ + 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) From 5809d98044f185bdaf1a1c9379553bd6dfc109bb Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 14:28:07 +0900 Subject: [PATCH 11/26] Accept the torsion columns and channels in the Python deck tools DeckFile validates the optional END CONNECTIONS torsion columns (TorsStiffness NxX NxY NxZ [Pretwist]) with the native rules: 6, 10 or 11 columns, Free/Rigid or a non-negative stiffness, a non-zero reference normal not parallel to Ez, and the line rules of a torsional line (finite EI, Fixed End B, explicit GJ, no attachments, no rod end, a Rigid6 body), the force blend for a dynamic run, no modal analysis and no coupled route. The TorqN, TwistN and Twist channels are recognised (one identity per quantity) and accepted only on a line restrained at both ends. DeckModel's EndConnection carries the optional torsion columns and writes them back, and add_end_connection takes them as keyword arguments. The parity tests pin each rule. --- python/cabledyn/builder.py | 75 +++++++- python/cabledyn/deck_file.py | 228 +++++++++++++++++++++++- python/tests/test_deck_native_parity.py | 172 ++++++++++++++++++ 3 files changed, 463 insertions(+), 12 deletions(-) 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_file.py b/python/cabledyn/deck_file.py index dcefd40..a409c91 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,8 @@ "cpld": "coupled", } _RIGID_END_CONNECTIONS = frozenset({"rigid", "infinity", "inf"}) +# 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. @@ -629,7 +643,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 +714,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 +810,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 +2403,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 +2487,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 +2846,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: @@ -2872,6 +2901,53 @@ 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 = row.tokens[6].lower() + if torsion_token in {"free", "zero"}: + restrained = False + elif torsion_token in _RIGID_END_CONNECTIONS: + restrained = True + else: + try: + torsion_k = _table_float(row.tokens[6]) + except ValueError: + torsion_k = -1.0 + if not math.isfinite(torsion_k) 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") + 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 / 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" + ) + torsion_ends.setdefault(line_id, {})[end] = restrained if non_pinned: has_finite_ei = any( line_type_meta[line_type][3] > 0.0 @@ -2952,6 +3028,17 @@ 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, + ) rod_types = self._rows("ROD TYPES") for row in rod_types: @@ -3605,6 +3692,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 +3746,106 @@ def is_body_kind(kind: str, pinned: bool) -> bool: continue raise DeckFormatError(f"{self._label}: unsupported OUTPUT channel {channel!r}") + 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, DeckRecord], + has_tmax: 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 a Fixed End B. Restrained at both ends, torsion is + solved: 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 + refuse any torsion column. + """ + 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() + for line_id, ends in torsion_ends.items(): + if not any(ends.values()): + continue + if self.caller_driven: + raise DeckFormatError( + f"{self._label}: torsion is not yet supported in coupled OpenFAST runs (nor on " + "the mixed EI = 0 + finite-EI aggregate route or in FAST.Farm); set " + "TorsStiffness Free in the END CONNECTIONS rows" + ) + 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" + ) + 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" + ) + if not (ends.get("a", False) and ends.get("b", False)): + continue + 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) + 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: diff --git a/python/tests/test_deck_native_parity.py b/python/tests/test_deck_native_parity.py index 8da0362..0d66481 100644 --- a/python/tests/test_deck_native_parity.py +++ b/python/tests/test_deck_native_parity.py @@ -757,3 +757,175 @@ 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 native torsion deck gate's pure-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, "not yet supported in coupled OpenFAST runs", caller_driven=True) + + +@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() From 71fcaa33b29f2cbb25fbd074eebd1ab2c07d0ea3 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 14:47:04 +0900 Subject: [PATCH 12/26] Record torsion dynamics in the changelog The Unreleased torsion entry now covers dynamics (quasi-static torsion with turning end frames and a motionFile roll column), the body torque in statics and dynamics, the range envelopes and the Python deck tools. --- CHANGELOG.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a07a65e..e2fb5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,17 @@ All notable changes to CableDyn are recorded in this file. The format follows ### Added -- Static torsion of finite-EI lines (condensed isotropic torsion): optional `END CONNECTIONS` - columns `TorsStiffness NxX NxY NxZ [Pretwist]` restrain the twist at a line end (`Free`, - `Rigid` or a stiffness) with a reference normal and a pretwist in degrees. A line restrained - at both ends needs an explicit `GJ` and is solved with its imposed twist, a stability check - and buckling descent; the torque is returned to a Rigid6 body at End A. New channels - `TorqN`, `TwistN` and `Twist`. Statics only (`TMax 0`) in the standalone - driver; dynamic, modal and coupled OpenFAST/FAST.Farm runs with torsion stop with an error. +- Torsion of finite-EI lines (condensed isotropic torsion, quasi-static: no torsional + inertia): optional `END CONNECTIONS` columns `TorsStiffness NxX NxY NxZ [Pretwist]` restrain + the twist at a line end (`Free`, `Rigid` or a stiffness) with a reference normal and a + pretwist in degrees. A line restrained at both ends needs an explicit `GJ`. Statics solve the + imposed twist with a stability check and buckling descent; dynamics carry the torque in every + step, with End A's frame turning with its body or vessel and an optional `motionFile` roll + column for an imposed twist history. The torque is returned to a Rigid6 body at End A, in + statics (for any ratio of the body's restoring stiffness to GJ/L) and in dynamics. New + channels `TorqN`, `TwistN` and `Twist`, and Torque and Twist range envelopes. + The Python deck tools accept the new columns and channels. Modal analysis and coupled + OpenFAST/FAST.Farm runs with torsion stop with an error. ### Fixed From 36c0ebe3c3023ccfcea2c6f73256bb5e5b7c67bb Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:15 +0900 Subject: [PATCH 13/26] Gate torsion on a Kirchhoff helix, localised onsets, the Cosserat path and the bordered step A new validation test checks zero-twist parity on a three-dimensional sagging line, a clamped four-turn Kirchhoff helix against its helical wrench, the clamped onset at T L^2/EI = 400 and 1600 against eq. (33), a bent and twisted rod against the Cosserat rod path, and the rank-one term of the bordered Newton step on post-buckled states (linear convergence at the predicted rate without it). --- CMakeLists.txt | 5 + tests/data/torsion_analytic_refs.txt | 10 + tests/test_torsion_validation.f90 | 727 +++++++++++++++++++++++++++ 3 files changed, 742 insertions(+) create mode 100644 tests/test_torsion_validation.f90 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c1be16..e71590b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -575,6 +575,11 @@ 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) diff --git a/tests/data/torsion_analytic_refs.txt b/tests/data/torsion_analytic_refs.txt index 0d20abe..6c1cbb1 100644 --- a/tests/data/torsion_analytic_refs.txt +++ b/tests/data/torsion_analytic_refs.txt @@ -13,3 +13,13 @@ 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/test_torsion_validation.f90 b/tests/test_torsion_validation.f90 new file mode 100644 index 0000000..5bef19f --- /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 = MAX(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 From cea2d424fe95eadbf5482903168b83a7363b6658 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:23 +0900 Subject: [PATCH 14/26] Compare torsion with OrcaFlex summary values test_torsion_orcaflex checks pure torsion through the deck, the clamped and semi-tangential buckling onsets, and the Gulf of Mexico 80 m lazy-wave cable twisted up to five turns at the hang-off against OrcaFlex 11.6d summary values. validation/scripts/orcaflex_torsion_reference.py regenerates them (summary values and settings only). --- CMakeLists.txt | 6 + tests/data/torsion_orcaflex_refs.txt | 41 + tests/test_torsion_orcaflex.f90 | 491 +++++++++++ validation/README.md | 1 + .../scripts/orcaflex_torsion_reference.py | 771 ++++++++++++++++++ 5 files changed, 1310 insertions(+) create mode 100644 tests/data/torsion_orcaflex_refs.txt create mode 100644 tests/test_torsion_orcaflex.f90 create mode 100644 validation/scripts/orcaflex_torsion_reference.py diff --git a/CMakeLists.txt b/CMakeLists.txt index e71590b..4502707 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1681,6 +1681,12 @@ 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) diff --git a/tests/data/torsion_orcaflex_refs.txt b/tests/data/torsion_orcaflex_refs.txt new file mode 100644 index 0000000..2f4eb8a --- /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 5d98aba2157587ef...). 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_torsion_orcaflex.f90 b/tests/test_torsion_orcaflex.f90 new file mode 100644 index 0000000..b7a373d --- /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) = MAX(worst(1), etq) + worst(2) = MAX(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) = MAX(worst(3), ekap) + worst(4) = MAX(worst(4), ey) + worst(5) = MAX(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/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/orcaflex_torsion_reference.py b/validation/scripts/orcaflex_torsion_reference.py new file mode 100644 index 0000000..e843613 --- /dev/null +++ b/validation/scripts/orcaflex_torsion_reference.py @@ -0,0 +1,771 @@ +# 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 + +import OrcFxAPI as ofx + +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 run(cases: list[str], out_dir: str) -> str: + """Run the OrcaFlex cases and write (update) the JSON summary; return its path.""" + 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() From 28bc3578983bffd5e7355fe854c2d91ee6ff78d1 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:23 +0900 Subject: [PATCH 15/26] Test a bending-pinned torsional end on a turning body and the torsion scope refusals The rolling-body gate also runs with a bending-pinned End A, and a rod end, a Point3 body and ATTACHMENTS on a torsional line stop by name. --- tests/test_torsion_deck.f90 | 105 ++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 3eec3a6..01c3ed1 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -23,7 +23,9 @@ !> 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. +!> 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. PROGRAM test_torsion_deck USE CableDyn_Precision, ONLY: wp USE CableDyn_Conventions, ONLY: CD_Body_Rotation @@ -57,7 +59,8 @@ PROGRAM test_torsion_deck 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() + 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 (n_fail > 0) THEN WRITE (*, '(A,I0,A)') 'FAIL: ', n_fail, ' torsion deck gate(s) failed' @@ -587,39 +590,44 @@ SUBROUTINE insert_option(path, row) END SUBROUTINE insert_option ! ------------------------------------------------------------------------------------------ - SUBROUTINE check_body_roll_dynamics() + 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. + !! 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 + 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('tdeck_broll.dat', [neutral_type], [brow], & + 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'], & - [CHARACTER(256) :: '1 A Rigid 0.8 0 -0.6 Rigid 0 1 0 0', cb], & + [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('tdeck_broll.dat', 'tdeck_broll', ok, em) + 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: the rolling-body deck runs and is probed: '//TRIM(em)) + 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 @@ -628,7 +636,7 @@ SUBROUTINE check_body_roll_dynamics() err_axis = 0.0_wp err_wave = 0.0_wp DO k = 0, NSTEP - CALL read_row('tdeck_broll.out', k, v, ok) + CALL read_row(TRIM(root)//'.out', k, v, ok) IF (.NOT. ok) EXIT t = v(1) mt = v(2) @@ -637,11 +645,12 @@ SUBROUTINE check_body_roll_dynamics() err_axis = MAX(err_axis, NORM2(rec(24:26, k) - mt*dk)/m0) err_wave = MAX(err_wave, ABS(mt - (m0*COS(omega*t) - amp*SIN(omega*t)))/m0) END DO - CALL require(ok, '8: all output rows readable') - WRITE (*, '(A,ES10.3,A,ES10.3,A,F8.4,A)') 'body roll dynamics: 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: the connection moment on the body is the torque along the axis') - CALL require(err_wave <= 1.0e-2_wp, '8: the body swings as a torsional pendulum at sqrt(GJ/(L I))') + 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 ! ------------------------------------------------------------------------------------------ @@ -807,6 +816,72 @@ SUBROUTINE check_errors() 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 From c2326ba64741c67679324670aa4b57833238be9a Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:24 +0900 Subject: [PATCH 16/26] Report torque and twist units and range graphs in the Python tools read_output attaches N-m and deg to the Torq and Twist channels, read_range_graphs returns the torque and twist envelopes, and the torsion option check takes option records. --- python/cabledyn/deck_file.py | 2 +- python/cabledyn/ranges.py | 18 ++++++++++++------ python/cabledyn/results.py | 2 ++ python/tests/test_checks_ranges.py | 24 +++++++++++++++++++++++- python/tests/test_results.py | 3 +++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/python/cabledyn/deck_file.py b/python/cabledyn/deck_file.py index a409c91..da7e78b 100644 --- a/python/cabledyn/deck_file.py +++ b/python/cabledyn/deck_file.py @@ -3755,7 +3755,7 @@ def _check_torsion_lines( normalized_line_endpoints: dict[str, tuple[str, str]], point_types: dict[str, str], body_types: dict[str, str], - option_by_key: dict[str, DeckRecord], + option_by_key: dict[str, OptionRecord], has_tmax: bool, ) -> set[str]: """Native ``check_torsion_line`` and the driver's torsion scope; the torsional lines. 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_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"), From 12ba70923d7cfed3b66d6604725f5dc2fea5f39d Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:29 +0900 Subject: [PATCH 17/26] Add a lazy-wave torsion example driven by the motionFile roll column torsion_lazy_wave_hangoff_twist.dat twists the Gulf of Mexico 80 m cable two turns at the hang-off over 60 s and holds it; it runs as a CTest example and is catalogued. --- CMakeLists.txt | 10 + doc/examples.rst | 11 + examples/README.md | 11 + .../data/torsion/generate_hangoff_roll.py | 47 + .../torsion/hangoff_roll_2turns_60s_dt01.txt | 905 ++++++++++++++++++ examples/torsion_lazy_wave_hangoff_twist.dat | 66 ++ 6 files changed, 1050 insertions(+) create mode 100644 examples/data/torsion/generate_hangoff_roll.py create mode 100644 examples/data/torsion/hangoff_roll_2turns_60s_dt01.txt create mode 100644 examples/torsion_lazy_wave_hangoff_twist.dat diff --git a/CMakeLists.txt b/CMakeLists.txt index 4502707..ac8b809 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1832,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. 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/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 ---------------------------------------- From 35a22b869e510a6a879585d49db346420a32e014 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:29 +0900 Subject: [PATCH 18/26] Document torsion Deck columns, explicit GJ, the motionFile roll column, the Torq and Twist channels and range columns, sign conventions, the condensed-torsion theory with its references, the semi-tangential pinned end, named errors and the scope limits. --- doc/capabilities.rst | 22 ++++++-- doc/conventions.rst | 25 +++++++++ doc/driver_format.md | 77 ++++++++++++++++++++++++- doc/file_formats.rst | 32 ++++++++++- doc/options.rst | 11 +++- doc/outputs.rst | 32 ++++++++++- doc/python.rst | 5 ++ doc/references.rst | 19 +++++++ doc/theory.rst | 121 +++++++++++++++++++++++++++++++++++++++- doc/troubleshooting.rst | 66 ++++++++++++++++++++++ 10 files changed, 395 insertions(+), 15 deletions(-) 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..0252142 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,29 @@ 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 of End A enters with a minus sign, + :math:`\Phi = \text{Pretwist}_B - \text{Pretwist}_A - \text{roll}`, the same as a right-handed + rotation of a body or vessel carrying End A about ``Ez``. +- **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..4d79173 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,67 @@ 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`, or a positive torsional end spring [N·m/rad] | +| `NxX`, `NxY`, `NxZ` | zero-twist reference normal of the end, in the frame of `Ez`; finite, non-zero and not parallel to `Ez` (within about 0.06°); orthonormalised against `Ez` by the parser | +| `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. `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 End A frame about its direction + ([Prescribed motion](#prescribed-motion-motionfile)). +- 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`, the coupled OpenFAST and FAST.Farm routes and the +mixed `EI = 0` + finite-EI aggregate (`torsion is not yet supported in coupled OpenFAST +runs ...`), 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 +1116,10 @@ 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]: the roll of that line's End A frame about +its direction, which imposes `Phi(t) = Pretwist(B) − Pretwist(A) − roll(t)`. 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 +1231,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/file_formats.rst b/doc/file_formats.rst index 4c2eeaf..786d4cf 100644 --- a/doc/file_formats.rst +++ b/doc/file_formats.rst @@ -171,8 +171,36 @@ 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 that line's End A frame about its End A direction ``Ez``, in degrees, +right-handed. 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), + +so a positive roll of End A lowers :math:`\Phi`, the same sign as rolling the End A frame by +the same angle through a body or vessel. 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 a point that is End A 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 End A 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..68ae8fa 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 `, eqs. 5 and 22). 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..cd2c449 100644 --- a/doc/troubleshooting.rst +++ b/doc/troubleshooting.rst @@ -304,6 +304,53 @@ 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 (nor on the mixed EI = 0 + + finite-EI aggregate route or in FAST.Farm)`` + - run the torsional line standalone, or set ``TorsStiffness Free`` in the coupled deck. + * - ``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 End A 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 End A frame of a torsional line, 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 +432,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 +460,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`` From 6011882c20a95f7a79892e67ebf3ccc88b04a8c6 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 15:50:29 +0900 Subject: [PATCH 19/26] Record the torsion validation and the changelog entry --- CHANGELOG.md | 27 ++++++----- VALIDATION.md | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2fb5ab..69e9332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,22 @@ All notable changes to CableDyn are recorded in this file. The format follows ### Added -- Torsion of finite-EI lines (condensed isotropic torsion, quasi-static: no torsional - inertia): optional `END CONNECTIONS` columns `TorsStiffness NxX NxY NxZ [Pretwist]` restrain - the twist at a line end (`Free`, `Rigid` or a stiffness) with a reference normal and a - pretwist in degrees. A line restrained at both ends needs an explicit `GJ`. Statics solve the - imposed twist with a stability check and buckling descent; dynamics carry the torque in every - step, with End A's frame turning with its body or vessel and an optional `motionFile` roll - column for an imposed twist history. The torque is returned to a Rigid6 body at End A, in - statics (for any ratio of the body's restoring stiffness to GJ/L) and in dynamics. New - channels `TorqN`, `TwistN` and `Twist`, and Torque and Twist range envelopes. - The Python deck tools accept the new columns and channels. Modal analysis and coupled - OpenFAST/FAST.Farm runs with torsion stop with an error. +- Torsion of finite-EI lines in the standalone driver: condensed isotropic torsion (uniform + torque, quasi-static, no torsional inertia) through the geometric twist of the cubic-Hermite + centreline, with no added degrees of freedom. Optional `END CONNECTIONS` columns + `TorsStiffness NxX NxY NxZ [Pretwist]` restrain the twist of a line end (`Free`, `Rigid` or a + spring) with a reference normal and a pretwist in degrees; a line restrained at both ends + needs an explicit `GJ`, and a bending-pinned end may be restrained (a semi-tangential end). + Statics ramp the imposed twist with a stability check and descend to the buckled shape above + an onset; dynamics carry the torque in every step, the End A frame turning with its Rigid6 + body or vessel, and the new `motionFile` roll column imposes a twist history. The torque is + returned to a Rigid6 body at End A in statics and dynamics. New channels `TorqN`, + `TwistN` and `Twist`, torque and twist range-graph columns, Python deck, channel-unit + and range-graph support, and the example `torsion_lazy_wave_hangoff_twist.dat`. Validated + against closed forms, the Cosserat rod path and OrcaFlex 11.6d (VALIDATION.md, Torsion). Coupled + OpenFAST and FAST.Farm runs, the mixed `EI = 0` + finite-EI aggregate, rod ends, bodies other + than Rigid6, lines with `ATTACHMENTS`, modal analysis and `False alpha_force_blend` stop with + a named error when torsion is requested; deck results without torsion are unchanged. ### Fixed diff --git a/VALIDATION.md b/VALIDATION.md index 12a0910..5a1bd68 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,121 @@ 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. This static form stands in for the proposed dynamic hockling case: 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. 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, or with one restrained end, every output is byte-identical to the + 6-column deck. 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, and the coupled entries 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: the dynamic +loop-formation (hockling) case and the twisted slack catenary of the proposed OrcaFlex set are +not part of this record. There is no experimental torsion case. + +| Proposed case | Gate | +|---|---| +| V0 kernel, invariants, fold guard | T-1 | +| V1 pure torsion | T-2 | +| V2 torsion off, bit-for-bit | backward compatibility; full CTest suite | +| V3 zero-twist parity | T-6 (lazy wave), T-7 (3D sagging line) | +| V4 Greenhill onsets and post-buckling | T-3, T-4 | +| V5 Kirchhoff helix | T-7 | +| V6 hockling onset | T-3 (static onset at `TL²/EI` 400, 1600); dynamic loop formation not run | +| V7 Cosserat cross-check | T-8 (pure torsion, bent and twisted rod) | +| V8 OrcaFlex | T-2, T-5, T-6; dynamic loop case not run | +| V9 energy | T-10 (free vibration; body pendulum) | +| V10 unwrap and restart | T-1, T-10 | +| V11 Rigid6 body torque | T-10 | +| V12 named errors | backward compatibility and refusals | + ## Cross-code comparison method Cross-code validation compares physical observables, not another solver's internal From 516da20d152dbc7a6d3ecc722d01e904b210598f Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 16:26:49 +0900 Subject: [PATCH 20/26] Converge a twisting body's moment balance to its rotation on the multibody route A body holding a line restrained in torsion had its moment rows scaled by its weight, so a heavy body with a small rotational inertia accepted moment residuals far above the line torque, and a trial turn above 180 deg aliased the line's twist by a whole turn. Its moment rows now scale with its rotational step stiffness and each trial turn stays inside the torsion step limit; the torsional pendulum follows the trapezoidal closed form at Omega dt = 2 and 5. --- src/CableDyn_DeckDriver.f90 | 82 +++++++++++++++++++++++++++++++++---- tests/test_torsion_deck.f90 | 77 +++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 8 deletions(-) diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index 463d487..f014e56 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -98,7 +98,7 @@ MODULE CableDyn_DeckDriver CD_HermiteCable_Dyn_Set_Friction_Axial, & CD_HermiteCable_Dyn_Max_Step_Rotation, CD_HermiteCable_Dyn_Energy, & CD_HermiteCable_Attachment_Drag, CD_HermiteCable_Dyn_Torsion_State - USE CableDyn_HermiteTorsion, ONLY: CD_HermiteTorsionType, CD_HermiteTorsion_Compliance + 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, & @@ -17900,18 +17900,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) @@ -17926,6 +17932,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) @@ -17965,6 +17974,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, & @@ -17996,7 +18020,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(:) @@ -18076,8 +18100,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) @@ -18139,7 +18170,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 @@ -18237,7 +18268,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) @@ -18308,6 +18344,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. diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 01c3ed1..30e48de 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -25,7 +25,9 @@ !> 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. +!> 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 @@ -62,6 +64,8 @@ PROGRAM test_torsion_deck 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 @@ -653,6 +657,77 @@ SUBROUTINE check_body_roll_dynamics(bend) 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 From c6adc5875fd2fb9abbe7bcf1212a6b10911dd3ac Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:07:54 +0900 Subject: [PATCH 21/26] Harden the torsion kernel, statics and coupled boundary A static re-solve from a stored Theta stops by name when the untwisted state is more than 90 deg of twist away, and a failed solve leaves the stored Theta unchanged. The bordered step falls back to a dense solve on small lines and names an ill-conditioned failure; the inertia count rejects a non-square band and names an unreliable count; the stability test trusts a reliable count and scales the zero-mode band by EI/(EA Le^2). Newton and descent steps bound their linearised twist change before the trial. SetCoupledKinematics checks the turned torsion frame before writing anything, a restored mirror checks its frames and recovers the parent orientation, and the committed-state torque queries no longer allocate (now gated with CalcOutput). Test maxima are NaN-aware. --- src/CableDyn_HermiteCableDynamic.f90 | 201 ++++++++++++++++++++++--- src/CableDyn_HermiteCableStatic.f90 | 103 ++++++++++--- src/CableDyn_HermiteTorsion.f90 | 149 +++++++++++++++--- src/CableDyn_OpenFAST_HermiteFMF.f90 | 152 ++++++++++++++++--- tests/test_hermite_torsion_dynamic.f90 | 12 +- tests/test_hermite_torsion_kernel.f90 | 163 ++++++++++++++++++-- tests/test_hermite_torsion_static.f90 | 75 ++++++++- tests/test_step_allocations.f90 | 47 +++++- tests/test_torsion_orcaflex.f90 | 10 +- tests/test_torsion_validation.f90 | 2 +- 10 files changed, 796 insertions(+), 118 deletions(-) diff --git a/src/CableDyn_HermiteCableDynamic.f90 b/src/CableDyn_HermiteCableDynamic.f90 index 9376e08..638a66e 100644 --- a/src/CableDyn_HermiteCableDynamic.f90 +++ b/src/CableDyn_HermiteCableDynamic.f90 @@ -338,6 +338,14 @@ MODULE CableDyn_HermiteCableDynamic 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 @@ -2424,23 +2432,45 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Drive(model, ends, phi, ErrStat, ErrM ErrMsg = '' END SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Drive - SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion_Frames(model, ends, ErrStat, ErrMsg) + 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) :: th + 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 = .FALSE. + 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) @@ -2472,7 +2502,7 @@ SUBROUTINE dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, r 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) :: 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 @@ -2972,6 +3002,12 @@ SUBROUTINE dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, r 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) @@ -3316,6 +3352,13 @@ SUBROUTINE dyn_step_core(model, dt, max_iter, tol, ErrStat, ErrMsg, iters_out, r 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 @@ -4349,8 +4392,8 @@ SUBROUTINE CD_HermiteCable_Dyn_Energy(model, kinetic, strain, ErrStat, ErrMsg, c END IF IF (model%torsion%active) THEN BLOCK - REAL(wp) :: th, mt, gq(model%ndof), eg(6) - CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, ErrStat, em2, gq, eg) + 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 @@ -4601,12 +4644,12 @@ SUBROUTINE CD_HermiteCable_Dyn_End_Force(model, node, force, ErrStat, ErrMsg) ! 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, gq(model%ndof), eg(6) - CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em2, gq, eg) + 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*gq(6*(node - 1) + 1:6*(node - 1) + 3) + 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 @@ -4668,8 +4711,16 @@ SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment(model, node, moment, ErrStat ! -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, gq(model%ndof), eg(6) - CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, gq, eg) + 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) @@ -4723,7 +4774,13 @@ SUBROUTINE CD_HermiteCable_Dyn_EndConnection_Moment(model, node, moment, ErrStat moment = CD_ZERO RETURN END IF - moment = moment + bend_moment + ! 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) @@ -4739,8 +4796,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg TYPE(CD_HermiteTorsionType) :: saved - REAL(wp) :: th, mt, eg(6) - REAL(wp), ALLOCATABLE :: gq(:) + REAL(wp) :: th, mt INTEGER :: es CHARACTER(200) :: em ErrStat = CD_HCDYN_OK @@ -4751,6 +4807,7 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) RETURN END IF saved = model%torsion + model%tors_c_valid = .FALSE. IF (.NOT. torsion%active) THEN model%torsion = CD_HermiteTorsionType() ELSE @@ -4775,14 +4832,15 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) 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 - ALLOCATE (gq(model%ndof)) - CALL CD_HermiteCable_Dyn_Torsion_State(model, th, mt, es, em, gq, eg) + 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 @@ -4809,28 +4867,42 @@ SUBROUTINE CD_HermiteCable_Dyn_Set_Torsion(model, torsion, ErrStat, ErrMsg) 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) + 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 and dTheta/d[w_1, w_2] 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). + !! 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 + 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, & @@ -4855,8 +4927,79 @@ SUBROUTINE CD_HermiteCable_Dyn_Torsion_State(model, theta, torque, ErrStat, ErrM 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 @@ -5018,6 +5161,13 @@ SUBROUTINE CD_HermiteCable_Dyn_End(model) 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. @@ -5528,7 +5678,14 @@ SUBROUTINE assemble_torsion(model, q_cfg, want_residual, want_tangent, ErrStat, model%ws_R(i) = model%ws_R(i) - mt*g(i) END DO END IF - IF (want_tangent) CALL CD_HermiteTorsion_Work_Add_Hessian(model%tors_work, model%ws_Kb, -mt) + 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, & diff --git a/src/CableDyn_HermiteCableStatic.f90 b/src/CableDyn_HermiteCableStatic.f90 index 178a730..7f2ec68 100644 --- a/src/CableDyn_HermiteCableStatic.f90 +++ b/src/CableDyn_HermiteCableStatic.f90 @@ -51,7 +51,8 @@ MODULE CableDyn_HermiteCableStatic 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_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 @@ -382,9 +383,18 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ !! 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 return torsion holds the accepted Theta, - !! the torque and the stability report. Not combined - !! with pseudo_transient or equilibrate_linear_system. + !! 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 @@ -693,6 +703,10 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ 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 @@ -912,10 +926,14 @@ SUBROUTINE CD_HermiteCable_Static_Solve(l0, EA, EI, w, seed, fixed_dofs, seabed_ END IF END IF IF (use_tors) THEN - torsion%theta = tors_theta_acc - torsion%has_theta = .TRUE. - torsion%torque = tors_mt - torsion%energy = pi_total + ! 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) @@ -1665,7 +1683,8 @@ 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, or of torsion%theta_hint on a first solve) Phi is ramped from Theta_0 + !! 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). @@ -1673,7 +1692,7 @@ SUBROUTINE torsion_continuation(ecs, ecm) CHARACTER(*), INTENT(OUT) :: ecm INTEGER, PARAMETER :: MAX_CUTS = 10 REAL(wp), ALLOCATABLE :: q_keep(:) - REAL(wp) :: raw, ref, phi0, total, lam, lam_try, dlam, dlam_max, th_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 @@ -1681,6 +1700,7 @@ SUBROUTINE torsion_continuation(ecs, ecm) CHARACTER(1024) :: wbuf ecs = CD_HCSTAT_OK ecm = '' + res_untwisted = res_out torsion%ramp_steps = 0 torsion%descents = 0 torsion%stable = .FALSE. @@ -1692,9 +1712,20 @@ SUBROUTINE torsion_continuation(ecs, ecm) ecm = 'CD_HermiteCable_Static_Solve: torsion at the untwisted equilibrium: '//TRIM(km) RETURN END IF - ref = torsion%theta_hint - IF (torsion%has_theta) ref = torsion%theta - tors_theta_acc = CD_HermiteTorsion_Unwrap(raw, ref) + 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. @@ -1725,6 +1756,15 @@ SUBROUTINE torsion_continuation(ecs, ecm) 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)// & @@ -1801,7 +1841,7 @@ SUBROUTINE torsion_stability(st, ecs, ecm) LOGICAL, INTENT(OUT) :: st INTEGER, INTENT(OUT) :: ecs CHARACTER(*), INTENT(OUT) :: ecm - REAL(wp) :: rn + REAL(wp) :: rn, ztol INTEGER :: ks, kin, nb, nk CHARACTER(512) :: km st = .FALSE. @@ -1819,6 +1859,11 @@ SUBROUTINE torsion_stability(st, ecs, ecm) 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 @@ -1831,14 +1876,31 @@ SUBROUTINE torsion_stability(st, ecs, ecm) END IF tors_have_mode = .TRUE. torsion%lambda_min = tors_lambda - IF (kin /= CD_HTORS_OK) THEN - nk = MERGE(1, 0, tors_lambda < -CD_HTORS_ZERO_MODE_TOL) + ! 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 - st = tors_lambda > CD_HTORS_ZERO_MODE_TOL .OR. & - (torsion%zero_mode_allowed .AND. nk <= 1 .AND. tors_lambda >= -CD_HTORS_ZERO_MODE_TOL) 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 @@ -1887,6 +1949,11 @@ SUBROUTINE torsion_descent(ok, why) 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. diff --git a/src/CableDyn_HermiteTorsion.f90 b/src/CableDyn_HermiteTorsion.f90 index 72c97c1..f554be5 100644 --- a/src/CableDyn_HermiteTorsion.f90 +++ b/src/CableDyn_HermiteTorsion.f90 @@ -35,6 +35,7 @@ MODULE CableDyn_HermiteTorsion !! 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 @@ -59,6 +60,12 @@ MODULE CableDyn_HermiteTorsion 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. @@ -123,6 +130,8 @@ MODULE CableDyn_HermiteTorsion !! 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 @@ -132,13 +141,16 @@ MODULE CableDyn_HermiteTorsion ! ------------------------------------------------------------------------------------------ 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)]. + !! 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 = tors%end_compliance(1) + tors%end_compliance(2) - IF (ALLOCATED(tors%gj)) THEN - IF (SIZE(tors%gj) == SIZE(l0)) c = c + SUM(l0/tors%gj) - END IF + 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) @@ -192,7 +204,7 @@ END SUBROUTINE CD_HermiteTorsion_Validate ! ------------------------------------------------------------------------------------------ SUBROUTINE CD_HermiteTorsion_Bordered_Solve(ab, kl, ku, g, compliance, rhs, x, ErrStat, ErrMsg, & - backward_error, shifted) + 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]): @@ -203,8 +215,12 @@ SUBROUTINE CD_HermiteTorsion_Bordered_Solve(ab, kl, ku, g, compliance, rhs, x, E !! 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. - !! A failure of both leaves x = 0 with a named error. B may be singular where K is not (the - !! rank-one term is positive semidefinite); at a straight state g = 0 and K = B. + !! 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(:) @@ -212,17 +228,18 @@ SUBROUTINE CD_HermiteTorsion_Bordered_Solve(ab, kl, ku, g, compliance, rhs, x, E INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg REAL(wp), INTENT(OUT), OPTIONAL :: backward_error - LOGICAL, INTENT(OUT), OPTIONAL :: shifted + 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 + 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 @@ -269,13 +286,65 @@ SUBROUTINE CD_HermiteTorsion_Bordered_Solve(ab, kl, ku, g, compliance, rhs, x, E 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 - ErrStat = CD_HTORS_NONFINITE - ErrMsg = 'CD_HermiteTorsion_Bordered_Solve: the bordered torsion step failed ('//TRIM(why)// & - '), also with a shifted factorization and refinement' + 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(:) @@ -394,7 +463,8 @@ SUBROUTINE CD_HermiteTorsion_Inertia(ab, kl, ku, mask, g, compliance, n_negative !! 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_STEP. + !! 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(:) @@ -412,9 +482,14 @@ SUBROUTINE CD_HermiteTorsion_Inertia(ab, kl, ku, mask, g, compliance, n_negative 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)) THEN + .NOT. (compliance > CD_ZERO) .OR. kl /= ku .OR. kl < 0) THEN ErrStat = CD_HTORS_BADINPUT - ErrMsg = 'CD_HermiteTorsion_Inertia: inconsistent input' + 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)) @@ -441,7 +516,7 @@ SUBROUTINE CD_HermiteTorsion_Inertia(ab, kl, ku, mask, g, compliance, n_negative END DO d(j) = acc IF (.NOT. (ABS(d(j)) > 1.0e-14_wp*smax)) THEN - ErrStat = CD_HTORS_STEP + ErrStat = CD_HTORS_UNRELIABLE ErrMsg = 'CD_HermiteTorsion_Inertia: vanishing pivot (count unreliable)' RETURN END IF @@ -452,7 +527,7 @@ SUBROUTINE CD_HermiteTorsion_Inertia(ab, kl, ku, mask, g, compliance, n_negative END DO lb(1 + i - j, j) = acc/d(j) IF (ABS(lb(1 + i - j, j)) > 1.0e8_wp) THEN - ErrStat = CD_HTORS_STEP + ErrStat = CD_HTORS_UNRELIABLE ErrMsg = 'CD_HermiteTorsion_Inertia: pivot growth (count unreliable)' RETURN END IF @@ -510,9 +585,14 @@ SUBROUTINE CD_HermiteTorsion_Lowest_Mode(ab, kl, ku, mask, g, compliance, lambda 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)) THEN + .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' + 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)) @@ -776,18 +856,32 @@ PURE SUBROUTINE CD_HermiteTorsion_Work_End(work) 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) + 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 - IF (n < 12 .OR. SIZE(hband, 2) /= n .OR. SIZE(hband, 1) < 2*CD_HTORS_KBAND + 1) RETURN + 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 @@ -806,7 +900,8 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er !! 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 element holonomies + chain angle in (-pi, pi]; Theta is defined + !! 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 @@ -853,11 +948,14 @@ PURE SUBROUTINE CD_HermiteTorsion_Line(q, Le, ends, theta_raw, grad, ErrStat, Er 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), & @@ -1067,7 +1165,10 @@ PURE SUBROUTINE CD_HermiteTorsion_Accept(theta_prev, theta_raw, theta_new, ErrSt !! |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. + !! 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 diff --git a/src/CableDyn_OpenFAST_HermiteFMF.f90 b/src/CableDyn_OpenFAST_HermiteFMF.f90 index ec04a41..b7b9d00 100644 --- a/src/CableDyn_OpenFAST_HermiteFMF.f90 +++ b/src/CableDyn_OpenFAST_HermiteFMF.f90 @@ -42,7 +42,8 @@ MODULE CableDyn_OpenFAST_HermiteFMF 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 + 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 @@ -811,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) @@ -834,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, & @@ -845,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 @@ -867,22 +954,14 @@ SUBROUTINE CD_HFMF_SetCoupledKinematics(self, u_pos, u_vel, u_acc, ErrStat, ErrM 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) - BLOCK - REAL(wp) :: tors_ends(3, 4) - INTEGER :: es - tors_ends = self%line%torsion%ends - 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))) - 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 - END BLOCK + ! 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 @@ -1520,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 @@ -1564,6 +1658,22 @@ SUBROUTINE CD_HFMF_UnpackMirror(self, buf, t, ErrStat, ErrMsg) 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 diff --git a/tests/test_hermite_torsion_dynamic.f90 b/tests/test_hermite_torsion_dynamic.f90 index 33820c3..e6f5861 100644 --- a/tests/test_hermite_torsion_dynamic.f90 +++ b/tests/test_hermite_torsion_dynamic.f90 @@ -178,8 +178,8 @@ SUBROUTINE check_step_response() IF (es /= CD_HFMF_OK) EXIT CALL CD_HermiteCable_Dyn_Torsion_State(cab%line, th, mt, es, em) mref = GJ*phi/LL - worst = MAX(worst, ABS(mt - mref)/mref) - bent = MAX(bent, MAXVAL(ABS(cab%line%q(2:NDOF:6))), MAXVAL(ABS(cab%line%q(3:NDOF:6)))) + 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. & @@ -214,8 +214,8 @@ SUBROUTINE check_turning_parent() 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 = MAX(err_th, ABS(th - (th0 - psi))) - err_mt = MAX(err_mt, ABS(mt - GJ*2.0_wp/LL)) + 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 @@ -276,12 +276,12 @@ SUBROUTINE energy_run(phi, band, e_vib, t_exchange) EXIT END IF CALL CD_HermiteCable_Dyn_Energy(m, ke, se, es, em) - emax = MAX(emax, ke + se) + 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 = MAX(etmax, et) + etmax = nan_max_abs([etmax, et]) etmin = MIN(etmin, et) END IF END DO diff --git a/tests/test_hermite_torsion_kernel.f90 b/tests/test_hermite_torsion_kernel.f90 index 5562d88..c3a9e51 100644 --- a/tests/test_hermite_torsion_kernel.f90 +++ b/tests/test_hermite_torsion_kernel.f90 @@ -15,7 +15,9 @@ PROGRAM test_hermite_torsion_kernel !! 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). + !! 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, & @@ -23,7 +25,10 @@ PROGRAM test_hermite_torsion_kernel 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_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 @@ -48,6 +53,7 @@ PROGRAM test_hermite_torsion_kernel 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' @@ -234,7 +240,7 @@ SUBROUTINE check_oracle_reference() DO j = 1, n DO i = j, MIN(n, j + KB) nb = nb + 1 - err_h = MAX(err_h, ABS(h(i, j) - band_ref(nb)), ABS(h(j, i) - band_ref(nb))) + 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 @@ -304,8 +310,8 @@ SUBROUTINE check_finite_differences() tp = 0.0_wp DO j = 1, n DO i = 1, n - IF (ABS(i - j) > KB) dum = MAX(dum, ABS(hfd(i, j))) - IF (ABS(i - j) == KB) tp = MAX(tp, ABS(h(i, j))) + 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)) @@ -429,13 +435,23 @@ SUBROUTINE check_workspace() 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) + 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) @@ -473,7 +489,7 @@ SUBROUTINE check_rigid_rotation() 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 = MAX(err_gen, nan_max_abs(gen)/MAXVAL(ABS(g))) + 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 @@ -484,8 +500,8 @@ SUBROUTINE check_rigid_rotation() ends2(:, k) = rotate(w, ends(:, k)) END DO CALL CD_HermiteTorsion_Line(q2, le, ends2, th2, g2, es, em) - err_t = MAX(err_t, ABS(wrap(th2 - th))) - err_g = MAX(err_g, nan_max_abs(g2 - grot)/MAXVAL(ABS(g))) + 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 @@ -609,7 +625,7 @@ SUBROUTINE check_helix_and_unwrap() prev = th END IF raw_prev = raw - err = MAX(err, ABS(prev - expected)) + 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, & @@ -758,4 +774,131 @@ SUBROUTINE check_element_routine() 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 index 7cb7f20..41f5b65 100644 --- a/tests/test_hermite_torsion_static.f90 +++ b/tests/test_hermite_torsion_static.f90 @@ -17,7 +17,8 @@ PROGRAM test_hermite_torsion_static !! 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). + !! 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 @@ -42,6 +43,7 @@ PROGRAM test_hermite_torsion_static 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' @@ -239,7 +241,8 @@ SUBROUTINE check_pure_torsion() 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 = MAX(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 + 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') @@ -313,7 +316,7 @@ SUBROUTINE check_post_buckling() 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 = MAX(nan_max_abs(q(2::6)), nan_max_abs(q(3::6))) + 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 @@ -485,6 +488,72 @@ SUBROUTINE check_fail_closed() 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 d903c7a..13e1644 100644 --- a/tests/test_step_allocations.f90 +++ b/tests/test_step_allocations.f90 @@ -19,7 +19,9 @@ PROGRAM test_step_allocations !! 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. + !! 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 @@ -28,7 +30,10 @@ PROGRAM test_step_allocations 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_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, & @@ -171,8 +176,11 @@ SUBROUTINE torsion_gate() 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 + 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) @@ -222,16 +230,39 @@ SUBROUTINE torsion_gate() CALL cd_test_allocation_counting(0_C_INT) CALL check('torsion cable step') n_alloc = n_alloc + (cd_test_allocation_count() - c0) - CALL CD_HFMF_CalcOutput(cab, f, es, em, y_moment=m) + ! 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)', 'torsion cable: ', N_COUNT, ' counted steps, ', n_alloc, ' heap allocations in the steps' - IF (n_alloc /= 0) THEN - PRINT '(A)', 'FAIL: the per-step advance of a cable with torsion allocated after warm-up' + 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 of a cable with torsion after warm-up' + 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) diff --git a/tests/test_torsion_orcaflex.f90 b/tests/test_torsion_orcaflex.f90 index b7a373d..059b4de 100644 --- a/tests/test_torsion_orcaflex.f90 +++ b/tests/test_torsion_orcaflex.f90 @@ -462,15 +462,15 @@ SUBROUTINE check_lazy_wave() ELSE etq = ABS(tq/lw(2, k) - 1.0_wp) eth = ABS(th - lw(3, k))/lw(1, k) - worst(1) = MAX(worst(1), etq) - worst(2) = MAX(worst(2), eth) + 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) = MAX(worst(3), ekap) - worst(4) = MAX(worst(4), ey) - worst(5) = MAX(worst(5), eten) + 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) diff --git a/tests/test_torsion_validation.f90 b/tests/test_torsion_validation.f90 index 5bef19f..a8c2a39 100644 --- a/tests/test_torsion_validation.f90 +++ b/tests/test_torsion_validation.f90 @@ -312,7 +312,7 @@ SUBROUTINE check_helix() IF (es /= CD_HCSTAT_OK) RETURN drift = 0.0_wp DO k = 0, NE - drift = MAX(drift, ABS(NORM2(q(6*k + 1:6*k + 2)) - a)) + 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 From 20b28691f746609f9c3f49c91807b337fd270a1c Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:10:02 +0900 Subject: [PATCH 22/26] Read the torsion columns the same way in the driver and in Python TorsStiffness takes Inf, Infinity, Rigid (also quoted), Free and Zero in the native reader as documented, and both readers parse the END CONNECTIONS stiffness columns as plain reals (no q exponent, finite, not subnormal). A Free end's reference normal is not checked, and one restrained end is ignored before the End B rule, so it no longer refuses a line with two moving ends. Python refuses torsion on a standalone mixed EI = 0 + finite-EI deck as the aggregate does, after the line rules; the refusal and the motionFile roll message name the deck's own ends, and a failed torsion stage is reported without a doubled prefix. The static retry starts from a fresh torsion state, the body pass loop passes the torque stiffness only with torsion, and the simple deck writer refuses torsion arguments. --- python/cabledyn/deck.py | 27 ++- python/cabledyn/deck_file.py | 125 ++++++++----- python/tests/test_deck_file.py | 2 +- python/tests/test_deck_native_parity.py | 231 +++++++++++++++++++++++- src/CableDyn_DeckDriver.f90 | 85 ++++++--- tests/test_torsion_deck.f90 | 17 +- 6 files changed, 401 insertions(+), 86 deletions(-) diff --git a/python/cabledyn/deck.py b/python/cabledyn/deck.py index 6ef7ea9..80eea0a 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 + :meth:`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 da7e78b..d122ef3 100644 --- a/python/cabledyn/deck_file.py +++ b/python/cabledyn/deck_file.py @@ -312,6 +312,8 @@ "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") @@ -566,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) @@ -2878,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 @@ -2903,17 +2919,14 @@ def end_key(token: str) -> str: ) if len(row.tokens) > 6: # native append_end_connection: TorsStiffness NxX NxY NxZ [Pretwist] - torsion_token = row.tokens[6].lower() - if torsion_token in {"free", "zero"}: + 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: - try: - torsion_k = _table_float(row.tokens[6]) - except ValueError: - torsion_k = -1.0 - if not math.isfinite(torsion_k) or torsion_k < 0.0: + 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" @@ -2929,24 +2942,9 @@ def end_key(token: str) -> str: ] if len(row.tokens) == 11: numeric(row.tokens[10], row, "END CONNECTIONS pretwist") - 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 / 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" - ) + 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( @@ -3038,6 +3036,7 @@ def end_key(token: str) -> str: body_types, option_by_key, has_tmax, + mixed_route, ) rod_types = self._rows("ROD TYPES") @@ -3746,6 +3745,33 @@ 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]], @@ -3757,15 +3783,18 @@ def _check_torsion_lines( 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 a Fixed End B. Restrained at both ends, torsion is - solved: every section type needs an explicit GJ > 0, no ATTACHMENTS, End A neither 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 - refuse any torsion column. + 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"): @@ -3774,28 +3803,25 @@ def _check_torsion_lines( 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 - if self.caller_driven: - raise DeckFormatError( - f"{self._label}: torsion is not yet supported in coupled OpenFAST runs (nor on " - "the mixed EI = 0 + finite-EI aggregate route or in FAST.Farm); set " - "TorsStiffness Free in the END CONNECTIONS rows" - ) + 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" ) - if not (ends.get("a", False) and ends.get("b", False)): - continue for name in section_types_by_line[line_id]: if not line_type_gj[name] > 0.0: raise DeckFormatError( @@ -3820,6 +3846,15 @@ def _check_torsion_lines( "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") @@ -4239,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 ---------- @@ -4249,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/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 0d66481..80f715c 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 @@ -862,7 +864,234 @@ def test_torsion_line_rules_follow_the_native_checks(): _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, "not yet supported in coupled OpenFAST runs", caller_driven=True) + _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( diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index f014e56..e944de0 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -2562,6 +2562,7 @@ RECURSIVE SUBROUTINE build_one_hermite_cable(ln, sections, types, points, opts, 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 @@ -2797,6 +2798,7 @@ SUBROUTINE torsion_stage(est, 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() @@ -2832,7 +2834,10 @@ SUBROUTINE torsion_stage(est, emt) 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 @@ -4891,9 +4896,8 @@ SUBROUTINE CD_Init_Deck_HermiteCable(deck_path, dt, cable, coupled_node, ErrStat 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 (nor on the mixed '// & - 'EI = 0 + finite-EI aggregate route or in FAST.Farm); set TorsStiffness Free in the END '// & - 'CONNECTIONS rows'); RETURN + 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; '// & @@ -5047,9 +5051,10 @@ SUBROUTINE CD_Init_Deck_Aggregate(deck_path, dt, agg, ErrStat, ErrMsg, env_gravi 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 coupled OpenFAST runs (nor on the mixed '// & - 'EI = 0 + finite-EI aggregate route or in FAST.Farm); set TorsStiffness Free in the END '// & - 'CONNECTIONS rows'); RETURN + 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 @@ -8642,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 @@ -11102,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 @@ -11145,20 +11152,26 @@ SUBROUTINE append_end_connection(line, endconns, nec, ErrStat, ErrMsg) CALL fail(ErrStat, ErrMsg, 'END CONNECTIONS torsion reference normal and pretwist must be finite') RETURN END IF - 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 + 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 - 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 ALLOCATE (grown(nec + 1), STAT=istat) @@ -13387,8 +13400,8 @@ SUBROUTINE finalize_checks(types, bodies, rod_types, rods, points, lines, sectio 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 - !! a Fixed End B. With both ends restrained (torsion active) every section's LINE TYPES row + !! 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 @@ -13404,6 +13417,9 @@ SUBROUTINE check_torsion_line(ln, es_t, em_t) '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 @@ -13411,7 +13427,6 @@ SUBROUTINE check_torsion_line(ln, es_t, em_t) TRIM(lid)//' has two moving ends') RETURN END IF - IF (.NOT. ALL(ln%tors_mode /= TORS_FREE)) RETURN 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 @@ -17934,7 +17949,7 @@ SUBROUTINE mb_imp_row_scales(mb, points, opts, bh2, rscale) 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)) + (MINVAL(mb%rigid(ib)%inertia)/bh2 + kt)*TORS_ROT_REF)) END DO DO ir = 1, SIZE(mb%rods) o = mb%jr_off(ir) @@ -21327,9 +21342,17 @@ SUBROUTINE solve_body_equilibrium(types, bodies, rod_types, rods, points, lines, 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, ext_km=ext_km, ext_rot=ext_rot) + ! 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 @@ -23734,8 +23757,10 @@ SUBROUTINE write_hermite_dynamic_out(out_root, channels, lines, points, opts, ca CONTAINS SUBROUTINE check_motion_roll(est, emt) - !! The motionFile roll column drives a torsional line through its End A point only, and - !! starts from the static state (roll 0 at t = 0; a constant twist is the Pretwist). + !! 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 @@ -23752,7 +23777,7 @@ SUBROUTINE check_motion_roll(est, emt) 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 End A there'); RETURN + '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)))// & diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 30e48de..17cf4ca 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -388,7 +388,8 @@ SUBROUTINE check_body_torque() CALL read_row('tdeck_body.out', 0, wrench, ok) CALL require(ok, 'body torque output readable') mnet = cross(MATMUL(r, off), fa) + ma - ew = MAX(NORM2(wrench(2:4) - fa)/MAX(NORM2(fa), ABS(m_ref)/20.0_wp), NORM2(wrench(5:7) - mnet)/NORM2(mnet)) + 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)') @@ -488,8 +489,8 @@ SUBROUTINE check_dynamic_roll() CALL require(ok, '7a: output row readable') IF (.NOT. ok) EXIT roll = MERGE(0.0_wp, 30.0_wp, k == 0) - werr_a = MAX(werr_a, ABS(v(4) + roll)) - terr = MAX(terr, ABS(v(2) - GJ*(-roll*PI/180.0_wp)/L), ABS(v(3) - v(2))) + 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') @@ -513,8 +514,8 @@ SUBROUTINE check_dynamic_roll() CALL require(ok, '7b: output row readable') IF (.NOT. ok) EXIT roll = 60.0_wp*0.05_wp*k - werr_b = MAX(werr_b, ABS(v(4) + roll)) - terr = MAX(terr, ABS(v(2) - GJ*(v(4)*PI/180.0_wp)/L)) + 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)') @@ -646,8 +647,8 @@ SUBROUTINE check_body_roll_dynamics(bend) mt = v(2) rk = RESHAPE(rec(8:16, k), [3, 3]) dk = MATMUL(rk, dg) - err_axis = MAX(err_axis, NORM2(rec(24:26, k) - mt*dk)/m0) - err_wave = MAX(err_wave, ABS(mt - (m0*COS(omega*t) - amp*SIN(omega*t)))/m0) + 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 ', & @@ -884,7 +885,7 @@ SUBROUTINE check_errors() [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, 'not yet supported in coupled OpenFAST runs') > 0, & + 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) From f8738af7cdcb8c8f98b9d62be6329eab5277ee17 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:20:02 +0900 Subject: [PATCH 23/26] Gate the boundary write, mirror reload and deck refusals of torsion A refused SetCoupledKinematics (and its preflight) leaves the cable unchanged, a mirror with non-orthonormal torsion frames is refused before any write, and a reload recovers a parent-attached frame's orientation. The deck gates add the Rigid/Infinity/Inf/quoted keywords, Free ends with an unused normal, a one-end row on a line with two moving ends (the six-column verdict) and a failed torsion stage named once. --- tests/test_hermite_torsion_dynamic.f90 | 75 ++++++++++++++++++++++++-- tests/test_torsion_deck.f90 | 58 +++++++++++++++++++- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/tests/test_hermite_torsion_dynamic.f90 b/tests/test_hermite_torsion_dynamic.f90 index e6f5861..d6b1f92 100644 --- a/tests/test_hermite_torsion_dynamic.f90 +++ b/tests/test_hermite_torsion_dynamic.f90 @@ -2,11 +2,11 @@ ! 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 (Route A) torsion on the cubic-Hermite cable, on a 10 m line + !! 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. Route A has no torsional inertia, so the torque follows the imposed twist + !! 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 @@ -18,6 +18,8 @@ PROGRAM test_hermite_torsion_dynamic !! 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 @@ -37,7 +39,7 @@ PROGRAM test_hermite_torsion_dynamic 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_End, CD_HFMF_OK, CD_HFMF_PreflightCoupledKinematics IMPLICIT NONE REAL(wp), PARAMETER :: PI = 3.14159265358979323846_wp @@ -49,6 +51,7 @@ PROGRAM test_hermite_torsion_dynamic CALL check_turning_parent() CALL check_energy() CALL check_restart() + CALL check_boundary_and_mirror() CALL check_bordered_and_blend() IF (nfail > 0) THEN @@ -394,6 +397,72 @@ SUBROUTINE check_restart() 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 diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index 17cf4ca..ada5465 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -6,8 +6,9 @@ !> 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, or with one end -!> restrained only, writes the same .out bytes as the 6-column rows; +!> 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 @@ -56,6 +57,7 @@ PROGRAM test_torsion_deck 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() @@ -293,8 +295,33 @@ SUBROUTINE check_backward_compatible() 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). @@ -813,6 +840,27 @@ SUBROUTINE expect_error(label, path, root, needle) 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. @@ -851,6 +899,12 @@ SUBROUTINE check_errors() 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') From 8c68b9ac0358ac1f2f41a14ad8d1a534a1ac174b Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:20:02 +0900 Subject: [PATCH 24/26] Document the torsion refinements and tidy the validation record The roll column is described in terms of the line's moving end, the Free-end normal and the keyword set as the reader takes them, the multibody 90-degree step limit and the refusal of torsion on mixed decks without a body. The validation record presents its cases without the planning labels and records the coarse-step pendulum; the changelog entry is shortened. The oracle script needs --out, the OrcaFlex script imports OrcFxAPI only to run OrcaFlex, and internal wording is removed from test data and docstrings. --- CHANGELOG.md | 23 ++++------- VALIDATION.md | 39 +++++++------------ doc/conventions.rst | 9 +++-- doc/driver_format.md | 29 +++++++++----- doc/file_formats.rst | 16 ++++---- doc/theory.rst | 2 +- doc/troubleshooting.rst | 21 ++++++---- python/tests/test_deck_native_parity.py | 2 +- src/CableDyn_HermiteCableStatic.f90 | 2 +- tests/data/torsion_analytic_refs.txt | 4 +- validation/scripts/hermite_torsion_oracle.py | 17 +++++--- .../scripts/orcaflex_torsion_reference.py | 13 ++++++- 12 files changed, 95 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e9332..c0696ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,22 +10,13 @@ All notable changes to CableDyn are recorded in this file. The format follows ### Added -- Torsion of finite-EI lines in the standalone driver: condensed isotropic torsion (uniform - torque, quasi-static, no torsional inertia) through the geometric twist of the cubic-Hermite - centreline, with no added degrees of freedom. Optional `END CONNECTIONS` columns - `TorsStiffness NxX NxY NxZ [Pretwist]` restrain the twist of a line end (`Free`, `Rigid` or a - spring) with a reference normal and a pretwist in degrees; a line restrained at both ends - needs an explicit `GJ`, and a bending-pinned end may be restrained (a semi-tangential end). - Statics ramp the imposed twist with a stability check and descend to the buckled shape above - an onset; dynamics carry the torque in every step, the End A frame turning with its Rigid6 - body or vessel, and the new `motionFile` roll column imposes a twist history. The torque is - returned to a Rigid6 body at End A in statics and dynamics. New channels `TorqN`, - `TwistN` and `Twist`, torque and twist range-graph columns, Python deck, channel-unit - and range-graph support, and the example `torsion_lazy_wave_hangoff_twist.dat`. Validated - against closed forms, the Cosserat rod path and OrcaFlex 11.6d (VALIDATION.md, Torsion). Coupled - OpenFAST and FAST.Farm runs, the mixed `EI = 0` + finite-EI aggregate, rod ends, bodies other - than Rigid6, lines with `ATTACHMENTS`, modal analysis and `False alpha_force_blend` stop with - a named error when torsion is requested; deck results without torsion are unchanged. +- 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 diff --git a/VALIDATION.md b/VALIDATION.md index 5a1bd68..8087502 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -822,8 +822,7 @@ in parentheses). 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. This static form stands in for the proposed dynamic hockling case: loop - formation and self-contact are not resolved (see Limits below). + 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 @@ -872,34 +871,22 @@ in parentheses). 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. The `motionFile` roll column and a rolling vessel give `Twist = −roll` and - the quasi-static torque at every step. + 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, or with one restrained end, every output is byte-identical to the - 6-column deck. Malformed rows, a missing `GJ`, an `EI = 0` line, a rod end, a `Point3` body, + 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, and the coupled entries stop with named errors. + 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: the dynamic -loop-formation (hockling) case and the twisted slack catenary of the proposed OrcaFlex set are -not part of this record. There is no experimental torsion case. - -| Proposed case | Gate | -|---|---| -| V0 kernel, invariants, fold guard | T-1 | -| V1 pure torsion | T-2 | -| V2 torsion off, bit-for-bit | backward compatibility; full CTest suite | -| V3 zero-twist parity | T-6 (lazy wave), T-7 (3D sagging line) | -| V4 Greenhill onsets and post-buckling | T-3, T-4 | -| V5 Kirchhoff helix | T-7 | -| V6 hockling onset | T-3 (static onset at `TL²/EI` 400, 1600); dynamic loop formation not run | -| V7 Cosserat cross-check | T-8 (pure torsion, bent and twisted rod) | -| V8 OrcaFlex | T-2, T-5, T-6; dynamic loop case not run | -| V9 energy | T-10 (free vibration; body pendulum) | -| V10 unwrap and restart | T-1, T-10 | -| V11 Rigid6 body torque | T-10 | -| V12 named errors | backward compatibility and refusals | +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 diff --git a/doc/conventions.rst b/doc/conventions.rst index 0252142..7de8853 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -172,9 +172,12 @@ These apply to a finite-EI line restrained in torsion at both ends (:doc:`driver - 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 of End A enters with a minus sign, - :math:`\Phi = \text{Pretwist}_B - \text{Pretwist}_A - \text{roll}`, the same as a right-handed - rotation of a body or vessel carrying End A about ``Ez``. + ``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 diff --git a/doc/driver_format.md b/doc/driver_format.md index 4d79173..477365b 100644 --- a/doc/driver_format.md +++ b/doc/driver_format.md @@ -771,16 +771,16 @@ LineID End Stiffness EzX EzY EzZ TorsStiffness NxX NxY NxZ Pretwist | Column | Meaning | |--------|---------| -| `TorsStiffness` | `Free`/`Zero`/`0` (default, no torsional restraint), `Rigid`/`Infinity`/`Inf`, or a positive torsional end spring [N·m/rad] | -| `NxX`, `NxY`, `NxZ` | zero-twist reference normal of the end, in the frame of `Ez`; finite, non-zero and not parallel to `Ez` (within about 0.06°); orthonormalised against `Ez` by the parser | +| `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. `Free` columns, or no columns, reproduce the - 6-column results exactly. + 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 @@ -792,8 +792,11 @@ Behaviour: 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 End A frame about its direction + 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 @@ -811,9 +814,11 @@ Scope. A torsionally restrained line needs a finite-EI line with a `Fixed` End B `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`, the coupled OpenFAST and FAST.Farm routes and the -mixed `EI = 0` + finite-EI aggregate (`torsion is not yet supported in coupled OpenFAST -runs ...`), and a `Torq`/`Twist` channel on a line that is not restrained at both ends. Seabed +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. @@ -1117,8 +1122,12 @@ cases. With an active path: 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, except that a deck with a line restrained -in torsion at both ends reads a twelfth, `roll` [deg]: the roll of that line's End A frame about -its direction, which imposes `Phi(t) = Pretwist(B) − Pretwist(A) − roll(t)`. The roll is 0 at +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 diff --git a/doc/file_formats.rst b/doc/file_formats.rst index 786d4cf..5958c11 100644 --- a/doc/file_formats.rst +++ b/doc/file_formats.rst @@ -176,24 +176,26 @@ Tokens after the eleventh are ignored, except on a deck with a torsional line (b **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 that line's End A frame about its End A direction ``Ez``, in degrees, -right-handed. It imposes a twist history without turning the point: the line's imposed twist 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), -so a positive roll of End A lowers :math:`\Phi`, the same sign as rolling the End A frame by -the same angle through a body or vessel. Rules: +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 a point that is End A 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 End A there``); +* 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. diff --git a/doc/theory.rst b/doc/theory.rst index 68ae8fa..ef2b08c 100644 --- a/doc/theory.rst +++ b/doc/theory.rst @@ -148,7 +148,7 @@ A line restrained in torsion at both ends (``END CONNECTIONS`` ``TorsStiffness`` :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 `, eqs. 5 and 22). The twist field +(:ref:`van der Heijden et al., 2003 `). The twist field then condenses to one scalar per line, and the line energy gains .. math:: diff --git a/doc/troubleshooting.rst b/doc/troubleshooting.rst index cd2c449..18d5bf1 100644 --- a/doc/troubleshooting.rst +++ b/doc/troubleshooting.rst @@ -333,9 +333,14 @@ Torsion - 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 (nor on the mixed EI = 0 + - finite-EI aggregate route or in FAST.Farm)`` - - run the torsional line standalone, or set ``TorsStiffness Free`` in the coupled deck. + * - ``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`` @@ -344,11 +349,11 @@ Torsion 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 End A 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 End A frame of a torsional line, starts from 0 (put a constant - twist in ``Pretwist``), and appears on every row of the point or on none + 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 diff --git a/python/tests/test_deck_native_parity.py b/python/tests/test_deck_native_parity.py index 80f715c..14dc66e 100644 --- a/python/tests/test_deck_native_parity.py +++ b/python/tests/test_deck_native_parity.py @@ -764,7 +764,7 @@ def test_coupled_route_takes_no_motion_file_and_needs_a_line(): # --------------------------------------------------------------------------- torsion # A straight finite-EI line clamped at a Coupled and a Fixed point with the optional torsion -# columns of END CONNECTIONS (the native torsion deck gate's pure-torsion deck). +# 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 diff --git a/src/CableDyn_HermiteCableStatic.f90 b/src/CableDyn_HermiteCableStatic.f90 index 7f2ec68..ed10896 100644 --- a/src/CableDyn_HermiteCableStatic.f90 +++ b/src/CableDyn_HermiteCableStatic.f90 @@ -366,7 +366,7 @@ 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 (Route A, + !! 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 diff --git a/tests/data/torsion_analytic_refs.txt b/tests/data/torsion_analytic_refs.txt index 6c1cbb1..8e57b41 100644 --- a/tests/data/torsion_analytic_refs.txt +++ b/tests/data/torsion_analytic_refs.txt @@ -1,9 +1,9 @@ -# Route A torsion analytic references (EI = L = 1; T = T L^2/EI, M_cr = M_cr L/EI). +# 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, the Route A pinned end with torsion restrained): tan(M L / 2 EI) = -M L / (6 EI) at +# 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 diff --git a/validation/scripts/hermite_torsion_oracle.py b/validation/scripts/hermite_torsion_oracle.py index fb39fbb..6359041 100644 --- a/validation/scripts/hermite_torsion_oracle.py +++ b/validation/scripts/hermite_torsion_oracle.py @@ -21,9 +21,9 @@ 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 [output] + python validation/scripts/hermite_torsion_oracle.py --out tests/data/torsion_oracle_reference.txt -The default output is ``tests/data/torsion_oracle_reference.txt``. Requires JAX and NumPy. +``--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, @@ -35,7 +35,6 @@ from __future__ import annotations import os -import sys import numpy as np import jax @@ -256,6 +255,12 @@ def main(path): if __name__ == "__main__": - here = os.path.dirname(os.path.abspath(__file__)) - default = os.path.join(here, "..", "..", "tests", "data", "torsion_oracle_reference.txt") - main(os.path.normpath(sys.argv[1] if len(sys.argv) > 1 else default)) + 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 index e843613..d942f15 100644 --- a/validation/scripts/orcaflex_torsion_reference.py +++ b/validation/scripts/orcaflex_torsion_reference.py @@ -53,7 +53,8 @@ import time import traceback -import OrcFxAPI as ofx +# 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" @@ -639,8 +640,18 @@ def case_c(): 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: From 66d57d267189c52b898a29e39c288bc31dbe7f7a Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:22:40 +0900 Subject: [PATCH 25/26] Name the torsion builder in the simple deck writer's docstring as a literal --- python/cabledyn/deck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cabledyn/deck.py b/python/cabledyn/deck.py index 80eea0a..81b66ec 100644 --- a/python/cabledyn/deck.py +++ b/python/cabledyn/deck.py @@ -274,7 +274,7 @@ def add_end_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 - :meth:`cabledyn.builder.DeckModel.add_end_connection`. + ``cabledyn.builder.DeckModel.add_end_connection``. Parameters ---------- From e739bac9290ed98db7e9bb345202573129e5b093 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Thu, 1 Oct 2026 17:53:30 +0900 Subject: [PATCH 26/26] Record the current OrcaFlex torsion script hash with its regenerated references --- tests/data/torsion_orcaflex_refs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/torsion_orcaflex_refs.txt b/tests/data/torsion_orcaflex_refs.txt index 2f4eb8a..d12cc02 100644 --- a/tests/data/torsion_orcaflex_refs.txt +++ b/tests/data/torsion_orcaflex_refs.txt @@ -1,7 +1,7 @@ # 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 5d98aba2157587ef...). Line IncludeTorsion Yes, statics Catenary then +# (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).