Quickstart¶
This page creates a project from the femforge template, solves it and opens the result in a browser. The project is one script: a disc magnet pulls on a steel plate across an air gap, solved at four gaps on the axisymmetric section.
Before you start¶
You need uv, which installs Python and the packages, and either Docker, which runs the Elmer solver, or a native Elmer install. Install covers all three.
Create the project¶
The command writes main.py, a README.md and a .gitignore into magnet-plate/, then prints the command that runs the project. It refuses a folder that is not empty unless you pass --force.
Run it¶
The first run downloads everything once:
- uv reads the script's header and installs Python and
femforge[workbench]: about 1.2 GB, a few minutes. - When Elmer is not installed natively, femforge pulls the solver's Docker image, about 1.3 GB. When the pull fails, it builds the image instead, which takes a few minutes longer.
- The four solves run, each in seconds. The terminal logs the force on the plate for each gap and the run that holds it.
- Your browser opens the workbench at http://localhost:8080 on the magnetic field of the newest run, the last gap. Pick another run in the Run list to compare.
Press Ctrl+C to stop. A later run loads each solved gap from magnet-plate/runs/ instead of solving it again, and starts in seconds.
The script¶
# /// script
# requires-python = ">=3.12"
# dependencies = ["femforge[workbench]~=0.1.0"]
# ///
"""A disc magnet pulls on a steel plate across an air gap: solve four gaps, then browse the fields in a workbench."""
from pathlib import Path
import build123d as b3d
import femforge as ff
HERE = Path(__file__).parent
RUNS = HERE / "runs"
TELEMETRY = HERE / "runs-index"
GAPS = (0.5, 1.0, 2.0, 4.0)
"""`[mm]`: The air gaps to solve."""
# The results spell body names in capitals, so these stay uppercase.
MAGNET = "MAGNET"
PLATE = "PLATE"
N52 = ff.materials.N52()
STEEL = ff.materials.Steel_1080()
AIR = ff.materials.Air()
class MagnetAndPlate(ff.Build123dGeometry):
"""A disc magnet with a steel plate above it, both centered on the Z axis, inside a box of air."""
gap: float
"""`[mm]`: Air gap between the top of the magnet and the bottom of the plate."""
magnet_diameter: float = 20.0
"""`[mm]`: Diameter of the magnet disc."""
magnet_thickness: float = 5.0
"""`[mm]`: Thickness of the magnet disc."""
plate_diameter: float = 40.0
"""`[mm]`: Diameter of the steel plate."""
plate_thickness: float = 5.0
"""`[mm]`: Thickness of the steel plate."""
padding: float = 100.0
"""`[mm]`: Air added around the bodies, half on each side."""
def model(self) -> ff.TaggedCompound:
"""The magnet from Z = 0 up, the plate ``gap`` above it, and the air around both."""
def disc(diameter: float, thickness: float, bottom: float) -> b3d.Part:
return b3d.Pos(0, 0, bottom + thickness / 2) * b3d.Cylinder(diameter / 2, thickness)
magnet = ff.TaggedPart([disc(self.magnet_diameter, self.magnet_thickness, 0)], tags={ff.TagKey.MATERIAL: N52.tag}, name=MAGNET)
plate = ff.TaggedPart([disc(self.plate_diameter, self.plate_thickness, self.magnet_thickness + self.gap)], tags={ff.TagKey.MATERIAL: STEEL.tag}, name=PLATE)
box = ff.boundary_box(ff.TaggedCompound(children=[magnet, plate]), ff.Vec3(self.padding, self.padding, self.padding))
air = ff.TaggedPart([box], tags={ff.TagKey.MATERIAL: AIR.tag})
return ff.TaggedCompound(children=[magnet, plate, air])
def config(gap: float = 1.0) -> ff.SimulationConfig:
"""The magnetostatic solve of the magnet and the plate ``gap`` mm apart, on the axisymmetric section about Z."""
# Mesh sizes are in metres; the geometry is in millimetres.
fine = 0.0002
return ff.SimulationConfig(
meta=ff.RunMeta(description=f"{gap:g} mm gap"),
geometry=MagnetAndPlate(gap=gap),
frame=ff.Axisymmetric(),
mesh=ff.MeshConfig(global_mesh_size=0.002),
physics=ff.PhysicsSelection(selected=ff.Physics.MAGNETOSTATICS),
domain=ff.DomainConfig(
materials=[N52, STEEL, AIR],
tags=[
ff.EntityTag(selector={ff.TagKey.NAME: MAGNET}, conditions=[ff.Magnetization(direction=ff.Direction(0, 0, 1))], mesh_size=fine),
ff.EntityTag(selector={ff.TagKey.NAME: PLATE}, conditions=[ff.ForceComponent()], mesh_size=fine),
],
),
)
def solve() -> None:
"""Solve every gap and log the axial force on the plate, negative toward the magnet; a gap solved before loads from ``runs/``."""
driver = ff.Driver(runs_dir=RUNS, telemetry=TELEMETRY)
for gap in GAPS:
result = driver.evaluate(config(gap))
newtons = result.axial_force(PLATE).m_as("newton")
ff.log.success(f"{gap:g} mm gap: {newtons:.1f} N on the plate, run {result.run_id}")
def viewer() -> ff.workbench.Split:
"""The field viewer on the newest run, coloring the flux density with the air hidden."""
display = ff.DisplaySpec(field="magnetic flux density", mode=ff.DisplayMode.MAGNITUDE, hidden_bodies=("AIR",))
runs = ff.find_runs(RUNS)
return ff.workbench.field_viewer(runs[0] if runs else None, display=display, telemetry=TELEMETRY)
if __name__ == "__main__":
solve()
# A function, not a built dock, so every save of this file rebuilds the workbench.
ff.workbench.show(viewer, title="Magnet and Plate", window=ff.workbench.Window.BROWSER)
MagnetAndPlateis the geometry: a build123d model whose parts carry their material and name. See Geometry and tags.config()builds oneSimulationConfig: the axisymmetric frame, the mesh sizes, the materials and the conditions. See Conditions and physics, Frames and Mesh.solve()runs each gap through aDriverand reads the force from the typedResult. See Driver and runs and Results.viewer()andshow()serve the field viewer. See Workbench.
What to change next¶
- Change a dimension in
MagnetAndPlate, or add a gap toGAPS, and run the script again. femforge solves only the gaps it has not solved before. - Swap the magnet grade for
ff.materials.N42(). See Materials. - Change what the workbench shows first in
viewer(), such as the colormap or the hidden bodies. A save ofmain.pyrebuilds the open workbench. - Pin every package at its current version with
uv lock --script magnet-plate/main.py.
The examples go further: a bearing seat in 2D and 3D, a heated copper puck on an FR4 board, and a solenoid pulling a steel plunger.