From e55be4bb5da98eab3bc506ddd6a54cb91d1bb6f3 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:42:19 +0200 Subject: [PATCH 1/4] fix: make calc_heading a method of KiteUtils.calc_heading KiteGeometry's two-argument calc_heading was its own function, so loading it beside KiteUtils left calc_heading ambiguous. It is now a method of KiteUtils.calc_heading, re-exported as the same binding. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ docs/src/structure.md | 2 +- src/KiteGeometry.jl | 2 +- src/transforms.jl | 2 +- test/test-transforms.jl | 8 +++++++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d585997..4d8a658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ SPDX-License-Identifier: MIT - `Metadata(; name, n_points)` builds metadata by keyword, with the other fields defaulting to an empty description and note, the awesIO version this package reads, the structure schema, and an empty `connectivity_sha`. +### Fixed + +- `calc_heading(R_b_to_w, pos)` is a method of KiteUtils' `calc_heading`, so `using KiteUtils, KiteGeometry` no longer leaves `calc_heading` ambiguous and both forms are callable unqualified. + ## KiteGeometry v0.3.0 2026-10-18 ### Added diff --git a/docs/src/structure.md b/docs/src/structure.md index ad413b7..e6250e9 100644 --- a/docs/src/structure.md +++ b/docs/src/structure.md @@ -68,7 +68,7 @@ place! place_transforms! reposition! transform_velocities -calc_heading +calc_heading(::Any, ::Any) ``` ## Components diff --git a/src/KiteGeometry.jl b/src/KiteGeometry.jl index 0ee8aa0..fe6ec28 100644 --- a/src/KiteGeometry.jl +++ b/src/KiteGeometry.jl @@ -15,7 +15,7 @@ for more information. module KiteGeometry using JSON: JSON -using KiteUtils: KiteUtils, Logger, Settings, SysLog, save_log, wrap2pi +using KiteUtils: KiteUtils, Logger, Settings, SysLog, calc_heading, save_log, wrap2pi using LinearAlgebra: Diagonal, I, cross, dot, norm, normalize using OrderedCollections: OrderedDict, OrderedSet using SHA: sha256 diff --git a/src/transforms.jl b/src/transforms.jl index 8f09eae..936246b 100644 --- a/src/transforms.jl +++ b/src/transforms.jl @@ -117,7 +117,7 @@ Heading [rad] of the frame `R_b_to_w` at `pos` [m] from the base: the angle of i in the tangential plane, from the elevation direction towards the azimuthal one; zero with the nose towards the base. """ -function calc_heading(R_b_to_w, pos) +function KiteUtils.calc_heading(R_b_to_w, pos) e_x_t = calc_R_t_to_w(pos)' * R_b_to_w[:, 1] return atan(e_x_t[2], e_x_t[1]) end diff --git a/test/test-transforms.jl b/test/test-transforms.jl index 662e7f1..b2382d0 100644 --- a/test/test-transforms.jl +++ b/test/test-transforms.jl @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT using KiteGeometry: radial_direction -using KiteUtils: Settings, set_data_path +using KiteUtils: KiteUtils, Settings, set_data_path using LinearAlgebra: norm, normalize using YAML @@ -135,3 +135,9 @@ end @test iszero(body_spin[1]) == (moving == 1) end end + +@testset "calc_heading is one function shared with KiteUtils" begin + probe = Module() + Core.eval(probe, :(using KiteUtils, KiteGeometry)) + @test Core.eval(probe, :calc_heading) === KiteUtils.calc_heading +end From 72b5ac267c34d9215d662cb6b2f01e34e04b96dc Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:46:53 +0200 Subject: [PATCH 2/4] test: call both calc_heading forms unqualified Pin which package owns each method and call each through the binding `using KiteUtils, KiteGeometry` resolves, so a two-argument method added to KiteUtils that replaced this one would fail here. Co-Authored-By: Claude Opus 5.5 --- test/test-transforms.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test-transforms.jl b/test/test-transforms.jl index b2382d0..0f8abac 100644 --- a/test/test-transforms.jl +++ b/test/test-transforms.jl @@ -139,5 +139,13 @@ end @testset "calc_heading is one function shared with KiteUtils" begin probe = Module() Core.eval(probe, :(using KiteUtils, KiteGeometry)) - @test Core.eval(probe, :calc_heading) === KiteUtils.calc_heading + heading = Core.eval(probe, :calc_heading) + @test heading === KiteUtils.calc_heading + yawed = [0.0 -1 0; 1 0 0; 0 0 1] + @test parentmodule(which(heading, Tuple{Matrix{Float64}, Vector{Float64}})) == + KiteGeometry + @test heading(yawed, [0.0, 0, 1]) ≈ π / 2 + @test parentmodule(which(heading, Tuple{Matrix{Float64}, Float64, Float64})) == + KiteUtils + @test isfinite(heading(yawed, deg2rad(30), 0.0)) end From 3f80e655b297be456c77c41309ea22d6c8653d25 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:03:24 +0200 Subject: [PATCH 3/4] refactor: place transforms with KiteUtils' calc_heading KiteGeometry's calc_heading(R_b_to_w, pos) duplicated KiteUtils' calc_heading(attitude, elevation, azimuth), which takes a rotation matrix and gives the same angle with the opposite sign. turn! now calls KiteUtils' function with the transform's own angles; the KiteGeometry method and its calc_R_t_to_w helper are gone and the name is no longer exported. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++++++---- docs/src/structure.md | 1 - src/KiteGeometry.jl | 3 +-- src/transforms.jl | 29 +++-------------------------- test/test-transforms.jl | 22 +++++++++------------- 5 files changed, 19 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e05f88..4508386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: MIT # Changelog +## Unreleased + +### Changed + +- BREAKING: KiteGeometry no longer has a `calc_heading(R_b_to_w, pos)` of its own; use KiteUtils' `calc_heading(attitude, elevation, azimuth)`. `using KiteUtils, KiteGeometry` no longer leaves `calc_heading` ambiguous. + ## KiteGeometry v0.3.2 2026-10-21 ### Added @@ -23,10 +29,6 @@ SPDX-License-Identifier: MIT - `Metadata(; name, n_points)` builds metadata by keyword, with the other fields defaulting to an empty description and note, the awesIO version this package reads, the structure schema, and an empty `connectivity_sha`. -### Fixed - -- `calc_heading(R_b_to_w, pos)` is a method of KiteUtils' `calc_heading`, so `using KiteUtils, KiteGeometry` no longer leaves `calc_heading` ambiguous and both forms are callable unqualified. - ## KiteGeometry v0.3.0 2026-10-18 ### Added diff --git a/docs/src/structure.md b/docs/src/structure.md index a37a035..6bdb6ae 100644 --- a/docs/src/structure.md +++ b/docs/src/structure.md @@ -69,7 +69,6 @@ place! place_transforms! reposition! transform_velocities -calc_heading(::Any, ::Any) ``` ## Components diff --git a/src/KiteGeometry.jl b/src/KiteGeometry.jl index 52646e4..8f7c074 100644 --- a/src/KiteGeometry.jl +++ b/src/KiteGeometry.jl @@ -24,8 +24,7 @@ using YAML: YAML export SystemDefinition, Rows, Metadata, Material, Point, Segment, SpanwiseStation, Pulley, Tether, Winch, Wing, CanopyFace, Body, Tube, pos_KA -export Transform, Pose, place!, place_transforms!, reposition!, transform_velocities, - calc_heading +export Transform, Pose, place!, place_transforms!, reposition!, transform_velocities export NameRef, AbstractModel, NoModel, SegmentSpring, register_model!, model_type, model_columns export load_structure, load_authoring, structure_document, connectivity_sha diff --git a/src/transforms.jl b/src/transforms.jl index 938d254..4657d3a 100644 --- a/src/transforms.jl +++ b/src/transforms.jl @@ -97,31 +97,6 @@ Pose(placement::Placement) = Pose(placement.pos, placement.body_pos, placement.b # ==================== TANGENTIAL FRAME ==================== # -""" - calc_R_t_to_w(pos) - -Rotation from the tangential frame at `pos` [m] into the world: z radial along `pos`, y -azimuthal and level, x towards elevation, `y × z`. -""" -function calc_R_t_to_w(pos) - z = normalize(pos) - y = abs(pos[1]) < 1e-6 && abs(pos[2]) < 1e-6 ? Vec3(0, 1, 0) : - normalize(Vec3(-pos[2], pos[1], 0)) - return hcat(cross(y, z), y, z) -end - -""" - calc_heading(R_b_to_w, pos) - -Heading [rad] of the frame `R_b_to_w` at `pos` [m] from the base: the angle of its x axis -in the tangential plane, from the elevation direction towards the azimuthal one; zero -with the nose towards the base. -""" -function KiteUtils.calc_heading(R_b_to_w, pos) - e_x_t = calc_R_t_to_w(pos)' * R_b_to_w[:, 1] - return atan(e_x_t[2], e_x_t[1]) -end - """ radial_direction(transform) @@ -203,7 +178,9 @@ function turn!(pose, transform, base_pos) radial = radial_direction(transform) rotate_members!(pose, transform, base_pos, min_rotation(normalize(rel_pos), radial)...) isnothing(transform.body) && return nothing - heading = calc_heading(pose.body_R[transform.body], radial) + # KiteUtils' heading turns the other way to a transform's + heading = -calc_heading(pose.body_R[transform.body], transform.elevation, + -transform.azimuth) rotate_members!(pose, transform, base_pos, radial, wrap2pi(transform.heading - heading)) return nothing diff --git a/test/test-transforms.jl b/test/test-transforms.jl index 7d32426..3bbb265 100644 --- a/test/test-transforms.jl +++ b/test/test-transforms.jl @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT using KiteGeometry: radial_direction -using KiteUtils: KiteUtils, Settings, set_data_path +using KiteUtils: KiteUtils, Settings, calc_heading, set_data_path using LinearAlgebra: norm, normalize using YAML @@ -34,6 +34,10 @@ function tilt_kite!(transforms) return transforms end +"""Heading [rad] of the kite body of `pose`, in the sense of `transform`'s `heading`.""" +kite_heading(pose, transform) = + -calc_heading(pose.body_R[1], transform.elevation, -transform.azimuth; respos=false) + """`data`, the chained system's YAML, with its kite tilted as [`tilt_kite!`](@ref).""" function tilt_kite_row!(data) data["transforms"]["data"][2][2] = 48.0 @@ -75,7 +79,7 @@ end ground, kcu, kite_origin = pose.pos[1], pose.pos[2], pose.pos[7] @test normalize(kcu - ground) ≈ radial_direction(main_tf) @test normalize(kite_origin - kcu) ≈ radial_direction(kite_tilt) - @test calc_heading(pose.body_R[1], kite_origin - kcu) ≈ kite_tilt.heading + @test kite_heading(pose, kite_tilt) ≈ kite_tilt.heading @test norm(pose.pos[4] - kite_origin) ≈ kite_rigid end @@ -85,7 +89,7 @@ end kcu, kite_origin = pose.pos[2], pose.pos[7] @test pose.pos[2] == system.points[2].pos_ENU @test normalize(kite_origin - kcu) ≈ radial_direction(transforms[2]) - @test calc_heading(pose.body_R[1], kite_origin - kcu) ≈ deg2rad(-35) + @test kite_heading(pose, transforms[2]) ≈ deg2rad(-35) end """`data`, the chained system's YAML, with its first transform turning no point.""" @@ -136,16 +140,8 @@ end end end -@testset "calc_heading is one function shared with KiteUtils" begin +@testset "calc_heading is KiteUtils' alone" begin probe = Module() Core.eval(probe, :(using KiteUtils, KiteGeometry)) - heading = Core.eval(probe, :calc_heading) - @test heading === KiteUtils.calc_heading - yawed = [0.0 -1 0; 1 0 0; 0 0 1] - @test parentmodule(which(heading, Tuple{Matrix{Float64}, Vector{Float64}})) == - KiteGeometry - @test heading(yawed, [0.0, 0, 1]) ≈ π / 2 - @test parentmodule(which(heading, Tuple{Matrix{Float64}, Float64, Float64})) == - KiteUtils - @test isfinite(heading(yawed, deg2rad(30), 0.0)) + @test Core.eval(probe, :calc_heading) === KiteUtils.calc_heading end From aa0f821feea608184607c891aac4172d9557a33f Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:27:41 +0100 Subject: [PATCH 4/4] fix: read heading's azimuth off the transform's radial turn! negated transform.azimuth to get KiteUtils' east-to-north azimuth, which goes wrong as soon as the transform's azimuth changes sense, as #86 does. Reading it off radial_direction holds in either sense, and dividing by cos(elevation) keeps it continuous through the zenith. The test helper now measures heading from where the kite stands rather than repeating the same sign, so a mismatch fails. Co-Authored-By: Claude Opus 5.5 --- src/transforms.jl | 5 +++-- test/test-transforms.jl | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/transforms.jl b/src/transforms.jl index 3dfc425..0d075c9 100644 --- a/src/transforms.jl +++ b/src/transforms.jl @@ -180,9 +180,10 @@ function turn!(pose, transform, base_pos) radial = radial_direction(transform) rotate_members!(pose, transform, base_pos, min_rotation(normalize(rel_pos), radial)...) isnothing(transform.body) && return nothing - # KiteUtils' heading turns the other way to a transform's + cos_el = cos(transform.elevation) + # KiteUtils' azimuth runs from east to north; its heading turns the other way to ours heading = -calc_heading(pose.body_R[transform.body], transform.elevation, - -transform.azimuth) + atan(radial[2] / cos_el, radial[1] / cos_el)) rotate_members!(pose, transform, base_pos, radial, wrap2pi(transform.heading - heading)) return nothing diff --git a/test/test-transforms.jl b/test/test-transforms.jl index 3bbb265..a438119 100644 --- a/test/test-transforms.jl +++ b/test/test-transforms.jl @@ -34,9 +34,12 @@ function tilt_kite!(transforms) return transforms end -"""Heading [rad] of the kite body of `pose`, in the sense of `transform`'s `heading`.""" -kite_heading(pose, transform) = - -calc_heading(pose.body_R[1], transform.elevation, -transform.azimuth; respos=false) +"""Heading [rad] of the kite body of `pose`, in the sense of a transform's `heading`, in +the tangential frame of where its origin stands from the KCU.""" +function kite_heading(pose) + x, y, z = pose.pos[7] - pose.pos[2] + return -calc_heading(pose.body_R[1], atan(z, hypot(x, y)), atan(y, x); respos=false) +end """`data`, the chained system's YAML, with its kite tilted as [`tilt_kite!`](@ref).""" function tilt_kite_row!(data) @@ -79,7 +82,7 @@ end ground, kcu, kite_origin = pose.pos[1], pose.pos[2], pose.pos[7] @test normalize(kcu - ground) ≈ radial_direction(main_tf) @test normalize(kite_origin - kcu) ≈ radial_direction(kite_tilt) - @test kite_heading(pose, kite_tilt) ≈ kite_tilt.heading + @test kite_heading(pose) ≈ kite_tilt.heading @test norm(pose.pos[4] - kite_origin) ≈ kite_rigid end @@ -89,7 +92,16 @@ end kcu, kite_origin = pose.pos[2], pose.pos[7] @test pose.pos[2] == system.points[2].pos_ENU @test normalize(kite_origin - kcu) ≈ radial_direction(transforms[2]) - @test kite_heading(pose, transforms[2]) ≈ deg2rad(-35) + @test kite_heading(pose) ≈ deg2rad(-35) +end + +@testset "a heading body turns smoothly through the zenith" begin + kite_frame_at(elevation) = begin + system, transforms = load_chained() + transforms[2].elevation = deg2rad(elevation) + return Pose(place!(system, transforms)).body_R[1] + end + @test norm(kite_frame_at(90.1) - kite_frame_at(89.9)) < 0.01 end """`data`, the chained system's YAML, with its first transform turning no point."""