Skip to content

CableDyn 0.3.0 - #48

Merged
SMI-Lab-Inha merged 17 commits into
mainfrom
release/0.3.0
Oct 23, 2026
Merged

SMI-Lab-Inha merged 17 commits into
mainfrom
release/0.3.0

Conversation

@SMI-Lab-Inha

Copy link
Copy Markdown
Owner

Summary

Release of CableDyn 0.3.0 from dev, following RELEASE.md. The changes since 0.2.0 are #33 to #47; CHANGELOG.md summarises them in its [0.3.0] section.

Highlights:

  • pip install cabledyn: the package is on PyPI, with wheels that carry the solver library and the driver:
    • Windows x64 and Linux x86-64, for processors with AVX2;
    • macOS on Apple silicon.
  • Speed: the release executables are built for x86-64-v3, 13–33 % faster on the benchmark decks. In OpenFAST, CalcOutput is lighter.
  • OpenFAST with dtM > DT:
    • the coupled loads are extrapolated between solves;
    • restarts continue bit for bit.
  • Restores: a restore (C API CableDyn_RestoreState, OpenFAST correction iterations) of a finite-EI cable with seabed friction continues bit for bit.
  • Touchdown on coarse meshes:
    • node tension is recovered correctly;
    • an initialisation note names the grounded element length that resolves the lumped seabed friction.

The two release commits:

  • chore(release): prepare CableDyn 0.3.0:
    • the version in every file the release check names (C API 0.3.0, ABI 1);
    • the CHANGELOG section;
    • the PyPI install instructions.
  • chore(release): record the builds and checks of 0.3.0: validation/RELEASE_0_3_0.md, and the provenance of the re-run Holcombe and Bergdahl records.

Verification (details in validation/RELEASE_0_3_0.md)

  • Release workflow (run 39898803772, the tree of the preparation commit):
    • every Windows gate passed, including the x86-64-v3 identification and the processor-check stop of both executables;
    • the OpenFAST solution build needed the script's resume after the known intermittent IFX parallel-compilation failure, then built with 0 errors.
  • Clean-machine test of the assets: 26 of 26 checks passed.
  • Release driver: all 51 shipped examples run to completion.
  • Wheels workflow (run 39898806566): the sdist and the Linux, macOS and Windows wheels were built and smoke-tested on their platforms.
  • Experimental records (Holcombe, Bergdahl), re-run with the 0.3.0 GNU build: reproduced byte for byte.
  • Local, on the release tree: ctest 298/298 (slow tier included); pytest 2052 passed; pre-commit, release metadata and the Sphinx -W build pass.
  • Tree check: the preparation commit's tree is identical to the tree the assets were built from. The record commit adds only the record and the two provenance files.

After merging: tag v0.3.0 and publish the GitHub release with the five assets. Publishing starts the PyPI upload, which waits for approval of the pypi environment.

Checklist

  • pre-commit run --all-files passes
  • ctest --test-dir build --output-on-failure passes
  • python -m pytest tests python/tests passes (if Python or validation records changed)
  • VALIDATION.md updated if a validation result changed (no result changed; the experimental provenance names v0.3.0)
  • CHANGELOG.md updated under [Unreleased] (heading added if missing) for user-visible changes
  • Documentation in doc/ updated for changed inputs, options, or outputs
  • Conventional Commit title (feat:, fix:, docs:, ...) (release PR title per RELEASE.md)
  • Commits are signed off (git commit -s) under the
    Developer Certificate of Origin

RELEASE.md closes a release by adding an empty [Unreleased] section to
the changelog; it compares against the v0.2.0 tag.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
The run-time profile (CABLEDYN_PROFILE=1) counted the steps and Newton
iterations of EI = 0 lines but did not say where their time went, and
it reported the factorisation and back-substitution of finite-EI cables
as one "linear solve". Deciding whether a reused, scheduled
factorisation is worth building needs both.

The step of an EI = 0 line now times its residual-only evaluations, its
residual and tangent evaluations, its LU factorisations and its
back-substitutions (CableDyn_Profile kernel counters, updated
atomically because lines step concurrently; their times are summed
over the lines). The finite-EI cable profile times its factorisations
apart from its back-substitutions, which now also include the
force-blend correction that ends each step. The standalone driver and
the OpenFAST module report both; the OpenFAST module arms and disarms
the line counters with its own profiler.

Unprofiled, every new entry point returns after one test: three decks
run in the same time as with 0.2.0 (ratios 0.96-1.00) and write
byte-identical outputs, and the driver tests check that every output
file is identical with and without the profile.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
…#35)

