.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/waveguides/plot_05_moving_solid_guide.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_examples_waveguides_plot_05_moving_solid_guide.py: A tapered graded-index fibre: adiabatic and abrupt mode conversion ================================================================== A fibre taper changes the **shape** of the modes it guides. In a parabolic graded-index core the fundamental mode is, to a very good approximation, a Gaussian whose width squared is proportional to the core radius, :math:`w^2 = 2a/(k n_1 \sqrt{2\Delta})`; narrow the core from 25 um to 15 um and the mode narrows by :math:`\sqrt{0.6}`. If the core narrows slowly, the light follows the local mode and stays in it -- the taper is **adiabatic**. If it narrows quickly, the light keeps the shape it had, which is no longer a mode of the narrower core, and part of it is left in the next mode that shares its symmetry, ``LP_02``. The rule of thumb that separates the two is Love's: a taper is adiabatic where its local length scale :math:`z_t = a/|da/dz|` is longer than the beat length :math:`z_b = 2\pi/(\beta_1 - \beta_2)` between the two modes (J. D. Love *et al.*, IEE Proc. J **138**, 343 (1991); T. A. Birks and Y. W. Li, J. Lightwave Technol. **10**, 432 (1992)). This example propagates such tapers on the **moving solid guide** of the general modal route (:doc:`/guide/waveguides/moving-modal`). The guide is a :class:`~solprop.modes.graded_index.GradedIndexProvider` whose core radius follows a :class:`~solprop.profiles.Linear` or :class:`~solprop.profiles.PiecewiseLinear` path. The route re-solves the fibre's fields along that path, so that the synthesis and projection maps -- which put the Kerr response on the fields of the core where the pulse *is* -- move with the modes, and it carries the connection :math:`\langle\psi_m|\partial_z\psi_n \rangle` between the channels in its frozen-section flow. It is the connection that converts ``LP_01`` into ``LP_02``; an effective-area model, or a guide held at its entrance shape, has nothing that could. The fibre has a 25 um core radius and an index contrast :math:`\Delta = 0.0095` (a numerical aperture of 0.2), tapered linearly to 15 um. The pump is at 1030 nm. First the linear problem: eight taper lengths from 50 um to 10 mm, at a picojoule, with the conversion each leaves in ``LP_02`` compared with a first-order coupled-mode estimate and with Love's criterion; then the fields of the channels at the entrance and at the waist of an abrupt taper, rebuilt from the run. Last, a 400 kW, 200 fs pulse through a slow taper followed by a 10 mm waist, where self-phase modulation grows as the mode area shrinks, against the scalar :class:`~solprop.nonlinear.gnlse.SpecifiedMode` model of the same fibre, against a one-channel moving guide, and against the fibre held at its entrance radius. **What to look for.** The conversion falls from several per cent for the shortest tapers to 1e-5 for the longest, as the square of the taper slope, with the oscillation in length a two-ended conversion makes; the estimate from the connection and the beat length tracks it wherever it is small, and the shortest taper approaches the sudden limit, a splice between the two cores. Along the slow taper ``LP_02`` beats at the beat length about a level set by the taper's slope; the abrupt taper's waist field carries ``LP_02``'s ring. In the nonlinear run the spectrum broadens more than the entrance guide allows, by the mode's own shrinking area; the one-channel moving guide and the scalar area path agree, because this taper is adiabatic; and the two-channel guide broadens 5 % more than either, because at 9 % of the critical power the Kerr effect narrows the beam itself -- a self-focusing lens that, on the modes, is an ``LP_02`` admixture. **What is approximate.** The provider solves the **weak-guidance scalar** ``LP`` sector, exact to leading order in :math:`\Delta` (vector corrections omitted), and only its two lowest radial channels are carried: ``LP_01`` couples to ``LP_02`` alone in an infinite parabola (the derivative of a Gaussian's width is the next Laguerre-Gauss mode), and ``LP_02``'s own coupling to ``LP_03`` enters only at second order. The Kerr lens is carried only as far as two radial channels can describe a narrowing beam: the agreement shown at 9 % of the estimated critical power is with a variational Gaussian model, not a mode-count convergence study, and does not establish accuracy near collapse. The material is a constant index of 1.45: over these lengths a 200 fs pulse's dispersion length in silica is metres, and a Sellmeier core would cost the guide's solves a factor of three. The flow freezes the modes' departure at each section's midpoint and carries the rest in the explicit residual, so its step is limited by the spread of the channels' propagation constants across a section (the run's advisory step, which ``dz_max`` respects here). The collar absorber at the grid's edges is the default. No Raman response is declared. .. GENERATED FROM PYTHON SOURCE LINES 76-85 The fibre --------- The core law is the parabola :class:`~solprop.modes.graded_index.PowerLawPermittivity` on a constant index; the cladding is the same index scaled by ``1 - 2 Delta``. The Kerr response is declared on the core by its :math:`n_2`. ``taper_guide`` builds the guide for any radius path: the provider owns the path, and :meth:`~solprop.modes.guide.ModalGuide.from_provider` samples it along z, at the Chebyshev degree its own held-out check demands, as well as at the frequency knots. .. GENERATED FROM PYTHON SOURCE LINES 85-167 .. code-block:: Python import time import warnings import matplotlib.pyplot as plt import numpy as np from scipy.interpolate import CubicSpline from solprop.constants import C from solprop.driver import Simulation, prop_gnlse, prop_waveguide from solprop.exceptions import GeneralModalBasisWarning from solprop.grid import EnvelopeGrid, FieldGrid from solprop.modes import ( GradedIndexProvider, IndexMaterial, MaterialKerr, ModalGuide, PowerLawPermittivity, ) from solprop.modes.connection import modal_connection from solprop.nonlinear.gnlse import SpecifiedMode from solprop.processing import get_spectral_density from solprop.processing.modal import coordinate_intensity, reconstructed_flux from solprop.profiles import Linear, PiecewiseLinear from solprop.translate.coupled import advisory_step from solprop.translate.general import general_modal_route from solprop.translate.modal_path import path_rule ENTRANCE = 25e-6 """Core radius at the entrance, m.""" WAIST = 15e-6 """Core radius at the taper's waist, m.""" DELTA = 0.0095 """Index contrast: eps(r) = eps_axis (1 - 2 Delta (r/a)^2) in the core.""" WAVELENGTH = 1.03e-6 """Pump wavelength, m.""" N2 = 2.7e-20 """Nonlinear index of the core, m^2/W.""" DZ_MAX = 1.2e-4 """Longest step, m: below the flow's advisory step, which the run prints (see below).""" started = time.perf_counter() core = IndexMaterial(1.45) cladding = IndexMaterial(1.45 * np.sqrt(1.0 - 2.0 * DELTA)) def taper_guide(radius, grid, knots=17, count=2): """Return the graded-index guide's ``LP_0m`` channels on a radius path.""" provider = GradedIndexProvider( permittivity=PowerLawPermittivity(core, DELTA), exterior=cladding, radius=radius, family="LP", count=count, elements=48, extent=2.0, core_responses=(MaterialKerr(n2=N2, at=WAVELENGTH),), ) active = grid.omega[grid.active_mask] frequencies = np.geomspace(0.99 * float(active[0]), 1.01 * float(active[-1]), knots) return ModalGuide.from_provider( provider, frequencies, nodes=provider.node_rule(float(frequencies[0]), 0.0, n_r=2, n_theta=2), ) def linear_grid(length): """Return the narrow grid the picojoule runs propagate on.""" return FieldGrid.from_limits(length, WAVELENGTH, (1.0e-6, 1.065e-6), 0.4e-12) reference_length = 1e-3 reference = taper_guide(Linear(ENTRANCE, WAIST, reference_length), linear_grid(1e-3)) print(reference) print(reference.sampling.path) .. rst-class:: sphx-glr-script-out .. code-block:: none ModalGuide: a mode collection and its full-vector provider provider: GradedIndexProvider(LP0, 2 channels, 48 elements, extent=2) channels: LP0,1, LP0,2 regions: core (MaterialKerr(n2=2.7e-20 m^2/W, at 1.03 um)), exterior sampling: ModalSampling(17 knots, 885.1 Trad/s to 2.085 Prad/s, 384 nodes) ModalPath: how a moving guide was sampled along z sections: 1 (1 per kink interval) span: 0 m to 1 mm degree: 8 positions: 9 budget: 2.446e-06 held-out error: 3.143e-07 gram departure: 1.335e-06 .. GENERATED FROM PYTHON SOURCE LINES 168-183 How fast is too fast -------------------- Two numbers decide it at every position: the connection :math:`\Gamma_{21}`, how fast ``LP_01`` turns into ``LP_02`` as the core narrows, and the splitting :math:`\beta_1 - \beta_2`, how fast the two drift out of phase. Both are measured here from the provider directly -- the connection by :func:`~solprop.modes.connection.modal_connection`, a difference of fresh solves, and the propagation constants by fresh solves -- along the 1 mm reference taper, at the pump frequency. For an infinite parabola :math:`\Gamma_{21} = a'/(2a)` exactly (the width :math:`w \propto \sqrt a`) and :math:`\beta_1 - \beta_2 = 2\sqrt{2\Delta}/a`, so their ratio :math:`a'/(4\sqrt{2\Delta})` does not change along a linear taper, and Love's ratio :math:`z_t/z_b` is :math:`\sqrt{2\Delta}/(\pi |a'|)`, the same at every position. The finite fibre, with its cladding, is within a part in a thousand of both (the sign of :math:`\Gamma_{21}` is the provider's choice of the modes' signs). .. GENERATED FROM PYTHON SOURCE LINES 183-208 .. code-block:: Python provider = reference.field_provider omega0 = 2.0 * np.pi * C / WAVELENGTH positions = np.linspace(0.0, 1.0, 65) connection = np.empty(positions.size) splitting = np.empty(positions.size) for i, fraction in enumerate(positions): z = fraction * reference_length connection[i] = np.real(modal_connection(provider, omega0, z).matrix[1, 0]) rule = provider.node_rule(omega0, z, n_r=2, n_theta=2) beta = np.real(1j * np.diag(provider.solve(omega0, z, nodes=rule).generator)) splitting[i] = beta[0] - beta[1] slope = (WAIST - ENTRANCE) / reference_length radius_along = ENTRANCE + slope * positions * reference_length for i in (0, 32, 64): print( f"a = {radius_along[i] * 1e6:4.1f} um: Gamma_21 = {connection[i]:7.1f} 1/m " f"(ideal {abs(slope) / (2.0 * radius_along[i]):7.1f}), " f"beta_1 - beta_2 = {splitting[i]:7.1f} 1/m " f"(ideal {2.0 * np.sqrt(2.0 * DELTA) / radius_along[i]:7.1f}), " f"beat length {2.0 * np.pi / splitting[i] * 1e3:.3f} mm" ) love_length = abs(WAIST - ENTRANCE) * np.pi / np.sqrt(2.0 * DELTA) print(f"Love's criterion z_t = z_b at a taper length of {love_length * 1e3:.3f} mm") .. rst-class:: sphx-glr-script-out .. code-block:: none a = 25.0 um: Gamma_21 = 200.2 1/m (ideal 200.0), beta_1 - beta_2 = 11065.2 1/m (ideal 11027.2), beat length 0.568 mm a = 20.0 um: Gamma_21 = 250.2 1/m (ideal 250.0), beta_1 - beta_2 = 13829.8 1/m (ideal 13784.0), beat length 0.454 mm a = 15.0 um: Gamma_21 = 333.5 1/m (ideal 333.3), beta_1 - beta_2 = 18441.3 1/m (ideal 18378.7), beat length 0.341 mm Love's criterion z_t = z_b at a taper length of 0.228 mm .. GENERATED FROM PYTHON SOURCE LINES 209-228 The first-order estimate. Launched in ``LP_01``, the amplitude left in ``LP_02`` after a taper of length :math:`L` is, while it is small, .. math:: b_2(L) \approx -\int_0^L \Gamma_{21}(z)\, e^{i\int_0^z (\beta_1 - \beta_2)\,dz'} \,dz . On a linear taper of any length the connection is the reference's scaled by ``1 mm / L`` at the same fraction of the taper, and the splitting is the same, so one set of measurements gives the estimate for every length; both are smooth, and are interpolated by cubic splines onto the fine grid the phase needs at 10 mm. At the ends of the taper the slope jumps, and the integrand's two ends are where the conversion is made: the two contributions interfere, which is the oscillation in length. The **sudden limit** is the other end: a taper so short that the field does not change at all, a splice, leaves :math:`|c_1|^2 = (2 w_0 w_1/(w_0^2+w_1^2))^2 ((w_0^2 - w_1^2)/(w_0^2 + w_1^2))^2` in ``LP_02`` (the overlap of one Gaussian with the other's first radial Laguerre-Gauss mode). .. GENERATED FROM PYTHON SOURCE LINES 228-247 .. code-block:: Python fine = np.linspace(0.0, 1.0, 20001) fine_connection = CubicSpline(positions, connection)(fine) phase = CubicSpline(positions, splitting).antiderivative()(fine) def first_order(length): """Return the estimated ``LP_02`` amplitude along a taper of `length`.""" integrand = fine_connection * reference_length * np.exp(1j * length * phase) steps = 0.5 * (integrand[1:] + integrand[:-1]) * np.diff(fine) return np.concatenate(([0.0], np.cumsum(steps))) ratio = WAIST / ENTRANCE sudden = (2.0 * np.sqrt(ratio) / (1.0 + ratio)) ** 2 * ( (1.0 - ratio) / (1.0 + ratio) ) ** 2 print(f"sudden limit (a splice from 25 um to 15 um): {sudden:.4f} of the power") .. rst-class:: sphx-glr-script-out .. code-block:: none sudden limit (a splice from 25 um to 15 um): 0.0586 of the power .. GENERATED FROM PYTHON SOURCE LINES 248-262 The linear conversion --------------------- Eight tapers from 50 um to 10 mm long, each launched with a 100 fs, 1 pJ pulse in ``LP_01`` -- weak enough to isolate linear conversion in this comparison. Each channel's energy at a plane is the frequency sum of its own :math:`|b|^2`: the channels are orthonormal in true power at every position, so the two add to the run's physical energy. ``dz_max`` holds every step below the advisory step the translation computes, :math:`0.9/K`, with :math:`K` the spread of the channels' propagation constants about a section's midpoint over the bins the error norm scores (the only ones the explicit residual is applied on). It does not depend on the taper's length, because the radius changes by the same amount whatever the length; the run prints it for the longest taper below. The two channels together keep the launched energy to about 1e-6 in every run. .. GENERATED FROM PYTHON SOURCE LINES 262-313 .. code-block:: Python lengths = np.array([0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]) * 1e-3 def channel_fractions(run): """Return the positions and each channel's energy over the launched energy.""" field = np.asarray(run["field"])[:, :, 0, :] energy = np.sum(np.abs(field) ** 2, axis=-1) return np.asarray(run["z"]), energy / energy[0].sum() linear_runs = {} for length in lengths: grid = linear_grid(length) guide = taper_guide(Linear(ENTRANCE, WAIST, length), grid) run = prop_waveguide( (), length, provider=guide, grid=grid, lambda0=WAVELENGTH, fwhm=100e-15, energy=1e-12, dz_max=min(DZ_MAX, length / 10.0), n_save=41, progress="none", ) linear_runs[length] = (guide, grid, run) z, fractions = channel_fractions(run) print( f"L = {length * 1e3:5.2f} mm: LP_02 holds {fractions[-1, 1]:.3e} at the exit " f"(first order {abs(first_order(length)[-1]) ** 2:.3e}); " f"energy lost {1.0 - fractions[-1].sum():.1e}" ) grid = linear_grid(lengths[-1]) problem = Simulation.from_waveguide( (), lengths[-1], provider=linear_runs[lengths[-1]][0], grid=grid, lambda0=WAVELENGTH, fwhm=100e-15, energy=1e-12, dz_max=DZ_MAX, ).translate() flow = problem.linear.coupled_flow assert flow is not None advisory = float(np.min(advisory_step(flow, problem.grids.inband_mask))) print(f"advisory step {advisory * 1e3:.3f} mm against dz_max {DZ_MAX * 1e3:.3f} mm") .. rst-class:: sphx-glr-script-out .. code-block:: none L = 0.05 mm: LP_02 holds 6.131e-02 at the exit (first order 6.267e-02); energy lost 8.1e-07 L = 0.10 mm: LP_02 holds 5.394e-02 at the exit (first order 5.517e-02); energy lost 8.2e-07 L = 0.20 mm: LP_02 holds 3.112e-02 at the exit (first order 3.192e-02); energy lost 8.5e-07 L = 0.50 mm: LP_02 holds 7.900e-04 at the exit (first order 7.597e-04); energy lost 1.1e-06 L = 1.00 mm: LP_02 holds 6.548e-04 at the exit (first order 6.496e-04); energy lost 1.1e-06 L = 2.00 mm: LP_02 holds 3.271e-04 at the exit (first order 3.272e-04); energy lost 8.3e-07 L = 5.00 mm: LP_02 holds 2.525e-05 at the exit (first order 2.520e-05); energy lost 1.2e-06 L = 10.00 mm: LP_02 holds 1.307e-05 at the exit (first order 1.307e-05); energy lost 1.3e-06 advisory step 0.130 mm against dz_max 0.120 mm .. GENERATED FROM PYTHON SOURCE LINES 314-323 The conversion at the exit against the taper's length. The dots are the runs; the line is the first-order estimate, with its oscillation, and the dotted line the sudden limit; the shaded region is where Love's criterion calls the taper non-adiabatic. From 0.5 mm up the runs sit on the estimate to within 4 % of the conversion, down to 1.3e-5 at 10 mm, where the exit happens to land near a maximum of the oscillation. Below Love's length the estimate overshoots, as it does in this case: at zero length it gives :math:`(\int\Gamma_{21}dz)^2 = (\ln(w_1/w_0))^2` = 0.065 against the sudden limit's 0.059, and the runs lie between the two, approaching the splice. .. GENERATED FROM PYTHON SOURCE LINES 323-349 .. code-block:: Python dense = np.geomspace(0.03e-3, 12e-3, 3000) fig, ax = plt.subplots(figsize=(6.4, 4.0)) ax.loglog( dense * 1e3, [abs(first_order(length)[-1]) ** 2 for length in dense], color="0.4", linewidth=0.9, label="first-order estimate", ) ax.loglog( lengths * 1e3, [channel_fractions(linear_runs[length][2])[1][-1, 1] for length in lengths], "o", label="moving guide", ) ax.axhline(sudden, color="k", linestyle=":", linewidth=0.8, label="sudden limit") ax.axvspan(dense[0] * 1e3, love_length * 1e3, color="0.9", zorder=0) ax.text(0.035, 2e-6, "z_t < z_b\n(Love)", fontsize="small") ax.set_xlim(dense[0] * 1e3, dense[-1] * 1e3) ax.set_ylim(1e-6, 0.3) ax.set_xlabel("taper length (mm)") ax.set_ylabel("fraction of the energy in LP_02 at the exit") ax.legend(frameon=False, fontsize="small", loc="upper right") fig.tight_layout() .. image-sg:: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_001.png :alt: plot 05 moving solid guide :srcset: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 350-362 An abrupt and a slow taper -------------------------- Along the taper, against its fraction of the length: the 0.2 mm taper (Love's ratio 0.9) and the 5 mm one (22), with the first-order estimate beside each. The abrupt taper is shorter than one beat length (0.34 to 0.57 mm), so its ``LP_02`` grows steadily across it, a splice spread over 0.2 mm. On the slow taper the entrance, where the slope jumps from zero, leaves an amplitude :math:`\Gamma_{21}/(\beta_1 - \beta_2) = a'/(4\sqrt{2\Delta})`, 3.6e-3, which then beats against the same amplitude the local modes carry while the core narrows: the power swings between nothing and :math:`4(a'/(4\sqrt{2\Delta}))^2` = 5e-5 once per beat length, and the exit, where the slope returns to zero, keeps what it finds there. Every point of the run is on the estimate. .. GENERATED FROM PYTHON SOURCE LINES 362-386 .. code-block:: Python fig, ax = plt.subplots(figsize=(6.4, 3.8)) for length, colour in ((0.2e-3, "C0"), (5e-3, "C1")): z, fractions = channel_fractions(linear_runs[length][2]) ax.semilogy( z / length, np.maximum(fractions[:, 1], 1e-12), "o", markersize=3, color=colour, label=f"L = {length * 1e3:g} mm", ) ax.semilogy( fine, np.maximum(np.abs(first_order(length)) ** 2, 1e-12), color=colour, linewidth=0.8, ) ax.set_ylim(1e-7, 0.2) ax.set_xlabel("fraction of the taper length") ax.set_ylabel("fraction of the energy in LP_02") ax.legend(frameon=False, fontsize="small") fig.tight_layout() .. image-sg:: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_002.png :alt: plot 05 moving solid guide :srcset: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_002.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 387-402 The fields ---------- The abrupt taper's field at its entrance and at its waist, rebuilt from the run's saved channel amplitudes and the guide's own fields **at each plane's position**: the accessors of :mod:`solprop.processing.modal` solve the guide again there, on the node rule that has moved with the core, so the waist plane is reconstructed on the waist's modes and nodes. The flux density (:func:`~solprop.processing.modal.reconstructed_flux` with ``density=True``) and each channel's own share (:func:`~solprop.processing.modal.coordinate_intensity`) are peaked over time and plotted against the radius, relative to the entrance's peak. The waist's ``LP_01`` is narrower and 1.6 times as intense: the area ratio, 1.67, less the 3 % the taper moved out of it; ``LP_02``, with its node at 3.5 um and its ring outside it, is what the abrupt narrowing left behind (at the entrance it is empty and off the scale). The grey line is the core's edge; the fields have fallen by four decades well inside it. .. GENERATED FROM PYTHON SOURCE LINES 402-448 .. code-block:: Python guide, grid, run = linear_runs[0.2e-3] with warnings.catch_warnings(): warnings.simplefilter("ignore", GeneralModalBasisWarning) tables = general_modal_route( guide, grid, z_max=0.2e-3, omega_reference=float(grid.omega0) ).tables fig, axes = plt.subplots(1, 2, figsize=(9.0, 3.6), sharey=True) peak = None for ax, plane, title in ( (axes[0], 0, "entrance, a = 25 um"), (axes[1], -1, "waist, a = 15 um"), ): z = float(np.asarray(run["z"])[plane]) nodes = path_rule(guide.field_provider, guide.sampling.nodes, z) radius = np.hypot(nodes.xy[:, 0], nodes.xy[:, 1]) order = np.argsort(radius) total = np.max( reconstructed_flux(run, tables, plane=plane, density=True, guide=guide), axis=-1 ) channels = np.max( coordinate_intensity(run, tables, plane=plane, guide=guide), axis=-1 ) if peak is None: peak = float(np.max(total)) ax.semilogy(radius[order] * 1e6, total[order] / peak, color="k", label="total") for channel, name in enumerate(("LP_01", "LP_02")): ax.semilogy( radius[order] * 1e6, np.maximum(channels[channel][order] / peak, 1e-30), linestyle="--", label=name, ) ax.axvline( float(guide.field_provider.radius(np.array([z]))[0]) * 1e6, color="0.6", linewidth=0.8, ) ax.set_title(title, fontsize="medium") ax.set_xlabel("radius (um)") ax.set_xlim(0.0, 30.0) axes[0].set_ylabel("peak flux density / entrance peak") axes[0].set_ylim(1e-4, 3.0) axes[1].legend(frameon=False, fontsize="small") fig.tight_layout() .. image-sg:: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_003.png :alt: entrance, a = 25 um, waist, a = 15 um :srcset: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_003.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 449-473 Self-phase modulation in a taper's waist ---------------------------------------- A 200 fs, 400 kW pulse through a 5 mm taper, adiabatic by a factor of 22 on Love's criterion, and a 10 mm waist. The Kerr term is the analytic one, which carries the fundamental band only, so that the envelope model below is a like-for-like comparison. Four runs: * the **moving guide**, two channels, which puts the Kerr response on the fields of the core where the pulse is and projects it back onto both; * the same moving guide with **one channel**, ``LP_01`` alone; * the **scalar model** of the same fibre: ``prop_gnlse`` with a :class:`~solprop.nonlinear.gnlse.SpecifiedMode` carrying the guide's own ``LP_01`` dispersion along z (:meth:`~solprop.nonlinear.gnlse.SpecifiedMode.from_mode`) and the effective area along the taper, measured from the provider's fields at the pump wavelength, so that :math:`\gamma(z) = n_2\omega_0/(c A(z))`; it moves the area but not the modes, and could not convert; * the fibre held at its **entrance** radius, a guide that does not move at all. The effective area of the ideal parabola is proportional to the radius, so a path that is linear between the taper's corners is exact for it; the printed mid-taper value checks that on the real fibre. The peak power is 9 % of the critical power for self-focusing, :math:`P_{cr} = \lambda^2/(2\pi n_0 n_2)` in the Gaussian-beam estimate (printed), which matters below. .. GENERATED FROM PYTHON SOURCE LINES 473-564 .. code-block:: Python LENGTH = 15e-3 CORNER = 5e-3 WINDOW = (0.93e-6, 1.15e-6) PULSE = {"lambda0": WAVELENGTH, "fwhm": 200e-15, "peak_power": 4e5} path = PiecewiseLinear( np.array([0.0, CORNER, LENGTH]), np.array([ENTRANCE, WAIST, WAIST]) ) field_grid = FieldGrid.from_limits(LENGTH, WAVELENGTH, WINDOW, 2e-12) envelope_grid = EnvelopeGrid.from_limits(LENGTH, WAVELENGTH, WINDOW, 2e-12) moving_guide = taper_guide(path, field_grid, knots=25) single_guide = taper_guide(path, field_grid, knots=25, count=1) still_guide = taper_guide(ENTRANCE, field_grid, knots=25) def effective_area(guide, z): """Return ``LP_01``'s effective area at the pump, m^2, from a fresh solve.""" source = guide.field_provider rule = source.node_rule(omega0, z, n_r=2, n_theta=2) field = source.solve(omega0, z, nodes=rule).electric[:, :, 0] density = np.sum(np.abs(field) ** 2, axis=1) weights = rule.weights return float(np.sum(weights * density) ** 2 / np.sum(weights * density**2)) areas = [effective_area(moving_guide, z) for z in (0.0, 0.5 * CORNER, CORNER)] print( "effective area: " f"{areas[0] * 1e12:.1f} um^2 at the entrance, {areas[2] * 1e12:.1f} um^2 in the " f"waist; mid-taper {areas[1] * 1e12:.2f} um^2 against the linear path's " f"{0.5 * (areas[0] + areas[2]) * 1e12:.2f}" ) gamma = [N2 * omega0 / (C * area) for area in areas] phase_moving = PULSE["peak_power"] * ( 0.5 * (gamma[0] + gamma[2]) * CORNER + gamma[2] * (LENGTH - CORNER) ) phase_still = PULSE["peak_power"] * gamma[0] * LENGTH print( f"gamma {gamma[0] * 1e3:.3f} /(W km) at the entrance, " f"{gamma[2] * 1e3:.3f} in the waist" ) print( f"peak nonlinear phase: {phase_moving:.1f} rad through the taper, " f"{phase_still:.1f} rad at the entrance radius" ) critical = WAVELENGTH**2 / (2.0 * np.pi * 1.45 * N2) lens = 1.0 / np.sqrt(1.0 - PULSE["peak_power"] / critical) print( f"critical power {critical * 1e-6:.2f} MW; P/P_cr = " f"{PULSE['peak_power'] / critical:.3f}; the Kerr lens raises the peak's " f"coefficient by {lens:.4f}" ) common = {"n_save": 31, "progress": "none", **PULSE} moving = prop_waveguide( (), LENGTH, provider=moving_guide, grid=field_grid, kerr_form="analytic", dz_max=DZ_MAX, **common, ) single = prop_waveguide( (), LENGTH, provider=single_guide, grid=field_grid, kerr_form="analytic", dz_max=DZ_MAX, **common, ) area_path = PiecewiseLinear( np.array([0.0, CORNER, LENGTH]), np.array([areas[0], areas[2], areas[2]]) ) scalar = prop_gnlse( None, LENGTH, grid=envelope_grid, fibre=SpecifiedMode.from_mode( moving_guide.mode_collection.modes[0], aeff=area_path ), n2=N2, shock=True, backend="numba_cpu", **common, ) still = prop_waveguide( (), LENGTH, provider=still_guide, grid=field_grid, kerr_form="analytic", **common ) .. rst-class:: sphx-glr-script-out .. code-block:: none effective area: 129.0 um^2 at the entrance, 77.3 um^2 in the waist; mid-taper 103.14 um^2 against the linear path's 103.15 gamma 1.277 /(W km) at the entrance, 2.130 in the waist peak nonlinear phase: 11.9 rad through the taper, 7.7 rad at the entrance radius critical power 4.31 MW; P/P_cr = 0.093; the Kerr lens raises the peak's coefficient by 1.0499 .. GENERATED FROM PYTHON SOURCE LINES 565-585 The spectral width of ``LP_01`` along the fibre. The rms width of a Gaussian pulse's spectrum grows with the peak nonlinear phase :math:`\phi` as :math:`\sqrt{1 + 4\phi^2/(3\sqrt3)}` (G. P. Agrawal, *Nonlinear Fiber Optics*, section 4.1), dispersion aside; the dotted lines are that growth for the two phases printed above. The taper's runs grow faster from the start and end well above the fibre held at its entrance radius, by the area ratio. The one-channel moving guide and the scalar area path end on their dotted line, together (the first is under the second): on an adiabatic taper, moving the modes and moving the area are the same physics. The **two-channel** runs end 5 % above their lines, taper and entrance guide alike. That is the Kerr lens: at 9 % of the critical power the beam's own index change narrows it, and in a parabolic core a Gaussian beam's area at power :math:`P` is its linear area times :math:`\sqrt{1 - P/P_{cr}}`; the dashed lines are the Gaussian law with the peak's phase raised by that factor. A narrower ``LP_01``-like beam is, on the fibre's modes, ``LP_01`` with a little ``LP_02`` in phase with it -- which is how the two-channel route carries it, and why a model with one field shape, however it moves along z, cannot. The width is taken on ``LP_01`` alone, since an rms width weights far wings by the square of their distance. .. GENERATED FROM PYTHON SOURCE LINES 585-619 .. code-block:: Python def rms_width(run, plane): """Return the rms width of ``LP_01``'s spectrum at a plane, rad/s.""" omega, density = get_spectral_density(run, "omega", plane=plane, modes=0) omega, density = np.asarray(omega), np.ravel(density) mean = np.sum(omega * density) / np.sum(density) return float(np.sqrt(np.sum((omega - mean) ** 2 * density) / np.sum(density))) nonlinear_runs = ( (moving, "moving guide"), (single, "moving guide, LP_01 only"), (scalar, "scalar area path"), (still, "entrance radius"), ) COLOURS = ("C0", "C1", "C2", "C3") fig, ax = plt.subplots(figsize=(6.4, 3.8)) for (run, name), colour in zip(nonlinear_runs, COLOURS, strict=True): z = np.asarray(run["z"]) widths = np.array([rms_width(run, plane) for plane in range(z.size)]) ax.plot(z * 1e3, widths / widths[0], color=colour, label=name) print(f"{name:24s}: LP_01 rms spectral width x{widths[-1] / widths[0]:.3f}") for phase_end in (phase_moving, phase_still): for factor, style in ((1.0, ":"), (lens, "--")): expected = np.sqrt(1.0 + 4.0 * (factor * phase_end) ** 2 / (3.0 * np.sqrt(3.0))) print(f"Gaussian SPM at {factor * phase_end:.2f} rad: x{expected:.3f}") ax.axhline(expected, color="0.5", linestyle=style, linewidth=0.8) ax.axvline(CORNER * 1e3, color="0.6", linestyle="--", linewidth=0.8) ax.set_xlabel("z (mm)") ax.set_ylabel("rms spectral width / launched") ax.legend(frameon=False, fontsize="small") fig.tight_layout() .. image-sg:: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_004.png :alt: plot 05 moving solid guide :srcset: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_004.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none moving guide : LP_01 rms spectral width x10.988 moving guide, LP_01 only: LP_01 rms spectral width x10.487 scalar area path : LP_01 rms spectral width x10.433 entrance radius : LP_01 rms spectral width x7.144 Gaussian SPM at 11.92 rad: x10.510 Gaussian SPM at 12.52 rad: x11.030 Gaussian SPM at 7.66 rad: x6.798 Gaussian SPM at 8.05 rad: x7.130 .. GENERATED FROM PYTHON SOURCE LINES 620-629 The spectra at the exit, in decibels below the ``LP_01`` peak of each run, with the two-channel runs' ``LP_02`` dashed. The ``LP_01`` spectra of the taper's runs share their long-wavelength edge; the two-channel run's short-wavelength lobe reaches further, as its larger phase requires. Its ``LP_02`` is 30 dB down and has the pump's own self-phase shape, which is what a Kerr lens looks like on the modes: a cross term the pump writes wherever it is intense, not a band of its own. The taper by itself converts 2.5e-5 at this length (above); the printed ``LP_02`` share is fifty times that, and nearly the same in the fibre that does not taper at all. .. GENERATED FROM PYTHON SOURCE LINES 629-657 .. code-block:: Python fig, ax = plt.subplots(figsize=(6.4, 4.0)) for (run, name), colour in zip(nonlinear_runs, COLOURS, strict=True): wavelength, density = get_spectral_density(run, "wavelength", plane=-1) density = np.asarray(density).reshape(-1, np.size(wavelength)) order = np.argsort(wavelength) reference_peak = float(np.max(density[0])) for channel, style in enumerate(("-", "--")[: density.shape[0]]): ax.plot( np.asarray(wavelength)[order] * 1e9, 10.0 * np.log10(np.maximum(density[channel][order] / reference_peak, 1e-12)), style, color=colour, linewidth=0.9, label=name if channel == 0 else f"{name}, LP_02", ) ax.set_xlim(940.0, 1140.0) ax.set_ylim(-100.0, 3.0) ax.set_xlabel("wavelength (nm)") ax.set_ylabel("spectral energy density (dB)") ax.legend(frameon=False, fontsize="small", loc="lower center", ncol=2) fig.tight_layout() for run, name in ((moving, "moving guide"), (still, "entrance radius")): z, fractions = channel_fractions(run) print(f"{name}: LP_02 holds {fractions[-1, 1]:.2e} of the energy at the exit") .. image-sg:: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_005.png :alt: plot 05 moving solid guide :srcset: /auto_examples/waveguides/images/sphx_glr_plot_05_moving_solid_guide_005.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none moving guide: LP_02 holds 1.30e-03 of the energy at the exit entrance radius: LP_02 holds 1.26e-03 of the energy at the exit .. GENERATED FROM PYTHON SOURCE LINES 658-661 Runtime and reproducibility --------------------------- Twelve guides, eight linear runs on the moving guide and four nonlinear runs. .. GENERATED FROM PYTHON SOURCE LINES 661-668 .. code-block:: Python settings = moving.metadata.backend.settings print( f"{time.perf_counter() - started:.0f} s; backend {moving.metadata.backend.name}, " f"{settings.get('thread_count', 'default')} threads" ) .. rst-class:: sphx-glr-script-out .. code-block:: none 195 s; backend native, 18 threads .. GENERATED FROM PYTHON SOURCE LINES 669-675 Related reading --------------- Continue with :doc:`/guide/waveguides/moving-modal` for the model, options and limits, and :doc:`/guide/numerics/accuracy` for convergence checks. For a paced introduction to propagation, start with the :doc:`/introduction/index`. .. rst-class:: sphx-glr-timing **Total running time of the script:** (3 minutes 15.497 seconds) .. _sphx_glr_download_auto_examples_waveguides_plot_05_moving_solid_guide.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_05_moving_solid_guide.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_05_moving_solid_guide.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_05_moving_solid_guide.zip `