FEMM/crates/femm-doc-mag/src/parser.rs

487 lines
21 KiB
Rust

//! .fem text parser: `[Section] = value` scalars and `<beginX>...<endX>` property stanzas.
use crate::{
ACSolver, ArcSegment, BlockLabel, BoundaryProp, CircuitProp, Coords, FemmDoc, LengthUnit,
MaterialProp, Node, PointProp, PrevType, ProblemType, Segment,
};
use num_complex::Complex64;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("legacy 3.2-format .fem files are not supported")]
LegacyFormat,
#[error("unexpected end of file while reading {context}")]
UnexpectedEof { context: &'static str },
#[error("malformed value in section {section}: {reason}")]
MalformedValue { section: &'static str, reason: String },
}
impl FemmDoc {
/// parses a .fem text buffer into a document.
pub fn parse(src: &str) -> Result<Self, ParseError> {
let mut doc = FemmDoc::default();
doc.depth = -1.0;
doc.smart_mesh = true;
doc.format = 4.0;
if src
.lines()
.next()
.map(|l| l.trim_start().starts_with("Frequency"))
.unwrap_or(false)
{
return Err(ParseError::LegacyFormat);
}
let mut lines = src.lines();
let mut version: i32 = 0;
let mut point = PointProp::default();
let mut bdry = BoundaryProp::default();
let mut mat = MaterialProp::default();
let mut circ = CircuitProp::default();
while let Some(raw) = lines.next() {
let tok = first_token(raw);
if tok.is_empty() { continue; }
// problem attributes
if tok.eq_ignore_ascii_case("[format]") {
let v = strip_key(raw);
let f = parse_f64(v);
doc.format = f;
version = (10.0 * f + 0.5) as i32;
} else if tok.eq_ignore_ascii_case("[frequency]") {
doc.frequency = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[depth]") {
doc.depth = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[precision]") {
doc.precision = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[acsolver]") {
doc.ac_solver = match parse_i32(strip_key(raw)) {
1 => ACSolver::Newton,
_ => ACSolver::SuccessiveApprox,
};
} else if tok.eq_ignore_ascii_case("[minangle]") {
doc.min_angle = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[dosmartmesh]") {
doc.smart_mesh = parse_i32(strip_key(raw)) != 0;
} else if tok.eq_ignore_ascii_case("[lengthunits]") {
doc.length_units = parse_length_units(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[problemtype]") {
doc.problem_type = parse_problem_type(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[coordinates]") {
doc.coords = parse_coords(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[comment]") {
doc.comment = unescape_comment(unquote(strip_key(raw)));
} else if tok.eq_ignore_ascii_case("[prevsoln]") {
doc.prev_soln = unquote(strip_key(raw));
if doc.prev_soln.is_empty() { doc.prev_type = PrevType::None; }
} else if tok.eq_ignore_ascii_case("[prevtype]") {
doc.prev_type = match parse_i32(strip_key(raw)) {
1 => PrevType::Incremental,
2 => PrevType::Frozen,
_ => PrevType::None,
};
} else if tok.eq_ignore_ascii_case("[extzo]") {
doc.ext_zo = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[extro]") {
doc.ext_ro = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("[extri]") {
doc.ext_ri = parse_f64(strip_key(raw));
// point property stanza
} else if tok.eq_ignore_ascii_case("<beginpoint>") {
point = PointProp::default();
point.name = String::from("New Point Property");
} else if tok.eq_ignore_ascii_case("<pointname>") {
point.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<a_re>") {
point.ap.re = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<a_im>") {
point.ap.im = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<i_re>") {
point.jp.re = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<i_im>") {
point.jp.im = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endpoint>") {
doc.points.push(std::mem::take(&mut point));
// boundary property stanza
} else if tok.eq_ignore_ascii_case("<beginbdry>") {
bdry = BoundaryProp::default();
bdry.name = String::from("New Boundary");
} else if tok.eq_ignore_ascii_case("<bdryname>") {
bdry.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<bdrytype>") {
bdry.format = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<mu_ssd>") {
bdry.mu = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<sigma_ssd>") {
bdry.sig = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<a_0>") {
bdry.a0 = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<a_1>") {
bdry.a1 = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<a_2>") {
bdry.a2 = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<phi>") {
bdry.phi = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<c0>") {
bdry.c0.re = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<c0i>") {
bdry.c0.im = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<c1>") {
bdry.c1.re = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<c1i>") {
bdry.c1.im = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<innerangle>") {
bdry.inner_angle = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<outerangle>") {
bdry.outer_angle = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endbdry>") {
doc.boundaries.push(std::mem::take(&mut bdry));
// material property stanza
} else if tok.eq_ignore_ascii_case("<beginblock>") {
mat = MaterialProp::default();
mat.name = String::from("New Material");
mat.mu_x = 1.0;
mat.mu_y = 1.0;
mat.lam_fill = 1.0;
} else if tok.eq_ignore_ascii_case("<blockname>") {
mat.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<mu_x>") {
mat.mu_x = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<mu_y>") {
mat.mu_y = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<h_c>") {
mat.h_c = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<h_cangle>") {
mat.theta_m = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<j_re>") {
mat.j_src.re = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<j_im>") {
mat.j_src.im = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<sigma>") {
mat.cduct = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<phi_h>") {
mat.theta_hn = parse_f64(strip_key(raw));
if version == 30 {
mat.theta_hx = mat.theta_hn;
mat.theta_hy = mat.theta_hn;
}
} else if tok.eq_ignore_ascii_case("<phi_hx>") {
mat.theta_hx = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<phi_hy>") {
mat.theta_hy = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<d_lam>") {
mat.lam_d = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<lamfill>") {
mat.lam_fill = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<lamtype>") {
mat.lam_type = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<nstrands>") {
mat.n_strands = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<wired>") {
mat.wire_d = parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<bhpoints>") {
let count = parse_i32(strip_key(raw));
if count > 0 {
mat.bh_curve.clear();
mat.bh_curve.reserve(count as usize);
for _ in 0..count {
let line = lines.next().ok_or(ParseError::UnexpectedEof {
context: "<BHPoints>",
})?;
let (b, rest) = take_f64(line);
let (h, _) = take_f64(rest);
mat.bh_curve.push((b, h));
}
}
} else if tok.eq_ignore_ascii_case("<endblock>") {
doc.materials.push(std::mem::take(&mut mat));
// circuit stanza
} else if tok.eq_ignore_ascii_case("<begincircuit>") {
circ = CircuitProp::default();
circ.name = String::from("New Circuit");
} else if tok.eq_ignore_ascii_case("<circuitname>") {
circ.name = unquote(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<totalamps_re>") {
circ.amps.re += parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<totalamps_im>") {
circ.amps.im += parse_f64(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<circuittype>") {
circ.circ_type = parse_i32(strip_key(raw));
} else if tok.eq_ignore_ascii_case("<endcircuit>") {
doc.circuits.push(std::mem::take(&mut circ));
// counted geometry sections
} else if tok.eq_ignore_ascii_case("[numpoints]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumPoints]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (group, _) = take_i32(rest);
let bm = resolve_name(bi, doc.points.iter().map(|p| p.name.as_str()));
doc.nodes.push(Node {
x, y,
boundary_marker: bm,
in_group: group,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numsegments]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumSegments]" })?;
let (n0, rest) = take_i32(line);
let (n1, rest) = take_i32(rest);
let (msl, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (hidden, rest) = take_i32(rest);
let (group, _) = take_i32(rest);
let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str()));
doc.segments.push(Segment {
n0, n1,
max_side_length: msl,
boundary_marker: bm,
hidden: hidden != 0,
in_group: group,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numarcsegments]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumArcSegments]" })?;
let (n0, rest) = take_i32(line);
let (n1, rest) = take_i32(rest);
let (al, rest) = take_f64(rest);
let (msl, rest) = take_f64(rest);
let (bi, rest) = take_i32(rest);
let (hidden, rest) = take_i32(rest);
let (group, _) = take_i32(rest);
let bm = resolve_name(bi, doc.boundaries.iter().map(|b| b.name.as_str()));
doc.arcs.push(ArcSegment {
n0, n1,
arc_length: al,
max_side_length: msl,
boundary_marker: bm,
hidden: hidden != 0,
in_group: group,
normal_direction: true,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numholes]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumHoles]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (group, _) = take_i32(rest);
doc.block_labels.push(BlockLabel {
x, y,
max_area: 0.0,
mag_dir: 0.0,
mag_dir_fctn: String::new(),
turns: 1,
block_type: String::from("<No Mesh>"),
in_circuit: String::from("<None>"),
in_group: group,
is_external: false,
is_default: false,
selected: false,
});
}
} else if tok.eq_ignore_ascii_case("[numblocklabels]") {
let n = parse_i32(strip_key(raw));
for _ in 0..n {
let line = lines.next().ok_or(ParseError::UnexpectedEof { context: "[NumBlockLabels]" })?;
let (x, rest) = take_f64(line);
let (y, rest) = take_f64(rest);
let (mi, rest) = take_i32(rest);
let (ma_diam, rest) = take_f64(rest);
let (ci, rest) = take_i32(rest);
let (mag_dir, rest) = take_f64(rest);
let (group, rest) = take_i32(rest);
let (turns, rest) = take_i32(rest);
let (ext_flags, rest) = take_i32(rest);
let mdf = take_quoted(rest);
let block_type = if mi == 0 {
String::from("<None>")
} else {
resolve_name(mi, doc.materials.iter().map(|m| m.name.as_str()))
};
let in_circuit = if ci == 0 {
String::from("<None>")
} else {
resolve_name(ci, doc.circuits.iter().map(|c| c.name.as_str()))
};
let max_area = if ma_diam < 0.0 {
0.0
} else {
std::f64::consts::PI * ma_diam * ma_diam / 4.0
};
doc.block_labels.push(BlockLabel {
x, y,
max_area,
mag_dir,
mag_dir_fctn: mdf,
turns,
block_type,
in_circuit,
in_group: group,
is_external: ext_flags & 1 != 0,
is_default: ext_flags & 2 != 0,
selected: false,
});
}
}
// unknown tokens are skipped silently, matching the original parser.
}
// 3.2-era files omitted [Depth]; fill the default in the current length unit.
if doc.depth == -1.0 {
doc.depth = match doc.length_units {
LengthUnit::Millimeters => 1000.0,
LengthUnit::Centimeters => 100.0,
LengthUnit::Meters => 1.0,
LengthUnit::Mils => 1000.0 / 0.0254,
LengthUnit::Microns => 1.0e6,
LengthUnit::Inches => 1.0 / 0.0254,
};
}
Ok(doc)
}
/// loads a .fem file by path.
pub fn open(path: impl AsRef<Path>) -> Result<Self, ParseError> {
let text = std::fs::read_to_string(path)?;
Self::parse(&text)
}
}
fn first_token(line: &str) -> &str {
line.trim_start().split_whitespace().next().unwrap_or("")
}
fn strip_key(line: &str) -> &str {
match line.find('=') {
Some(i) => &line[i + 1..],
None => "",
}
}
fn unquote(s: &str) -> String {
let s = s.trim();
let first = s.find('"');
let last = s.rfind('"');
match (first, last) {
(Some(a), Some(b)) if a < b => s[a + 1..b].to_string(),
_ => s.to_string(),
}
}
fn unescape_comment(s: String) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'n' {
out.push('\r');
out.push('\n');
i += 2;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
fn parse_f64(s: &str) -> f64 {
let s = s.trim().trim_matches(|c: char| c == ',' || c.is_whitespace());
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
s[..end].parse::<f64>().unwrap_or(0.0)
}
fn parse_i32(s: &str) -> i32 {
let s = s.trim().trim_matches(|c: char| c == ',' || c.is_whitespace());
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
s[..end].parse::<i32>().unwrap_or(0)
}
/// peels one f64 off the head of a whitespace/comma-separated row, returning the remainder.
fn take_f64(s: &str) -> (f64, &str) {
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
let (head, tail) = s.split_at(end);
(head.parse::<f64>().unwrap_or(0.0), tail)
}
/// peels one i32 off the head of a whitespace/comma-separated row, returning the remainder.
fn take_i32(s: &str) -> (i32, &str) {
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
let (head, tail) = s.split_at(end);
(head.parse::<i32>().unwrap_or(0), tail)
}
/// extracts the contents of the first double-quoted span in `s`, or returns empty.
fn take_quoted(s: &str) -> String {
let first = s.find('"');
if let Some(a) = first {
let after = &s[a + 1..];
if let Some(b) = after.find('"') {
return after[..b].to_string();
}
}
String::new()
}
fn parse_length_units(s: &str) -> LengthUnit {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("millimeters") { LengthUnit::Millimeters }
else if t.starts_with("c") { LengthUnit::Centimeters } // matches original's 1-char check
else if t.starts_with("meters") { LengthUnit::Meters }
else if t.starts_with("mils") { LengthUnit::Mils }
else if t.starts_with("microns") { LengthUnit::Microns }
else { LengthUnit::Inches }
}
fn parse_problem_type(s: &str) -> ProblemType {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("axi") { ProblemType::Axisymmetric } else { ProblemType::Planar }
}
fn parse_coords(s: &str) -> Coords {
let t = first_token(s).to_ascii_lowercase();
if t.starts_with("polar") { Coords::Polar } else { Coords::Cartesian }
}
/// resolves a 1-based property index into the matching property name, falling back to empty.
fn resolve_name<'a, I: Iterator<Item = &'a str>>(idx: i32, names: I) -> String {
if idx <= 0 { return String::new(); }
let want = idx as usize;
for (i, name) in names.enumerate() {
if i + 1 == want { return name.to_string(); }
}
String::new()
}
// silence dead-code warning in lib.rs until the writer module consumes it.
#[allow(dead_code)]
fn _force_complex_use(_c: Complex64) {}