sim-assembly-fea · git:20261025.0dcd877 · 2026-10-25 · sha256 8340192ccf082210

sim-assembly-fea git:20261025.0dcd877A

Immutable. This exact content never changes and is served at /api/v1/blob/8340192ccf082210.

---
name: sim-assembly-fea
description: Turn what a simulation of an assembly measured into a stress analysis of one part in it - run the assembly's `simulate:` entry, read the forces and moments acting on the body the part is welded into at the end of the run (the "after" state), work out which of them load the part and where, and add the part a new named FEA scenario (an entry of its `fea:` section, beside any it already has) that applies them as forces with directions in its own frame (with its apparent gravity), then run that scenario with `pc cae fea -f` and, in an IDE, show the result in the Digital Thread. Use for /pc:sim-assembly-fea, or when the user asks how a simulated motion stresses a part, wants an FEA "under the loads from the simulation", or asks whether a gear, a shaft or a bracket survives what a mechanism does to it.
---

# pc:sim-assembly-fea

Find out what a mechanism does to one of its parts, and analyse the part under
it. `$ARGUMENTS` names three things: the **assembly**, the **simulation** (the
name of an entry in its `simulate:`), and the **part** (a link path in the
assembly, such as `lower/knee-worm-gear/gear`, or a part name such as
`motion/worm_gear_28t` when only one placement of it is in the assembly). Ask
for whichever is missing; `pc --no-ansi info -a <assembly>` prints the
assembly's `simulate:` entries, and the result of a run lists its parts.

What you produce is a new **FEA scenario** -- one named entry of the part's
`fea:` section, which is a map of scenarios the way `simulate:` is a map of
simulations -- whose every number came out of the simulation, a run of that
scenario with its result, and a short account of how each load got from the
simulation to the part. You add a scenario; you never replace the section. A
part may already be analysed under other loads -- by hand, or from another
simulation or another moment of this one -- and those scenarios are somebody's
questions too.

A rigid-body simulation knows what acts on each **body**, and a body is often
several parts welded together. What passes between two parts of one body is
the body's own business: no simulation reports it. So reading the loads is
arithmetic, which the helper beside this file does for you, and turning them
into the part's loads is engineering, which you do and explain. Never present
the second as if it were the first.

Pass `--no-ansi` on every run (it is global and goes before the subcommand). Make
sure PartCAD is available as `/pc:init` describes.

## 1. Run the simulation

```sh
mkdir -p /tmp/pc-sim-fea
pc --no-ansi sim -a <assembly> -f <simulation> --json > /tmp/pc-sim-fea/sim.json 2> /tmp/pc-sim-fea/sim.log
```

`--json` prints the whole result on standard output and the log goes to
standard error, so keep them apart. Check, in this order:

- the run happened (`"error": null`); a run that did not is reported with what
  it said, and there is nothing to analyse;
- the result has `parts` -- which body each part is in -- and the bodies have
  `loads`. A plugin that reports neither is older than loads, or was run with
  `params: {loads: false}`: say so, and stop. MuJoCo reports every load;
  Gazebo reports what each joint passes and each body's weight, and names in
  `warnings` what it does not (a weld to the world, contacts, accelerations);
- what `warnings` says. A load an engine could not report is named there, and
  an analysis that leaves it out has to say so.

The loads this skill uses are the **`after`** reading -- the state the run ended
in. If the user asks about the worst moment rather than the end, run with
`params: {samples: N}` and pick the sample with the largest load on the part's
body; say which instant you used.

## 2. Read the loads in the part's frame

```sh
python <this skill's directory>/part_loads.py loads /tmp/pc-sim-fea/sim.json <part>
```

It finds the part's body and where the part sits in it, and prints every load on
that body with `at` (mm), `force` (N) and `torque` (N·m about `at`) in the
**part's own frame** -- the frame its ports, and so an FEA scenario, are
written in -- beside:

- `apparent_gravity`: the scene's gravity less the acceleration of the part's
  origin, `g - a`, in m/s² and the part's frame -- on a body that turns, its
  centre of mass's and what the turning adds that far from it. That is the
  scenario's `gravity:` below: the part's own weight, and what its acceleration
  flings it by;
- `total`: the loads added up. For a body every load of which was reported it is
  the body's mass times its acceleration; on Gazebo it need not be;
- `scenario`: the name the FEA scenario of these loads goes under (section 4) --
  the simulation's, and the reading's for any reading but `after`.

`point` puts a point (or with `--direction` a direction) of another part's frame
into this part's -- which is how you find where a mating part touches it:

```sh
python <this skill's directory>/part_loads.py point /tmp/pc-sim-fea/sim.json <other part> <part> X Y Z
```

