← all demos

topology_optimization

SIMP topology optimization of a beam to half its material, compared with the solid beam.

uv run python examples/cli.py run topology_optimization

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. The optimized truss is only 1.68x 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.
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. The optimized truss is only 1.68x 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.
Density evolving over the SIMP iterations, from an even grey to the black-and-white truss.
Density evolving over the SIMP iterations, from an even grey to the black-and-white truss.
compliance, solid (100% material)     0.0099
compliance, optimized (50% material)  0.0166
ratio                                 1.68x

What was imposed

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.
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.

Source

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 the demo that assembles them
"""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)