Extensions ==================== This document describes the built-in extensions available in π-PIC, their physical models, parameters, and usage examples. Overview -------- Extensions add physics modules to simulations via custom handlers. They are registered using ``sim.add_handler()`` and operate on particles or fields during the simulation advance. Extensions ---------- QED Extensions ^^^^^^^^^^^^^^ qed_gonoskov2015 """""""""""""""" **Description:** QED photon emission and pair production using the locally constant field approximation (LCFA) with rejection sampling. **Reference:** A. Gonoskov et al., PRE (2015), arXiv:1412.6426 **Physics:** - Photon emission (synchrotron radiation) from electrons/positrons - Pair production from photons - Based on synchrotron functions with efficient approximations **Parameters:** .. code-block:: python handler = qed.handler(electron_type, positron_type, photon_type, probability_threshold=1e-3, probability_subcycle=0.1) - ``electron_type`` (int): Type index for electrons (use ``sim.get_type_index('electron')``). - ``positron_type`` (int): Type index for positrons (use ``sim.get_type_index('positron')``). - ``photon_type`` (int): Type index for photons (use ``sim.get_type_index('photon')``). - ``probability_threshold`` (float): Below this threshold, event weights are increased and events are rarified. Default: 1e-3. - ``probability_subcycle`` (float): Maximum estimated probability per QED event within a single substep. Default: 0.1. **Usage:** .. code-block:: python import pipic.extensions.qed_gonoskov2015 as qed # Must have electron, positron, and photon types defined sim.add_particles(name='electron', ...) sim.add_particles(name='positron', ...) sim.add_particles(name='photon', charge=0, mass=0, ...) # Get type indices electron_type = sim.get_type_index('electron') positron_type = sim.get_type_index('positron') photon_type = sim.get_type_index('photon') handler_ptr = qed.handler(electron_type, positron_type, photon_type) sim.add_handler(name='qed_gonoskov2015', subject='electron, positron, photon', handler=handler_ptr) qed_volokitin2023 """"""""""""""""" **Description:** Optimized QED event generator using minimal rate computations per event (faster equivalent to qed_gonoskov2015). **Reference:** V. Volokitin et al., JCS **74**, 102170 (2023) - `DOI: 10.1016/j.jocs.2023.102170 `__ **Physics:** - Photon emission (Compton scattering) from electrons/positrons - Pair production (Breit-Wheeler process) from photons - Based on Fast_QED module from pyHiChi - Optimized algorithm minimizing rate computations **Developer:** Joel Magnusson (joel.magnusson@physics.gu.se), based on pyHiChi implementation **Parameters:** .. code-block:: python handler = qed.handler(electron_type, positron_type, photon_type) - ``electron_type`` (int): Type index for electrons (use ``sim.get_type_index('electron')``). - ``positron_type`` (int): Type index for positrons (use ``sim.get_type_index('positron')``). - ``photon_type`` (int): Type index for photons (use ``sim.get_type_index('photon')``). **Usage:** .. code-block:: python import pipic.extensions.qed_volokitin2023 as qed # Must have electron, positron, and photon types defined sim.add_particles(name='electron', ...) sim.add_particles(name='positron', ...) sim.add_particles(name='photon', charge=0, mass=0, ...) # Get type indices electron_type = sim.get_type_index('electron') positron_type = sim.get_type_index('positron') photon_type = sim.get_type_index('photon') handler_ptr = qed.handler(electron_type, positron_type, photon_type) sim.add_handler(name='qed_volokitin2023', subject='electron, positron, photon', handler=handler_ptr) **Notes:** - Equivalent physics to qed_gonoskov2015 but with optimized performance. - Recommended over qed_gonoskov2015 for production simulations. - See ``examples/qed_volokitin2023_test.py`` for usage example. Particle Management ^^^^^^^^^^^^^^^^^^^ downsampler_gonoskov2022 """""""""""""""""""""""" **Description:** Agnostic conservative downsampling for dynamically reducing particle count while preserving distributions and conserved quantities. **Reference:** A. Gonoskov, CPC **271**, 108200 (2022) - `DOI: 10.1016/j.cpc.2021.108200 `__, `arXiv:1607.03755 `__ **Physics:** - Reduces particle ensemble without introducing flattening or systematic distribution changes - Preserves conserved quantities: total weight, energy, momentum, CIC contributions - Operates on subsets of particles contributing to nearby grid nodes (2/4/8 nodes depending on dimensionality) - Can be configured for multiple particle types via ``add_assignment()`` **Developer:** Arkady Gonoskov (arkady.gonoskov@physics.gu.se) **Parameters:** .. code-block:: python handler = downsampler.handler(ensemble_data, type_index, preserve_energy=True, preserve_momentum=True, preserve_cic_weight=True, cap=15, target_ratio=1.0) - ``ensemble_data`` (int): Ensemble data pointer from ``sim.ensemble_data()``. - ``type_index`` (int): Type index of particles to downsample. - ``preserve_energy`` (bool): Preserve energy of particle subsets. Default: True. - ``preserve_momentum`` (bool): Preserve momentum of particle subsets. Default: True. - ``preserve_cic_weight`` (bool): Preserve CIC contributions to grid nodes. Default: True. - ``cap`` (int): Threshold for number of particles per subset (effectively per cell). Default: 15. - ``target_ratio`` (float): Target particle count after downsampling as fraction of cap (≤1.0). Default: 1.0. **Additional Configuration:** .. code-block:: python downsampler.add_assignment(type_index, preserve_energy=True, preserve_momentum=True, preserve_cic_weight=True, cap=15, target_ratio=1.0) - Use to configure downsampling for additional particle types after initialization. - Parameters same as ``handler()`` except ``ensemble_data`` not needed. **Usage:** .. code-block:: python import pipic.extensions.downsampler_gonoskov2022 as downsampler # Get ensemble data pointer ensemble_ptr = sim.ensemble_data() # Get type index for electrons electron_type = sim.get_type_index('electron') # Initialize handler handler_ptr = downsampler.handler(ensemble_ptr, electron_type, preserve_energy=True, preserve_momentum=True, preserve_cic_weight=True, cap=15, target_ratio=1.0) # Add additional particle types if needed positron_type = sim.get_type_index('positron') downsampler.add_assignment(positron_type, cap=15, target_ratio=1.0) # Register handler (must use 'cells' as subject) sim.add_handler(name='downsampler', subject='cells', handler=handler_ptr) **Notes:** - Subject must be set to 'cells', not specific particle types. - Downsampling applied when particle count in a cell exceeds ``cap``. - Reduces particles to ``target_ratio * cap`` when triggered. - See ``examples/downsampler_gonoskov2022_test.py`` for usage example. Radiation Reaction ^^^^^^^^^^^^^^^^^^ landau_lifshitz """"""""""""""" **Description:** Account for radiation reaction using leading terms of the Landau-Lifshitz model. **Physics:** - Classical radiation reaction force on charged particles - Based on Landau-Lifshitz equation (leading order) - Accounts for energy loss due to radiation in strong electromagnetic fields **Developer:** Joel Magnusson (joel.magnusson@physics.gu.se) **Parameters:** .. code-block:: python handler = landau_lifshitz.handler() - No parameters required for initialization. **Usage:** .. code-block:: python import pipic.extensions.landau_lifshitz as ll # Initialize handler handler_ptr = ll.handler() # Register handler for electrons and positrons sim.add_handler(name='landau_lifshitz', subject='electron, positron', handler=handler_ptr) **Notes:** - Applies classical radiation reaction force to particles. - Most relevant for high-intensity laser-plasma interactions where radiation damping is significant. - Can be applied to any charged particle types. Field Generation ^^^^^^^^^^^^^^^^ focused_pulse """"""""""""" **Description:** Inject a focused Gaussian laser pulse via a field handler (paraxial approximation). **Configuration functions:** .. code-block:: python focused_pulse.set_box(nx, ny, nz, xmin, ymin, zmin, xmax, ymax, zmax) focused_pulse.set_center(x, y, z) focused_pulse.set_path(x, y, z) # Propagation direction focused_pulse.set_e_axis(x, y, z) # Polarization direction focused_pulse.set_theta_max(theta_max) # Angular aperture focused_pulse.set_l_size(l_size) # Characteristic length focused_pulse.set_shape(shape) # Temporal shape function focused_pulse.add_pulse() # Add configured pulse focused_pulse.clear_pulse() # Clear all pulses **Usage:** .. code-block:: python import pipic.extensions.focused_pulse as fp fp.set_box(sim.nx, sim.ny, sim.nz, sim.xmin, sim.ymin, sim.zmin, sim.xmax, sim.ymax, sim.zmax) fp.set_center(0, 0, 0) fp.set_path(1, 0, 0) fp.set_e_axis(0, 1, 0) fp.set_theta_max(30.0) fp.set_l_size(3e-4) fp.set_shape(lambda t: ...) fp.add_pulse() field_handler_ptr = fp.field_loop_cb() sim.add_handler(name='focused_pulse', subject='cells', field_handler=field_handler_ptr) **Notes:** - Only use as a field handler (no particle handler). - Paraxial approximation; keep beam waist and angle consistent with grid. Boundary Handling ^^^^^^^^^^^^^^^^^ x_reflector_c """"""""""""" **Description:** Reflect particles in a finite region along x. **Parameters:** .. code-block:: python handler = x_reflector.handler(location, thickness) - ``location`` (float): Center position of reflective region (cm). - ``thickness`` (float): Thickness of reflective region (cm). **Usage:** .. code-block:: python import pipic.extensions.x_reflector_c as reflector handler_ptr = reflector.handler(location=1e-3, thickness=1e-5) sim.add_handler(name='x_reflector', subject='electron, positron', handler=handler_ptr) **Behavior:** - Particles within the region with :math:`p_x > 0` have momentum reversed: :math:`p_x \to -p_x`. - Region spans ``location - thickness/2`` to ``location + thickness/2``. x_converter_c """"""""""""" **Description:** Convert particles to another type when traversing an x-plane region. **Parameters:** .. code-block:: python handler = x_converter.handler(location, thickness, typeTo) - ``location`` (float): Center position of conversion region (cm). - ``thickness`` (float): Thickness of conversion region (cm). - ``typeTo`` (int): Target particle type index. **Usage:** .. code-block:: python import pipic.extensions.x_converter_c as converter photon_idx = sim.get_type_index('photon') handler_ptr = converter.handler(location=0, thickness=1e-4, typeTo=photon_idx) sim.add_handler(name='x_converter', subject='electron', handler=handler_ptr) **Notes:** - Converted particles keep position, momentum, and weight. absorbing_boundaries """""""""""""""""""" **Description:** Absorb particles and damp fields near boundaries. **Parameters (particle handler):** .. code-block:: python handler = absorber.handler(ensemble_data, simulation_box, characteristic_wavelength, density_profile=-1, boundary_size=-1.0, axis='x', fall=-1.0, temperature=0.0, particles_per_cell=1.0, remove_particles_every=10, moving_window_velocity=0.0, moving_window_direction='x') - ``ensemble_data`` (int): Ensemble data pointer from ``sim.ensemble_data()``. - ``simulation_box`` (int): Pointer from ``sim.simulation_box()``. - ``characteristic_wavelength`` (float): Characteristic wavelength (cm). - ``density_profile`` (int): Density profile callback address. Default: -1. - ``boundary_size`` (float): Layer thickness (cm). Default: -1.0 (auto). - ``axis`` (char): Boundary axis. Default: 'x'. - ``fall`` (float): Field damping coefficient. Default: -1.0. - ``temperature`` (float): Temperature for re-injected particles. Default: 0.0. - ``particles_per_cell`` (float): Density for re-injection. Default: 1.0. - ``remove_particles_every`` (int): Removal frequency. Default: 10. - ``moving_window_velocity`` (float): Moving window speed (cm/s). Default: 0.0. - ``moving_window_direction`` (char): Moving window axis. Default: 'x'. **Parameters (field handler):** .. code-block:: python field_handler = absorber.field_handler(simulation_box, characteristic_wavelength, boundary_size=-1.0, axis='x', fall=-1.0) - ``simulation_box`` (int): Simulation box pointer. - ``characteristic_wavelength`` (float): Characteristic wavelength (cm). - ``boundary_size`` (float): Absorption layer thickness (cm). Default: -1.0. - ``axis`` (char): Boundary axis. Default: 'x'. - ``fall`` (float): Field damping coefficient. Default: -1.0. **Usage:** .. code-block:: python import pipic.extensions.absorbing_boundaries as absorber ensemble_ptr = sim.ensemble_data() simbox_ptr = sim.simulation_box() handler_ptr = absorber.handler(ensemble_ptr, simbox_ptr, characteristic_wavelength=0.8e-4, boundary_size=20*dx, axis='x') field_handler_ptr = absorber.field_handler(simbox_ptr, characteristic_wavelength=0.8e-4, boundary_size=20*dx, axis='x') sim.add_handler(name='absorber', subject='all_types', handler=handler_ptr, field_handler=field_handler_ptr) **Behavior:** - Particle weights reduced in absorption layer; optional re-injection. - Fields damped near boundaries to suppress reflections. Moving Window ^^^^^^^^^^^^^ moving_window """"""""""""" **Description:** Move the simulation window while maintaining plasma injection. **Parameters (particle handler):** .. code-block:: python handler = mw.handler(simulation_box, particles_per_cell, temperature, density_profile, thickness=-1, velocity=lightVelocity, axis='x', angle=0) - ``simulation_box`` (int): Pointer from ``sim.simulation_box()``. - ``particles_per_cell`` (float): Particle density for re-injection. - ``temperature`` (float): Temperature for re-injected particles (erg). - ``density_profile`` (int): Density profile callback address. - ``thickness`` (float): Boundary thickness (cm). Default: -1. - ``velocity`` (float): Window velocity (cm/s). Default: ``lightVelocity``. - ``axis`` (char): Movement axis. Default: 'x'. - ``angle`` (float): Window angle (radians). Default: 0. **Parameters (field handler):** .. code-block:: python field_handler = mw.field_handler(simulation_box, thickness=-1, velocity=lightVelocity, axis='x', angle=0) - ``simulation_box`` (int): Simulation box pointer. - ``thickness`` (float): Boundary thickness (cm). Default: -1. - ``velocity`` (float): Window velocity (cm/s). Default: ``lightVelocity``. - ``axis`` (char): Movement axis. Default: 'x'. - ``angle`` (float): Window angle (radians). Default: 0. **Usage:** .. code-block:: python import pipic.extensions.moving_window as mw from pipic import consts, types @cfunc(types.add_particles_callback) def density_profile(r, data_double, data_int): return 1e18 simbox_ptr = sim.simulation_box() handler_ptr = mw.handler(simbox_ptr, particles_per_cell=10, temperature=1e-6*consts.electron_mass*consts.light_velocity**2, density_profile=density_profile.address, velocity=consts.light_velocity, axis='x') field_handler_ptr = mw.field_handler(simbox_ptr, velocity=consts.light_velocity, axis='x') sim.add_handler(name='moving_window', subject='all_types', handler=handler_ptr, field_handler=field_handler_ptr) **Behavior:** - Shifts simulation domain along ``axis`` at ``velocity``. - Removes particles exiting the rear; optionally re-injects at front via ``density_profile``. Extension Development --------------------- π-PIC offers the possibility to develop custom extensions in Python, C/C++, Fortran, and other languages that can produce a callable function for Python. Extensions can modify, add, and remove particles based on local field state, and can also modify field state. This enables implementing physics like ionization, radiation reaction, QED processes, etc. Overview ^^^^^^^^ Extensions connect to π-PIC via two types of an interfaces: one for particles and one for fields. The particle interface is based on the ``cellInterface`` structure, which provides direct data access without performance loss. The field access is realized through the ``fieldLoop()`` method defined for every ``field_solver``. The interfaces are defined in: - Python: ``/pipic/interfaces/cellinterface.py`` - C/C++: ``src/interfaces.h`` Extensions must provide *Handler* and/or *fieldHandler* functions that process particle subsets and/or electromagnetic fields. The Handler is called during ``advance()`` for each subset of particles in each cell for each specified type, while the fieldHandler is called for each gridpoint. **Key capabilities:** - Add new particles within the same cell (buffered until next loop), enabling implementation of particle creation mechanisms - Process particles by type or process all cells via ``subject='cells'`` for applications like ionization - Pass custom data via ``data_double`` and ``data_int`` parameters for storing extension-specific state across calls **Example registration:** .. code-block:: python import my_extension from pipic import addressof # Initialize handlers handler_ptr = my_extension.handler(param1, param2) field_handler_ptr = my_extension.field_handler(param3) # Optional: allocate custom data arrays data_double = np.zeros((10,), dtype=np.double) data_int = np.zeros((5,), dtype=np.intc) # Register both particle and field handlers sim.add_handler(name=my_extension.name, subject='electron, positron', # Apply particle handler to these types handler=handler_ptr, # Processes particles field_handler=field_handler_ptr, # Processes fields (optional) data_double=addressof(data_double), # Custom data (optional) data_int=addressof(data_int)) # Custom data (optional) Python Extensions ^^^^^^^^^^^^^^^^^ Python extensions use `Numba C callbacks `__ for high performance while maintaining Python flexibility. **Extension Structure:** 1. **Set extension name** (must match filename): .. code-block:: python name = "my_extension" 2. **Allocate shared data** (if needed, must be thread-safe): .. code-block:: python from numba import int32, float64 from ctypes import c_double, c_int DataDouble = (c_double * 10)() # Array of 10 doubles DataInt = (c_int * 5)() # Array of 5 integers 3. **Implement Handler** using ``CellInterface``: .. code-block:: python from numba import cfunc from pipic.types import handler_callback from pipic.interfaces import CellInterface @cfunc(handler_callback) def Handler(CI_I, CI_D, CI_F, CI_P, CI_NP, data_double, data_int): C = CellInterface(CI_I, CI_D, CI_F, CI_P, CI_NP) # Process each particle in the subset for ip in range(C.particleSubsetSize): P = C.Particle(ip) # Access electromagnetic field at particle position E, B = C.interpolateField(P.r) # Modify particle momentum/position P.p.x += ... # Add new particle if needed if C.particleBufferSize < C.particleBufferCapacity: newP = C.newParticle(C.particleBufferSize) newP.r = P.r newP.p = ... newP.w = P.w newP.id = target_type_index C.particleBufferSize += 1 4. **Create initialization function** returning Handler address: .. code-block:: python def handler(param1, param2, ...): # Configure extension with parameters DataDouble[0] = param1 DataInt[0] = param2 return Handler.address 5. **Provide data accessors** (if using shared data): .. code-block:: python from ctypes import addressof def data_double(): return addressof(DataDouble) def data_int(): return addressof(DataInt) **Usage example:** .. code-block:: python import my_extension handler_ptr = my_extension.handler(param1=1.0, param2=42) sim.add_handler(name=my_extension.name, subject='electron', handler=handler_ptr, data_double=my_extension.data_double(), data_int=my_extension.data_int()) **Reference:** See `x_reflector_py.py `__ and its usage in `x_reflector_py_test.py `__. C/C++ Extensions ^^^^^^^^^^^^^^^^ C/C++ extensions provide maximum flexibility and performance. Unlike Python extensions using Numba, C/C++ extensions are compiled independently and can achieve full CPU performance. The following outlines both local development and the recommended contribution workflow. **Extension Structure:** 1. **Include required headers:** .. code-block:: cpp #include "interfaces.h" #include #include "pybind11/stl.h" #include 2. **Set extension name and global static parameters** (used for module export): .. code-block:: cpp const string name = "my_extension"; static double globalParameter; 3. **Implement Handler function** processing particles: .. code-block:: cpp // Particle handler: processes particles of specified types void Handler(int *I, double *D, double *F, double *P, double *NP, double *dataDouble, int *dataInt) { cellInterface CI(I, D, F, P, NP); // Generate new particles when CI.particleTypeIndex == -1 (cell-level handler) if (CI.particleTypeIndex == -1) { // Called once per cell, independent of particle types // Use this for particle generation/injection if(CI.particleBufferSize < CI.particleBufferCapacity) { particle *newP = CI.newParticle(CI.particleBufferSize); newP->r = ...; // Set position newP->p = ...; // Set momentum newP->w = ...; // Set weight newP->id = target_type_index; CI.particleBufferSize++; } } // Modify/remove particles when CI.particleTypeIndex == particleTypeIndex else if (CI.particleTypeIndex == particleTypeIndex) { // Called for each particle subset of the specified type // Use this for particle modification or removal for(int ip = 0; ip < CI.particleSubsetSize; ip++) { particle *P = CI.Particle(ip); // Interpolate EM field at particle position double3 E, B; CI.interpolateField(P->r, E, B); // Modify particle momentum. Note: do not modify positions since it can cause buffer overflow. P->p.x += ...; // Remove particle by setting weight to zero // P->w = 0.0; } } } 4. **Implement initialization function** returning Handler address: .. code-block:: cpp int64_t handler(double param1, int param2) { globalParameter = param1; return (int64_t)Handler; } 5. **Implement fieldHandler function** processing electromagnetic fields: .. code-block:: cpp // Field handler: processes electromagnetic fields void fieldHandler(int *ind, double *r, double *E, double *B, double *dataDouble, int *dataInt) { // Modify field at position r and grid index ind E[0] *= damping_factor; // ... } 6. **Implement initialization function** returning fieldHandler address: .. code-block:: cpp int64_t field_handler(double param1) { globalParameter = param1; return (int64_t)fieldHandler; } 7. **Export via pybind11:** .. code-block:: cpp namespace py = pybind11; PYBIND11_MODULE(_my_extension, object) { object.attr("name") = name; object.def("handler", &handler, py::arg("param1"), py::arg("param2")); object.def("field_handler", &field_handler, py::arg("param1")); } **Thread Management in Handlers** Handlers are executed in parallel using OpenMP, with multiple threads processing different cells simultaneously. Any data structures that are **modified during Handler execution** must be thread-safe to avoid race conditions and data corruption. **Example: threadHandler struct** Random number generation (RNG) requires maintaining state (the current seed value) that changes after each call, otherwise multiple OpenMP threads access the same RNG state simultaneously. The solution is to provide each OpenMP thread with its own independent RNG instance via thread-local storage and deterministic seeding based on ``CI.rngSeed``. .. code-block:: cpp struct threadHandler { mt19937 rng; std::uniform_real_distribution U1; std::normal_distribution N1; double working_array[100]; // Temporary computation buffers threadHandler(): U1(0, 1.0), N1(0, 1.0) {} double random() { return U1(rng); } }; static vector Thread; void Handler(...) { cellInterface CI(...); Thread.resize(omp_get_max_threads()); threadHandler &th = Thread[CI.threadNum]; // Get thread-local instance th.rng.seed(CI.rngSeed); // Deterministic per-cell seeding // Use th.random(), th.working_array[i], etc. } **Example from extension.cpp:** See `extension.cpp `__ for a minimal example using `threadHandler`. Build extension (C/C++) ^^^^^^^^^^^^^^^^^^^^^^^^^^ Two build strategies are available for extensions: **local build** which creates a local executable, or **integrated build** which makes the extension callable within the π-PIC framework. **Local build** 1. **Prepare build environment** (from a working directory): .. code-block:: bash mkdir my_extension && cd my_extension git clone https://github.com/hi-chi/pipic.git cd pipic # Optional: create and switch to development branch git checkout -b my_extension # Optional: reinstall pipic pip uninstall pipic python3 -m pip install . 2. **Set up extension folder:** .. code-block:: bash cd src/extensions mkdir my_extension && cd my_extension 3. **Copy necessary files:** .. code-block:: bash git clone https://github.com/pybind/pybind11 cp ../../primitives.h . cp ../../interfaces.h . cp ../../ensemble.h . # For particle management cp ../../services.h . # For logging cp ../../CMakeLists.txt . 4. **Configure CMakeLists.txt** (replace pipic references): Change from: .. code-block:: cmake set(pipic pipic.cpp) pybind11_add_module(_pipic${pipic}) To: .. code-block:: cmake set(my_extension my_extension.cpp) pybind11_add_module(_my_extension ${my_extension}) Optional: Remove FFTW if not needed: .. code-block:: bash # Remove these lines from CMakeLists.txt: # find_package(PkgConfig REQUIRED) # pkg_search_module(FFTW REQUIRED fftw3 IMPORTED_TARGET) # Remove #include from primitives.h 5. **Add your extension files**: ``my_extension.cpp``. See `extension.cpp `__ for a complete example. 6. **Compile locally:** .. code-block:: bash cmake . make 7. **Test in Python:** .. code-block:: python import my_extension from pipic import addressof # Initialize extension handler_ptr = my_extension.handler(param1=1.0, param2=42) field_handler_ptr = my_extension.field_handler(param1=0.1) # Register handlers sim.add_handler(name=my_extension.name, subject='electron', handler=handler_ptr, field_handler=field_handler_ptr) **Practical Example:** The tutorial `plasma_oscillation_extension_development.ipynb `__ demonstrates developing an absorbing boundary extension. **Integrated build** After successful development and testing: 1. **Update pybind11 module name** (add underscore prefix to avoid conflicts): .. code-block:: cpp PYBIND11_MODULE(_my_extension, object) { 2. **Organize files** in ``src/extensions/my_extension/``: .. code-block:: bash src/extensions/my_extension/ ├── my_extension.cpp └── my_extension.h (if needed) 3. **Create example** ``my_extension_test.py`` in ``examples/`` folder: .. code-block:: python from pipic.extensions import my_extension import pipic from pipic import consts sim = pipic.init(...) handler_ptr = my_extension.handler(...) sim.add_handler(name=my_extension.name, subject='electron', handler=handler_ptr) 4. **Register in** ``pipic/extensions/__init__.py`` 5. **Test clean build:** .. code-block:: bash pip uninstall pipic python3 -m pip install . python3 examples/my_extension_test.py 6. **Commit and push:** .. code-block:: bash git add src/extensions/my_extension/ git add examples/my_extension_test.py git add pipic/extensions/__init__.py git commit -m "Add my_extension: ..." git push origin my_extension # Create pull request on GitHub 7. **Document in EXTENSIONS.md** with physics description, parameters, and usage. Performance Tips ^^^^^^^^^^^^^^^^ - **Use C/C++ for performance-critical extensions**; numba callbacks are slower but more convenient for prototyping. - **Minimize allocations** inside hot loops (particle/cell processing). - **Use thread-local RNG** via ``CI.rngSeed`` for deterministic random number generation. - **Disable threading** during debugging: ``sim.advance(..., use_omp=False)``.