531 lines
16 KiB
Rust
531 lines
16 KiB
Rust
use ytbn_graphing_software::{AppSettings, EguiHelper, FunctionEntry, Riemann};
|
|
|
|
fn app_settings_constructor(
|
|
sum: Riemann,
|
|
integral_min_x: f64,
|
|
integral_max_x: f64,
|
|
pixel_width: usize,
|
|
integral_num: usize,
|
|
min_x: f64,
|
|
max_x: f64,
|
|
) -> AppSettings {
|
|
AppSettings {
|
|
riemann_sum: sum,
|
|
integral_min_x,
|
|
integral_max_x,
|
|
min_x,
|
|
max_x,
|
|
integral_changed: true,
|
|
integral_num,
|
|
do_extrema: false,
|
|
do_roots: false,
|
|
plot_width: pixel_width,
|
|
}
|
|
}
|
|
|
|
static BACK_TARGET: [(f64, f64); 11] = [
|
|
(-1.0, 1.0),
|
|
(-0.8, 0.6400000000000001),
|
|
(-0.6, 0.36),
|
|
(-0.4, 0.16000000000000003),
|
|
(-0.19999999999999996, 0.03999999999999998),
|
|
(0.0, 0.0),
|
|
(0.19999999999999996, 0.03999999999999998),
|
|
(0.3999999999999999, 0.15999999999999992),
|
|
(0.6000000000000001, 0.3600000000000001),
|
|
(0.8, 0.6400000000000001),
|
|
(1.0, 1.0),
|
|
];
|
|
|
|
static DERIVATIVE_TARGET: [(f64, f64); 11] = [
|
|
(-1.0, -2.0),
|
|
(-0.8, -1.6),
|
|
(-0.6, -1.2),
|
|
(-0.4, -0.8),
|
|
(-0.19999999999999996, -0.3999999999999999),
|
|
(0.0, 0.0),
|
|
(0.19999999999999996, 0.3999999999999999),
|
|
(0.3999999999999999, 0.7999999999999998),
|
|
(0.6000000000000001, 1.2000000000000002),
|
|
(0.8, 1.6),
|
|
(1.0, 2.0),
|
|
];
|
|
|
|
#[cfg(test)]
|
|
fn do_test(sum: Riemann, area_target: f64) {
|
|
let settings = app_settings_constructor(sum, -1.0, 1.0, 10, 10, -1.0, 1.0);
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2");
|
|
function.integral = true;
|
|
function.derivative = true;
|
|
|
|
let mut settings = settings;
|
|
{
|
|
function.calculate(true, true, false, settings);
|
|
assert!(!function.back_data.is_empty());
|
|
assert_eq!(function.back_data.len(), settings.plot_width + 1);
|
|
|
|
assert!(function.integral);
|
|
assert!(function.derivative);
|
|
|
|
assert_eq!(!function.root_data.is_empty(), settings.do_roots);
|
|
assert_eq!(!function.extrema_data.is_empty(), settings.do_extrema);
|
|
assert!(!function.derivative_data.is_empty());
|
|
assert!(function.integral_data.is_some());
|
|
|
|
assert_eq!(function.integral_data.clone().unwrap().1, area_target);
|
|
|
|
let a = function.derivative_data.clone().to_tuple();
|
|
|
|
assert_eq!(a.len(), DERIVATIVE_TARGET.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a[i].0 as f32, DERIVATIVE_TARGET[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a[i].1 as f32, DERIVATIVE_TARGET[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", DERIVATIVE_TARGET, a);
|
|
}
|
|
}
|
|
|
|
let a_1 = function.back_data.clone().to_tuple();
|
|
|
|
assert_eq!(a_1.len(), BACK_TARGET.len());
|
|
|
|
assert_eq!(a.len(), BACK_TARGET.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a_1[i].0 as f32, BACK_TARGET[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a_1[i].1 as f32, BACK_TARGET[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", BACK_TARGET, a_1);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
settings.min_x += 1.0;
|
|
settings.max_x += 1.0;
|
|
function.calculate(true, true, false, settings);
|
|
|
|
let a = function
|
|
.derivative_data
|
|
.clone()
|
|
.to_tuple()
|
|
.iter()
|
|
.take(6)
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
let b = DERIVATIVE_TARGET
|
|
.iter()
|
|
.rev()
|
|
.take(6)
|
|
.rev()
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
assert_eq!(a.len(), b.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", b, a);
|
|
}
|
|
}
|
|
|
|
let a_1 = function
|
|
.back_data
|
|
.clone()
|
|
.to_tuple()
|
|
.iter()
|
|
.take(6)
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
let b_1 = BACK_TARGET
|
|
.iter()
|
|
.rev()
|
|
.take(6)
|
|
.rev()
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
assert_eq!(a_1.len(), b_1.len());
|
|
|
|
assert_eq!(a.len(), b_1.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
settings.min_x -= 2.0;
|
|
settings.max_x -= 2.0;
|
|
function.calculate(true, true, false, settings);
|
|
|
|
let a = function
|
|
.derivative_data
|
|
.clone()
|
|
.to_tuple()
|
|
.iter()
|
|
.rev()
|
|
.take(6)
|
|
.rev()
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
let b = DERIVATIVE_TARGET
|
|
.iter()
|
|
.take(6)
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
assert_eq!(a.len(), b.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", b, a);
|
|
}
|
|
}
|
|
|
|
let a_1 = function
|
|
.back_data
|
|
.clone()
|
|
.to_tuple()
|
|
.iter()
|
|
.rev()
|
|
.take(6)
|
|
.rev()
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
let b_1 = BACK_TARGET
|
|
.iter()
|
|
.take(6)
|
|
.cloned()
|
|
.collect::<Vec<(f64, f64)>>();
|
|
|
|
assert_eq!(a_1.len(), b_1.len());
|
|
|
|
assert_eq!(a.len(), b_1.len());
|
|
|
|
for i in 0..a.len() {
|
|
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
|
|
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
|
|
{
|
|
panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
function.update_string("sin(x)");
|
|
assert!(function.get_test_result().is_none());
|
|
assert_eq!(&function.raw_func_str, "sin(x)");
|
|
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
assert!(!function.integral);
|
|
assert!(!function.derivative);
|
|
|
|
assert!(function.back_data.is_empty());
|
|
assert!(function.integral_data.is_none());
|
|
assert!(function.root_data.is_empty());
|
|
assert!(function.extrema_data.is_empty());
|
|
assert!(function.derivative_data.is_empty());
|
|
|
|
settings.min_x -= 1.0;
|
|
settings.max_x -= 1.0;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
assert!(!function.back_data.is_empty());
|
|
assert!(function.integral_data.is_none());
|
|
assert!(function.root_data.is_empty());
|
|
assert!(function.extrema_data.is_empty());
|
|
assert!(!function.derivative_data.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn left_function() {
|
|
do_test(Riemann::Left, 0.9600000000000001);
|
|
}
|
|
|
|
#[test]
|
|
fn middle_function() {
|
|
do_test(Riemann::Middle, 0.92);
|
|
}
|
|
|
|
#[test]
|
|
fn right_function() {
|
|
do_test(Riemann::Right, 0.8800000000000001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
|
|
settings.do_extrema = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 - 4"); // Parabola with vertex at (0, -4)
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// For f(x) = x^2 - 4, f'(x) = 2x
|
|
// Extrema occurs where f'(x) = 0, so at x = 0
|
|
assert!(!function.extrema_data.is_empty());
|
|
|
|
// Should have exactly one extremum at x = 0
|
|
assert_eq!(function.extrema_data.len(), 1);
|
|
|
|
let extremum = function.extrema_data[0];
|
|
assert!(emath::almost_equal(extremum.x as f32, 0.0, f32::EPSILON));
|
|
assert!(emath::almost_equal(extremum.y as f32, -4.0, f32::EPSILON));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema_multiple() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
|
|
settings.do_extrema = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^3 - 3*x"); // Cubic with local max and min
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// For f(x) = x^3 - 3x, f'(x) = 3x^2 - 3
|
|
// Extrema occur where f'(x) = 0, so at x = ±1
|
|
assert!(!function.extrema_data.is_empty());
|
|
|
|
// Should have exactly two extrema
|
|
assert_eq!(function.extrema_data.len(), 2);
|
|
|
|
// Sort by x coordinate for consistent testing
|
|
let mut extrema = function.extrema_data.clone();
|
|
extrema.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
|
|
|
|
// First extremum at x = -1, f(-1) = -1 + 3 = 2
|
|
assert!(emath::almost_equal(extrema[0].x as f32, -1.0, 0.01));
|
|
assert!(emath::almost_equal(extrema[0].y as f32, 2.0, 0.01));
|
|
|
|
// Second extremum at x = 1, f(1) = 1 - 3 = -2
|
|
assert!(emath::almost_equal(extrema[1].x as f32, 1.0, 0.01));
|
|
assert!(emath::almost_equal(extrema[1].y as f32, -2.0, 0.01));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema_disabled() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
|
|
settings.do_extrema = false; // Disable extrema
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 - 4");
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Extrema data should be empty when disabled
|
|
assert!(function.extrema_data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_roots() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
|
|
settings.do_roots = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 - 4"); // Parabola crossing x-axis at ±2
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// For f(x) = x^2 - 4, roots occur where x^2 = 4, so at x = ±2
|
|
assert!(!function.root_data.is_empty());
|
|
|
|
// Should have exactly two roots
|
|
assert_eq!(function.root_data.len(), 2);
|
|
|
|
// Sort by x coordinate for consistent testing
|
|
let mut roots = function.root_data.clone();
|
|
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
|
|
|
|
// First root at x = -2
|
|
assert!(emath::almost_equal(roots[0].x as f32, -2.0, 0.01));
|
|
assert!(emath::almost_equal(roots[0].y as f32, 0.0, 0.001));
|
|
|
|
// Second root at x = 2
|
|
assert!(emath::almost_equal(roots[1].x as f32, 2.0, 0.01));
|
|
assert!(emath::almost_equal(roots[1].y as f32, 0.0, 0.001));
|
|
}
|
|
|
|
#[test]
|
|
fn test_roots_single() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
|
|
settings.do_roots = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x - 1"); // Linear function crossing x-axis at x = 1
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// For f(x) = x - 1, root occurs at x = 1
|
|
assert!(!function.root_data.is_empty());
|
|
|
|
// Should have exactly one root
|
|
assert_eq!(function.root_data.len(), 1);
|
|
|
|
let root = function.root_data[0];
|
|
assert!(emath::almost_equal(root.x as f32, 1.0, 0.01));
|
|
assert!(emath::almost_equal(root.y as f32, 0.0, f32::EPSILON));
|
|
}
|
|
|
|
#[test]
|
|
fn test_roots_disabled() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
|
|
settings.do_roots = false; // Disable roots
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 - 4");
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Root data should be empty when disabled
|
|
assert!(function.root_data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema_and_roots_together() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
|
|
settings.do_extrema = true;
|
|
settings.do_roots = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 - 1"); // Parabola with vertex at (0, -1) and roots at ±1
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Should have one extremum at x = 0
|
|
assert!(!function.extrema_data.is_empty());
|
|
assert_eq!(function.extrema_data.len(), 1);
|
|
let extremum = function.extrema_data[0];
|
|
assert!(emath::almost_equal(extremum.x as f32, 0.0, 0.01));
|
|
assert!(emath::almost_equal(extremum.y as f32, -1.0, 0.01));
|
|
|
|
// Should have two roots at x = ±1
|
|
assert!(!function.root_data.is_empty());
|
|
assert_eq!(function.root_data.len(), 2);
|
|
|
|
let mut roots = function.root_data.clone();
|
|
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
|
|
|
|
assert!(emath::almost_equal(roots[0].x as f32, -1.0, 0.01));
|
|
assert!(emath::almost_equal(roots[1].x as f32, 1.0, 0.01));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema_no_extrema() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
|
|
settings.do_extrema = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x"); // Linear function has no extrema
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Linear function should have no extrema
|
|
assert!(function.extrema_data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_roots_no_roots() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
|
|
settings.do_roots = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("x^2 + 1"); // Parabola that never crosses x-axis
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Function that never crosses x-axis should have no roots
|
|
assert!(function.root_data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_extrema_and_roots_with_trig() {
|
|
let mut settings = app_settings_constructor(Riemann::Middle, -4.0, 4.0, 300, 300, -4.0, 4.0);
|
|
settings.do_extrema = true;
|
|
settings.do_roots = true;
|
|
|
|
let mut function = FunctionEntry::default();
|
|
function.update_string("sin(x)"); // Sine function has extrema at odd multiples of π/2
|
|
function.integral = false;
|
|
function.derivative = false;
|
|
|
|
function.calculate(true, true, false, settings);
|
|
|
|
// Sine function should have extrema in the given range
|
|
assert!(!function.extrema_data.is_empty());
|
|
|
|
// Should have multiple extrema (local max/min)
|
|
assert!(function.extrema_data.len() >= 2);
|
|
|
|
// Check that extrema are at approximately the right locations
|
|
// Local max at π/2 ≈ 1.57, local min at 3π/2 ≈ 4.71 (outside range)
|
|
// Local min at -π/2 ≈ -1.57, local max at -3π/2 ≈ -4.71 (outside range)
|
|
let extrema_x: Vec<f32> = function.extrema_data.iter().map(|p| p.x as f32).collect();
|
|
|
|
// Should have extrema near ±π/2
|
|
assert!(
|
|
extrema_x
|
|
.iter()
|
|
.any(|&x| emath::almost_equal(x, std::f32::consts::PI / 2.0, 0.1))
|
|
);
|
|
assert!(
|
|
extrema_x
|
|
.iter()
|
|
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI / 2.0, 0.1))
|
|
);
|
|
|
|
let roots_x: Vec<f32> = function.root_data.iter().map(|p| p.x as f32).collect();
|
|
|
|
assert!(
|
|
roots_x
|
|
.iter()
|
|
.any(|&x| emath::almost_equal(x, std::f32::consts::PI, 0.1))
|
|
);
|
|
assert!(
|
|
roots_x
|
|
.iter()
|
|
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI, 0.1))
|
|
);
|
|
|
|
assert!(roots_x.iter().any(|&x| emath::almost_equal(x, 0.0, 0.1)));
|
|
}
|