Skip to content

Commit b7d0166

Browse files
authored
Add a Winch interface (#210)
* Generalize accessor type * Revert "Generalize accessor type" This reverts commit 254f642. * Eval for symbolic register * Working kps4 example * Fix orientation matrix * Looks to work fine * More general types, bench still passes * Getting close to kitemodels example * Use data interpolations * Improved match by using cubic spline * Update settings fields * Keep one example * Use simplified aero * Don't enforce nonlin * Add example and test * Add forwarddiff * Remove separate twist * Update manifests * Working lin example * Simplified aero and improved twisting * Align examples * Fix rhs allocs * Test allocs for all integrators * Update manifests * Add alloc testing to all integrators * Update manifests * Add missing docstrings * Remove render * Update packages * Require vsm update * Update default manifests * Correct citations * Reset integ by default * Simplify init * Larger dt for linear wing * Update manifests * Tighter tol * Update default manifests * Fix licenses * Default no remake * Better prn true false * Fix conflicts * Fix documentation * Move to exported * Same name for faster testing * Fix yaml weighted ref test * Update default manifests * Larger dt for lin wing * Add winch interface * Add steering tape winch * Custom tape winch * Point straight down * Use min wind for linearize * Add .log
1 parent d49b078 commit b7d0166

15 files changed

Lines changed: 634 additions & 189 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/LocalPreferences*
2+
*.log
23
*.old
34
*.mp4
45
*.gif

docs/src/exported_functions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ set_body_frame_damping
4141
calc_steady_torque
4242
```
4343

44+
## Winch components
45+
46+
The winch motor model is a pluggable MTK `ODESystem`. Pass a builder
47+
to `Winch(...; model=...)` to override the default torque-driven motor.
48+
Custom components must respect the connector contract enforced by
49+
[`validate_winch_component`](@ref).
50+
51+
```@docs
52+
default_winch_component
53+
validate_winch_component
54+
```
55+
4456
## State accessor functions
4557

4658
Use these functions to retrieve state information and calculated values from a model

docs/src/tutorial_julia.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,16 @@ inertia_total = 0.024 # [kgm²]
172172

173173
winches = [Winch(:winch, [:main],
174174
gear_ratio, drum_radius, f_coulomb, c_vf, inertia_total;
175-
winch_point=:anchor)]
175+
winch_point=:anchor,
176+
model=default_winch_component)]
176177
```
177178

179+
The `model` keyword selects the MTK component that defines the
180+
motor dynamics. [`default_winch_component`](@ref) is the built-in
181+
torque-driven motor; you can pass any builder of the same signature
182+
to swap in a custom motor model — see
183+
[Custom winch components](#Custom-winch-components) below.
184+
178185
Build and simulate with a constant torque of -20 Nm:
179186

180187
```julia
@@ -208,6 +215,76 @@ SymbolicAWEModels.record(lg, sam.sys_struct, "winch_sim.gif")
208215

209216
![Winch simulation](assets/winch_sim.gif)
210217

218+
### Custom winch components
219+
220+
The default winch is a pure-algebraic motor with no internal state.
221+
A researcher's motor model often has *its own* dynamic state — a
222+
current integrator, a controller, a thermal model. Plug it in by
223+
writing a builder that returns an `ODESystem` with the same connector
224+
contract.
225+
226+
The connectors are:
227+
228+
| connector | direction | meaning |
229+
|-------------|-----------|-------------------------------------|
230+
| `vel` | input | drum-perimeter velocity [m/s] |
231+
| `force` | input | summed tether tension at winch [N] |
232+
| `set_value` | input | abstract setpoint (you choose units)|
233+
| `brake` | input | brake in [0, 1] |
234+
| `acc` | output | drum-perimeter acceleration [m/s²] |
235+
| `friction` | output | friction torque [N·m] |
236+
237+
The component is algebraic at the connector boundary (the outer
238+
integrator owns `winch_vel` and `tether_len`), but it may declare
239+
any number of internal differential states. Example: a current-driven
240+
motor where `set_value` is the *commanded* current and the actual
241+
current `I` lags via a first-order electrical time constant `τ_e`:
242+
243+
```julia
244+
using ModelingToolkit
245+
using ModelingToolkit: t_nounits as t, D_nounits as D
246+
247+
function current_driven_winch(sys_struct, winch_idx; name)
248+
SST = typeof(sys_struct)
249+
@parameters (psys::SST = sys_struct), [tunable = false]
250+
@variables begin
251+
vel(t); force(t); set_value(t); brake(t)
252+
acc(t); friction(t)
253+
I(t) = 0.0
254+
end
255+
K_t = 0.5
256+
τ_e = 0.02
257+
drum = SymbolicAWEModels.get_winch_drum_radius(psys, winch_idx)
258+
gear = SymbolicAWEModels.get_winch_gear_ratio(psys, winch_idx)
259+
inertia = SymbolicAWEModels.get_winch_inertia_total(psys, winch_idx)
260+
ratio = drum / gear
261+
262+
eqs = [
263+
D(I) ~ (set_value - I) / τ_e
264+
friction ~ 0.0
265+
acc ~ ifelse(brake > 0.5, 0.0,
266+
ratio * (K_t * I + ratio * force) / inertia)
267+
]
268+
return System(eqs, t; name)
269+
end
270+
```
271+
272+
Pass the builder to the `Winch` constructor:
273+
274+
```julia
275+
winches = [Winch(:winch, [:main], gear_ratio, drum_radius,
276+
f_coulomb, c_vf, inertia_total;
277+
winch_point=:anchor,
278+
model=current_driven_winch)]
279+
```
280+
281+
SymbolicAWEModels automatically calls
282+
[`validate_winch_component`](@ref) on the returned subsystem during
283+
model build, and reports a clear error if a required connector is
284+
missing or if the component tries to take ownership of `D(vel)` or
285+
`D(len)`. You can also call it directly on a manually built subsystem
286+
during development.
287+
211288
## Step 3: adding a pulley
212289

213290
A [`Pulley`](@ref) enforces length redistribution between two

examples/custom_tape_winch.jl

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# SPDX-FileCopyrightText: 2026 Bart van de Lint
2+
# SPDX-License-Identifier: LGPL-3.0-only
3+
4+
"""
5+
Custom winch model: cascaded length-velocity control with a v_max cap.
6+
7+
`set_value` is interpreted as a target tether length [m]. An outer
8+
proportional law turns the length error into a velocity reference,
9+
hard-clamped at ±v_max. An inner P-on-velocity controller with load
10+
feedforward tracks that reference. The feedforward (`+friction -
11+
ratio·force`) cancels the load disturbance so tether spring-mass
12+
bouncing doesn't perturb `vel`. No integrator → no windup at
13+
saturation → no overshoot. Closed-loop velocity response is 1st-order
14+
with time constant `I / (ratio · K_p)`.
15+
"""
16+
17+
using Pkg
18+
if Base.active_project() != joinpath(@__DIR__, "Project.toml")
19+
Pkg.activate(@__DIR__)
20+
end
21+
22+
using GLMakie
23+
using KiteUtils: init!, next_step!
24+
using ModelingToolkit
25+
using ModelingToolkit: t_nounits as t, D_nounits as D
26+
using SymbolicAWEModels
27+
using SymbolicAWEModels: SystemStructure,
28+
get_winch_gear_ratio, get_winch_drum_radius,
29+
get_winch_f_coulomb, get_winch_c_vf,
30+
get_winch_inertia_total, get_winch_friction_epsilon
31+
import SymbolicAWEModels: Point # resolve ambiguity with GLMakie
32+
33+
set_data_path(joinpath(dirname(@__DIR__), "data"))
34+
set = Settings("base/system.yaml")
35+
set.v_wind = 0.0
36+
37+
function make_length_to_velocity_winch(;
38+
v_max::Float64, K_pos::Float64, K_p::Float64)
39+
return function (sys_struct::SystemStructure, winch_idx::Int; name)
40+
SST = typeof(sys_struct)
41+
@parameters (psys::SST = sys_struct), [tunable = false]
42+
@variables begin
43+
vel(t)
44+
len(t)
45+
force(t)
46+
set_value(t)
47+
brake(t)
48+
acc(t)
49+
friction(t)
50+
ω_motor(t)
51+
vel_unclamped(t)
52+
vel_ref(t)
53+
tau_cmd(t)
54+
tau_net(t)
55+
end
56+
57+
gear_ratio = get_winch_gear_ratio(psys, winch_idx)
58+
drum_radius = get_winch_drum_radius(psys, winch_idx)
59+
f_coulomb = get_winch_f_coulomb(psys, winch_idx)
60+
c_vf = get_winch_c_vf(psys, winch_idx)
61+
inertia_total = get_winch_inertia_total(psys, winch_idx)
62+
friction_eps = get_winch_friction_epsilon(psys, winch_idx)
63+
smooth_sign(x, eps) = x / sqrt(x * x + eps * eps)
64+
ratio = drum_radius / gear_ratio
65+
66+
eqs = [
67+
ω_motor ~ vel / ratio
68+
friction ~ smooth_sign(ω_motor, friction_eps) *
69+
f_coulomb * ratio +
70+
c_vf * ω_motor * ratio^2
71+
vel_unclamped ~ K_pos * (set_value - len)
72+
vel_ref ~ max(-v_max, min(v_max, vel_unclamped))
73+
tau_cmd ~ K_p * (vel_ref - vel) + friction - ratio * force
74+
tau_net ~ tau_cmd + ratio * force - friction
75+
acc ~ ifelse(brake > 0.5, 0.0,
76+
ratio * tau_net / inertia_total)
77+
]
78+
return System(eqs, t,
79+
[vel, len, force, set_value, brake, acc, friction,
80+
ω_motor, vel_unclamped, vel_ref, tau_cmd, tau_net],
81+
[psys]; name)
82+
end
83+
end
84+
85+
points = [
86+
Point(:ground, [0.0, 0.0, 0.0], STATIC),
87+
Point(:mass, [0.0, 0.0, -50.0], DYNAMIC; extra_mass=10.0),
88+
]
89+
segments = [
90+
Segment(:line, :ground, :mass, 50_000.0, 500.0, 0.005; l0=50.0),
91+
]
92+
tethers = [Tether(:main, [:line], 50.0)]
93+
winches = [Winch(:winch, set, [:main]; winch_point=:ground,
94+
model=make_length_to_velocity_winch(
95+
v_max=1.0, K_pos=50.0, K_p=10.0))]
96+
winches[1].inertia_total = 0.001 # tiny rotor → near-instant velocity tracking
97+
98+
sys_struct = SystemStructure("custom_winch_vel", set;
99+
points, segments, tethers, winches)
100+
sam = SymbolicAWEModel(set, sys_struct)
101+
init!(sam; remake=true, prn=false)
102+
103+
winch = sam.sys_struct.winches[:winch]
104+
tether = sam.sys_struct.tethers[:main]
105+
106+
dt = 0.02
107+
n_steps = 1500
108+
times = Float64[]
109+
ref_len = Float64[]
110+
meas_len = Float64[]
111+
meas_vel = Float64[]
112+
113+
for k in 1:n_steps
114+
tnow = k * dt
115+
l_target =
116+
tnow < 2.0 ? 50.0 :
117+
tnow < 15.0 ? 45.0 :
118+
55.0
119+
next_step!(sam; set_values=[l_target], dt=dt, vsm_interval=0)
120+
push!(times, tnow)
121+
push!(ref_len, l_target)
122+
push!(meas_len, tether.len)
123+
push!(meas_vel, winch.vel)
124+
end
125+
126+
fig = Figure(size=(900, 600))
127+
ax1 = Axis(fig[1, 1]; ylabel="tether length [m]",
128+
title="Cascaded length→velocity winch (v_max=1.0 m/s)")
129+
lines!(ax1, times, ref_len; label="target", linestyle=:dash)
130+
lines!(ax1, times, meas_len; label="measured")
131+
axislegend(ax1; position=:rb)
132+
133+
ax2 = Axis(fig[2, 1]; xlabel="t [s]",
134+
ylabel="reel-out velocity [m/s]")
135+
hlines!(ax2, [+1.0, -1.0]; color=:gray, linestyle=:dot)
136+
lines!(ax2, times, meas_vel)
137+
138+
display(fig)

examples/menu.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ options = [
2424
"kps4_comparison = include(\"kps4_comparison.jl\")",
2525
"vsm_linearization = include(\"vsm_linearization.jl\")",
2626
"sam_tutorial = include(\"sam_tutorial.jl\")",
27+
"custom_winch = include(\"custom_winch.jl\")",
28+
"custom_winch_vel = include(\"custom_winch_vel.jl\")",
2729
"quit",
2830
]
2931

src/SymbolicAWEModels.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export winch_force
9494
export unstretched_length
9595
export tether_length
9696

97+
# --- Winch component API ---
98+
export default_winch_component
99+
export validate_winch_component
100+
97101
# --- Helper Functions ---
98102
export init_module
99103
export update_plot_observables!
@@ -150,6 +154,8 @@ function plot_aoa end
150154
function find_steady_state! end
151155
function make_lin_sys_state end
152156
function create_model_archive end
157+
function default_winch_component end
158+
function validate_winch_component end
153159

154160
function __init__()
155161
data_dir = joinpath(pwd(), "data")

src/generate_system/accessors.jl

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,6 @@ get_brake(sys::SystemStructure, idx::Int64) =
252252
sys.winches[idx].brake
253253
@register_symbolic get_brake(
254254
sys::SystemStructure, idx::Int64)
255-
get_speed_controlled(sys::SystemStructure, idx::Int64) =
256-
sys.winches[idx].speed_controlled
257-
@register_symbolic get_speed_controlled(
258-
sys::SystemStructure, idx::Int64)
259255
get_winch_gear_ratio(sys::SystemStructure, idx::Int64) =
260256
sys.winches[idx].gear_ratio
261257
@register_symbolic get_winch_gear_ratio(

src/generate_system/create_sys.jl

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ function create_sys!(s::SymbolicAWEModel, system::SystemStructure;
171171
pulley_vel(t)[eachindex(pulleys)]
172172
tether_len(t)[eachindex(tethers)]
173173
winch_vel(t)[eachindex(winches)]
174+
winch_acc(t)[eachindex(winches)]
175+
winch_force_vec(t)[1:3, eachindex(winches)]
176+
winch_force(t)[eachindex(winches)]
177+
winch_friction(t)[eachindex(winches)]
174178
end
175179

176180
# ==================== CALL COMPONENT FUNCTIONS ==================== #
@@ -210,9 +214,12 @@ function create_sys!(s::SymbolicAWEModel, system::SystemStructure;
210214
)
211215

212216
# 5. Winch equations (motor dynamics, tether reeling)
213-
eqs, defaults = winch_eqs!(
214-
eqs, defaults, winches, tethers, points, psys;
215-
point_force, set_values, tether_len, winch_vel
217+
eqs, defaults, winch_subsystems = winch_eqs!(
218+
eqs, defaults, winches, tethers, segments, points,
219+
system, psys;
220+
spring_force_vec, set_values, tether_len,
221+
winch_vel, winch_acc, winch_force_vec, winch_force,
222+
winch_friction
216223
)
217224

218225
# 6. Tether equations (stretched length, average force)
@@ -285,7 +292,13 @@ function create_sys!(s::SymbolicAWEModel, system::SystemStructure;
285292
end
286293
end
287294

288-
time = @elapsed @named sys = System(eqs, t)
295+
time = @elapsed begin
296+
if isempty(winch_subsystems)
297+
@named sys = System(eqs, t)
298+
else
299+
@named sys = System(eqs, t; systems = winch_subsystems)
300+
end
301+
end
289302
prn && println("\tCreated System in $time seconds.")
290303

291304
defaults = [

0 commit comments

Comments
 (0)