I’m working on my first major Julia project which is a physics simulation package. The overall goal is to develop this package to be as neat, efficient, and extensible as possible using good Julian code conventions so people can easily add to it in the future.
The user will be able to specify the calculation parameters and physical system in an input file, then ask for various properties to be calculated. As these properties are expensive to compute, I’m aiming for the package to calculate them in an intelligent way.
As an analogy, if the physical system was a circle then we should calculate and store the area only if this field is accessed, not when the object is created and not every time the field is accessed. I have come up with one way to do this by overloading Base.getproperty
(see MWE below).
A further complication (that may need a separate post later) is that several properties (a, b, c) can be calculated from the same mathematical object M. Getting M is the expensive part of the calculation so I’d like to set things up such that when the user asks for a or b or c, the package calculates all three.
The current data structure looks like the following where all capitalised fields are structs with their own fields:
Output # mutable, highest level struct
Calculation # immutable, defined from input file
Parameters
Options
System
Parameter1 # calculated when requested with output.parameter1 or similar
anintegervalue
SubParam1 # calculated when SubParam1 or SubParam2 is requested
SubParam2 # calculated when SubParam1 or SubParam2 is requested
...
My questions are:
- What is an appropriate layout for the various structs here? Putting everything in one big nested object seems like a clunky way to operate but I haven’t yet worked out anything better.
- Should the user get data with
circle.area
(as in the MWE) orarea(circle)
? The latter seems more in line with what I’ve seen of Julia so far. - Is there a better way to calculate properties on demand without repeating the same calculations twice?
MWE:
mutable struct Circle
radius::Number
area::Union{Number, Missing}
Circle(radius::Number) = new(radius, missing)
end
""" Extends Base.getproperty such that missing data is filled in when accessed """
function Base.getproperty(object::Circle, field::Symbol)
if ismissing(getfield(object, field))
setfield!(object, field, getfieldondemand(object, field))
end
return getfield(object, field)
end
function getfieldondemand(C::Circle, field::Symbol)
if field === :area
return areaofcircle(C)
end
end
function areaofcircle(C::Circle)
return pi * C.radius^2 # some long and complicated calculation
end
circle = Circle(1.0)
@info("circle.area = $(getfield(circle, :area))")
circle.area
@info("circle.area = $(getfield(circle, :area))")