cabledyn 0.3.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cabledyn/__init__.py ADDED
@@ -0,0 +1,298 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Python interfaces for the CableDyn cable and mooring dynamics solver.
3
+
4
+ The standalone-driver API works with the statically linked release executable.
5
+ The in-process :class:`CableDyn` class, and the ``abi_version``, ``abi_minor``,
6
+ ``library_path``, and ``version_string`` helpers, load the CableDyn shared
7
+ library on first access. They are deliberately left out of ``__all__`` so that
8
+ ``from cabledyn import *`` works in installations without the shared library.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from cabledyn.animation import Recorder, Seabed, Snapshots, WaterSurface, animate
16
+ from cabledyn.builder import DeckModel, DeckReferenceError, SyropeEA
17
+ from cabledyn.checks import (
18
+ API_RP_2SK_SAFETY_FACTORS,
19
+ DNV_OS_E301_PARTIAL_FACTORS,
20
+ DNV_OS_E301_STRENGTH_FACTOR,
21
+ BendCheck,
22
+ BendLimits,
23
+ TensionCheck,
24
+ bend_check,
25
+ mbr_utilisation,
26
+ tension_check,
27
+ )
28
+ from cabledyn.clearance import (
29
+ Bathymetry,
30
+ ClearanceMatrix,
31
+ LineClearance,
32
+ SeabedClearance,
33
+ clearance_matrix,
34
+ line_clearance,
35
+ read_bathymetry,
36
+ seabed_clearance,
37
+ segment_distance,
38
+ )
39
+ from cabledyn.compare import ChannelComparison, HistoryComparison, compare_histories
40
+ from cabledyn.curves import (
41
+ FatigueCurve,
42
+ api_rp_2sk_curve,
43
+ chain_nominal_area,
44
+ dnv_os_e301_curve,
45
+ dnv_rp_c203_curve,
46
+ )
47
+ from cabledyn.damage import (
48
+ ChannelDamage,
49
+ DamageProfile,
50
+ LifetimeFatigue,
51
+ SeaStateDamage,
52
+ cable_stress,
53
+ channel_damage,
54
+ damage_along_arc,
55
+ lifetime_fatigue,
56
+ miner_damage,
57
+ study_sea_state_damage,
58
+ )
59
+ from cabledyn.deck import DeckWriter
60
+ from cabledyn.deck_file import DeckFile, GeneratedCase, generate_deck_cases
61
+ from cabledyn.driver import (
62
+ CableDynDriver,
63
+ DriverError,
64
+ DriverExecutionError,
65
+ DriverNotFoundError,
66
+ DriverResult,
67
+ read_output,
68
+ )
69
+ from cabledyn.errors import (
70
+ CableDynError,
71
+ ConvergenceError,
72
+ DeckFormatError,
73
+ OutputFormatError,
74
+ StudyFormatError,
75
+ StudyOutputError,
76
+ )
77
+ from cabledyn.extremes import (
78
+ GumbelFit,
79
+ WeibullFit,
80
+ block_maxima,
81
+ fit_gumbel,
82
+ fit_weibull,
83
+ upcrossing_maxima,
84
+ )
85
+ from cabledyn.fatigue import (
86
+ FatigueResult,
87
+ RainflowCycle,
88
+ RainflowHistogram,
89
+ cycle_histogram,
90
+ damage_equivalent_range,
91
+ rainflow_cycles,
92
+ )
93
+ from cabledyn.formats import (
94
+ CoupledRun,
95
+ MoorDynLineHistory,
96
+ read_coupled_run,
97
+ read_moordyn_line,
98
+ read_moordyn_output,
99
+ read_openfast_output,
100
+ read_table,
101
+ )
102
+ from cabledyn.geometry import LineGeometry, Touchdown, line_geometry
103
+ from cabledyn.processing import fft_filter, moving_average, resample
104
+ from cabledyn.profiles import (
105
+ ArcProfile,
106
+ LineField,
107
+ LinePositions,
108
+ available_quantities,
109
+ line_field,
110
+ line_positions,
111
+ line_range_graph,
112
+ profile_at,
113
+ profiles_at,
114
+ )
115
+ from cabledyn.ranges import (
116
+ RangeGraph,
117
+ element_range_graph,
118
+ node_range_graph,
119
+ read_range_graph,
120
+ read_range_graphs,
121
+ static_range_graph,
122
+ )
123
+ from cabledyn.results import (
124
+ ChannelStatistics,
125
+ LineNodeHistory,
126
+ LineSegmentHistory,
127
+ OutputTable,
128
+ SpatialStatistics,
129
+ StaticLineSummary,
130
+ StaticProfile,
131
+ TimeHistory,
132
+ )
133
+ from cabledyn.spectra import (
134
+ CoherenceResult,
135
+ PowerSpectrum,
136
+ SpectralPeak,
137
+ magnitude_squared_coherence,
138
+ power_spectrum,
139
+ )
140
+ from cabledyn.study import StudyCaseResult, StudyResult, run_study
141
+ from cabledyn.summary import (
142
+ ChannelSummary,
143
+ LineSummary,
144
+ SummaryTable,
145
+ channel_summary,
146
+ line_summary,
147
+ )
148
+ from cabledyn.sweep import parameter_grid
149
+ from cabledyn.touchdown import TouchdownHistory, touchdown_history
150
+
151
+ if TYPE_CHECKING:
152
+ from cabledyn._lib import abi_minor as abi_minor
153
+ from cabledyn._lib import abi_version as abi_version
154
+ from cabledyn._lib import library_path as library_path
155
+ from cabledyn._lib import version_string as version_string
156
+ from cabledyn.model import CableDyn as CableDyn
157
+
158
+ __all__ = [
159
+ "API_RP_2SK_SAFETY_FACTORS",
160
+ "DNV_OS_E301_PARTIAL_FACTORS",
161
+ "DNV_OS_E301_STRENGTH_FACTOR",
162
+ "ArcProfile",
163
+ "Bathymetry",
164
+ "BendCheck",
165
+ "BendLimits",
166
+ "CableDynDriver",
167
+ "CableDynError",
168
+ "ChannelComparison",
169
+ "ChannelDamage",
170
+ "ChannelStatistics",
171
+ "ChannelSummary",
172
+ "ClearanceMatrix",
173
+ "CoherenceResult",
174
+ "ConvergenceError",
175
+ "CoupledRun",
176
+ "DamageProfile",
177
+ "DeckFile",
178
+ "DeckFormatError",
179
+ "DeckModel",
180
+ "DeckReferenceError",
181
+ "DeckWriter",
182
+ "DriverError",
183
+ "DriverExecutionError",
184
+ "DriverNotFoundError",
185
+ "DriverResult",
186
+ "FatigueCurve",
187
+ "FatigueResult",
188
+ "GeneratedCase",
189
+ "GumbelFit",
190
+ "HistoryComparison",
191
+ "LifetimeFatigue",
192
+ "LineClearance",
193
+ "LineField",
194
+ "LineGeometry",
195
+ "LineNodeHistory",
196
+ "LinePositions",
197
+ "LineSegmentHistory",
198
+ "LineSummary",
199
+ "MoorDynLineHistory",
200
+ "OutputFormatError",
201
+ "OutputTable",
202
+ "PowerSpectrum",
203
+ "RainflowCycle",
204
+ "RainflowHistogram",
205
+ "RangeGraph",
206
+ "Recorder",
207
+ "SeaStateDamage",
208
+ "Seabed",
209
+ "SeabedClearance",
210
+ "Snapshots",
211
+ "SpatialStatistics",
212
+ "SpectralPeak",
213
+ "StaticLineSummary",
214
+ "StaticProfile",
215
+ "StudyCaseResult",
216
+ "StudyFormatError",
217
+ "StudyOutputError",
218
+ "StudyResult",
219
+ "SummaryTable",
220
+ "SyropeEA",
221
+ "TensionCheck",
222
+ "TimeHistory",
223
+ "Touchdown",
224
+ "TouchdownHistory",
225
+ "WaterSurface",
226
+ "WeibullFit",
227
+ "animate",
228
+ "api_rp_2sk_curve",
229
+ "available_quantities",
230
+ "bend_check",
231
+ "block_maxima",
232
+ "cable_stress",
233
+ "chain_nominal_area",
234
+ "channel_damage",
235
+ "channel_summary",
236
+ "clearance_matrix",
237
+ "compare_histories",
238
+ "cycle_histogram",
239
+ "damage_along_arc",
240
+ "damage_equivalent_range",
241
+ "dnv_os_e301_curve",
242
+ "dnv_rp_c203_curve",
243
+ "element_range_graph",
244
+ "fft_filter",
245
+ "fit_gumbel",
246
+ "fit_weibull",
247
+ "generate_deck_cases",
248
+ "lifetime_fatigue",
249
+ "line_clearance",
250
+ "line_field",
251
+ "line_geometry",
252
+ "line_positions",
253
+ "line_range_graph",
254
+ "line_summary",
255
+ "magnitude_squared_coherence",
256
+ "mbr_utilisation",
257
+ "miner_damage",
258
+ "moving_average",
259
+ "node_range_graph",
260
+ "parameter_grid",
261
+ "power_spectrum",
262
+ "profile_at",
263
+ "profiles_at",
264
+ "rainflow_cycles",
265
+ "read_bathymetry",
266
+ "read_coupled_run",
267
+ "read_moordyn_line",
268
+ "read_moordyn_output",
269
+ "read_openfast_output",
270
+ "read_output",
271
+ "read_range_graph",
272
+ "read_range_graphs",
273
+ "read_table",
274
+ "resample",
275
+ "run_study",
276
+ "seabed_clearance",
277
+ "segment_distance",
278
+ "static_range_graph",
279
+ "study_sea_state_damage",
280
+ "tension_check",
281
+ "touchdown_history",
282
+ "upcrossing_maxima",
283
+ ]
284
+
285
+ __version__ = "0.3.0"
286
+
287
+
288
+ def __getattr__(name: str) -> Any:
289
+ """Load the optional shared-library API only when requested."""
290
+ if name == "CableDyn":
291
+ from cabledyn.model import CableDyn
292
+
293
+ return CableDyn
294
+ if name in {"abi_minor", "abi_version", "library_path", "version_string"}:
295
+ from cabledyn import _lib
296
+
297
+ return getattr(_lib, name)
298
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
cabledyn/_csv.py ADDED
@@ -0,0 +1,44 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Atomic CSV writing shared by the design-check and range-graph results."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextlib
7
+ import csv
8
+ import os
9
+ import tempfile
10
+ from collections.abc import Iterable, Sequence
11
+ from pathlib import Path
12
+
13
+
14
+ def number(value: float) -> str:
15
+ """Format a float with full round-trip precision."""
16
+ return f"{float(value):.17g}"
17
+
18
+
19
+ def write_csv(
20
+ path: str | os.PathLike[str],
21
+ header: Sequence[str],
22
+ rows: Iterable[Sequence[object]],
23
+ *,
24
+ overwrite: bool,
25
+ ) -> Path:
26
+ """Atomically write ``rows`` under ``header``; return the absolute path."""
27
+ target = Path(path).expanduser().resolve()
28
+ if target.exists() and not overwrite:
29
+ raise FileExistsError(f"output already exists: {target}")
30
+ target.parent.mkdir(parents=True, exist_ok=True)
31
+ fd, temporary = tempfile.mkstemp(
32
+ prefix=f".{target.name}.", suffix=".tmp", dir=target.parent, text=True
33
+ )
34
+ try:
35
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream:
36
+ writer = csv.writer(stream, lineterminator="\n")
37
+ writer.writerow(header)
38
+ writer.writerows(rows)
39
+ os.replace(temporary, target)
40
+ except BaseException:
41
+ with contextlib.suppress(FileNotFoundError):
42
+ os.unlink(temporary)
43
+ raise
44
+ return target
cabledyn/_discovery.py ADDED
@@ -0,0 +1,163 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Locate the CableDyn shared library without loading it.
3
+
4
+ The search order is:
5
+
6
+ 1. ``CABLEDYN_LIBRARY``: an exact path, used as given.
7
+ 2. The package directory, for a wheel or deployment that bundles the library.
8
+ 3. The CMake build tree of a source checkout (``<repo>/build``).
9
+
10
+ In the build tree only linker products are considered: ``build/bin`` and
11
+ ``build`` for a single-configuration generator, and ``build/bin/<Config>`` and
12
+ ``build/<Config>`` for a multi-configuration generator. The self-contained
13
+ ``build/runtime`` bundle is a copy and is never auto-selected, so it cannot
14
+ shadow a newer build. When more than one configuration holds a library,
15
+ ``CABLEDYN_BUILD_CONFIG`` must name the intended one; discovery never guesses.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ CMAKE_CONFIGURATIONS = ("Release", "RelWithDebInfo", "MinSizeRel", "Debug")
25
+ _UNCONFIGURED = "single-configuration"
26
+
27
+
28
+ def library_basenames(os_name: str | None = None, platform: str | None = None) -> tuple[str, ...]:
29
+ """Return the shared-library file names loadable by the given (or current) host."""
30
+ host_os = os.name if os_name is None else os_name
31
+ host_platform = sys.platform if platform is None else platform
32
+ if host_os == "nt":
33
+ # GNU prefixes the DLL name; IFX/MSVC use the unprefixed name.
34
+ return ("libcabledyn.dll", "cabledyn.dll")
35
+ if host_platform == "darwin":
36
+ return ("libcabledyn.dylib",)
37
+ return ("libcabledyn.so",)
38
+
39
+
40
+ def _existing(directory: Path, basenames: tuple[str, ...]) -> list[Path]:
41
+ """Return the libraries present in one directory, rejecting ambiguous pairs."""
42
+ found = [directory / name for name in basenames if (directory / name).is_file()]
43
+ if len(found) > 1:
44
+ names = ", ".join(path.name for path in found)
45
+ raise OSError(
46
+ f"cabledyn: several CableDyn shared libraries exist in {directory} ({names}). "
47
+ "Remove the stale product or set CABLEDYN_LIBRARY to the exact library path."
48
+ )
49
+ return found
50
+
51
+
52
+ def build_tree_layouts(build: Path) -> dict[str, tuple[Path, ...]]:
53
+ """Return the linker-product directories of each CMake layout, in search order."""
54
+ layouts: dict[str, tuple[Path, ...]] = {_UNCONFIGURED: (build / "bin", build)}
55
+ for configuration in CMAKE_CONFIGURATIONS:
56
+ layouts[configuration] = (build / "bin" / configuration, build / configuration)
57
+ return layouts
58
+
59
+
60
+ def selected_configuration(value: str | None = None) -> str | None:
61
+ """Return the canonical ``CABLEDYN_BUILD_CONFIG`` value, or ``None`` when unset."""
62
+ requested = (os.environ.get("CABLEDYN_BUILD_CONFIG", "") if value is None else value).strip()
63
+ if not requested:
64
+ return None
65
+ for configuration in CMAKE_CONFIGURATIONS:
66
+ if configuration.lower() == requested.lower():
67
+ return configuration
68
+ choices = ", ".join(CMAKE_CONFIGURATIONS)
69
+ raise OSError(f"cabledyn: CABLEDYN_BUILD_CONFIG must be one of {choices}; got {requested!r}")
70
+
71
+
72
+ def find_in_build_tree(
73
+ build: Path,
74
+ basenames: tuple[str, ...],
75
+ configuration: str | None,
76
+ ) -> tuple[Path | None, list[Path]]:
77
+ """Return ``(library, searched)`` for a CMake build tree.
78
+
79
+ ``library`` is ``None`` when nothing was found. Several populated layouts
80
+ without an explicit ``configuration`` raise :class:`OSError`.
81
+ """
82
+ layouts = build_tree_layouts(build)
83
+ searched = [
84
+ directory / name for dirs in layouts.values() for directory in dirs for name in basenames
85
+ ]
86
+ # An explicit configuration also accepts a single-configuration build whose
87
+ # CMAKE_BUILD_TYPE is not encoded in the directory layout.
88
+ chosen = [configuration, _UNCONFIGURED] if configuration is not None else list(layouts)
89
+ populated: dict[str, Path] = {}
90
+ for name in chosen:
91
+ for directory in layouts[name]:
92
+ found = _existing(directory, basenames)
93
+ if found:
94
+ populated[name] = found[0]
95
+ break
96
+ if configuration is not None:
97
+ if configuration in populated:
98
+ return populated[configuration], searched
99
+ if _UNCONFIGURED in populated and not any(
100
+ _existing(directory, basenames)
101
+ for name in CMAKE_CONFIGURATIONS
102
+ for directory in layouts[name]
103
+ ):
104
+ return populated[_UNCONFIGURED], searched
105
+ return None, searched
106
+ if len(populated) > 1:
107
+ layouts_found = ", ".join(sorted(populated))
108
+ raise OSError(
109
+ f"cabledyn: {build} contains CableDyn libraries for several build layouts "
110
+ f"({layouts_found}). "
111
+ "Set CABLEDYN_BUILD_CONFIG to the intended configuration or CABLEDYN_LIBRARY to the "
112
+ "exact library path."
113
+ )
114
+ return (next(iter(populated.values())) if populated else None), searched
115
+
116
+
117
+ def find_library(package_directory: Path | None = None) -> Path:
118
+ """Return the CableDyn shared library to load, or raise :class:`OSError`."""
119
+ exact = os.environ.get("CABLEDYN_LIBRARY", "").strip()
120
+ if exact:
121
+ path = Path(exact).expanduser()
122
+ if not path.is_file():
123
+ raise OSError(f"cabledyn: CABLEDYN_LIBRARY does not name an existing file: {exact}")
124
+ return path.resolve()
125
+
126
+ package = Path(__file__).resolve().parent if package_directory is None else package_directory
127
+ basenames = library_basenames()
128
+ bundled = _existing(package, basenames)
129
+ if bundled:
130
+ return bundled[0]
131
+
132
+ # Source checkout: <repo>/python/cabledyn -> <repo>/build.
133
+ build = package.parent.parent / "build"
134
+ library, searched = find_in_build_tree(build, basenames, selected_configuration())
135
+ if library is not None:
136
+ return library.resolve()
137
+ tried = [package / name for name in basenames] + searched
138
+ raise OSError(
139
+ "cabledyn: could not locate the CableDyn shared library. Set CABLEDYN_LIBRARY to its "
140
+ "full path, or build it with `cmake --build build --target cabledyn_shared`. Tried:\n "
141
+ + "\n ".join(str(path) for path in tried)
142
+ )
143
+
144
+
145
+ def dependency_directories(library: Path) -> list[Path]:
146
+ """Return directories that may hold the library's toolchain runtime DLLs.
147
+
148
+ The library's own directory comes first. For a multi-configuration build
149
+ tree the GNU/OpenBLAS runtime set is staged once in ``build/bin`` and
150
+ ``build/runtime`` rather than beside each configuration's product.
151
+ """
152
+ directories = [library.parent]
153
+ for ancestor in library.parents:
154
+ if ancestor.name == "build":
155
+ directories.extend(
156
+ path for path in (ancestor / "bin", ancestor / "runtime") if path.is_dir()
157
+ )
158
+ break
159
+ unique: list[Path] = []
160
+ for directory in directories:
161
+ if directory not in unique:
162
+ unique.append(directory)
163
+ return unique