Implementation Details
This document describes the internal architecture and implementation details of ZenoWrapper, including the optimization strategies used to minimize data transfer between Python and C++ layers.
Overview
This implementation minimizes data transfer between Python and C++ layers by performing all ZENO computations (geometry building, Walk on Spheres, Interior Sampling) entirely in the C++ layer, with only a single function call per frame from Python.
Architecture
Python Layer (main.py)
ZenoWrapper Class Structure:
__init__: Initializes ZENO parameter objects once
Creates
ParametersWalkOnSpheres,ParametersInteriorSampling, andParametersResultsobjectsConfigures all parameters (number of walks, error tolerances, physical properties, etc.)
Validates atom types and creates radii array
_prepare: Initializes output data structures
Creates
Propertyobjects for all ZENO results (capacitance, polarizability, volume, etc.)Allocates numpy arrays for storing results across all frames
Handles optional results (friction coefficient, diffusion coefficient, etc.) based on provided parameters
_single_frame: Analyzes a single frame
Extracts atomic positions from MDAnalysis
Single C++ call:
zenolib.compute_zeno_single_frame(positions, radii, params_walk, params_interior, params_results)Unpacks flat C++ arrays into numpy tensors
Stores mean and variance values for this frame
_conclude: Summarizes results
Computes overall mean and total variance across all frames
Produces final results for the user
C++ Layer (zenolib.cpp)
Key Components:
ZenoResults Struct: Efficient result container
Contains all ZENO outputs as simple doubles and double arrays
Mean and variance for each property
Flat arrays for tensors (row-major order) to minimize marshalling overhead
compute_zeno_single_frame Function: Main computation entry point
ZenoResults compute_zeno_single_frame( positions, // Nx3 numpy array radii, // N numpy array params_walk, params_interior, params_results )
Internal workflow:
Builds
MixedModel<double>from positions and radiiCreates spheres using ZENO’s
Vector3andSphereclassesInstantiates
Zenoobject with the modelCalls
doWalkOnSpheres()anddoInteriorSampling()Extracts results via
getResults()Packs all means and variances into
ZenoResultsstructReturns struct to Python (nanobind handles conversion)
Nanobind Bindings: Expose C++ classes to Python
ParametersWalkOnSpheres: Walk-on-Spheres configurationParametersInteriorSampling: Interior sampling configurationParametersResults: Physical parameters and unitsZenoResults: Read-only result struct with all properties exposedcompute_zeno_single_frame: Main computation function
Benefits
Performance: Minimized Python/C++ crossing overhead
Simplicity: Clean API with single function call
Memory Efficiency: Direct numpy array access via nanobind
Maintainability: Clear separation of concerns
Type Safety: Strongly typed C++ with automatic Python bindings
ZENO Workflow
The implementation follows ZENO’s standard workflow:
Geometry Construction: Build
MixedModelfrom spheresPreprocessing: ZENO automatically preprocesses the model
Walk on Spheres: Monte Carlo method for exterior properties (capacitance, polarizability)
Interior Sampling: Monte Carlo method for interior properties (volume, gyration)
Results Compilation: ZENO computes derived properties from raw Monte Carlo results
All steps 1-5 occur in a single C++ function call, with geometry built from numpy arrays.
Usage Example
from zenowrapper import ZenoWrapper
import MDAnalysis as mda
# Load trajectory
u = mda.Universe("topology.pdb", "trajectory.xtc")
# Define radii for each atom type
type_radii = {'CA': 2.0, 'CB': 1.5}
# Create analyzer
zw = ZenoWrapper(
u.atoms,
type_radii=type_radii,
n_walks=100000,
n_interior_samples=100000,
temperature=298.15,
viscosity=0.01
)
# Run analysis (automatically calls _prepare, _single_frame for each frame, _conclude)
zw.run()
# Access results
print(f"Hydrodynamic radius: {zw.results["hydrodynamic_radius"]overall_value} ± "
f"{np.sqrt(zw.results["hydrodynamic_radius"]overall_variance)}")
Implementation Notes
All ZENO computation parameters are configured once during
__init__The same parameter objects are reused for all frames (thread-safe)
Results are accumulated frame-by-frame in Python numpy arrays
Final statistics computed in
_concludeusing standard uncertainty propagation