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), the GridSpec and the BoundarySpec;
  • Structure objects, each a geometry plus a material, and MaterialRegion design 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:

ConstructorSolverReturns
Simulation.fdtd(scene, frequencies=..., ...)Time-domain Yee solver, the primary runtimeSimulation
Simulation.fdfd(scene, frequency=..., ...)Single-frequency sparse solver, limited and experimentalSimulation
Simulation.electrostatic(scene, ...)Laplace/Poisson solvea runner whose run() returns Result
Simulation.capacitance(scene, ...)N-terminal capacitance matrixa runner whose run() returns Result
Simulation.batch(simulations, execution=...)Many variants of one FDTD scene, experimentalBatchSimulation

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 with full_field_dft=True;
  • result.materials and result.material(name): the compiled permittivity and permeability;
  • result.ports, result.network, result.circuits for 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.