Problem with mutation and copying of Matrices/Vectors

I have the below MWE of my code. Here, R.I_prev = R.I; is working as copy but I found that it works as mutation in my code, so any change of R.I will change directly R.I_prev. I faced this issue many times. Can any body guide me if there are some rules that I am missing?

Base.@kwdef mutable struct Rs
    I::Vector{Float64} = [2,2,2]
    I_prev::Vector{Float64} = [1,1,1]
end
R = Rs();
function CC(R)
    I_temp = [3;3;3]
    R.I_prev = R.I;
    R.I = I_temp;
end
CC(R);
julia> R
Rs([3.0, 3.0, 3.0], [2.0, 2.0, 2.0])

This syntax does not copy data from one array into the other. Instead, it replaces R.I_prev by R.I.

Broadcasting does what you want: R.I_prev .= R.I.

Note that, if you only want to move data around, you can actually make your struct immutable. This will still allow modifications to the elements of the I and I_prev arrays, while preventing precisely the issue that you’re seeing.

4 Likes

Note that if you are really working with lots of little 3-component vectors (e.g. representing positions in 3d), you might consider using StaticArrays. (With StaticArrays you can use = for copying.)

3 Likes

Thank you!
Actually, I dont really get the difference between replace, broadcast, mutate, copy. Can you support examples about them?

Got it and thank you for your help.

@stevengj
I am using a vector of Rs to access as R[index-1].field[index-2], as as in below (do you recommend another faster way?)

Base.@kwdef mutable struct Rs
    I::Vector{Float64} = [2,2,2]
    I_prev::Vector{Float64} = [1,1,1]
    Dot::Array{Float64, 2} = [0 0 0; 0 0 0; 0 0 0]
end
R = Rs[];
push!(R, Rs()); # the values of fields may change in the code
push!(R, Rs()); # the values of fields may change in the code
#=
do works on R[index-1].field[index-2]
=#

Does using StaticArrays also helps here such as in below?

Base.@kwdef mutable struct Rs
    I::MVector{Float64} = [2,2,2]
    I_prev::MVector{Float64} = [1,1,1]
    Dot::MArray{Float64, 2} = [0 0 0; 0 0 0; 0 0 0]
end

After this, R.I and R.I_prev do not point to the same array, so changes to one should not affect the other.

It is possibly overkill to have a mutable struct with mutable fields. Which parts (if any) of your type do you really need to mutate?

Have you considered making the struct entirely immutable with immutable fields? In some cases that can yield performance and safety improvements.

1 Like

@DNF the format of fields (and struct) are not changing but the values inside the fields are changing as in below:

Base.@kwdef mutable struct Rs
    I::Vector{Float64} = [2,2,2]
    I_prev::Vector{Float64} = [1,1,1]
    Dot::Array{Float64, 2} = [0 0 0; 0 0 0; 0 0 0]
end
R = Rs[];
push!(R, Rs()); # the values of fields may change in the code
push!(R, Rs()); # the values of fields may change in the code
#=
do works on R[index-1].field[index-2]
=#

Do you think I can delete the mutable in front of struct?
Maybe I dont really know the meaning of mutable in struct. For me, it means I can change the values inside the fields, am I right?

I’m confused, you’re not changing (“mutating”) any of the values of the fields of a given Rs struct here, you’re only adding a new Rs struct to the array R. That is, you’re not showing any code that would require an MVector as opposed to an SVector.

1 Like

@stevengj
Here

for example,

#=
R[index-1].field1[index-2] = R[index-1].field2[index-2] + R[index-1].field3[index-2]
=#

It looks to me like you may want mutable fields (i.e. MVector), but you don’t need Rs to be mutable. Just delete mutable, and see how it goes :smiley:

3 Likes

@DNF Mutable structure means its number, names, and values of its fields are changing, correct? So deleting the work mutable before structure means so this means its number, names, and values of its fields are not changing, correct?
Since I only need to change he values of the fields then I can only follow the below suggestion, correct?

What is about the Dot field, MMatrix?

Base.@kwdef mutable struct Rs
    I::MVector{Float64} = [2,2,2]
    I_prev::MVector{Float64} = [1,1,1]
    Dot::MMatrix{Float64, 2} = [0 0 0; 0 0 0; 0 0 0]
end

This is not how you declare an MVector (or an SVector) — the distinguishing fact about staticarrays is that the size of the array is part of the type. This makes them fast for small fixed sizes, but also less flexible since you can’t change the size without changing the type. Also, to declare a literal MVector you can use @MVector:

julia> @MVector [2,2,2]
3-element MVector{3, Int64} with indices SOneTo(3):
 2
 2
 2

Note that this also helpfully shows the type. Similarly for MArray. I would start with something like:

Base.@kwdef struct Rs
    I::SVector{3, Float64} = @SVector [2,2,2]
    I_prev::SVector{3, Float64} = @SVector [1,1,1]
    Dot::SMatrix{3, 3, Float64, 9} = @SMatrix [0 0 0; 0 0 0; 0 0 0]
end

Here, nothing is mutable to start with. You should only make things mutable if you find that there is an operation you need to do that you can’t if it is immutable.

(And you should learn what mutable struct means vs mutable fields ala MVector, since it seems that you might be confused on this point.)

2 Likes

For example, an immutable version of your CC function from above is:

CC(R) = Rs(I = @SVector[3,3,3], I_prev = R.I, Dot = R.Dot)

which constructs a new Rs struct instead of mutating the old one.

(In general, immutable objects are more efficient in Julia; you should typically work with mutable objects only if you need to reference the same object in two or more places, so that changing the object in one place alters it in the other place. In non performance-sensitive contexts, mutable objects can also sometimes be more convenient, since you don’t have to construct a new object just to change a single field.)

2 Likes

Also, once you get even more Julia experience, you would tend to define this as a parameterized type, e.g.

Base.@kwdef struct Rs{T<:Number}
    I::SVector{3, T} = @SVector [2,2,2]
    I_prev::SVector{3, T} = @SVector [1,1,1]
    Dot::SMatrix{3, 3, T, 9} = @SMatrix [0 0 0; 0 0 0; 0 0 0]
end

which defines a whole “family” of types Rs{T} for any numeric type T. That way, you can use the same code for double precision (Float64), single precision (Float32), and even more exotic types like unitful values or dual numbers for automatic differentiation.

But I would recommend learning the basics first by hard-coding Float64; you can always upgrade your code to parameterized types later, and in the meantime they are trickier to work with.

3 Likes

@stevengj
Thank you very much for your explanations. Starting from this version below

and if I need to do some operations such as:

R = Rs[];
push!(R, Rs());
push!(R, Rs());
R[2].I .= R[1].I + R[1].I_prev

Then I need to define the structure from the beginning as below, correct?

Base.@kwdef struct Rs
    I::MVector{3, Float64} = @MVector [2,2,2]
    I_prev::MVector{3, Float64} = @MVector [1,1,1]
    Dot::MMatrix{3, 3, Float64, 9} = @MMatrix [0 0 0; 0 0 0; 0 0 0]
end

No. To change the value of R[2].I, you need a mutable struct, but you don’t need the field to be an MVector. If you just do

R[2].I = R[1].I + R[1].I_prev

then it will work with SVector — it is replacing the value of I, not overwriting the contents of a mutable object. (You don’t need .= with SVector because for SVector making a copy is essentially zero cost, since it’s not heap-allocated.)

However, you could also accomplish this with a non-mutable struct via

R[2] = Rs(I = R[1].I + R[1].I_prev, I_prev = R[2].I_prev, Dot = R[2].Dot)

(In many cases this will be more efficient because non-mutable structs are stored in-line in the array, not as pointers to locations elsewhere on the heap.)

2 Likes

For example, numbers are immutable in Julia. If you have x = 3, that doesn’t mean you can’t change the value of x! But if you do x = 4 then you are making x refer to a new value — you aren’t changing the value of 3! That is:

x = 3
y = x
x = 4
y # still 3!

In contrast, a Vector is mutable. If you have x = [3,4,5], and you do x[1] = 7, then you are changing the contents of the array in-place:

x = [3,4,5]
y = x # not a copy — y "points" to the same object
x[1] = 7
y # contents are now [7,4,5]

The signature of a mutable object is that if you change it, then other references to the object (via name = object) “see” the change.

If R.I is an SVector in a mutable object R, then if you do:

y = R.I
R.I = @SVector [3,4,5]    # only possible if R is a mutable struct
y # hasn't changed!

whereas if R.I is a mutable object like a Vector or an MVector, then even if R is a (non-mutable) struct, you can change the contents of R.I:

y = R.I
R.I .= @SVector [3,4,5] # .= changes the *contents* of R.I
R.I[1] = 2  # you can also change individual elements
y # prints [2,4,5] — it "sees" the change
2 Likes

What do you mean by “number” and “names”? You cannot add new fields to an existing struct or change the name of its fields, independently if the struct is mutable or not.

The keyword mutable just means you can replace the value/object stored in each field after the struct creation. For example, if you create an immutable struct with only Int values, then you cannot change anything, because you cannot change what the fields refer to (can’t change the Int object stored), nor you can change the Int objects themselves (Ints are immutable). However, if you create an immutable struct with an Vector inside, then you cannot replace the Vector object in the field by another Vector object that you have allocated after, but the stored Vector itself can be changed because it is mutable (you can call push!, empty!, or assign new values to specific positions). Both the mutable and its absence (the default immutability) are not recursive within the struct.

2 Likes