.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_tutorials/waveguides/plot_06_graded_index.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_waveguides_plot_06_graded_index.py: Self-imaging in a graded-index guide, and a launch declared as a field ====================================================================== The general modal route (:doc:`/guide/waveguides/general-modal`) takes any provider that solves its own three-component E and H, not only a step-index strand. This example builds the **parabolic** graded-index sector, checks its two radial channels against the analytic weak-guidance closed form, and propagates the beat between them to the length at which the transverse pattern repeats -- self-imaging. It also shows the two ways of saying what is launched: into a named channel, or as a declared transverse **field** that the route projects onto the channels and reports what it captured. .. GENERATED FROM PYTHON SOURCE LINES 20-60 .. code-block:: Python import matplotlib.pyplot as plt import numpy as np from solprop.constants import C from solprop.driver import Simulation from solprop.grid import FieldGrid from solprop.modes import ( GradedIndexProvider, IndexMaterial, ModalGuide, PowerLawPermittivity, ) from solprop.translate.general import general_modal_route from solprop.translate.modal_tables import ( project_transverse_field, sample_modal_table, ) RADIUS = 25e-6 """Core radius, m.""" CONTRAST = 0.005 """Index contrast Delta, dimensionless: eps = eps_axis (1 - 2 Delta rho^2).""" AXIS_INDEX = 1.45 """Index on the axis.""" WAVELENGTH = 1.03e-6 """Reference wavelength, m.""" ELEMENTS = 64 """Radial P1 elements per core radius. The two propagation constants converge on the closed form at about second order in this count; their **difference**, which sets the imaging length, is three orders worse in relative terms because it is a small difference of two large numbers. """ .. rst-class:: sphx-glr-script-out .. code-block:: none 'Radial P1 elements per core radius.\n\nThe two propagation constants converge on the closed form at about second order\nin this count; their **difference**, which sets the imaging length, is three\norders worse in relative terms because it is a small difference of two large\nnumbers.\n' .. GENERATED FROM PYTHON SOURCE LINES 61-65 The guide. A radial provider owns its sector, its channel count, its element count and its matching radius; the ``LP`` family is the scalar weak-guidance approximation, which is what the parabolic closed form below describes. No response is declared, so this is a linear guide and the run is pure dispersion. .. GENERATED FROM PYTHON SOURCE LINES 65-75 .. code-block:: Python provider = GradedIndexProvider( permittivity=PowerLawPermittivity(IndexMaterial(AXIS_INDEX), CONTRAST), exterior=IndexMaterial(AXIS_INDEX * np.sqrt(1.0 - 2.0 * CONTRAST)), radius=RADIUS, family="LP", count=2, elements=ELEMENTS, extent=2.0, ) .. GENERATED FROM PYTHON SOURCE LINES 76-83 The grid, and the guide's knots across its whole **active** band, which starts at half the lowest declared frequency and so reaches 2.1 um here. Thirteen knots, and the count matters twice: below nine the interpolated basis is further from orthonormal *where this pulse is* than the route will call the identity, and it is refused by name; and below thirteen it still leaves that budget at the long-wavelength end of the active band, where this pulse puts nothing but a broadening run would, which the route warns about by name. .. GENERATED FROM PYTHON SOURCE LINES 83-92 .. code-block:: Python grid = FieldGrid.from_limits(1e-3, WAVELENGTH, (1.0e-6, 1.06e-6), 100e-15) active = grid.omega[grid.active_mask] guide = ModalGuide.from_provider( provider, np.linspace(0.99 * float(active[0]), 1.01 * float(active[-1]), 13), nodes=provider.node_rule(0.99 * float(active[0]), n_r=2), ) print([label.display_label for label in guide.mode_collection.channel_labels]) .. rst-class:: sphx-glr-script-out .. code-block:: none ['LP0,1', 'LP0,2'] .. GENERATED FROM PYTHON SOURCE LINES 93-97 The closed form. An ideal parabolic profile has ``beta_g = sqrt(q^2 - 2 q sqrt(2 Delta) g) / a`` with ``q = omega n_axis a / c`` and the group number ``g = 2m + l + 1``, which for these two ``l = 0`` channels is 1 and 3. Their spacing sets the self-imaging length ``2 pi / Delta beta``. .. GENERATED FROM PYTHON SOURCE LINES 97-111 .. code-block:: Python sampling = guide.sampling table = sample_modal_table( guide.field_provider, sampling.omega, np.array([sampling.z]), sampling.nodes ) centre = int(np.argmin(np.abs(grid.omega - grid.omega0))) omega = float(grid.omega[centre]) solved = np.real(1j * np.diag(table.evaluate(omega, 0.0).generator)) q = omega / C * RADIUS * AXIS_INDEX ideal = np.sqrt(q * q - 2.0 * q * np.sqrt(2.0 * CONTRAST) * np.array([1, 3])) / RADIUS length = float(2.0 * np.pi / (ideal[0] - ideal[1])) print(f"solved spacing {solved[0] - solved[1]:.3f} 1/m") print(f"ideal spacing {ideal[0] - ideal[1]:.3f} 1/m") print(f"imaging length {length * 1e6:.2f} um") .. rst-class:: sphx-glr-script-out .. code-block:: none solved spacing 8014.428 1/m ideal spacing 8007.321 1/m imaging length 784.68 um .. GENERATED FROM PYTHON SOURCE LINES 112-115 A continuous wave in both channels at once, propagated to that length in ten steps. One coarse bin is exactly periodic on the time window, so the beat is the two channels' relative phase and nothing else. .. GENERATED FROM PYTHON SOURCE LINES 115-134 .. code-block:: Python simulation = Simulation.from_waveguide( (), length, provider=guide, grid=grid, lambda0=WAVELENGTH, fwhm=40e-15, energy=1e-15, pulseshape="gauss", n_save=9, dz_max=length / 10, dz_init=length / 10, backend="numpy_ref", ) launch = np.zeros((2, 1, grid.n_omega), dtype=complex) launch[:, 0, centre] = 1.0 run = simulation.with_input(launch).run(progress="none") planes = run.planes() .. GENERATED FROM PYTHON SOURCE LINES 135-138 The transverse intensity along the beat. The two channels' patterns add in phase at the entrance, cancel on the axis half a beat later, and come back at the imaging length: the guide has reproduced its own launch. .. GENERATED FROM PYTHON SOURCE LINES 138-161 .. code-block:: Python sample = table.evaluate(omega, 0.0) nodes = sampling.nodes radius = np.hypot(nodes.xy[:, 0], nodes.xy[:, 1]) order = np.argsort(radius) fig, ax = plt.subplots(figsize=(6, 3.5), constrained_layout=True) for plane in (planes[0], planes[len(planes) // 2], planes[-1]): pattern = np.einsum("qcm,m->qc", sample.electric, plane.field[:, 0, centre]) intensity = np.sum(np.abs(pattern) ** 2, axis=1) ax.plot( radius[order] * 1e6, intensity[order] / intensity.max(), label=f"z = {plane.z * 1e6:.0f} um", linewidth=1.2, ) ax.set( xlabel="Radius (um)", ylabel="Normalised transverse intensity", title="Self-imaging of a parabolic graded-index guide", xlim=(0.0, 1.5 * RADIUS * 1e6), ) ax.legend(fontsize="small") plt.show() .. image-sg:: /auto_tutorials/waveguides/images/sphx_glr_plot_06_graded_index_001.png :alt: Self-imaging of a parabolic graded-index guide :srcset: /auto_tutorials/waveguides/images/sphx_glr_plot_06_graded_index_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 162-165 The residual phase after a full turn is the solved spacing's departure from the ideal one, and nothing else: the guide reproduces its own dispersion, and what is left over is the P1 discretisation of an analytic answer. .. GENERATED FROM PYTHON SOURCE LINES 165-174 .. code-block:: Python entrance = planes[0].field[:, 0, centre] exit_amplitudes = planes[-1].field[:, 0, centre] turned = float( np.angle((exit_amplitudes[1] / exit_amplitudes[0]) / (entrance[1] / entrance[0])) ) print(f"residual relative phase {turned:.3e} rad") predicted = ((solved[0] - solved[1]) - (ideal[0] - ideal[1])) * length print(f"predicted from the spacing {predicted:.3e} rad") .. rst-class:: sphx-glr-script-out .. code-block:: none residual relative phase 5.577e-03 rad predicted from the spacing 5.577e-03 rad .. GENERATED FROM PYTHON SOURCE LINES 175-179 A launch declared as a **field** rather than as a channel. The route projects it onto the channels through the record's coordinate dual and reports what the bound channels did not capture, so a profile the guide cannot carry says so instead of being quietly rescaled. .. GENERATED FROM PYTHON SOURCE LINES 179-190 .. code-block:: Python route = general_modal_route( guide, grid, z_max=length, omega_reference=float(grid.omega0) ) weights = np.array([1.0, 0.5]) declared = np.einsum("qcmw,m->qcw", route.tables.electric[0], weights) projected = project_transverse_field(route.tables, declared) print(projected) ratio = projected.amplitudes[1, centre] / projected.amplitudes[0, centre] declared_ratio = weights[1] / weights[0] print(f"amplitude ratio {abs(ratio):.4f} against the declared {declared_ratio}") .. rst-class:: sphx-glr-script-out .. code-block:: none ProjectedLaunch: a declared field in a guide's channels channels: 2 frequencies: 65 peak captured: 1.25 W missed share: 1.818e-16 amplitude ratio 0.5000 against the declared 0.5 .. GENERATED FROM PYTHON SOURCE LINES 191-194 A field the channels cannot span leaves a remainder, and the projection names it rather than rescaling it away: here the same pattern with its sign flipped on every other node, which is no combination of the two channels at all. .. GENERATED FROM PYTHON SOURCE LINES 194-199 .. code-block:: Python spoiled = declared.copy() spoiled[::2] *= -1.0 missed = project_transverse_field(route.tables, spoiled).residual_share print(f"missed share {missed:.3f}") .. rst-class:: sphx-glr-script-out .. code-block:: none missed share 1.000 .. GENERATED FROM PYTHON SOURCE LINES 200-206 The same happens, less obviously, to a profile declared at **one** frequency and held across the band: the channels' patterns change with frequency, so away from where it was declared the field is no longer a guided superposition. The projection reports that too. The share is a reconstruction residual on the guide's nodes, not radiated power: the energy account of an incident beam is what ``incident=`` records (:doc:`/guide/waveguides/general-modal`). .. GENERATED FROM PYTHON SOURCE LINES 206-212 .. code-block:: Python held = np.zeros_like(declared) support = np.any(np.abs(route.tables.electric[0]) > 0.0, axis=(0, 1, 2)) held[:, :, support] = declared[:, :, centre][:, :, None] held_share = project_transverse_field(route.tables, held).residual_share print(f"held-profile missed share {held_share:.4f}") .. rst-class:: sphx-glr-script-out .. code-block:: none held-profile missed share 0.0286 .. GENERATED FROM PYTHON SOURCE LINES 213-219 Related reading --------------- Continue with :doc:`/guide/waveguides/graded-index` for the model, options and limits, and :doc:`/guide/numerics/accuracy` for convergence checks. For a paced propagation lesson, return to :doc:`/introduction/lessons/plot_02_soliton`. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 3.454 seconds) .. _sphx_glr_download_auto_tutorials_waveguides_plot_06_graded_index.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_06_graded_index.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_06_graded_index.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_06_graded_index.zip `