SIMP topology optimization of a beam to half its material, compared with the solid beam.
uv run python examples/cli.py run topology_optimization
compliance, solid (100% material) 0.0099 compliance, optimized (50% material) 0.0166 ratio 1.68x
The functions that pose and solve the problem. The figures are below the fold.
"""SIMP topology optimization of a simply supported beam to half its material. `mbb_conditions`, `solve_solid`, and `optimize` each state and solve one problem; `run` calls them and returns a `TopologyStudy` of plain results. Nothing here draws: `figures.py` does that from the study, and this file is what the gallery shows. """ from dataclasses import dataclass import numpy as np from fem.analysis.design import DesignHistory, DesignOptimizer, SIMPModel, calculate_smoothing_matrix from fem.boundary import Dirichlet, Neumann from fem.conditions import Conditions from fem.mesh.mesh import Mesh from fem.physics.equations import LinearElastic from fem.post.solution import ElasticSolution from fem.regions import in_box, intersect, on_plane E, NU = 200.0, 0.4 equation = LinearElastic(E, NU) def mbb_conditions(mesh) -> Conditions: """A simply supported (MBB) beam, the classic topology-optimization test: pinned at one bottom corner, a vertical roller at the other, a downward load at the top centre.""" w = np.max(mesh.vertices[:, 0]) h = np.max(mesh.vertices[:, 1]) bottom, top = on_plane(1, 0.0), on_plane(1, h) return Conditions( Dirichlet(intersect(bottom, in_box([None, None], [0.04 * w, None])), [0, 0]), Dirichlet(intersect(bottom, in_box([0.96 * w, None], [None, None])), [None, 0]), # A load over the central fifth of the top rather than a point, so it lands on a # boundary edge on any mesh, including the tiny smoke-test one. Neumann(intersect(top, in_box([0.4 * w, None], [0.6 * w, None])), [0, -0.5]), ) def solve_solid(mesh, bc) -> ElasticSolution: """The solid block: 100% material, the baseline the optimized one is measured against.""" return equation.problem(mesh, bc).solve() def optimize(mesh, bc, iters, smoothing_radius=0.05) -> tuple[DesignOptimizer, DesignHistory]: """Where to put half the material. Compliance is u.f, the work the load does, so a lower value is a stiffer structure; SIMP minimizes it under the volume constraint. The smoothing radius is a physical length, so a finer mesh resolves the same structure rather than growing thinner members.""" model = SIMPModel(equation.problem(mesh, bc), sensitivity_filter=calculate_smoothing_matrix(mesh, smoothing_radius)) design = DesignOptimizer(model, volume_frac=0.5, iters=iters, move=0.1) return design, design.run() @dataclass class TopologyStudy: """Everything `run` computed, for the figures and the summary to read.""" mesh: Mesh bc: Conditions solid: ElasticSolution optimized: ElasticSolution history: DesignHistory @property def aspect(self) -> float: return float(np.max(self.mesh.vertices[:, 0]) / np.max(self.mesh.vertices[:, 1])) @property def compliance_solid(self) -> float: return float(self.solid.compliance.sum()) @property def compliance_opt(self) -> float: return float(self.history.objective[-1]) @property def ratio(self) -> float: """The optimized beam's compliance as a fraction of the solid one's.""" return self.compliance_opt / self.compliance_solid def run(mesh, iters=60) -> TopologyStudy: """Solve the solid beam, then optimize half its material away.""" bc = mbb_conditions(mesh) solid = solve_solid(mesh, bc) design, history = optimize(mesh, bc, iters) assert design.solution is not None return TopologyStudy(mesh, bc, solid, design.solution, history)
"""The figures and summary of the topology optimization demo, drawn from a `TopologyStudy`.""" from functools import partial import numpy as np from demo_registry import Demo, DemoResult, Figure from demos._charts import conditions_figure from demos.topology_optimization import physics from demos.topology_optimization.physics import TopologyStudy, run from fem.mesh.structured import box_mesh from fem.plot.plotter import Plotter def _comparison_figure(s: TopologyStudy) -> Figure: solid_disp = np.linalg.norm(s.solid.nodal_values, axis=1) # Explicit figsize: two 4:1 panels stacked, each filling its row. comparison = Plotter(2, 1, figsize=(6.5, 4.6), title='Half the material, comparable stiffness') comparison.plot(s.solid.deformed_mesh(), solid_disp, mode='colored', idx=(0, 0), label='|u|', title=f'Solid: 100% material, compliance {s.compliance_solid:.3f}') comparison.plot(s.optimized.deformed_mesh(), s.history.rho[-1], mode='colored', idx=(1, 0), label='density', title=f'Optimized: 50% material, compliance {s.compliance_opt:.3f} ' f'({s.ratio:.2f}x)') return Figure( comparison, 'The same simply supported beam under the same central load, solid and then ' 'with half its material removed by optimization, both drawn deformed. ' 'Compliance is the work the load does, so it measures deflection under load. ' f'The optimized truss is only {s.ratio:.2f}x as compliant as the fully solid ' 'block on half the material; what it removed was near the neutral axis, ' 'where it was barely resisting the bending.', 'comparison') def _animation_figure(s: TopologyStudy) -> Figure: animation = Plotter(title='Topology optimization', panel_aspect=s.aspect) animation.plot_animation(s.mesh, s.history.rho, mode='colored', label='density') return Figure( animation, 'Density evolving over the SIMP iterations, from an even grey to the ' 'black-and-white truss.', 'animation') def _conditions_figure(s: TopologyStudy) -> Figure: return conditions_figure( s.mesh, s.bc, 'Simply supported, pinned at one bottom corner (both directions held) with a ' 'vertical roller at the other (free to slide horizontally), and a downward ' 'load at the top centre.', panel_aspect=s.aspect) def _summary(s: TopologyStudy) -> str: return (f'compliance, solid (100% material) {s.compliance_solid:.4f}\n' f'compliance, optimized (50% material) {s.compliance_opt:.4f}\n' f'ratio {s.ratio:.2f}x') def demo(mesh, **kwargs) -> DemoResult: """SIMP topology optimization of a beam to half its material, compared with the solid beam.""" s = run(mesh, **kwargs) return DemoResult([ _comparison_figure(s), _animation_figure(s), _conditions_figure(s), ], text=_summary(s)) # A 4:1 simply supported (MBB) beam, the aspect that optimizes into the classic arch. DEMO = Demo('topology_optimization', demo, section='Solids & structures', domain=partial(box_mesh, [[0.0, 0.0], [4.0, 1.0]], (160, 40)), smoke_kwargs={'iters': 3}, show_source=physics)