.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_tutorials/free_space/plot_09_transverse_window.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_tutorials_free_space_plot_09_transverse_window.py: Is my transverse window big enough? =================================== A free-space grid ends at a boundary: a radial grid has a hard wall at its outer radius, which reflects, and a Cartesian or line grid is periodic, so light that leaves one side comes back at the other. By default solprop puts no absorber there. It folds a **safety ramp** into the operator, which removes light only at the grid's largest transverse wavenumbers, and its **boundary monitor** reads the outer 5 % of the grid, the transverse **edge**. A run whose light reaches the edge warns, and stops at ten times the budget, naming a size to try. This tutorial sizes a radial window with the monitor, checks the result against a much wider window, and then asks for a designed real-space collar instead. It is the spatial companion of :doc:`/auto_tutorials/basics/plot_07_time_window`. The beam: a 30 fs, 800 nm Gaussian pulse focused to a 35 um waist in argon at one bar, with the Kerr response at 0.9 of the critical power, over 1 cm. That is a few Rayleigh lengths, so the beam spreads well beyond its waist, and the self-focusing throws a little light out at larger angles. The input shot noise is off (``noise=None``), so that grids of different sizes see the same launch. The free-space model's section on transverse boundaries (:doc:`/guide/free-space/model`) describes the ramp, the monitor and the collar. .. GENERATED FROM PYTHON SOURCE LINES 31-36 A window that is too small -------------------------- One function builds the beam on a requested radius, three waists by default, with ``fraction`` of that radius added outside it as room and ``absorber`` the transverse boundary. The radial spacing stays the same on every grid. .. GENERATED FROM PYTHON SOURCE LINES 36-97 .. code-block:: Python import math import time import warnings import numpy as np from scipy.special import j0 from solprop.boundaries import SpatialAbsorber from solprop.constants import C from solprop.driver import Simulation, SimulationSettings from solprop.exceptions import BoundaryError, BoundaryWarning from solprop.grid import FieldGrid, RadialGrid from solprop.monitor import DEFAULT_BOUNDARY_CHECK from solprop.nonlinear.responses import KerrResponse from solprop.physdata import gamma3 from solprop.pulses import GaussianPulse, SpatialPulse from solprop.spatial import SpatialMedium, critical_power from solprop.stepper import PIController LENGTH = 0.01 WAIST = 35.36e-6 RADIUS = 3 * WAIST """The requested radius: three waists.""" SAMPLES = 64 """Radial samples over the requested radius.""" def beam(fraction=0.0, absorber=None, check=True): """Return the beam on `RADIUS` with `fraction` of it added outside as room.""" grid = FieldGrid.from_limits(LENGTH, 800e-9, (500e-9, 1.6e-6), 250e-15) space = RadialGrid( RADIUS, round(SAMPLES * (1 + fraction)), collar_fraction=fraction ) medium = SpatialMedium("Ar", 1.0) chi3 = gamma3("Ar", wavelength=800e-9) density = float(medium.density(np.array([0.0]))[0]) index = float(np.real(medium.index(np.array([2 * math.pi * C / 800e-9]))[0])) power = 0.9 * critical_power(800e-9, chi3 * density, index=index) energy = power * 30e-15 * math.sqrt(math.pi / (4 * math.log(2))) settings = SimulationSettings( controller=PIController(rtol=1e-7, atol=0.0), save_planes=[LENGTH], dz_max=LENGTH, dz_init=LENGTH * 1e-4, ) return Simulation.from_radial( grid, space, medium, [KerrResponse(chi3, thg=False)], SpatialPulse( GaussianPulse(wavelength=800e-9, fwhm=30e-15, energy=energy), WAIST ), settings=settings, noise=None, spatial_absorber=absorber, boundary_check=DEFAULT_BOUNDARY_CHECK if check else None, ) .. GENERATED FROM PYTHON SOURCE LINES 98-101 On three waists the beam reaches the wall well before the end, and the monitor stops the run. The message names a radius to try, from how far the light walked in the distance it ran, and the samples that keep the spacing: .. GENERATED FROM PYTHON SOURCE LINES 101-110 .. code-block:: Python try: with warnings.catch_warnings(): # It warns first, as the edge passes the warning; the stop says it all. warnings.simplefilter("ignore", BoundaryWarning) beam().run(progress="none") except BoundaryError as error: print(f"stopped: {error}") .. rst-class:: sphx-glr-script-out .. code-block:: none stopped: light reached the transverse edge on its r face at z = 4.9229e-03 m: 1.01e-04 of the launch energy above its launch value, past the stop of 1e-04; the wall or the wrap returns it into the aperture. Try a radius of at least 0.000391 m, at the same spacing (n_r of at least 236); this is a lower bound, and light generated late may need more. A user who knows better raises BoundaryCheck.transverse_budget .. GENERATED FROM PYTHON SOURCE LINES 111-118 Reading what the monitor saw ---------------------------- Room outside the requested radius moves the wall out. ``collar_fraction`` adds it as a fraction of the radius, with no absorber unless one is asked for, so ``fraction=1`` puts the wall at twice the radius. Each run's record holds the edge's ``transverse_status`` and the largest share of the launch energy it read there above its launch, ``edge_max``. .. GENERATED FROM PYTHON SOURCE LINES 118-148 .. code-block:: Python RADII = np.linspace(0.0, RADIUS, 481) """Where the fields are compared: the requested radius.""" def run(simulation): """Run; return the field at the end on `RADII`, the edge's record and wall time.""" started = time.perf_counter() with warnings.catch_warnings(): warnings.simplefilter("ignore", BoundaryWarning) output = simulation.run(progress="none") wall = time.perf_counter() - started monitor = output.outcome.execution.get("boundary_monitor") or {} record = monitor.get("transverse", {}) # The Fourier-Bessel series of the output's columns, exact on any radii, so that # grids of different sizes are compared on the same points. hankel = simulation.spatial_grid.hankel coefficient = 2.0 / (hankel.radius**2 * hankel.j1**2) spectrum = np.asarray(output["field"][-1])[:, 0, :] field = j0(np.outer(RADII, hankel.k_r)) @ (coefficient[:, None] * spectrum) return field, record, wall for fraction in (1.0, 2.0): _, record, _ = run(beam(fraction)) print( f"wall at {1 + fraction:g} R: {record['transverse_status']}, " f"largest edge share {max(record['edge_max']):.1e} over {record['reads']} reads" ) .. rst-class:: sphx-glr-script-out .. code-block:: none wall at 2 R: warned, largest edge share 2.4e-05 over 19 reads wall at 3 R: clear, largest edge share 3.6e-09 over 19 reads .. GENERATED FROM PYTHON SOURCE LINES 149-157 At twice the radius the edge reads a few times 1e-5 of the energy, above the warning of 1e-6, so the run warns; at three times it is clear. Checking against a wider window ------------------------------- The monitor reads light *at* the edge, not light that came back from it. A run on a wall far further out says what each smaller window cost: the field error on the requested radius, against a wall at eight times the radius. .. GENERATED FROM PYTHON SOURCE LINES 157-180 .. code-block:: Python reference, _, reference_wall = run(beam(7.0, check=False)) def error(field): """Return the area-weighted field error on the requested radius.""" weight = 2 * np.pi * RADII difference = np.sum(weight[:, None] * np.abs(field - reference) ** 2) return math.sqrt(difference / np.sum(weight[:, None] * np.abs(reference) ** 2)) print(" wall status edge share field error wall time (s)") for fraction in (0.0, 1.0, 2.0): # Three waists would be stopped: it runs to the end unwatched here. field, record, wall = run(beam(fraction, check=fraction > 0)) status = record.get("transverse_status", "off") share = max(record["edge_max"]) if record.get("edge_max") else float("nan") print( f"{1 + fraction:5g} R {status:9s} {share:10.1e} {error(field):10.1e}" f" {wall:8.2f}" ) print(f" 8 R reference {reference_wall:8.2f}") .. rst-class:: sphx-glr-script-out .. code-block:: none wall status edge share field error wall time (s) 1 R off nan 1.0e-01 0.20 2 R warned 2.4e-05 4.0e-05 0.21 3 R clear 3.6e-09 8.9e-09 0.26 8 R reference 0.54 .. GENERATED FROM PYTHON SOURCE LINES 181-194 With the monitor off, three waists end about a tenth wrong: the stop was right. Twice the radius ends a few times 1e-5 wrong in field, as its warning said, and three times is clean, at about 1e-7. The monitor errs on the cautious side, since light at the edge has not yet come back; the radius the stop named, 3.7 radii, is a lower bound for the light seen so far. A collar on request ------------------- Instead of a wider wall, a real-space collar in the room outside the radius can absorb what reaches it. ``SpatialAbsorber()`` designs one from the launch's band of transverse wavenumbers and the room's width, and checks it at translation by propagating the launch through it; a design that cannot hold its budget is refused by name. Here it needs 2.6 radii of room: .. GENERATED FROM PYTHON SOURCE LINES 194-202 .. code-block:: Python field, record, wall = run(beam(2.6, SpatialAbsorber())) print( f"collar in 2.6 R of room: {record['transverse_status']}, field error " f"{error(field):.1e}, removed {record['spatial_removed']:.1e} of the energy, " f"{wall:.2f} s" ) .. rst-class:: sphx-glr-script-out .. code-block:: none collar in 2.6 R of room: clear, field error 1.7e-06, removed 7.7e-05 of the energy, 1.83 s .. GENERATED FROM PYTHON SOURCE LINES 203-225 The collar holds the error near 1e-6 and reads clear: its monitor reads the light it sends back, rather than the edge. For this beam it is not the better choice. A bare wall at three radii is more accurate and several times faster, because the collar's rate limits the step and adds work to each one. A collar earns its place where room is what you cannot afford: fast light at an angle, such as four-wave-mixing products or conical emission, which would need a far wider window to keep (:doc:`plot_06_four_wave_mixing_scan`). What the monitor cannot judge ----------------------------- The stop names a size from the light the run has seen. Light generated later, by a filament forming near the end, may need more. A weak signal at an angle, such as a four-wave-mixing sideband, can be far below the warning and still be what you measure: declare it, with ``SpatialAbsorber(kappa=...)`` or ``angle=...``, so that a collar is designed for it, or give it room and check it against a wider run, as above. **In short.** A run that finishes ``clear`` had no light at its edge above a millionth of its energy. When it warns or stops, give it the radius the message names, or room and a ``SpatialAbsorber``, and check what you measure against one wider run. ``spatial_absorber=False`` removes the safety ramp, and ``boundary_check=None`` the monitor. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 4.466 seconds) .. _sphx_glr_download_auto_tutorials_free_space_plot_09_transverse_window.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_09_transverse_window.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_09_transverse_window.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_09_transverse_window.zip `