diff --git a/docs/source/dataanalysis/dataanalysis.rst b/docs/source/dataanalysis/dataanalysis.rst index 4371f317c..cc170b57a 100644 --- a/docs/source/dataanalysis/dataanalysis.rst +++ b/docs/source/dataanalysis/dataanalysis.rst @@ -84,7 +84,7 @@ The code writes out the values in an ASCII file prefixed ``reduced_beam_characte * ``sig_px``, ``sig_py``, ``sig_pt`` Standard deviation of the particle momentum deviations (energy difference for ``pt``) normalized by the magnitude of the reference particle momentum (unit: dimensionless) * ``emittance_x``, ``emittance_y``, ``emittance_t`` - Normalized rms beam emittance (unit: meter) + Unnormalized rms beam emittances (unit: meter) * ``alpha_x``, ``alpha_y``, ``alpha_t`` Courant-Snyder (Twiss) alpha (unit: dimensionless). Transverse Twiss functions are calculated after removing correlations with particle energy. * ``beta_x``, ``beta_y``, ``beta_t`` @@ -93,6 +93,11 @@ The code writes out the values in an ASCII file prefixed ``reduced_beam_characte Horizontal and vertical dispersion (unit: meter) * ``dispersion_px``, ``dispersion_py`` Derivative of horizontal and vertical dispersion (unit: dimensionless) +* ``emittance_xn``, ``emittance_yn``, ``emittance_tn`` + Normalized rms beam emittances (unit: meter) +* ``emittance_1``, ``emittance_2``, ``emittance_3`` + Normalized rms beam eigenemittances (aka mode emittances) (unit: meter) + These three diagnostics are written optionally if the flag eigenemittances = True. * ``charge`` Total beam charge (unit: Coulomb) diff --git a/docs/source/usage/examples.rst b/docs/source/usage/examples.rst index 426881b7f..3eed51f4d 100644 --- a/docs/source/usage/examples.rst +++ b/docs/source/usage/examples.rst @@ -33,6 +33,8 @@ Single Particle Dynamics examples/achromatic_spectrometer/README.rst examples/fodo_programmable/README.rst examples/dogleg/README.rst + examples/coupled_optics/README.rst + Collective Effects ------------------ diff --git a/docs/source/usage/parameters.rst b/docs/source/usage/parameters.rst index d21b1f2ab..9d3204b52 100644 --- a/docs/source/usage/parameters.rst +++ b/docs/source/usage/parameters.rst @@ -683,8 +683,8 @@ Diagnostics and output This option is ignored for the openPMD output elements (remove them from the lattice to disable). * ``diag.slice_step_diagnostics`` (``boolean``, optional, default: ``false``) - By default, diagnostics is performed at the beginning and end of the simulation. - Enabling this flag will write diagnostics every step and slice step + By default, diagnostics are computed and written at the beginning and end of the simulation. + Enabling this flag will write diagnostics at every step and slice step. * ``diag.file_min_digits`` (``integer``, optional, default: ``6``) The minimum number of digits used for the step number appended to the diagnostic file names. @@ -694,6 +694,10 @@ Diagnostics and output Diagnostics for particles lost in apertures, stored as ``diags/openPMD/particles_lost.*`` at the end of the simulation. See the ``beam_monitor`` element for backend values. +* ``diag.eigenemittances`` (``boolean``, optional, default: ``false``) + If this flag is enabled, the 3 eigenemittances of the 6D beam distribution are computed and written as diagnostics. + This flag is disabled by default to reduce computational cost. + .. _running-cpp-parameters-diagnostics-insitu: diff --git a/docs/source/usage/python.rst b/docs/source/usage/python.rst index 52acfe210..2a27cb5b9 100644 --- a/docs/source/usage/python.rst +++ b/docs/source/usage/python.rst @@ -156,6 +156,13 @@ Collective Effects & Overall Simulation Parameters Diagnostics for particles lost in apertures. See the ``BeamMonitor`` element for backend values. + .. py:property:: eigenemittances + + Enable (``True``) or disable (``False``) output of eigenemittances at every slice step in elements (default: ``False``). + + If this flag is enabled, the 3 eigenemittances of the 6D beam distribution are computed and written as diagnostics. + This flag is disabled by default to reduce computational cost. + .. py:method:: init_grids() Initialize AMReX blocks/grids for domain decomposition & space charge mesh. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d33221240..353743bff 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -959,3 +959,19 @@ add_impactx_test(dogleg.py examples/dogleg/analysis_dogleg.py OFF # no plot script yet ) + +# Coupled Optics ############################################################# +# +# w/o space charge +add_impactx_test(coupled-optics + examples/coupled_optics/input_coupled_optics.in + ON # ImpactX MPI-parallel + examples/coupled_optics/analysis_coupled_optics.py + OFF # no plot script yet +) +add_impactx_test(coupled-optics.py + examples/coupled_optics/run_coupled_optics.py + OFF # ImpactX MPI-parallel + examples/coupled_optics/analysis_coupled_optics.py + OFF # no plot script yet +) diff --git a/examples/coupled_optics/README.rst b/examples/coupled_optics/README.rst new file mode 100644 index 000000000..ff17b9e20 --- /dev/null +++ b/examples/coupled_optics/README.rst @@ -0,0 +1,51 @@ +.. _examples-coupled-optics: + +Coupled Optics +============== + +This is a lattice illustrating fully coupled 6D transport. It is obtained from the example "dogleg" by adding a solenoid after the first bending dipole. +The solenoid is identical to that found in the example "solenoid". + +Its primary purpose is to benchmark the calculation of the three beam eigenemittances (mode emittances). + +In this test, the initial and final values of :math:`\lambda_x`, :math:`\lambda_y`, :math:`\lambda_t`, :math:`\epsilon_x`, :math:`\epsilon_y`, and :math:`\epsilon_t` must +agree with nominal values. + +In addition, the initial and final values of :math:`emittance_1`, :math:`emittance_2`, :math:`emittance_3` must coincide. + + +Run +--- + +This example can be run **either** as: + +* **Python** script: ``python3 run_coupled_optics.py`` or +* ImpactX **executable** using an input file: ``impactx input_coupled_optics.in`` + +For `MPI-parallel `__ runs, prefix these lines with ``mpiexec -n 4 ...`` or ``srun -n 4 ...``, depending on the system. + +.. tab-set:: + + .. tab-item:: Python: Script + + .. literalinclude:: run_coupled_optics.py + :language: python3 + :caption: You can copy this file from ``examples/coupled_optics/run_coupled_optics.py``. + + .. tab-item:: Executable: Input File + + .. literalinclude:: input_coupled_optics.in + :language: ini + :caption: You can copy this file from ``examples/coupled_optics/input_coupled_optics.in``. + + +Analyze +------- + +We run the following script to analyze correctness: + +.. dropdown:: Script ``analysis_coupled_optics.py`` + + .. literalinclude:: analysis_coupled_optics.py + :language: python3 + :caption: You can copy this file from ``examples/coupled_optics/analysis_coupled_optics.py``. diff --git a/examples/coupled_optics/analysis_coupled_optics.py b/examples/coupled_optics/analysis_coupled_optics.py new file mode 100755 index 000000000..04b3eb998 --- /dev/null +++ b/examples/coupled_optics/analysis_coupled_optics.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright 2022-2023 ImpactX contributors +# Authors: Axel Huebl, Chad Mitchell +# License: BSD-3-Clause-LBNL +# + +import numpy as np +import openpmd_api as io +from scipy.stats import moment + + +def get_moments(beam): + """Calculate standard deviations of beam position & momenta + and emittance values + + Returns + ------- + sigx, sigy, sigt, emittance_x, emittance_y, emittance_t + """ + sigx = moment(beam["position_x"], moment=2) ** 0.5 # variance -> std dev. + sigpx = moment(beam["momentum_x"], moment=2) ** 0.5 + sigy = moment(beam["position_y"], moment=2) ** 0.5 + sigpy = moment(beam["momentum_y"], moment=2) ** 0.5 + sigt = moment(beam["position_t"], moment=2) ** 0.5 + sigpt = moment(beam["momentum_t"], moment=2) ** 0.5 + + epstrms = beam.cov(ddof=0) + emittance_x = (sigx**2 * sigpx**2 - epstrms["position_x"]["momentum_x"] ** 2) ** 0.5 + emittance_y = (sigy**2 * sigpy**2 - epstrms["position_y"]["momentum_y"] ** 2) ** 0.5 + emittance_t = (sigt**2 * sigpt**2 - epstrms["position_t"]["momentum_t"] ** 2) ** 0.5 + + return (sigx, sigy, sigt, emittance_x, emittance_y, emittance_t) + + +def get_eigenemittances(openpmd_beam): + """Return eigenemittances from an openPMD particle species + + Returns + ------- + emittance_1, emittance_2, emittance_3 + """ + emittance_1 = openpmd_beam.get_attribute("emittance_1") + emittance_2 = openpmd_beam.get_attribute("emittance_2") + emittance_3 = openpmd_beam.get_attribute("emittance_3") + + return (emittance_1, emittance_2, emittance_3) + + +# initial/final beam +series = io.Series("diags/openPMD/monitor.h5", io.Access.read_only) +last_step = list(series.iterations)[-1] +initial_beam = series.iterations[1].particles["beam"] +initial = initial_beam.to_df() +final_beam = series.iterations[last_step].particles["beam"] +final = final_beam.to_df() + +# compare number of particles +num_particles = 10000 +assert num_particles == len(initial) +assert num_particles == len(final) + +print("Initial Beam:") +sigx, sigy, sigt, emittance_x, emittance_y, emittance_t = get_moments(initial) +print(f" sigx={sigx:e} sigy={sigy:e} sigt={sigt:e}") +print( + f" emittance_x={emittance_x:e} emittance_y={emittance_y:e} emittance_t={emittance_t:e}" +) + +atol = 0.0 # ignored +rtol = 2.2 * num_particles**-0.5 # from random sampling of a smooth distribution +print(f" rtol={rtol} (ignored: atol~={atol})") + +assert np.allclose( + [sigx, sigy, sigt, emittance_x, emittance_y, emittance_t], + [ + 6.4214719960819659e-005, + 3.6603372435649773e-005, + 1.9955175623579313e-004, + 1.0198263116327677e-010, + 1.0308359092878036e-010, + 4.0035161705244885e-010, + ], + rtol=rtol, + atol=atol, +) + + +print("") +print("Final Beam:") +sigx, sigy, sigt, emittance_x, emittance_y, emittance_t = get_moments(final) +print(f" sigx={sigx:e} sigy={sigy:e} sigt={sigt:e}") +print( + f" emittance_x={emittance_x:e} emittance_y={emittance_y:e} emittance_t={emittance_t:e}" +) + +atol = 0.0 # ignored +rtol = 2.2e12 * num_particles**-0.5 # from random sampling of a smooth distribution +print(f" rtol={rtol} (ignored: atol~={atol})") + +assert np.allclose( + [sigx, sigy, sigt, emittance_x, emittance_y, emittance_t], + [ + 1.922660e-03, + 2.166654e-05, + 1.101353e-04, + 8.561046e-09, + 1.020439e-10, + 8.569865e-09, + ], + rtol=rtol, + atol=atol, +) + +print("") +print("Initial eigenemittances:") +emittance_1i, emittance_2i, emittance_3i = get_eigenemittances(initial_beam) +print( + f" emittance_1={emittance_1i:e} emittance_2={emittance_2i:e} emittance_3={emittance_3i:e}" +) + +print("") +print("Final eigenemittances:") +emittance_1f, emittance_2f, emittance_3f = get_eigenemittances(final_beam) +print( + f" emittance_1={emittance_1f:e} emittance_2={emittance_2f:e} emittance_3={emittance_3f:e}" +) + +atol = 0.0 # ignored +rtol = 3.5 * num_particles**-0.5 # from random sampling of a smooth distribution +print(f" rtol={rtol} (ignored: atol~={atol})") + +assert np.allclose( + [emittance_1f, emittance_2f, emittance_3f], + [ + emittance_1i, + emittance_2i, + emittance_3i, + ], + rtol=rtol, + atol=atol, +) diff --git a/examples/coupled_optics/input_coupled_optics.in b/examples/coupled_optics/input_coupled_optics.in new file mode 100644 index 000000000..f0ee840c4 --- /dev/null +++ b/examples/coupled_optics/input_coupled_optics.in @@ -0,0 +1,72 @@ +############################################################################### +# Particle Beam(s) +############################################################################### +beam.npart = 10000 +beam.units = static +beam.kin_energy = 5.0e3 +beam.charge = 1.0e-9 +beam.particle = electron +beam.distribution = waterbag +beam.lambdaX = 2.2951017632e-5 +beam.lambdaY = 1.3084093142e-5 +beam.lambdaT = 5.5555553e-8 +beam.lambdaPx = 1.598353425e-6 +beam.lambdaPy = 2.803697378e-6 +beam.lambdaPt = 2.000000000e-6 +beam.muxpx = 0.933345606203060 +beam.muypy = 0.933345606203060 +beam.mutpt = 0.999999961419755 + + +############################################################################### +# Beamline: lattice elements and segments +############################################################################### +lattice.elements = monitor sbend1 dipedge1 sol drift1 dipedge2 sbend2 drift2 monitor +lattice.nslice = 25 + +sbend1.type = sbend +sbend1.ds = 0.500194828041958 # projected length 0.5 m, angle 2.77 deg +sbend1.rc = -10.3462283686195526 + +drift1.type = drift +drift1.ds = 5.0058489435 # projected length 5 m + +sbend2.type = sbend +sbend2.ds = 0.500194828041958 # projected length 0.5 m, angle 2.77 deg +sbend2.rc = 10.3462283686195526 + +drift2.type = drift +drift2.ds = 0.5 + +dipedge1.type = dipedge # dipole edge focusing +dipedge1.psi = -0.048345620280243 +dipedge1.rc = -10.3462283686195526 +dipedge1.g = 0.0 +dipedge1.K2 = 0.0 + +dipedge2.type = dipedge +dipedge2.psi = 0.048345620280243 +dipedge2.rc = 10.3462283686195526 +dipedge2.g = 0.0 +dipedge2.K2 = 0.0 + +sol.type = solenoid +sol.ds = 3.820395 +sol.ks = 0.8223219329893234 + +monitor.type = beam_monitor +monitor.backend = h5 + + +############################################################################### +# Algorithms +############################################################################### +algo.particle_shape = 2 +algo.space_charge = false + + +############################################################################### +# Diagnostics +############################################################################### +diag.slice_step_diagnostics = true +diag.eigenemittances = true diff --git a/examples/coupled_optics/run_coupled_optics.py b/examples/coupled_optics/run_coupled_optics.py new file mode 100644 index 000000000..c6e6632d1 --- /dev/null +++ b/examples/coupled_optics/run_coupled_optics.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# +# Copyright 2022-2023 ImpactX contributors +# Authors: Marco Garten, Axel Huebl, Chad Mitchell +# License: BSD-3-Clause-LBNL +# +# -*- coding: utf-8 -*- + +from impactx import ImpactX, distribution, elements + +sim = ImpactX() + +# set numerical parameters and IO control +sim.particle_shape = 2 # B-spline order +sim.space_charge = False +# sim.diagnostics = False # benchmarking +sim.slice_step_diagnostics = True +sim.eigenemittances = True + +# domain decomposition & space charge mesh +sim.init_grids() + +# load a 5 GeV electron beam with an initial +# normalized transverse rms emittance of 1 um +kin_energy_MeV = 5.0e3 # reference energy +bunch_charge_C = 1.0e-9 # used with space charge +npart = 10000 # number of macro particles + +# reference particle +ref = sim.particle_container().ref_particle() +ref.set_charge_qe(-1.0).set_mass_MeV(0.510998950).set_kin_energy_MeV(kin_energy_MeV) + +# particle bunch +distr = distribution.Waterbag( + lambdaX=2.2951017632e-5, + lambdaY=1.3084093142e-5, + lambdaT=5.5555553e-8, + lambdaPx=1.598353425e-6, + lambdaPy=2.803697378e-6, + lambdaPt=2.000000000e-6, + muxpx=0.933345606203060, + muypy=0.933345606203060, + mutpt=0.999999961419755, +) +sim.add_particles(bunch_charge_C, distr, npart) + +# add beam diagnostics +monitor = elements.BeamMonitor("monitor", backend="h5") + +# design the accelerator lattice +ns = 25 # number of slices per ds in the element +rc = 10.3462283686195526 # bend radius (meters) +psi = 0.048345620280243 # pole face rotation angle (radians) +lb = 0.500194828041958 # bend arc length (meters) + +# Drift elements +dr1 = elements.Drift(ds=5.0058489435, nslice=ns) +dr2 = elements.Drift(ds=0.5, nslice=ns) + +# Bend elements +sbend1 = elements.Sbend(ds=lb, rc=-rc, nslice=ns) +sbend2 = elements.Sbend(ds=lb, rc=rc, nslice=ns) + +# Dipole Edge Focusing elements +dipedge1 = elements.DipEdge(psi=-psi, rc=-rc, g=0.0, K2=0.0) +dipedge2 = elements.DipEdge(psi=psi, rc=rc, g=0.0, K2=0.0) + +# Solenoid element +sol = elements.Sol(ds=3.820395, ks=0.8223219329893234) + +lattice_coupled = [sbend1, dipedge1, sol, dr1, dipedge2, sbend2, dr2] + +sim.lattice.append(monitor) +sim.lattice.extend(lattice_coupled) +sim.lattice.append(monitor) + +# run simulation +sim.evolve() + +# clean shutdown +sim.finalize() diff --git a/src/particles/diagnostics/CMakeLists.txt b/src/particles/diagnostics/CMakeLists.txt index 1ff032aad..50ad97560 100644 --- a/src/particles/diagnostics/CMakeLists.txt +++ b/src/particles/diagnostics/CMakeLists.txt @@ -2,4 +2,5 @@ target_sources(lib PRIVATE ReducedBeamCharacteristics.cpp DiagnosticOutput.cpp + EmittanceInvariants.cpp ) diff --git a/src/particles/diagnostics/CovarianceMatrixMath.H b/src/particles/diagnostics/CovarianceMatrixMath.H new file mode 100644 index 000000000..a3b0099cf --- /dev/null +++ b/src/particles/diagnostics/CovarianceMatrixMath.H @@ -0,0 +1,219 @@ +/* Copyright 2022-2023 The Regents of the University of California, through Lawrence + * Berkeley National Laboratory (subject to receipt of any required + * approvals from the U.S. Dept. of Energy). All rights reserved. + * + * This file is part of ImpactX. + * + * Authors: Chad Mitchell, Axel Huebl + * License: BSD-3-Clause-LBNL + */ +#ifndef COVARIANCE_MATRIX_MATH_H +#define COVARIANCE_MATRIX_MATH_H + +#include +#include + +#include +#include +#include + +#include +#include + + +namespace impactx::diagnostics +{ + + /** Function to return the roots of a cubic polynomial ax^3 + bx^2 + cx + d. + * The trigonometric form of Cardano's formula is used. + * This implementation expects three real roots, which is verified + * by checking the sign of the discriminant. + * + * @param[in] a coefficient of cubic term + * @param[in] b coefficient of quadratic term + * @param[in] c coefficient of linear term + * @param[in] d coefficient of constant term + * @returns tuple of three real roots + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal + > + CubicRootsTrig ( + amrex::ParticleReal a, + amrex::ParticleReal b, + amrex::ParticleReal c, + amrex::ParticleReal d + ) + { + using namespace amrex::literals; + using ablastr::constant::math::pi; + + std::tuple roots; + amrex::ParticleReal x1 = 0.0_prt; + amrex::ParticleReal x2 = 0.0_prt; + amrex::ParticleReal x3 = 0.0_prt; + + amrex::ParticleReal Q = (3.0_prt*a*c - std::pow(b,2))/(9.0_prt * std::pow(a,2)); + amrex::ParticleReal R = (9.0_prt*a*b*c - 27_prt*std::pow(a,2)*d - 2.0_prt*std::pow(b,3))/(54.0_prt*std::pow(a,3)); + amrex::ParticleReal discriminant = std::pow(Q,3) + std::pow(R,2); + + // Discriminant should be < 0. Otherwise, keep theta at default and throw an error. + amrex::ParticleReal tol = 1.0e-12; //allow for roundoff error + if (discriminant > tol) { + + ablastr::warn_manager::WMRecordWarning( + "Impactx::diagnostics::CubicRootsTrig", + "Polynomial appearing in CubicRootsTrig has one or more complex " + "(non-real) roots. Only the real part is returned. This " + "suggests a loss of numerical precision in computation of the " + "eigenemittances. Treat eigenemittance values with caution.", + ablastr::warn_manager::WarnPriority::medium + ); + + std::cout << "Polynomial in CubicRoots has one or more complex roots." << "\n"; + + } else if (Q == 0.0_prt) { // Special case of a triple root + + x1 = - b/(3.0_prt*a); + x2 = - b/(3.0_prt*a); + x3 = - b/(3.0_prt*a); + + } else { + + //Three real roots in trigonometric form. + amrex::ParticleReal theta = std::acos(R/std::sqrt(-std::pow(Q,3))); + x1 = 2.0_prt*std::sqrt(-Q)*std::cos(theta/3.0_prt) - b/(3.0_prt*a); + x2 = 2.0_prt*std::sqrt(-Q)*std::cos(theta/3.0_prt + 2.0_prt*pi/3.0_prt) - b/(3.0_prt*a); + x3 = 2.0_prt*std::sqrt(-Q)*std::cos(theta/3.0_prt + 4.0_prt*pi/3.0_prt) - b/(3.0_prt*a); + + } + + roots = std::make_tuple(x1,x2,x3); + return roots; + } + + + /** Function to return the roots of a cubic polynomial ax^3 + bx^2 + cx + d. + * The algebraic form of Cardano's formula is used. + * This implementation expects three real roots, which is verified + * by checking the sign of the discriminant. + * + * @param[in] a coefficient of cubic term + * @param[in] b coefficient of quadratic term + * @param[in] c coefficient of linear term + * @param[in] d coefficient of constant term + * @returns tuple of three real roots + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal + > + CubicRootsAlg ( + amrex::ParticleReal a, + amrex::ParticleReal b, + amrex::ParticleReal c, + amrex::ParticleReal d + ) + { + using namespace amrex::literals; + using Complex = amrex::GpuComplex; + + std::tuple roots; + amrex::ParticleReal x1 = 0.0_prt; + amrex::ParticleReal x2 = 0.0_prt; + amrex::ParticleReal x3 = 0.0_prt; + + amrex::ParticleReal Q = (3.0_prt*a*c - std::pow(b,2))/(9.0_prt * std::pow(a,2)); + amrex::ParticleReal R = (9.0_prt*a*b*c - 27_prt*std::pow(a,2)*d - 2.0_prt*std::pow(b,3))/(54.0_prt*std::pow(a,3)); + amrex::ParticleReal discriminant = std::pow(Q,3) + std::pow(R,2); + + // Define complex variable C + Complex Qc(Q,0.0_prt); + Complex Rc(R,0.0_prt); + Complex Dc(discriminant,0.0_prt); + Complex C = amrex::pow(-Rc + amrex::sqrt(Dc),1.0/3.0); + + // Define a cubic root of unity xi + amrex::ParticleReal xire = -1.0/2.0; + amrex::ParticleReal xiim = std::sqrt(3.0)/2.0; + Complex xi(xire,xiim); + + //Three roots in algebraic form. + + if (C.m_real == 0.0_prt && C.m_imag == 0.0_prt) { // Special case of a triple root + + x1 = - b/(3.0_prt*a); + x2 = - b/(3.0_prt*a); + x3 = - b/(3.0_prt*a); + + } else { + + Complex z1 = Qc/C - C; + Complex z2 = Qc/(xi*C) - xi*C; + Complex z3 = Qc/(amrex::pow(xi,2)*C) - amrex::pow(xi,2)*C; + x1 = z2.m_real - b/(3.0*a); + x2 = z1.m_real - b/(3.0*a); + x3 = z3.m_real - b/(3.0*a); + } + + roots = std::make_tuple(x1,x2,x3); + return roots; + } + + + /** Function to take the trace of a square 6x6 matrix. + * + * @param[in] A a square matrix + * @returns the trace of A + */ + amrex::ParticleReal + TraceMat ( + amrex::Array2D const & A + ) + { + int const dim = 6; + amrex::ParticleReal trA = 0.0; + + for (int i = 1; i < dim+1; i++) { + trA += A(i,i); + } + return trA; + } + + + /** Function to multiply two square matrices of dimension 6. + * + * @param[in] A a square matrix + * @param[in] B square matrix + * @returns the matrix C = AB + */ + amrex::Array2D + MultiplyMat ( + amrex::Array2D const & A, + amrex::Array2D const & B + ) + { + amrex::Array2D C; + int const dim = 6; + + for (int i = 1; i < dim+1; i++) { + for (int j = 1; j < dim+1; j++) { + C(i,j) = 0; + + for (int k = 1; k < dim+1; k++) { + C(i,j) += A(i,k) * B(k,j); + } + + } + + } + return C; + } + + +} // namespace impactx::diagnostics + +#endif // COVARIANCE_MATRIX_MATH_H diff --git a/src/particles/diagnostics/DiagnosticOutput.cpp b/src/particles/diagnostics/DiagnosticOutput.cpp index fee8af6eb..1ec35610f 100644 --- a/src/particles/diagnostics/DiagnosticOutput.cpp +++ b/src/particles/diagnostics/DiagnosticOutput.cpp @@ -43,7 +43,33 @@ namespace impactx::diagnostics if (otype == OutputType::PrintRefParticle) { file_handler << "step s beta gamma beta_gamma x y z t px py pz pt\n"; } else if (otype == OutputType::PrintReducedBeamCharacteristics) { - file_handler << "step" << " " << "s" << " " + + // determine whether to output eigenemittances + amrex::ParmParse pp_diag("diag"); + bool compute_eigenemittances = false; + pp_diag.queryAdd("eigenemittances", compute_eigenemittances); + + if (compute_eigenemittances) { + file_handler << "step" << " " << "s" << " " + << "x_mean" << " " << "x_min" << " " << "x_max" << " " + << "y_mean" << " " << "y_min" << " " << "y_max" << " " + << "t_mean" << " " << "t_min" << " " << "t_max" << " " + << "sig_x" << " " << "sig_y" << " " << "sig_t" << " " + << "px_mean" << " " << "px_min" << " " << "px_max" << " " + << "py_mean" << " " << "py_min" << " " << "py_max" << " " + << "pt_mean" << " " << "pt_min" << " " << "pt_max" << " " + << "sig_px" << " " << "sig_py" << " " << "sig_pt" << " " + << "emittance_x" << " " << "emittance_y" << " " << "emittance_t" << " " + << "alpha_x" << " " << "alpha_y" << " " << "alpha_t" << " " + << "beta_x" << " " << "beta_y" << " " << "beta_t" << " " + << "dispersion_x" << " " << "dispersion_px" << " " + << "dispersion_y" << " " << "dispersion_py" << " " + << "emittance_xn" << " " << "emittance_yn" << " " << "emittance_tn" << " " + << "emittance_1" << " " << "emittance_2" << " " << "emittance_3" << " " + << "charge_C" << " " + << "\n"; + } else { + file_handler << "step" << " " << "s" << " " << "x_mean" << " " << "x_min" << " " << "x_max" << " " << "y_mean" << " " << "y_min" << " " << "y_max" << " " << "t_mean" << " " << "t_min" << " " << "t_max" << " " @@ -57,8 +83,10 @@ namespace impactx::diagnostics << "beta_x" << " " << "beta_y" << " " << "beta_t" << " " << "dispersion_x" << " " << "dispersion_px" << " " << "dispersion_y" << " " << "dispersion_py" << " " + << "emittance_xn" << " " << "emittance_yn" << " " << "emittance_tn" << " " << "charge_C" << " " << "\n"; + } } } @@ -92,7 +120,31 @@ namespace impactx::diagnostics amrex::ParticleReal const s = pc.GetRefParticle().s; - file_handler << step << " " << s << " " + // determine whether to output eigenemittances + amrex::ParmParse pp_diag("diag"); + bool compute_eigenemittances = false; + pp_diag.queryAdd("eigenemittances", compute_eigenemittances); + + if (compute_eigenemittances) { + file_handler << step << " " << s << " " + << rbc.at("x_mean") << " " << rbc.at("x_min") << " " << rbc.at("x_max") << " " + << rbc.at("y_mean") << " " << rbc.at("y_min") << " " << rbc.at("y_max") << " " + << rbc.at("t_mean") << " " << rbc.at("t_min") << " " << rbc.at("t_max") << " " + << rbc.at("sig_x") << " " << rbc.at("sig_y") << " " << rbc.at("sig_t") << " " + << rbc.at("px_mean") << " " << rbc.at("px_min") << " " << rbc.at("px_max") << " " + << rbc.at("py_mean") << " " << rbc.at("py_min") << " " << rbc.at("py_max") << " " + << rbc.at("pt_mean") << " " << rbc.at("pt_min") << " " << rbc.at("pt_max") << " " + << rbc.at("sig_px") << " " << rbc.at("sig_py") << " " << rbc.at("sig_pt") << " " + << rbc.at("emittance_x") << " " << rbc.at("emittance_y") << " " << rbc.at("emittance_t") << " " + << rbc.at("alpha_x") << " " << rbc.at("alpha_y") << " " << rbc.at("alpha_t") << " " + << rbc.at("beta_x") << " " << rbc.at("beta_y") << " " << rbc.at("beta_t") << " " + << rbc.at("dispersion_x") << " " << rbc.at("dispersion_px") << " " + << rbc.at("dispersion_y") << " " << rbc.at("dispersion_py") << " " + << rbc.at("emittance_xn") << " " << rbc.at("emittance_yn") << " " << rbc.at("emittance_tn") << " " + << rbc.at("emittance_1") << " " << rbc.at("emittance_2") << " " << rbc.at("emittance_3") << " " + << rbc.at("charge_C") << "\n"; + } else { + file_handler << step << " " << s << " " << rbc.at("x_mean") << " " << rbc.at("x_min") << " " << rbc.at("x_max") << " " << rbc.at("y_mean") << " " << rbc.at("y_min") << " " << rbc.at("y_max") << " " << rbc.at("t_mean") << " " << rbc.at("t_min") << " " << rbc.at("t_max") << " " @@ -106,7 +158,9 @@ namespace impactx::diagnostics << rbc.at("beta_x") << " " << rbc.at("beta_y") << " " << rbc.at("beta_t") << " " << rbc.at("dispersion_x") << " " << rbc.at("dispersion_px") << " " << rbc.at("dispersion_y") << " " << rbc.at("dispersion_py") << " " + << rbc.at("emittance_xn") << " " << rbc.at("emittance_yn") << " " << rbc.at("emittance_tn") << " " << rbc.at("charge_C") << "\n"; + } } // if( otype == OutputType::PrintReducedBeamCharacteristics) // TODO: add as an option to the monitor element diff --git a/src/particles/diagnostics/EmittanceInvariants.H b/src/particles/diagnostics/EmittanceInvariants.H new file mode 100644 index 000000000..177371953 --- /dev/null +++ b/src/particles/diagnostics/EmittanceInvariants.H @@ -0,0 +1,77 @@ +/* Copyright 2022-2023 The Regents of the University of California, through Lawrence + * Berkeley National Laboratory (subject to receipt of any required + * approvals from the U.S. Dept. of Energy). All rights reserved. + * + * This file is part of ImpactX. + * + * Authors: Marco Garten, Chad Mitchell, Axel Huebl + * License: BSD-3-Clause-LBNL + */ +#ifndef IMPACTX_EMITTANCE_INVARIANTS +#define IMPACTX_EMITTANCE_INVARIANTS + +#include "particles/ImpactXParticleContainer.H" + +#include +#include + +#include + + + +namespace impactx::diagnostics +{ + + /** Returns the three independent kinematic moment invariants + * denoted I2, I4, and I6 as constructed from the 6x6 + * beam covariance matrix. These three quantities are invariant + * under any linear symplectic transport map, and are used in the + * calculation of the three eigenemittances, as described in: + * + * G. Rangarajan, F. Neri, and A. Dragt, "Generalized Emittance + * Invariants," in Proc. 1989 IEEE Part. Accel. Conf., Chicago, IL, + * 1989, doi:10.1109/PAC.1989.73422. + * A. Dragt, F. Neri, and G. Rangarajan, "General Moment Invariants + * for Linear Hamiltonian Systems," Phys. Rev. A 45, 2572-2585 (1992), + * doi:10.1103/PhysRevA.45.2572. + * + * @param[in] Sigma symmetric 6x6 covariance matrix + * @returns tuple containing invariants I2, I4, and I6 + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal + > + KineticInvariants ( + amrex::Array2D const & Sigma + ); + + /** Returns the three eigenemittances + * denoted e1, e2, and e3 as constructed from the 6x6 + * beam covariance matrix. These three quantities are invariant + * under any linear symplectic transport map, and reduce to + * the projected normalized rms emittances in the limit of + * uncoupled transport. These quantities are described in: + * + * G. Rangarajan, F. Neri, and A. Dragt, "Generalized Emittance + * Invariants," in Proc. 1989 IEEE Part. Accel. Conf., Chicago, IL, + * 1989, doi:10.1109/PAC.1989.73422. + * A. Dragt, F. Neri, and G. Rangarajan, "General Moment Invariants + * for Linear Hamiltonian Systems," Phys. Rev. A 45, 2572-2585 (1992), + * doi:10.1103/PhysRevA.45.2572. + * + * @param[in] Sigma symmetric 6x6 covariance matrix + * @returns tuple containing eigenemittances e1, e2, and e3 + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal> + Eigenemittances ( + amrex::Array2D const & Sigma + ); + +} // namespace impactx::diagnostics + +#endif // IMPACTX_EMITTANCE_INVARIANTS diff --git a/src/particles/diagnostics/EmittanceInvariants.cpp b/src/particles/diagnostics/EmittanceInvariants.cpp new file mode 100644 index 000000000..20c6df32a --- /dev/null +++ b/src/particles/diagnostics/EmittanceInvariants.cpp @@ -0,0 +1,146 @@ +/* Copyright 2022-2023 The Regents of the University of California, through Lawrence + * Berkeley National Laboratory (subject to receipt of any required + * approvals from the U.S. Dept. of Energy). All rights reserved. + * + * This file is part of ImpactX. + * + * Authors: Chad Mitchell, Axel Huebl + * License: BSD-3-Clause-LBNL + */ +#include "CovarianceMatrixMath.H" + +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace impactx::diagnostics +{ + + /** This function returns the three independent kinetic invariants + * denoted I2, I4, and I6 as constructed from the 6x6 + * beam covariance matrix. These three quantities are invariant + * under any linear symplectic transport map, and are used in the + * calculation of the three eigenemittances. + * + * input - Sigma symmetric 6x6 covariance matrix + * returns - tuple containing invarants I2, I4, and I6 + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal + > + KineticInvariants ( + amrex::Array2D const & Sigma + ) + { + using namespace amrex::literals; + + std::tuple invariants; + amrex::ParticleReal I2 = 0.0_prt; + amrex::ParticleReal I4 = 0.0_prt; + amrex::ParticleReal I6 = 0.0_prt; + + // Intermediate matrices used for storage. + amrex::Array2D S1; + amrex::Array2D S2; + amrex::Array2D S4; + amrex::Array2D S6; + + // Construct the matrix S1 = Sigma*J. This is a + // permutation of the columns of Sigma with + // a change of sign. + for (int i = 1; i < 7; i++) { + for (int j = 1; j < 7; j++) { + if (j % 2 != 0) { + S1(i,j) = -Sigma(i,j+1); // if j is odd + } + else { + S1(i,j) = +Sigma(i,j-1); // if j is even + } + } + } + + // Carry out necessary matrix multiplications (3 are needed). + S2 = impactx::diagnostics::MultiplyMat(S1,S1); + S4 = impactx::diagnostics::MultiplyMat(S2,S2); + S6 = impactx::diagnostics::MultiplyMat(S2,S4); + + // Define the three kinematic invariants (should be nonnegative). + I2 = -impactx::diagnostics::TraceMat(S2)/2.0_prt; + I4 = +impactx::diagnostics::TraceMat(S4)/2.0_prt; + I6 = -impactx::diagnostics::TraceMat(S6)/2.0_prt; + + + invariants = std::make_tuple(I2,I4,I6); + return invariants; + } + + + /** This function returns the three eigenemittances + * denoted e1, e2, and e3 as constructed from the 6x6 + * beam covariance matrix. These three quantities are invariant + * under any linear symplectic transport map, and reduce to + * the projected normalized rms emittances in the limit of + * uncoupled transport. + * + * input - Sigma symmetric 6x6 covariance matrix + * returns - tuple containing eigenemittances e1, e2, and e3 + */ + std::tuple< + amrex::ParticleReal, + amrex::ParticleReal, + amrex::ParticleReal> + Eigenemittances ( + amrex::Array2D const & Sigma + ) + { + BL_PROFILE("impactx::diagnostics::Eigenemittances"); + + using namespace amrex::literals; + + std::tuple invariants; + std::tuple roots; + std::tuple emittances; + + // Get the invariants I2, I4, and I6 from the covariance matrix. + invariants = KineticInvariants(Sigma); + amrex::ParticleReal I2 = std::get<0>(invariants); + amrex::ParticleReal I4 = std::get<1>(invariants); + amrex::ParticleReal I6 = std::get<2>(invariants); + + // Construct the coefficients of the cubic polynomial. + // This expression for the characteristic polynomial can be found in: + // V. Balandin, W. Decking, and N. Golubeva, "Relations Between Projected + // Emittances and Eigenemittances," in IPAC2013, Shanghai, China, 2013, + // doi:10.48550/arXiv.1305.1532. + amrex::ParticleReal a = 1.0_prt; + amrex::ParticleReal b = -I2; + amrex::ParticleReal c = (pow(I2,2)-I4)/2.0_prt; + amrex::ParticleReal d = -pow(I2,3)/6.0_prt + I2*I4/2.0_prt - I6/3.0_prt; + + // Return the cubic coefficients + //std::cout << "Return a,b,c,d " << a << " " << b << " " << c << " " << d << "\n"; + + // Solve for the roots to obtain the eigenemittances. + // Caution: The order of e1,e2,e3 should be consistent with the + // order ex,ey,et in the limit of uncoupled transport. + // The order below is important and has been checked. + roots = CubicRootsTrig(a,b,c,d); + amrex::ParticleReal e1 = sqrt(std::abs(std::get<1>(roots))); + amrex::ParticleReal e2 = sqrt(std::abs(std::get<2>(roots))); + amrex::ParticleReal e3 = sqrt(std::abs(std::get<0>(roots))); + + emittances = std::make_tuple(e1,e2,e3); + return emittances; + } + + +} // namespace impactx::diagnostics diff --git a/src/particles/diagnostics/ReducedBeamCharacteristics.cpp b/src/particles/diagnostics/ReducedBeamCharacteristics.cpp index 0b6ed024d..a7b623beb 100644 --- a/src/particles/diagnostics/ReducedBeamCharacteristics.cpp +++ b/src/particles/diagnostics/ReducedBeamCharacteristics.cpp @@ -12,6 +12,7 @@ #include "particles/ImpactXParticleContainer.H" #include "particles/ReferenceParticle.H" +#include "EmittanceInvariants.H" #include // for TinyProfiler #include // for AMREX_GPU_DEVICE @@ -33,6 +34,9 @@ namespace impactx::diagnostics RefPart const ref_part = pc.GetRefParticle(); // reference particle charge in C amrex::ParticleReal const q_C = ref_part.charge; + // reference particle relativistic beta*gamma + amrex::ParticleReal const bg = ref_part.beta_gamma(); + amrex::ParticleReal const bg2 = bg*bg; // preparing access to particle data: SoA using PType = typename ImpactXParticleContainer::SuperParticleType; @@ -151,7 +155,7 @@ namespace impactx::diagnostics * https://stackoverflow.com/questions/55136414/constexpr-variable-captured-inside-lambda-loses-its-constexpr-ness */ // number of reduction operations in second concurrent batch - static constexpr std::size_t num_red_ops_2 = 14; + static constexpr std::size_t num_red_ops_2 = 22; // prepare reduction operations for calculation of mean square and correlation values amrex::TypeMultiplier reduce_ops_2; using ReducedDataT2 = amrex::TypeMultiplier; @@ -182,12 +186,20 @@ namespace impactx::diagnostics const amrex::ParticleReal p_xpx = (p_x-x_mean)*(p_px-px_mean)*p_w; const amrex::ParticleReal p_ypy = (p_y-y_mean)*(p_py-py_mean)*p_w; const amrex::ParticleReal p_tpt = (p_t-t_mean)*(p_pt-pt_mean)*p_w; - // prepare correlations for dispersion + // prepare correlations for dispersion (4 required) const amrex::ParticleReal p_xpt = (p_x-x_mean)*(p_pt-pt_mean)*p_w; const amrex::ParticleReal p_pxpt = (p_px-px_mean)*(p_pt-pt_mean)*p_w; const amrex::ParticleReal p_ypt = (p_y-y_mean)*(p_pt-pt_mean)*p_w; const amrex::ParticleReal p_pypt = (p_py-py_mean)*(p_pt-pt_mean)*p_w; - + // prepare additional cross-plane correlations (8 required) + const amrex::ParticleReal p_xy = (p_x-x_mean)*(p_y-y_mean)*p_w; + const amrex::ParticleReal p_xpy = (p_x-x_mean)*(p_py-py_mean)*p_w; + const amrex::ParticleReal p_xt = (p_x-x_mean)*(p_t-t_mean)*p_w; + const amrex::ParticleReal p_pxy = (p_px-px_mean)*(p_y-y_mean)*p_w; + const amrex::ParticleReal p_pxpy = (p_px-px_mean)*(p_py-py_mean)*p_w; + const amrex::ParticleReal p_pxt = (p_px-px_mean)*(p_t-t_mean)*p_w; + const amrex::ParticleReal p_yt = (p_y-y_mean)*(p_t-t_mean)*p_w; + const amrex::ParticleReal p_pyt = (p_py-py_mean)*(p_t-t_mean)*p_w; const amrex::ParticleReal p_charge = q_C*p_w; @@ -195,6 +207,7 @@ namespace impactx::diagnostics p_px_ms, p_py_ms, p_pt_ms, p_xpx, p_ypy, p_tpt, p_xpt, p_pxpt, p_ypt, p_pypt, + p_xy, p_xpy, p_xt, p_pxy, p_pxpy, p_pxt, p_yt, p_pyt, p_charge}; }, reduce_ops_2 @@ -207,6 +220,7 @@ namespace impactx::diagnostics * px_ms, py_ms, pt_ms, * xpx, ypy, tpt, * p_xpt, p_pxpt, p_ypt, p_pypt, + * p_xy, p_xpy, p_xt, p_pxy, p_pxpy, p_pxt, p_yt, p_pyt, * charge */ amrex::constexpr_for<0, num_red_ops_2> ([&](auto i) { @@ -248,7 +262,15 @@ namespace impactx::diagnostics amrex::ParticleReal const pxpt = values_per_rank_2nd.at(10) /= w_sum; amrex::ParticleReal const ypt = values_per_rank_2nd.at(11) /= w_sum; amrex::ParticleReal const pypt = values_per_rank_2nd.at(12) /= w_sum; - amrex::ParticleReal const charge = values_per_rank_2nd.at(13); + amrex::ParticleReal const xy = values_per_rank_2nd.at(13) /= w_sum; + amrex::ParticleReal const xpy = values_per_rank_2nd.at(14) /= w_sum; + amrex::ParticleReal const xt = values_per_rank_2nd.at(15) /= w_sum; + amrex::ParticleReal const pxy = values_per_rank_2nd.at(16) /= w_sum; + amrex::ParticleReal const pxpy = values_per_rank_2nd.at(17) /= w_sum; + amrex::ParticleReal const pxt = values_per_rank_2nd.at(18) /= w_sum; + amrex::ParticleReal const yt = values_per_rank_2nd.at(19) /= w_sum; + amrex::ParticleReal const pyt = values_per_rank_2nd.at(20) /= w_sum; + amrex::ParticleReal const charge = values_per_rank_2nd.at(21); // standard deviations of positions amrex::ParticleReal const sig_x = std::sqrt(x_ms); amrex::ParticleReal const sig_y = std::sqrt(y_ms); @@ -283,6 +305,65 @@ namespace impactx::diagnostics amrex::ParticleReal const alpha_y = - ypy_d / emittance_yd; amrex::ParticleReal const alpha_t = - tpt / emittance_t; + // Calculate normalized emittances + amrex::ParticleReal emittance_xn = emittance_x * bg; + amrex::ParticleReal emittance_yn = emittance_y * bg; + amrex::ParticleReal emittance_tn = emittance_t * bg; + + // Determine whether to calculate eigenemittances, and initialize + amrex::ParmParse pp_diag("diag"); + bool compute_eigenemittances = false; + pp_diag.queryAdd("eigenemittances", compute_eigenemittances); + amrex::ParticleReal emittance_1 = emittance_xn; + amrex::ParticleReal emittance_2 = emittance_yn; + amrex::ParticleReal emittance_3 = emittance_tn; + + if (compute_eigenemittances) { + // Store the covariance matrix in dynamical variables: + amrex::Array2D Sigma; + Sigma(1,1) = x_ms; + Sigma(1,2) = xpx * bg; + Sigma(1,3) = xy; + Sigma(1,4) = xpy * bg; + Sigma(1,5) = xt; + Sigma(1,6) = xpt * bg; + Sigma(2,1) = xpx * bg; + Sigma(2,2) = px_ms * bg2; + Sigma(2,3) = pxy * bg; + Sigma(2,4) = pxpy * bg2; + Sigma(2,5) = pxt * bg; + Sigma(2,6) = pxpt * bg2; + Sigma(3,1) = xy; + Sigma(3,2) = pxy * bg; + Sigma(3,3) = y_ms; + Sigma(3,4) = ypy * bg; + Sigma(3,5) = yt; + Sigma(3,6) = ypt * bg; + Sigma(4,1) = xpy * bg; + Sigma(4,2) = pxpy * bg2; + Sigma(4,3) = ypy * bg; + Sigma(4,4) = py_ms * bg2; + Sigma(4,5) = pyt * bg; + Sigma(4,6) = pypt * bg2; + Sigma(5,1) = xt; + Sigma(5,2) = pxt * bg; + Sigma(5,3) = yt; + Sigma(5,4) = pyt * bg; + Sigma(5,5) = t_ms; + Sigma(5,6) = tpt * bg; + Sigma(6,1) = xpt * bg; + Sigma(6,2) = pxpt * bg2; + Sigma(6,3) = ypt * bg; + Sigma(6,4) = pypt * bg2; + Sigma(6,5) = tpt * bg; + Sigma(6,6) = pt_ms * bg2; + // Calculate eigenemittances + std::tuple emittances = Eigenemittances(Sigma); + emittance_1 = std::get<0>(emittances); + emittance_2 = std::get<1>(emittances); + emittance_3 = std::get<2>(emittances); + } + std::unordered_map data; data["x_mean"] = x_mean; data["x_min"] = x_min; @@ -322,6 +403,14 @@ namespace impactx::diagnostics data["dispersion_y"] = dispersion_y; data["dispersion_py"] = dispersion_py; data["charge_C"] = charge; + data["emittance_xn"] = emittance_xn; + data["emittance_yn"] = emittance_yn; + data["emittance_tn"] = emittance_tn; + if (compute_eigenemittances) { + data["emittance_1"] = emittance_1; + data["emittance_2"] = emittance_2; + data["emittance_3"] = emittance_3; + } return data; } diff --git a/src/python/ImpactX.cpp b/src/python/ImpactX.cpp index 0326ca496..02e15a4d7 100644 --- a/src/python/ImpactX.cpp +++ b/src/python/ImpactX.cpp @@ -223,6 +223,16 @@ void init_ImpactX (py::module& m) }, "Number of longitudinal bins used for CSR calculations (default: 150)." ) + .def_property("eigenemittances", + [](ImpactX & /* ix */) { + return detail::get_or_throw("diag", "eigenemittances"); + }, + [](ImpactX & /* ix */, bool const enable) { + amrex::ParmParse pp_diag("diag"); + pp_diag.add("eigenemittances", enable); + }, + "Enable or disable eigenemittance diagnostic calculations (default: disabled)." + ) .def_property("space_charge", [](ImpactX & /* ix */) { return detail::get_or_throw("algo", "space_charge"); diff --git a/tests/python/test_transformation.py b/tests/python/test_transformation.py index 93b52966c..ad660564c 100644 --- a/tests/python/test_transformation.py +++ b/tests/python/test_transformation.py @@ -81,7 +81,7 @@ def test_transformation(): for key, val in rbc_s0.items(): if not np.isclose(val, rbc_s[key], rtol=rtol, atol=atol): print(f"initial[{key}]={val}, final[{key}]={rbc_s[key]} not equal") - assert np.isclose(val, rbc_s[key], rtol=rtol, atol=atol) + assert False # assert that the t-based beam is different, at least in the following keys: large_st_diff_keys = [ "beta_x",