Automated FEA optimization loop.
This script runs outside FreeCAD, calling it as a subprocess
for each FEM analysis. This approach is more robust than running
everything inside a single FreeCAD session (which can leak memory
over hundreds of iterations).
from itertools import product
from typing import List, Dict, Tuple
WORK_DIR = "/tmp/bracket_optimization/"
FREECAD_PATH = "freecadcmd" # FreeCAD command-line executable
# === Parameter space ===
THICKNESSES = [2.0, 3.0, 4.0, 5.0, 6.0]
RIB_COUNTS = [0, 1, 2, 3, 4]
FILLET_RADII = [1.0, 2.0, 4.0, 6.0, 8.0]
def generate_and_export_bracket(
t: float, n_ribs: int, r_f: float, output_dir: str
"""Generate a bracket and export to STEP.
(step_file_path, mass_grams)
bracket = parametric_bracket(
volume_mm3 = bracket.val().Volume()
mass_g = volume_mm3 * 2.70e-6 * 1000 # Al 6061 density
step_path = os.path.join(output_dir, "bracket.step")
cq.exporters.export(bracket, step_path)
step_path: str, output_dir: str,
force_n: float, mesh_size: float
"""Write a FreeCAD Python script for FEM analysis.
Returns the path to the generated script.
STEP_FILE = "{step_path}"
OUTPUT_DIR = "{output_dir}"
RESULTS_FILE = os.path.join(OUTPUT_DIR, "results.json")
doc = FreeCAD.newDocument("BracketFEM")
Part.insert(STEP_FILE, doc.Name)
analysis = ObjectsFem.makeAnalysis(doc, "Analysis")
material = ObjectsFem.makeMaterialSolid(doc, "Al6061T6")
mat["Name"] = "Aluminum 6061-T6"
mat["YoungsModulus"] = "68900 MPa"
mat["PoissonRatio"] = "0.33"
mat["Density"] = "2700 kg/m^3"
analysis.addObject(material)
femmesh = ObjectsFem.makeMeshGmsh(doc, "FEMMesh")
femmesh.CharacteristicLengthMax = MESH_SIZE
femmesh.CharacteristicLengthMin = MESH_SIZE / 4
femmesh.ElementOrder = "2nd"
from femmesh.gmshtools import GmshTools
gmsh_tools = GmshTools(femmesh)
analysis.addObject(femmesh)
# Fixed constraint (bottom face)
fixed = ObjectsFem.makeConstraintFixed(doc, "Fixed")
for i, face in enumerate(shape.Shape.Faces):
if abs(face.CenterOfMass.z) < 0.1:
fixed.References = [(shape, "Face" + str(i+1))]
analysis.addObject(fixed)
force = ObjectsFem.makeConstraintForce(doc, "Force")
for i, face in enumerate(shape.Shape.Faces):
hasattr(face.Surface, 'TypeId') and
'Cylinder' in str(face.Surface)):
force.References = [(shape, "Face" + str(i+1))]
analysis.addObject(force)
solver = ObjectsFem.makeSolverCalculix(doc, "Solver")
solver.WorkingDir = OUTPUT_DIR
solver.AnalysisType = "static"
solver.GeometricalNonlinearity = "linear"
analysis.addObject(solver)
from femtools import ccxtools
fea = ccxtools.FemToolsCcx(analysis, solver)
msg = fea.check_prerequisites()
if hasattr(obj, "vonMises"):
vm = list(result_obj.vonMises)
"max_von_mises_MPa": max(vm),
"mean_von_mises_MPa": sum(vm)/len(vm),
results = {{"status": "no_results"}}
results = {{"status": "prereq_fail", "message": msg}}
results = {{"status": "error", "message": str(e)}}
with open(RESULTS_FILE, 'w') as f:
json.dump(results, f, indent=2)
FreeCAD.closeDocument(doc.Name)
script_path = os.path.join(output_dir, "fem_script.py")
with open(script_path, 'w') as f:
t: float, n_ribs: int, r_f: float
"""Run one complete generate → mesh → solve cycle.
Dictionary with parameters and results
# Create output directory for this configuration
config_name = f"t{t:.0f}_r{n_ribs}_f{r_f:.0f}"
config_dir = os.path.join(WORK_DIR, config_name)
os.makedirs(config_dir, exist_ok=True)
# Step 1: Generate geometry and export STEP
step_path, mass_g = generate_and_export_bracket(
t, n_ribs, r_f, config_dir
# Step 2: Write FEM script
script_path = write_fem_script(
step_path, config_dir, FORCE_N, MESH_SIZE
# Step 3: Run FreeCAD as subprocess
[FREECAD_PATH, "--console", script_path],
capture_output=True, text=True,
timeout=300, # 5 minute timeout per analysis
except subprocess.TimeoutExpired:
"t": t, "n_ribs": n_ribs, "r_f": r_f,
results_path = os.path.join(config_dir, "results.json")
if os.path.exists(results_path):
with open(results_path, 'r') as f:
fem_results = json.load(f)
fem_results = {"status": "no_output"}
"""Execute the full parameter sweep."""
os.makedirs(WORK_DIR, exist_ok=True)
total = len(THICKNESSES) * len(RIB_COUNTS) * len(FILLET_RADII)
print(f"Starting optimization: {total} configurations")
for i, (t, n_r, r_f) in enumerate(
product(THICKNESSES, RIB_COUNTS, FILLET_RADII)
print(f"[{i+1}/{total}] t={t}mm, ribs={n_r}, "
f"fillet={r_f}mm ... ", end="", flush=True)
result = run_single_analysis(t, n_r, r_f)
elapsed = time.time() - start
status = result.get("status", "unknown")
sigma = result["max_von_mises_MPa"]
print(f"OK ({elapsed:.1f}s) — "
f"mass={result['mass_g']:.1f}g, "
f"σ_max={sigma:.1f} MPa")
print(f"FAILED ({status})")
all_results.append(result)
output_path = os.path.join(WORK_DIR, "all_results.json")
with open(output_path, 'w') as f:
json.dump(all_results, f, indent=2)
print(f"\nResults saved to: {output_path}")
# all_results = run_optimization()
Comments