Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
db9afa5
Build a KiteGeometry SystemDefinition from KPS3 and KPS4
1-Bort-1 Sep 30, 2026
a7a87e6
Carry the system definition in the torque-control reel-out log
1-Bort-1 Sep 30, 2026
39a72c0
Document system_definition and topology_metadata
1-Bort-1 Sep 30, 2026
f100446
Address review: massless bridle segments, internal assembly helper, u…
1-Bort-1 Sep 30, 2026
afc7d10
merge: latest frame-unification
1-Bort-1 Oct 11, 2026
688934c
merge: latest frame-unification
1-Bort-1 Oct 11, 2026
1ddb5b0
merge: latest main
1-Bort-1 Oct 11, 2026
812892d
merge: latest main
1-Bort-1 Oct 11, 2026
997f9d3
merge: latest main
1-Bort-1 Oct 12, 2026
6034f6c
build: depend on registered KiteGeometry 0.1.0
1-Bort-1 Oct 12, 2026
7d11d7f
merge: latest main
1-Bort-1 Oct 12, 2026
ecaa8a1
build: drop YAML from the test project
1-Bort-1 Oct 12, 2026
b44220a
refactor: save the definition through KiteGeometry's save_log
1-Bort-1 Oct 12, 2026
9ca7e8a
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
1-Bort-1 Oct 12, 2026
3ab2754
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
1-Bort-1 Oct 12, 2026
941c90b
merge: latest main
1-Bort-1 Oct 12, 2026
09e19d6
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
1-Bort-1 Oct 13, 2026
4fd5371
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
1-Bort-1 Oct 13, 2026
d12ea31
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
1-Bort-1 Oct 13, 2026
38faff5
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
ufechner7 Oct 13, 2026
2137939
build: pin KiteGeometry to the save_log commit
1-Bort-1 Oct 14, 2026
dd51d85
merge: latest main
1-Bort-1 Oct 15, 2026
e8007d8
merge: latest main
1-Bort-1 Oct 15, 2026
642f766
build: depend on registered KiteGeometry 0.1.1
1-Bort-1 Oct 15, 2026
faba81e
Merge branch 'main' into agent/322-one-kite-system-definition-across-…
ufechner7 Oct 15, 2026
40e5ca0
feat: name KPS4 kite springs as bridle lines or struts
1-Bort-1 Oct 15, 2026
70eca3a
merge: latest main
1-Bort-1 Oct 17, 2026
2ffaad8
feat: build the definition against KiteGeometry 0.2
1-Bort-1 Oct 17, 2026
6a1de63
merge: latest main
1-Bort-1 Oct 17, 2026
59d66e1
build: pin KiteGeometry to the PR head on top of v0.3.0
1-Bort-1 Oct 19, 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
12 changes: 10 additions & 2 deletions Manifest-v1.12.toml.default
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

julia_version = "1.12.7"
manifest_format = "2.0"
project_hash = "162e9cab96105475bf149cc73f32e0b2834e92d3"
project_hash = "d416c753e698d9dec8c72224c3df6a024aafbbc8"

[[deps.ADTypes]]
deps = ["PrecompileTools"]
Expand Down Expand Up @@ -1396,8 +1396,16 @@ git-tree-sha1 = "9eda8292dd3268b3b7ec9df21bbfac24e177ec52"
uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
version = "0.6.12"

[[deps.KiteGeometry]]
deps = ["JSON", "KiteUtils", "LinearAlgebra", "OrderedCollections", "SHA", "StaticArrays", "YAML"]
git-tree-sha1 = "6ccec684141faa74ce21eaed1b2e055e49825a36"
repo-rev = "ea27241194c7fc91ec5b1d3ec8d753a065e09195"
repo-url = "https://github.com/OpenSourceAWE/KiteGeometry.jl"
uuid = "941ca69c-4b42-4a27-9601-abc4d37bf99e"
version = "0.3.0"

