-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend quadrature type to include a storage type parameter #991
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #991 +/- ##
==========================================
+ Coverage 93.18% 93.20% +0.01%
==========================================
Files 38 38
Lines 5766 5766
==========================================
+ Hits 5373 5374 +1
+ Misses 393 392 -1 ☔ View full report in Codecov by Sentry. |
src/Quadrature/quadrature.jl
Outdated
function QuadratureRule{shape, T}(weights::Vector{T}, points::Vector{Vec{dim, T}}) where {dim, shape <: AbstractRefShape{dim}, T} | ||
# struct QuadratureRule{shape, T, rdim, WeightStorageType <: AbstractVector{T}, PointStorageType <: AbstractVector{<:Vec{rdim,T}}} | ||
struct QuadratureRule{shape, T, rdim, WeightStorageType <: AbstractVector{T}, PointStorageType <: AbstractVector} # The one above crashes the deprecation in deprecations.jl for some reason... | ||
weights::WeightStorageType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do they need to be able to have different storage type and different array type? Couldn't the change here only allow for swapping out the Vector
part while keeping the rest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fredrik just asked me the same :D I do not know how, because the vectors store different things. I may be able to store some compressed variant of the Vec (as a AbstractVector{T} with num weights * dim entries), but not sure if it is worth the effort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative is to have an AbstractQuadratureRule
that needs to support some interface and then if you need really exotic ones you can always BYOQR (Bring Your Own Quadrature Rule). And then the FEValues would just take a QR <: AbstractQuadratureRule
.
Just to see what people think, is removing the |
I am really split on this one. On the one hand I find it annoying to move around with the rule, so I cannot plug my QuadratureRule in everywhere. On the other hand I am also happy to have this safety net when I accidentally plug a quadrature rule into a place where it should not be such that I get an early error message. |
How should the constructor get information about the quadrature points then? I don't really see the problem with having the refshape, but with this change, I think using struct QuadratureRule{RefShape, WT, PT}
weights::WT
points::PT
function QuadratureRule{RefShape}(weights::AbstractVector{T}, points::AbstractVector{Vec{rdim, T}}) where {rdim, RefShape <: AbstractRefShape{rdim}, T}
if length(weights) != length(points)
throw(ArgumentError("number of weights and number of points do not match"))
end
new{RefShape, typeof(weights), typeof(points)}(weights, points)
end
end should suffice. The inner constructor ensures that Edit: Removed T from " |
We can leave the constructor as is. |
We could, but as interface it isn't so nice that |
And with what I wrote above, we should adopt the same API for giving the number type as for FEValues, e.g., function QuadratureRule{shape}(::Type{T}, order::Int) where {shape <: AbstractRefShape, T <: Number}
quad_type = (shape === RefPrism || shape === RefPyramid) ? (:polyquad) : (:legendre)
return QuadratureRule{shape}(T, quad_type, order)
end |
There would be a separation between the convenience function to create different quadrature rules (from the tables) and the type constructor itself (that would just take points and weights). So you would have something like |
After thinking about this today, my answer would be yes, I think that is worth spending a type parameter on. I'm almost 100 % sure that without this parameter, bugs will occur in user codes (including my own) and silently give wrong results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be my suggestion for how to do this
src/Quadrature/quadrature.jl
Outdated
struct FacetQuadratureRule{shape, T, dim, QRType <: QuadratureRule{shape, T, dim}, FacetRulesType <: Union{NTuple{<:Any, QRType}, AbstractVector{QRType}}} | ||
face_rules::FacetRulesType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the Array
-based QuadratureRule
this only contains references to two arrays, so I think we can simplify to always use a homogeneous tuple. nfaces(RefShape)
should give a Const return value if needed to get N
.
struct FacetQuadratureRule{shape, T, dim, QRType <: QuadratureRule{shape, T, dim}, FacetRulesType <: Union{NTuple{<:Any, QRType}, AbstractVector{QRType}}} | |
face_rules::FacetRulesType | |
struct FacetQuadratureRule{RefShape, QR<:QuadratureRule{RefShape}, N} | |
face_rules::NTuple{N, QR} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If everyone is okay with this then we can also follow this idea. I mean even tesseract only has 8 facets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well allow any container type then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But does everything in Ferrite work if we use a non-indexable container? I mean, the array interface is the supertype for all indexable containers, right?
src/Quadrature/quadrature.jl
Outdated
points::Vector{Vec{dim,T}} | ||
function QuadratureRule{shape, T}(weights::Vector{T}, points::Vector{Vec{dim, T}}) where {dim, shape <: AbstractRefShape{dim}, T} | ||
# struct QuadratureRule{shape, T, rdim, WeightStorageType <: AbstractVector{T}, PointStorageType <: AbstractVector{<:Vec{rdim,T}}} | ||
struct QuadratureRule{shape, T, rdim, WeightStorageType <: AbstractVector{T}, PointStorageType <: AbstractVector} # The one above crashes the deprecation in deprecations.jl for some reason... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct QuadratureRule{RefShape, WT, PT}
weights::WT
points::PT
function QuadratureRule{RefShape}(weights::AbstractVector{T}, points::AbstractVector{Vec{rdim, T}}) where {rdim, RefShape <: AbstractRefShape{rdim}, T}
if length(weights) != length(points)
throw(ArgumentError("number of weights and number of points do not match"))
end
new{RefShape, typeof(weights), typeof(points)}(weights, points)
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And change all (L60-L155)
- function QuadratureRule{ConcreteRefShape, T}(quad_type::Symbol, order::Int) where T
+ function QuadratureRule{ConcreteRefShape}(::Type{T}, quad_type::Symbol, order::Int) where {T<:Number}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if I understand your suggestion correctly, then you would remove all type constraints in the type parametrization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but these should be checked in the inner constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I left one in FacetQuadratureRule
but that was just since it makes it more readable IMO, but that could also just be checked in the inner constructor of course)
src/Quadrature/quadrature.jl
Outdated
function QuadratureRule{shape}(weights::AbstractVector{T}, points::AbstractVector{Vec{rdim, T}}) where {rdim, shape <: AbstractRefShape{rdim}, T} | ||
QuadratureRule{shape, T}(weights, points) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- QuadratureRule{shape, T}(weights, points)
+ QuadratureRule{shape}(weights, points)
I think there should be some consideration on code simplicity here. Personally I think trying to have every functionality in one |
I agree, we should think carefully about this as FEValues are already quite intricately typed, and as @KristofferC mentions above, the interface is quite minimial for BYOQR. I guess the point is, that a GPU-compatible implementation allowing compatible arrays would also support the current functionality. So then it feels like we are duplicating the code, but perhaps that is worth it? If we go for separate Furthermore, I believe that the change to have the constructor as |
I agree on the argument that we should keep the code simple and this is exactly the reason why I propose the change. I think it is simpler to have the storage parametrized than maintaining additional >300 line of duplicated code for each possible storage type. I hope that this blog post here can explain the DRY principle better than I can https://scalastic.io/en/solid-dry-kiss/ . Can we discuss the audit of the CellValues separately?
If there is no disadvantage associated to this change, why not? Should we still provide a constructor which takes an abstract vector tho?
Since I have been repeatedly asked to make only small changes and one by one: Can we discuss this issue separately? This PR is really just about the storage type and not the quadrature custruction interface. |
That comment was simply regarding the cognitive load for users, not directly asking for changes to CellValues.
Not sure, but this is (in practice) always internal right, so not required to provide such a convenience interface?
If we change the type-parameterization from Yet, I find the discussion of how it should look relevant here, since we are discussing how to have |
Perhaps I'm missing something, but a custom QuadratureRule should be quite small? struct MyQR{RefShape, WT, PT} <: AbstractQuadratureRule{RefShape}
weights::WT
points::PT
function MyQR{RefShape}(weights::AbstractVector{T}, points::AbstractVector{<:Vec{rdim, T}) where {RefShape, T, rdim}
@assert length(weights) == length(points)
@assert rdim == getrefdim(RefShape)
return new{RefShape, typeof(weights), typeof(points)}(weights, points)
end
end
function MyQR{RefShape}(::Type{StorageType}, args...) where {RefShape, StorageType<:AbstractVector}
weights, points = get_weights_and_points(RefShape, args...)
# convert to StorageType
return MyQR{RefShape}(converted_weights, converted_points)
end
getweights(qr::MyQR) = qr.weights
getpoints(qr::MyQR) = qr.points where (But of course, this implementation would also cover the current usage, so I agree that it is code duplication. I guess the "difficulty" with this general implementation is the less clear parameterization |
Sorry, I cannot follow what exact your argument Knut and Elias. So let me try to summarize what I understand here: Instead of defining a single type with parametrized storage constrained to collections, we should have an interface, that tells we should define one type per collection type, which weights and points of concrete collections each and change the bulk rename QuadratureRule to AbstractQuadratureRule in most parts of the codebase? Also
That is correct for your proposal. But please note that for the current state of the PR |
Just skimming through some of the principles outlined there, it seems like the first three principles (srp, ocp, isp), could be used as arguments against this PR :P I think maintaining a monolithic QuadratureRule is much more difficult that maintaining a But I dont want to block this PR, I just wanted to offer my view, so if you think it will simplify stuff later on, go for it :) . |
I agree that we can argue for both options being easier or harder, depending on the point of view.
Yes, which would also allow users to "hack" in using their own quadrature rule (e.g. if I want a quadrature rule with tuple elements, or if you want to skip the weights (as we do for e.g. point values). If this would make things easier to understand: I'm not sure.
The interface wouldn't have to say that, just the
Yes, but since noone objected to that proposal, I assume that is one possible solution, and then I think we should discuss the implications of that here?
An option could perhaps be just adding the comments struct QuadratureRule{RefShape, WT, PT}
weights::WT # E.g. Vector{Float64}
points::PT # E.g. Vector{Vec{2, Float64}}
function QuadratureRule{RefShape}(weights::AbstractVector{T}, points::AbstractVector{Vec{rdim, T}})
... ? I'm open for both solutions, but I do think that in the current state, the parameterization is too complex (hence my review suggestions). |
I like the definition from Knuts last comment. It is simple but flexible. We can of course also have AbstractQUadrature if the type definition there does not suffice in some case. |
No. Why? The QuadratureRule is responsible for managing the access to quadrature points and the weights, hence all proposals adhere to the single responsibility principle. The proposal for the adding a supertype and making the storage parameter in this PR also follow the open-closed principle, whereas the QuadratureRule on master does not, as we cannot extend it (i.e. "not open"). The proposal to leave the type parameters unconstrained violates the Liskov substituion principle, as we can use storage types which might not be ordered and hence the relation between the weights and points might be messed up. But for this one I am not sure if this will really be an issue in practice, as I think users who use such advanced data structures should be aware of this fact?
So, if I understand correctly, then your point is essentially that I should add more documentation and devdocs?
But if you do not have weights, then it would not be a quadrature rule in first place, right? (But something like QuadraturePointVector or similar).
But what would define the interface for an
If no one else is concerned by the hypothetical correctness issue pointed out above, then I can also do this (as I also do not think that this will be really a practical issue). |
Just |
One things I want to say (which is independent of the parameterization) is that I think the current way of creating quadrature rules is not ideal. For example we write:
but there are many quadrature rules and it isn't clear which one this creates etc. This was changed by me in #58 and 8 years later I am suggesting we change it back to standard functions like:
etc. |
Resolves #989