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

relative_eq() #567

Merged
merged 19 commits into from
Jan 4, 2021
Merged

relative_eq() #567

merged 19 commits into from
Jan 4, 2021

Conversation

martinfrances107
Copy link
Contributor

@martinfrances107 martinfrances107 commented Dec 11, 2020

Added num_coords() method to MultiPoint. To Point, Line, LineString and MultiPoint test helpers and associated tests were added to assert that two structs were relatively equal. That is identical within the limits of numerical rounding accuracy.

  • I agree to follow the project's code of conduct.
  • I added an entry to CHANGES.md if knowledge of this change could be valuable to users.

Copy link
Member

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

Hi @martinfrances107 - this is looking pretty good!

With these traits implemented, we can now use something like:

approx::assert_relative_eq!(line_string_1, line_string_2);

Right? It'd be nice to see that macro usage in a test, since that's how we usually access these traits.

Is there a reason not to implement this on all of geo_types::Geometry?

Other than that, I have left some (I think) small requests for changes.

epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.start_point()
Copy link
Member

Choose a reason for hiding this comment

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

Rather than line.start_point()/line.end_point() which construct a Points, can we instead compare the underlying Coordinates directly?

I'd prefer something like this:

self.0.relative_eq(other.0, epsilon, max_relative) 
&& self.1.relative_eq(other.1, epsilon, max_relative)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have fixed this in the commit, to this PR, labelled 'points.rs Simplify RelativeEq for Point'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I want to respond to this question

With these traits implemented, we can now use something like:
approx::assert_relative_eq!(line_string_1, line_string_2);
Right?

I have added a new commit here "Added new feature relative_eq"
It does not work as I want yet... so there is more to come.

But I am at the point where I should discuss some architectural re-plumbing in public.

I actually want this to work in a module I am developing. This project rightly wants to keep approx as an internal dev-dependency. So If I want to see in in my third party module then I need to optionally expose approx when the other build environment ( Cargo.toml) enables the geo module's feature "relative_eq" This might explain some of my changes.

My current issue is that I can ONLY demonstrate approx::assert_relative_eq working in a limited number of build environments. It works in 'test' and 'doctest' ONLY and ONLY inside geo-types. When I try and demo this in geo sub-crate ( ie geo/src/algorithm/rotate.rs) or my third party module.. then it fails to compile..


#[inline]
fn abs_diff_eq(&self, other: &Line<T>, epsilon: Self::Epsilon) -> bool {
self.start_point()
Copy link
Member

Choose a reason for hiding this comment

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

ditto about point vs. coords

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks you fixed in : -
lines.rs Simplify "AbsDiffEq for Line" to operate of coords not points.

where
T: CoordinateType + Float + AbsDiffEq,
{
// type Epsilon = T;
Copy link
Member

Choose a reason for hiding this comment

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

Does this comment mean something or just leftover and can can be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks .. I will fix this

}

#[inline]
fn abs_diff_eq(&self, other: &LineString<T>, epsilon: Self::Epsilon) -> bool {
Copy link
Member

Choose a reason for hiding this comment

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

Can we do the num_coords check optimization here too like you did in relative_eq?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks this was fixed in the commit

line_string.rs - abs_diff_eq() Now rejects line_strings with unequal lengths. (with tests)

geo-types/src/line_string.rs Outdated Show resolved Hide resolved
geo-types/src/multi_point.rs Outdated Show resolved Hide resolved
geo-types/src/point.rs Outdated Show resolved Hide resolved
@@ -409,6 +414,79 @@ where
}
}

#[cfg(test)]
impl<T> RelativeEq for Point<T>
Copy link
Member

Choose a reason for hiding this comment

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

Please put the implementation on Coordinate and change Point's implementation to delegate to that - like:

