refraction/src/bin/flat/main.rs
2024-06-10 17:28:33 +03:00

729 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::f32::consts::{FRAC_PI_2, PI};
use flo_draw::*;
use flo_canvas::*;
use glam::*;
mod riemann;
use riemann::{Tens2, Decomp2, Metric, trace_iter};
use shape::Shape;
use Subspace::{Boundary, Inner, Outer};
#[cfg(test)]
use approx::assert_abs_diff_eq;
const DT: f32 = 0.1;
fn draw_loop(gc: &mut Vec<Draw>, mut pts: impl Iterator<Item=Vec2>) {
gc.new_path();
let Some(first) = pts.next() else { return; };
gc.move_to(first.x, first.y);
for pt in pts {
gc.line_to(pt.x, pt.y);
}
gc.close_path();
gc.stroke();
}
pub fn main() {
with_2d_graphics(move || {
let canvas = create_drawing_window("Refraction");
canvas.draw(|gc| {
let tube = Rect {
inner_radius: 30.0,
outer_radius: 50.0,
internal_halflength: 100.0,
external_halflength: 300.0,
};
let objs: Vec<_> = [-1.25, -1.00, -0.85, -0.50, 0.00, 0.40, 0.70, 0.95, 1.05]
.iter()
.enumerate()
.map(|(k, y)| Object {
id: k as i32,
loc: {
let pos = vec2(0.0, y * tube.external_halflength);
let adj = tube.sqrt_at(pos).inverse().into();
Location {
pos,
rot: adj,
}
},
r: 20.0,
})
.collect();
let space = Space { rect: tube, objs };
gc.canvas_height(500.0);
gc.transform(Transform2D::rotate(FRAC_PI_2));
tube.render(gc);
gc.line_width(0.5);
// gc.stroke_color(Color::Rgba(1.0, 0.5, 0.0, 0.5));
// draw_fan(gc, &tube, vec2(-500.0, 0.0), vec2(1.0, 0.0), 1.0);
gc.stroke_color(Color::Rgba(1.0, 0.5, 0.0, 1.0));
draw_fan_2(gc, &space, vec2(-500.0, 0.0), vec2(1.0, 0.0), 1.0);
gc.stroke_color(Color::Rgba(0.5, 1.0, 0.0, 1.0));
draw_fan_2(gc, &space, vec2(-2.5 * tube.outer_radius, 1.25 * tube.external_halflength), vec2(1.0, -1.0), 1.0);
draw_track(gc, &space, vec2(-500.0, 0.0), vec2(1.0, 0.2));
draw_track(gc, &space, vec2(-500.0, 0.0), vec2(1.0, 0.5));
draw_track(gc, &space, vec2(-0.5 * tube.inner_radius, -1.25 * tube.external_halflength), vec2(0.1, 1.0));
let circle_segments = 47;
for obj in &space.objs {
let pos = obj.loc.pos;
gc.new_path();
gc.circle(pos.x, pos.y, 5.0);
gc.fill_color(Color::Rgba(0.0, 0.5, 1.0, 1.0));
gc.fill();
gc.stroke_color(Color::Rgba(0.0, 0.0, 0.0, 0.5));
draw_loop(gc, itertools_num::linspace(0.0, 2.0 * PI, circle_segments).skip(1).map(|φ| {
let dir = Vec2::from_angle(φ) * obj.r;
let dir = obj.loc.rot * dir;
pos + dir
}));
gc.stroke_color(Color::Rgba(0.0, 0.5, 1.0, 0.5));
draw_loop(gc, itertools_num::linspace(0.0, 2.0 * PI, circle_segments).skip(1).map(|φ| {
let dir = Vec2::from_angle(φ) * obj.r;
let dir = obj.loc.rot * dir;
space.trace_step(Ray { pos, dir }).pos
}));
gc.stroke_color(Color::Rgba(0.5, 0.0, 1.0, 1.0));
draw_loop(gc, itertools_num::linspace(0.0, 2.0 * PI, circle_segments).skip(1).map(|φ| {
let n = obj.r.floor();
let d = obj.r / n;
let dir = Vec2::from_angle(φ);
let dir = obj.loc.rot * dir * d;
space.trace_iter(Ray { pos, dir }).nth(n as usize).unwrap().pos
}));
}
});
});
}
#[derive(Copy, Clone, Debug)]
struct Location {
/// Положение в основной СК
pos: Vec2,
/// Преобразование вектора из локальной ортонормированной в основную СК
rot: Mat2,
}
#[derive(Copy, Clone, Debug)]
struct Object {
id: i32,
loc: Location,
r: f32,
}
struct Space {
rect: Rect,
objs: Vec<Object>,
}
#[derive(PartialEq, Eq, Debug)]
enum Subspace {
Outer,
Boundary,
Inner,
}
struct Hit {
distance: f32,
id: i32,
}
struct FlatTraceResult {
end: Option<Ray>,
objects: Vec<Hit>,
}
impl Space {
fn which_subspace(&self, pt: Vec2) -> Subspace {
if pt.y.abs() > self.rect.external_halflength {
Outer
} else if pt.x.abs() > self.rect.outer_radius {
Outer
} else if pt.x.abs() > self.rect.inner_radius {
Boundary
} else {
Inner
}
}
fn flat_to_global(&self, at: Vec2) -> Mat2 {
Mat2::from(self.rect.sqrt_at(at).inverse())
}
fn global_to_flat(&self, at: Vec2) -> Mat2 {
Mat2::from(self.rect.sqrt_at(at))
}
/// Выполняет один шаг трассировки. Работает в любой части пространства, но вне Boundary доступны более эффективные методы.
/// ray задаётся в основной СК.
fn trace_step(&self, ray: Ray) -> Ray {
let a: Vec2 = -riemann::contract2(riemann::krist(&self.rect, ray.pos), ray.dir);
let v = ray.dir + a;
let p = ray.pos + v;
Ray { pos: p, dir: v }
}
/// Выполняет один шаг перемещения. Работает в любой части пространства.
/// off задаётся в локальной СК. Рекомендуется считать небольшими шагами.
fn move_step(&self, loc: Location, off: Vec2) -> Location {
let corr = Mat2::IDENTITY - riemann::contract(riemann::krist(&self.rect, loc.pos), loc.rot * off);
let p = loc.pos + corr * loc.rot * off;
Location { pos: p, rot: corr * loc.rot }
}
fn trace_iter(&self, ray: Ray) -> impl Iterator<Item=Ray> + '_ {
std::iter::successors(Some(ray), |&ray| Some(self.trace_step(ray)))
}
fn trace_inner(&self, ray: Ray) -> Ray {
assert_eq!(self.which_subspace(ray.pos), Inner);
let cell = RectInside { rect: self.rect };
let ray = cell.ray_to_local(ray);
let dist = cell.to_boundary(ray).expect("Can't get outta here!");
let ray = ray.forward(dist);
cell.ray_to_global(ray)
}
fn trace_outer(&self, ray: Ray) -> FlatTraceResult {
assert_eq!(self.which_subspace(ray.pos), Outer);
let cell = basic_shapes::Rect { size: vec2(self.rect.outer_radius, self.rect.external_halflength) };
let objs = Self::trace_to_objects(self.list_objects_outer(), ray);
if let Some(dist) = cell.trace_into(ray) {
FlatTraceResult {
end: Some(ray.forward(dist)),
objects: objs.into_iter().filter(|hit| hit.distance < dist).collect(),
}
} else {
FlatTraceResult {
end: None,
objects: objs,
}
}
}
fn trace_boundary(&self, ray: Ray) -> Ray {
assert_eq!(self.which_subspace(ray.pos), Boundary);
self.trace_iter(ray)
.find(|&ray| self.which_subspace(ray.pos) != Boundary)
.expect("Can't get outta the wall!")
}
fn list_objects_outer(&self) -> &[Object] {
self.objs.as_slice()
}
fn trace_to_objects(objs: &[Object], ray: Ray) -> Vec<Hit> {
objs.iter()
.filter_map(|obj| {
let rel = ray.pos - obj.loc.pos;
let diff = rel.dot(ray.dir).powi(2) - ray.dir.length_squared() * (rel.length_squared() - obj.r.powi(2));
if diff > 0.0 {
let t = (-rel.dot(ray.dir) - diff.sqrt()) / ray.dir.length_squared();
if t >= 0.0 {
return Some(Hit { id: obj.id, distance: t });
}
}
None
}).collect()
}
}
fn draw_ray_2(gc: &mut Vec<Draw>, space: &Space, base: Vec2, dir: Vec2) {
let mut hits = Vec::<Draw>::new();
let dir = space.rect.globalize(base, dir);
gc.new_path();
gc.move_to(base.x, base.y);
let mut ray = Ray { pos: base, dir: space.rect.normalize_vec_at(base, dir) * DT };
for _ in 0..10000 {
ray = space.trace_step(ray);
gc.line_to(ray.pos.x, ray.pos.y);
if ray.pos.abs().cmpgt(Vec2::splat(1000.0)).any() {
break;
}
let sub = space.which_subspace(ray.pos);
if sub == Boundary {
continue;
}
gc.stroke();
gc.new_dash_pattern();
// gc.dash_length(6.0);
gc.new_path();
gc.move_to(ray.pos.x, ray.pos.y);
match sub {
Inner => {
ray = space.trace_inner(ray);
}
Outer => {
let ret = space.trace_outer(ray);
for hit in ret.objects {
let r = ray.forward(hit.distance);
hits.circle(r.pos.x, r.pos.y, 1.5);
}
ray = match ret.end {
Some(r) => r,
None => {
ray = ray.forward(1000.0 / DT);
gc.line_to(ray.pos.x, ray.pos.y);
break;
}
};
}
Boundary => panic!(),
}
gc.line_to(ray.pos.x, ray.pos.y);
gc.stroke();
gc.new_dash_pattern();
gc.new_path();
gc.move_to(ray.pos.x, ray.pos.y);
}
gc.stroke();
gc.new_path();
gc.new_dash_pattern();
gc.append(&mut hits);
gc.stroke();
}
fn draw_fan_2(gc: &mut Vec<Draw>, space: &Space, base: Vec2, dir: Vec2, spread: f32) {
let dir = dir.normalize();
let v = vec2(-dir.y, dir.x);
for y in itertools_num::linspace(-spread, spread, 101) {
draw_ray_2(gc, space, base, dir + y * v);
}
}
fn draw_ray(gc: &mut Vec<Draw>, space: &impl Metric, base: Vec2, dir: Vec2) {
let dir = space.globalize(base, dir);
gc.new_path();
gc.move_to(base.x, base.y);
for pt in trace_iter(space, base, dir, DT).take(10000) {
gc.line_to(pt.x, pt.y);
if pt.abs().cmpgt(Vec2::splat(1000.0)).any() {
break;
}
}
gc.stroke();
}
fn draw_track(gc: &mut Vec<Draw>, space: &Space, start: Vec2, dir: Vec2) {
const SCALE: f32 = 5.0;
const STEP: f32 = 2.0 * SCALE;
// let mut loc = Location { pos: start, rot: Mat2::IDENTITY };
// let dir = space.rect.globalize(start, dir);
// let v = space.rect.normalize(start, dir);
let mut loc = Location { pos: start, rot: mat2(dir, vec2(-dir.y, dir.x)) };
let v = vec2(1.0, 0.0);
let mut draw = |loc: &Location| {
let p = loc.pos;
let ax = p + loc.rot.x_axis * SCALE;
let ay = p + loc.rot.y_axis * SCALE;
gc.new_path();
gc.stroke_color(Color::Rgba(0.7, 0.0, 0.0, 1.0));
gc.move_to(p.x, p.y);
gc.line_to(ax.x, ax.y);
gc.stroke();
gc.new_path();
gc.stroke_color(Color::Rgba(0.0, 0.7, 0.0, 1.0));
gc.move_to(p.x, p.y);
gc.line_to(ay.x, ay.y);
gc.stroke();
};
draw(&loc);
for _ in 0..1000 {
let N = (STEP / DT).floor() as i32;
for _ in 0..N {
loc = space.move_step(loc, v * DT);
}
draw(&loc);
}
}
fn draw_fan(gc: &mut Vec<Draw>, space: &impl Metric, base: Vec2, dir: Vec2, spread: f32) {
let dir = dir.normalize();
let v = vec2(-dir.y, dir.x);
for y in itertools_num::linspace(-spread, spread, 101) {
draw_ray(gc, space, base, dir + y * v);
}
}
trait Renderable {
fn render(&self, gc: &mut Vec<Draw>);
}
impl Renderable for Rect {
fn render(&self, gc: &mut Vec<Draw>) {
gc.new_path();
gc.rect(-self.outer_radius, -self.external_halflength, self.outer_radius, self.external_halflength);
gc.rect(-self.inner_radius, -self.external_halflength, self.inner_radius, self.external_halflength);
gc.winding_rule(WindingRule::EvenOdd);
gc.fill_color(Color::Rgba(0.8, 0.8, 0.8, 1.0));
gc.fill();
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
struct Ray {
pos: Vec2,
dir: Vec2,
}
impl Ray {
fn forward(&self, dist: f32) -> Ray {
Ray { pos: self.pos + self.dir * dist, dir: self.dir }
}
}
mod basic_shapes {
use glam::{Vec2, vec2};
use crate::Ray;
use crate::shape::Shape;
pub struct Rect {
pub size: Vec2,
}
impl Rect {
/// Отражает луч, чтобы все координаты направления были положительны (допустимо благодаря симметрии Rect).
fn flip_ray(ray: Ray) -> Ray {
Ray { pos: ray.pos * ray.dir.signum(), dir: ray.dir.abs() }
}
}
impl Shape for Rect {
fn is_inside(&self, pt: Vec2) -> bool {
pt.abs().cmplt(self.size).all()
}
fn trace_into(&self, ray: Ray) -> Option<f32> {
let ray = Self::flip_ray(ray);
// ray.pos.x + t * ray.dir.x = size.x
let ts = (-self.size - ray.pos) / ray.dir;
let t = ts.max_element();
let pt = ray.pos + t * ray.dir;
if t < 0.0 { return None; }
if pt.cmpgt(self.size).any() { return None; }
Some(t)
}
fn trace_out_of(&self, ray: Ray) -> Option<f32> {
let ray = Self::flip_ray(ray);
// ray.pos.x + t * ray.dir.x = +size.x
let ts = (self.size - ray.pos) / ray.dir;
let t = ts.min_element();
Some(t)
}
fn visualise(&self) -> Vec<Vec2> {
vec![vec2(-self.size.x, -self.size.y), vec2(self.size.x, -self.size.y), vec2(self.size.x, self.size.y), vec2(-self.size.x, self.size.y)]
}
}
#[test]
fn test_rect() {
assert_eq!(Rect::flip_ray(Ray { pos: vec2(2.0, 3.0), dir: vec2(4.0, 5.0) }), Ray { pos: vec2(2.0, 3.0), dir: vec2(4.0, 5.0) });
assert_eq!(Rect::flip_ray(Ray { pos: vec2(2.0, 3.0), dir: vec2(-4.0, 5.0) }), Ray { pos: vec2(-2.0, 3.0), dir: vec2(4.0, 5.0) });
assert_eq!(Rect::flip_ray(Ray { pos: vec2(2.0, 3.0), dir: vec2(4.0, -5.0) }), Ray { pos: vec2(2.0, -3.0), dir: vec2(4.0, 5.0) });
assert_eq!(Rect::flip_ray(Ray { pos: vec2(2.0, 3.0), dir: vec2(4.0, 0.0) }), Ray { pos: vec2(2.0, 3.0), dir: vec2(4.0, 0.0) });
let r = Rect { size: vec2(2.0, 3.0) };
assert_eq!(r.trace_into(Ray { pos: vec2(3.0, 3.0), dir: vec2(1.0, 1.0) }), None);
assert_eq!(r.trace_into(Ray { pos: vec2(-3.0, 2.0), dir: vec2(1.0, 0.0) }), Some(1.0));
assert_eq!(r.trace_into(Ray { pos: vec2(-3.0, 2.0), dir: vec2(-1.0, 0.0) }), None);
assert_eq!(r.trace_into(Ray { pos: vec2(-3.0, 1.0), dir: vec2(2.0, 2.0) }), Some(0.5));
assert_eq!(r.trace_into(Ray { pos: vec2(-3.0, 2.1), dir: vec2(2.0, 2.0) }), None);
assert_eq!(r.trace_into(Ray { pos: vec2(2.0, 3.0), dir: vec2(1.0, 1.0) }), None);
assert_eq!(r.trace_into(Ray { pos: vec2(-2.0, 3.0), dir: vec2(-1.0, 1.0) }), None);
assert_eq!(r.trace_into(Ray { pos: vec2(2.0, 3.0), dir: vec2(-1.0, -1.0) }), Some(0.0));
assert_eq!(r.trace_into(Ray { pos: vec2(2.0, -3.0), dir: vec2(-1.0, 1.0) }), Some(0.0));
assert_eq!(r.trace_out_of(Ray { pos: vec2(0.0, 0.0), dir: vec2(1.0, 1.0) }), Some(2.0));
assert_eq!(r.trace_out_of(Ray { pos: vec2(0.0, 0.0), dir: vec2(0.0, 1.0) }), Some(3.0));
assert_eq!(r.trace_out_of(Ray { pos: vec2(0.0, 1.0), dir: vec2(0.0, -1.0) }), Some(4.0));
assert_eq!(r.trace_out_of(Ray { pos: vec2(1.0, 1.0), dir: vec2(0.0, -1.0) }), Some(4.0));
assert_eq!(r.trace_out_of(Ray { pos: vec2(2.0, 3.0), dir: vec2(1.0, 1.0) }), Some(0.0));
}
}
mod shape {
use glam::Vec2;
use crate::Ray;
pub trait Shape {
fn is_inside(&self, pt: Vec2) -> bool;
/// Ищет ближайшее пересечение луча с границей в направлении внутрь контура. Возвращает расстояние (в ray.dir).
fn trace_into(&self, ray: Ray) -> Option<f32>;
/// Ищет ближайшее пересечение луча с границей в направлении вовне контура. Возвращает расстояние (в ray.dir).
fn trace_out_of(&self, ray: Ray) -> Option<f32>;
/// Возвращает визуальное представление контура, для отладки.
fn visualise(&self) -> Vec<Vec2>;
}
}
trait FlatCell: std::fmt::Debug {
fn pos_to_global(&self, pos: Vec2) -> Vec2;
fn pos_to_local(&self, pos: Vec2) -> Vec2;
fn ray_to_global(&self, ray: Ray) -> Ray;
fn ray_to_local(&self, ray: Ray) -> Ray;
fn is_inside(&self, pos: Vec2) -> bool {
let bnd = self.local_bounds();
pos.cmpge(bnd.0).all() && pos.cmple(bnd.1).all()
}
fn local_bounds(&self) -> (Vec2, Vec2);
fn to_boundary(&self, ray: Ray) -> Option<f32> {
assert!(self.is_inside(ray.pos));
let sgn = ray.dir.signum();
let p = ray.pos * sgn;
let v = ray.dir * sgn;
let mut bnd = self.local_bounds();
if sgn.x < 0.0 {
(bnd.0.x, bnd.1.x) = (-bnd.1.x, -bnd.0.x);
}
if sgn.y < 0.0 {
(bnd.0.y, bnd.1.y) = (-bnd.1.y, -bnd.0.y);
}
let t = if (bnd.1.x - p.x) * v.y <= (bnd.1.y - p.y) * v.x {
(bnd.1.x - p.x) / v.x
} else {
(bnd.1.y - p.y) / v.y
};
if t <= 100000.0 {
Some(t)
} else {
None
}
}
}
#[derive(Debug)]
struct RectInside {
rect: Rect,
}
impl FlatCell for RectInside {
fn pos_to_global(&self, pos: Vec2) -> Vec2 {
vec2(pos.x, self.rect.x(pos.y))
}
fn pos_to_local(&self, pos: Vec2) -> Vec2 {
vec2(pos.x, self.rect.u(pos.y))
}
fn ray_to_global(&self, ray: Ray) -> Ray {
Ray {
pos: self.pos_to_global(ray.pos),
dir: vec2(ray.dir.x, self.rect.dx(ray.pos.y, ray.dir.y)),
}
}
fn ray_to_local(&self, ray: Ray) -> Ray {
Ray {
pos: self.pos_to_local(ray.pos),
dir: vec2(ray.dir.x, self.rect.du(ray.pos.y, ray.dir.y)),
}
}
fn local_bounds(&self) -> (Vec2, Vec2) {
(vec2(-self.rect.inner_radius, -self.rect.internal_halflength), vec2(self.rect.inner_radius, self.rect.internal_halflength))
}
}
#[derive(Copy, Clone, Debug)]
struct Rect {
outer_radius: f32,
inner_radius: f32,
external_halflength: f32,
internal_halflength: f32,
}
impl Rect {
fn fx(&self) -> fns::RectX { fns::RectX { min: self.inner_radius, max: self.outer_radius } }
fn fy(&self) -> fns::RectY { fns::RectY { internal: self.internal_halflength, external: self.external_halflength } }
pub fn x(&self, u: f32) -> f32 { self.fy().x(u) }
pub fn u(&self, x: f32) -> f32 { self.fy().u(x) }
pub fn dx(&self, u: f32, du: f32) -> f32 { self.fy().dx(u) * du }
pub fn du(&self, x: f32, dx: f32) -> f32 { self.fy().du(x) * dx }
}
mod fns {
use crate::FloatExt2;
#[cfg(test)]
use approx::abs_diff_eq;
pub struct RectX {
pub min: f32,
pub max: f32,
}
impl RectX {
pub fn value(&self, x: f32) -> f32 { (self.min, self.max).inverse_lerp(x.abs()).clamp(0.0, 1.0) }
pub fn derivative(&self, x: f32) -> f32 { if x.abs() > self.min && x.abs() < self.max { x.signum() / (self.max - self.min) } else { 0.0 } }
}
pub struct RectY {
pub internal: f32,
pub external: f32,
}
/// Продолжает функцию f с [-lim, lim] линейно в предположении f(±lim) = ±val, f'(±lim) = 1.
fn extend_linear(t: f32, f: impl FnOnce(f32) -> f32, lim: f32, val: f32) -> f32 {
if t.abs() <= lim { f(t) } else { t + t.signum() * (val - lim) }
}
/// Продолжает функцию f с [-lim, lim] константой в предположении f(±lim) = val, f'(±lim) = 0.
fn extend_const(t: f32, f: impl FnOnce(f32) -> f32, lim: f32, val: f32) -> f32 {
if t.abs() <= lim { f(t) } else { val }
}
impl RectY {
fn a(&self) -> f32 { -(self.external - self.internal) / self.internal.powi(2) }
fn b(&self) -> f32 { 2.0 * self.external / self.internal - 1.0 }
fn root(&self, x: f32) -> f32 { (self.b().powi(2) + 4.0 * self.a() * x.abs()).sqrt() }
pub fn x(&self, u: f32) -> f32 { extend_linear(u, |u| (self.a() * u.abs() + self.b()) * u, self.internal, self.external) }
pub fn u(&self, x: f32) -> f32 { extend_linear(x, |x| 0.5 * x.signum() * (-self.b() + self.root(x)) / self.a(), self.external, self.internal) }
pub fn dx(&self, u: f32) -> f32 { extend_const(u, |u| 2.0 * self.a() * u.abs() + self.b(), self.internal, 1.0) }
pub fn du(&self, x: f32) -> f32 { extend_const(x, |x| 1.0 / self.root(x), self.external, 1.0) }
pub fn d2u(&self, x: f32) -> f32 { extend_const(x, |x| -2.0 * x.signum() * self.a() * self.root(x).powi(-3), self.external, 0.0) }
}
#[test]
fn test_rect_y() {
let testee = RectY { internal: 100.0, external: 150.0 };
let ε = 1.0e-4f32;
let δ = 1.0 / 8.0; // Mathematically, you want this to be small. Computationally, you dont.
let margin = 1.0 / 16.0;
let mul = 1.0 + margin;
for x in itertools_num::linspace(-mul * testee.external, mul * testee.external, 100) {
let u = testee.u(x);
assert!(abs_diff_eq!(x, testee.x(u), epsilon = ε), "At x={}:\nu(x): {}\nx(u(x)): {}\n", x, u, testee.x(u));
let du = (testee.u(x + δ) - testee.u(x - δ)) / (2. * δ);
assert!(abs_diff_eq!(du, testee.du(x), epsilon = ε), "At x={}, u':\nexpected: {}\nactual: {}\n", x, du, testee.du(x));
let d2u = (testee.du(x + δ) - testee.du(x - δ)) / (2. * δ);
assert!(abs_diff_eq!(d2u, testee.d2u(x), epsilon = ε), "At x={}, u'':\nexpected: {}\nactual: {}\n", x, d2u, testee.d2u(x));
}
for u in itertools_num::linspace(-mul * testee.internal, mul * testee.internal, 100) {
let x = testee.x(u);
assert!(abs_diff_eq!(x, testee.x(u), epsilon = ε), "At u={}:\nx(u): {}\nu(x(u)): {}\n", u, x, testee.u(x));
let dx = (testee.x(u + δ) - testee.x(u - δ)) / (2. * δ);
assert!(abs_diff_eq!(dx, testee.dx(u), epsilon = ε), "At u={}, x':\nexpected: {}\nactual: {}\n", u, dx, testee.dx(u));
}
}
}
#[test]
fn test_rect() {
let r = Rect {
outer_radius: 50.0,
inner_radius: 20.0,
external_halflength: 100.0,
internal_halflength: 10.0,
};
assert_abs_diff_eq!(r.x(r.internal_halflength), r.external_halflength, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.x(-r.internal_halflength), -r.external_halflength, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(r.external_halflength), r.internal_halflength, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(-r.external_halflength), -r.internal_halflength, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(r.internal_halflength, 3.0), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(-r.internal_halflength, 3.0), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(r.external_halflength, 3.0), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(-r.external_halflength, 3.0), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(r.x(1.0)), 1.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(r.x(5.0)), 5.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(r.x(-5.0)), -5.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.u(r.x(10.0)), 10.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.x(r.u(10.0)), 10.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.x(r.u(50.0)), 50.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.x(r.u(-50.0)), -50.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.x(r.u(100.0)), 100.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(10.0, r.dx(r.u(10.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(50.0, r.dx(r.u(50.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(-50.0, r.dx(r.u(-50.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.du(100.0, r.dx(r.u(100.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(1.0, r.du(r.x(1.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(5.0, r.du(r.x(5.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(-5.0, r.du(r.x(-5.0), 3.0)), 3.0, epsilon = 1.0e-5);
assert_abs_diff_eq!(r.dx(10.0, r.du(r.x(10.0), 3.0)), 3.0, epsilon = 1.0e-5);
}
trait FloatExt2 {
fn lerp(&self, t: f32) -> f32;
fn inverse_lerp(&self, y: f32) -> f32;
}
impl FloatExt2 for (f32, f32) {
fn lerp(&self, t: f32) -> f32 { f32::lerp(self.0, self.1, t) }
fn inverse_lerp(&self, y: f32) -> f32 { f32::inverse_lerp(self.0, self.1, y) }
}
impl Metric for Rect {
fn sqrt_at(&self, pos: Vec2) -> Decomp2 {
let sx = self.fx().value(pos.x);
let sy = self.fy().du(pos.y);
let s = sx + sy - sx * sy;
assert!(sx.is_finite());
assert!(sy.is_finite());
assert!(sy > 0.0);
Decomp2 {
ortho: Mat2::IDENTITY,
diag: vec2(1.0, s),
}
}
fn part_derivs_at(&self, pos: Vec2) -> Tens2 {
let sx = self.fx().value(pos.x);
let sy = self.fy().du(pos.y);
let s = sx + sy - sx * sy;
let dsx_dx = self.fx().derivative(pos.x);
let dsy_dy = self.fy().d2u(pos.y);
let ds2_dx = 2.0 * s * (1.0 - sy) * dsx_dx;
let ds2_dy = 2.0 * s * (1.0 - sx) * dsy_dy;
[
Mat2::from_cols_array(&[0.0, 0.0, 0.0, ds2_dx]),
Mat2::from_cols_array(&[0.0, 0.0, 0.0, ds2_dy]),
]
}
}
#[test]
fn test_rect_metric_derivs() {
struct Approx(Rect);
impl Metric for Approx {
fn sqrt_at(&self, pos: Vec2) -> Decomp2 { self.0.sqrt_at(pos) }
}
let testee = Rect {
inner_radius: 30.0,
outer_radius: 50.0,
internal_halflength: 100.0,
external_halflength: 300.0,
};
let approx = Approx(testee);
let epsilon = 1.0e-3;
let margin = 1.0 / 16.0;
let mul = 1.0 + margin;
for x in itertools_num::linspace(-mul * testee.outer_radius, mul * testee.outer_radius, 100) {
for y in itertools_num::linspace(-mul * testee.external_halflength, mul * testee.external_halflength, 100) {
let pos = vec2(x, y);
let computed = testee.part_derivs_at(pos);
let reference = approx.part_derivs_at(pos);
let eq = (0..2).all(|coord| computed[coord].abs_diff_eq(reference[coord], epsilon));
assert!(eq, "At {}:\nactual: {:?}\nexpected: {:?}\n", pos, computed, reference);
}
}
}