# 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
θ::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}
θ::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

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