Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ target/
# Compiled Python extension (produced by maturin develop)
*.so
*.pyd
*.pdb

# Python bytecode
__pycache__/
Expand Down
188 changes: 188 additions & 0 deletions crates/arcanum-geometry/src/discretize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// discretize.rs — Wire discretization into segments (Steps 2–4)

use std::f64::consts::PI;

use arcanum_nec_import::{ArcWire, HelixWire, StraightWire, WireDescription};
use nalgebra::Vector3;

use crate::errors::{GeometryError, GeometryErrorKind, GeometryWarnings};
use crate::mesh::{ArcParams, CurveParams, HelixParams, LinearParams, Material, Segment, TagMap};

pub(crate) fn discretize_wires(
wires: &[WireDescription],
_warnings: &mut GeometryWarnings,
) -> Result<(Vec<Segment>, TagMap), GeometryError> {
let mut segments: Vec<Segment> = Vec::new();
let mut tag_map = TagMap::new();

for (wire_index, wire) in wires.iter().enumerate() {
let first = segments.len();
match wire {
WireDescription::Straight(w) => discretize_straight(w, wire_index, &mut segments)?,
WireDescription::Arc(w) => discretize_arc(w, wire_index, &mut segments)?,
WireDescription::Helix(w) => discretize_helix(w, wire_index, &mut segments)?,
}
let last = segments.len() - 1;
tag_map.insert(wire.tag(), first, last);
}

Ok((segments, tag_map))
}

// ─────────────────────────────────────────────────────────────────────────────
// Step 2 — Linear (GW)
// ─────────────────────────────────────────────────────────────────────────────

fn discretize_straight(
wire: &StraightWire,
wire_index: usize,
segments: &mut Vec<Segment>,
) -> Result<(), GeometryError> {
let n = wire.segment_count as usize;

if n == 0 {
return Err(GeometryError::new(
GeometryErrorKind::ZeroSegmentCount,
wire_index,
format!("GW tag={} has segment count 0", wire.tag),
));
}

let r_a = Vector3::new(wire.x1, wire.y1, wire.z1);
let r_b = Vector3::new(wire.x2, wire.y2, wire.z2);

if (r_b - r_a).norm() == 0.0 {
return Err(GeometryError::new(
GeometryErrorKind::ZeroLengthWire,
wire_index,
format!(
"GW tag={} has identical start and end coordinates",
wire.tag
),
));
}

let base_index = segments.len();
for k in 0..n {
// Evaluate endpoints from closed-form — never accumulate incrementally.
let t0 = k as f64 / n as f64;
let t1 = (k + 1) as f64 / n as f64;
let start = r_a + t0 * (r_b - r_a);
let end = r_a + t1 * (r_b - r_a);

segments.push(Segment {
curve: CurveParams::Linear(LinearParams { start, end }),
wire_radius: wire.radius,
material: Material::PEC,
tag: wire.tag,
segment_index: base_index + k,
wire_index,
is_image: false,
});
}
Ok(())
}

// ─────────────────────────────────────────────────────────────────────────────
// Step 3 — Arc (GA)
// ─────────────────────────────────────────────────────────────────────────────

fn discretize_arc(
wire: &ArcWire,
wire_index: usize,
segments: &mut Vec<Segment>,
) -> Result<(), GeometryError> {
let n = wire.segment_count as usize;
let theta1 = wire.angle1.to_radians();
let theta2 = wire.angle2.to_radians();
let r = wire.arc_radius;

// Evaluate arc point in XZ plane: r(θ) = (R cosθ, 0, R sinθ)
let arc_point =
|theta: f64| -> Vector3<f64> { Vector3::new(r * theta.cos(), 0.0, r * theta.sin()) };

let base_index = segments.len();
for k in 0..n {
// Closed-form angle bounds for segment k.
let t0 = k as f64 / n as f64;
let t1 = (k + 1) as f64 / n as f64;
let th1k = theta1 + t0 * (theta2 - theta1);
let th2k = theta1 + t1 * (theta2 - theta1);

let start = arc_point(th1k);
let end = arc_point(th2k);

segments.push(Segment {
curve: CurveParams::Arc(ArcParams {
radius: r,
theta1: th1k,
theta2: th2k,
start,
end,
}),
wire_radius: wire.radius,
material: Material::PEC,
tag: wire.tag,
segment_index: base_index + k,
wire_index,
is_image: false,
});
}
Ok(())
}

// ─────────────────────────────────────────────────────────────────────────────
// Step 4 — Helix (GH)
// ─────────────────────────────────────────────────────────────────────────────

