Skip to content
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

Implementing domains as an interface #141

Merged
merged 13 commits into from
Oct 9, 2023
Merged

Implementing domains as an interface #141

merged 13 commits into from
Oct 9, 2023

Conversation

daanhb
Copy link
Member

@daanhb daanhb commented Aug 28, 2023

I think this is a candidate for becoming 0.7.

It mostly implements the idea of a domain as an interface (#14, #120) and addresses some related issues ( JuliaMath/IntervalSets.jl#115, JuliaMath/IntervalSets.jl#136). The main goal is to improve interoperability with other packages.

Among other things this PR includes a copy of the contents of a newly proposed DomainSetsCore.jl package, with the exception of the definition of Domain{T} which remains in IntervalSets.jl for now. See core.jl for the code and the README of DomainSetsCore for a formal definition of the "domain interface". If this works well, we can still register DomainSetsCore and move the definitions there.

Most importantly this PR implements all operations of DomainSets for any type, whether it inherits from Domain or not, except for functions that have a standard name outside of DomainSets. In that case, those functions are only implemented for variables of type Domain or variables which are passed "as a domain" (using the AsDomain(d)) syntax. A prominent example is eltype. An alternative function which is "owned" by DomainSets is domaineltype.

As an example of interoperability, see here for a package extension to make domains co-exist with types of GeometryBasics.jl. No changes to either packages are required, hence it could be an extension in either of the two packages. I've illustrated here what it takes for intervals of IntervalSets.jl and Intervals.jl to interact with each other.

An example of the usefulness is that in a package like DomainIntegrals.jl one can specify the integral domain as any domain (an interval from IntervalSets.jl, or from Intervals.jl, or a rectangle from GeometryBasics.jl) and it "just works". (Up to possible bugs and missing functionality for now, of course....)

@daanhb
Copy link
Member Author

daanhb commented Aug 28, 2023

I think these changes are almost completely backwards compatible with the 0.6 version of DomainSets, because functionality has mostly been added. However, the internal change is quite substantial, and I'm pretty sure that I've changed the default behaviour of == and issubset for domains, so a version upgrade is called for.

@daanhb
Copy link
Member Author

daanhb commented Aug 28, 2023

Also, a note on eltype. Both IntervalSets.jl and Intervals.jl implement eltype because they see intervals as sets. However, both can have an Int eltype when the interval has integer endpoints. This creates funny situations in computations. The domains in GeometryBasics.jl and in Meshes.jl do not have an eltype (their eltype is Any), but they all implicitly work with vectors of fixed length DIM and element type T. Finally, the sets in LazySets.jl have an eltype of Float64 even when they describe sets of vectors of floats.

The only solution is to create a new function, which I've called domaineltype. It has a sensible definition for all domains in the packages mentioned above. For now, internally this PR still uses eltype for variables of type Domain{T} and that eltype is T. It uses domaineltype (abbreviated to deltype internally) for variables which represent domains but whose type may be something else.

@codecov
Copy link

codecov bot commented Aug 28, 2023

Codecov Report

Patch coverage: 79.90% and project coverage change: -4.81% ⚠️

Comparison is base (f9638be) 85.70% compared to head (1162e66) 80.90%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #141      +/-   ##
==========================================
- Coverage   85.70%   80.90%   -4.81%     
==========================================
  Files          34       35       +1     
  Lines        2659     2697      +38     
==========================================
- Hits         2279     2182      -97     
- Misses        380      515     +135     
Files Changed Coverage Δ
src/DomainSets.jl 100.00% <ø> (ø)
src/domains/cube.jl 96.50% <ø> (ø)
src/domains/interval.jl 57.32% <41.66%> (-31.50%) ⬇️
src/generic/core.jl 58.33% <58.33%> (ø)
src/domains/trivial.jl 90.54% <62.50%> (-3.83%) ⬇️
src/domains/indicator.jl 86.20% <66.66%> (+3.44%) ⬆️
src/generic/canonical.jl 71.42% <66.66%> (-1.91%) ⬇️
src/generic/domain.jl 86.20% <80.00%> (-10.23%) ⬇️
src/util/common.jl 93.40% <80.00%> (-2.05%) ⬇️
src/generic/setoperations.jl 88.05% <83.33%> (-4.17%) ⬇️
... and 11 more

... and 1 file with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@dlfivefifty
Copy link
Member

Probably the behaviour that makes most logical sense is eltype(0..1) == Real. But then there could be boundaryeltype(0..1) == Int. (Probably needs a more general name…)

then wherever is calling eltype now could call float(boundaryeltype(d))

@daanhb
Copy link
Member Author

daanhb commented Aug 30, 2023

In the current design I'd say that float(boundaryeltype(d)) is a good candidate implementation for eltype or domaineltype of intervals with numeric types.

Yes, eltype being Real makes conceptual sense, but it does not convey any useful information. For example, if rand for an interval returns a point in the interval, which will be the type of that point? That is what T is for <:Domain{T} and what domaineltype is for other objects satisfying the domain interface.

@daanhb
Copy link
Member Author

daanhb commented Sep 5, 2023

Okay to merge this?
It only adds behaviour, which we can test in more cases before making other changes (like moving the definition of Domain).

unionbox1(d1, d2) = unionbox2(d1, d2)
unionbox2(d1, d2) = fullspace(d1)
unionbox1(d1::EmptySpace, d2) = d2
unionbox1(d1::FullSpace, d2) = d1
unionbox2(d1, d2::EmptySpace) = d1
unionbox2(d1, d2::FullSpace) = d2

unionbox(d1::D, d2::D) where {D<:FixedInterval} = d1
unionbox1(d1::D, d2::D) where {D<:FixedInterval} = d1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's unionbox1 and below intersectbox1?

Copy link
Member Author

@daanhb daanhb Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unionbox(d1,d2) is a function of two arguments, which sometimes simplifies for certain d1 (independently of d2 or for some possibilities of d2). Similarly, it may simplify for certain d2. The call chain goes:
unionbox(d1,d2) -> unionbox1(d1,d2) -> unionbox2(d1,d2) -> some_default_algorithm

The idea is that unionbox1 can dispatch safely (without ambiguity) solely on d1, and unionbox2 can dispatch on d2. I've used this pattern in many places and it works rather well. If a combination of d1 and d2 is specific enough, then one can dispatch on it using just the unionbox function.

@@ -244,8 +248,8 @@ function similar_interval(d::HalfLine{T,C}, a::S, b::S) where {T,S,C}
HalfLine{promote_type(float(T),S),C}()
end

point_in_domain(d::NonnegativeRealLine) = zero(eltype(d))
point_in_domain(d::PositiveRealLine) = one(eltype(d))
point_in_domain(d::NonnegativeRealLine) = zero(deltype(d))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this name is clearer, but this is called a "choice function" in the Axiom of Choice so we could just call this choice(d)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it would be good if choice(s) was defined for a Set too, for example, but it isn't. This function really is just for testing purposes, but it could have any name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just add choice(d::AbstractSet) = first(d)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iterate(d)[1] is kind of the same...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the point_in_domain function roughly speaking picks some point in the interior of a domain, if possible. It may be good to think about how to "sample" points from a domain more generally (like rand), but that is a separate issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm renaming to choice and adding a deprecation for point_in_domain

src/generic/domain.jl Outdated Show resolved Hide resolved
@ChrisRackauckas
Copy link

What was breaking in this release?

@daanhb
Copy link
Member Author

daanhb commented Feb 16, 2024

Syntactically, normally nothing. A lot of the internals changed though, which mainly results in more functionality (as opposed to functionality changing) for types that do not inherit from the main Domain type.

There were some nuances in the decision whether two domains are equal which may have changed and which prompted a new series, out of caution. I would expect 0.7 to work as is for most people who had been using 0.6.x (and I'm assuming you're asking because you didn't notice a difference).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants