Writing an input file#
An input file is a Python script that assembles one problem
dictionary and hands it to matto.driver.OptimizationDriver.
The examples under examples/ are the reference; the closest one to a
new problem is the right starting point.
from matto.driver import OptimizationDriver
from matto.design import volume_constraint
from matto.materials import HardMagneticSoftMaterial
problem = {
"mesh": mesh,
"mesh_serial": mesh_serial,
"design_variables": design_variables,
"boundary_conditions": boundary_conditions,
"traction_boundaries": traction_boundaries,
"load_steps": load_steps,
"load_cases": load_cases,
"material": HardMagneticSoftMaterial(**material_parameters),
"build_objective": build_objective,
"build_constraints": build_constraints,
"requested_output_fields": requested_output_fields,
"fem_options": fem_options,
"optimization_options": optimization_options,
"output_options": output_options,
}
OptimizationDriver(problem).run()
What the dictionary holds#
mesh,mesh_serialA dolfinx mesh on the working communicator, and a copy of it on
MPI.COMM_SELFon rank 0 (Noneelsewhere), used to gather the final fields into serial ordering for the saved arrays. 2D and 3D meshes are supported.design_variablesOne entry per design field. Each gives
active(optimized or prescribed),initial,bounds, and the chain ofoperatorsthat maps the raw field the optimizer controls to the physical field the material model reads:"rho": { "active": True, "initial": 0.5, "bounds": (0.05, 1.0), "operators": [ {"type": "density_filter", "radius": 1.0}, {"type": "heaviside", "beta_initial": 1.0, "beta_update_interval": 25, "beta_max": 4.0}, ], }
The raw field lives in
("DG", 0)and the physical field in("CG", 1)unlessraw_spaceandphysical_spacesay otherwise. An inactive variable takesprescribed_value(default:initial) as its physical field.fixed_regionsholds regions where the raw field is held at a value.boundary_conditions,traction_boundariesDirichlet conditions as
{name, on_boundary, value}and named facet sets on which load cases can apply tractions.load_steps,load_casesEvery load case is solved from the undeformed state, its body force, tractions and stimuli ramped together in
load_stepsequal increments. A load case names itstractionsby boundary and itsstimuliby name; the material declares which stimuli it needs and with what shape. Load cases carry aweightin the objective.materialA
matto.materials.Material: one of the shipped models, or a subclass defined in the input script itself. The material declares the design fields it reads, the stimuli it needs, and its parameters; a mismatch with the rest of the problem is reported at construction.build_objective(u_field, external_work, dx)Returns the objective as a UFL form.
external_workis the work of the applied loads, the compliance.build_constraints(design_variables, dx)Returns
{name: {"form", "normalize_by", "upper_bound"}}.matto.design.volume_constraint()builds the usual volume constraint. Constraints may depend on the design fields but not on the displacement.requested_output_fields,build_output_fieldsWhich fields go to the
.bpfiles.uand every<name>_rawand<name>_physare available;build_output_fieldscan add derived UFL expressions.fem_optionsquadrature_degreeandsolver_optionswithstate,adjointandfilterblocks. Each block takes PETSc options; the state block also takes the Newton tolerances.optimization_optionsmax_iter,opt_toland the MMAmovelimit.output_optionsoutput_dirandsim_output_interval.postprocessors(optional)A list of
matto.postprocess.PostProcessorobjects that the driver calls while it runs. Without the entry the driver behaves as before.
Following a run#
Three postprocessors come with the package:
from matto import DesignSnapshots, HistoryWriter, SnapshotPlotter
problem["postprocessors"] = [
HistoryWriter(), # history.csv, one row per iteration
SnapshotPlotter(every=10, # arrays and a picture every 10 iterations
direction="theta", weight="phi"),
]
DesignSnapshots saves the raw and physical
design fields and the displacement as snapshots/iter_NNNN.npz, and
the design at a failed state solve as failure_iter_NNNN.npz.
SnapshotPlotter adds a picture drawn with
matplotlib: one panel per field in 2D; in 3D the cells above a density
threshold as a body, one panel per camera view. Views are given as
views=[{"elev": 25, "azim": -60}, ...]; without them the body is
seen from above, and a plate-like body also from below. A parallel run
needs mesh_serial for both, as the final arrays do.
To draw something else, subclass SnapshotPlotter and override
draw(figure, data); to do something other than saving and drawing,
subclass PostProcessor and override the
hooks on_start, on_iteration, on_failure and on_finish.
A postprocessor that raises is reported and dropped, and the
optimization continues. One that gathers fields must do its gathers
before any work done on rank 0 only.
Using the driver without optimizing#
Construction does all the setup and validation. After that the object can be used for a single analysis or a gradient without an MMA step:
driver = OptimizationDriver(problem)
driver.forward() # raw -> physical fields
result = driver.evaluate() # every load case; objective,
# gradients, constraints
name, max_u = driver.solve_load_case(problem["load_cases"][0])
or the state problem alone:
from matto.state import StateProblem
state = StateProblem(problem, driver.design_variables)
max_u = state.solve(load_case, load_steps)