Skip to content

Add resistive wall Wakefields#111

Merged
ChristopherMayes merged 35 commits intomasterfrom
add_wakefields
Jan 15, 2026
Merged

Add resistive wall Wakefields#111
ChristopherMayes merged 35 commits intomasterfrom
add_wakefields

Conversation

@ChristopherMayes
Copy link
Owner

@ChristopherMayes ChristopherMayes commented Jul 17, 2025

New Classes

ResistiveWallWakefield

  • Accurate impedance-based model using FFT convolution
  • Supports both round (circular pipe) and flat (parallel plate) geometries
  • Computes impedance Z(k) using surface impedance formalism with AC conductivity (Drude model)
  • Computes wakefield W(z) via FFT-based cosine transform
  • Material presets: from_material("copper-slac-pub-10707", radius=2.5e-3)

ResistiveWallPseudomode

  • Fast O(N) pseudomode approximation using damped sinusoid fit
  • ~10-20% error compared to full impedance model
  • Suitable for production tracking simulations
  • Exports to Bmad format via to_bmad()

ImpedanceWakefield

  • Create wakefield from any user-defined impedance function Z(k)
  • FFT-based wake evaluation for arrays (fast)
  • Quadrature for scalar evaluation (accurate)

PseudomodeWakefield

  • General damped sinusoid wakefield model
  • Supports multiple modes for complex wake shapes
  • O(N) particle kick algorithm

Geometry Enum

  • Type-safe geometry specification: Geometry.ROUND or Geometry.FLAT
  • Backwards compatible with string inputs ("round", "flat")

Shared Wakefield API

All wakefield classes inherit from WakefieldBase and share a consistent interface:

Method Description
wake(z) Evaluate wakefield W(z) [V/C/m]
impedance(k) Evaluate impedance Z(k) [Ω/m]
convolve_density(ρ, dz, plot=False) FFT-based density convolution
particle_kicks(z, weight) Per-particle momentum kicks [eV/m]
plot() Visualize wakefield
plot_impedance() Visualize impedance (Re and Im)

ParticleGroup Integration

New methods added to ParticleGroup:

Method Description
apply_wakefield(wake, length, inplace=False) Apply wakefield kicks over a length of beamline
wakefield_plot(wake) Plot per-particle kicks overlaid with bunch density

Low-Level Functions

Exposed in pmd_beamphysics.wakefields.resistive_wall:

  • ac_conductivity() — Drude model AC conductivity
  • surface_impedance() — Surface impedance for conducting wall
  • longitudinal_impedance_round() / longitudinal_impedance_flat() — Direct impedance calculation
  • wakefield_from_impedance() — Wakefield via quadrature
  • wakefield_from_impedance_fft() — Wakefield via FFT (fast)
  • characteristic_length() — Characteristic length scale s₀
  • krs0_round() / krs0_flat() — Polynomial fits for k_r·s₀
  • Qr_round() / Qr_flat() — Polynomial fits for Q_r

Documentation

New example notebooks in docs/examples/wakefields/:

Notebook Description
resistive_wall.ipynb Comprehensive tutorial for resistive wall models
impedance_wakefield.ipynb Custom impedance functions with ImpedanceWakefield

References

  • K. Bane and G. Stupakov, SLAC-PUB-10707 (2004)
  • D. Sagan, Bmad Manual, Section 24.6
  • A. Chao, "Physics of Collective Beam Instabilities," Wiley (1993)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a comprehensive resistive wall impedance module (ResistiveWallImpedance) that computes longitudinal impedance Z(k) and wakefield W(z) through direct numerical integration. This complements the existing fast pseudomode approximation (ResistiveWallWakefield) by providing high-accuracy calculations suitable for validation and research applications.

Key changes:

  • New ResistiveWallImpedance class supporting flat and round geometries with direct impedance/wakefield calculations
  • Enhanced ResistiveWallWakefield with lazy matplotlib imports, improved __repr__, and standardized validation
  • Added wakefield_plot() visualization function to ParticleGroup and plot module
  • Updated documentation with two example notebooks and navigation entries

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pmd_beamphysics/wakefields/resistive_wall_impedance.py New module implementing numerical impedance/wakefield calculations with AC conductivity support
pmd_beamphysics/wakefields/resistive_wall.py Existing pseudomode wakefield implementation with improved documentation and lazy imports
pmd_beamphysics/wakefields/init.py Module initialization exporting both wakefield classes and utility functions
pmd_beamphysics/plot.py Added wakefield_plot() function and enhanced density_plot() with flexible axis support
pmd_beamphysics/particles.py Added wakefield_plot() method to ParticleGroup class
mkdocs.yml Added wakefields section to documentation navigation
docs/examples/wakefields/resistive_wall_wakefield.ipynb Tutorial notebook for pseudomode wakefield usage
docs/examples/wakefields/resistive_wall_impedance.ipynb Tutorial notebook comparing numerical and pseudomode approaches
docs/examples/data/SLAC-PUB-10707-digitized-Fig8-AC-Cu.csv Reference data for flat geometry validation
docs/examples/data/SLAC-PUB-10707-digitized-Fig4-AC-Cu.csv Reference data for round geometry validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ChristopherMayes and others added 2 commits January 6, 2026 16:16
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@ChristopherMayes ChristopherMayes changed the title Add longitudinal Wakefields Add resistive wall Wakefields Jan 7, 2026
@MichaelEhrlichman
Copy link

