487 lines
21 KiB
Rust
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) {}
|