Modifying trajectories
Modifying existing trajectories can be useful for a variety of reasons. Sometimes, you may want to change the values of the states, controls, or other components of the trajectory. Other times, you may want to add or remove components from the trajectory.
using NamedTrajectories
Create a random trajectory with 5 time steps, a state variable x
of dimension 3, and a control variable u
of dimension 2
traj = rand(NamedTrajectory, 5)
traj.names
(:x, :u)
Add a new state variable y
to the trajectory. Notice this is in-place.
y_data = rand(4, 5)
add_component!(traj, :y, y_data)
traj.names
(:x, :u, :y)
Remove the state variable y
from the trajectory. This is not in place.
restored_traj = remove_component(traj, :y)
restored_traj.names
(:x, :u)
Adding suffixes
Another common operation is to add or remove a suffix from the components of a trajectory. This can be useful when you want to create a modified version of a trajectory that is related to the original trajectory in some way, or when you want to create a new trajectory that is a combination of two or more existing trajectories.
For now, these tools are used to create a new trajectory.
Add a suffix "_new" to the state variable x
modified_traj = add_suffix(traj, "_modified")
modified_traj.names
(:x_modified, :y_modified, :u_modified)
The modified trajectory contains the same data
modified_traj.x_modified == traj.x
true
Merging trajectories
You can also merge two or more trajectories into a single trajectory. This can be useful when you want to combine data. Mergining trajectories is like taking a direct sum of the underlying data.
Merge the original trajectory with the modified trajectory
merged_traj = merge(traj, modified_traj)
merged_traj.names |> println
(:x, :y, :x_modified, :y_modified, :u, :u_modified)
You can also extract a specific suffix from the components of a trajectory
extracted_traj = get_suffix(merged_traj, "_modified")
extracted_traj.names
(:x_modified, :y_modified, :u_modified)
If you want the original names, you can remove the suffix
original_traj = get_suffix(merged_traj, "_modified", remove=true)
original_traj.names
(:x, :y, :u)
Merging with conflicts
If there are any conflicting symbols, you can specify how to resolve the conflict.
conflicting_traj = rand(NamedTrajectory, 5)
traj.names, conflicting_traj.names
((:x, :u, :y), (:x, :u))
In this case, keep the u
data from the first trajectory and the x
data from the second trajectory
merged_traj = merge(traj, conflicting_traj; merge_names=(u=1, x=2,))
println(merged_traj.u == traj.u, ", ", merged_traj.u == conflicting_traj.u)
println(merged_traj.x == traj.x, ", ", merged_traj.x == conflicting_traj.x)
true, false
false, true
Merged names
merged_traj.names
(:y, :x, :u)
Advanced usage
Sometimes it may be desirable to have direct access to the underlying data matrix/vector associated with the trajectory. In other circumstances it is more useful to employ the built-in per-component and per-knot-point indexing functionality. We detail the relationship between these different methods of access here.
traj = rand(NamedTrajectory, 5)
NamedTrajectory{Float64}([0.9359022897327065 -0.5399244690047508 … -1.1591191467563131 -0.3106585502479884; -1.1840123466914483 -0.16272015853828586 … 0.19913320090544606 -0.6328664451640903; … ; 2.718726554002487 -0.19670450190977112 … -0.03809709296662128 -1.4489900182927609; -0.20957969138866905 0.8048114725668519 … 0.4671984787994504 0.9983815453045227], [0.9359022897327065, -1.1840123466914483, -1.026938739984246, 2.718726554002487, -0.20957969138866905, -0.5399244690047508, -0.16272015853828586, -1.1631593765536652, -0.19670450190977112, 0.8048114725668519 … -1.1591191467563131, 0.19913320090544606, -0.010822335394171611, -0.03809709296662128, 0.4671984787994504, -0.3106585502479884, -0.6328664451640903, 0.7934122569648145, -1.4489900182927609, 0.9983815453045227], 5, 1.0, 5, (x = 3, u = 2, states = 3, controls = 2), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), (x = 1:3, u = 4:5, states = [1, 2, 3], controls = [4, 5]), NamedTuple(), 0, NamedTuple(), NamedTuple(), (:x, :u), (:x,), (:u,))
The "backing store" of a NamedTrajectory
is its datavec
field, a Vector{<:Real}
:
traj.datavec
25-element Vector{Float64}:
0.9359022897327065
-1.1840123466914483
-1.026938739984246
2.718726554002487
-0.20957969138866905
-0.5399244690047508
-0.16272015853828586
-1.1631593765536652
-0.19670450190977112
0.8048114725668519
⋮
0.19913320090544606
-0.010822335394171611
-0.03809709296662128
0.4671984787994504
-0.3106585502479884
-0.6328664451640903
0.7934122569648145
-1.4489900182927609
0.9983815453045227
The data
field holds a reshaped "view" of the "backing store", in a form that is somewhat easier to work with:
traj.data
5×5 reshape(view(::Vector{Float64}, :), 5, 5) with eltype Float64:
0.935902 -0.539924 2.38666 -1.15912 -0.310659
-1.18401 -0.16272 -0.903514 0.199133 -0.632866
-1.02694 -1.16316 -1.51828 -0.0108223 0.793412
2.71873 -0.196705 0.209261 -0.0380971 -1.44899
-0.20958 0.804811 1.24953 0.467198 0.998382
Indexing
The data
matrix is of dimension (traj.dim, traj.T)
, where length(traj.names) == traj.dim
The nth component's indices are given by traj.components[traj.names[n]]
println(traj.names)
println(traj.components)
(:x, :u)
(x = 1:3, u = 4:5, states = [1, 2, 3], controls = [4, 5])
For instance, the indices of a given component at a given knot point are given as follows:
idx = 1 # x
t = 3
slice = traj.datavec[((t - 1) * traj.T) .+ traj.components[traj.names[idx]]]
println(slice == traj[t].x == traj.x[:, t])
true
More generally, the indices of a given component across all knot points are given as follows:
idx = 1 # x
println([((k - 1) * traj.dim) .+ getproperty(traj.components, traj.names[idx]) for k in 1:traj.T])
idx = 2 # u
println([((k - 1) * traj.dim) .+ getproperty(traj.components, traj.names[idx]) for k in 1:traj.T])
UnitRange{Int64}[1:3, 6:8, 11:13, 16:18, 21:23]
UnitRange{Int64}[4:5, 9:10, 14:15, 19:20, 24:25]
Writability
Views and Backing Stores
In Julia, a "view" (SubArray
) is intrinsically linked to some "parent" Array. Any in-place modification of one is reflected by the other.
The following are "safe" operations on a NamedTrajectory (in-place modification of the datavec
):
traj = rand(NamedTrajectory, 5)
traj.datavec
println(traj.datavec)
traj.datavec[1] *= 0.
println(traj.datavec)
traj.datavec[:] = rand(length(traj.datavec))
println(traj.datavec)
[0.628940108246519, -0.031341930879655186, 0.989787415201165, 0.28233427859903576, 1.2383577241696155, 0.6359093422622137, -0.39423444668611574, 0.6992645408921024, 1.2734647684483136, 0.09348261632879054, -0.22037478815348888, -2.3813480111331007, 0.5853657549298499, -0.596035909698749, -1.2364530307539028, -0.5494283119649687, 1.1132919731490338, 1.1762355259297517, -1.4522658829225128, 1.5170257233931586, 1.238292045087252, 0.7922706644886653, 1.4808450387743357, -0.12240178678168617, 0.566298020289145]
[0.0, -0.031341930879655186, 0.989787415201165, 0.28233427859903576, 1.2383577241696155, 0.6359093422622137, -0.39423444668611574, 0.6992645408921024, 1.2734647684483136, 0.09348261632879054, -0.22037478815348888, -2.3813480111331007, 0.5853657549298499, -0.596035909698749, -1.2364530307539028, -0.5494283119649687, 1.1132919731490338, 1.1762355259297517, -1.4522658829225128, 1.5170257233931586, 1.238292045087252, 0.7922706644886653, 1.4808450387743357, -0.12240178678168617, 0.566298020289145]
[0.5209115030400462, 0.22310177426225775, 0.265092478663288, 0.2825444178709914, 0.4874140594511386, 0.565021915908337, 0.9723727195596421, 0.17022125308779923, 0.7479929341508788, 0.4658970829925625, 0.6677174310310272, 0.20459734760536508, 0.23681074964255022, 0.7758405703144919, 0.24806718252176474, 0.8908916349001365, 0.9626496152987761, 0.9974426702807784, 0.58263666438866, 0.6981482885465873, 0.4424657885733654, 0.19688726524012434, 0.4728059600961071, 0.5027184842341622, 0.2773234263207335]
The following is an example of an "unsafe" operation (non-in-place modification of the datavec
):
traj = rand(NamedTrajectory, 5)
println(traj.datavec == traj.data[:])
traj.datavec = rand(length(traj.datavec))
println(traj.datavec == traj.data[:]) # the `data` field now points to a "backing store" that is no longer accessible via `traj.datavec`; this will lead to undefined behavior:
true
false
The following is likewise an "unsafe" operation (non-in-place modification of data
, replacing a "view" of datavec
with a "raw" matrix):
traj = rand(NamedTrajectory, 5)
println(traj.datavec == traj.data[:])
traj.data = rand(size(traj.data)...)
println(traj.datavec == traj.data[:]) # the `data` field no longer points to any "backing store", i.e. is independent of `traj.datavec`; this will lead to undefined behavior:
true
false
In general, reassigning the values of any of the fields of a trajectory may lead to undefined behavior:
fieldnames(NamedTrajectory)
(:data, :datavec, :T, :timestep, :dim, :dims, :bounds, :initial, :final, :goal, :components, :global_data, :global_dim, :global_dims, :global_components, :names, :state_names, :control_names)
TODO:
- Prevent this issue by catching attempts to set sensitive fields in
Base.setproperty!(::NamedTrajectory, ::Symbol, ::Any)
(datavec
anddata
are the primary concern in this regard; however, issuing a warning of some kind may be appropriate).- Particularly because it is confusing that
traj.datavec = zeros(length(datavec))
is "discouraged", whiletraj.x = zeros(traj.dims.x, traj.T)
andtraj[1].x = zeros(traj.dims.x)
are both valid.
- Particularly because it is confusing that
Components and Knot Points
traj = rand(NamedTrajectory, 5)
NamedTrajectory{Float64}([-0.6631827125662183 0.10855255236960826 … 0.07970403386380705 0.23990738030325373; -1.070371103449843 1.2847629645406133 … -0.3921891713796184 -0.39002222653457597; … ; -0.38453893031311187 -0.833648874771814 … 0.4838118988709584 -1.254549797135627; 0.695061988412953 -1.0324272799635863 … -0.2086995965924075 1.3601284153330822], [-0.6631827125662183, -1.070371103449843, -0.2352158270540751, -0.38453893031311187, 0.695061988412953, 0.10855255236960826, 1.2847629645406133, -2.0100491743004434, -0.833648874771814, -1.0324272799635863 … 0.07970403386380705, -0.3921891713796184, 0.2248313501938784, 0.4838118988709584, -0.2086995965924075, 0.23990738030325373, -0.39002222653457597, 1.068043229310047, -1.254549797135627, 1.3601284153330822], 5, 1.0, 5, (x = 3, u = 2, states = 3, controls = 2), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), (x = 1:3, u = 4:5, states = [1, 2, 3], controls = [4, 5]), NamedTuple(), 0, NamedTuple(), NamedTuple(), (:x, :u), (:x,), (:u,))
Trajectory components are accessible (as a "view") via getproperty
:
traj.x
3×5 view(reshape(view(::Vector{Float64}, :), 5, 5), 1:3, :) with eltype Float64:
-0.663183 0.108553 3.70222 0.079704 0.239907
-1.07037 1.28476 -0.821651 -0.392189 -0.390022
-0.235216 -2.01005 -1.91799 0.224831 1.06804
Components are also writable via setproperty!
:
traj.x = rand(traj.dims.x, traj.T)
traj.x
3×5 view(reshape(view(::Vector{Float64}, :), 5, 5), 1:3, :) with eltype Float64:
0.17964 0.354886 0.254342 0.12451 0.936795
0.737976 0.302626 0.620118 0.615342 0.642698
0.202852 0.84534 0.867612 0.300094 0.549747
or may be modified directly:
traj.x[1] *= 0.
traj.x
3×5 view(reshape(view(::Vector{Float64}, :), 5, 5), 1:3, :) with eltype Float64:
0.0 0.354886 0.254342 0.12451 0.936795
0.737976 0.302626 0.620118 0.615342 0.642698
0.202852 0.84534 0.867612 0.300094 0.549747
Knot points are likewise accessible via getindex
:
traj[1]
KnotPoint(1, [0.0, 0.7379758994365869, 0.2028516366562857, -0.38453893031311187, 0.695061988412953], 1.0, (x = 1:3, u = 4:5, states = [1, 2, 3], controls = [4, 5]), (:x, :u), (:u,))
A KnotPoint
behaves much like a NamedTrajectory
, with respect to getting, setting, and/or modifying its components:
traj[1].u
2-element view(reshape(view(::Vector{Float64}, :), 5, 5), 4:5, 1) with eltype Float64:
-0.38453893031311187
0.695061988412953
traj[1].u = rand(traj.dims.u)
traj[1].u
2-element view(reshape(view(::Vector{Float64}, :), 5, 5), 4:5, 1) with eltype Float64:
0.09808692777592143
0.42002377759669807
traj[1].u[1] *= 0
traj[1].u
2-element view(reshape(view(::Vector{Float64}, :), 5, 5), 4:5, 1) with eltype Float64:
0.0
0.42002377759669807
The parent trajectory will reflect any modifications made in this fashion:
traj.datavec
25-element Vector{Float64}:
0.0
0.7379758994365869
0.2028516366562857
0.0
0.42002377759669807
0.3548861838459252
0.3026257631646354
0.8453395502268236
-0.833648874771814
-1.0324272799635863
⋮
0.6153416592198294
0.3000937226746553
0.4838118988709584
-0.2086995965924075
0.936795286010289
0.6426983907069637
0.5497472118535665
-1.254549797135627
1.3601284153330822
This page was generated using Literate.jl.