use std::path::PathBuf;
use crate::coords::CoordSystem;
use crate::error::{Error, Result};
use crate::grid::RectilinearGrid;
use crate::math::{BoundingBox, Vec3};
use crate::param::{ParameterSet, Scalar};
use crate::primitives::bvh::Bvh;
use crate::primitives::{Primitive, Shape};
use crate::properties::{MaterialQuantity, Property, PropertyKind, TypeMask};
#[derive(Debug, Clone, PartialEq)]
pub struct BackgroundMaterial {
pub epsilon: Scalar,
pub mue: Scalar,
pub kappa: Scalar,
pub sigma: Scalar,
pub extra_attributes: Vec<(String, String)>,
}
impl Default for BackgroundMaterial {
fn default() -> Self {
BackgroundMaterial {
epsilon: Scalar::new(1.0),
mue: Scalar::new(1.0),
kappa: Scalar::new(0.0),
sigma: Scalar::new(0.0),
extra_attributes: Vec::new(),
}
}
}
impl BackgroundMaterial {
fn update(&mut self, p: &ParameterSet) -> Result<()> {
self.epsilon.update(p)?;
self.mue.update(p)?;
self.kappa.update(p)?;
self.sigma.update(p)
}
pub fn value(&self, q: MaterialQuantity) -> f64 {
match q {
MaterialQuantity::Epsilon => self.epsilon.value(),
MaterialQuantity::Mue => self.mue.value(),
MaterialQuantity::Kappa => self.kappa.value(),
MaterialQuantity::Sigma => self.sigma.value(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PropertyId(u32);
impl PropertyId {
pub const fn get(self) -> u32 {
self.0
}
}
impl core::fmt::Display for PropertyId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "#{}", self.0)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationIssue {
DuplicateName {
name: String,
},
EmptyProperty {
name: String,
},
EmptyPrimitive {
property: String,
index: usize,
shape: &'static str,
},
}
impl core::fmt::Display for ValidationIssue {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ValidationIssue::DuplicateName { name } => {
write!(f, "duplicate property name {name:?}")
}
ValidationIssue::EmptyProperty { name } => {
write!(f, "property {name:?} has no primitives")
}
ValidationIssue::EmptyPrimitive {
property,
index,
shape,
} => write!(
f,
"property {property:?}: primitive #{index} ({shape}) is empty"
),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct Hit<'a> {
pub property: &'a Property,
pub primitive: &'a Primitive,
pub primitive_index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Medium {
pub epsilon: [f64; 3],
pub mue: [f64; 3],
pub kappa: [f64; 3],
pub sigma: [f64; 3],
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ContinuousStructure {
properties: Vec<Property>,
next_id: u32,
pub grid: RectilinearGrid,
pub parameters: ParameterSet,
pub background: BackgroundMaterial,
pub coord_system: CoordSystem,
pub base_dir: Option<PathBuf>,
pub extra_attributes: Vec<(String, String)>,
pub extra_elements: Vec<crate::io::dom::Element>,
}
impl ContinuousStructure {
pub fn new() -> Self {
Self::default()
}
pub fn add_property(&mut self, mut p: Property) -> PropertyId {
self.next_id += 1;
p.id = self.next_id;
self.properties.push(p);
PropertyId(self.next_id)
}
pub fn add_primitive(
&mut self,
id: PropertyId,
prim: impl Into<Primitive>,
) -> Result<&mut Primitive> {
let p = self
.property_mut(id)
.ok_or_else(|| Error::Invalid(format!("no property {id:?}")))?;
Ok(p.add_primitive(prim))
}
pub fn remove_property(&mut self, id: PropertyId) -> Option<Property> {
let i = self.properties.iter().position(|p| p.id == id.get())?;
let mut p = self.properties.remove(i);
p.id = 0;
Some(p)
}
pub fn properties(&self) -> &[Property] {
&self.properties
}
pub fn properties_mut(&mut self) -> impl Iterator<Item = &mut Property> {
self.properties.iter_mut()
}
pub fn properties_of(&self, mask: impl Into<TypeMask>) -> impl Iterator<Item = &Property> {
let mask = mask.into();
self.properties
.iter()
.filter(move |p| mask.contains(p.property_type()))
}
pub fn property(&self, id: PropertyId) -> Option<&Property> {
self.properties.iter().find(|p| p.id == id.get())
}
pub fn property_mut(&mut self, id: PropertyId) -> Option<&mut Property> {
self.properties.iter_mut().find(|p| p.id == id.get())
}
pub fn property_by_name(&self, name: &str) -> Option<&Property> {
self.properties.iter().find(|p| p.name == name)
}
pub fn property_by_name_mut(&mut self, name: &str) -> Option<&mut Property> {
self.properties.iter_mut().find(|p| p.name == name)
}
pub fn property_id(&self, name: &str) -> Option<PropertyId> {
self.property_by_name(name).map(|p| PropertyId(p.id))
}
pub fn primitives(&self) -> impl Iterator<Item = (&Property, &Primitive)> {
self.properties
.iter()
.flat_map(|p| p.primitives.iter().map(move |q| (p, q)))
}
pub fn update(&mut self) -> Result<()> {
self.update_parameters()?;
for p in &mut self.properties {
for prim in &mut p.primitives {
if let Shape::PolyhedronReader(r) = &mut prim.shape
&& !r.is_loaded()
{
r.load(self.base_dir.as_deref())?;
}
}
}
Ok(())
}
pub fn update_parameters(&mut self) -> Result<()> {
let params = &self.parameters;
self.background.update(params)?;
for p in &mut self.properties {
p.update(params)?;
}
Ok(())
}
pub fn property_at(
&self,
p: Vec3,
cs: CoordSystem,
mask: impl Into<TypeMask>,
) -> Option<Hit<'_>> {
let mask = mask.into();
let mut best: Option<Hit<'_>> = None;
for prop in self
.properties
.iter()
.filter(|q| mask.contains(q.property_type()))
{
for (i, prim) in prop.primitives.iter().enumerate() {
if best.is_some_and(|b| b.primitive.priority >= prim.priority) {
continue;
}
if prim.contains_in(p, cs, self.coord_system) {
best = Some(Hit {
property: prop,
primitive: prim,
primitive_index: i,
});
}
}
}
best
}
pub fn properties_at(
&self,
p: Vec3,
cs: CoordSystem,
mask: impl Into<TypeMask>,
) -> Vec<Hit<'_>> {
let mask = mask.into();
let mut hits: Vec<Hit<'_>> = Vec::new();
for prop in self
.properties
.iter()
.filter(|q| mask.contains(q.property_type()))
{
for (i, prim) in prop.primitives.iter().enumerate() {
if prim.contains_in(p, cs, self.coord_system) {
hits.push(Hit {
property: prop,
primitive: prim,
primitive_index: i,
});
}
}
}
hits.sort_by_key(|h| core::cmp::Reverse(h.primitive.priority));
hits
}
pub fn medium_at(&self, p: Vec3, cs: CoordSystem) -> Option<Medium> {
let hit = self.property_at(
p,
cs,
TypeMask::MATERIALS | TypeMask::of(crate::PropertyType::Metal),
);
let cart = cs.to_cartesian(p);
match hit.map(|h| &h.property.kind) {
Some(PropertyKind::Metal) => None,
Some(k) if let Some(m) = k.material() => Some(Medium {
epsilon: m.values_at(MaterialQuantity::Epsilon, cart),
mue: m.values_at(MaterialQuantity::Mue, cart),
kappa: m.values_at(MaterialQuantity::Kappa, cart),
sigma: m.values_at(MaterialQuantity::Sigma, cart),
}),
_ => {
let b = &self.background;
Some(Medium {
epsilon: [b.epsilon.value(); 3],
mue: [b.mue.value(); 3],
kappa: [b.kappa.value(); 3],
sigma: [b.sigma.value(); 3],
})
}
}
}
pub fn bounding_box(&self, cs: CoordSystem) -> BoundingBox {
self.primitives()
.map(|(_, q)| q.bounding_box_in(cs, self.coord_system))
.filter(|b| b.min.is_finite() && b.max.is_finite())
.fold(BoundingBox::EMPTY, |a, b| a.union(&b))
}
pub fn primitives_in<'a>(
&'a self,
region: &'a BoundingBox,
cs: CoordSystem,
mask: impl Into<TypeMask>,
) -> impl Iterator<Item = (&'a Property, &'a Primitive)> + 'a {
let mask = mask.into();
let dcs = self.coord_system;
self.primitives()
.filter(move |(p, _)| mask.contains(p.property_type()))
.filter(move |(_, q)| q.bounding_box_in(cs, dcs).intersects(region))
}
#[must_use]
pub fn spatial_index(&self, cs: CoordSystem, mask: impl Into<TypeMask>) -> SpatialIndex<'_> {
SpatialIndex::new(self, cs, mask.into())
}
#[must_use]
pub fn validate(&self) -> Vec<ValidationIssue> {
let mut w = Vec::new();
for (i, p) in self.properties.iter().enumerate() {
if self.properties[..i].iter().any(|q| q.name == p.name) {
w.push(ValidationIssue::DuplicateName {
name: p.name.clone(),
});
}
if p.primitives.is_empty() {
w.push(ValidationIssue::EmptyProperty {
name: p.name.clone(),
});
}
for (k, q) in p.primitives.iter().enumerate() {
let b = q.bounding_box(self.coord_system);
if b.is_empty() {
w.push(ValidationIssue::EmptyPrimitive {
property: p.name.clone(),
index: k,
shape: q.shape.type_name(),
});
}
}
}
w
}
}
struct Entry<'a> {
bbox: BoundingBox,
hit: Hit<'a>,
}
pub struct SpatialIndex<'a> {
entries: Vec<Entry<'a>>,
bvh: Bvh,
cs: CoordSystem,
default_cs: CoordSystem,
}
impl core::fmt::Debug for SpatialIndex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SpatialIndex")
.field("primitives", &self.entries.len())
.field("cs", &self.cs)
.field("default_cs", &self.default_cs)
.finish_non_exhaustive()
}
}
impl<'a> SpatialIndex<'a> {
fn new(s: &'a ContinuousStructure, cs: CoordSystem, mask: TypeMask) -> Self {
let mut entries: Vec<Entry<'a>> = Vec::new();
for prop in s
.properties
.iter()
.filter(|q| mask.contains(q.property_type()))
{
for (i, prim) in prop.primitives.iter().enumerate() {
let b = prim.bounding_box(s.coord_system);
let scale = b.size().norm().max(b.min.norm()).max(b.max.norm());
let bbox = if b.is_empty() {
b
} else {
b.expanded(1e-9 * scale)
};
entries.push(Entry {
bbox,
hit: Hit {
property: prop,
primitive: prim,
primitive_index: i,
},
});
}
}
entries.sort_by_key(|e| core::cmp::Reverse(e.hit.primitive.priority));
let boxes: Vec<BoundingBox> = entries.iter().map(|e| e.bbox).collect();
SpatialIndex {
bvh: Bvh::build(&boxes),
entries,
cs,
default_cs: s.coord_system,
}
}
pub fn query(&self, p: Vec3) -> Option<Hit<'a>> {
let c = self.cs.to_cartesian(p);
let mut buf = [0u32; 32];
let mut n = 0usize;
let mut spill: Vec<u32> = Vec::new();
self.bvh.visit(
|b| b.contains(c),
|i| {
if self.entries[i as usize].bbox.contains(c) {
if n < buf.len() {
buf[n] = i;
n += 1;
} else {
spill.push(i);
}
}
true
},
);
let cand: &mut [u32] = if spill.is_empty() {
&mut buf[..n]
} else {
spill.extend_from_slice(&buf[..n]);
&mut spill
};
cand.sort_unstable();
cand.iter()
.map(|&i| &self.entries[i as usize])
.find(|e| e.hit.primitive.contains_in(p, self.cs, self.default_cs))
.map(|e| e.hit)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::{BoxShape, Sphere};
use crate::properties::Material;
fn model() -> ContinuousStructure {
let mut s = ContinuousStructure::new();
let sub = s.add_property(Property::material("sub", Material::new(4.0, 0.0)));
s.add_primitive(
sub,
Primitive::new(BoxShape::new([0.0, 0.0, 0.0], [10.0, 10.0, 1.0])).with_priority(1),
)
.unwrap();
let m = s.add_property(Property::metal("pec"));
s.add_primitive(
m,
Primitive::new(Sphere::new([5.0, 5.0, 1.0], 0.5)).with_priority(5),
)
.unwrap();
s
}
#[test]
fn priority_ties() {
let mut s = ContinuousStructure::new();
let a = s.add_property(Property::material("A", Material::new(2.0, 0.0)));
let b = s.add_property(Property::material("B", Material::new(3.0, 0.0)));
let unit =
|| Primitive::new(BoxShape::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])).with_priority(3);
s.add_primitive(b, unit()).unwrap();
s.add_primitive(a, unit()).unwrap();
let p = Vec3::new(0.5, 0.5, 0.5);
let hit = s
.property_at(p, CoordSystem::Cartesian, TypeMask::ANY)
.unwrap();
assert_eq!(hit.property.name, "A");
let all = s.properties_at(p, CoordSystem::Cartesian, TypeMask::ANY);
assert_eq!(all[0].property.name, "A");
let idx = s.spatial_index(CoordSystem::Cartesian, TypeMask::ANY);
assert_eq!(idx.query(p).unwrap().property.name, "A");
}
#[test]
fn priority() {
let s = model();
let h = s
.property_at(
Vec3::new(5.0, 5.0, 0.8),
CoordSystem::Cartesian,
TypeMask::ANY,
)
.unwrap();
assert_eq!(h.property.name, "pec");
let h = s
.property_at(
Vec3::new(1.0, 1.0, 0.5),
CoordSystem::Cartesian,
TypeMask::ANY,
)
.unwrap();
assert_eq!(h.property.name, "sub");
assert!(
s.property_at(
Vec3::new(1.0, 1.0, 2.0),
CoordSystem::Cartesian,
TypeMask::ANY
)
.is_none()
);
assert_eq!(
s.properties_at(
Vec3::new(5.0, 5.0, 0.8),
CoordSystem::Cartesian,
TypeMask::ANY
)
.len(),
2
);
let idx = s.spatial_index(CoordSystem::Cartesian, TypeMask::ANY);
for p in [
Vec3::new(5.0, 5.0, 0.8),
Vec3::new(1.0, 1.0, 0.5),
Vec3::new(1.0, 1.0, 2.0),
] {
assert_eq!(
idx.query(p).map(|h| &h.property.name),
s.property_at(p, CoordSystem::Cartesian, TypeMask::ANY)
.map(|h| &h.property.name)
);
}
}
#[test]
fn media() {
let s = model();
assert_eq!(
s.medium_at(Vec3::new(1.0, 1.0, 0.5), CoordSystem::Cartesian)
.unwrap()
.epsilon,
[4.0; 3]
);
assert_eq!(
s.medium_at(Vec3::new(1.0, 1.0, 5.0), CoordSystem::Cartesian)
.unwrap()
.epsilon,
[1.0; 3]
);
assert!(
s.medium_at(Vec3::new(5.0, 5.0, 1.0), CoordSystem::Cartesian)
.is_none()
);
let b = s.bounding_box(CoordSystem::Cartesian);
assert_eq!(b.max, Vec3::new(10.0, 10.0, 1.5));
}
#[test]
fn parametric() {
let mut s = ContinuousStructure::new();
s.parameters.set("w", 2.0);
let id = s.add_property(Property::metal("m"));
s.add_primitive(
id,
BoxShape {
start: [0.0, 0.0, 0.0].into(),
stop: crate::ScalarPoint(["w".into(), "w*2".into(), 1.0.into()]),
},
)
.unwrap();
s.update().unwrap();
assert!(
s.property_at(
Vec3::new(1.9, 3.9, 0.5),
CoordSystem::Cartesian,
TypeMask::ANY
)
.is_some()
);
s.parameters.set("w", 1.0);
s.update().unwrap();
assert!(
s.property_at(
Vec3::new(1.9, 3.9, 0.5),
CoordSystem::Cartesian,
TypeMask::ANY
)
.is_none()
);
assert!(s.validate().is_empty());
assert!(s.remove_property(id).is_some());
}
#[test]
fn index_matches_linear_scan() {
use crate::primitives::UserDefined;
let mut seed = 11u64;
let mut rnd = move || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(seed >> 11) as f64 / (1u64 << 53) as f64
};
let mut s = ContinuousStructure::new();
for k in 0..60 {
let id = s.add_property(Property::material(format!("m{k}"), Material::new(1.0, 0.0)));
for _ in 0..1 + k % 3 {
let c = Vec3::new(rnd() * 20.0, rnd() * 20.0, rnd() * 20.0);
let d = Vec3::new(rnd() * 4.0, rnd() * 4.0, rnd() * 4.0);
let prim = if rnd() < 0.7 {
Primitive::new(BoxShape::new(c - d, c + d))
} else {
Primitive::new(Sphere::new(c, d[0]))
};
s.add_primitive(id, prim.with_priority((rnd() * 4.0) as i32))
.unwrap();
}
}
let u = s.add_property(Property::metal("half"));
s.add_primitive(
u,
Primitive::new(UserDefined::new("z > 15")).with_priority(2),
)
.unwrap();
let idx = s.spatial_index(CoordSystem::Cartesian, TypeMask::ANY);
for _ in 0..5000 {
let p = Vec3::new(rnd() * 22.0 - 1.0, rnd() * 22.0 - 1.0, rnd() * 22.0 - 1.0);
let a = idx.query(p);
let b = s.property_at(p, CoordSystem::Cartesian, TypeMask::ANY);
match (a, b) {
(None, None) => {}
(Some(a), Some(b)) => {
assert!(core::ptr::eq(a.primitive, b.primitive), "point {p:?}");
assert_eq!(a.property.id(), b.property.id());
assert_eq!(a.primitive_index, b.primitive_index);
}
_ => panic!("index and linear scan disagree at {p:?}"),
}
}
}
}