forked from 1b-agent/fieldcraft-upstream
solver, decks: symmetry about an inclined plane (#242)
The symmetry condition takes any plane normal ({"normal": [nx, ny, nz]}).
An inclined one holds n·u = 0 and, at shell, beam and reference point
nodes, the rotations about the two in-plane axes: constraint rows the
implicit elimination already handles, its support forces reported as
reactions. Decks: Abaqus *EQUATION, Nastran MPC sets per subcase, LS-DYNA
*BOUNDARY_SPC in a *DEFINE_COORDINATE_SYSTEM along the normal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
a079e51f9d
commit
ac1037e1af
11 files changed
+1009
-53
No files matched your search
+103
-6
@@ -148,7 +148,7 @@ use std::fmt::Write as _;
|
||||
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, ContactSurfaces, CouplingKind, Elastic, Element, ElementKind, Hardening, Hyperelastic, InitialPenetration,
|
||||
Integration, Load, Model, OutputVariable, Plastic, Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
Integration, Load, Model, OutputVariable, Plastic, Procedure, Resolved, Section, SectionKind, Step, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, unsupported};
|
||||
@@ -193,6 +193,7 @@ pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
|
||||
d.couplings(model, r);
|
||||
d.contacts(model, r)?;
|
||||
d.ties(model)?;
|
||||
d.equations(model, r)?;
|
||||
d.steps(model, r)?;
|
||||
Ok(d.out)
|
||||
}
|
||||
@@ -370,9 +371,36 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
bad.insert(format!("coupling `{name}`: an Abaqus distributing coupling always couples translations 1-3"));
|
||||
}
|
||||
}
|
||||
for name in inclined_symmetries(model) {
|
||||
if !model.steps().iter().all(|s| s.boundary_conditions.contains(name)) {
|
||||
bad.insert(format!(
|
||||
"boundary condition `{name}`: a symmetry plane not normal to X, Y or Z active in some steps only (it is written as `*EQUATION`, which is model data and holds in every step)"
|
||||
));
|
||||
}
|
||||
}
|
||||
unsupported(FORMAT, bad)
|
||||
}
|
||||
|
||||
/// The symmetry conditions on inclined planes some step uses: linear equations rather than held
|
||||
/// degrees of freedom.
|
||||
fn inclined_symmetries(model: &Model) -> BTreeSet<&String> {
|
||||
model
|
||||
.boundary_conditions()
|
||||
.iter()
|
||||
.filter(|(n, bc)| {
|
||||
matches!(bc, BoundaryCondition::Symmetry { plane, .. } if plane.axis().is_none()) && !model.boundary_condition_users(n).is_empty()
|
||||
})
|
||||
.map(|(n, _)| n)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The boundary conditions of `names` that `*BOUNDARY` writes (inclined symmetry planes are
|
||||
/// `*EQUATION`s).
|
||||
fn held_conditions(model: &Model, names: &BTreeSet<String>) -> BTreeSet<String> {
|
||||
let inclined = inclined_symmetries(model);
|
||||
names.iter().filter(|n| !inclined.contains(n)).cloned().collect()
|
||||
}
|
||||
|
||||
/// The property of a group of discrete elements.
|
||||
enum DiscreteProperty {
|
||||
Mass(f64),
|
||||
@@ -1076,10 +1104,12 @@ impl Deck {
|
||||
match bc {
|
||||
BoundaryCondition::Fixed { .. } => self.line(&format!("{target}, ENCASTRE")),
|
||||
BoundaryCondition::Symmetry { plane, .. } => {
|
||||
let s = match plane {
|
||||
Symmetry::X => "XSYMM",
|
||||
Symmetry::Y => "YSYMM",
|
||||
Symmetry::Z => "ZSYMM",
|
||||
let s = match plane.axis() {
|
||||
Some(0) => "XSYMM",
|
||||
Some(1) => "YSYMM",
|
||||
Some(2) => "ZSYMM",
|
||||
// An inclined plane is an `*EQUATION` (model data), never here.
|
||||
_ => continue,
|
||||
};
|
||||
self.line(&format!("{target}, {s}"));
|
||||
}
|
||||
@@ -1097,8 +1127,74 @@ impl Deck {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The inclined symmetry planes: one `*EQUATION` per node and held direction (n · u = 0, and
|
||||
/// the rotations about the plane's axes at shell and beam nodes), whose first term Abaqus
|
||||
/// eliminates; it is chosen among the degrees of freedom no boundary condition, coupling
|
||||
/// or tie constrains ([`crate::symmetry_equations`]).
|
||||
fn equations(&mut self, model: &Model, r: &Resolved) -> Result<()> {
|
||||
let inclined = inclined_symmetries(model);
|
||||
if inclined.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let steps = model.steps();
|
||||
// Per node and degree of freedom: the value it is held at in each step that holds it.
|
||||
let mut held: BTreeMap<(u32, u8), Vec<f64>> = BTreeMap::new();
|
||||
for step in steps {
|
||||
for name in held_conditions(model, &step.boundary_conditions) {
|
||||
let Some(bc) = model.boundary_condition(&name) else { continue };
|
||||
for &node in r.target_nodes(bc.target()) {
|
||||
for dof in bc.dofs().list() {
|
||||
let v = if matches!(step.procedure, Procedure::Frequency { .. } | Procedure::Buckle { .. }) {
|
||||
0.0
|
||||
} else {
|
||||
bc.value(dof).unwrap_or(0.0)
|
||||
};
|
||||
held.entry((node, dof)).or_default().push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut dependent: BTreeSet<(u32, u8)> = BTreeSet::new();
|
||||
for c in model.couplings().values().filter(|c| c.kind == CouplingKind::Kinematic) {
|
||||
for &node in r.sets.get(&c.set).map(|s| s.nodes.as_slice()).unwrap_or_default() {
|
||||
dependent.extend(c.dofs.list().into_iter().map(|d| (node, d)));
|
||||
}
|
||||
}
|
||||
for t in model.ties().values() {
|
||||
for &node in r.sets.get(&t.secondary).map(|s| s.nodes.as_slice()).unwrap_or_default() {
|
||||
dependent.extend((1..=6).map(|d| (node, d)));
|
||||
}
|
||||
}
|
||||
let state = |node: u32, dof: u8| {
|
||||
if dependent.contains(&(node, dof)) {
|
||||
return crate::Held::Sometimes;
|
||||
}
|
||||
match held.get(&(node, dof)) {
|
||||
None => crate::Held::Free,
|
||||
Some(v) if v.len() == steps.len() && v.iter().all(|x| *x == v[0]) => crate::Held::Always(v[0]),
|
||||
Some(_) => crate::Held::Sometimes,
|
||||
}
|
||||
};
|
||||
let mut problems = BTreeSet::new();
|
||||
let equations = crate::symmetry_equations(model, r, inclined, &model.rotating_nodes(), state, &mut problems);
|
||||
unsupported(FORMAT, problems)?;
|
||||
self.comment("Symmetry planes not normal to X, Y or Z: per node, no motion along the normal (and no rotation about the plane's axes)");
|
||||
for e in &equations {
|
||||
self.line("*EQUATION");
|
||||
self.line(&e.terms.len().to_string());
|
||||
let mut fields = Vec::new();
|
||||
for (dof, c) in &e.terms {
|
||||
fields.push(format!("{}, {dof}", e.node));
|
||||
fields.push(number(*c)?);
|
||||
}
|
||||
self.line(&fields.join(", "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Boundary conditions of a step against the state it starts from.
|
||||
fn step_boundaries(&mut self, model: &Model, r: &Resolved, base: &BTreeSet<String>, active: &BTreeSet<String>) -> Result<()> {
|
||||
let (base, active) = (&held_conditions(model, base), &held_conditions(model, active));
|
||||
if active == base {
|
||||
self.comment("boundary conditions: as in the base state");
|
||||
Ok(())
|
||||
@@ -1188,7 +1284,8 @@ impl Deck {
|
||||
BTreeSet::new()
|
||||
} else {
|
||||
let in_every_step = |n: &String| steps.iter().all(|s| s.boundary_conditions.contains(n));
|
||||
model.boundary_conditions().iter().filter(|(n, bc)| holds_at_zero(bc) && in_every_step(n)).map(|(n, _)| n.clone()).collect()
|
||||
let all = model.boundary_conditions().iter().filter(|(n, bc)| holds_at_zero(bc) && in_every_step(n)).map(|(n, _)| n.clone()).collect();
|
||||
held_conditions(model, &all)
|
||||
};
|
||||
if !initial.is_empty() {
|
||||
self.comment("Initial state: boundary conditions active in every step");
|
||||
|
||||
+115
-1
@@ -16,7 +16,7 @@ pub mod nastran;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use fieldcraft_model::nodal::edge_pressure;
|
||||
use fieldcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||
use fieldcraft_model::{BoundaryCondition, Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@@ -176,6 +176,120 @@ pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIte
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// How a degree of freedom is constrained apart from the inclined symmetry planes, for choosing
|
||||
/// the dependent one of a plane's equation (formats eliminate it, so it may carry no other
|
||||
/// constraint).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Held {
|
||||
Free,
|
||||
/// Held at this value wherever the equation applies: it cannot depend on the others, and an
|
||||
/// equation of held ones alone is redundant when the values satisfy it.
|
||||
Always(f64),
|
||||
/// Held in some steps only, or made dependent by a coupling or tie: it cannot be dependent,
|
||||
/// nor can the equation be dropped.
|
||||
Sometimes,
|
||||
}
|
||||
|
||||
/// A linear equation Σ cⱼ u(node, dofⱼ) = 0 of an inclined symmetry plane, dependent degree of
|
||||
/// freedom first.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Equation {
|
||||
pub node: u32,
|
||||
pub terms: Vec<(u8, f64)>,
|
||||
}
|
||||
|
||||
/// A coefficient below this fraction of its row's largest is round-off of the elimination.
|
||||
const NEGLIGIBLE: f64 = 1e-12;
|
||||
|
||||
/// The equations of the inclined symmetry conditions `names` (others are skipped), node by node
|
||||
/// in label order: n · u = 0 and, at a node in `rotating`, t₁ · θ = 0 and t₂ · θ = 0 for the
|
||||
/// plane's in-plane axes ([`fieldcraft_model::Symmetry`]). The rows of one node (several planes
|
||||
/// meet at an edge) are reduced against each other, so each has a dependent degree of freedom
|
||||
/// of its own: the largest coefficient among those `held` says are free. A row of held degrees
|
||||
/// of freedom only is left out when their values satisfy it; otherwise it is a problem, as is a
|
||||
/// row with none free that cannot be left out.
|
||||
pub(crate) fn symmetry_equations<'a>(
|
||||
model: &Model,
|
||||
r: &Resolved,
|
||||
names: impl IntoIterator<Item = &'a String>,
|
||||
rotating: &BTreeSet<u32>,
|
||||
held: impl Fn(u32, u8) -> Held,
|
||||
problems: &mut BTreeSet<String>,
|
||||
) -> Vec<Equation> {
|
||||
let mut rows: BTreeMap<u32, Vec<(&str, [f64; 6])>> = BTreeMap::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for name in names {
|
||||
if !seen.insert(name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let Some(BoundaryCondition::Symmetry { target, plane }) = model.boundary_condition(name) else { continue };
|
||||
let (Some(n), Some([t1, t2])) = (plane.inclined(), plane.in_plane()) else { continue };
|
||||
for &node in r.target_nodes(target) {
|
||||
let at = rows.entry(node).or_default();
|
||||
at.push((name, [n[0], n[1], n[2], 0.0, 0.0, 0.0]));
|
||||
if rotating.contains(&node) || target.reference_point().is_some() {
|
||||
at.push((name, [0.0, 0.0, 0.0, t1[0], t1[1], t1[2]]));
|
||||
at.push((name, [0.0, 0.0, 0.0, t2[0], t2[1], t2[2]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for (node, rows) in rows {
|
||||
// The equations of this node so far: (dependent index, coefficients).
|
||||
let mut done: Vec<(usize, [f64; 6])> = Vec::new();
|
||||
for (name, mut c) in rows {
|
||||
let scale = c.iter().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
for (d, e) in &done {
|
||||
let f = c[*d] / e[*d];
|
||||
for k in 0..6 {
|
||||
c[k] -= f * e[k];
|
||||
}
|
||||
c[*d] = 0.0;
|
||||
}
|
||||
for v in &mut c {
|
||||
if v.abs() <= NEGLIGIBLE * scale {
|
||||
*v = 0.0;
|
||||
}
|
||||
}
|
||||
if c.iter().all(|v| *v == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let dof = |k: usize| k as u8 + 1;
|
||||
let pivot = (0..6).filter(|&k| c[k] != 0.0 && held(node, dof(k)) == Held::Free).fold(None, |best: Option<usize>, k| match best {
|
||||
Some(b) if c[b].abs() >= c[k].abs() => Some(b),
|
||||
_ => Some(k),
|
||||
});
|
||||
let Some(pivot) = pivot else {
|
||||
let mut residual = 0.0;
|
||||
let mut size = 0.0;
|
||||
let mut always = true;
|
||||
for k in (0..6).filter(|&k| c[k] != 0.0) {
|
||||
match held(node, dof(k)) {
|
||||
Held::Always(v) => {
|
||||
residual += c[k] * v;
|
||||
size += (c[k] * v).abs();
|
||||
}
|
||||
_ => always = false,
|
||||
}
|
||||
}
|
||||
if !always {
|
||||
problems.insert(format!(
|
||||
"boundary condition `{name}`: at node {node} every degree of freedom the inclined symmetry plane relates is held in some steps only or follows a coupling or tie, so none can be eliminated by its equation"
|
||||
));
|
||||
} else if residual.abs() > 1e-9 * size {
|
||||
problems.insert(format!("boundary condition `{name}`: node {node} is held off the inclined symmetry plane"));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let mut terms = vec![(dof(pivot), c[pivot])];
|
||||
terms.extend((0..6).filter(|&k| k != pivot && c[k] != 0.0).map(|k| (dof(k), c[k])));
|
||||
out.push(Equation { node, terms });
|
||||
done.push((pivot, c));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The global degree of freedom (1–6) a spring or dashpot direction acts along, when it is one
|
||||
/// (a vector or line along an axis counts: the sign does not matter to a spring).
|
||||
pub(crate) fn axis_dof(d: ResolvedDirection) -> Option<u8> {
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
//! LS-DYNA's defaults; a step without nonlinear geometry and hyperelastic materials in nonlinear
|
||||
//! steps are refused); boundary conditions
|
||||
//! as `*BOUNDARY_SPC_*_ID` (named) and `*BOUNDARY_PRESCRIBED_MOTION_*_ID` for non-zero
|
||||
//! displacements; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
|
||||
//! displacements, a symmetry plane not normal to X, Y or Z as `*BOUNDARY_SPC_*_ID` in a
|
||||
//! `*DEFINE_COORDINATE_SYSTEM` whose x axis is the plane's normal; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
|
||||
//! resolved nodal values; pressure as `*LOAD_SEGMENT_SET`; gravity as `*LOAD_BODY_X/Y/Z`
|
||||
//! (with `*LOAD_BODY_PARTS` when it acts on a set); output as `*DATABASE_BINARY_D3PLOT`,
|
||||
//! `*DATABASE_SPCFORC` and `*DATABASE_EXTENT_BINARY`.
|
||||
@@ -1048,6 +1049,24 @@ fn boundary_condition(
|
||||
(r.reference_point_nodes.get(p).copied().ok_or_else(|| broken(format!("boundary condition `{name}`: reference point `{p}`")))?, "NODE")
|
||||
}
|
||||
};
|
||||
// A symmetry plane not normal to an axis: held in a coordinate system whose x axis is its
|
||||
// normal and whose xy plane holds its first in-plane axis (translation x, rotations y and z).
|
||||
if let BoundaryCondition::Symmetry { plane, .. } = bc
|
||||
&& let (Some(n), Some([t1, _])) = (plane.inclined(), plane.in_plane())
|
||||
{
|
||||
*constraint_id = constraint_id.saturating_add(1);
|
||||
let cid = *constraint_id;
|
||||
d.line("*DEFINE_COORDINATE_SYSTEM");
|
||||
d.header(&["cid", "xo", "yo", "zo", "xl", "yl", "zl", "cidl"]);
|
||||
d.card([Field::from(cid), Field::from(0.0), Field::from(0.0), Field::from(0.0), Field::from(n[0]), Field::from(n[1]), Field::from(n[2])]);
|
||||
d.header(&["xp", "yp", "zp"]);
|
||||
d.card([Field::from(t1[0]), Field::from(t1[1]), Field::from(t1[2])]);
|
||||
d.line(&format!("*BOUNDARY_SPC_{suffix}_ID"));
|
||||
id_card(d, constraint_id, name);
|
||||
d.header(&["nsid", "cid", "dofx", "dofy", "dofz", "dofrx", "dofry", "dofrz"]);
|
||||
d.card([Field::from(entity), Field::from(cid)].into_iter().chain([1, 0, 0, 0, 1, 1].map(|v: i32| Field::from(v))));
|
||||
return Ok(());
|
||||
}
|
||||
let dofs = bc.dofs();
|
||||
// Held degrees of freedom (in a frequency step every constrained one is held).
|
||||
let held: Vec<u8> = dofs.list().into_iter().filter(|dof| !is_static || bc.value(*dof).is_none_or(|v| v == 0.0)).collect();
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
//! the basic X axis through a `CORD2R`;
|
||||
//! - boundary conditions: per step, one constraint set of `SPC1` (held degrees of freedom) and
|
||||
//! `SPC` (prescribed values); in a modes step every constrained degree of freedom is held;
|
||||
//! symmetry about a plane not normal to X, Y or Z: one `MPC` set per step, an equation per node
|
||||
//! and held direction (n · u = 0, and t · θ = 0 about the plane's two axes at shell and beam
|
||||
//! grids), its dependent component one no `SPC` or rigid element constrains;
|
||||
//! - forces, moments and tractions: `FORCE` and `MOMENT` at the nodal values the model resolves,
|
||||
//! so every writer and the solver share one distribution; pressure: `PLOAD4` per element face;
|
||||
//! gravity: `GRAV`; a step with several loads combines them with `LOAD`;
|
||||
@@ -241,6 +244,8 @@ type Constraints = BTreeMap<u32, [Option<f64>; 6]>;
|
||||
struct Subcase<'m> {
|
||||
step: &'m Step,
|
||||
spc: Option<u32>,
|
||||
/// The equations of its inclined symmetry planes.
|
||||
mpc: Option<u32>,
|
||||
load: Option<u32>,
|
||||
method: Option<u32>,
|
||||
}
|
||||
@@ -269,6 +274,8 @@ struct Deck<'m> {
|
||||
dependent: BTreeMap<u32, (Dofs, &'m str)>,
|
||||
/// Constraint sets by id.
|
||||
spc_sets: Vec<(u32, Constraints)>,
|
||||
/// Multipoint constraint sets (inclined symmetry planes) by id.
|
||||
mpc_sets: Vec<(u32, Vec<crate::Equation>)>,
|
||||
/// Load name → load set id.
|
||||
load_sids: BTreeMap<&'m str, u32>,
|
||||
/// Combinations: (id, load set ids).
|
||||
@@ -310,7 +317,7 @@ impl<'m> Deck<'m> {
|
||||
model,
|
||||
r,
|
||||
sol,
|
||||
subcases: steps.into_iter().map(|step| Subcase { step, spc: None, load: None, method: None }).collect(),
|
||||
subcases: steps.into_iter().map(|step| Subcase { step, spc: None, mpc: None, load: None, method: None }).collect(),
|
||||
problems,
|
||||
next_sid: 1,
|
||||
materials: BTreeMap::new(),
|
||||
@@ -321,6 +328,7 @@ impl<'m> Deck<'m> {
|
||||
discrete: Vec::new(),
|
||||
dependent: BTreeMap::new(),
|
||||
spc_sets: Vec::new(),
|
||||
mpc_sets: Vec::new(),
|
||||
load_sids: BTreeMap::new(),
|
||||
combinations: Vec::new(),
|
||||
}
|
||||
@@ -587,6 +595,7 @@ impl<'m> Deck<'m> {
|
||||
/// One constraint set per distinct step constraint, checked against coupled nodes.
|
||||
fn plan_constraints(&mut self) -> Result<()> {
|
||||
let (model, r) = (self.model, self.r);
|
||||
let rotating = model.rotating_nodes();
|
||||
// Conflicts, one problem per pair of definitions: the nodes they meet on.
|
||||
let mut clashes: BTreeMap<(&str, &str, &str), BTreeSet<u32>> = BTreeMap::new();
|
||||
let mut on_dependent: BTreeMap<(&str, &str), BTreeSet<u32>> = BTreeMap::new();
|
||||
@@ -598,6 +607,10 @@ impl<'m> Deck<'m> {
|
||||
for name in &step.boundary_conditions {
|
||||
let Some(bc) = model.boundary_condition(name) else { continue };
|
||||
let dofs = bc.dofs();
|
||||
// Inclined symmetry planes hold no global degree of freedom: they are equations.
|
||||
if dofs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// In a modes step every constrained degree of freedom is held.
|
||||
let values: Vec<(u8, f64)> = dofs.list().into_iter().filter_map(|d| Some((d, if modes { 0.0 } else { bc.value(d)? }))).collect();
|
||||
for &node in r.target_nodes(bc.target()) {
|
||||
@@ -622,6 +635,19 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let dependent = &self.dependent;
|
||||
let held = |node: u32, dof: u8| match (
|
||||
dependent.get(&node),
|
||||
set.get(&node).and_then(|v| v.get(usize::from(dof).checked_sub(1)?).copied().flatten()),
|
||||
) {
|
||||
(Some((d, _)), _) if d.contains(dof) => crate::Held::Sometimes,
|
||||
(_, Some(v)) => crate::Held::Always(v),
|
||||
_ => crate::Held::Free,
|
||||
};
|
||||
let equations = crate::symmetry_equations(model, r, &step.boundary_conditions, &rotating, held, &mut self.problems);
|
||||
if !equations.is_empty() {
|
||||
sub.mpc = Some(intern(&mut self.mpc_sets, &mut self.next_sid, equations)?);
|
||||
}
|
||||
if !set.is_empty() {
|
||||
sub.spc = Some(intern(&mut self.spc_sets, &mut self.next_sid, set)?);
|
||||
}
|
||||
@@ -707,7 +733,7 @@ impl<'m> Deck<'m> {
|
||||
let _ = writeln!(out, "SUBCASE {}", i + 1);
|
||||
let title: String = format!(" SUBTITLE = {}", sub.step.name).chars().take(72).collect();
|
||||
let _ = writeln!(out, "{title}");
|
||||
for (key, sid) in [("SPC", sub.spc), ("LOAD", sub.load), ("METHOD", sub.method)] {
|
||||
for (key, sid) in [("SPC", sub.spc), ("MPC", sub.mpc), ("LOAD", sub.load), ("METHOD", sub.method)] {
|
||||
if let Some(sid) = sid {
|
||||
let _ = writeln!(out, " {key} = {sid}");
|
||||
}
|
||||
@@ -715,6 +741,7 @@ impl<'m> Deck<'m> {
|
||||
for v in &sub.step.output {
|
||||
let request = match v {
|
||||
OutputVariable::Displacement => "DISPLACEMENT",
|
||||
OutputVariable::ReactionForce if sub.mpc.is_some() => "SPCFORCES = ALL\n MPCFORCES",
|
||||
OutputVariable::ReactionForce => "SPCFORCES",
|
||||
OutputVariable::Stress => "STRESS",
|
||||
OutputVariable::Strain => "STRAIN",
|
||||
@@ -892,6 +919,23 @@ impl<'m> Deck<'m> {
|
||||
Card::new("SPC").int(*sid).int(node).int(component(Dofs::from_bits(mask))).real(value).write(out)?;
|
||||
}
|
||||
}
|
||||
for (sid, equations) in &self.mpc_sets {
|
||||
for sub in self.subcases.iter().filter(|s| s.mpc == Some(*sid)) {
|
||||
comment(out, &format!("step {}: inclined symmetry planes, MPC {sid} (the first grid component is dependent)", sub.step.name));
|
||||
}
|
||||
for e in equations {
|
||||
// SID, then (G, C, A) triples: two on the first line (its last field blank), two
|
||||
// per continuation after a blank first field.
|
||||
let mut card = Card::new("MPC").int(*sid);
|
||||
for (i, (dof, c)) in e.terms.iter().enumerate() {
|
||||
if i % 2 == 0 && i > 0 {
|
||||
card = card.blank().blank();
|
||||
}
|
||||
card = card.int(e.node).int(i64::from(*dof)).real(*c);
|
||||
}
|
||||
card.write(out)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user