The first coupled profile showed that, on a mooring with a finite-EI
power cable, CalcOutput and the SeaState sampling cost as much as the
mooring step itself, and that the cable step is dominated by its
residual and tangent assemblies. Choosing what to optimise first needs
the inside of each.

The residual and tangent assembly of a finite-EI cable now times its
sections (band zeroing, tangent element kernels, tangent scatter,
structural residual, weight, attachments, dry buoyancy, seabed contact,
axial damping, drag, end connections, wave and held Froude-Krylov loads,
torsion). The OpenFAST module splits CalcOutput into the overlay of the
current kinematics, the output channels and the restore of the
committed state, and the SeaState sampling into the node queries, the
grid checks and the hand-over of the fields. The standalone driver and
the OpenFAST module report them.

Unprofiled, each new timer is skipped after one test; outputs are
byte-identical with and without the profile.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
At the glue rate (nsuper = 1) CalcOutput saves the committed coupled
kinematics, transfers the current ones, evaluates the loads and channels
and restores the committed state. The save read the kinematics with
CD_AGG_GetMovingPointMesh, which also evaluated the reaction of every
finite-EI cable (a full residual and added-mass assembly) only for the
caller to overwrite it with the loads evaluated after the transfer.
The profile put this at 17 of the 41 s of CalcOutput in a 600 s run of
the IEA-15 VolturnUS-S with a 248-element power cable.

CD_AGG_GetMovingPointMesh takes an optional cable_loads = .FALSE. that
skips the cable reactions and returns their load columns as zero; the
save uses it. The reactions write only the cable's scratch, which the
loads evaluation that follows overwrites, so the outputs are unchanged:
the coupled chain-only and mixed runs write byte-identical OpenFAST and
CableDyn outputs, and the mixed run takes 212 s instead of 227 s.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
The reaction of a finite-EI cable at a node (the fairlead load the
OpenFAST module returns on every CalcOutput) assembled the residual and
the added mass of the whole line and multiplied the whole mass by the
acceleration, to read three rows. With a 248-element power cable this
was 20 s of a 212 s coupled run.

assemble_residual takes an optional node_only with a caller residual
R_out: it assembles only the elements that reach the node and the
node's own terms (attachments, contact, end connection), each in the
order of the full assembly, so the node's rows are bit-identical and the
model's scratch is not touched. The contact and attachment assemblies
take node bounds, and contact writes through the residual it is given.
The reaction builds the node's rows of the added mass from the same one
or two element matrices and forms the inertia column by column as
band_matvec does. A line with torsion, which couples every element,
keeps the full assembly, as does full_assembly = .TRUE.

test_hermite_node_reaction compares the two at every node, bit for
bit, on hand-built cables with every load section (contact with
friction on a controlled seabed, attachments, axial damping, drag,
added mass, Airy waves or a held field, finite, rigid or no end
connections) at scattered states, and along a coupled trajectory of the
IEA-15 MW mixed mooring example, also after a fairlead transfer; it
fails when the inertia sum is reordered. All 51 examples write
byte-identical outputs, and the coupled chain-only and mixed OpenFAST
runs write byte-identical outputs, the mixed one in 194 s instead of
212 s.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
A coarser mooring step is the main way to make coupled runs cheaper,
but nothing told a user when dtM was too coarse for a line. With the
IEA-15 MW mixed example in a turbulent 600 s OpenFAST run, the power
cable (axial period 0.351 s) matches the dtM = DT fairlead tension
within 0.3 % of its standard deviation at dtM = 0.05 s (7 steps per
period), but at 0.1 s (3.5 steps) its axial dynamics are damped and
its peak tension is about 35 % high; the chains (0.78 s) keep their tension
statistics within 0.3 % at 0.1 s.

At initialisation each line's axial period is estimated as that of a
bar fixed at both ends, twice the sum over its sections of the length
over sqrt(EA/m) (unstretched lengths, dry mass, the dynamic EA of a
constant-stiffness viscoelastic type). A line that dtM resolves in
fewer than six steps gets an initialisation note with the period, the
number of steps and a dtM that resolves it, on the standalone, mixed
and OpenFAST routes. Load-dependent types (ElasticMod 3, Syrope) are
not assessed. Nothing else changes.

