Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ea6b295
kabsch first commit
Mahmood-Sinan Feb 4, 2026
655af91
fixed the logic issue
Mahmood-Sinan Feb 5, 2026
60818bb
add: comments and complex support
Mahmood-Sinan Feb 6, 2026
400b85d
removed complex kinds
Mahmood-Sinan Feb 7, 2026
5208695
add complex types back, correct reflection
Mahmood-Sinan Feb 7, 2026
25df8d2
modify example, add tests, use suffix in constants and move use stdli…
Mahmood-Sinan Feb 7, 2026
33859b6
remove: debug comments
Mahmood-Sinan Feb 7, 2026
c7b3227
remove: unnecessary fypp conditions
Mahmood-Sinan Feb 7, 2026
78f35a4
added instrinsics functions to spatial
Mahmood-Sinan Feb 8, 2026
a9c489c
modify: test only for P
Mahmood-Sinan Feb 11, 2026
fb08933
modify: set arguments to pass to kabsh in test to zero
Mahmood-Sinan Feb 11, 2026
88e76fb
remove: debug statements in test
Mahmood-Sinan Feb 11, 2026
e73a93f
remove: unused imports and modify: S and rmsd init inside complex kab…
Mahmood-Sinan Feb 11, 2026
e3a1cc2
change name: kabsch to kabsch_umeyama
Mahmood-Sinan Feb 13, 2026
27b68ca
add stdlib_constants to test_kabsch_umeyama.fypp
Mahmood-Sinan Feb 13, 2026
c895ed5
move tol declaration inside blocks
Mahmood-Sinan Feb 13, 2026
d722366
revert accidental changes to example_sum.f90
Mahmood-Sinan Feb 13, 2026
b9b9832
change complex weights to real weights
Mahmood-Sinan Feb 26, 2026
37b3cba
minor changes
Mahmood-Sinan Feb 26, 2026
723661b
revert cmake change
Mahmood-Sinan Feb 26, 2026
3a7221f
add sum_w error check
Mahmood-Sinan Mar 7, 2026
51a193d
reflection handling in real cases and modify tests
Mahmood-Sinan Mar 8, 2026
7554914
add target stdlib_core to spatial/CMakeLists.txt
Mahmood-Sinan Mar 8, 2026
12f1513
modify test/spatial/CMakeLists.txt
Mahmood-Sinan Mar 8, 2026
fb4f1a2
Merge remote-tracking branch 'upstream/master' into kabsch_algorithm
Mahmood-Sinan Apr 1, 2026
fe7d96e
modify: example, add: comment in test, minor changes
Mahmood-Sinan Apr 1, 2026
81a228c
modify: dependencies
Mahmood-Sinan Apr 1, 2026
a9f0fc1
modify: docs, ford comments and example
Mahmood-Sinan Apr 2, 2026
42db95b
minor changes
Mahmood-Sinan Apr 2, 2026
fa159bd
add: debug messages for test
Mahmood-Sinan Apr 3, 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
1 change: 1 addition & 0 deletions example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ if (STDLIB_QUADRATURE)
endif()
add_subdirectory(selection)
add_subdirectory(sorting)
add_subdirectory(spatial)
add_subdirectory(specialfunctions_gamma)
if (STDLIB_SPECIALMATRICES)
add_subdirectory(specialmatrices)
Expand Down
1 change: 1 addition & 0 deletions example/spatial/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ADD_EXAMPLE(kabsch_umeyama)
36 changes: 36 additions & 0 deletions example/spatial/example_kabsch_umeyama.f90
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
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ADD_SUBDIR(system)
ADD_SUBDIR(stats)

add_subdirectory(sparse)
add_subdirectory(spatial)

set(fppFiles
stdlib_version.fypp
Expand All @@ -70,5 +71,6 @@ target_link_libraries(${PROJECT_NAME} PUBLIC
${PROJECT_NAME}_strings
${PROJECT_NAME}_blas ${PROJECT_NAME}_lapack ${PROJECT_NAME}_lapack_extended
${PROJECT_NAME}_sparse
${PROJECT_NAME}_spatial
${OPTIONAL_LIB}
)
14 changes: 14 additions & 0 deletions src/spatial/CMakeLists.txt
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)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdlib_spatial uses stdlib_error (error_stop) but the spatial target links only constants/linalg/intrinsics. To avoid undefined references when linking ${PROJECT_NAME}_spatial directly, add ${PROJECT_NAME}_core (or whichever target provides stdlib_error) to target_link_libraries here.