fn discretize_helix(
wire: &HelixWire,
wire_index: usize,
segments: &mut Vec<Segment>,
) -> Result<(), GeometryError> {
let n = wire.segment_count as usize;
let a1 = wire.radius_start;
let a2 = wire.radius_end;
let hl = wire.total_length;
let n_turns = wire.n_turns;

// Radius at parameter t ∈ [0,1] of the full helix.
let radius_at = |t: f64| -> f64 { a1 + t * (a2 - a1) };

// Helix position at parameter t ∈ [0,1]:
// r(t) = (A(t) cos(2π N t), A(t) sin(2π N t), HL·t)
let helix_point = |t: f64| -> Vector3<f64> {
let a = radius_at(t);
let angle = 2.0 * PI * n_turns * t;
Vector3::new(a * angle.cos(), a * angle.sin(), hl * t)
};

let base_index = segments.len();
for k in 0..n {
// Closed-form parameter values — never accumulated.
let t0 = k as f64 / n as f64;
let t1 = (k + 1) as f64 / n as f64;
let start = helix_point(t0);
let end = helix_point(t1);

segments.push(Segment {
curve: CurveParams::Helix(HelixParams {
radius_start: a1,
radius_end: a2,
total_length: hl,
n_turns,
n_segments: n as u32,
segment_index: k as u32,
start,
end,
}),
wire_radius: wire.radius,
material: Material::PEC,
tag: wire.tag,
segment_index: base_index + k,
wire_index,
is_image: false,
});
}
Ok(())
}
122 changes: 122 additions & 0 deletions crates/arcanum-geometry/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// errors.rs — Phase 1 error and warning types

/// A hard error that aborts mesh construction.
#[derive(Debug, Clone)]
pub struct GeometryError {
pub kind: GeometryErrorKind,
/// 1-based index into the wire list (not a line number — Phase 1 works
/// with already-parsed WireDescriptions).
pub wire_index: usize,
pub message: String,
}

impl GeometryError {
pub fn new(kind: GeometryErrorKind, wire_index: usize, message: impl Into<String>) -> Self {
GeometryError {
kind,
wire_index,
message: message.into(),
}
}
}

impl std::fmt::Display for GeometryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[wire {}] {}: {}",
self.wire_index,
self.kind.as_str(),
self.message
)
}
}

/// Category of a hard geometry error.
#[derive(Debug, Clone, PartialEq)]
pub enum GeometryErrorKind {
/// Wire has zero length (start == end).
ZeroLengthWire,
/// Segment count is zero (should have been caught by nec-import, but
/// checked here defensively).
ZeroSegmentCount,
/// A GM operation references a tag that does not exist in the wire list.
UnknownTagReference,
/// A GM copy operation would generate a duplicate tag.
DuplicateTag,
/// A coordinate is NaN or infinite.
InvalidCoordinate,
}

impl GeometryErrorKind {
pub fn as_str(&self) -> &'static str {
match self {
GeometryErrorKind::ZeroLengthWire => "ZeroLengthWire",
GeometryErrorKind::ZeroSegmentCount => "ZeroSegmentCount",
GeometryErrorKind::UnknownTagReference => "UnknownTagReference",
GeometryErrorKind::DuplicateTag => "DuplicateTag",
GeometryErrorKind::InvalidCoordinate => "InvalidCoordinate",
}
}
}

/// A non-fatal condition worth reporting.
#[derive(Debug, Clone)]
pub struct GeometryWarning {
pub kind: GeometryWarningKind,
pub message: String,
}

impl GeometryWarning {
pub fn new(kind: GeometryWarningKind, message: impl Into<String>) -> Self {
GeometryWarning {
kind,
message: message.into(),
}
}
}

/// Category of a geometry warning.
#[derive(Debug, Clone, PartialEq)]
pub enum GeometryWarningKind {
/// Two wire endpoints are closer than 10ε but farther than ε — possible
/// modeling error.
NearCoincidentEndpoints,
/// A wire lies entirely in the z = 0 ground plane; no image generated.
WireInGroundPlane,
}

impl GeometryWarningKind {
pub fn as_str(&self) -> &'static str {
match self {
GeometryWarningKind::NearCoincidentEndpoints => "NearCoincidentEndpoints",
GeometryWarningKind::WireInGroundPlane => "WireInGroundPlane",
}
}
}

/// Accumulated list of geometry warnings.
#[derive(Debug, Clone, Default)]
pub struct GeometryWarnings(Vec<GeometryWarning>);

impl GeometryWarnings {
pub fn new() -> Self {
GeometryWarnings::default()
}

pub fn push(&mut self, w: GeometryWarning) {
self.0.push(w);
}

pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

pub fn into_vec(self) -> Vec<GeometryWarning> {
self.0
}

pub fn iter(&self) -> impl Iterator<Item = &GeometryWarning> {
self.0.iter()
}
}
Loading
Loading