Resources Numerical accuracy Calculation policy

CalculiX deck export and result import

Writes a nonlinear study as a text input deck for the separate CalculiX program and reads its text result file back.

How the precision is kept

Node coordinates, material values and loads are 64-bit in the model but are written to the deck with 11 significant digits. Slanted support directions are written with 9 decimals. Results are read from a fixed-width text file whose 12-character number fields set how many digits come back.

Open item. Write deck numbers with toExponential(16) so node coordinates, material values and loads reach the other program with all their digits.

Calculations under this policy

6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

calculix.js PartCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/partcraft-test/test-sim08.mjs, tools/partcraft-test/calculix-compare.mjs

Numbers written to the deck

64-bit

Formats every coordinate, material value, load and penalty for the input deck.

text = v.toExponential(10)

Ten digits after the point in scientific notation, 11 significant digits in all. A 64-bit number needs up to 17 to be reproduced, so each exported value is rounded at about 1 part in 10¹¹. A value that is not finite is written as 0.

  • calculix.js
  • line 19
  • PartCraft 0.8.1
const num = (v) => (Number.isFinite(v) ? v.toExponential(10) : '0.');

Material cards

64-bit

Writes elastic constants, density and the two-point plastic curve of each part's material.

density (tonne/mm³) = density (kg/m³) × 1e-12; plastic: (σ_y, 0) and (σ_y + H, 1)

The unit factor is a power of ten applied in one multiplication. The hardening line is given by its value at a plastic strain of 1. All values then pass through the 11-digit formatter.

  • calculix.js
  • lines 55 to 60
  • PartCraft 0.8.1
materials.forEach((m, b) => {
  L.push(`*MATERIAL, NAME=MAT_${label(b)}`, '*ELASTIC', ` ${num(m.E)}, ${num(m.nu)}`);
  if (m.density) L.push('*DENSITY', ` ${num(m.density * 1e-12)}`);
  if (Number.isFinite(m.sy)) L.push('*PLASTIC', ` ${num(m.sy)}, 0.`, ` ${num(m.sy + m.H)}, 1.`);
  L.push(`*SOLID SECTION, ELSET=${label(b)}, MATERIAL=MAT_${label(b)}`);
});

Support directions

64-bit

Writes each held direction, and a local axis system for supports on slanted faces.

axis when ||v| − 1| < 1e-9; slanted: local x = n, local y = unit(t − (t · n) n)

Slanted normals are written with toFixed(9), so each component is rounded at the ninth decimal and the written vector is no longer exactly unit length. Normals that agree to 9 decimals share one node set.

  • calculix.js
  • lines 61 to 80
  • PartCraft 0.8.1
// supports: axis directions as they are; other directions get a *TRANSFORM group (one per node)
const bnd = [], transforms = new Map();
for (let q = 0; q < cons.nodes.length; q++) {
  const node = cons.nodes[q] + 1, dirs = cons.dirs[q], skew = [];
  for (const d of dirs) {
    const ax = d.findIndex((v) => Math.abs(Math.abs(v) - 1) < 1e-9);
    if (ax >= 0) bnd.push(` ${node}, ${ax + 1}, ${ax + 1}, 0.`); else skew.push(d);
  }
  if (skew.length > 1) throw new Error('calculix: a node held along two slanted directions cannot be written (supports on slanted faces that meet)');   // i18n-ignore: internal check, the UI explains
  if (skew.length) { const k = skew[0].map((v) => v.toFixed(9)).join(','); (transforms.get(k) || transforms.set(k, { n: skew[0], nodes: [] }).get(k)).nodes.push(node); }
}
let ti = 0;
for (const { n, nodes } of transforms.values()) {
  ti++;
  L.push(`*NSET, NSET=SLANT${ti}`); for (let i = 0; i < nodes.length; i += 8) L.push(' ' + nodes.slice(i, i + 8).join(', '));
  const t = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0], d = t[0] * n[0] + t[1] * n[1] + t[2] * n[2], p = [t[0] - d * n[0], t[1] - d * n[1], t[2] - d * n[2]], pl = Math.hypot(...p);
  // rectangular system: local x = n, local y in the plane through the origin, n and p
  L.push(`*TRANSFORM, NSET=SLANT${ti}, TYPE=R`, ` ${n.map((v) => v.toFixed(9)).join(', ')}, ${p.map((v) => (v / pl).toFixed(9)).join(', ')}`);
  bnd.push(` SLANT${ti}, 1, 1, 0.`);
}

