Basic struct usage questions

I have defined a type called CylindricalStress for bundling the results of Lamé’s Equations which I understand is good practice:

struct CylindricalStress
    r::Number # Radial Stress
    θ::Number # Tangential (Hoop) Stress
    z::Number # Longitudinal Stress
end

"""
    lame(r, pᵢ, pₒ=0)

Compute theoretical stress distribution from Lamé's thick wall cylinder equations.

# Arguments
- `r `: vector of points to compute stress over; must span from inside boundary to outside boundary
- `pᵢ`: pressure on inside boundary
- `pₒ`: pressure on outside boundary (pₒ=0 if argument is omitted)
"""
function lame(r::AbstractVector, pᵢ, pₒ = 0)
    (rᵢ, rₒ) = extrema(r)

    term1 = (rᵢ^2 * pᵢ - rₒ^2 * pₒ) / (rₒ^2 - rᵢ^2)
    term2 = rᵢ^2 * rₒ^2 * (pᵢ - pₒ) / (rₒ^2 - rᵢ^2) ./ r.^2

    σr = term1 .- term2
    σθ = term1 .+ term2
    σz = fill(term1, length(r))
    σ = CylindricalStress.(σr, σθ, σz)
    return σ
end
julia> σ = lame([5,10,15], 100)
3-element Vector{CylindricalStress}:
 CylindricalStress(-100.0, 125.0, 12.5)
 CylindricalStress(-15.625, 40.625, 12.5)
 CylindricalStress(0.0, 25.0, 12.5)

1. How can I define my type as a subtype of something else so I don’t have to extend every function manually?
I just want this to act like any three-component vector for sum(σ), mean(σ), 10.*σ, norm.(σ), etc.

2. What types should I enforce in the struct and in the function?
I have heard you need AbstractVector in order to accept a slice, and that you can’t restrict to Number if you want to accept Uniful.jl types. I would like to give the user some indication of what to input, especially scalar vs vector. (I debugged some errors already related to this.) How can I provide wide compatibility, but also helpful documentation and error checking on input/output types?

3. How can I keep my type as a “private implementation detail”?
I have heard the stance on this forum that custom types should not be publicly available. Why is that and how would that be implemented in this case?

3 Likes

these are abstract types, bad practices.

There’s no good solution to 1, as far as I can tell, this isn’t really a vector, the tree components are different components, similar to Point(x,y) is not (logically) similar to a vector with 2 elements.

2 Likes

Maybe FieldVEctor from StaticArrays:

julia> using StaticArrays

julia> struct CylindricalStress{T} <: FieldVector{3,T}
           r::T # Radial Stress
           θ::T # Tangential (Hoop) Stress
           z::T # Longitudinal Stress
       end

julia> x = rand(CylindricalStress{Float64})
3-element CylindricalStress{Float64} with indices SOneTo(3):
 0.4695879908230596
 0.5473285905891696
 0.1754218840752042

julia> using LinearAlgebra

julia> norm(x)
0.7421955972747745

julia> v = rand(CylindricalStress{Float64},4)
4-element Vector{CylindricalStress{Float64}}:
 [0.8180545584117769, 0.10041135767552167, 0.38783069260937464]
 [0.6358575553230066, 0.8592006273725905, 0.7945119110861931]
 [0.8944549081297775, 0.12048601852665253, 0.8480448102782157]
 [0.4306380623978796, 0.17528782969751577, 0.821600492706112]

julia> sum(v)
3-element CylindricalStress{Float64} with indices SOneTo(3):
 2.7790050842624403
 1.2553858332722805
 2.8519879066798954


(although, as pointed above, some of the operations may not make sense - the norm being one of them. Others, like the sum, maybe?)

You could start from that and overload some functions that you need with specific implementations when needed.

3 Likes

I think this is relevant:
https://docs.julialang.org/en/v1/manual/interfaces/index.html#man-interface-array

in general:
https://docs.julialang.org/en/v1/manual/interfaces/index.html

1 Like

I’m not sure what you’re referring to here. Can you give an example?

There are no “private” types in Julia, and there’s no way to hide a struct definition. I’d say don’t worry about trying to hide things unless you have some really compelling special reason.

1 Like

I think this is not a particular concern in general. Just not export the types, if the user doesn’t have to access them directly.

AbstractVector or AbstractArray (depending on whether you actually want precisely a 1-dimensional array or you just want any array of any number of dimensions) is usually a good choice. Restricting your number-like scalar types to a subtype of Number is probably a good compromise between flexibility and providing an explicit signal to your users about what kind of input is expected.

Also, I think you’ve been misinformed about Number and Unitful:

julia> supertypes(typeof(1u"m"))
(Quantity{Int64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, Unitful.AbstractQuantity{Int64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, Number, Any)


julia> 1u"m" isa Number
true

A Unitful.Quantity is a subtype of Number, so you can use ::Number for type annotations even if you want to accept unitful quantities.

3 Likes

Reference for question #3:

Reference for question #2:

Fields being private unless indicated otherwise is just a convention, not something enforced by the language.

Cf the custom of not entering someone else’s home without being invited in, even if the door is unlocked.

Perhaps you can help us understand the implementation more. To calculate the norm of your CylidricalStress, is it just the euclidian norm of the elements?

Or are there other calculations involved, given one is the radial stress, one is the longitudinal stress?

Sorry, I don’t mean “How do I enforce privacy?”.
I am wondering how you can get away with not explaining your types to the user? Define every possible function they could want to use it with in the package, and make sure those functions return only Base types?

I think the key thing is whether or not it is iterable. If you define iteration for your type, (which is easy), then std, var, sum, etc. all intuitively make sense to the user, and don’t need to be overloaded or documented. Since its the same behavior as the docstring for sum(::Any) which exists in base.

Yes, the standard Euclidian norm equals the von Mises equivalent stress since these are principal stresses. +, -, *, mean, etc. will be used mostly for comparisons between datasets. I am going to be comparing the theoretical value above to FEA data. All those functions should act component-wise like a vector which is why I don’t want to bother defining all my own methods. However, I like the convenience of naming the components in my own struct.

Yep, that’s misinformation.

There is a point to not needlessly restricting types, but Number is a fairly high level, not much of a restriction.

1 Like

I am not sure what “explaining your types” means in this context.

Generally, users should understand the API that your types conform to; if that is clear then they have no need to understand type internals. The API can be a polished interface like iteration, or something that you just define for this package.

Then FieldVector from StaticArrays is probably the easiest path.

2 Likes

I 2nd this.

You could implement the vector interface yourself, but if StaticArrays.jl does it for you, use that.

You can still overload other, more specialized, functions for your type. And document those. Seems pretty convenient.

1 Like

Is using the type annotations ::Number and ::AbstractArray the best way to enforce scalars vs. collections respectively then?

I think you want parameteric types so that the struct members are not abstract.

3 Likes