[[deps.KiteModels]]
deps = ["AtmosphericModels", "Dierckx", "DiffEqBase", "DiscretePIDs", "DocStringExtensions", "Interpolations", "KitePodModels", "KiteUtils", "LinearAlgebra", "LinearSolve", "Logging", "NLSolversBase", "NLsolve", "NonlinearSolve", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqNonlinearSolve", "Parameters", "Pkg", "PrecompileTools", "REPL", "Reexport", "Rotations", "StaticArrays", "Statistics", "Sundials", "Suppressor", "WinchModels"]
deps = ["AtmosphericModels", "Dierckx", "DiffEqBase", "DiscretePIDs", "DocStringExtensions", "Interpolations", "KiteGeometry", "KitePodModels", "KiteUtils", "LinearAlgebra", "LinearSolve", "Logging", "NLSolversBase", "NLsolve", "NonlinearSolve", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqNonlinearSolve", "Parameters", "Pkg", "PrecompileTools", "REPL", "Reexport", "Rotations", "StaticArrays", "Statistics", "Sundials", "Suppressor", "WinchModels"]
path = "."
uuid = "b94af626-7959-4878-9336-2adc27959007"
version = "0.13.0"
Expand Down
12 changes: 10 additions & 2 deletions Manifest-v1.13.toml.default
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

julia_version = "1.13.2"
manifest_format = "2.1"
project_hash = "336b0177d1d5ebff387cd289b867b10f9ae0bb6a"
project_hash = "d416c753e698d9dec8c72224c3df6a024aafbbc8"

[[deps.ADTypes]]
deps = ["PrecompileTools"]
Expand Down Expand Up @@ -1554,8 +1554,16 @@ registries = "General"
uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
version = "0.6.12"

[[deps.KiteGeometry]]
deps = ["JSON", "KiteUtils", "LinearAlgebra", "OrderedCollections", "SHA", "StaticArrays", "YAML"]
git-tree-sha1 = "6ccec684141faa74ce21eaed1b2e055e49825a36"
repo-rev = "ea27241194c7fc91ec5b1d3ec8d753a065e09195"
repo-url = "https://github.com/OpenSourceAWE/KiteGeometry.jl"
uuid = "941ca69c-4b42-4a27-9601-abc4d37bf99e"
version = "0.3.0"

[[deps.KiteModels]]
deps = ["AtmosphericModels", "Dierckx", "DiffEqBase", "DiscretePIDs", "DocStringExtensions", "Interpolations", "KitePodModels", "KiteUtils", "LinearAlgebra", "LinearSolve", "Logging", "NLSolversBase", "NLsolve", "NonlinearSolve", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqNonlinearSolve", "Parameters", "Pkg", "PrecompileTools", "REPL", "Reexport", "Rotations", "StaticArrays", "Statistics", "Sundials", "Suppressor", "WinchModels"]
deps = ["AtmosphericModels", "Dierckx", "DiffEqBase", "DiscretePIDs", "DocStringExtensions", "Interpolations", "KiteGeometry", "KitePodModels", "KiteUtils", "LinearAlgebra", "LinearSolve", "Logging", "NLSolversBase", "NLsolve", "NonlinearSolve", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqNonlinearSolve", "Parameters", "Pkg", "PrecompileTools", "REPL", "Reexport", "Rotations", "StaticArrays", "Statistics", "Sundials", "Suppressor", "WinchModels"]
path = "."
uuid = "b94af626-7959-4878-9336-2adc27959007"
version = "0.13.0"
Expand Down
5 changes: 5 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
DiscretePIDs = "c1363496-6848-4723-8758-079b737f6baf"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
KiteGeometry = "941ca69c-4b42-4a27-9601-abc4d37bf99e"
KitePodModels = "9de5dc81-f971-414a-927b-652b2f41c539"
KiteUtils = "90980105-b163-44e5-ba9f-8b1c83bb0533"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand All @@ -36,13 +37,17 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
WinchModels = "7dcfa46b-7979-4771-bbf4-0aee0da42e1f"

[sources]
KiteGeometry = {url = "https://github.com/OpenSourceAWE/KiteGeometry.jl", rev = "ea27241194c7fc91ec5b1d3ec8d753a065e09195"}

[compat]
AtmosphericModels = "0.3.8"
Dierckx = "0.5.4"
DiffEqBase = "6.161.0, 7.21"
DiscretePIDs = "0.1.5, 1.0"
DocStringExtensions = "0.9.4"
Interpolations = "0.15.1, 0.16"
KiteGeometry = "0.3"
KitePodModels = "0.4.0"
KiteUtils = "0.13"
LinearAlgebra = "1.12, 1.13"
Expand Down
8 changes: 8 additions & 0 deletions changelog.d/322-one-kite-system-definition-across-the-ki.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!--
SPDX-FileCopyrightText: 2026 Bart van de Lint
SPDX-License-Identifier: MIT
-->
### Added
- `system_definition(s)` builds a KiteGeometry `SystemDefinition` of a KPS3 or KPS4 model: its points, the springs it integrates as segments, the tether and the winch. Each segment names a `linear` material that holds its density and Young's modulus. KPS4's kite springs are named `bridle_i` where they end at the KCU and `strut_i` where they join two kite points.
- `save_log(logger, system_definition(s), name)` stores that definition in the log, and `KiteGeometry.definition(load_log(name))` reads it back.
- KiteGeometry is re-exported.
19 changes: 19 additions & 0 deletions docs/src/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ states
SysState
```

## System definition

A `SystemDefinition` from [KiteGeometry](https://github.com/OpenSourceAWE/KiteGeometry.jl) describes the points, segments, tether and winch of a model, so a log can be drawn by anything that reads a definition. Build it after `init!` and pass it to `save_log`, which stores it in the log; `KiteGeometry.definition` reads it back from the loaded log:

```julia
integrator = init!(s)
definition = system_definition(s)
# ... simulate and log! ...
save_log(logger, definition, "sim_log")
KiteGeometry.definition(load_log("sim_log"))
```

```@docs
system_definition
```

## Saving the system state

The system state of one time step can be saved as human-readable YAML file, for example to
Expand Down Expand Up @@ -115,4 +131,7 @@ UNUSED_FIELDS_KPS3
inner_loop!
loop!
make_jac
assemble_system_definition
line_material!
spring_name
```
3 changes: 2 additions & 1 deletion examples/reel_out_4p_torque_control.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function simulate(integrator, steps, plot=false)
end

integrator = KiteModels.init!(kps4; prn=STATISTIC)
definition = system_definition(kps4)
kps4.sync_speed = 0.0

av_steps = if PLOT
Expand Down Expand Up @@ -124,7 +125,7 @@ println("Kite mass: $(set.mass) kg")
println("KCU mass: $(set.kcu_mass) kg")
println("Tether diameter: $(set.d_tether) mm")

save_log(logger, "reel_out_4p_torque_control")
save_log(logger, definition, "reel_out_4p_torque_control")
# savefig("docs/src/reelout_force_4p.png")

# Solver: DFBDF, reltol=0.000001
Expand Down
4 changes: 3 additions & 1 deletion src/KiteModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import KiteUtils: SysState, calc_course, calc_elevation, calc_heading
@reexport using KitePodModels
@reexport using WinchModels
@reexport using AtmosphericModels
@reexport using KiteGeometry
using Rotations
import Base.zero
import DiffEqBase
Expand All @@ -39,7 +40,7 @@ export cl_cd, lift_drag, lift_over_drag, reel_out_speed, tether_length, unstretc
export calculate_rotational_inertia!
export copy_model_settings, install_examples_3d, kite_ref_frame, menu2, orient_euler, reactivate_host_app
export get_default_turbulence, load_state, n_springs, save_state, set_default_turbulence, spring_forces, states
export upwind_dir
export system_definition, upwind_dir
import LinearAlgebra: norm

set_zero_subnormals(true) # required to avoid drastic slow down on Intel CPUs when numbers become very small
Expand Down Expand Up @@ -166,6 +167,7 @@ end

include("KPS4.jl") # include code, specific for the four point kite model
include("KPS3.jl") # include code, specific for the one point kite model
include("system_definition.jl")
include("utils.jl") # functions to calculate the initial state vector, the initial masses and initial springs

function menu2()
Expand Down
107 changes: 107 additions & 0 deletions src/system_definition.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: 2026 Bart van de Lint
# SPDX-License-Identifier: MIT

"""
system_definition(s::KPS4)

The `SystemDefinition` of the four-point model: its points where they are now, and as
segments the springs it integrates, at full stiffness. Segments are named `tether_i`, and
the massless kite springs `bridle_i` where they end at the KCU and `strut_i` where they join
two kite points. Call it after `init!` for the initial pose.
"""
function system_definition(s::KPS4)
tether_segments = s.set.segments
extra_masses = zeros(length(s.pos))
extra_masses[tether_segments + 1] = s.set.kcu_mass
extra_masses[(tether_segments + 2):end] .= s.masses[(tether_segments + 2):end]
materials = Material[]
segments = Segment[]
for (i, spring) in enumerate(s.springs)
in_tether = i <= tether_segments
diameter = (in_tether ? s.set.d_tether : s.set.d_line) / 1000
density = in_tether ? s.set.rho_tether : 0.0
material = line_material!(materials, in_tether ? "tether" : "kite_line", density,
spring.axial_stiffness * spring.length, diameter)
push!(segments, Segment(; name=spring_name(s, i, spring),
points=(Int(spring.p1), Int(spring.p2)),
rest_length=spring.length, diameter, material))
end
return assemble_system_definition(s, materials, segments, extra_masses)
end

"""
spring_name(s::KPS4, i, spring)

The segment name of `spring`, the `i`-th of `s.springs`: `tether_i` for a tether spring,
then, numbered from 1, `bridle_j` for a kite spring ending at the KCU and `strut_j` for one
joining two kite points.
"""
function spring_name(s::KPS4, i, spring)
tether_segments = s.set.segments
i <= tether_segments && return "tether_$i"
kcu = tether_segments + 1
role = kcu in (spring.p1, spring.p2) ? "bridle" : "strut"
return "$(role)_$(i - tether_segments)"
end

"""
system_definition(s::KPS3)

The `SystemDefinition` of the one-point model: its points where they are now, the tether
segments between them at full stiffness, and the kite and KCU mass on the last point. Call
it after `init!` for the initial pose.
"""
function system_definition(s::KPS3)
extra_masses = zeros(length(s.pos))
extra_masses[end] = s.set.mass + s.set.kcu_mass
diameter = s.set.d_tether / 1000
materials = Material[]
material = line_material!(materials, "tether", s.set.rho_tether,
s.axial_stiffness * s.segment_length, diameter)
segments = [Segment(; name="tether_$i", points=(i, i + 1), rest_length=s.segment_length,
diameter, material)
for i in 1:s.set.segments]
return assemble_system_definition(s, materials, segments, extra_masses)
end

"""
line_material!(materials, name, density, unit_stiffness, diameter)

The name of the `linear` material among `materials` of `density` [kg/m³] that gives a
line of `diameter` [m] the axial stiffness times rest length `unit_stiffness` [N], added
as `name` where none matches.
"""
function line_material!(materials, name, density, unit_stiffness, diameter)
area = π * diameter^2 / 4
parameters = Dict{String, Any}("density" => density,
"youngs_modulus" => unit_stiffness / area)
for material in materials
material.parameters == parameters && return material.name
end
push!(materials, Material(; name, law="linear", parameters))
return name
end

"""
assemble_system_definition(s::AbstractKiteModel, materials, segments, extra_masses)

The `SystemDefinition` of `s` with its `segments`, made of `materials`, the first
`s.set.segments` of which are the tether from the winch at point 1 to point
`s.set.segments + 1`.
"""
function assemble_system_definition(s::AbstractKiteModel, materials, segments, extra_masses)
n_points = length(s.pos)
tether_segments = s.set.segments
points = [Point(; name=string(i), fixed=i == 1, pos_ENU=s.pos[i],
extra_mass=extra_masses[i])
for i in 1:n_points]
tether = Tether(; name="tether", start_point=1, end_point=tether_segments + 1,
segments=collect(1:tether_segments))
winch = Winch(; name="winch", tethers=[1], winch_point=1, gear_ratio=s.set.gear_ratio,
drum_radius=s.set.drum_radius)
tables = (; materials, points, segments, tethers=[tether], winches=[winch])
name = string(nameof(typeof(s)))
draft = SystemDefinition(; metadata=Metadata(; name, n_points), tables...)
metadata = Metadata(; name, n_points, connectivity_sha=connectivity_sha(draft))
return SystemDefinition(; metadata, tables...)
end
77 changes: 77 additions & 0 deletions test/test-system-definition.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: 2026 Bart van de Lint
# SPDX-License-Identifier: MIT

using Pkg
if dirname(Pkg.project().path) != @__DIR__
Pkg.activate(@__DIR__)
end
using KiteModels, KiteUtils, Test
using KiteModels: init!

set_data_path(joinpath(dirname(dirname(pathof(KiteModels)::String)), "data"))

"The pair of point indices each segment of `definition` joins."
segment_points(definition) = [segment.points for segment in definition.segments]

"The `parameters` of the material `segment` of `definition` is made of."
parameters(definition, segment) = definition.materials[segment.material].parameters

"The axial stiffness times rest length [N] of `segment`, from its `linear` material."
unit_stiffness(definition, segment) =
parameters(definition, segment)["youngs_modulus"] * π * segment.diameter^2 / 4

@testset "KPS4 definition holds the points and springs the model integrates" begin
kps4 = KPS4(load_settings("system.yaml"))
init!(kps4; prn=false)
definition = system_definition(kps4)
segments = kps4.set.segments
@test definition.metadata.n_points == length(kps4.pos) == segments + 5
@test segment_points(definition) == [(spring.p1, spring.p2) for spring in kps4.springs]
@test [point.pos_ENU for point in definition.points] == kps4.pos
@test [point.fixed for point in definition.points] == (1:length(kps4.pos) .== 1)
@test [segment.rest_length for segment in definition.segments] ==
[spring.length for spring in kps4.springs]
@test only(definition.tethers).segments == 1:segments
@test only(definition.tethers).end_point == segments + 1
@test only(definition.winches).winch_point == 1
@test [unit_stiffness(definition, segment) for segment in definition.segments] ≈
[spring.axial_stiffness * spring.length for spring in kps4.springs]
@test sum(point.extra_mass for point in definition.points) ≈
kps4.set.mass + kps4.set.kcu_mass
@test all(parameters(definition, segment)["density"] == 0
for segment in definition.segments[(segments + 1):end])
@test all(material.law == "linear" for material in definition.materials)
@test definition.metadata.connectivity_sha == connectivity_sha(definition)
kcu = segments + 1
kite_springs = kps4.springs[(segments + 1):end]
@test [segment.name for segment in definition.segments] ==
[["tether_$i" for i in 1:segments];
[kcu in (spring.p1, spring.p2) ? "bridle_$i" : "strut_$i"
for (i, spring) in enumerate(kite_springs)]]
@test count(startswith("strut"), segment.name for segment in definition.segments) == 6
end

@testset "KPS3 definition is one tether from the winch to the kite" begin
kps3 = KPS3(load_settings("system.yaml"))
init!(kps3)
definition = system_definition(kps3)
segments = kps3.set.segments
@test definition.metadata.n_points == length(kps3.pos) == segments + 1
@test segment_points(definition) == [(i, i + 1) for i in 1:segments]
@test only(definition.tethers).segments == 1:segments
@test [segment.name for segment in definition.segments] ==
["tether_$i" for i in 1:segments]
@test definition.points[end].extra_mass ≈ kps3.set.mass + kps3.set.kcu_mass
end

@testset "a saved log carries the KPS4 definition under topology" begin
kps4 = KPS4(load_settings("system.yaml"))
init!(kps4; prn=false)
logger = Logger(length(kps4.pos), 1; segments=n_springs(kps4))
log!(logger, SysState(kps4))
path = mktempdir()
definition = system_definition(kps4)
save_log(logger, definition, "topology"; path)
@test structure_document(KiteGeometry.definition(load_log("topology"; path))) ==
structure_document(definition)
end
Loading