Read each load: what it is (`kind`), what it comes from (`from`), and for a
joint its name (`joint`). Find the connection that made each joint in the ASSY
file -- its `joint:` or `<child link>-<target link>` -- and note which
interfaces of which parts it joins. `pc --no-ansi info <part>` lists the
part's interfaces, instances and ports, with each port's location in its frame.

## 3. Work out what loads the part

Every condition of an FEA scenario lands on a port of an interface the part
implements, so each load has to end at one. Three cases:

**The part is the whole body.** Every load on the body acts on the part. A joint's
load lands on the interface the connection used on this part; a contact lands on
the port nearest its `at`. Its gravity is `apparent_gravity`.

**A load acts at one of the part's own ports.** A joint whose connection is at
an interface of this part (its `at` is at one of the part's ports) passes its
force there: that is a `load:` on that port.

**The load reaches the part through what it does.** The part is welded into a
larger body, and a load on the body passes through it on its way somewhere:
a gear carries the torque of the shaft it is keyed to, a lever carries the force
at its tip, a bracket carries what hangs from it. Work it out by statics, and
write down how:

- Find the **path**: where the load enters the part and where it leaves. The
  ASSY says what the part is connected to; the loads say what is pushed where.
  Loads that bypass the part (a shaft's bending carried by its bearings, not by
  the gear on it) are not its loads.
- **Hold the part where its load leaves to the rest of its body** -- the bolts
  of a hub, the bore it is pressed on: those interfaces are its `fix:`, and the
  analysis finds their reactions. Fixing one side and loading the other is how a
  stress analysis is posed; the loads need not balance, the fixed side does.
- **Load it where the load enters**, as the force that does the job there. A
  gear transmitting a torque `T` about its axis carries, at the tooth in mesh,
  a tangential force `T / r` (`r` the pitch radius, in metres) along the pitch
  circle, and a separating force `T / r · tan α` (`α` the pressure angle, 20°
  unless the part says otherwise) toward its axis; a worm wheel's axial force is
  the worm's tangential one, `T / r · tan λ` for a lead angle `λ`, and is small --
  say whether you included it. Which tooth is in mesh is the one nearest the
  mating part: put the mating part's teeth (or its axis) into this part's frame
  with `point`, and take the nearest tooth port (`pc info` lists them). A torque
  the gear's own frame gives as `torque` about its axis is the `T` -- check the
  axis against the part's geometry, and the sign: the force has to turn the gear
  *against* the torque the rest of the body applies, which is what holds it.
- A moment that lands on one port has no spelling in a scenario (a `load:` is
  a force). Load two ports a known distance apart with the couple, or move the
  `fix:` there and load the rest.

Every load becomes a **force with a direction**, `[x N, y N, z N]` in the part's
frame, and the scenario's `gravity:` is `apparent_gravity`. Leave out a load of
nought; round to three or four significant figures.

## 4. Add the FEA scenario

On the part itself if the package being worked in declares it. A part from a
dependency -- a bought gear, a vendor's bracket -- is analysed through a
reference to it in the package that declares the assembly: the loads are a fact
about this product, not about the part. Name it after the part and the product,
and keep the source's geometry and ports. If such a reference already exists
from an earlier run, add to it rather than making another.

**Name the scenario after where its loads came from** -- the `scenario` that
`part_loads.py loads` printed: the simulation's name for its `after` reading
(`dropped`), and with the reading for any other (`dropped-t0.02` for a sample at
0.020 s). Use another only when the user asks for one (`dropped-worst` for the
worst moment, say). Engine-specific runs of one claim get the simulation's own
name too (`dropped_in_gazebo`). Then:

- **Read the part's `fea:` first** (`pc --no-ansi info <part>` prints its
  configuration, or read `partcad.yaml`). Every scenario already there stays
  exactly as it is.
- If a scenario **of that name is already there**, it is the one an earlier run
  of this skill wrote from the same reading: show the user the old loads beside
  the new and ask before replacing it, rather than overwriting a scenario
  somebody may have edited since.
- If the section is still in the old **unnamed form** -- `fea:` holding `fix:`
  and `load:` itself rather than names -- move what it holds under a name first
  (`default`, which is what PartCAD already calls it, or a better one if its
  comments say where it came from), then add yours beside it. Say that you did.
- If the simulation names the requirement it demonstrates (`feature:` on the
  `simulate:` entry), give the scenario the same `feature:`: the analysis is
  evidence for the same claim, and the IDE's Requirements tab lists it there.