A warning from pytest on my own desktop and in the github test runner:

tests/test_wavefront.py::test_from_gaussian_polarization /home/runner/miniconda3/envs/beamphysics-dev/lib/python3.12/site-packages/pmd_beamphysics/wavefront/wavefront.py:1867: RuntimeWarning: invalid value encountered in divide u = u / np.sqrt(integral2)

@MichaelEhrlichman
Copy link

docs/examples/wakefields/*.ipynb run as expected. Comparison to digitized plot from SLAC pub 10707 suggests formulas are correct. New code in the wakefields directory is clean and well documented.

Copy link

@MichaelEhrlichman MichaelEhrlichman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition to openPMD-beamphysics!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a header, even a simple one.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a header, even a simple one.

@ChristopherMayes ChristopherMayes marked this pull request as ready for review January 14, 2026 18:23
"ResistiveWallWakefieldBase",
"ResistiveWallWakefield",
"ResistiveWallPseudomode",
"pseudomode", # Legacy alias
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the code is all new, there shouldn't really be legacy stuff, right? If this was from previous version, consider removing before merge so you don't have to support it for the rest of time.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed.

"""
return self.wake(-1e-15)

def apply_to_particles(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Euler's method for one step and only for pz. I am worried about not consistently changing the other coordinates, and also since this is ODE solving, the interface for the function really locks you in to having a basic method. I'm also not sure that ODE solving should be "owned" by the force class.

If it was me, I would consider moving ODE solving to it's own place in the code (if it's something openpmd-beamphysics should implement) with an interface where it could be expanded with more advanced methods down the line or remove from the class since .particle_kicks already exists and you can let users to the more ad-hoc pz+=wake*len on their own without building it into the code.

Alternatively, you have scipy as a dependency already and could wrap scipy.integrate in a way to accept ParticleGroup objects and force objects.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly for analysis convenience. I don't plan on implementing solvers in this package. Note that simply multiplying by a length is what is often done in practice. For example. at LCLS the resistive wall wake is applied in one place with a 1 km length! I can add a note that this is simple.


Parameters
----------
particle_group : ParticleGroup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also the only place in the WakefieldBase interface that uses ParticleGroup. Taking it out fully decouples the two classes and simplifies your interface.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Instead now we have ParticleGroup.apply_wakefield(wakefield: WakefieldBase, length: float)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skimmed this and it appears to have a lot of duplicate functions from pmd_beamphysics/wakefields/resistive_wall.py and also isn't imported by anything. Is this leftover from some refactoring?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a major refactor, removing a lot of redundant code.

else: # quad
return wakefield_from_impedance(z, self.impedance, k_max=k_max)

def convolve_density(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these functions are duplicated across the two resistive wall classes and could be promoted to the parent class for DRY code.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be cleaned up with the latest refactor.

return self._wakefield_func(z)

# Numerical cosine transform
from scipy.integrate import quad
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move imports to top of file

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is quite long, consider breaking out the two concrete wakefield implementations into own file.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is also very long, could also break out concrete classes? Github is lagging badly rendering the diffs even without having an existing file to compare to.

Maybe:

wakefields/
  base.py
  impedance.py
  psuedomode.py
  tabular.py
  resistive_wall/
    base.py
    psuedomode.py
    impedance.py

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if return_figure:
return fig

def wakefield_plot(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the worst thing, but I will point out that this is the only method coupling ParticleGroup with the wakefields module. Removing this just means the user calls

wakefield_plot(pg, wf)

instead of

pg.wakefield_plot(wf)

but reduces coupling between the sections of the code.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason that I don't do that is because it requires another import.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
from ..base import WakefieldBase


class Geometry(str, Enum):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was more like this:

class Geometry:
    ...

class GeometryRound(Geometry):
    radius: float

    def W0(self):
        return c_light * Z0 / (np.pi * self.radius**2)

class GeometryFlat(Geometry):
    gap: float

    def W0(self):
        return c_light * Z0 /  self.gap**2 * np.pi / 4

That way the parameter names are specialized to the type of geometry and potentially more complicated things that need stuff more than a radius could be accomodated later on.

Then

class ResistiveWallWakefieldBase(WakefieldBase):
    conductivity: float
    relaxation_time: float
    geometry: Geometry = GeometryRound()

    def W0(self):
        return self.geometry.W0()

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, but then the user would have to either import these classes, or we'd do some post init. I don't really want the user to have to import extra things in order to instantiate the wakefield.

ax : matplotlib.axes.Axes, optional
Axes to plot on. If None, creates a new figure.
"""
import matplotlib.pyplot as plt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some places where imports are inside functions. Preferably they should be moved to top of file to make dependencies explicit.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@ChristopherMayes ChristopherMayes merged commit c7342b7 into master Jan 15, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants