rbscan 0.2.1__tar.gz
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.
- rbscan-0.2.1/.clang-format +6 -0
- rbscan-0.2.1/.gitattributes +2 -0
- rbscan-0.2.1/.gitignore +27 -0
- rbscan-0.2.1/.gitlab-ci.yml +503 -0
- rbscan-0.2.1/CHANGELOG.md +51 -0
- rbscan-0.2.1/CMakeLists.txt +52 -0
- rbscan-0.2.1/CMakePresets.json +54 -0
- rbscan-0.2.1/CONTRIBUTING.md +43 -0
- rbscan-0.2.1/LICENSE +21 -0
- rbscan-0.2.1/LICENSES/HDF5-LBNL.txt +61 -0
- rbscan-0.2.1/LICENSES/HDF5.txt +106 -0
- rbscan-0.2.1/LICENSES/LZ4.txt +24 -0
- rbscan-0.2.1/LICENSES/libdeflate.txt +22 -0
- rbscan-0.2.1/LICENSES/nlohmann_json.txt +21 -0
- rbscan-0.2.1/LICENSES/pybind11.txt +29 -0
- rbscan-0.2.1/LICENSES/zlib.txt +22 -0
- rbscan-0.2.1/PKG-INFO +351 -0
- rbscan-0.2.1/README.md +311 -0
- rbscan-0.2.1/SECURITY.md +13 -0
- rbscan-0.2.1/THIRD_PARTY_NOTICES.md +24 -0
- rbscan-0.2.1/ci/linux.sh +176 -0
- rbscan-0.2.1/cmake/rbscanConfig.cmake.in +10 -0
- rbscan-0.2.1/cmake/rbscan_deps.cmake +175 -0
- rbscan-0.2.1/cmake/rbscan_embed_schemas.cmake +51 -0
- rbscan-0.2.1/cmake/rbscan_warnings.cmake +16 -0
- rbscan-0.2.1/docs/format.md +270 -0
- rbscan-0.2.1/examples/CMakeLists.txt +6 -0
- rbscan-0.2.1/examples/read_benchmark.cpp +83 -0
- rbscan-0.2.1/examples/record_ct_scan.cpp +236 -0
- rbscan-0.2.1/include/raybotic/scan/export.hpp +25 -0
- rbscan-0.2.1/include/raybotic/scan/rbscan.hpp +19 -0
- rbscan-0.2.1/include/raybotic/scan/reader.hpp +113 -0
- rbscan-0.2.1/include/raybotic/scan/schema.hpp +141 -0
- rbscan-0.2.1/include/raybotic/scan/tools.hpp +187 -0
- rbscan-0.2.1/include/raybotic/scan/types.hpp +202 -0
- rbscan-0.2.1/include/raybotic/scan/writer.hpp +239 -0
- rbscan-0.2.1/pyproject.toml +66 -0
- rbscan-0.2.1/python/CMakeLists.txt +47 -0
- rbscan-0.2.1/python/examples/compare_readers.py +155 -0
- rbscan-0.2.1/python/examples/inspect_scan.py +33 -0
- rbscan-0.2.1/python/examples/record_ct_scan.py +183 -0
- rbscan-0.2.1/python/rbscan/__init__.py +151 -0
- rbscan-0.2.1/python/rbscan/__main__.py +101 -0
- rbscan-0.2.1/python/src/bindings.cpp +731 -0
- rbscan-0.2.1/python/tests/conftest.py +16 -0
- rbscan-0.2.1/python/tests/test_crash.py +89 -0
- rbscan-0.2.1/python/tests/test_rbscan.py +279 -0
- rbscan-0.2.1/python/tests/test_tools.py +132 -0
- rbscan-0.2.1/schemas/annotations.json +45 -0
- rbscan-0.2.1/schemas/camera.json +27 -0
- rbscan-0.2.1/schemas/ct_acquisition.json +51 -0
- rbscan-0.2.1/schemas/depth_camera.json +27 -0
- rbscan-0.2.1/schemas/ezrt_import.json +24 -0
- rbscan-0.2.1/schemas/force_torque.json +19 -0
- rbscan-0.2.1/schemas/profile_scanner.json +24 -0
- rbscan-0.2.1/schemas/reconstruction.json +32 -0
- rbscan-0.2.1/schemas/robot_arm.json +39 -0
- rbscan-0.2.1/schemas/sensor.json +24 -0
- rbscan-0.2.1/schemas/trajectory_plan.json +48 -0
- rbscan-0.2.1/schemas/transform.json +20 -0
- rbscan-0.2.1/schemas/volume.json +44 -0
- rbscan-0.2.1/schemas/xray_detector.json +54 -0
- rbscan-0.2.1/schemas/xray_source.json +43 -0
- rbscan-0.2.1/src/CMakeLists.txt +92 -0
- rbscan-0.2.1/src/compression.cpp +568 -0
- rbscan-0.2.1/src/compression.hpp +47 -0
- rbscan-0.2.1/src/convert.cpp +87 -0
- rbscan-0.2.1/src/convert.hpp +15 -0
- rbscan-0.2.1/src/geometry.cpp +476 -0
- rbscan-0.2.1/src/h5.cpp +636 -0
- rbscan-0.2.1/src/h5.hpp +91 -0
- rbscan-0.2.1/src/internal.cpp +135 -0
- rbscan-0.2.1/src/internal.hpp +46 -0
- rbscan-0.2.1/src/pose_math.hpp +87 -0
- rbscan-0.2.1/src/raw_file.cpp +82 -0
- rbscan-0.2.1/src/raw_file.hpp +33 -0
- rbscan-0.2.1/src/reader.cpp +732 -0
- rbscan-0.2.1/src/schema.cpp +403 -0
- rbscan-0.2.1/src/segmented_writer.cpp +356 -0
- rbscan-0.2.1/src/series_layout.cpp +210 -0
- rbscan-0.2.1/src/series_layout.hpp +79 -0
- rbscan-0.2.1/src/thread_pool.hpp +70 -0
- rbscan-0.2.1/src/tools.cpp +762 -0
- rbscan-0.2.1/src/types.cpp +93 -0
- rbscan-0.2.1/src/vds.cpp +177 -0
- rbscan-0.2.1/src/writer.cpp +502 -0
- rbscan-0.2.1/src/writer_impl.hpp +129 -0
- rbscan-0.2.1/tests/CMakeLists.txt +26 -0
- rbscan-0.2.1/tests/consumer/CMakeLists.txt +21 -0
- rbscan-0.2.1/tests/consumer/main.cpp +52 -0
- rbscan-0.2.1/tests/scan_test.cpp +378 -0
- rbscan-0.2.1/tests/schema_test.cpp +84 -0
- rbscan-0.2.1/tests/segmented_test.cpp +344 -0
- rbscan-0.2.1/tests/test_util.hpp +34 -0
- rbscan-0.2.1/tests/tools_test.cpp +337 -0
- rbscan-0.2.1/tools/CMakeLists.txt +10 -0
- rbscan-0.2.1/tools/rbscan_cli.cpp +165 -0
- rbscan-0.2.1/uv.lock +634 -0
rbscan-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Build trees and outputs
|
|
2
|
+
build/
|
|
3
|
+
build-*/
|
|
4
|
+
build_*/
|
|
5
|
+
dist/
|
|
6
|
+
out/
|
|
7
|
+
tmp/
|
|
8
|
+
|
|
9
|
+
# HDF5 / zlib superbuild (sources, build trees, install)
|
|
10
|
+
.deps/
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
.venv/
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
|
|
19
|
+
# IDEs and local agent directories
|
|
20
|
+
.vs/
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
.claude/
|
|
24
|
+
CMakeUserPresets.json
|
|
25
|
+
|
|
26
|
+
# Local viewer layout (Dear ImGui)
|
|
27
|
+
raybotic_viewer.ini
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# GitLab CI/CD Pipeline for rbscan (RayBotic Scan Data Format)
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
# Stages:
|
|
5
|
+
# 1. lint - clang-format check, release tag matches the CMake version
|
|
6
|
+
# 2. build - CMake Release build (C++ shared library, CLI tool, tests)
|
|
7
|
+
# 3. test - C++ GoogleTest + Python pytest suite via CTest (JUnit report)
|
|
8
|
+
# 4. package - C++ SDK (DLLs/.so, headers, CMake config, licenses), a build of
|
|
9
|
+
# a separate project against that SDK, and Python wheels.
|
|
10
|
+
# Published to the GitLab Generic Package Registry
|
|
11
|
+
# (packages/generic/rbscan/<version>/) on the default branch
|
|
12
|
+
# (<version>-dev.<sha>) and on release tags (<version>).
|
|
13
|
+
# 5. release - GitLab release for a tag, notes from CHANGELOG.md
|
|
14
|
+
# 6. publish - wheels + sdist to PyPI on a tag (Trusted Publishing, no
|
|
15
|
+
# stored token); TestPyPI by hand (manual job)
|
|
16
|
+
#
|
|
17
|
+
# Linux jobs run in the manylinux_2_28 image, so the wheels can be repaired
|
|
18
|
+
# into manylinux wheels that PyPI accepts.
|
|
19
|
+
#
|
|
20
|
+
# Windows jobs run on the runner tagged $RBSCAN_WINDOWS_RUNNER_TAG, Linux jobs
|
|
21
|
+
# on the one tagged $RBSCAN_LINUX_RUNNER_TAG. Leave RBSCAN_LINUX_RUNNER_TAG
|
|
22
|
+
# empty (CI/CD variable or "Run pipeline" form) to skip the Linux jobs.
|
|
23
|
+
#
|
|
24
|
+
# Releasing: set the version in CMakeLists.txt, add it to CHANGELOG.md, merge
|
|
25
|
+
# to main and push the tag v<version>.
|
|
26
|
+
# ==============================================================================
|
|
27
|
+
|
|
28
|
+
stages:
|
|
29
|
+
- lint
|
|
30
|
+
- build
|
|
31
|
+
- test
|
|
32
|
+
- package
|
|
33
|
+
- release
|
|
34
|
+
- publish
|
|
35
|
+
|
|
36
|
+
workflow:
|
|
37
|
+
rules:
|
|
38
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
39
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
40
|
+
- if: $CI_COMMIT_TAG
|
|
41
|
+
- if: $CI_PIPELINE_SOURCE == "web"
|
|
42
|
+
|
|
43
|
+
variables:
|
|
44
|
+
RBSCAN_WINDOWS_RUNNER_TAG: "raybot1"
|
|
45
|
+
RBSCAN_LINUX_RUNNER_TAG: "raybot-linux"
|
|
46
|
+
# Keep .deps/ (prebuilt HDF5 + zlib, uv + Python toolchain) and build trees
|
|
47
|
+
# across jobs; tag pipelines still build from scratch.
|
|
48
|
+
GIT_CLEAN_FLAGS: "none"
|
|
49
|
+
GIT_CONFIG_COUNT: "1"
|
|
50
|
+
GIT_CONFIG_KEY_0: "safe.directory"
|
|
51
|
+
GIT_CONFIG_VALUE_0: "*"
|
|
52
|
+
# uv is installed into .deps/uv at exactly this version.
|
|
53
|
+
UV_VERSION: "0.12.5"
|
|
54
|
+
# Python versions for which wheels are built in the package stage (via uv)
|
|
55
|
+
RBSCAN_WHEEL_PYTHONS: "3.11 3.12 3.13 3.14"
|
|
56
|
+
|
|
57
|
+
.windows:
|
|
58
|
+
tags:
|
|
59
|
+
- $RBSCAN_WINDOWS_RUNNER_TAG
|
|
60
|
+
|
|
61
|
+
.linux:
|
|
62
|
+
image: quay.io/pypa/manylinux_2_28_x86_64
|
|
63
|
+
tags:
|
|
64
|
+
- $RBSCAN_LINUX_RUNNER_TAG
|
|
65
|
+
rules:
|
|
66
|
+
- if: $RBSCAN_LINUX_RUNNER_TAG
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# Shared: uv (pinned) + .venv (with Python headers, numpy, pybind11, pytest, h5py, hdf5plugin)
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# The GitLab Runner service runs as LocalSystem, where user-installed uv/Python
|
|
72
|
+
# are not in the PATH. uv is installed into .deps/uv inside the workspace.
|
|
73
|
+
.setup_python: &setup_python |
|
|
74
|
+
$ErrorActionPreference = "Stop"
|
|
75
|
+
$uvExe = "$PWD/.deps/uv/uv.exe"
|
|
76
|
+
$uvHave = if (Test-Path $uvExe) { (& $uvExe --version).Split(' ')[1] } else { "" }
|
|
77
|
+
if ($uvHave -ne $env:UV_VERSION) {
|
|
78
|
+
Write-Host "Installing uv $env:UV_VERSION into $PWD/.deps/uv..."
|
|
79
|
+
$env:UV_INSTALL_DIR = "$PWD/.deps/uv"
|
|
80
|
+
$env:UV_NO_MODIFY_PATH = "1"
|
|
81
|
+
irm "https://astral.sh/uv/$env:UV_VERSION/install.ps1" | iex
|
|
82
|
+
}
|
|
83
|
+
$env:PATH = "$PWD/.deps/uv;$env:PATH"
|
|
84
|
+
$env:UV_PYTHON_INSTALL_DIR = "$PWD/.deps/uv-python"
|
|
85
|
+
$env:UV_CACHE_DIR = "$PWD/.deps/uv-cache"
|
|
86
|
+
|
|
87
|
+
$venvPython = "$PWD/.venv/Scripts/python.exe"
|
|
88
|
+
if (-not (Test-Path $venvPython)) {
|
|
89
|
+
Write-Host "Creating .venv with Python 3.12 via uv..."
|
|
90
|
+
uv venv --python 3.12 .venv
|
|
91
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
92
|
+
}
|
|
93
|
+
uv pip install --python $venvPython "pybind11>=3.0" "numpy>=1.24" "pytest>=8" "h5py>=3.10" "hdf5plugin>=4.4"
|
|
94
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
95
|
+
$cmakePython = ($venvPython -replace '\\', '/')
|
|
96
|
+
Write-Host "Using Python for CMake: $cmakePython"
|
|
97
|
+
|
|
98
|
+
# Package version: the one in CMakeLists.txt; on a tag the tag must match it,
|
|
99
|
+
# elsewhere <version>-dev.<short sha>.
|
|
100
|
+
.release_version: &release_version |
|
|
101
|
+
$cmakeVersion = (Select-String -Path CMakeLists.txt -Pattern '^\s+VERSION ([0-9]+\.[0-9]+\.[0-9]+)' | Select-Object -First 1).Matches[0].Groups[1].Value
|
|
102
|
+
if ($env:CI_COMMIT_TAG) {
|
|
103
|
+
if (($env:CI_COMMIT_TAG -replace '^v', '') -ne $cmakeVersion) {
|
|
104
|
+
Write-Error "Tag $env:CI_COMMIT_TAG does not match the version $cmakeVersion in CMakeLists.txt"
|
|
105
|
+
exit 1
|
|
106
|
+
}
|
|
107
|
+
$version = $cmakeVersion
|
|
108
|
+
} else {
|
|
109
|
+
$version = "$cmakeVersion-dev.$env:CI_COMMIT_SHORT_SHA"
|
|
110
|
+
}
|
|
111
|
+
Write-Host "Package version: $version"
|
|
112
|
+
|
|
113
|
+
.configure_windows: &configure_windows |
|
|
114
|
+
cmake -B build -S . -G "Visual Studio 17 2022" -A x64 `
|
|
115
|
+
-UPython* `
|
|
116
|
+
"-DPython_EXECUTABLE=$cmakePython" `
|
|
117
|
+
-DRBSCAN_BUILD_TESTS=ON `
|
|
118
|
+
-DRBSCAN_BUILD_PYTHON=ON `
|
|
119
|
+
-DRBSCAN_BUILD_EXAMPLES=ON `
|
|
120
|
+
-DRBSCAN_BUILD_TOOLS=ON `
|
|
121
|
+
-DRBSCAN_WARNINGS_AS_ERRORS=ON
|
|
122
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
123
|
+
cmake --build build --config Release --parallel
|
|
124
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# Stage: Lint & Version Check
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
lint:clang-format:
|
|
130
|
+
extends: .windows
|
|
131
|
+
stage: lint
|
|
132
|
+
script:
|
|
133
|
+
- |
|
|
134
|
+
$clangFormat = Get-Command clang-format -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
|
|
135
|
+
if (-not $clangFormat) {
|
|
136
|
+
$searchPaths = @(
|
|
137
|
+
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin\clang-format.exe",
|
|
138
|
+
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin\clang-format.exe",
|
|
139
|
+
"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\x64\bin\clang-format.exe",
|
|
140
|
+
"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\bin\clang-format.exe",
|
|
141
|
+
"C:\Program Files\LLVM\bin\clang-format.exe"
|
|
142
|
+
)
|
|
143
|
+
foreach ($p in $searchPaths) {
|
|
144
|
+
if (Test-Path $p) {
|
|
145
|
+
$clangFormat = $p
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (-not $clangFormat) {
|
|
151
|
+
Write-Error "clang-format not found on the runner (install the VS 'C++ Clang tools' or LLVM)."
|
|
152
|
+
exit 1
|
|
153
|
+
}
|
|
154
|
+
Write-Host "Using clang-format from: $clangFormat"
|
|
155
|
+
& $clangFormat --version
|
|
156
|
+
$files = Get-ChildItem -Path src, include, tools, examples, tests, python -Include *.h, *.cpp, *.hpp -Recurse
|
|
157
|
+
$failedFiles = @()
|
|
158
|
+
foreach ($file in $files) {
|
|
159
|
+
$output = & $clangFormat --dry-run --Werror $file.FullName 2>&1
|
|
160
|
+
if ($LASTEXITCODE -ne 0) {
|
|
161
|
+
Write-Host "Formatting violation in: $($file.FullName)"
|
|
162
|
+
$failedFiles += $file.FullName
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if ($failedFiles.Count -gt 0) {
|
|
166
|
+
Write-Error "Found $($failedFiles.Count) files with clang-format style violations."
|
|
167
|
+
exit 1
|
|
168
|
+
}
|
|
169
|
+
Write-Host "All C++ files are compliant with .clang-format."
|
|
170
|
+
|
|
171
|
+
lint:version:
|
|
172
|
+
extends: .windows
|
|
173
|
+
stage: lint
|
|
174
|
+
rules:
|
|
175
|
+
- if: $CI_COMMIT_TAG
|
|
176
|
+
script:
|
|
177
|
+
- *release_version
|
|
178
|
+
- |
|
|
179
|
+
$heading = "## [$version]"
|
|
180
|
+
if (-not (Select-String -Path CHANGELOG.md -SimpleMatch $heading -Quiet)) {
|
|
181
|
+
Write-Error "CHANGELOG.md has no section '$heading'"
|
|
182
|
+
exit 1
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Stage: Build Release Binaries
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
build:windows:
|
|
189
|
+
extends: .windows
|
|
190
|
+
stage: build
|
|
191
|
+
script:
|
|
192
|
+
- *setup_python
|
|
193
|
+
- |
|
|
194
|
+
$ErrorActionPreference = "Stop"
|
|
195
|
+
Stop-Process -Name "rbscan", "rbscan_tests", "rbscan_record_ct_scan", "rbscan_read_benchmark" -Force -ErrorAction SilentlyContinue
|
|
196
|
+
# Release tags always start from a clean build tree.
|
|
197
|
+
if ($env:CI_COMMIT_TAG -and (Test-Path build)) { Remove-Item -Path build -Recurse -Force }
|
|
198
|
+
- *configure_windows
|
|
199
|
+
|
|
200
|
+
build:linux:
|
|
201
|
+
extends: .linux
|
|
202
|
+
stage: build
|
|
203
|
+
needs: []
|
|
204
|
+
script:
|
|
205
|
+
- ci/linux.sh build
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Stage: Execute CTest Suite & Export JUnit Report
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
test:windows:
|
|
211
|
+
extends: .windows
|
|
212
|
+
stage: test
|
|
213
|
+
needs:
|
|
214
|
+
- build:windows
|
|
215
|
+
script:
|
|
216
|
+
- *setup_python
|
|
217
|
+
- |
|
|
218
|
+
$ErrorActionPreference = "Stop"
|
|
219
|
+
if (-not (Test-Path "build/bin/Release/rbscan.dll")) {
|
|
220
|
+
Write-Error "No Release build in build/ (build:windows must run on this runner first)"
|
|
221
|
+
exit 1
|
|
222
|
+
}
|
|
223
|
+
ctest --test-dir build -C Release --output-on-failure --output-junit ctest_report.xml
|
|
224
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
225
|
+
artifacts:
|
|
226
|
+
when: always
|
|
227
|
+
expire_in: 14 days
|
|
228
|
+
reports:
|
|
229
|
+
junit: build/ctest_report.xml
|
|
230
|
+
|
|
231
|
+
test:linux:
|
|
232
|
+
extends: .linux
|
|
233
|
+
stage: test
|
|
234
|
+
needs:
|
|
235
|
+
- build:linux
|
|
236
|
+
script:
|
|
237
|
+
- ci/linux.sh test
|
|
238
|
+
artifacts:
|
|
239
|
+
when: always
|
|
240
|
+
expire_in: 14 days
|
|
241
|
+
reports:
|
|
242
|
+
junit: build-linux/ctest_report.xml
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Stage: Package SDK & Python Wheels, publish to the Package Registry
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
package:windows:
|
|
248
|
+
extends: .windows
|
|
249
|
+
stage: package
|
|
250
|
+
needs:
|
|
251
|
+
- build:windows
|
|
252
|
+
- test:windows
|
|
253
|
+
script:
|
|
254
|
+
- *setup_python
|
|
255
|
+
- *release_version
|
|
256
|
+
- |
|
|
257
|
+
$ErrorActionPreference = "Stop"
|
|
258
|
+
if (-not (Test-Path "build/bin/Release/rbscan.dll")) {
|
|
259
|
+
Write-Error "No Release build in build/ (build:windows must run on this runner first)"
|
|
260
|
+
exit 1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Clean previous packaging output (workspace persists on runner)
|
|
264
|
+
if (Test-Path "dist") { Remove-Item -Path "dist" -Recurse -Force }
|
|
265
|
+
New-Item -ItemType Directory -Path "dist/dll", "dist/wheels", "dist/sdk" -Force | Out-Null
|
|
266
|
+
|
|
267
|
+
# 1. Install the C++ SDK (rbscan.dll, rbscan_hdf5.dll, rbscan.exe, rbscan.lib,
|
|
268
|
+
# headers, CMake package config, embedded schemas and licenses)
|
|
269
|
+
cmake --install build --config Release --component sdk --prefix dist/sdk
|
|
270
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
271
|
+
|
|
272
|
+
# A separate project must find and use the installed SDK.
|
|
273
|
+
if (Test-Path "build/consumer") { Remove-Item -Path "build/consumer" -Recurse -Force }
|
|
274
|
+
cmake -S tests/consumer -B build/consumer -G "Visual Studio 17 2022" -A x64 "-DCMAKE_PREFIX_PATH=$PWD/dist/sdk"
|
|
275
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
276
|
+
cmake --build build/consumer --config Release
|
|
277
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
278
|
+
& build/consumer/Release/rbscan_consumer.exe build
|
|
279
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
280
|
+
|
|
281
|
+
# Copy the DLLs, import lib, and CLI executable to dist/dll/ for direct access
|
|
282
|
+
Copy-Item -Path "dist/sdk/bin/rbscan.dll" -Destination "dist/dll/" -Force
|
|
283
|
+
Copy-Item -Path "dist/sdk/bin/rbscan_hdf5.dll" -Destination "dist/dll/" -Force
|
|
284
|
+
Copy-Item -Path "dist/sdk/bin/rbscan.exe" -Destination "dist/dll/" -Force
|
|
285
|
+
Copy-Item -Path "dist/sdk/lib/rbscan.lib" -Destination "dist/dll/" -Force
|
|
286
|
+
|
|
287
|
+
# Also bundle the full C++ SDK into a single archive
|
|
288
|
+
Compress-Archive -Path "dist/sdk/*" -DestinationPath "dist/rbscan-sdk-win64.zip" -Force
|
|
289
|
+
Remove-Item -Path "dist/sdk" -Recurse -Force
|
|
290
|
+
|
|
291
|
+
# 2. Build Python wheels (reuses prebuilt HDF5 in .deps/)
|
|
292
|
+
$versions = ($env:RBSCAN_WHEEL_PYTHONS -split '\s+') | Where-Object { $_ }
|
|
293
|
+
foreach ($py in $versions) {
|
|
294
|
+
Write-Host "Building Python wheel for Python $py..."
|
|
295
|
+
uv build --wheel --python $py -o dist/wheels
|
|
296
|
+
if ($LASTEXITCODE -ne 0) {
|
|
297
|
+
Write-Error "Failed to build wheel for Python $py"
|
|
298
|
+
exit $LASTEXITCODE
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
Remove-Item -Path "dist/wheels/.gitignore" -Force -ErrorAction SilentlyContinue
|
|
302
|
+
|
|
303
|
+
$wheels = Get-ChildItem -Path "dist/wheels" -Filter "*.whl"
|
|
304
|
+
if (-not $wheels) {
|
|
305
|
+
Write-Error "No Python wheels were built in dist/wheels"
|
|
306
|
+
exit 1
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Smoke-test the Python 3.12 wheel in an isolated environment
|
|
310
|
+
$testWheel = $wheels | Where-Object { $_.Name -match '-cp312-' } | Select-Object -First 1
|
|
311
|
+
if ($testWheel) {
|
|
312
|
+
Write-Host "Smoke-testing wheel $($testWheel.Name)..."
|
|
313
|
+
uv run --isolated --no-project --python 3.12 --with $testWheel.FullName --with pytest --with h5py --with hdf5plugin pytest python/tests
|
|
314
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
Write-Host "=== Packaged Artifacts ==="
|
|
318
|
+
Get-ChildItem -Path "dist" -Recurse -File | ForEach-Object {
|
|
319
|
+
Write-Host (" {0} ({1:N1} KB)" -f $_.FullName.Substring($PWD.Path.Length + 1), ($_.Length / 1KB))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# 3. Publish SDK archive, DLLs, and Python wheels to the GitLab Generic Package Registry
|
|
323
|
+
# on the default branch and on release tags (packages/generic/rbscan/<version>/...).
|
|
324
|
+
if ($env:CI_COMMIT_TAG -or ($env:CI_COMMIT_BRANCH -eq $env:CI_DEFAULT_BRANCH)) {
|
|
325
|
+
$filesToPublish = @()
|
|
326
|
+
$filesToPublish += Get-Item "dist/rbscan-sdk-win64.zip"
|
|
327
|
+
$filesToPublish += Get-ChildItem -Path "dist/dll" -File
|
|
328
|
+
$filesToPublish += Get-ChildItem -Path "dist/wheels" -Filter "*.whl"
|
|
329
|
+
|
|
330
|
+
foreach ($file in $filesToPublish) {
|
|
331
|
+
$url = "$env:CI_API_V4_URL/projects/$env:CI_PROJECT_ID/packages/generic/rbscan/$version/$($file.Name)"
|
|
332
|
+
Write-Host "Publishing $($file.Name) ($([math]::Round($file.Length / 1KB, 1)) KB) to $url ..."
|
|
333
|
+
$status = curl.exe --fail --silent --show-error --header "JOB-TOKEN: $env:CI_JOB_TOKEN" --upload-file $file.FullName --write-out "%{http_code}" $url
|
|
334
|
+
if ($LASTEXITCODE -ne 0) {
|
|
335
|
+
Write-Error "Upload of $($file.Name) to the Generic Package Registry failed (HTTP $status, curl exit code $LASTEXITCODE)"
|
|
336
|
+
exit 1
|
|
337
|
+
}
|
|
338
|
+
Write-Host "Published: $url"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
artifacts:
|
|
342
|
+
name: "rbscan-windows-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
|
|
343
|
+
expire_in: 30 days
|
|
344
|
+
paths:
|
|
345
|
+
- "dist/dll/rbscan.dll"
|
|
346
|
+
- "dist/dll/rbscan_hdf5.dll"
|
|
347
|
+
- "dist/dll/rbscan.lib"
|
|
348
|
+
- "dist/dll/rbscan.exe"
|
|
349
|
+
- "dist/rbscan-sdk-win64.zip"
|
|
350
|
+
- "dist/wheels/*.whl"
|
|
351
|
+
|
|
352
|
+
package:linux:
|
|
353
|
+
extends: .linux
|
|
354
|
+
stage: package
|
|
355
|
+
needs:
|
|
356
|
+
- build:linux
|
|
357
|
+
- test:linux
|
|
358
|
+
script:
|
|
359
|
+
- ci/linux.sh version
|
|
360
|
+
- ci/linux.sh package
|
|
361
|
+
- |
|
|
362
|
+
if [ -n "${CI_COMMIT_TAG:-}" ] || [ "${CI_COMMIT_BRANCH:-}" = "$CI_DEFAULT_BRANCH" ]; then
|
|
363
|
+
ci/linux.sh publish
|
|
364
|
+
fi
|
|
365
|
+
artifacts:
|
|
366
|
+
name: "rbscan-linux-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
|
|
367
|
+
expire_in: 30 days
|
|
368
|
+
paths:
|
|
369
|
+
- "dist/rbscan-sdk-linux-x86_64.tar.gz"
|
|
370
|
+
- "dist/rbscan-[0-9]*.tar.gz"
|
|
371
|
+
- "dist/wheels/*.whl"
|
|
372
|
+
|
|
373
|
+
# ------------------------------------------------------------------
|
|
374
|
+
# Stage: GitLab Release (tags only)
|
|
375
|
+
# ------------------------------------------------------------------
|
|
376
|
+
release:
|
|
377
|
+
extends: .windows
|
|
378
|
+
stage: release
|
|
379
|
+
rules:
|
|
380
|
+
- if: $CI_COMMIT_TAG
|
|
381
|
+
needs:
|
|
382
|
+
- job: package:windows
|
|
383
|
+
artifacts: true
|
|
384
|
+
- job: package:linux
|
|
385
|
+
artifacts: true
|
|
386
|
+
optional: true
|
|
387
|
+
script:
|
|
388
|
+
- *release_version
|
|
389
|
+
- |
|
|
390
|
+
$ErrorActionPreference = "Stop"
|
|
391
|
+
# Release notes: the CHANGELOG.md section of this version.
|
|
392
|
+
$notes = @()
|
|
393
|
+
$inSection = $false
|
|
394
|
+
foreach ($line in Get-Content CHANGELOG.md -Encoding UTF8) {
|
|
395
|
+
if ($line -like "## *") {
|
|
396
|
+
if ($inSection) { break }
|
|
397
|
+
$inSection = $line.StartsWith("## [$version]")
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
if ($inSection) { $notes += $line }
|
|
401
|
+
}
|
|
402
|
+
$packageUrl = "$env:CI_API_V4_URL/projects/$env:CI_PROJECT_ID/packages/generic/rbscan/$version"
|
|
403
|
+
$files = @(Get-Item "dist/rbscan-sdk-*" -ErrorAction SilentlyContinue) + @(Get-ChildItem "dist/wheels" -Filter "*.whl" -ErrorAction SilentlyContinue)
|
|
404
|
+
$links = @($files | ForEach-Object {
|
|
405
|
+
@{ name = $_.Name; url = "$packageUrl/$($_.Name)"; link_type = "package" }
|
|
406
|
+
})
|
|
407
|
+
$body = @{
|
|
408
|
+
tag_name = $env:CI_COMMIT_TAG
|
|
409
|
+
name = "rbscan $version"
|
|
410
|
+
description = ($notes -join "`n").Trim()
|
|
411
|
+
assets = @{ links = $links }
|
|
412
|
+
} | ConvertTo-Json -Depth 5
|
|
413
|
+
$bodyFile = Join-Path $env:TEMP "rbscan-release.json"
|
|
414
|
+
[System.IO.File]::WriteAllText($bodyFile, $body, (New-Object System.Text.UTF8Encoding $false))
|
|
415
|
+
curl.exe --fail --silent --show-error --header "JOB-TOKEN: $env:CI_JOB_TOKEN" --header "Content-Type: application/json" --data "@$bodyFile" "$env:CI_API_V4_URL/projects/$env:CI_PROJECT_ID/releases"
|
|
416
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
417
|
+
Write-Host "`nCreated release rbscan $version with $($links.Count) package links"
|
|
418
|
+
|
|
419
|
+
# ------------------------------------------------------------------
|
|
420
|
+
# Stage: Publish to PyPI / TestPyPI (Trusted Publishing)
|
|
421
|
+
# ------------------------------------------------------------------
|
|
422
|
+
# PyPI trusts this project's GitLab OIDC token (pending/trusted publisher:
|
|
423
|
+
# namespace raybotic/machine, project rbscan, workflow .gitlab-ci.yml,
|
|
424
|
+
# environment pypi resp. testpypi). The job swaps the token for a short-lived
|
|
425
|
+
# upload token; nothing is stored in CI/CD variables. PyPI accepts every
|
|
426
|
+
# version only once.
|
|
427
|
+
.pypi_upload:
|
|
428
|
+
extends: .linux
|
|
429
|
+
image: python:3.12-slim
|
|
430
|
+
variables:
|
|
431
|
+
# Before the artifacts arrive: drop old dist/ files, keep the build caches.
|
|
432
|
+
GIT_CLEAN_FLAGS: "-ffdx -e .deps -e .venv -e build-linux"
|
|
433
|
+
needs:
|
|
434
|
+
- job: package:windows
|
|
435
|
+
artifacts: true
|
|
436
|
+
- job: package:linux
|
|
437
|
+
artifacts: true
|
|
438
|
+
script:
|
|
439
|
+
- python -m pip install --quiet --upgrade twine
|
|
440
|
+
- ls -l dist dist/wheels
|
|
441
|
+
- python -m twine check --strict dist/wheels/*.whl dist/rbscan-[0-9]*.tar.gz
|
|
442
|
+
- |
|
|
443
|
+
api_token=$(python - <<'PY'
|
|
444
|
+
import json, os, urllib.request
|
|
445
|
+
req = urllib.request.Request(
|
|
446
|
+
os.environ["PYPI_MINT_URL"],
|
|
447
|
+
data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
|
|
448
|
+
headers={"Content-Type": "application/json"},
|
|
449
|
+
method="POST",
|
|
450
|
+
)
|
|
451
|
+
try:
|
|
452
|
+
with urllib.request.urlopen(req) as r:
|
|
453
|
+
print(json.load(r)["token"])
|
|
454
|
+
except urllib.error.HTTPError as e:
|
|
455
|
+
import base64, sys
|
|
456
|
+
print(f"minting the upload token failed: {e.code} {e.read().decode()}", file=sys.stderr)
|
|
457
|
+
# The claims PyPI matches against the trusted publisher (not secret;
|
|
458
|
+
# the token itself is never printed).
|
|
459
|
+
payload = os.environ["PYPI_ID_TOKEN"].split(".")[1]
|
|
460
|
+
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
|
|
461
|
+
print("This job's OIDC claims; the trusted publisher on PyPI must match them:", file=sys.stderr)
|
|
462
|
+
for key in ("aud", "namespace_path", "project_path", "ci_config_ref_uri",
|
|
463
|
+
"environment", "ref_path", "ref_protected"):
|
|
464
|
+
print(f" {key}: {claims.get(key)}", file=sys.stderr)
|
|
465
|
+
raise SystemExit(1)
|
|
466
|
+
PY
|
|
467
|
+
) || exit 1
|
|
468
|
+
- python -m twine upload --non-interactive --repository-url "$PYPI_UPLOAD_URL" $TWINE_EXTRA -u __token__ -p "$api_token" dist/wheels/*.whl dist/rbscan-[0-9]*.tar.gz
|
|
469
|
+
|
|
470
|
+
publish:testpypi:
|
|
471
|
+
extends: .pypi_upload
|
|
472
|
+
stage: publish
|
|
473
|
+
environment: testpypi
|
|
474
|
+
id_tokens:
|
|
475
|
+
PYPI_ID_TOKEN:
|
|
476
|
+
aud: testpypi
|
|
477
|
+
variables:
|
|
478
|
+
PYPI_MINT_URL: "https://test.pypi.org/_/oidc/mint-token"
|
|
479
|
+
PYPI_UPLOAD_URL: "https://test.pypi.org/legacy/"
|
|
480
|
+
# A trial run may repeat a version that is already there.
|
|
481
|
+
TWINE_EXTRA: "--skip-existing"
|
|
482
|
+
rules:
|
|
483
|
+
- if: $RBSCAN_LINUX_RUNNER_TAG == null || $RBSCAN_LINUX_RUNNER_TAG == ""
|
|
484
|
+
when: never
|
|
485
|
+
- if: $CI_COMMIT_TAG || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
486
|
+
when: manual
|
|
487
|
+
allow_failure: true
|
|
488
|
+
|
|
489
|
+
publish:pypi:
|
|
490
|
+
extends: .pypi_upload
|
|
491
|
+
stage: publish
|
|
492
|
+
environment: pypi
|
|
493
|
+
id_tokens:
|
|
494
|
+
PYPI_ID_TOKEN:
|
|
495
|
+
aud: pypi
|
|
496
|
+
variables:
|
|
497
|
+
PYPI_MINT_URL: "https://pypi.org/_/oidc/mint-token"
|
|
498
|
+
PYPI_UPLOAD_URL: "https://upload.pypi.org/legacy/"
|
|
499
|
+
TWINE_EXTRA: ""
|
|
500
|
+
rules:
|
|
501
|
+
- if: $RBSCAN_LINUX_RUNNER_TAG == null || $RBSCAN_LINUX_RUNNER_TAG == ""
|
|
502
|
+
when: never
|
|
503
|
+
- if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to rbscan are listed here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the library uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
The library version (below) and the on-disk **format version** (`format_version`
|
|
8
|
+
in every scan, see [docs/format.md](docs/format.md)) are independent: a
|
|
9
|
+
library release can read every format version with the same major number
|
|
10
|
+
(`1.x`). Schemas carry their own version (`raybotic.xray_detector/1`).
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.2.1] - 2026-11-02
|
|
15
|
+
|
|
16
|
+
First release published on PyPI and GitHub. Writes and reads format version 1.0.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- The Linux release build no longer stops without a message on a fresh CI
|
|
21
|
+
workspace (the reason 0.2.0 was never published).
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Scan directories with an HDF5 master file (`scan.rbscan.h5`) and segmented
|
|
26
|
+
sensor streams (`seg_NNNNNN.h5`), linked into the master as virtual datasets.
|
|
27
|
+
- Inline and segmented series, multi-process recording, live reading via SWMR,
|
|
28
|
+
appending to stored inline series and `refresh()` while reading.
|
|
29
|
+
- Compression: Bitshuffle+LZ4 (default, HDF5 filter 32008, readable with
|
|
30
|
+
`hdf5plugin`), shuffle+deflate, deflate, none; parallel encoding and decoding
|
|
31
|
+
outside the global HDF5 lock.
|
|
32
|
+
- Built-in schemas for X-ray source and detector, cameras, depth cameras,
|
|
33
|
+
profile scanners, force/torque sensors, robot arms, transforms, CT
|
|
34
|
+
acquisitions, volumes, annotations, reconstruction settings, trajectory plans
|
|
35
|
+
and EZRT imports; custom schemas at runtime or via `RBSCAN_SCHEMA_PATH`.
|
|
36
|
+
- Tools in C++, Python and the `rbscan` CLI: `info`, `validate`,
|
|
37
|
+
`resolve-geometry`, `rebuild`, `schemas`.
|
|
38
|
+
- `rebuild` recovers a scan whose recording process crashed: it clears the
|
|
39
|
+
HDF5 status flags the crashed writer left in the master file and relinks
|
|
40
|
+
every segment on disk.
|
|
41
|
+
- C++ SDK (Windows, Linux) with CMake package config.
|
|
42
|
+
- On PyPI (`pip install rbscan`): wheels for CPython 3.11–3.14 on Windows x64
|
|
43
|
+
and Linux x86_64 (`manylinux_2_28`), plus the source distribution.
|
|
44
|
+
- `Array::values<T>()` returns a `std::span` on a named array and an owning
|
|
45
|
+
`std::vector<T>` on a temporary (`reader.read(...).values<T>()`), so the
|
|
46
|
+
result never dangles.
|
|
47
|
+
|
|
48
|
+
## [0.2.0] - 2026-11-02
|
|
49
|
+
|
|
50
|
+
Tagged but not published: its Linux release build failed. Everything is in
|
|
51
|
+
0.2.1.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.24)
|
|
2
|
+
|
|
3
|
+
project(rbscan
|
|
4
|
+
VERSION 0.2.1
|
|
5
|
+
DESCRIPTION "RayBotic scan data format: HDF5 master file + segmented sensor streams"
|
|
6
|
+
LANGUAGES C CXX)
|
|
7
|
+
|
|
8
|
+
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
|
9
|
+
|
|
10
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
11
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
12
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
13
|
+
|
|
14
|
+
if(PROJECT_IS_TOP_LEVEL)
|
|
15
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
16
|
+
endif()
|
|
17
|
+
|
|
18
|
+
option(RBSCAN_BUILD_TESTS "Build the C++ unit tests" ${PROJECT_IS_TOP_LEVEL})
|
|
19
|
+
option(RBSCAN_BUILD_PYTHON "Build the Python extension module" ${PROJECT_IS_TOP_LEVEL})
|
|
20
|
+
option(RBSCAN_BUILD_EXAMPLES "Build the C++ examples" ${PROJECT_IS_TOP_LEVEL})
|
|
21
|
+
option(RBSCAN_BUILD_TOOLS "Build the rbscan command line tool" ON)
|
|
22
|
+
option(RBSCAN_INSTALL "Generate install rules (off when used as a subproject)" ${PROJECT_IS_TOP_LEVEL})
|
|
23
|
+
option(RBSCAN_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
|
|
24
|
+
|
|
25
|
+
# Everything lands next to each other, so rbscan.dll / hdf5.dll are found by
|
|
26
|
+
# tests and examples without touching PATH.
|
|
27
|
+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
|
28
|
+
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
|
29
|
+
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
|
30
|
+
|
|
31
|
+
include(rbscan_warnings)
|
|
32
|
+
include(rbscan_deps)
|
|
33
|
+
include(rbscan_embed_schemas)
|
|
34
|
+
|
|
35
|
+
add_subdirectory(src)
|
|
36
|
+
|
|
37
|
+
if(RBSCAN_BUILD_TOOLS)
|
|
38
|
+
add_subdirectory(tools)
|
|
39
|
+
endif()
|
|
40
|
+
|
|
41
|
+
if(RBSCAN_BUILD_PYTHON)
|
|
42
|
+
add_subdirectory(python)
|
|
43
|
+
endif()
|
|
44
|
+
|
|
45
|
+
if(RBSCAN_BUILD_EXAMPLES)
|
|
46
|
+
add_subdirectory(examples)
|
|
47
|
+
endif()
|
|
48
|
+
|
|
49
|
+
if(RBSCAN_BUILD_TESTS)
|
|
50
|
+
enable_testing()
|
|
51
|
+
add_subdirectory(tests)
|
|
52
|
+
endif()
|