Absorption strategy plugins¶
When to write one¶
Write an absorption strategy for a new boundary damping profile or a
potential-based absorbing term. The strategy is wrapped by
BoundaryCondition, which exposes before-step and after-step actions to
solvers.
Contract¶
create_absorption_strategy calls
StrategyClass(grid, boundary_config, physics). Subclass
AbsorptionStrategy. Its abstract constructor declaration takes no
arguments, but the factory passes those three objects.
| Name and signature | Caller and timing | Return or state | Status |
|---|---|---|---|
__init__(grid, boundary_config, physics) |
Factory, once | Store profile and parameters | Required |
set_time_step(dt) |
BoundaryCondition, after construction |
Store dt if per-step damping needs it |
Optional override |
get_potential_distribution() |
Controller setup, once | Scalar or (ny, nx) potential term |
Abstract |
apply_absorption(psi) |
Solver after a step | Updated real-space field (ny, nx) |
Abstract |
after_step_is_noop |
CUDA chunk eligibility | True only if apply_absorption leaves psi unchanged |
Optional class attribute |
create_absorption_profile(ny, nx, boundary_config) builds a configured
edge profile. A strategy can use that helper or construct another profile.
Step-by-step¶
- Create
polarism_plugins/absorption/soft_edge.pyand register a name. - In
__init__, compute a profile with grid shape and save the configured strength. Usecompute_engine.xpfor backend operations. - Store
dtinset_time_step;BoundaryConditioncalls it before the simulation runs. - Return zero from
get_potential_distributionfor a pure after-step mask. Implementapply_absorptionto multiply the completedpsiby the mask. - Select
cfg.boundary_condition.absorptionand compare edge and center values in a small test.
Complete example¶
from polarism.boundary_conditions.absorption import register_absorption
from polarism.boundary_conditions.absorption.absorption_strategy import (
AbsorptionStrategy,
create_absorption_profile,
)
from polarism.compute_engine import compute_engine
@register_absorption("example-soft-edge")
class SoftEdgeAbsorption(AbsorptionStrategy):
after_step_is_noop = False
def __init__(self, grid, absorption_cfg, physics_constants):
self.xp = compute_engine.xp
self.profile = create_absorption_profile(grid.ny, grid.nx, absorption_cfg)
self.strength = absorption_cfg.strength
self.dt = None
def set_time_step(self, dt):
self.dt = dt
def get_potential_distribution(self):
return 0.0
def apply_absorption(self, psi):
return psi * self.xp.exp(-0.5 * self.strength * self.dt * self.profile)
create_absorption_profile supplies edge weights. The example returns zero
potential, then applies exp(-strength * dt * profile / 2) after each step.
It sets after_step_is_noop = False because that multiplication changes the
field. The physics_constants argument is accepted because the factory
passes it, though this particular mask does not use it.
GPU and performance notes¶
The profile and mask calculation use compute_engine.xp, so they remain on
CuPy for GPU runs. Both rk4-cuda and split-step-fft-cuda require an
after-step no-op for optimized chunks and CUDA graphs. A mask like this takes
their regular stepping path. For a CAP-style strategy, return the complex
potential in get_potential_distribution, leave psi unchanged in
apply_absorption, and then set after_step_is_noop = True.
Supported by solvers¶
| Solver family | Plugin strategy behavior |
|---|---|
rk4-fdm, rk4-fdm-fused, split-step-fft, etd-rk2, ip-rk4, ifrk-fft-cuda, ifrk4-fft-cuda |
Use the combined potential and after-step action |
rk4-cuda, split-step-fft-cuda |
Accept the strategy; post-step actions disable their optimized chunk path |
Pitfalls¶
- Do not apply the same damping in both the potential and mask paths unless that double action is intentional.
after_step_is_noop=Truemust reflectapply_absorptionbehavior, not just the absence of a Python exception.- A post-step mask is a separate operation after the integrator's stages.
Testing¶
cfg.boundary_condition.absorption = "example-soft-edge"
run = SimulationController(cfg)
field = np.ones((cfg.grid.ny, cfg.grid.nx), dtype=np.complex128)
damped = run.boundary_condition.after_step_action(field)
assert np.abs(damped).max() <= 1.0
The integration suite additionally runs two complete steps with this strategy.