This class defines solutions to instances of the Problem class. They usually result from calling a topo_solver with a problem object, but can also be instantiated manually by passing a problem and a density distribution.
from dl4to.solution import Solution
from dl4to.datasets import BasicDataset
problem = BasicDataset(resolution=40).ledge()
shape = [40, 4, 8]
θ = torch.zeros(1, *shape)
solution_zeros = Solution(problem, θ)
camera_position = (0, 0.35, 0.2)
solution_zeros.plot(camera_position=camera_position,
display=False)
As we can see, the density distribution $\theta$ has been modified inside of the solution object such that it is not $0$ everywhere anymore. More precisely, it has been adjusted according to the design space information that we prescribed in the problem formulation: We enforced densities of $1$ at certain voxels by setting $\Omega_\text{dirichlet}$ to $1$.
We can check that the density distribution inside of the solution object has indeed been modified and this is not just the visualization:
assert torch.any(solution_zeros.θ != 0)
assert torch.all(θ == 0)
The easiest type of solution is what we call a "trivial solution". The density distribution of a trivial solution in $1$ everywhere, where it is permitted (i.e. where $\Omega_\text{design}$ is not $0$). Together with the zero-density example above this therefore constitutes the simplest solution to a TO problem - we simply choose the thickest possible structure!
Each problem object directly comes with its own trivial solution. It can be accessed via:
trivial_solution = problem.trivial_solution
trivial_solution.plot(camera_position=camera_position,
display=False)
In order to evaluate the stresses we need to solve the partial differential equation (PDE) of linear elasticity. This library comes with its own in-built finite differences method (FDM) solver, which solves the PDE for us. We found handling with finite differences easier than with finite elements. This is attributed to the regular grid structure, which makes the FDM a suitable and intuitive approach. It is however also possible to include custom PDE solvers, e.g., learned PDE solvers - which we will discuss later.
PDE solvers are passed to problem instances:
from dl4to.pde import FDM
problem.pde_solver = FDM()
Passing PDE solvers to problems (instead of passing them to solutions or TO solvers) my seem unintuitive at first, but it comes with several advantages: First, all solution objects that are derived from this problem will also have access to the PDE solver. The same holds for all TO solver algorithms that we apply to this problem. Second, our implementation automatically constructs most of the PDE system matrix in the background when it is passed to a problem. This saves a lot of time for all future evaluations.
We can now use this FDM solver to solve the PDE. This is done via the "solve_pde" command, which returns three tensors:
- The displacements $u$, which is a ($3\times n_x \times n_y \times n_z$)-tensor.
- The stresses $\sigma$, which is a symmetric ($9\times n_x \times n_y \times n_z$)-tensor.
- The von Mises stresses $\sigma_\text{vM}$, which is a ($1\times n_x \times n_y \times n_z$)-tensor, i.e., a scalar field.
u, σ, σ_vm = trivial_solution.solve_pde()
After the PDE has been solved for a solution, the displacements $u$ are stored inside the solution object and can be accessed via "trivial_solution.u". This avoids solving the same PDE several times.
In order to check if the von Mises stresses are too large, we need to compare its maximum to the yield stress $\sigma_\text{ys}$ of the material:
σ_vm.max() / problem.σ_ys
Since the fraction returns a value below $1$, this indicates that the structure indees holds the applied forces and does not break!
We can also visualize the spacial distribution of the (normed) displacements and von Mises stresses by passing "solve_pde=True" to the plotting function:
trivial_solution.plot(camera_position=camera_position,
solve_pde=True,
display=False)