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

feat: make FrechetDistance generic over Metric Spaces #1274

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

grevgeny
Copy link
Contributor

@grevgeny grevgeny commented Nov 21, 2024

  • 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.

Part of #1181

@grevgeny
Copy link
Contributor Author

@michaelkirk What do you think about the following approach to FrechetDistance with new metric space setup for lines?

@grevgeny grevgeny marked this pull request as ready for review November 22, 2024 14:22
@lnicola
Copy link
Member

lnicola commented Dec 3, 2024

This is only a drive-by comment, but wouldn't it make sense to pass the metric as a value, e.g. g1.frechet_distance(&Euclidean, &g2)? I think some metrics can be parameterized. An example is Mahalanobis, which is somewhat like a skewed Euclidean, and takes a point and a covariance matrix.

@michaelkirk
Copy link
Member

Hi @grevgeny - this approach nicely extends frechet to other metric spaces. Thank you!

I have a preference for the API to be like this:

Euclidean::frechet_distance(&line_string_1, &line_string_2)

rather than

line_string.frechet_distance::<Euclidean>(&line_string_2)

They are equivalent at this point, but I have it in mind to one day make the metric spaces customizable (e.g. custom earth radius for Haversine) and I think that change will be a little cleaner if we approach it as suggested.

@lnicola
Copy link
Member

lnicola commented Dec 3, 2024

We could go directly for Euclidean.frechet_distance(&line_string_1, &line_string), then.

@michaelkirk
Copy link
Member

michaelkirk commented Dec 3, 2024

We could go directly for Euclidean.frechet_distance(&line_string_1, &line_string, then.

I'm not sure what you're talking about — could you be more complete in your thought?

If you're talking about (someday) customizing metric spaces, I was imaging something like:

struct Haversine { meters_radius: f64 };

impl Default for Haversine {
  fn default() -> Self {
    Self { meters_radius: 6_378_137 }
  }
}

trait FrechetDistance: Default , Distance<Point, Point> {
  fn frechet_distance(lhs, rhs) -> F {
    Self::default()::custom_frechet_distance(lhs, rhs)
  }
  fn custom_frechet_distance(&self, lhs: &LineString, rhs: &LineString) -> F {
     // more or less the same impl as it exists today
  }
}

// stay concise for the vast majority (I think?) of people who don't want to customize anything
Haversine:::frechet_distance(&line_string_1, &line_string)

// Offer a more verbose API for the 0.5% of users who want to customize the metric space
let mars_haversine = Haversine::with_radius(3_389_500);
mars_haversine.custom_frechet_distance(&line_string_1, &line_string)

But probably more thought is due.

@lnicola
Copy link
Member

lnicola commented Dec 3, 2024

Yeah, something like that. In my example, Euclidean is an empty struct, and there's a single trait method, which takes &self. I imagine we could also make constants for the more common parameterizations.

@michaelkirk
Copy link
Member

michaelkirk commented Dec 3, 2024

Euclidean isn't a very illuminating example, because I don't think you'd ever want to customize it, so let's talk about Haversine.

... maybe this would be better:

trait FrechetDistance: Distance<Point, Point> {
  fn frechet_distance(&self, lhs: &LineString, rhs: &LineString) -> F {
     // more or less the same impl as it exists today
  }
}

pub struct Haversine;
pub struct CustomHaversine { radius_meters: f64 };

impl Haversine {
  pub fn with_radius(radius_meters: f64) -> CustomHaversine {
    // Is it weird that this returns something other than `Self`?
    CustomHaversine { radius_meters }
  }
  pub fn wgs84() -> CustomHaversine {
    CustomHaversine { radius_meters: 6_378_137 }
  }
  pub fn earth_mean_radius() -> CustomHaversine {
    CustomHaversine { radius_meters: 6_371_000 }
  }
}

impl Distance<Point, Point> for CustomHaversine {
  fn to_radians(degrees: f64) -> f64 {
    degrees * PI / 180.0
  }
  fn distance(&self, lhs: &Point, rhs: &Point) -> F {
     let lat1 = Self::to_radians(lat1);
     let lon1 = Self::to_radians(lon1);
     let lat2 = Self::to_radians(lat2);
     let lon2 = Self::to_radians(lon2);

     let dlat = lat2 - lat1;
     let dlon = lon2 - lon1;

     let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
     let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());

    // uses custom radius
     self.radius_meters * c  
  }
}

impl Distance<Point, Point> for Haversine {
 fn distance(&self, lhs: &Point, rhs: &Point) -> F {
    // assume Earth default radius
    CustomHaversine::wgs84().distance(lhs, rhs)
  }
}

// stays concise for the vast majority (I think?) of people who don't want to customize anything
Haversine.frechet_distance(&line_string_1, &line_string_2)

// Offer a slightly more verbose API for the 0.5% of users who want to customize the metric space
let mars_haversine = Haversine::with_radius(3_389_500);
mars_haversine.frechet_distance(&line_string_1, &line_string_2)

vs. my previous suggestion, this would avoid the weird distinction between frechet_distance and custom_frechet_distance. For something like Euclidean this distinction probably wouldn't even make sense.

Sorry to derail your PR review @grevgeny - it's kind of related though. 😅

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