-
Notifications
You must be signed in to change notification settings - Fork 221
Add stdlib_spatial module with Kabsch–Umeyama vector alignment algorithm #1119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 17 commits
ea6b295
655af91
60818bb
400b85d
5208695
25df8d2
33859b6
c7b3227
78f35a4
a9c489c
fb08933
88e76fb
e73a93f
e3a1cc2
27b68ca
c895ed5
d722366
b9b9832
37b3cba
723661b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ADD_EXAMPLE(kabsch_umeyama) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| program example_kabsch_umeyama | ||
| use stdlib_linalg_constants, only: dp | ||
| use stdlib_spatial, only: kabsch_umeyama | ||
| implicit none | ||
|
|
||
| integer, parameter :: d = 2, N = 3 | ||
| real(dp) :: P(d, N), Q(d, N), R(d, d), t(d), c, rmsd | ||
|
|
||
| integer :: i | ||
|
|
||
| P(:,1) = [3.0_dp, -2.0_dp] | ||
| P(:,2) = [7.0_dp, 4.0_dp] | ||
| P(:,3) = [5.0_dp, 0.0_dp] | ||
|
|
||
| Q(:,1) = [2.0_dp, 3.0_dp] | ||
| Q(:,2) = [-1.0_dp, 5.0_dp] | ||
| Q(:,3) = [1.0_dp, 4.0_dp] | ||
|
|
||
| call kabsch_umeyama(P, Q, R, t, c, rmsd) | ||
|
|
||
| print *, "" | ||
| print *, "Recovered rotation R:" | ||
| do i = 1, d | ||
| print *, R(i,:) | ||
| end do | ||
|
|
||
| print *, "Recovered scale c:", c | ||
|
|
||
| print *, "" | ||
| print *, "Recovered translation t:" | ||
| print *, t | ||
|
|
||
| print *, "" | ||
| print *, "RMSD:", rmsd | ||
|
|
||
| end program example_kabsch_umeyama |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| set(spatial_fppFiles | ||
| stdlib_spatial.fypp | ||
| stdlib_spatial_kabsch_umeyama.fypp | ||
| ) | ||
|
|
||
| set(spatial_cppFiles | ||
| ) | ||
|
|
||
| set(spatial_f90Files | ||
| ) | ||
|
|
||
| configure_stdlib_target(${PROJECT_NAME}_spatial spatial_f90Files spatial_fppFiles spatial_cppFiles) | ||
|
|
||
| target_link_libraries(${PROJECT_NAME}_spatial PUBLIC ${PROJECT_NAME}_constants ${PROJECT_NAME}_linalg_core ${PROJECT_NAME}_linalg ${PROJECT_NAME}_intrinsics) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||
| #:include "common.fypp" | ||||||
| #:set R_KINDS_TYPES = list(zip(REAL_KINDS, REAL_TYPES, REAL_SUFFIX)) | ||||||
| #:set C_KINDS_TYPES = list(zip(CMPLX_KINDS, CMPLX_TYPES, CMPLX_SUFFIX)) | ||||||
| #:set KINDS_TYPES = R_KINDS_TYPES+C_KINDS_TYPES | ||||||
| module stdlib_spatial | ||||||
| use stdlib_linalg_constants | ||||||
| use stdlib_constants | ||||||
| use stdlib_error, only: error_stop | ||||||
| implicit none | ||||||
| private | ||||||
| public :: kabsch_umeyama | ||||||
|
|
||||||
| interface kabsch_umeyama | ||||||
| !----------------------------------------------------------------------- | ||||||
| !> Compute the optimal similarity transform (Kabsch–Umeyama): | ||||||
| !> | ||||||
| !> P ≈ c * R * Q + t | ||||||
| !> | ||||||
| !> where: | ||||||
| !> - R is an orthogonal rotation matrix | ||||||
| !> - c is an optional scale factor | ||||||
| !> - t is a translation vector | ||||||
| !> | ||||||
| !> The transformation minimizes the RMSD between corresponding columns | ||||||
| !> of P and Q, optionally using weights. | ||||||
| !----------------------------------------------------------------------- | ||||||
| #:for k, t, s in (KINDS_TYPES) | ||||||
| module subroutine kabsch_umeyama_${s}$(P, Q, R, t, c, rmsd, W, scale) | ||||||
| !> Reference point set (d × N) | ||||||
| ${t}$, intent(in) :: P(:, :) | ||||||
| !> Target point set (d × N) | ||||||
| ${t}$, intent(in) :: Q(:, :) | ||||||
| !> Optimal rotation matrix (d × d) | ||||||
| ${t}$, intent(out) :: R(:,:) | ||||||
| !> Translation vector (d) | ||||||
| ${t}$, intent(out) :: t(:) | ||||||
| !> Scale factor | ||||||
| ${t}$, intent(out) :: c | ||||||
| !> Root-mean-square deviation | ||||||
| real(${k}$), intent(out) :: rmsd | ||||||
| !> Optional weights | ||||||
| ${t}$, intent(in), optional :: W(:) | ||||||
|
||||||
| ${t}$, intent(in), optional :: W(:) | |
| real(${k}$), intent(in), optional :: W(:) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,173 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #:include "common.fypp" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #:set R_KINDS_TYPES = list(zip(REAL_KINDS, REAL_TYPES, REAL_SUFFIX)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #:set C_KINDS_TYPES = list(zip(CMPLX_KINDS, CMPLX_TYPES, CMPLX_SUFFIX)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #:set KINDS_TYPES = R_KINDS_TYPES+C_KINDS_TYPES | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| submodule(stdlib_spatial) stdlib_spatial_kabsch_umeyama | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use stdlib_linalg, only: svd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use stdlib_intrinsics, only: stdlib_sum_kahan, stdlib_dot_product_kahan, kahan_kernel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contains | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #:for k, t, s in (KINDS_TYPES) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module subroutine kabsch_umeyama_${s}$(P, Q, R, t, c, rmsd, W, scale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Reference point set (d × N) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(in) :: P(:, :) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Target point set (d × N) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(in) :: Q(:, :) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Optimal rotation matrix (d × d) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(out) :: R(:,:) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Translation vector (d) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(out) :: t(:) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Scale factor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(out) :: c | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Root-mean-square deviation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| real(${k}$), intent(out) :: rmsd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Optional weights | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, intent(in), optional :: W(:) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !> Enable scaling | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logical, intent(in), optional :: scale | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
23
to
28
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ! Internal variables. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| integer(ilp) :: i, j, point, d, N | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, allocatable :: covariance(:,:), U(:,:), Vt(:,:), B(:,:), vec(:), tmp_N(:), tmp_d(:), c_P(:), c_Q(:) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$ :: vp, vq | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${t}$, allocatable :: covariance(:,:), U(:,:), Vt(:,:), B(:,:), vec(:), tmp_N(:), tmp_d(:), c_P(:), c_Q(:) | |
| ${t}$ :: vp, vq | |
| ${t}$, allocatable :: covariance(:,:), U(:,:), Vt(:,:), vec(:), tmp_N(:), tmp_d(:), c_P(:), c_Q(:) |
Outdated
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error_stop messages for shape mismatches are all the same and don’t identify which argument/shape is wrong, which makes debugging harder. Consider including the routine name and expected vs actual shapes (e.g., which dimensions of P/Q/R/t/W mismatch).
| ${t}$, allocatable :: covariance(:,:), U(:,:), Vt(:,:), B(:,:), vec(:), tmp_N(:), tmp_d(:), c_P(:), c_Q(:) | |
| ${t}$ :: vp, vq | |
| real(${k}$) :: sum_w, variance_p | |
| real(${k}$), allocatable :: S(:) | |
| logical :: scale_ | |
| real(${k}$) :: rmsd_err | |
| ! Dimension checks | |
| if(size(P,dim=1)/=size(Q,dim=1) .or. size(P,dim=1)/=size(R,dim=1) .or. size(P,dim=1)/=size(R,dim=2) & | |
| .or. size(P,dim=1)/=size(t)) then | |
| call error_stop("array sizes do not match") | |
| end if | |
| if(size(P,dim=2)/=size(Q,dim=2)) then | |
| call error_stop("array sizes do not match") | |
| end if | |
| if (present(W)) then | |
| if (size(W) /= size(P,dim=2)) then | |
| call error_stop("array sizes do not match") | |
| integer(ilp) :: dP, dQ, dR1, dR2, dt, nP, nQ, nW | |
| ${t}$, allocatable :: covariance(:,:), U(:,:), Vt(:,:), B(:,:), vec(:), tmp_N(:), tmp_d(:), c_P(:), c_Q(:) | |
| ${t}$ :: vp, vq | |
| real(${k}$) :: sum_w, variance_p | |
| real(${k}$), allocatable :: S(:) | |
| logical :: scale_ | |
| real(${k}$) :: rmsd_err | |
| character(len=:), allocatable :: errmsg | |
| ! Dimension checks | |
| dP = size(P, dim=1) | |
| dQ = size(Q, dim=1) | |
| dR1 = size(R, dim=1) | |
| dR2 = size(R, dim=2) | |
| dt = size(t) | |
| if (dP /= dQ .or. dP /= dR1 .or. dP /= dR2 .or. dP /= dt) then | |
| write(errmsg, '(A,5(I0,:,", "))') & | |
| 'kabsch_umeyama_${s}$: mismatched leading dimensions (P, Q, R(:,1), R(1,:), t) = ', & | |
| dP, dQ, dR1, dR2, dt | |
| call error_stop(errmsg) | |
| end if | |
| nP = size(P, dim=2) | |
| nQ = size(Q, dim=2) | |
| if (nP /= nQ) then | |
| write(errmsg, '(A,2(I0,:,", "))') & | |
| 'kabsch_umeyama_${s}$: mismatched second dimensions (P(:,N), Q(:,N)) = ', & | |
| nP, nQ | |
| call error_stop(errmsg) | |
| end if | |
| if (present(W)) then | |
| nW = size(W) | |
| if (nW /= nP) then | |
| write(errmsg, '(A,2(I0,:,", "))') & | |
| 'kabsch_umeyama_${s}$: mismatched number of points between P and W (size(P,2), size(W)) = ', & | |
| nP, nW | |
| call error_stop(errmsg) |
Outdated
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several scalars are declared as real(${k}$) (sum_w, variance_p, rmsd), but are initialized/assigned using one_${s}$ / zero_${s}$, which become complex literals in the complex instantiations. This relies on implicit complex→real conversion (discarding the imaginary part) and can mask small numerical imaginary components. Prefer using one_${k}$ / zero_${k}$ (and explicitly real(...) where needed) for real-typed quantities.
| variance_p = zero_${s}$ | |
| variance_p = zero_${k}$ |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The computed rotation matrix is R = U * Vt with no correction for improper rotations (reflections). For real inputs this can yield det(R) = -1, which contradicts the “proper rotation” requirement in the PR description and standard Kabsch/Umeyama. Consider applying the usual determinant/sign correction (e.g., a diagonal matrix with last entry = sign(det(U*Vt))) before forming R.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is being dealt inside SVD internally. SVD returns righthanded matrices.
Outdated
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scale factor uses variance_p (computed from P) and divides by sum(S). In Umeyama for P ≈ c R Q + t, the scale minimizing RMSD depends on the variance of the source set being transformed (Q) and should incorporate the same reflection-correction used for R (i.e., tr(S*D)/var(Q)). Using variance of P biases c in the presence of noise/outliers.
| ! Scaling factor | |
| c = variance_p / (sum(S(1:d))) | |
| ! Scaling factor (Umeyama): c = tr(S) / var(Q) | |
| c = stdlib_sum_kahan(S(1:d)) / variance_q |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation follows the covariance convention H = (P - c_P) * (Q - c_Q)^T , under which the scale naturally depends on var(P) not var(Q)
Outdated
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even when scale is disabled, c is computed first (including a potential divide-by-zero if sum(S) is 0) and only then overwritten. Consider guarding the scale computation with if (scale_) then ... else c = 1 to avoid unnecessary work and avoid raising errors in degenerate cases when scaling is off.
| c = variance_p / (sum(S(1:d))) | |
| if (.not. scale_) c = one_${s}$ | |
| if (scale_) then | |
| c = variance_p / (sum(S(1:d))) | |
| else | |
| c = one_${s}$ | |
| end if |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,7 @@ | ||||||
| set( | ||||||
| fppFiles | ||||||
| "test_spatial_kabsch_umeyama.fypp" | ||||||
| ) | ||||||
| fypp_f90pp("${fyppFlags}" "${fppFiles}" outFiles) | ||||||
|
||||||
| fypp_f90pp("${fyppFlags}" "${fppFiles}" outFiles) | |
| fypp_f90("${fyppFlags}" "${fppFiles}" outFiles) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stdlib_spatialusesstdlib_error(error_stop) but the spatial target links only constants/linalg/intrinsics. To avoid undefined references when linking${PROJECT_NAME}_spatialdirectly, add${PROJECT_NAME}_core(or whichever target providesstdlib_error) totarget_link_librarieshere.