impl<T> RelativeEq for Point<T> {
    fn relative_eq(self, other, epsilon) -> bool {
        self.0.relative_eq(other.0, epsilon)
    }
   ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

please put the implementation on

I think when I submit a fix... there is going to be a radical change here. So I want to signal it ahead of time

in the main issue .. I talked about euclidean distance versus 'Manhattan' distance.

Coordinate{x: T, y: T} has such a similar structure to Complex{ re: T, im: T}

I think I made a over-complication when I look in approx-0.4..0 at the implementation of Complex I see this ..

The implementation forgets the euclidean distance and just ensures that the both re and im meet the constraint individually. Because I copy/modifed the default implementation .. the same infinity checks for example will be done in the same order .. etc

#[cfg(feature = "num-complex")]
impl<T: RelativeEq> RelativeEq for Complex<T>
where
    T::Epsilon: Clone,
{
    #[inline]
    fn default_max_relative() -> T::Epsilon {
        T::default_max_relative()
    }

    #[inline]
    fn relative_eq(
        &self,
        other: &Complex<T>,
        epsilon: T::Epsilon,
        max_relative: T::Epsilon,
    ) -> bool {
        T::relative_eq(&self.re, &other.re, epsilon.clone(), max_relative.clone())
            && T::relative_eq(&self.im, &other.im, epsilon.clone(), max_relative.clone())
    }
}

Copy link
Member

Choose a reason for hiding this comment

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

Honestly - I think using manhattan distance is fine — it might even be preferred because it's so simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great -- so this has been fixed. The implementation was not moved into coords .. it was simply dissolved away.

Copy link
Member

Choose a reason for hiding this comment

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

I'd actually still prefer if this were moved into Coordinate as described, even if the implementation is much simpler now.

Reason being that Coordinate is kind of the currency of lots of geo algorithms, so it'd be great if we could start to leverage this functionality in those tests too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you .. yep I had only sorted this out for RelativeEq... While I was fixing this - I noticed that I was systematically over constraining this stuff with 'Float' ... fixed.

this commit.

Removed type constraint on RelativeEq and AbsDiffEq that the type must be Float. Also AbsDiffEq for Point now uses AbsDiffEq in Coords.

/// let multi_point: MultiPoint<f32> = coords.into_iter().collect();
/// assert_eq!(3, multi_point.num_coords());
/// ```
pub fn num_coords(&self) -> usize {
Copy link
Member

Choose a reason for hiding this comment

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

This is a helpful method.

Since @frewsxcv has a WIP implementation for the same functionality here: #563, I think it'd be better to avoid the public API churn - can you make this method private or pub(crate) for now, and add a comment linking to the as-of-yet unmerged PR to explain why it's not public.

Copy link
Member

Choose a reason for hiding this comment

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

BTW #563 has since merged.

Could you please rebase or merge against master and use the newly added CoordsIter#coords_count method instead of this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a question of placement, I would like to discuss..

CoordsItrer#coords_count is defined in the geo module.

And there is a security conflict. The implementation of the trait must be in the module defining the trait or the module containing the struct upon which the trait operates.

The conflict is that I can't

A) use coords_count() in geo_types.

or

B) I can't define RelatveEq for Point in geo because it it viewed as a unsafe third party module.

I will post and update when I have found it ... but I can't do what you ask.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I have rebased and resolved the deprection notices. here

"After rebase, drop the use of the deprecated num_coords() method."

The solution is that coords_count is a simple one-liner - so I opted to break the DRY principle and inline the function is a few places .. no biggie

Copy link
Member

Choose a reason for hiding this comment

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

I think this will lead to frustration for our users down the road to have methods with identical functionality but different names. I don't think people will be able to remember that all geo::Geometries can use coords_count but that only some geo::Geometries can use num_coords.

As you rightly point out, we unfortunately can't use the geo::CoordsIter trait in the geo-types crate, so instead I think it's best to delete this num_coords method and instead use the more verbose self.0.len() in the geo-types crate.

epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
// Handle same infinities.
Copy link
Member

Choose a reason for hiding this comment

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

I was initially surprised that we weren't simply delegating to the approx::relative_eq, but I think I understand why.

It might be helpful for future readers if there were a comment that explained this - something like:

// this implementation is based on the [approx crates `relative_eq`](some link) 
// implementation, but rather than comparing the difference
// between two scalars, we compare the euclidean distance between two 
// coordinates

(feel free to improve)

@martinfrances107
Copy link
Contributor Author

I am slowly fixing up the code in response to the review. I will post again when I think I have gotten to all the problems.

@michaelkirk
Copy link
Member

Looks like you're making steady progress @martinfrances107! Please tag me when you're ready for another round of review.

@martinfrances107
Copy link
Contributor Author

martinfrances107 commented Dec 24, 2020

@michaelkirk , Hi

I think I have fixed all the outstanding issues - and this is PR is ready for a review again

I have modified geo/src/algorithm/rotate.rs to use the new assertions and avoid hardcoding numerical rounding errors in the expected test results.

I have checked cargo doc ... and looked at the HTML output .. example of how to used the new methods are documented.

I wanted to be able to demonstrate these test / dependencies features being used my third party module but I think I want to leave that as a followup.

Thanks for the detailed review it really did greatly simplify the code and caught some holes in the test strategy

Merry Christmas!

Copy link
Member

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

This is looking very close!

I had some additional feedback and am hoping to solicit some input from other georust folks in one comment in particular...

/// let a = Line::new(Coordinate { x: 0., y: 0. }, Coordinate { x: 1., y: 1. });
/// let b = Line::new(Coordinate { x: 0., y: 0. }, Coordinate { x: 1.001, y: 1. });
///
/// approx::assert_relative_eq!(a, b, epsilon=0.1)
Copy link
Member

Choose a reason for hiding this comment

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

assert_abs_diff_eq

T::default_epsilon()
}

/// Equality assertion within a relative limit.
Copy link
Member

Choose a reason for hiding this comment

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

Comment seems like its for relative_eq not abs_diff_eq

/// let mut coords_b = vec![(0., 0.), (5., 0.), (7.001, 9.)];
/// let b: LineString<f32> = coords_b.into_iter().collect();
///
/// approx::assert_relative_eq!(a, b, epsilon=0.1)
Copy link
Member

Choose a reason for hiding this comment

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

ditto about assert_abs_diff_eq and the comment