Suggested change
target_link_libraries(${PROJECT_NAME}_spatial PUBLIC ${PROJECT_NAME}_constants ${PROJECT_NAME}_linalg_core ${PROJECT_NAME}_linalg ${PROJECT_NAME}_intrinsics)
target_link_libraries(${PROJECT_NAME}_spatial PUBLIC ${PROJECT_NAME}_core ${PROJECT_NAME}_constants ${PROJECT_NAME}_linalg_core ${PROJECT_NAME}_linalg ${PROJECT_NAME}_intrinsics)

Copilot uses AI. Check for mistakes.
48 changes: 48 additions & 0 deletions src/spatial/stdlib_spatial.fypp
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(:)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public interface also types W as ${t}$, which makes weights complex for the complex overloads. If weights are intended to be real scalars, consider changing W here to real(${k}$) (and mirroring that in the implementation) to avoid confusing/invalid API usage.

Suggested change
${t}$, intent(in), optional :: W(:)
real(${k}$), intent(in), optional :: W(:)

Copilot uses AI. Check for mistakes.
!> Enable scaling
logical, intent(in), optional :: scale
end subroutine
#:endfor
end interface
end module stdlib_spatial
173 changes: 173 additions & 0 deletions src/spatial/stdlib_spatial_kabsch_umeyama.fypp
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 +29
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W is declared as the same type as the point arrays (${t}$). For complex variants this implies complex weights, which then flow into sum_w/centroid/covariance computations via stdlib_sum_kahan/stdlib_dot_product_kahan and require implicit complex→real conversions. If weights are intended (as in the issue description) to be real and non-negative, consider typing W as real(${k}$) for both real and complex point sets and validate sum(W) > 0.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

! 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
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B, vp, and vq are declared but never used. If B was intended for the determinant/reflection correction step in Kabsch/Umeyama, it should be used; otherwise remove these unused variables to keep the implementation easier to maintain.

Suggested change
${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(:)

Copilot uses AI. Check for mistakes.
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")
Copy link

Copilot AI Feb 13, 2026

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).

Suggested change
${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)

Copilot uses AI. Check for mistakes.
end if
end if
d = size(P,dim=1)
N = size(P,dim=2)
scale_ = .true.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optval can be used to initialize this value. Also, optional parameters initialization are better placed at the begining of the procedure.

if(present(scale)) scale_ = scale

sum_w = one_${s}$ / N
if(present(W)) sum_w = one_${s}$ / stdlib_sum_kahan(W)

allocate(c_P(d), source=zero_${s}$)
allocate(c_Q(d), source=zero_${s}$)

! Compute centroids of P and Q
if(present(W)) then
do i = 1, d
c_P(i) = stdlib_dot_product_kahan(w,P(i, :))
c_Q(i) = stdlib_dot_product_kahan(w,Q(i, :))
end do
else
do i = 1, d
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdlib_sum_kahan also accepts a dim parameter, so in this case the expresion can be simply:

c_P(1:d) = stdlib_sum_kahan(P, dim=2)
c_Q(1:d) = stdlib_sum_kahan(Q, dim=2)

c_P(i) = stdlib_sum_kahan(P(i, :))
c_Q(i) = stdlib_sum_kahan(Q(i, :))
end do
end if
c_P = c_P * sum_w
c_Q = c_Q * sum_w

! Compute covariance matrix H = (P - c_P) * (Q - c_Q)^T and variance of P
allocate(covariance(d,d), source=zero_${s}$)
allocate(tmp_N(N), source=zero_${s}$)
allocate(tmp_d(d), source=zero_${s}$)
variance_p = zero_${s}$
Copy link

Copilot AI Feb 13, 2026

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.

Suggested change
variance_p = zero_${s}$
variance_p = zero_${k}$

Copilot uses AI. Check for mistakes.

if (present(W)) then
do point = 1, N
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loops in this section should be revisited. This algorithm is usually applied for N >> d, this means that the best approach in terms of memory access is to handle, for each point in N everything related to the topological dimension d. In that same line, stdlib_sum and stdlib_dot_product (and their kahan versions) are very interesting performance wise when handling large arrays in one go.

tmp_d = P(:, point) - c_P(:)
tmp_N(point) = stdlib_dot_product_kahan(tmp_d, tmp_d)
end do
variance_p = stdlib_dot_product_kahan(w, tmp_N)
do j = 1, d
do i = 1, d
#:if t.startswith('complex')
tmp_N(:) = (P(i,:) - c_P(i)) * conjg(Q(j,:) - c_Q(j))
covariance(i,j) = stdlib_dot_product_kahan(w, tmp_N)
#:else
tmp_N(:) = (P(i,:) - c_P(i)) * (Q(j,:) - c_Q(j))
covariance(i,j) = stdlib_dot_product_kahan(w, tmp_N)
#:endif
end do
end do
else
do point = 1, N
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tmp_d = P(:, point) - c_P(:)
tmp_N(point) = stdlib_dot_product_kahan(tmp_d, tmp_d)
end do
variance_p = stdlib_sum_kahan(tmp_N)
do j = 1, d
do i = 1, d
#:if t.startswith('complex')
tmp_N(:) = (P(i,:) - c_P(i)) * conjg(Q(j,:) - c_Q(j))
covariance(i,j) = stdlib_sum_kahan(tmp_N)
#:else
tmp_N(:) = (P(i,:) - c_P(i)) * (Q(j,:) - c_Q(j))
covariance(i,j) = stdlib_sum_kahan(tmp_N)
#:endif
end do
end do
end if

covariance = covariance * sum_w
variance_p = variance_p * sum_w

allocate(U(d,d), source=zero_${s}$)
allocate(Vt(d,d), source=zero_${s}$)
allocate(S(d), source=zero_${k}$)

! SVD of covariance matrix H -> H = U * S * Vt
call svd(covariance, S, U, Vt)

! Optimal rotation matrix.
do i = 1,d
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before. I'm worried that using stdlib_dot_product_kahan here will be detrimental performance wise in this loop construct. I think this could be better handled with blas gemm

do j = 1,d
#:if t.startswith('complex')
R(i,j) = stdlib_dot_product_kahan(conjg(U(i,:)), Vt(:, j))
#:else
R(i,j) = stdlib_dot_product_kahan(U(i,:), Vt(:, j))
#:endif
end do
end do
Comment on lines +135 to +156
Copy link

Copilot AI Feb 13, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


! Scaling factor
c = variance_p / (sum(S(1:d)))
Copy link

Copilot AI Feb 13, 2026

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.

Suggested change
! 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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

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)

if (.not. scale_) c = one_${s}$
Copy link

Copilot AI Feb 13, 2026

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.

Suggested change
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

Copilot uses AI. Check for mistakes.

! Translation vector t = c_P - c*R*c_Q
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This operation might be better of being writtent using gemv (https://stdlib.fortran-lang.org/interface/gemv.html) with alpha=-1 and beta=1 (assuming t is first initialized to c_P

do i = 1, d
#:if t.startswith('complex')
t(i) = c_P(i) - c * stdlib_dot_product_kahan(conjg(R(i,1:d)), c_Q(1:d))
#:else
t(i) = c_P(i) - c * stdlib_dot_product_kahan(R(i,1:d), c_Q(1:d))
#:endif
end do

! Compute RMSD
allocate(vec(d), source=zero_${s}$)
rmsd = zero_${s}$
rmsd_err = zero_${s}$
do point = 1, N
! Calculate the k^th difference vector by the formula vec_k = c*R*Q_k + t - P_k
do i = 1, d
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before, vec can be computed using gemv instead

#:if t.startswith('complex')
vec(i) = c * stdlib_dot_product_kahan(conjg(R(i,1:d)), Q(1:d,point))
#:else
vec(i) = c * stdlib_dot_product_kahan(R(i,1:d), Q(1:d,point))
#:endif
end do
vec(1:d) = vec(1:d) + t(1:d) - P(1:d,point)
call kahan_kernel(real(stdlib_dot_product_kahan(vec,vec), kind=${k}$), rmsd, rmsd_err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is preferable to avoid passing expressions as arguments to procedures. This induces the creation of temporal variables. Better to declare the required variables, assign the expression and then pass the argument.

end do
rmsd = sqrt(rmsd * sum_w)
end subroutine
#:endfor
end submodule stdlib_spatial_kabsch_umeyama
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ endif()
add_subdirectory(optval)
add_subdirectory(selection)
add_subdirectory(sorting)
add_subdirectory(spatial)
add_subdirectory(specialfunctions)
if (STDLIB_STATS)
add_subdirectory(stats)
Expand Down
7 changes: 7 additions & 0 deletions test/spatial/CMakeLists.txt
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)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file doesn’t appear to require C-preprocessor directives (it’s pure FYPP). Using fypp_f90pp will run an extra preprocessing stage and produce .F90 output unnecessarily. Consider using fypp_f90 here for consistency with other test directories unless there’s a specific need for f90pp.

Suggested change
fypp_f90pp("${fyppFlags}" "${fppFiles}" outFiles)
fypp_f90("${fyppFlags}" "${fppFiles}" outFiles)

Copilot uses AI. Check for mistakes.

ADDTESTPP(spatial_kabsch_umeyama)
Loading
Loading