```yaml
parts:
  knee-gear:
    type: alias
    source: //pub/robotics/parts/gobilda:motion/worm_gear_28t
    desc: The knee's worm gear, as the arm loads it
    fea:
      # Any scenario already here stays; this one is added beside it.
      dropped:
        desc: At rest at the end of 'dropped', held by the worm at tooth 13
        # Who reads a direction and a gravity: an implementation whose 'reads:'
        # lists 'force' and 'gravity'. CalculiX, Elmer, Code_Aster and MOOSE do.
        implementation: //pub/feature/cae/calculix:fea
        # Where its load leaves: bolted to the hub by its four threaded holes.
        fix:
          //pub/robotics/parts/gobilda:4xM4-16mm-pattern-threaded-thru-4mm: [outer]
        load:
          # Where it enters: the knee's -2.59 N*m about the gear's axis, held by the
          # worm at tooth 13 (pitch radius 17.75 mm): 145.9 N along the pitch circle
          # and 53.1 N toward the axis (20 deg pressure angle).
          "//pub/robotics/parts/gobilda:gear-28t;module=1.25,shift=0.2":
            "13-tooth": [0 N, -2.7 N, -155.2 N]
        # g - a in the gear's frame: its own weight, standing on its edge.
        gravity: [0, -8.962, -3.990]
```

The keys of a scenario besides the conditions: `desc:` (say which simulation,
which reading, which engine), `feature:`, `implementation:`, and `params:` --
the implementation's parameters for this scenario alone, such as a finer
`mesh_size` where the load is concentrated. A scenario cannot be named after any
of those keys or after a condition (`fix`, `load`, `gravity`).

- A name under an interface is an instance of it, or one of its **ports** -- the
  way one tooth of a gear, whose teeth are the ports of one instance, is named.
  `pc info <part>` lists both.
- `implementation:` has to name one that **reads** what the scenario states: a
  direction (`force`) and a `gravity`. PartCAD refuses an analysis to one that
  does not, saying so, rather than letting it pull every load straight down.
- The material: an alias may carry its own `cae: fea:` -- `youngs_modulus`,
  `poissons_ratio`, `yield_strength` (Pa), `density` (kg/m³) -- for what the part
  is made of, when the implementation's defaults (steel) are not it. That is the
  implementation's configuration for every scenario of the part, not part of
  yours: add it only if it is missing, and say where the numbers come from.
- Comments say, for every condition, which load of which simulation it came
  from, at what time, and the arithmetic. A reader has to be able to redo it.

## 5. Run the analysis

Run your scenario by name -- `-f` -- so that the part's other scenarios are not
re-run for this question:

```sh
pc --no-ansi cae fea -f <scenario> :<part> 2>&1
```

The log carries the peak stress and displacement the implementation measured,
and the **findings** -- what it wants acted on (an empty list is a pass). Read the
warnings too: a condition that reached no material did nothing, and an analysis
with a condition doing nothing is answering another question. A port whose
sketch "lies on no surface" (a gear's tooth, whose sketch is its cross-section
at the pitch circle; a hole's outline over the hole) is applied to the surface
within `port_radius` of the port instead, and PartCAD says so and how much
area that was: check it is the surface you meant. CalculiX applies every
condition to the mesh nodes near its port either way, and names a port it found
no node near. Any implementation whose `reads:` lists `force` and `gravity`
will do -- CalculiX, Elmer, Code_Aster, MOOSE; on the same part they should
agree to within what their meshes differ by. The model is written
beside the package as `<part>.fea.<scenario>.glb`. `pc test -f fea :<part>` runs
every scenario of the part as a check, yours included.

If the daemon was already running when you edited an ASSY file, `pc daemon stop`
first: it rereads a changed `partcad.yaml` on its own, and an ASSY file it does
not.

## 6. Show it, where there is somewhere to show it

In an IDE -- `TERM_PROGRAM` is `vscode` (Visual Studio Code, VSCodium, the
PartCAD IDE), or `pc --no-ansi ide state` answers rather than saying no IDE is
running -- open the result in the Digital Thread, on its FEA tab, with your
scenario selected in the tab's list:

```sh
pc --no-ansi ide view --analysis-fea --scenario <scenario> :<part>
```

Opening the tab runs the selected scenario, as clicking it does, and shows the
stresses with the findings under them. Elsewhere, say where the model file is.

## 7. Report

- The simulation and the instant (`after`, at `t` s), and the engine.
- Every load on the part's body, and what became of it: a condition on which
  port, or why it does not load the part.
- The scenario you added -- its name, the part and the file -- and any scenario
  you moved, renamed or (with the user's word) replaced.
- What the analysis measured (peak stress against the allowable, displacement)
  and its findings, and what was left out: an axial force, a contact the engine
  did not report, a moment that was not spelled.