Nodal loads

64-bit

Writes the nodal forces of the study as concentrated loads.

one line per non-zero force component

The forces are the solver's own nodal load vector. Components that are exactly zero are left out; each written value has 11 significant digits, so the exported loads no longer sum exactly to the applied total.

  • calculix.js
  • lines 95 to 97
  • PartCraft 0.8.1
const cl = [];
for (let i = 0; i < model.n; i++) for (let a = 0; a < 3; a++) { const v = f[3 * i + a]; if (v !== 0) cl.push(` ${i + 1}, ${a + 1}, ${num(v)}`); }
if (cl.length) L.push('*CLOAD', ...cl);

Load increments

64-bit

The first and smallest load increment given to the other program.

first = 1 ÷ steps; smallest = 1 ÷ (64 × steps); total and largest = 1

Simple quotients, formatted with 11 significant digits.

  • calculix.js
  • line 93
  • PartCraft 0.8.1
L.push('*STEP', '*STATIC', ` ${num(1 / steps)}, 1., ${num(1 / (steps * 64))}, 1.`);

Reading the result file

64-bit

Reads node positions and result values from the text result file.

value = parseFloat(12-character field, D exponent read as E)

Each number is parsed from a 12-character text field into a 64-bit number. The precision of what is read is that of the text: a 12-character field with sign, point and exponent holds about 6 significant digits.

  • calculix.js
  • lines 102 to 133
  • PartCraft 0.8.1
/** Parse an ASCII .frd → { nodes: Map(id → [x, y, z]), blocks: [{ name, step, components: [name], values: Map(node → number[]) }] }. */
export function parseFrd(text) {
  const nodes = new Map(), blocks = [], lines = text.split(/\r?\n/);
  const field = (s, a, n) => s.slice(a, a + n);
  const read = (s) => { const v = parseFloat(s.replace(/D/g, 'E')); return v; };
  let i = 0;
  while (i < lines.length) {
    const ln = lines[i];
    if (/^\s*2C/.test(ln)) {   // node block
      i++;
      for (; i < lines.length && !/^ -3/.test(lines[i]); i++) {
        const s = lines[i]; if (!/^ -1/.test(s)) continue;
        nodes.set(parseInt(field(s, 3, 10), 10), [0, 1, 2].map((k) => read(field(s, 13 + 12 * k, 12))));
      }
    } else if (/^\s*100C/.test(ln)) {   // result block
      const step = (ln.match(/\s(\d+)\s*$/) || [])[1];
      const blk = { name: '', step: step ? parseInt(step, 10) : 0, components: [], values: new Map() };
      i++;
      let prev = null;
      for (; i < lines.length && !/^ -3/.test(lines[i]); i++) {
        const s = lines[i];
        if (/^ -4/.test(s)) blk.name = s.slice(5, 13).trim();
        else if (/^ -5/.test(s)) { const nm = s.slice(5, 13).trim(); if (nm !== 'ALL') blk.components.push(nm); }
        else if (/^ -1/.test(s)) { const id = parseInt(field(s, 3, 10), 10), vals = []; for (let p = 13; p + 12 <= s.length; p += 12) vals.push(read(field(s, p, 12))); blk.values.set(id, vals); prev = id; }
        else if (/^ -2/.test(s) && prev !== null) { const vals = blk.values.get(prev); for (let p = 13; p + 12 <= s.length; p += 12) vals.push(read(field(s, p, 12))); }
      }
      blocks.push(blk);
    }
    i++;
  }
  return { nodes, blocks };
}

In detail

Method
Deck keywords as listed in the file's header (C3D10 elements, *CLOAD nodal forces, penalty contact or *TIE). Result file read by fixed columns: a 10-character node number and 12-character number fields.
Number types
64-bit numbers formatted with toExponential(10) on the way out; parseFloat on the way in; displacements into a Float64Array.
Tolerances
A support direction counts as a coordinate axis when one component is within 1e-9 of ±1. Slanted directions are grouped and written at 9 decimals. Bonded pairs default to a position tolerance of 1.
Limits
Exported values carry 11 significant digits, not 17. A 12-character field that also holds the sign, the decimal point and the exponent has room for about 6 significant digits, so imported results are limited by the file format.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach