use std::fmt;
use std::ops::Mul;
use rand::Rng;
pub type Point = [f64; 2];
#[derive(Debug, Clone, PartialEq)]
pub enum IFSBuildError {
Empty,
MissingProbability,
InvalidProbabilities,
}
impl fmt::Display for IFSBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "IFS cannot be empty."),
Self::MissingProbability => {
write!(f, "All TMAs in an IFS must have a probability for stochastic generation.")
}
Self::InvalidProbabilities => {
write!(f, "Probability values must be finite and sum to a positive value.")
}
}
}
}
impl std::error::Error for IFSBuildError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TMA {
pub matrix: [[f64; 2]; 2],
pub vector: Point,
pub probability: Option<f64>,
}
impl TMA {
pub fn new(matrix: [[f64; 2]; 2], vector: Point) -> Self {
TMA {
matrix,
vector,
probability: None,
}
}
pub fn identity() -> Self {
TMA {
matrix: [[1.0, 0.0], [0.0, 1.0]],
vector: [0.0, 0.0],
probability: None,
}
}
pub fn from_scale(s: f64) -> Self {
TMA::new([[s, 0.0], [0.0, s]], [0.0, 0.0])
}
pub fn from_translation(tx: f64, ty: f64) -> Self {
TMA::new([[1.0, 0.0], [0.0, 1.0]], [tx, ty])
}
pub fn from_rotation(theta: f64) -> Self {
let (sin_t, cos_t) = theta.sin_cos();
TMA::new([[cos_t, -sin_t], [sin_t, cos_t]], [0.0, 0.0])
}
pub fn from_shear(xy: f64, yx: f64) -> Self {
TMA::new([[1.0, xy], [yx, 1.0]], [0.0, 0.0])
}
pub fn with_probability(mut self, p: f64) -> Self {
self.probability = Some(p);
self
}
pub fn apply(&self, p: Point) -> Point {
let x = p[0];
let y = p[1];
let new_x = self.matrix[0][0] * x + self.matrix[0][1] * y + self.vector[0];
let new_y = self.matrix[1][0] * x + self.matrix[1][1] * y + self.vector[1];
[new_x, new_y]
}
pub fn compose(&self, other: &TMA) -> Self {
let m1 = self.matrix;
let m2 = other.matrix;
let new_matrix = [
[
m1[0][0] * m2[0][0] + m1[0][1] * m2[1][0],
m1[0][0] * m2[0][1] + m1[0][1] * m2[1][1],
],
[
m1[1][0] * m2[0][0] + m1[1][1] * m2[1][0],
m1[1][0] * m2[0][1] + m1[1][1] * m2[1][1],
],
];
let c1 = self.vector;
let c2 = other.vector;
let new_vector = [
m1[0][0] * c2[0] + m1[0][1] * c2[1] + c1[0],
m1[1][0] * c2[0] + m1[1][1] * c2[1] + c1[1],
];
TMA::new(new_matrix, new_vector)
}
}
impl Mul<TMA> for TMA {
type Output = TMA;
fn mul(self, rhs: TMA) -> Self::Output {
self.compose(&rhs)
}
}
impl Mul<Point> for TMA {
type Output = Point;
fn mul(self, rhs: Point) -> Self::Output {
self.apply(rhs)
}
}
#[derive(Debug, PartialEq)]
pub struct IFS {
transformations: Vec<TMA>,
cumulative_probs: Vec<f64>,
}
impl IFS {
pub fn transformations(&self) -> &[TMA] {
&self.transformations
}
pub fn new(transformations: Vec<TMA>) -> Result<Self, IFSBuildError> {
if transformations.is_empty() {
return Err(IFSBuildError::Empty);
}
let mut probs = Vec::with_capacity(transformations.len());
let mut total_prob = 0.0;
for tma in &transformations {
let probability = tma
.probability
.ok_or(IFSBuildError::MissingProbability)?;
if !probability.is_finite() || probability < 0.0 {
return Err(IFSBuildError::InvalidProbabilities);
}
total_prob += probability;
probs.push(probability);
}
if !total_prob.is_finite() || total_prob <= 0.0 {
return Err(IFSBuildError::InvalidProbabilities);
}
let mut cumulative_probs = Vec::with_capacity(transformations.len());
let mut running_total = 0.0;
for probability in probs {
running_total += probability / total_prob;
cumulative_probs.push(running_total);
}
Ok(IFS {
transformations,
cumulative_probs,
})
}
pub fn cumulative_probabilities(&self) -> &[f64] {
&self.cumulative_probs
}
pub fn choose_index<R: Rng + ?Sized>(&self, rng: &mut R) -> usize {
let r = rng.gen_range(0.0..1.0);
self.cumulative_probs
.iter()
.position(|&cumulative_prob| r < cumulative_prob)
.unwrap_or_else(|| self.transformations.len().saturating_sub(1))
}
pub fn choose_transformation<R: Rng + ?Sized>(&self, rng: &mut R) -> &TMA {
let index = self.choose_index(rng);
&self.transformations[index]
}
pub fn choose_transformation_thread_rng(&self) -> &TMA {
let mut rng = rand::thread_rng();
self.choose_transformation(&mut rng)
}
pub fn run_chaos_game(
&self,
num_points: usize,
warmup_iterations: usize,
) -> Vec<(Point, usize)> {
let mut rng = rand::thread_rng();
self.run_chaos_game_with_rng(num_points, warmup_iterations, &mut rng)
}
pub fn run_chaos_game_with_rng<R: Rng + ?Sized>(
&self,
num_points: usize,
warmup_iterations: usize,
rng: &mut R,
) -> Vec<(Point, usize)> {
let mut points = Vec::with_capacity(num_points);
let mut current_point: Point = [0.0, 0.0];
let total_iterations = num_points + warmup_iterations;
for iteration in 0..total_iterations {
let chosen_index = self.choose_index(rng);
let tma = &self.transformations[chosen_index];
current_point = tma.apply(current_point);
if iteration >= warmup_iterations {
points.push((current_point, chosen_index));
}
}
points
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BranchNode {
pub point: Point,
pub depth: usize,
pub flow: f64,
pub capacity: f64,
pub parent: Option<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BranchEdge {
pub from: usize,
pub to: usize,
pub weight: f64,
pub capacity: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FlowSummaryEntry {
pub node_index: usize,
pub depth: usize,
pub flow: f64,
pub capacity: f64,
pub utilization: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BranchNetwork {
nodes: Vec<BranchNode>,
edges: Vec<BranchEdge>,
}
pub type BranchGraph = BranchNetwork;
impl BranchNetwork {
pub fn new(root_point: Point) -> Self {
Self {
nodes: vec![BranchNode {
point: root_point,
depth: 0,
flow: 1.0,
capacity: 1.0,
parent: None,
}],
edges: Vec::new(),
}
}
pub fn root(&self) -> &BranchNode {
&self.nodes[0]
}
pub fn root_index(&self) -> usize {
0
}
pub fn nodes(&self) -> &[BranchNode] {
&self.nodes
}
pub fn edges(&self) -> &[BranchEdge] {
&self.edges
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn max_depth(&self) -> usize {
self.nodes.iter().map(|node| node.depth).max().unwrap_or(0)
}
pub fn total_flow(&self) -> f64 {
self.nodes.iter().map(|node| node.flow).sum()
}
pub fn total_capacity(&self) -> f64 {
self.nodes.iter().map(|node| node.capacity).sum()
}
pub fn flow_summary(&self) -> Vec<FlowSummaryEntry> {
self.nodes
.iter()
.enumerate()
.map(|(index, node)| {
let utilization = if node.capacity > 0.0 {
node.flow / node.capacity
} else {
0.0
};
FlowSummaryEntry {
node_index: index,
depth: node.depth,
flow: node.flow,
capacity: node.capacity,
utilization,
}
})
.collect()
}
pub fn parent_of(&self, node_index: usize) -> Option<usize> {
self.nodes.get(node_index).and_then(|node| node.parent)
}
pub fn node_depth(&self, node_index: usize) -> Option<usize> {
self.nodes.get(node_index).map(|node| node.depth)
}
pub fn node_capacity(&self, node_index: usize) -> Option<f64> {
self.nodes.get(node_index).map(|node| node.capacity)
}
pub fn children_of(&self, node_index: usize) -> Vec<usize> {
self.edges
.iter()
.filter_map(|edge| (edge.from == node_index).then_some(edge.to))
.collect()
}
pub fn traverse_from(&self, start_index: usize) -> Vec<usize> {
if self.nodes.get(start_index).is_none() {
return Vec::new();
}
let mut order = Vec::new();
let mut visited = vec![false; self.nodes.len()];
let mut frontier = std::collections::VecDeque::from([start_index]);
visited[start_index] = true;
while let Some(index) = frontier.pop_front() {
order.push(index);
for child in self.children_of(index) {
if !visited[child] {
visited[child] = true;
frontier.push_back(child);
}
}
}
order
}
pub fn grow_from_ifs<R: Rng + ?Sized>(&mut self, ifs: &IFS, rng: &mut R, depth_limit: usize) {
let mut frontier = std::collections::VecDeque::from([0usize]);
while let Some(index) = frontier.pop_front() {
let current = self.nodes[index].clone();
if current.depth >= depth_limit {
continue;
}
for _ in 0..2 {
let chosen = ifs.choose_index(rng);
let transform = &ifs.transformations()[chosen];
let next_point = transform.apply(current.point);
let next_index = self.nodes.len();
let next_flow = current.flow * 0.9;
let next_capacity = current.capacity * 0.9 + current.flow * 0.1;
self.nodes.push(BranchNode {
point: next_point,
depth: current.depth + 1,
flow: next_flow,
capacity: next_capacity,
parent: Some(index),
});
self.edges.push(BranchEdge {
from: index,
to: next_index,
weight: current.flow,
capacity: next_capacity,
});
frontier.push_back(next_index);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_probability_preserves_weight_for_normalization() {
let weighted = TMA::identity().with_probability(2.0);
let negative = TMA::identity().with_probability(-0.1);
assert_eq!(weighted.probability, Some(2.0));
assert_eq!(negative.probability, Some(-0.1));
}
#[test]
fn composition_applies_transforms_in_order() {
let scale = TMA::from_scale(2.0);
let translate = TMA::from_translation(3.0, 4.0);
let composed = translate * scale;
let out = composed.apply([1.0, 2.0]);
assert_eq!(out, [5.0, 8.0]);
}
#[test]
fn ifs_normalizes_probabilities() {
let ifs = IFS::new(vec![
TMA::identity().with_probability(2.0),
TMA::identity().with_probability(1.0),
])
.expect("valid IFS should be created");
assert_eq!(ifs.cumulative_probabilities().len(), 2);
assert!((ifs.cumulative_probabilities()[0] - 0.666_666_666_666_666_6).abs() < 1e-12);
assert!((ifs.cumulative_probabilities()[1] - 1.0).abs() < 1e-12);
}
#[test]
fn ifs_rejects_negative_probabilities() {
let result = IFS::new(vec![
TMA::identity().with_probability(0.5),
TMA::identity().with_probability(-0.1),
]);
assert_eq!(result, Err(IFSBuildError::InvalidProbabilities));
}
#[test]
fn branch_network_tracks_recursive_growth() {
let ifs = IFS::new(vec![
TMA::from_translation(1.0, 0.0).with_probability(0.6),
TMA::from_translation(-1.0, 0.0).with_probability(0.4),
])
.expect("valid flow network");
let mut network = BranchNetwork::new([0.0, 0.0]);
network.grow_from_ifs(&ifs, &mut rand::thread_rng(), 2);
assert!(network.len() > 1);
assert!(network.root().depth == 0);
}
#[test]
fn branch_network_tracks_edges_between_parents_and_children() {
let ifs = IFS::new(vec![
TMA::from_translation(1.0, 0.0).with_probability(0.6),
TMA::from_translation(-1.0, 0.0).with_probability(0.4),
])
.expect("valid flow network");
let mut network = BranchNetwork::new([0.0, 0.0]);
network.grow_from_ifs(&ifs, &mut rand::thread_rng(), 1);
assert!(!network.edges().is_empty());
assert!(network.edges()[0].to > network.edges()[0].from);
assert!(network.nodes()[0].depth == 0);
}
#[test]
fn branch_network_reports_metrics() {
let ifs = IFS::new(vec![
TMA::from_translation(1.0, 0.0).with_probability(0.6),
TMA::from_translation(-1.0, 0.0).with_probability(0.4),
])
.expect("valid flow network");
let mut network = BranchNetwork::new([0.0, 0.0]);
network.grow_from_ifs(&ifs, &mut rand::thread_rng(), 1);
assert!(network.max_depth() >= 1);
assert!(network.total_flow() > 0.0);
assert!(network.children_of(0).len() >= 1);
}
#[test]
fn branch_network_supports_graph_traversal_and_capacity_metrics() {
let ifs = IFS::new(vec![
TMA::from_translation(1.0, 0.0).with_probability(0.6),
TMA::from_translation(-1.0, 0.0).with_probability(0.4),
])
.expect("valid flow network");
let mut network = BranchNetwork::new([0.0, 0.0]);
network.grow_from_ifs(&ifs, &mut rand::thread_rng(), 2);
let root_children = network.children_of(0);
assert!(!root_children.is_empty());
assert_eq!(network.parent_of(root_children[0]), Some(0));
assert!(network.node_depth(root_children[0]).is_some());
assert!(network.node_capacity(root_children[0]).is_some());
assert!(network.total_capacity() >= network.total_flow());
assert!(network.traverse_from(0).contains(&0));
assert!(network.traverse_from(0).len() >= root_children.len() + 1);
}
#[test]
fn branch_network_exposes_flow_summary_metrics() {
let ifs = IFS::new(vec![
TMA::from_translation(1.0, 0.0).with_probability(0.6),
TMA::from_translation(-1.0, 0.0).with_probability(0.4),
])
.expect("valid flow network");
let mut network = BranchNetwork::new([0.0, 0.0]);
network.grow_from_ifs(&ifs, &mut rand::thread_rng(), 2);
let summary = network.flow_summary();
assert!(!summary.is_empty());
assert!(summary[0].utilization >= 0.0);
assert!(summary.iter().all(|entry| entry.capacity >= entry.flow));
}
}