Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
841b4c7
docs(changelog): open the Unreleased section after 0.2.0 (#33)
SMI-Lab-Inha Oct 20, 2026
4c42daa
perf(profile): split the line and cable steps into their kernels (#34)
SMI-Lab-Inha Oct 21, 2026
eb28b20
perf(profile): split the cable assembly, CalcOutput and wave sampling…
SMI-Lab-Inha Oct 21, 2026
8c1f525
perf(openfast): skip the discarded cable reactions in CalcOutput (#36)
SMI-Lab-Inha Oct 21, 2026
da5dfad
perf(hermite): assemble a cable reaction from its node's elements (#37)
SMI-Lab-Inha Oct 21, 2026
99fbaa8
feat(init): note a dtM that does not resolve a line's axial period (#38)
SMI-Lab-Inha Oct 21, 2026
a6439d0
feat(openfast): extrapolate the coupled loads between dtM solves (#39)
SMI-Lab-Inha Oct 21, 2026
ec3c6d9
fix(openfast): restore the held SeaState fields on a supercycled rest…
SMI-Lab-Inha Oct 21, 2026
f0358ae
docs(validation): record the supercycling verification (#41)
SMI-Lab-Inha Oct 22, 2026
312179f
build(python): build platform wheels that carry the native library (#42)
SMI-Lab-Inha Oct 22, 2026
825d016
perf(build): compile the release for x86-64-v3 (AVX2) (#43)
SMI-Lab-Inha Oct 22, 2026
fc9fdb2
build(ci): build and smoke-test the release wheels (#44)
SMI-Lab-Inha Oct 22, 2026
419007e
build(ci): publish the wheels through PyPI Trusted Publishing (#45)
SMI-Lab-Inha Oct 22, 2026
ab7cdfc
fix(hermite): keep the cached committed force across snapshot and res…
SMI-Lab-Inha Oct 23, 2026
6399796
fix(output): recover node tension through touchdown on coarse meshes …
SMI-Lab-Inha Oct 23, 2026
7fa9e03
chore(release): prepare CableDyn 0.3.0
SMI-Lab-Inha Oct 23, 2026
c887917
chore(release): record the builds and checks of 0.3.0
SMI-Lab-Inha Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University
#
# The published Python distribution: an sdist and one py3-none platform wheel each for Linux
# x86-64 (manylinux_2_28), macOS arm64 and Windows x86-64, every wheel carrying the CableDyn
# library and the standalone driver. The x86-64 wheels target x86-64-v3 (AVX2), as the static
# Windows release does. Each wheel is installed into a clean environment and
# smoke-tested (release/smoke_test_wheel.py) on its own platform. The distributions are workflow
# artifacts. They are uploaded only by the two publish jobs, through PyPI Trusted Publishing
# (no stored token): to TestPyPI when a manual run asks for it, and to PyPI when a GitHub
# release is published, after checking that the distributions carry the release tag's version.
name: wheels

on:
workflow_dispatch:
inputs:
upload:
description: Upload the built distributions (TestPyPI only; PyPI uploads come from releases)
type: choice
options: [none, testpypi]
default: none
release:
types: [published]
pull_request:
paths:
- pyproject.toml
- CMakeLists.txt
- python/cabledyn/**
- release/build_windows_wheel.ps1
- release/smoke_test_wheel.py
- .github/workflows/wheels.yml

permissions:
contents: read

jobs:
sdist:
runs-on: ubuntu-latest
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

- name: Build and check the sdist
run: |
python -m pip install --upgrade pip build twine
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
python -m build --sdist --outdir dist .
python -m twine check dist/*

- name: Upload the sdist
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-sdist
path: dist/*.tar.gz
if-no-files-found: error

linux:
runs-on: ubuntu-latest
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build, repair (auditwheel) and test the manylinux wheel
uses: pypa/cibuildwheel@8c2dcd2e96041a619f5ff03c9638d7941e2553a4 # v4.3.3
env:
# one py3-none wheel serves every Python; build it once
CIBW_BUILD: cp312-manylinux_x86_64
CIBW_ARCHS_LINUX: x86_64
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
# gfortran comes with the image's GCC toolset; OpenBLAS supplies LAPACK and is bundled
# into the wheel by auditwheel together with the Fortran runtime
CIBW_BEFORE_ALL_LINUX: >-
dnf install -y epel-release && dnf install -y openblas-devel
# x86-64-v3, as the Windows release (CABLEDYN_TARGET_AVX2): the wheel's library and
# driver stop with a message on a processor without AVX2
CIBW_ENVIRONMENT_LINUX: SKBUILD_CMAKE_DEFINE="CABLEDYN_TARGET_AVX2=ON"
CIBW_TEST_COMMAND: >-
python {project}/release/smoke_test_wheel.py {project}/examples/dynamic_chain_current.dat --expect-build x86-64-v3
with:
output-dir: wheelhouse

- name: Upload the Linux wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-linux-x86_64
path: wheelhouse/*.whl
if-no-files-found: error

macos:
runs-on: macos-14
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build, repair (delocate) and test the macOS wheel
uses: pypa/cibuildwheel@8c2dcd2e96041a619f5ff03c9638d7941e2553a4 # v4.3.3
env:
CIBW_BUILD: cp312-macosx_arm64
CIBW_ARCHS_MACOS: arm64
# Homebrew gfortran and OpenBLAS; delocate bundles them with the Fortran runtime. The
# deployment target is the runner's macOS, the oldest the Homebrew libraries support.
CIBW_BEFORE_ALL_MACOS: brew install gcc openblas
CIBW_ENVIRONMENT_MACOS: >-
MACOSX_DEPLOYMENT_TARGET=14.0
FC=gfortran
CMAKE_PREFIX_PATH=$(brew --prefix openblas)
CIBW_TEST_COMMAND: >-
python {project}/release/smoke_test_wheel.py {project}/examples/dynamic_chain_current.dat --expect-build baseline
with:
output-dir: wheelhouse

- name: Upload the macOS wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-macos-arm64
path: wheelhouse/*.whl
if-no-files-found: error

windows:
timeout-minutes: 180
runs-on: windows-2022
steps:
- name: Check out CableDyn
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

- name: Install Intel Fortran Essentials
# the release compiler, as in release-windows-static.yml
shell: powershell
env:
URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/33676fcf-14a3-4e96-a9b9-72976b1145d9/intel-fortran-essentials-2025.3.1.25_offline.exe
run: |
$installer = Join-Path $env:RUNNER_TEMP 'intel-fortran-essentials-2025.3.1.25.exe'
curl.exe --fail --location --output $installer --url $env:URL --retry 5 --retry-delay 5
if ($LASTEXITCODE -ne 0) { throw "Intel Fortran download failed with exit $LASTEXITCODE" }
if ((Get-Item -LiteralPath $installer).Length -ne 880951016) {
throw 'Intel Fortran installer length differs from the pinned release payload'
}
$signature = Get-AuthenticodeSignature -LiteralPath $installer
if ($signature.Status -ne 'Valid' -or
$signature.SignerCertificate.Subject -notmatch '(^|, )O=Intel Corporation(,|$)') {
throw "Intel Fortran installer signature is not a valid Intel Corporation signature: $($signature.Status)"
}
$arguments = @(
'-s', '-a', '--silent', '--eula', 'accept',
'-p=NEED_VS2017_INTEGRATION=0',
'-p=NEED_VS2019_INTEGRATION=0',
'-p=NEED_VS2022_INTEGRATION=1',
'--log-dir=.'
)
$process = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru
if ($process.ExitCode -notin @(0, 3010)) {
throw "Intel Fortran installer failed with exit $($process.ExitCode)"
}

- name: Build the static Windows wheel
shell: powershell
run: .\release\build_windows_wheel.ps1 -OutputDir "$PWD\wheelhouse" -Jobs 4

- name: Install the wheel into a clean environment and smoke-test it
shell: powershell
run: |
$venv = Join-Path $env:RUNNER_TEMP 'wheel-test'
python -m venv $venv
& "$venv\Scripts\python.exe" -m pip install --upgrade pip
$wheel = (Get-ChildItem wheelhouse\cabledyn-*-win_amd64.whl).FullName
& "$venv\Scripts\python.exe" -m pip install $wheel
Push-Location $env:RUNNER_TEMP
try {
& "$venv\Scripts\python.exe" "$env:GITHUB_WORKSPACE\release\smoke_test_wheel.py" `
"$env:GITHUB_WORKSPACE\examples\dynamic_chain_current.dat" --expect-build x86-64-v3
if ($LASTEXITCODE -ne 0) { throw "smoke test failed ($LASTEXITCODE)" }
} finally {
Pop-Location
}

- name: Upload the Windows wheel
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cabledyn-wheel-windows-x86_64
path: wheelhouse/*.whl
if-no-files-found: error

publish-testpypi:
# manual runs that ask for it; a version already on TestPyPI is skipped, not an error
if: github.event_name == 'workflow_dispatch' && inputs.upload == 'testpypi'
needs: [sdist, linux, macos, windows]
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/cabledyn
permissions:
id-token: write
steps:
- name: Download the distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: cabledyn-*
merge-multiple: true
path: dist

- name: Check the distribution set
run: |
ls -l dist
test "$(ls dist/*.tar.gz | wc -l)" -eq 1
test "$(ls dist/*.whl | wc -l)" -eq 3

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true

publish-pypi:
# published GitHub releases only, behind the pypi environment
if: github.event_name == 'release'
needs: [sdist, linux, macos, windows]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/cabledyn
permissions:
id-token: write
steps:
- name: Download the distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: cabledyn-*
merge-multiple: true
path: dist

- name: Check the distributions against the release tag
env:
TAG: ${{ github.event.release.tag_name }}
run: |
ls -l dist
version="${TAG#v}"
test "$(ls dist/*.tar.gz | wc -l)" -eq 1
test "$(ls dist/*.whl | wc -l)" -eq 3
for f in dist/*; do
case "$(basename "$f")" in
"cabledyn-${version}.tar.gz"|"cabledyn-${version}-py3-none-"*.whl) ;;
*) echo "::error::$(basename "$f") is not version ${version} of release ${TAG}"; exit 1 ;;
esac
done

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,57 @@ All notable changes to CableDyn are recorded in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.3.0] - 2026-10-23

### Added

- `pip install cabledyn`: the package is on PyPI, with wheels for Windows x64 and Linux x86-64
(processors with AVX2) and macOS on Apple silicon that carry the solver library and the
standalone driver, so it runs decks without the release executables; other platforms build
from the source distribution. The release wheels are built and smoke-tested in CI and published
through PyPI Trusted Publishing.
- OpenFAST with `dtM` larger than `DT`: between CableDyn solves the coupled loads are extrapolated
from the last two solves instead of held (deck `OPTIONS predictLoads`, default `True`). On the
IEA-15 MW VolturnUS-S at `dtM = 0.1 s` and `DT = 0.025 s` the platform motions differ from a
`dtM = DT` run by at most 0.12 % of their standard deviation, against 0.42 % held. Runs at
`dtM = DT` (the default) do not change.
- Initialisation notes, none of which changes results: a `dtM` that resolves a line's axial
period in fewer than six steps, with the `dtM` that resolves it; and a finite-EI line whose
grounded elements lock more seabed friction than 15 % of the touchdown tension, with the
grounded element length that resolves the tension of the laid part.
- `validation/scripts/openfast_supercycle_ab.py`, which scores coupled runs at a coarser `dtM`
against `dtM = DT` (`VALIDATION.md`, supercycling verification).
- The run-time profile (`CABLEDYN_PROFILE=1`) splits the line and cable steps into their kernels
and assembly sections, and the OpenFAST `CalcOutput` and SeaState sampling into their parts.

### Changed

- Speed: the Windows release executables are compiled for processors with AVX2 (x86-64-v3:
Intel Haswell and AMD Excavator or newer), which makes the driver 13-33 % faster on the
benchmark decks; on an older processor they stop at start with a message naming the missing
instructions. In OpenFAST, `CalcOutput` no longer evaluates cable reactions it discarded and
assembles a fairlead reaction from the elements at its node (a 600 s IEA-15 MW run with a
248-element power cable: 227 s to 194 s). Results change at round-off only.
- A static solve in a current that seabed friction cannot hold fails closed sooner (22 s to
17.7 s on a 1000 m lazy wave), with the same message.
- The manual explains the mesh dependence of the negative `BA` (damping ratio) form and of
node-lumped seabed friction, and the measured trade-off of `modified_newton`, which stays off.

### Fixed

- Finite-EI node tension, curvature and bend moment at touchdown understated the last suspended
node's tension by half an element's floor reaction (5.3 % on a 10 m lazy-wave mesh); the coarse
mesh now follows a four times finer one within 3 %. End forces and the solution do not change.
- OpenFAST restarts with `dtM` larger than `DT` now continue the run bit for bit (the checkpoint
carries the held SeaState fields). A checkpoint written by an earlier CableDyn cannot be
restarted with this one.
- A restore (`CableDyn_RestoreState`, OpenFAST correction iterations) of a finite-EI cable with
seabed friction now continues bit for bit like the step it repeats.
- The FAQ and the OpenFAST tutorial gave 0.1 s as the default `dtM` in OpenFAST; it is the glue
`DT`.

## [0.2.0] - 2026-10-20

### Added
Expand Down Expand Up @@ -534,6 +585,8 @@ Corrections made during the pre-release review.
values of the shipped example decks and states which tension (end element or
`FairTen` end force) each comparison uses.

[Unreleased]: https://github.com/SMI-Lab-Inha/CableDyn/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/SMI-Lab-Inha/CableDyn/releases/tag/v0.3.0
[0.2.0]: https://github.com/SMI-Lab-Inha/CableDyn/releases/tag/v0.2.0
[0.1.1]: https://github.com/SMI-Lab-Inha/CableDyn/releases/tag/v0.1.1
[0.1.0]: https://github.com/SMI-Lab-Inha/CableDyn/releases/tag/v0.1.0
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ authors:
repository-code: "https://github.com/SMI-Lab-Inha/CableDyn"
url: "https://github.com/SMI-Lab-Inha/CableDyn"
license: Apache-2.0
version: 0.2.0
date-released: 2026-10-20
version: 0.3.0
date-released: 2026-10-23
keywords:
- mooring dynamics
- cable dynamics
Expand Down
Loading
Loading