Initial commit
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2024 RustyTorch++ Team
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
//! Mesh partitioning for parallel processing.
|
||||
|
||||
pub mod partitioning_algorithms;
|
||||
pub mod partitioning_types;
|
||||
|
||||
pub use partitioning_algorithms::*;
|
||||
pub use partitioning_types::*;
|
||||
|
||||
#[cfg(disabled)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mesh::{MaterialId, geometry::Rectangle};
|
||||
|
||||
#[test]
|
||||
fn test_single_partition() {
|
||||
let rect = Rectangle::new(1.0, 1.0);
|
||||
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 1, PartitioningStrategy::Coordinate)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(partitions.len(), 1);
|
||||
assert_eq!(partitions[0].element_count(), 4);
|
||||
assert_eq!(partitions[0].node_count(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coordinate_partitioning() {
|
||||
let rect = Rectangle::new(2.0, 1.0);
|
||||
let mesh = rect.generate_quad_mesh(4, 2, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(partitions.len(), 2);
|
||||
// Each partition should have roughly half the elements
|
||||
assert!(partitions[0].element_count() > 0);
|
||||
assert!(partitions[1].element_count() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_balance() {
|
||||
let rect = Rectangle::new(3.0, 3.0);
|
||||
let mesh = rect.generate_quad_mesh(6, 6, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 4, PartitioningStrategy::LoadBalanced)
|
||||
.unwrap();
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&partitions);
|
||||
assert!(stats.is_well_balanced());
|
||||
assert!(stats.load_imbalance < 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boundary_node_detection() {
|
||||
let rect = Rectangle::new(1.0, 1.0);
|
||||
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
|
||||
.unwrap();
|
||||
|
||||
// Check that boundary nodes are correctly identified
|
||||
for partition in &partitions {
|
||||
assert!(!partition.boundary_nodes.is_empty());
|
||||
assert!(!partition.internal_nodes.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_statistics() {
|
||||
let rect = Rectangle::new(2.0, 2.0);
|
||||
let mesh = rect.generate_quad_mesh(4, 4, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 4, PartitioningStrategy::Graph)
|
||||
.unwrap();
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&partitions);
|
||||
assert_eq!(stats.num_partitions, 4);
|
||||
assert_eq!(stats.total_elements, 16);
|
||||
assert!(stats.efficiency() > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_element_per_partition() {
|
||||
let rect = Rectangle::new(0.5, 0.5);
|
||||
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
// Request more partitions than elements
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 8, PartitioningStrategy::LoadBalanced)
|
||||
.unwrap();
|
||||
|
||||
// Should create only as many partitions as there are elements
|
||||
assert_eq!(partitions.len(), 4);
|
||||
for partition in &partitions {
|
||||
assert_eq!(partition.element_count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_direction_auto() {
|
||||
// Create a mesh that's wider than tall
|
||||
let rect = Rectangle::new(4.0, 1.0);
|
||||
let mesh = rect.generate_quad_mesh(8, 2, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
partitioner.set_direction(PartitionDirection::Auto);
|
||||
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
|
||||
.unwrap();
|
||||
|
||||
// Auto should choose X direction for this wide mesh
|
||||
assert_eq!(partitions.len(), 2);
|
||||
// Elements should be divided roughly equally
|
||||
let diff =
|
||||
(partitions[0].element_count() as i32 - partitions[1].element_count() as i32).abs();
|
||||
assert!(diff <= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interface_elements() {
|
||||
let rect = Rectangle::new(1.0, 1.0);
|
||||
let mesh = rect.generate_quad_mesh(3, 3, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 2, PartitioningStrategy::Graph)
|
||||
.unwrap();
|
||||
|
||||
// Check that interface elements are correctly identified
|
||||
let total_interface: usize = partitions.iter().map(|p| p.interface_elements.len()).sum();
|
||||
assert!(total_interface > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbor_detection() {
|
||||
let rect = Rectangle::new(2.0, 2.0);
|
||||
let mesh = rect.generate_quad_mesh(4, 4, MaterialId(0)).unwrap();
|
||||
|
||||
let partitioner = MeshPartitioner::new();
|
||||
let partitions = partitioner
|
||||
.partition(&mesh, 4, PartitioningStrategy::Coordinate)
|
||||
.unwrap();
|
||||
|
||||
// Each partition should have neighbors
|
||||
for partition in &partitions {
|
||||
if partitions.len() > 1 {
|
||||
assert!(!partition.neighbors.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
// Copyright (c) 2024 RustyTorch++ Team
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
//! Partitioning algorithms implementation.
|
||||
|
||||
use super::partitioning_types::{
|
||||
MeshPartition, PartitionDirection, PartitioningStats, PartitioningStrategy,
|
||||
};
|
||||
use crate::error::{FeaResult, MeshError};
|
||||
use crate::mesh::{Element, ElementId, ElementType, Mesh, NodeId, connectivity::ConnectivityInfo};
|
||||
use indexmap::IndexSet;
|
||||
use nalgebra::Vector3;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Result of mesh partitioning.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartitioningResult {
|
||||
pub partitions: Vec<MeshPartition>,
|
||||
pub stats: PartitioningStats,
|
||||
}
|
||||
|
||||
/// Mesh partitioning algorithms.
|
||||
pub struct MeshPartitioner;
|
||||
|
||||
impl MeshPartitioner {
|
||||
/// Partition mesh using specified strategy.
|
||||
pub fn partition(
|
||||
mesh: &Mesh,
|
||||
num_partitions: usize,
|
||||
strategy: PartitioningStrategy,
|
||||
) -> FeaResult<PartitioningResult> {
|
||||
if num_partitions == 0 {
|
||||
return Err(MeshError::PartitioningFailed {
|
||||
reason: "Number of partitions must be positive".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
if num_partitions == 1 {
|
||||
return Self::create_single_partition(mesh);
|
||||
}
|
||||
|
||||
match strategy {
|
||||
PartitioningStrategy::Coordinate => {
|
||||
Self::coordinate_partitioning(mesh, num_partitions, PartitionDirection::Auto)
|
||||
}
|
||||
PartitioningStrategy::Graph => Self::graph_partitioning(mesh, num_partitions),
|
||||
PartitioningStrategy::LoadBalanced => {
|
||||
Self::load_balanced_partitioning(mesh, num_partitions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create single partition containing entire mesh.
|
||||
fn create_single_partition(mesh: &Mesh) -> FeaResult<PartitioningResult> {
|
||||
let mut partition = MeshPartition::new(0);
|
||||
|
||||
// Add all elements
|
||||
for &element_id in mesh.elements.keys() {
|
||||
partition.add_element(element_id);
|
||||
}
|
||||
|
||||
// Add all nodes as internal nodes
|
||||
for &node_id in mesh.nodes.keys() {
|
||||
partition.add_internal_node(node_id);
|
||||
}
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&[partition.clone()]);
|
||||
|
||||
Ok(PartitioningResult {
|
||||
partitions: vec![partition],
|
||||
stats,
|
||||
})
|
||||
}
|
||||
|
||||
/// Coordinate-based partitioning along specified direction.
|
||||
pub fn coordinate_partitioning(
|
||||
mesh: &Mesh,
|
||||
num_partitions: usize,
|
||||
direction: PartitionDirection,
|
||||
) -> FeaResult<PartitioningResult> {
|
||||
// Determine partitioning direction
|
||||
let direction = match direction {
|
||||
PartitionDirection::Auto => Self::determine_best_direction(mesh)?,
|
||||
_ => direction,
|
||||
};
|
||||
|
||||
// Get element centroids
|
||||
let centroids = Self::compute_element_centroids(mesh)?;
|
||||
|
||||
// Sort elements by centroid coordinate along partitioning direction
|
||||
let mut element_coords: Vec<(ElementId, f64)> = centroids
|
||||
.iter()
|
||||
.map(|(&elem_id, centroid)| {
|
||||
let coord = match direction {
|
||||
PartitionDirection::X | PartitionDirection::Auto => centroid.x,
|
||||
PartitionDirection::Y => centroid.y,
|
||||
PartitionDirection::Z => centroid.z,
|
||||
};
|
||||
(elem_id, coord)
|
||||
})
|
||||
.collect();
|
||||
|
||||
element_coords.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
|
||||
// Distribute elements to partitions
|
||||
let elements_per_partition = mesh.num_elements() / num_partitions;
|
||||
let mut partitions = Vec::new();
|
||||
|
||||
for partition_id in 0..num_partitions {
|
||||
let mut partition = MeshPartition::new(partition_id);
|
||||
|
||||
let start_idx = partition_id * elements_per_partition;
|
||||
let end_idx = if partition_id == num_partitions - 1 {
|
||||
mesh.num_elements()
|
||||
} else {
|
||||
(partition_id + 1) * elements_per_partition
|
||||
};
|
||||
|
||||
for idx in start_idx..end_idx {
|
||||
if idx < element_coords.len() {
|
||||
partition.add_element(element_coords[idx].0);
|
||||
}
|
||||
}
|
||||
|
||||
partitions.push(partition);
|
||||
}
|
||||
|
||||
// Update node information
|
||||
Self::update_node_information(mesh, &mut partitions)?;
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&partitions);
|
||||
|
||||
Ok(PartitioningResult { partitions, stats })
|
||||
}
|
||||
|
||||
/// Graph-based partitioning using connectivity information.
|
||||
fn graph_partitioning(mesh: &Mesh, num_partitions: usize) -> FeaResult<PartitioningResult> {
|
||||
// Build connectivity graph
|
||||
let connectivity = ConnectivityInfo::build(&mesh.elements)?;
|
||||
|
||||
// Use multilevel graph partitioning approach
|
||||
let partitions = Self::multilevel_partition(mesh, &connectivity, num_partitions)?;
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&partitions);
|
||||
|
||||
Ok(PartitioningResult { partitions, stats })
|
||||
}
|
||||
|
||||
/// Load-balanced partitioning considering element computational costs.
|
||||
fn load_balanced_partitioning(
|
||||
mesh: &Mesh,
|
||||
num_partitions: usize,
|
||||
) -> FeaResult<PartitioningResult> {
|
||||
// Compute element weights based on type and quality
|
||||
let weights = Self::compute_element_weights(mesh)?;
|
||||
|
||||
// Sort elements by weight
|
||||
let mut weighted_elements: Vec<(ElementId, f64)> = weights.into_iter().collect();
|
||||
weighted_elements.sort_by(|a, b| b.1.total_cmp(&a.1));
|
||||
|
||||
// Distribute using bin packing algorithm
|
||||
let mut partitions = Vec::new();
|
||||
let mut partition_weights: Vec<f64> = vec![0.0; num_partitions];
|
||||
|
||||
for _ in 0..num_partitions {
|
||||
partitions.push(MeshPartition::new(partitions.len()));
|
||||
}
|
||||
|
||||
// Greedily assign elements to least loaded partition
|
||||
for (element_id, weight) in weighted_elements {
|
||||
let min_partition = partition_weights
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|a, b| (*a.1).total_cmp(b.1))
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap();
|
||||
|
||||
partitions[min_partition].add_element(element_id);
|
||||
partition_weights[min_partition] += weight;
|
||||
}
|
||||
|
||||
// Update node information
|
||||
Self::update_node_information(mesh, &mut partitions)?;
|
||||
|
||||
let stats = PartitioningStats::from_partitions(&partitions);
|
||||
|
||||
Ok(PartitioningResult { partitions, stats })
|
||||
}
|
||||
|
||||
/// Multilevel graph partitioning.
|
||||
fn multilevel_partition(
|
||||
mesh: &Mesh,
|
||||
connectivity: &ConnectivityInfo,
|
||||
num_partitions: usize,
|
||||
) -> FeaResult<Vec<MeshPartition>> {
|
||||
// Simple recursive bisection for now
|
||||
let element_ids: Vec<ElementId> = mesh.elements.keys().copied().collect();
|
||||
let mut partition_map = HashMap::new();
|
||||
|
||||
Self::recursive_bisection(
|
||||
mesh,
|
||||
connectivity,
|
||||
&element_ids,
|
||||
num_partitions,
|
||||
0,
|
||||
&mut partition_map,
|
||||
)?;
|
||||
|
||||
// Convert partition map to MeshPartition structures
|
||||
let mut partitions = Vec::new();
|
||||
for partition_id in 0..num_partitions {
|
||||
partitions.push(MeshPartition::new(partition_id));
|
||||
}
|
||||
|
||||
for (&element_id, &partition_id) in &partition_map {
|
||||
partitions[partition_id].add_element(element_id);
|
||||
}
|
||||
|
||||
Self::update_node_information(mesh, &mut partitions)?;
|
||||
|
||||
Ok(partitions)
|
||||
}
|
||||
|
||||
/// Recursive bisection for graph partitioning.
|
||||
fn recursive_bisection(
|
||||
mesh: &Mesh,
|
||||
connectivity: &ConnectivityInfo,
|
||||
elements: &[ElementId],
|
||||
target_partitions: usize,
|
||||
base_partition: usize,
|
||||
partition_map: &mut HashMap<ElementId, usize>,
|
||||
) -> FeaResult<()> {
|
||||
if target_partitions == 1 || elements.is_empty() {
|
||||
// Assign all elements to current partition
|
||||
for &element_id in elements {
|
||||
partition_map.insert(element_id, base_partition);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Bisect elements
|
||||
let (left_elements, right_elements) = Self::bisect_elements(mesh, connectivity, elements)?;
|
||||
|
||||
// Recursively partition each half
|
||||
let left_partitions = target_partitions / 2;
|
||||
let right_partitions = target_partitions - left_partitions;
|
||||
|
||||
Self::recursive_bisection(
|
||||
mesh,
|
||||
connectivity,
|
||||
&left_elements,
|
||||
left_partitions,
|
||||
base_partition,
|
||||
partition_map,
|
||||
)?;
|
||||
|
||||
Self::recursive_bisection(
|
||||
mesh,
|
||||
connectivity,
|
||||
&right_elements,
|
||||
right_partitions,
|
||||
base_partition + left_partitions,
|
||||
partition_map,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bisect a set of elements into two balanced sets.
|
||||
fn bisect_elements(
|
||||
mesh: &Mesh,
|
||||
_connectivity: &ConnectivityInfo,
|
||||
elements: &[ElementId],
|
||||
) -> FeaResult<(Vec<ElementId>, Vec<ElementId>)> {
|
||||
// Simple coordinate-based bisection
|
||||
let centroids = Self::compute_element_centroids_subset(mesh, elements)?;
|
||||
|
||||
// Find principal direction (largest extent)
|
||||
let mut min_coords = Vector3::new(f64::MAX, f64::MAX, f64::MAX);
|
||||
let mut max_coords = Vector3::new(f64::MIN, f64::MIN, f64::MIN);
|
||||
|
||||
for centroid in centroids.values() {
|
||||
min_coords.x = min_coords.x.min(centroid.x);
|
||||
min_coords.y = min_coords.y.min(centroid.y);
|
||||
min_coords.z = min_coords.z.min(centroid.z);
|
||||
max_coords.x = max_coords.x.max(centroid.x);
|
||||
max_coords.y = max_coords.y.max(centroid.y);
|
||||
max_coords.z = max_coords.z.max(centroid.z);
|
||||
}
|
||||
|
||||
let extents = max_coords - min_coords;
|
||||
let direction = if extents.x >= extents.y && extents.x >= extents.z {
|
||||
PartitionDirection::X
|
||||
} else if extents.y >= extents.z {
|
||||
PartitionDirection::Y
|
||||
} else {
|
||||
PartitionDirection::Z
|
||||
};
|
||||
|
||||
// Sort elements by coordinate along principal direction
|
||||
let mut sorted_elements: Vec<(ElementId, f64)> = elements
|
||||
.iter()
|
||||
.map(|&elem_id| {
|
||||
let centroid = ¢roids[&elem_id];
|
||||
let coord = match direction {
|
||||
PartitionDirection::X | PartitionDirection::Auto => centroid.x,
|
||||
PartitionDirection::Y => centroid.y,
|
||||
PartitionDirection::Z => centroid.z,
|
||||
};
|
||||
(elem_id, coord)
|
||||
})
|
||||
.collect();
|
||||
|
||||
sorted_elements.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
|
||||
// Split at median
|
||||
let mid = sorted_elements.len() / 2;
|
||||
let left_elements: Vec<ElementId> =
|
||||
sorted_elements[..mid].iter().map(|(id, _)| *id).collect();
|
||||
let right_elements: Vec<ElementId> =
|
||||
sorted_elements[mid..].iter().map(|(id, _)| *id).collect();
|
||||
|
||||
Ok((left_elements, right_elements))
|
||||
}
|
||||
|
||||
/// Determine best partitioning direction based on mesh geometry.
|
||||
fn determine_best_direction(mesh: &Mesh) -> FeaResult<PartitionDirection> {
|
||||
let mut min_coords = Vector3::new(f64::MAX, f64::MAX, f64::MAX);
|
||||
let mut max_coords = Vector3::new(f64::MIN, f64::MIN, f64::MIN);
|
||||
|
||||
for node in mesh.nodes.values() {
|
||||
let pos = node.position();
|
||||
min_coords.x = min_coords.x.min(pos.x);
|
||||
min_coords.y = min_coords.y.min(pos.y);
|
||||
min_coords.z = min_coords.z.min(pos.z);
|
||||
max_coords.x = max_coords.x.max(pos.x);
|
||||
max_coords.y = max_coords.y.max(pos.y);
|
||||
max_coords.z = max_coords.z.max(pos.z);
|
||||
}
|
||||
|
||||
let extents = max_coords - min_coords;
|
||||
|
||||
// Choose direction with largest extent
|
||||
if extents.x >= extents.y && extents.x >= extents.z {
|
||||
Ok(PartitionDirection::X)
|
||||
} else if extents.y >= extents.z {
|
||||
Ok(PartitionDirection::Y)
|
||||
} else {
|
||||
Ok(PartitionDirection::Z)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute element centroids.
|
||||
fn compute_element_centroids(mesh: &Mesh) -> FeaResult<HashMap<ElementId, Vector3<f64>>> {
|
||||
let mut centroids = HashMap::new();
|
||||
|
||||
for (&element_id, element) in &mesh.elements {
|
||||
let centroid = Self::compute_element_centroid(mesh, element)?;
|
||||
centroids.insert(element_id, centroid);
|
||||
}
|
||||
|
||||
Ok(centroids)
|
||||
}
|
||||
|
||||
/// Compute element centroids for a subset.
|
||||
fn compute_element_centroids_subset(
|
||||
mesh: &Mesh,
|
||||
elements: &[ElementId],
|
||||
) -> FeaResult<HashMap<ElementId, Vector3<f64>>> {
|
||||
let mut centroids = HashMap::new();
|
||||
|
||||
for &element_id in elements {
|
||||
if let Some(element) = mesh.get_element(element_id) {
|
||||
let centroid = Self::compute_element_centroid(mesh, element)?;
|
||||
centroids.insert(element_id, centroid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(centroids)
|
||||
}
|
||||
|
||||
/// Compute centroid of a single element.
|
||||
fn compute_element_centroid(mesh: &Mesh, element: &Element) -> FeaResult<Vector3<f64>> {
|
||||
let mut centroid = Vector3::zeros();
|
||||
let mut count = 0;
|
||||
|
||||
for &node_id in &element.nodes {
|
||||
if let Some(node) = mesh.get_node(node_id) {
|
||||
centroid += node.position();
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return Err(MeshError::InvalidElement("Element has no valid nodes".to_string()).into());
|
||||
}
|
||||
|
||||
Ok(centroid / count as f64)
|
||||
}
|
||||
|
||||
/// Compute element weights based on type and quality.
|
||||
fn compute_element_weights(mesh: &Mesh) -> FeaResult<HashMap<ElementId, f64>> {
|
||||
let mut weights = HashMap::new();
|
||||
|
||||
for (&element_id, element) in &mesh.elements {
|
||||
// Base weight depends on element type complexity
|
||||
let base_weight = match element.element_type {
|
||||
ElementType::Tri3 => 1.0,
|
||||
ElementType::Tri6 => 2.0,
|
||||
ElementType::Quad4 => 1.5,
|
||||
ElementType::Quad8 | ElementType::Quad9 => 3.0,
|
||||
ElementType::Tet4 => 2.0,
|
||||
ElementType::Tet10 => 4.0,
|
||||
ElementType::Hex8 => 3.0,
|
||||
ElementType::Hex20 | ElementType::Hex27 => 6.0,
|
||||
ElementType::Wedge6 => 2.5,
|
||||
ElementType::Wedge15 => 5.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
// Additional weight for high-aspect-ratio elements (more expensive to compute)
|
||||
let quality_factor = Self::estimate_element_quality(mesh, element)?;
|
||||
let total_weight = base_weight * (1.0 + (1.0 / quality_factor.max(0.1)));
|
||||
|
||||
weights.insert(element_id, total_weight);
|
||||
}
|
||||
|
||||
Ok(weights)
|
||||
}
|
||||
|
||||
/// Update node information for partitions.
|
||||
fn update_node_information(mesh: &Mesh, partitions: &mut [MeshPartition]) -> FeaResult<()> {
|
||||
// Collect nodes for each partition
|
||||
for partition in partitions.iter_mut() {
|
||||
let mut all_nodes = IndexSet::new();
|
||||
|
||||
for &element_id in &partition.elements {
|
||||
if let Some(element) = mesh.get_element(element_id) {
|
||||
for &node_id in &element.nodes {
|
||||
all_nodes.insert(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partition.nodes = all_nodes;
|
||||
}
|
||||
|
||||
// Identify boundary nodes and partition neighbors
|
||||
let mut node_to_partitions: HashMap<NodeId, IndexSet<usize>> = HashMap::new();
|
||||
|
||||
for (partition_id, partition) in partitions.iter().enumerate() {
|
||||
for &node_id in &partition.nodes {
|
||||
node_to_partitions
|
||||
.entry(node_id)
|
||||
.or_default()
|
||||
.insert(partition_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Update partition boundary/internal nodes and neighbors
|
||||
for partition in partitions.iter_mut() {
|
||||
for &node_id in &partition.nodes {
|
||||
let sharing_partitions = &node_to_partitions[&node_id];
|
||||
|
||||
if sharing_partitions.len() == 1 {
|
||||
partition.internal_nodes.insert(node_id);
|
||||
} else {
|
||||
partition.boundary_nodes.insert(node_id);
|
||||
|
||||
// Add other partitions as neighbors
|
||||
for &other_partition in sharing_partitions {
|
||||
if other_partition != partition.id {
|
||||
partition.neighbors.insert(other_partition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn estimate_element_quality(mesh: &Mesh, element: &Element) -> FeaResult<f64> {
|
||||
// Simple quality estimate based on aspect ratio
|
||||
let node_coords: Vec<Vector3<f64>> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|&node_id| {
|
||||
mesh.nodes
|
||||
.get(&node_id)
|
||||
.map(|node| {
|
||||
// Convert DVector to Vector3
|
||||
if node.coordinates.len() >= 3 {
|
||||
Vector3::new(
|
||||
node.coordinates[0],
|
||||
node.coordinates[1],
|
||||
node.coordinates[2],
|
||||
)
|
||||
} else if node.coordinates.len() == 2 {
|
||||
Vector3::new(node.coordinates[0], node.coordinates[1], 0.0)
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
}
|
||||
})
|
||||
.ok_or(MeshError::NodeNotFound { node_id: node_id.0 })
|
||||
})
|
||||
.collect::<Result<Vec<Vector3<f64>>, _>>()?;
|
||||
|
||||
match element.element_type {
|
||||
ElementType::Tri3 => Self::triangle_quality(&node_coords),
|
||||
ElementType::Quad4 => Self::quadrilateral_quality(&node_coords),
|
||||
ElementType::Tet4 => Self::tetrahedron_quality(&node_coords),
|
||||
ElementType::Hex8 => Self::hexahedron_quality(&node_coords),
|
||||
_ => Ok(1.0), // Default quality
|
||||
}
|
||||
}
|
||||
|
||||
fn triangle_quality(coords: &[Vector3<f64>]) -> FeaResult<f64> {
|
||||
if coords.len() < 3 {
|
||||
return Ok(0.1);
|
||||
}
|
||||
|
||||
let a = (coords[1] - coords[0]).magnitude();
|
||||
let b = (coords[2] - coords[1]).magnitude();
|
||||
let c = (coords[0] - coords[2]).magnitude();
|
||||
|
||||
// Quality based on aspect ratio
|
||||
let s = (a + b + c) / 2.0;
|
||||
let area = (s * (s - a) * (s - b) * (s - c)).sqrt();
|
||||
|
||||
if area > 0.0 {
|
||||
let quality = (4.0 * (3.0_f64).sqrt() * area) / (a * a + b * b + c * c);
|
||||
Ok(quality.max(0.1).min(1.0))
|
||||
} else {
|
||||
Ok(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn quadrilateral_quality(coords: &[Vector3<f64>]) -> FeaResult<f64> {
|
||||
if coords.len() < 4 {
|
||||
return Ok(0.1);
|
||||
}
|
||||
|
||||
// Compute diagonals
|
||||
let diag1 = (coords[2] - coords[0]).magnitude();
|
||||
let diag2 = (coords[3] - coords[1]).magnitude();
|
||||
|
||||
// Quality based on diagonal ratio
|
||||
let quality = diag1.min(diag2) / diag1.max(diag2);
|
||||
Ok(quality.max(0.1).min(1.0))
|
||||
}
|
||||
|
||||
fn tetrahedron_quality(coords: &[Vector3<f64>]) -> FeaResult<f64> {
|
||||
if coords.len() < 4 {
|
||||
return Ok(0.1);
|
||||
}
|
||||
|
||||
// Volume-based quality metric
|
||||
let v1 = coords[1] - coords[0];
|
||||
let v2 = coords[2] - coords[0];
|
||||
let v3 = coords[3] - coords[0];
|
||||
|
||||
let volume = v1.cross(&v2).dot(&v3).abs() / 6.0;
|
||||
|
||||
if volume > 0.0 {
|
||||
// Compute surface area
|
||||
let area1 = v1.cross(&v2).magnitude() / 2.0;
|
||||
let area2 = v1.cross(&v3).magnitude() / 2.0;
|
||||
let area3 = v2.cross(&v3).magnitude() / 2.0;
|
||||
let area4 = (coords[2] - coords[1])
|
||||
.cross(&(coords[3] - coords[1]))
|
||||
.magnitude()
|
||||
/ 2.0;
|
||||
|
||||
let surface_area = area1 + area2 + area3 + area4;
|
||||
let quality =
|
||||
(36.0 * std::f64::consts::PI * volume * volume).powf(1.0 / 3.0) / surface_area;
|
||||
Ok(quality.max(0.1).min(1.0))
|
||||
} else {
|
||||
Ok(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn hexahedron_quality(coords: &[Vector3<f64>]) -> FeaResult<f64> {
|
||||
if coords.len() < 8 {
|
||||
return Ok(0.1);
|
||||
}
|
||||
|
||||
// Simplified quality based on edge length ratios
|
||||
let mut min_edge = f64::MAX;
|
||||
let mut max_edge = f64::MIN;
|
||||
|
||||
// Check edges
|
||||
let edges = [
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 0),
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 4),
|
||||
(0, 4),
|
||||
(1, 5),
|
||||
(2, 6),
|
||||
(3, 7),
|
||||
];
|
||||
|
||||
for (i, j) in &edges {
|
||||
let length = (coords[*j] - coords[*i]).magnitude();
|
||||
min_edge = min_edge.min(length);
|
||||
max_edge = max_edge.max(length);
|
||||
}
|
||||
|
||||
let quality = if max_edge > 0.0 {
|
||||
min_edge / max_edge
|
||||
} else {
|
||||
0.1
|
||||
};
|
||||
|
||||
Ok(quality.max(0.1).min(1.0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2024 RustyTorch++ Team
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
//! Type definitions for mesh partitioning.
|
||||
|
||||
use crate::mesh::{ElementId, NodeId};
|
||||
use indexmap::IndexSet;
|
||||
|
||||
/// Partitioning strategy for parallel processing.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PartitioningStrategy {
|
||||
/// Simple coordinate-based partitioning
|
||||
Coordinate,
|
||||
/// Graph-based partitioning using connectivity
|
||||
Graph,
|
||||
/// Load-balanced partitioning
|
||||
LoadBalanced,
|
||||
}
|
||||
|
||||
/// Partitioning direction for coordinate-based methods.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PartitionDirection {
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Auto, // Choose based on mesh geometry
|
||||
}
|
||||
|
||||
/// Mesh partition containing elements and associated data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MeshPartition {
|
||||
/// Partition ID
|
||||
pub id: usize,
|
||||
/// Elements in this partition
|
||||
pub elements: IndexSet<ElementId>,
|
||||
/// Nodes in this partition (including boundary nodes)
|
||||
pub nodes: IndexSet<NodeId>,
|
||||
/// Internal nodes (owned by this partition only)
|
||||
pub internal_nodes: IndexSet<NodeId>,
|
||||
/// Boundary nodes (shared with other partitions)
|
||||
pub boundary_nodes: IndexSet<NodeId>,
|
||||
/// Neighboring partitions
|
||||
pub neighbors: IndexSet<usize>,
|
||||
/// Interface elements (shared with other partitions)
|
||||
pub interface_elements: IndexSet<ElementId>,
|
||||
}
|
||||
|
||||
impl MeshPartition {
|
||||
/// Create a new empty partition.
|
||||
pub fn new(id: usize) -> Self {
|
||||
Self {
|
||||
id,
|
||||
elements: IndexSet::new(),
|
||||
nodes: IndexSet::new(),
|
||||
internal_nodes: IndexSet::new(),
|
||||
boundary_nodes: IndexSet::new(),
|
||||
neighbors: IndexSet::new(),
|
||||
interface_elements: IndexSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an element to the partition.
|
||||
pub fn add_element(&mut self, element_id: ElementId) {
|
||||
self.elements.insert(element_id);
|
||||
}
|
||||
|
||||
/// Add a node to the partition.
|
||||
pub fn add_node(&mut self, node_id: NodeId) {
|
||||
self.nodes.insert(node_id);
|
||||
}
|
||||
|
||||
/// Add an internal node (owned only by this partition).
|
||||
pub fn add_internal_node(&mut self, node_id: NodeId) {
|
||||
self.internal_nodes.insert(node_id);
|
||||
self.nodes.insert(node_id);
|
||||
}
|
||||
|
||||
/// Add a boundary node (shared with other partitions).
|
||||
pub fn add_boundary_node(&mut self, node_id: NodeId) {
|
||||
self.boundary_nodes.insert(node_id);
|
||||
self.nodes.insert(node_id);
|
||||
}
|
||||
|
||||
/// Add a neighboring partition.
|
||||
pub fn add_neighbor(&mut self, neighbor_id: usize) {
|
||||
if neighbor_id != self.id {
|
||||
self.neighbors.insert(neighbor_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an interface element.
|
||||
pub fn add_interface_element(&mut self, element_id: ElementId) {
|
||||
self.interface_elements.insert(element_id);
|
||||
}
|
||||
|
||||
/// Get the number of elements in this partition.
|
||||
pub fn element_count(&self) -> usize {
|
||||
self.elements.len()
|
||||
}
|
||||
|
||||
/// Get the number of nodes in this partition.
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
/// Get the number of boundary nodes.
|
||||
pub fn boundary_node_count(&self) -> usize {
|
||||
self.boundary_nodes.len()
|
||||
}
|
||||
|
||||
/// Get the number of internal nodes.
|
||||
pub fn internal_node_count(&self) -> usize {
|
||||
self.internal_nodes.len()
|
||||
}
|
||||
|
||||
/// Check if this partition contains the given element.
|
||||
pub fn contains_element(&self, element_id: &ElementId) -> bool {
|
||||
self.elements.contains(element_id)
|
||||
}
|
||||
|
||||
/// Check if this partition contains the given node.
|
||||
pub fn contains_node(&self, node_id: &NodeId) -> bool {
|
||||
self.nodes.contains(node_id)
|
||||
}
|
||||
|
||||
/// Check if the given node is a boundary node.
|
||||
pub fn is_boundary_node(&self, node_id: &NodeId) -> bool {
|
||||
self.boundary_nodes.contains(node_id)
|
||||
}
|
||||
|
||||
/// Check if the given node is an internal node.
|
||||
pub fn is_internal_node(&self, node_id: &NodeId) -> bool {
|
||||
self.internal_nodes.contains(node_id)
|
||||
}
|
||||
|
||||
/// Get load balance statistics for this partition.
|
||||
pub fn load_balance_info(&self) -> PartitionLoadInfo {
|
||||
PartitionLoadInfo {
|
||||
id: self.id,
|
||||
element_count: self.elements.len(),
|
||||
node_count: self.nodes.len(),
|
||||
internal_node_count: self.internal_nodes.len(),
|
||||
boundary_node_count: self.boundary_nodes.len(),
|
||||
interface_element_count: self.interface_elements.len(),
|
||||
neighbor_count: self.neighbors.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load balance information for a partition.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartitionLoadInfo {
|
||||
pub id: usize,
|
||||
pub element_count: usize,
|
||||
pub node_count: usize,
|
||||
pub internal_node_count: usize,
|
||||
pub boundary_node_count: usize,
|
||||
pub interface_element_count: usize,
|
||||
pub neighbor_count: usize,
|
||||
}
|
||||
|
||||
/// Statistics for mesh partitioning.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartitioningStats {
|
||||
pub num_partitions: usize,
|
||||
pub total_elements: usize,
|
||||
pub total_nodes: usize,
|
||||
pub avg_elements_per_partition: f64,
|
||||
pub max_elements_per_partition: usize,
|
||||
pub min_elements_per_partition: usize,
|
||||
pub load_imbalance: f64,
|
||||
pub total_interface_nodes: usize,
|
||||
pub communication_volume: usize,
|
||||
}
|
||||
|
||||
impl PartitioningStats {
|
||||
/// Create new statistics from a set of partitions.
|
||||
pub fn from_partitions(partitions: &[MeshPartition]) -> Self {
|
||||
let num_partitions = partitions.len();
|
||||
let total_elements: usize = partitions.iter().map(MeshPartition::element_count).sum();
|
||||
|
||||
// Count unique nodes (avoiding duplicates in boundary nodes)
|
||||
let mut all_nodes: IndexSet<NodeId> = IndexSet::new();
|
||||
for partition in partitions {
|
||||
all_nodes.extend(&partition.nodes);
|
||||
}
|
||||
let total_nodes = all_nodes.len();
|
||||
|
||||
let element_counts: Vec<usize> = partitions.iter().map(MeshPartition::element_count).collect();
|
||||
let max_elements = element_counts.iter().max().copied().unwrap_or(0);
|
||||
let min_elements = element_counts.iter().min().copied().unwrap_or(0);
|
||||
let avg_elements = if num_partitions > 0 {
|
||||
total_elements as f64 / num_partitions as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Calculate load imbalance
|
||||
let load_imbalance = if avg_elements > 0.0 {
|
||||
(max_elements as f64 - avg_elements) / avg_elements
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Count total interface nodes
|
||||
let total_interface_nodes: usize = partitions.iter().map(MeshPartition::boundary_node_count).sum();
|
||||
|
||||
// Estimate communication volume
|
||||
let communication_volume: usize = partitions
|
||||
.iter()
|
||||
.map(|p| p.boundary_node_count() * p.neighbors.len())
|
||||
.sum();
|
||||
|
||||
Self {
|
||||
num_partitions,
|
||||
total_elements,
|
||||
total_nodes,
|
||||
avg_elements_per_partition: avg_elements,
|
||||
max_elements_per_partition: max_elements,
|
||||
min_elements_per_partition: min_elements,
|
||||
load_imbalance,
|
||||
total_interface_nodes,
|
||||
communication_volume,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the partitioning is well-balanced.
|
||||
pub fn is_well_balanced(&self) -> bool {
|
||||
self.load_imbalance < 0.1 // Less than 10% imbalance
|
||||
}
|
||||
|
||||
/// Get efficiency rating (0.0 to 1.0).
|
||||
pub fn efficiency(&self) -> f64 {
|
||||
let balance_score = 1.0 / (1.0 + self.load_imbalance);
|
||||
let comm_score = if self.total_nodes > 0 {
|
||||
1.0 - (self.total_interface_nodes as f64 / self.total_nodes as f64).min(1.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
f64::midpoint(balance_score, comm_score)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user