/// let multi_point: MultiPoint<f32> = coords.into_iter().collect();
/// assert_eq!(3, multi_point.num_coords());
/// ```
pub fn num_coords(&self) -> usize {
Copy link
Member

Choose a reason for hiding this comment

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

I think this will lead to frustration for our users down the road to have methods with identical functionality but different names. I don't think people will be able to remember that all geo::Geometries can use coords_count but that only some geo::Geometries can use num_coords.

As you rightly point out, we unfortunately can't use the geo::CoordsIter trait in the geo-types crate, so instead I think it's best to delete this num_coords method and instead use the more verbose self.0.len() in the geo-types crate.

/// let a = MultiPoint(vec![point![x: 0., y: 0.], point![x: 10., y: 10.]]);
/// let b = MultiPoint(vec![point![x: 0., y: 0.], point![x: 10.001, y: 10.]]);
///
/// approx::assert_relative_eq!(a, b, epsilon=0.1)
Copy link
Member

Choose a reason for hiding this comment

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

ditto about assert_abs_diff_eq and the comment

geo/Cargo.toml Outdated
@@ -22,19 +22,20 @@ geographiclib-rs = { version = "0.2" }

proj = { version = "0.20.3", optional = true }

geo-types = { version = "0.6.2", path = "../geo-types", features = ["rstar"] }
geo-types = { version = "0.6.2", optional = true, path = "../geo-types", features = ["rstar"] }
Copy link
Member

Choose a reason for hiding this comment

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

I'm not 100% I understand the changes in this file. Surely geo cannot be built without geo-types, so making it an optional dependency seems fraught.

I'm guessing this is related to enabling the relative_eq by default only as a devDependency... Is that right? I see why that might be tricky.

A maybe radical alternative which avoids the problem is to get rid of the relative_eq feature you've introduced and simply have the new functionality you've written enabled by default.

I appreciate the intent, because we try to keep geo-types svelte, but since geo-types already unconditionally relies on the approx crate, including your new code for the approx impls seems like an acceptable addition to me.

Anyone else care to weigh in?
/cc @georust/geo-publishers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review, this is a tricky one

yes to a radical alternatives... this frustrates me ..Let me state the problem in very simplistic terms just so someone who has seen this before can say .. Oh that is easy.

I want different features to be present on (A) the release build and (B) the dev build.

Making geo-types optional is bad when it must always be included. I was constrained here ... there are long threads on stackoverflow about this - and meandering discussions amongst rustlang developers with lots of well meaning comments along the line of .. we can do better but no action. I hope I am just making a silly mistake.

Copy link
Member

Choose a reason for hiding this comment

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

To be clear, I'm just waiting a few days to solicit feedback from @georust/geo-publishers

The specific question is:

Since we already include approx by default in geo-types, are we ok with adding these implementations of approx traits on geo-types by default as well.

Copy link
Member

Choose a reason for hiding this comment

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

This looks like the feature we'd want: rust-lang/cargo#7916

Copy link
Member

Choose a reason for hiding this comment

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

This looks like the feature we'd want: rust-lang/cargo#7916

And if so, it looks like this feature should stabilize soon: rust-lang/cargo#8997

So it seems like we have a few options:

  1. Wait to merge this until Tracking issue for -Z features=dev_dep rust-lang/cargo#7916 lands on stable Rust
  2. Make geo-types optional, which is what this pull request currently does. When Tracking issue for -Z features=dev_dep rust-lang/cargo#7916 lands, we can make geo-types required again
  3. Keep geo-types required, but always include the relative_eq feature. Even on non-test builds. When Tracking issue for -Z features=dev_dep rust-lang/cargo#7916 lands, we can turn on relative_eq only in dev-dependencies

I'm fine with any of these options, with a slight preference for option 3. It's unfortunate we introduce a new dependency for everyone, but it'll hopefully be temporary.

If we go with options 2 or 3, we should add a comment or open a GitHub Issue saying we can improve the situation when rust-lang/cargo#7916 lands.

Copy link
Member

Choose a reason for hiding this comment

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

I'm working on a slightly different proposal... hopefully I'll have something to share in 20min.

Copy link
Member

Choose a reason for hiding this comment

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

please see martinfrances107#1

Copy link
Member

Choose a reason for hiding this comment

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

What do you think of this solution @frewsxcv (et al)?

Currently in master:

  • geo-types dependency on approx is not optional
  • the methods in geo_types::private_utils are unconditionally built, but only used within geo-types if the rstar feature is enabled.

After this PR:

  • geo-types dependency on approx is optional
  • These new implementations for RelativeEq and AbsDiffEq are enabled with the now optional approx feature
  • There is "public" API breakage, in that methods in geo_types::private_utils are now skipped unless the rstar feature which requires them is enabled (geo builds geo-types w/ rstar feature, so this won't affect geo).

So technically, if some other crate is using the methods in private_utils, and not enabling the rstar feature, this would break for them. Even though this technically breaks a public API, I think it's OK to treat this as a minor change since the changes are in a module with the word "private" in its name. WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

SGTM! Great solution

Copy link
Member

Choose a reason for hiding this comment

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

^^ Agreed!

let line1 = Line::from([(1., 0.9999999999999999), (-1., 1.)]);
assert_eq!(line0.rotate(90.), line1);
let line1 = Line::from([(1., 1.), (-1., 1.)]);
assert_relative_eq!(line0.rotate(90.0), line1);
Copy link
Member

Choose a reason for hiding this comment

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

🎉 yay!

}
#[test]
fn test_rotate_line_around_point() {
let line0 = Line::new(Point::new(0., 0.), Point::new(0., 2.));
let line1 = Line::new(
Point::new(0., 0.),
Point::new(-2., 0.00000000000000012246467991473532),
Copy link
Member

Choose a reason for hiding this comment

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

🎉 yay! good riddance!

@michaelkirk
Copy link
Member

And thank you for all the good docs and tests!

@martinfrances107
Copy link
Contributor Author

Thanks for the carefully review again... I have fixed all my silly mistakes.

The major problem concerning how to pull in different features into different build environment persists.

Rename `relative_eq` feature to `approx` and make optional
Copy link
Member

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

bors r+

Ok, everything seems to be in order! Thanks for working through the lengthy review process with us @martinfrances107. I'm pretty happy with where it ended up. Thanks for your contribution!

@bors
Copy link
Contributor

bors bot commented Jan 4, 2021

Build succeeded:

@bors bors bot merged commit 7dbc853 into georust:master Jan 4, 2021
@martinfrances107 martinfrances107 deleted the relative_eq branch January 5, 2021 09:48
@michaelkirk michaelkirk mentioned this pull request Feb 20, 2021
2 tasks
bors bot added a commit that referenced this pull request Feb 22, 2021
628: approx for all Geometry types r=frewsxcv a=michaelkirk

- [x] I agree to follow the project's [code of conduct](https://github.com/georust/geo/blob/master/CODE_OF_CONDUCT.md).
- [x] I added an entry to `CHANGES.md` if knowledge of this change could be valuable to users.
---

Follow up to #567 which implemented for Coord, Point, LineString, and a few others, this PR implements it for the remaining types, and `Geometry` itself.

FYI this is a precursor to an integration I'm working on with some of the JTS test suite.



Co-authored-by: Michael Kirk <michael.code@endoftheworl.de>
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.

None yet

4 participants