Best way to create a struct with a variable data contents

Hello everyone,

I’m trying to create a mini materials database. In this database, I would have materials which can have a varying number of properties: optical, electrical, thermal …
Each material can have only one of those, some of those or all of those.

I thought to create a type Material in which the data is kept in a dict:

struct Material{K,V <: Description}
    data :: Dict{K,V}
end

where depending on the property I can have a description that can change (using a model or data)

abstract type Description end
struct Model <: Description end
struct SomeData <: Description end

I would then describe silicon in the following way

Si = Material(  Dict(:optical    => Model(), 
                     :electrical => SomeData()) )

Now I can compute some properties:

permitivitty(material :: Material{K,V}) where {K,V<:Description} = permitivitty(material.data[:optical])
permitivitty(::Model) = "model"
permitivitty(::SomeData) = "some data"

This works as expected

julia> permitivitty(Si)
"model"

This approach works, but I don’t like too much how to declare the materials.

I have three questions:

  1. I’m wondering if there is a better way to describe the data in the Material struct.
  2. Is there a less verbose way to declare the material (maybe linked to the first question)?
  3. how can I produce a message “no optical properties defined” when I’m computing the permittivity of a material for which there is no :optical key?

Many thanks in advance,
Olivier

Concerning question three:
There is a function haskey(key) which checks if a Dict contains a key-value pair with
given key.

1 Like

If you don’t need to add properties after instantiation, you could wrap a NamedTuple instead of a Dict, i.e.

struct Material{K,V}
       data::NamedTuple{K,V}
end
Material(; kwargs...) = Material(kwargs.data)
Si = Material(optical = Model(), electrical = SomeData())
function permitivitty(material :: Material{K,V}) where {K,V}
    if :optical in K
         permitivitty(material.data.optical)
    else
         @warn "$material has no propery optical"
    end
end
1 Like

This is indeed much nicer.
Thanks again

Hi, just thought about the following solution:

# Union for descriptions: either a description, or nothing
DescriptionUnion = Union{Description, Nothing}

struct Material
    optical::DescriptionUnion
    electrical::DescriptionUnion
end

# keyword-arguments constructor (could be generated by a macro for convenience...)
Material(;optical=nothing, electrical=nothing) = Material(optical, electrical)

# possible usage
Material(nothing, SomeData()) # default constructor
Material() # all fields are "nothing"
Material(optical=Model()) # set some of the data fields

Then you’d define functions on material properties as:

permitivitty(material :: Material) = permitivitty(material.optical)
permitivitty(::Model)    = "model"
permitivitty(::SomeData) = "some data"
permitivitty(::Nothing)  = @warn "optical properties undefined"

I’m also relatively new to Julia, so I’ve no idea whether this is a good solution or not…

1 Like

IMO @asprionj’s solution is a lot better, since it comes with type safety… if you use a dictionary or named tuple, a typo in one of your properties will result in a bug (as if the material is missing that property), vs refusing to create the material in the first place. (Of course, you could detect that even with a dictionary by adding verification code in the constructor, but that seems a lot messier.)

If you think this won’t be an issue in practice, consider that you already have a typo in the word “permitivitty” in your code :slight_smile:

1 Like

The compiler may prefer a parametric type

struct Material{To,Te}
    optical::To
    electrical::Te
end

Look e.g. at m = Material(optical=Model()); @code_warntype permitivitty(m).
If there are only two properties, I would also go with this approach. But I understood the OP that there can be many more properties, and then I see a trade-off between convenience (the NamedTuple-approach) and typo-safety (the explicit parametric struct approach). In terms of performance they should be equivalent.

1 Like

Indeed, after benchmarking I see that
method 1 : @jbrea 's first solution gives

@btime permitivitty($Si)
  1.688 ns (0 allocations: 0 bytes)

method 2 : @asprionj 's solution gives

  @btime permitivitty($Si2)
  4.203 ns (0 allocations: 0 bytes)

method 3: parametric type instead of union gives again

@btime permitivitty($Si)
  1.688 ns (0 allocations: 0 bytes)

But I understood the OP that there can be many more properties, and then I see a trade-off between convenience (the NamedTuple -approach) and typo-safety

I agree 200%
In my case I don’t have too many properties, but if for some reason I have a material for which I want to add other properties, the NamedTuple allows me to keep the types as they are.

Thanks to all of you for looking into this.
I learned a lot again.