Scene, Simulation, Result
Maxwell's three-object model, what each object owns, and what happens between declaring a scene and reading results.
Every Maxwell run follows the same path: you declare a Scene, wrap it in a Simulation that names the solver and its settings, and call run() to get a Result. This page explains what each object owns and where the expensive work happens.
Scene: what to simulate#
A Scene is a declarative description. It holds:
- the physical
Domain(axis bounds in metres), theGridSpecand theBoundarySpec; Structureobjects, each a geometry plus a material, andMaterialRegiondesign regions;- sources, monitors, ports, lumped elements, circuits, networks and thin wires;
- the target
device(default"cuda"), subpixel averaging and symmetry settings.
Building a scene does no numerical work. The add_* methods append and return the scene, so you can chain them:
scene = (
mw.Scene(domain=mw.Domain(bounds=((-0.5, 0.5),) * 3), grid=mw.GridSpec.uniform(0.02),
boundary=mw.BoundarySpec.pml(num_layers=10), device="cuda")
.add_structure(mw.Structure(name="slab", geometry=mw.Box(position=(0, 0, 0), size=(1.0, 1.0, 0.1)), material=mw.Material(eps_r=2.25)))
.add_source(mw.PointDipole(position=(0, 0, -0.2), polarization="Ex", source_time=mw.GaussianPulse(frequency=3e9, fwidth=1e9)))
.add_monitor(mw.PlaneMonitor("xz", axis="y", position=0.0, fields=("Ex",), frequencies=(3e9,)))
)Because the scene is only a description, you can build it from PyTorch tensors (a box position or size, a MaterialRegion density array) and keep the autograd graph intact. scene.clone(**overrides) copies a scene with some arguments replaced.
SceneModule#
For optimization code, subclass SceneModule (a torch.nn.Module), store design parameters as nn.Parameter, and implement to_scene(). You pass the module itself to Simulation; Maxwell calls to_scene() on every run, so each run sees the current parameter values.
class Design(mw.SceneModule):
def __init__(self):
super().__init__()
self.density = torch.nn.Parameter(torch.full((20, 20, 4), 0.5, device="cuda"))
def to_scene(self):
scene = base_scene() # your function that adds domain, sources and monitors
scene.add_material_region(
mw.MaterialRegion(
name="design",
geometry=mw.Box(position=(0.0, 0.0, 0.0), size=(0.4, 0.4, 0.08)),
density=self.density,
eps_bounds=(1.0, 4.0),
)
)
return scene
sim = mw.Simulation.fdtd(Design(), frequencies=[3e9])Simulation: how to solve it#
Simulation binds a scene to a method and its settings. You create it with a constructor for each solver:
| Constructor | Solver | Returns |
|---|---|---|
Simulation.fdtd(scene, frequencies=..., ...) | Time-domain Yee solver, the primary runtime | Simulation |
Simulation.fdfd(scene, frequency=..., ...) | Single-frequency sparse solver, limited and experimental | Simulation |
Simulation.electrostatic(scene, ...) | Laplace/Poisson solve | a runner whose run() returns Result |
Simulation.capacitance(scene, ...) | N-terminal capacitance matrix | a runner whose run() returns Result |
Simulation.batch(simulations, execution=...) | Many variants of one FDTD scene, experimental | BatchSimulation |
frequencies are the frequencies, in hertz, at which FDTD accumulates spectra for monitors and full-field output. They do not have to match the source spectrum exactly, but they should lie inside it.
Prepare and run#
Simulation.run() validates the scene, compiles it, runs the solver and wraps the output. Compilation turns the declarative scene into solver state: it builds the Yee-grid coordinates (adding PML cells outside the domain), samples every structure and material onto the six staggered field components with subpixel averaging, compiles sources and monitors to grid indices, and chooses the time step. This state lives on an internal prepared scene, not on your Scene object.
Simulation.prepare() does the validation and compilation without stepping and returns a prepared simulation with its own run(). Use it when you want errors early, or to run the same compiled setup more than once:
prepared = sim.prepare() # raises here if a feature combination is unsupported
result = prepared.run()Maxwell checks feature combinations at preparation time and raises a specific error rather than silently falling back. For example, a trainable input that has no implemented reverse path fails in prepare(), before any time step runs.
Result: what came out#
Result holds everything the run produced, as PyTorch tensors on the scene device:
result.monitor(name): a dictionary per monitor (field components, coordinates, flux, power, mode amplitudes);result.E: the full-grid electric field spectrum when you ran withfull_field_dft=True;result.materialsandresult.material(name): the compiled permittivity and permeability;result.ports,result.network,result.circuitsfor RF runs;result.stats(): time step, step count, timing, memory and boundary settings;result.plot.field(...),result.plot.material(...): quick Matplotlib views;result.save(path): write the result to disk.
With several frequencies, result.at(frequency=...) selects one of them. See Read and plot results and the Result API.