The FAQ and the OpenFAST tutorial still gave 0.1 s as the OpenFAST
default dtM; since 0.2.0 it is the glue DT.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
With dtM larger than the glue DT, CableDyn solves once per nsuper glue
steps and held its loads between solves, so the platform received them
on average dtM/2 late: a first-order lag that acts as negative damping
of about the mooring stiffness times dtM/2. This is why 0.2.0 defaults
to dtM = DT, at four times the mooring cost of dtM = 0.1 s.

Between solves the loads are now extrapolated from the last two
committed solves, F = F_c + tau (F_c - F_p) with tau the fraction of
the mooring step elapsed, clamped to [0, 1]; the error is second order
in dtM. The extrapolation is used only between consecutive commits, so
the first step keeps the held loads. CD_UpdateStates shifts the served
loads into the previous ones when a mooring step starts (not on a
correction re-entry over the same step), keeping CalcOutput state-const,
and mirrors them into the OtherState (pred_t, pred_loads), so a restart
continues the extrapolation. The output channels and the .CD.out keep
the committed samples. The deck OPTION predictLoads (default True)
switches it; False keeps the hold. The arithmetic lives in
CableDyn_LoadPredictor and is unit-tested.

IEA-15 MW VolturnUS-S, turbulent 600 s DLC-1.6, dtM = 0.1 s against
dtM = DT = 0.025 s: the platform surge, sway, roll and pitch differ by
at most 0.12 % of their standard deviation (0.42 % held), on the chain
and on the chain and power-cable mooring; the chain-only run takes a
fifth less time (116 s against 141 s). Runs at dtM = DT are
byte-identical, and with predictLoads False the outputs equal the held
runs byte for byte.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
…art (#40)

A committed mooring step holds the SeaState fields sampled at its
interval end at the node positions of its start. On openfast -restart
the module re-sampled them at the restored, end-of-step positions. At
dtM = DT this was harmless, because the next solve re-samples before
any load is served; with dtM larger than DT the loads served until the
next solve came from the re-sampled fields, and the restarted run
drifted from the uninterrupted one (up to 2e-3 of a tension's standard
deviation in the CableDyn record, on held and extrapolated loads
alike). A full-precision trace placed the first difference at the
first load served after the restart, with identical host inputs.

The OtherState carries the held fields (fluid_held: velocity,
acceleration, free-surface elevation and dynamic pressure at the line
nodes), saved at Init and at every committed step, and the restart
hands them back to the aggregate instead of re-sampling. OpenFAST
checkpoints are not versioned, so a checkpoint written by an earlier
CableDyn (without these or the load predictor's fields) cannot be
restarted with this one; the CHANGELOG says so.

openfast -restart at 50 s now reproduces every later OpenFAST and
.CD.out row of the IEA-15 VolturnUS-S chain mooring at dtM = 0.1 s
(extrapolated and held loads) and at dtM = DT, and of the chain and
power-cable mooring at dtM = 0.05 s. Uninterrupted runs do not change.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
The coupled time-step work (#38 to #40) lets CableDyn solve on a dtM
larger than the OpenFAST DT. This records the verification of that
change on the IEA-15 MW VolturnUS-S chain mooring in a DLC-1.6
environment (NTM 10.6 m/s turbulent wind, JONSWAP Hs 8 m / Tp 12 s,
3800 s), at dtM = 4 DT = 0.1 s against the same model at dtM = DT and
against stock MoorDyn.

With the coupled loads extrapolated between CableDyn solves, every
platform DOF's time series differs from the dtM = DT run by at most
0.14 % of its standard deviation (0.41 % held), the tension statistics
stay within 0.16 %, and the rainflow damage-equivalent loads change by
at most 0.21 % at the fairleads and 1.5 % at the anchors, of the order
of the reference's own sampling effect (0.94 %). Against stock MoorDyn
the in-plane platform standard deviations agree within 0.03 %.

validation/scripts/openfast_supercycle_ab.py computes these figures
from the twin runs; its rainflow count (ASTM E1049, four-point method,
half-cycle residue, a plateau of equal samples counted once) is tested
against the standard's worked example. Every statistic of a run, its DEL
change included, is taken over the samples it shares with the
reference, and a run covering fewer of them is flagged.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
The cabledyn package was published only as a pure-Python wheel: it
loads the CableDyn shared library and runs the standalone driver, but
the user had to build or download both separately. This prepares
publication on PyPI.

A root pyproject.toml (scikit-build-core) builds an sdist of the
sources and a platform wheel. CMake builds only the cabledyn_shared and
cabledyn targets, serially, and under scikit-build installs them through
the `python` component: the library beside the package modules, where
cabledyn._discovery already looks first, and the driver in
cabledyn/bin. The wheel is tagged py3-none-<platform>, since the
package reaches the library through ctypes. The sdist leaves out the
validation records, the manual, the OpenFAST integration and the release
tooling, and builds a wheel on its own.

The driver search takes the bundled driver after CABLEDYN_DRIVER and
before PATH, under either toolchain name (CableDyn_driver, cabledyn).
python/pyproject.toml stays the pure package for development against a
build tree; tests/test_packaging_metadata.py keeps the two [project]
tables identical except for the readme, licence files and platform
classifiers, and the release checks cover the new file.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
The wave kinematics and the element kernels spend most of their time in
vectorised cos/sin/exp, which the release build ran two doubles wide
(SSE2). CABLEDYN_TARGET_AVX2 compiles the Fortran core for x86-64-v3
(/arch:CORE-AVX2 with IFX, -march=x86-64-v3 with gfortran 11+), and the
static Windows release sets it: the IFX driver runs the benchmark decks
13-33% faster (lazy_wave_vessel_rao 11.9 s to 8.8 s,
als_volturnus_line_break_time 35.5 s to 23.9 s).

Contraction into fused multiply-adds stays off (/Qfma-,
-ffp-contract=off): with it the compiler fuses the same expression
differently where it is inlined, and a restored instance no longer
continued a run bit for bit (test_line_attachments).

src/cabledyn_cpu_guard.c, compiled for baseline x86-64, checks the
processor before any Fortran code runs and stops a program on an older
one with a message naming the missing instructions (exit 3). It writes
with the operating system's calls only: in the Windows GNU build the C
library names can resolve to openblas.dll's exports and import it at
load time. CABLEDYN_CPU_GUARD_SIMULATE exercises the stop on any
processor; the release script checks it for both executables, and the
driver's --version and the OpenFAST module's start-up lines report the
x86-64-v3 build.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
.github/workflows/wheels.yml builds the published distribution as
workflow artifacts (manual runs, published releases, and pull requests
that touch the packaging); publishing stays a separate step:

- sdist, checked with twine;
- Linux x86-64 (cibuildwheel, manylinux_2_28, image GCC toolset):
  OpenBLAS and the Fortran runtime bundled by auditwheel, built for
  x86-64-v3 (CABLEDYN_TARGET_AVX2), as the Windows release;
- macOS arm64 (cibuildwheel, Homebrew gfortran and OpenBLAS, bundled by
  delocate, deployment target 14.0);
- Windows x86-64 (release/build_windows_wheel.ps1): the static release
  configuration with IFX and MSVC and the release's pinned reference
  LAPACK; dumpbin checks that the library and the driver import only
  Windows system DLLs.

release/smoke_test_wheel.py installs nothing itself: run in a clean
environment with the wheel installed, it requires the package to load
the library it carries and select the driver it carries, the driver to
report the expected build (--expect-build), and a standalone deck to
run. tests/test_packaging_metadata.py keeps the reference LAPACK
version, checksum and flags of the wheel and release scripts equal.

The Windows wheel is inspected with System.IO.Compression.ZipFile and an explicit extension filter (Windows PowerShell 5.1's Expand-Archive refuses .whl, and its Get-ChildItem -LiteralPath ignores -Include), and the static release's shared library links with /Qipo like the executables.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
wheels.yml gains two publish jobs that upload the sdist and the three
release wheels its build jobs smoke-tested, with no stored token
(id-token: write only; the publishers are registered on pypi.org and
test.pypi.org for this repository, wheels.yml and the environments
pypi and testpypi):

- publish-testpypi: a manual run with upload = testpypi; a version
  already on TestPyPI is skipped;
- publish-pypi: a published GitHub release, behind the pypi environment,
  after checking that the distribution set is one sdist and three wheels
  of the release tag's version.

pypa/gh-action-pypi-publish is pinned to v1.14.2 by commit. RELEASE.md
adds the TestPyPI preflight with a clean-environment install and smoke
test, the approval step, and the publisher setup of a new repository.

DEVELOPMENT.md links RELEASE.md by its repository URL (the Sphinx manual includes DEVELOPMENT.md but not RELEASE.md).

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
…tore (#46)

A restore followed by a step must be bit-identical to taking that step from the saved state (CableDyn_RestoreState, OpenFAST correction iterations). Restore discarded the committed force cached from the previous step and evaluated it afresh. On a finite-EI cable with seabed friction the two differ in the last bits: the commit's return mapping moves a sliding node's anchor to q - c*dxy, and the spring force from q minus that anchor reproduces the capped force only to rounding. Whether it showed depended on the trajectory (the controlled-cable snapshot test failed on the Linux Debug runner only).

Snapshot now keeps the cache with the state, and Restore brings it back unless a setter invalidated the cache since the snapshot (fc_clears counts the invalidations, including the OpenFAST checkpoint reload). The control test asserts the restored cache again instead of clearing the reference's, and the cache comment states what holds. Runs without restores do not change.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
…47)

The equilibrium recovery of finite-EI node values spread each node's
floor reaction over the elements beside it by tributary length,
including the touchdown element, which lies on the floor at one end
only. That understated the tension of the last suspended node by the
floor reaction of half an element: 5.3 % on a 10 m lazy-wave mesh
against a mesh four times finer. A node's reaction is now shared by the
grounded elements beside it (both nodes on the floor); a node with none
keeps the tributary split. The coarse mesh follows the fine one within
3 % through touchdown (tests/check_touchdown_recovery.py fails on the
old recovery). The node-value cache keys on the neighbouring nodes'
positions as well, since their contact now decides an element's share.
Line end forces and the solution do not change.

The larger coarse-mesh wiggle comes from seabed friction, which acts at
the nodes: as the line lands and lifts, a grounded element locks or
releases up to mu w L, and the laid tension swings by that much (std
491, 136, 42, 33 N with 10, 5, 2.5, 1 m elements on the same cable).
At initialisation a finite-EI line whose grounded elements lock more
than 15 % of the touchdown tension now gets a note with the grounded
element length that resolves it (flat seabed; standalone and OpenFAST).

The manual explains this mesh resolution, the mesh dependence of the
negative BA (damping ratio) form, whose ratio holds for each element's
own axial mode while the line's modes get zeta n pi / (2N), and the
measured trade-off of modified_newton, which stays off by default.



A static solve in a current that fails because seabed friction holds the grounded run while the current pushes the suspended span into the touchdown ran the mesh-sequenced route to its iteration budget after the catenary-continuation route had diagnosed it; the frictionless grounded-run diagnosis already skipped that route. Both diagnoses now skip it (the symmetry-breaking pass still runs): the robustness study's 1000 m lazy wave in 440 m of water fails closed in 17.7 s instead of 22 s with the same message, and its 15 converging friction-and-current cases are unchanged.



An anchor held exactly on the floor has no contact force of its own, so the grounded-element rule of the node recovery counted its laid end element as suspended and moved the next node's whole floor reaction to the following element; the held-end floor support restored only the anchor's side, leaving a weight moment at the anchor and its neighbour (521 N.m, 31 % of w l0^2/12, on a 100 m line laid on the penalty floor of CD_HermiteCable_Dyn_Init). A held end whose end element the floor supports (held_end_floor_support) now counts as on the floor; test_hermite_cable_static checks the laid line (4.6 N.m left, the bending of the line settling into the floor).



Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
Set the version to 0.3.0 (released 2026-10-23) in every file the release metadata check names (C API 0.3.0, ABI 1), and replace the unreleased notes with a short [0.3.0] section, one entry per user-visible change. The README, the Python package README (the PyPI project page) and the installation chapter install from PyPI with pip install cabledyn, name the platforms whose wheels carry the solver, and keep the pure wheel of the release page with its driver search.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
validation/RELEASE_0_3_0.md records the source, pins, toolchains, the GNU and CI gates, the Windows executable gates of the release workflow (run 39898803772), the 26-check clean-machine test of the assets and every shipped example with the release driver, the hashes of the release assets and of the preflight PyPI distributions (run 39898806566). The Holcombe and Bergdahl records were re-run with the 0.3.0 GNU build and reproduce byte for byte; their provenance names v0.3.0.

Signed-off-by: Jae Hoon Seo <jaehoon.seo@inha.ac.kr>
@SMI-Lab-Inha

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T05:37:37.466232Z c887917 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 CableDyn | 🛠️ Build #35345049 | 📁 Comparing c887917 against latest (d3ddfeb)

  🔍 Preview build  

17 files changed · ± 17 modified

± Modified

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: c887917029

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@SMI-Lab-Inha
SMI-Lab-Inha merged commit c887917 into main Oct 23, 2026
21 checks passed
@SMI-Lab-Inha
SMI-Lab-Inha deleted the release/0.3.0 branch October 23, 2026 07:21

This branch was successfully deployed

1 active deployment
pypi — c8879170 Deployed Oct 23, 2026 by SMI-Lab-Inha via publish-pypi #13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant