//! Thermograph constructed from scaffolds with support for subzero thermography use crate::{ display, drawing::svg::{self, ImmSvg, Svg}, numeric::{dyadic_rational_number::DyadicRationalNumber, rational::Rational}, short::partizan::trajectory::Trajectory, }; use ahash::{HashSet, HashSetExt}; use core::fmt; use std::{cmp::Ordering, fmt::Display, iter::once}; /// See [thermograph](self) header #[derive(Debug, Hash, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Thermograph { pub(crate) left_wall: Trajectory, pub(crate) right_wall: Trajectory, } impl Thermograph { /// Construct a thermograph with only a mast at given value pub fn with_mast(mast: Rational) -> Self { let t = Trajectory::new_constant(mast); Self { left_wall: t.clone(), right_wall: t, } } /// Get the temperature of the thermograph where both scaffolds merge into a mast #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_panics_doc))] pub fn temperature(&self) -> DyadicRationalNumber { let left = self.get_left_temperature(); let right = self.get_right_temperature(); assert!(self.left_wall.value_at(left) <= self.right_wall.value_at(right),); DyadicRationalNumber::from_rational(left.max(right)) .expect("unreachable: finite thermograph should give finite temperature") } fn get_left_temperature(&self) -> Rational { if self.left_wall.critical_points.is_empty() { Rational::from(-1) } else { self.left_wall.critical_points[0] } } fn get_right_temperature(&self) -> Rational { if self.right_wall.critical_points.is_empty() { Rational::from(-1) } else { self.right_wall.critical_points[0] } } /// Get the mast value of the thermograph pub fn get_mast(&self) -> Rational { let temperature = self.temperature().to_rational(); if self.left_wall == Trajectory::new_constant(Rational::PositiveInfinity) { if self.right_wall.slopes[0] == Rational::from(0) { self.right_wall.value_at(temperature) } else { Rational::PositiveInfinity } } else if self.right_wall == Trajectory::new_constant(Rational::NegativeInfinity) { if self.left_wall.slopes[0] == Rational::from(0) { self.left_wall.value_at(temperature) } else { Rational::NegativeInfinity } } else { self.left_wall.value_at(temperature) } } /// Calculate a thermograph given left and right scaffold. Note that scaffolds should be /// [tilted](Trajectory::tilt) before. #[cfg_attr( feature = "cargo-clippy", allow(clippy::cognitive_complexity, clippy::missing_panics_doc) )] pub fn thermographic_intersection( left_scaffold: Trajectory, right_scaffold: Trajectory, ) -> Self { if left_scaffold == Trajectory::new_constant(Rational::PositiveInfinity) || right_scaffold == Trajectory::new_constant(Rational::NegativeInfinity) { return Self { left_wall: left_scaffold, right_wall: right_scaffold, }; } let mut left_wall_cps: Vec = Vec::new(); let mut left_wall_slopes: Vec = Vec::new(); let mut left_wall_x_intercepts: Vec = Vec::new(); let mut right_wall_cps: Vec = Vec::new(); let mut right_wall_slopes: Vec = Vec::new(); let mut right_wall_x_intercepts: Vec = Vec::new(); let minus_one = Rational::from(-1); let zero = Rational::from(0); let ls_at_base: Rational = left_scaffold.value_at(minus_one); let rs_at_base: Rational = right_scaffold.value_at(minus_one); let mut previous_cave_value: Option; if ls_at_base < rs_at_base || (ls_at_base == rs_at_base && left_scaffold.slopes.last().unwrap() < right_scaffold.slopes.last().unwrap()) { // The left scaffold is smaller than the right scaffold immediately // above the base. So we start in a cave region. // The value of this cave region is 0 if 0 lies between the left // and right scaffolds at the base. Otherwise it's the value of // the scaffold that lies *closer* to 0 at the base. if ls_at_base > zero { previous_cave_value = Some(ls_at_base); } else if rs_at_base < zero { previous_cave_value = Some(rs_at_base); } else { previous_cave_value = Some(zero); } } else { // We start in a hill region. previous_cave_value = None; } // We work bottom-up and reverse the lists at the end. let mut next_cp_left = left_scaffold.critical_points.len() as i32 - 1; let mut next_cp_right = right_scaffold.critical_points.len() as i32 - 1; while next_cp_left >= -1 || next_cp_right >= -1 { // <0 for left, 0 for both, >0 for Right let current_cp_owner: i32; let current_cp: Rational; if next_cp_left == -1 && next_cp_right == -1 { // We've reached the end of the "real" critical points. Now we // need to consider infinity as an "artificial" critical point. current_cp_owner = 0; current_cp = Rational::PositiveInfinity; } else { if next_cp_left == -1 { current_cp_owner = 1; } else if next_cp_right == -1 { current_cp_owner = -1; } else { current_cp_owner = left_scaffold.critical_points[next_cp_left as usize] .cmp(&right_scaffold.critical_points[next_cp_right as usize]) as i32; } current_cp = if current_cp_owner <= 0 { left_scaffold.critical_points[next_cp_left as usize] } else { right_scaffold.critical_points[next_cp_right as usize] } } let now_in_hill_region: bool = matches!( left_scaffold.compare_to_at(&right_scaffold, current_cp), Ordering::Greater | Ordering::Equal ); if previous_cave_value.is_none() && !now_in_hill_region { // We were previously in a hill region, but just entered a cave region. // Extend the hill to the crossover point. let crossover_point = Trajectory::intersection_point( &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ); // debug_assert_eq!( // left_scaffold.value_at(&crossover_point), // right_scaffold.value_at(&crossover_point), // "Invalid crossover point" // ); Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, &crossover_point, &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], ); Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, &crossover_point, &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ); // Now add the cave mast. let cave_mast_slope: Rational; let cave_mast_intercept: Rational; if left_scaffold.value_at(current_cp) > left_scaffold.value_at(crossover_point) { // The left scaffold moves to the left above the crossover point. // The cave mast follows the left scaffold. cave_mast_slope = left_scaffold.slopes[(next_cp_left + 1) as usize]; cave_mast_intercept = left_scaffold.x_intercepts[(next_cp_left + 1) as usize]; previous_cave_value = Some(left_scaffold.value_at(current_cp)); } else if right_scaffold.value_at(current_cp) < right_scaffold.value_at(crossover_point) { // The right scaffold moves to the right above the crossover point. // The cave mast follows the right scaffold. cave_mast_slope = right_scaffold.slopes[(next_cp_right + 1) as usize]; cave_mast_intercept = right_scaffold.x_intercepts[(next_cp_right + 1) as usize]; previous_cave_value = Some(right_scaffold.value_at(current_cp)); } else { // Neither of the above. // The cave mast extends vertically above the crossover point. cave_mast_slope = Rational::from(0); cave_mast_intercept = left_scaffold.value_at(crossover_point); previous_cave_value = Some(cave_mast_intercept); } // Extend the trajectories according to the cave mast/intercept. Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, ¤t_cp, &cave_mast_slope, &cave_mast_intercept, ); Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, ¤t_cp, &cave_mast_slope, &cave_mast_intercept, ); } else if let Some(previous_cave_value_r) = &previous_cave_value { // We were previously in a cave region. There are three cases: // (i) The left scaffold moves to the left of the previous cave value, // (ii) The right scaffold moves to the right of the previous cave value, // (iii) The previous cave value remains between the left and right // scaffolds. // If both scaffolds move past the previous cave value, then we favor // case (i) or (ii) depending on which happens *first*. // First determine which crossing points exist and find their values. let left_scaffold_crossing_point = if &left_scaffold.value_at(current_cp) > previous_cave_value_r { Some( (previous_cave_value_r - left_scaffold.x_intercepts[(next_cp_left + 1) as usize]) / left_scaffold.slopes[(next_cp_left + 1) as usize], ) } else { None }; let right_scaffold_crossing_point = if &right_scaffold.value_at(current_cp) < previous_cave_value_r { Some( (previous_cave_value_r - right_scaffold.x_intercepts[(next_cp_right + 1) as usize]) / right_scaffold.slopes[(next_cp_right + 1) as usize], ) } else { None }; if left_scaffold_crossing_point.is_some() && (right_scaffold_crossing_point.is_none() || left_scaffold_crossing_point.as_ref().unwrap() <= right_scaffold_crossing_point.as_ref().unwrap()) { // We are in case (i). First add the truncated vertical mast. Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, left_scaffold_crossing_point.as_ref().unwrap(), &0.into(), previous_cave_value_r, ); Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, left_scaffold_crossing_point.as_ref().unwrap(), // it should be left &0.into(), previous_cave_value_r, ); // Now add the tilted mast for the left wall. (The left wall follows the left // scaffold up to currentCP even if the scaffolds enter a hill region.) Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, ¤t_cp, &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], ); // To handle the right wall we need to know whether we've re-entered a hill // region or not. let new_right_cp = if now_in_hill_region { Trajectory::intersection_point( &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ) } else { previous_cave_value = Some(left_scaffold.value_at(current_cp)); current_cp }; // Extend the right trajectory. Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, &new_right_cp, &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], ); } else if let Some(right_scaffold_crossing_point_r) = &right_scaffold_crossing_point { // We are in case (ii). First add the truncated vertical mast. Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, right_scaffold_crossing_point_r, // it should be right &Rational::from(0), previous_cave_value_r, ); Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, left_scaffold_crossing_point.as_ref().unwrap(), &Rational::from(0), previous_cave_value_r, ); // Now add the tilted mast for the right wall. (The right wall follows the right // scaffold up to currentCP even if the scaffolds enter a hill region.) Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, ¤t_cp, &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ); // To handle the left wall we need to know whether we've re-entered a hill // region or not. let new_left_cp = if now_in_hill_region { // A hill region is indeed re-entered. So the tilted mast for Left extends // just up to the scaffolds' next point of intersection. Trajectory::intersection_point( &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ) } else { previous_cave_value = Some(right_scaffold.value_at(current_cp)); current_cp }; Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, &new_left_cp, &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ); } else { // We are in case (iii). Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, ¤t_cp, &Rational::from(0), previous_cave_value_r, ); Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, ¤t_cp, &Rational::from(0), previous_cave_value_r, ); } } if now_in_hill_region { // We're in a hill region, so we need to add the critical point(s) for the hill, // regardless of what region we were in previously. if current_cp_owner <= 0 { Trajectory::extend_trajectory( true, &mut left_wall_cps, &mut left_wall_slopes, &mut left_wall_x_intercepts, ¤t_cp, &left_scaffold.slopes[(next_cp_left + 1) as usize], &left_scaffold.x_intercepts[(next_cp_left + 1) as usize], ); } if current_cp_owner >= 0 { Trajectory::extend_trajectory( true, &mut right_wall_cps, &mut right_wall_slopes, &mut right_wall_x_intercepts, ¤t_cp, &right_scaffold.slopes[(next_cp_right + 1) as usize], &right_scaffold.x_intercepts[(next_cp_right + 1) as usize], ); } previous_cave_value = None; } if current_cp_owner <= 0 { next_cp_left -= 1; } if current_cp_owner >= 0 { next_cp_right -= 1; } } // Now remove the "infinite" critical point from the end. left_wall_cps.pop(); left_wall_cps.reverse(); left_wall_slopes.reverse(); left_wall_x_intercepts.reverse(); let left_wall = Trajectory { critical_points: left_wall_cps, slopes: left_wall_slopes, x_intercepts: left_wall_x_intercepts, }; right_wall_cps.pop(); right_wall_cps.reverse(); right_wall_slopes.reverse(); right_wall_x_intercepts.reverse(); let right_wall = Trajectory { critical_points: right_wall_cps, slopes: right_wall_slopes, x_intercepts: right_wall_x_intercepts, }; Self { left_wall, right_wall, } } } impl Svg for Thermograph { fn to_svg(&self, buf: &mut W) -> fmt::Result where W: fmt::Write, { // Chosen arbitrarily, may be customizable in the future let svg_scale: u32 = 96; let mast_arrow_len = DyadicRationalNumber::from(2); let axis_weight = 1; let thermograph_line_weight = 3; let padding_x: u32 = 48; let padding_y: u32 = 16; let thermograph_x_min = self.right_wall.value_at(Rational::from(-1)); let thermograph_x_max = self.left_wall.value_at(Rational::from(-1)); let thermograph_y_min = -1; let thermograph_y_max = (self.temperature() + mast_arrow_len).ceil(); let thermograph_width = (thermograph_x_max.try_round().unwrap() - thermograph_x_min.try_round().unwrap()) as u32; let thermograph_height = (thermograph_y_max - thermograph_y_min) as u32; let svg_width = svg_scale * thermograph_width + (2 * padding_x); let svg_height = svg_scale * thermograph_height + (2 * padding_y); let translate_thermograph_helper = |value: Rational, min: Rational, total: u32, padding: u32| { let svg_value: Rational = value - min; let svg_value = (svg_value * Rational::from(svg_scale as i32)) .try_round() .unwrap() as i32; total as i32 - svg_value - padding as i32 }; let translate_thermograph_horizontal = |thermograph_x| { translate_thermograph_helper(thermograph_x, thermograph_x_min, svg_width, padding_x) }; let translate_thermograph_vertical = |thermograph_y| { translate_thermograph_helper( thermograph_y, Rational::from(thermograph_y_min), svg_height, padding_y, ) }; let draw_scaffold = |w: &mut W, labeled_points: &mut HashSet<(i32, i32)>, trajectory: &Trajectory| -> fmt::Result { let mut previous = None; let y_points = once(self.temperature().to_rational() + mast_arrow_len.to_rational()) .chain(trajectory.critical_points.iter().copied()); for point_y in y_points { let point_x = trajectory.value_at(point_y); let image_x = translate_thermograph_horizontal(point_x); let image_y = translate_thermograph_vertical(point_y); if labeled_points.insert((image_x, image_y)) { // TODO: Make it less ugly, maybe move values to axis rather than having them on // critical points ImmSvg::text( w, &svg::Text { x: image_x, y: image_y, text: format!("({}, {})", point_x, point_y), text_anchor: svg::TextAnchor::Middle, ..svg::Text::default() }, )?; } if let Some((previous_x, previous_y)) = previous { ImmSvg::line( w, previous_x, previous_y, image_x, image_y, thermograph_line_weight, )?; } previous = Some((image_x, image_y)); } Ok(()) }; ImmSvg::new(buf, svg_width, svg_height, |buf| { ImmSvg::g(buf, "black", |buf| { let horizontal_axis_y = translate_thermograph_vertical(Rational::from(0)); ImmSvg::line( buf, 0, horizontal_axis_y, svg_width as i32, horizontal_axis_y, axis_weight, )?; let vertical_axis_x = translate_thermograph_horizontal(Rational::from(0)); ImmSvg::line( buf, vertical_axis_x, 0, vertical_axis_x, svg_height as i32, axis_weight, )?; let mut labeled_points = HashSet::new(); draw_scaffold(buf, &mut labeled_points, &self.left_wall)?; draw_scaffold(buf, &mut labeled_points, &self.right_wall)?; Ok(()) }) }) } } impl Display for Thermograph { /// Follows cgsuite format fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Thermograph")?; display::parens(f, |f| write!(f, "{}, {}", self.left_wall, self.right_wall)) } }