pyfracval.fractal ================= .. py:module:: pyfracval.fractal .. autoapi-nested-parse:: Fractal metrics and validation for PyFracVAL. Functions for computing mass, radius of gyration, gamma values, cluster properties, and validating fractal structure. Functions --------- calculate_mass Compute total particle mass (proportional to r^3). calculate_rg Compute theoretical radius of gyration from fractal scaling law. gamma_calculation Compute the centre-to-centre distance (gamma) for PCA/CCA sticking. calculate_cluster_properties Compute total mass, Rg, centre-of-mass, and max radius of a cluster. compute_empirical_rg Compute empirical Rg from actual particle coordinates. compute_pair_correlation_dimensions Estimate fractal dimension from pair-correlation (mass-radius) scaling. validate_fractal_structure Validate that generated aggregate matches target fractal parameters. Module Contents --------------- .. py:function:: calculate_mass(radii, densities = None) Calculate particle mass from radii and (optionally) per-particle density. ``m_i = (4/3) pi r_i^3 rho_i``. :param radii: Array of particle radii. :type radii: np.ndarray :param densities: Per-particle densities. ``None`` (the default) means uniform density, in which case mass is proportional to r^3 and every density-aware quantity reduces exactly to its single-material form. Supplying densities is what makes *heterogeneous* aggregates - different materials, not just different sizes - physically meaningful, since the center of mass, radius of gyration and the Gamma equation are all mass-weighted. :type densities: np.ndarray, optional :returns: Array of corresponding particle masses. :rtype: np.ndarray .. py:function:: resolve_densities(densities, n, context = 'densities') Validate an optional per-particle density array against a count. Returns the array unchanged (as float) when supplied, or ``None`` for the uniform-density case so downstream code can keep taking the cheaper ``None`` path rather than carrying an array of ones. .. py:function:: compute_empirical_rg_polydisperse(coords, radii, densities = None) Radius of gyration of actual coordinates, including each primary particle's own gyration radius (paper Eq. 4). Differs from :func:`compute_empirical_rg`, which treats particles as point masses and so omits the :math:`r_{g,i}^2` term below. That omission is a constant offset for monodisperse input but grows with polydispersity, and it makes the point-mass form **inconsistent with the Gamma equation**, whose derivation (paper Appendix A, Eq. A.5) carries the term throughout. Anything that has to agree with Gamma - the measured-Rg feedback in ``cca/pairing.py``, the per-aggregate quality record - must use this function; use :func:`compute_empirical_rg` only where a point-mass Rg is what is actually wanted. The counterpart to :func:`calculate_rg`, which returns the radius of gyration the fractal scaling law *prescribes* for a given particle count. This one measures what a built aggregate actually has: .. math:: R_g^2 = \frac{1}{m_a}\sum_i m_{p,i}[(R_i - R_c)^2 + r_{g,i}^2] with :math:`r_{g,i}^2 = \frac{3}{5}r_{p,i}^2` the primary particle's own gyration radius and :math:`R_c` the mass-weighted center of mass (Eq. 5). The :math:`r_{g,i}^2` term is what makes this valid for polydisperse primary particles :cite:p:`Moran2019FracVAL`; dropping it (as monodisperse treatments do) underestimates Rg for wide size distributions. :param coords: Nx3 particle coordinates. :type coords: np.ndarray :param radii: N particle radii. :type radii: np.ndarray :param densities: Per-particle densities; ``None`` means uniform. Both the center of mass and the mass weighting below use these, so a heterogeneous aggregate's Rg is only correct when they are supplied. :type densities: np.ndarray, optional :returns: Measured radius of gyration; 0.0 for an empty aggregate. :rtype: float .. py:function:: calculate_rg(radii, npp, df, kf) Calculate the radius of gyration using the fractal scaling law. Implements the formula Rg = a * (N / kf)^(1/Df), where 'a' is the geometric mean radius calculated from the input `radii` array. See :cite:p:`Moran2019FracVAL` and morphology context :cite:p:`Filippov2000Tunable`. :param radii: Array of radii of particles in the cluster/aggregate. :type radii: np.ndarray :param npp: Number of primary particles (N) in the cluster. :type npp: int :param df: Fractal dimension (Df). :type df: float :param kf: Fractal prefactor (kf). :type kf: float :returns: The calculated radius of gyration (Rg). Returns 0.0 if `npp` is 0, `kf` or `df` is zero, or if calculation fails (e.g., log error). :rtype: float .. py:function:: gamma_calculation(m1, rg1, radii1, m2, rg2, radii2, df, kf, use_mass = False, all_radii = None) Calculates Gamma_pc for adding the next monomer (aggregate 2). The Gamma_pc relation follows the FracVAL CCA/PCA formulation :cite:p:`Moran2019FracVAL`. :param use_mass: Which form of the Gamma equation to solve. ``False`` substitutes particle *counts* for the masses, giving Filippov et al. (2000) Eq. 7 - what the Fortran PCA does (``PCA_cca.f90``'s ``Gamma_calculation`` takes ``n1, n2, n3``) and what this port has historically done everywhere. ``True`` uses the true masses passed in as ``m1``/``m2``, giving Moran et al. (2019) Eq. 6 - the paper's central polydisperse contribution, and what the Fortran *CCA* actually does (``CCA_module.f90:301``). Identical for monodisperse primary particles; they diverge as polydispersity grows. See NOTE.md 1.2. :type use_mass: bool, default False :param all_radii: If provided, the geometric mean radius for rg3 is computed from this full set of radii (matching Fortran behaviour where R contains all N particles). When None the geometric mean is taken from the local combined set (radii1 + radii2). :type all_radii: np.ndarray, optional :param rg3_override: Use this radius of gyration for the *combined* aggregate instead of deriving it from the scaling law. Only meaningful together with measured (rather than scaling-law) rg1/rg2 - see ``cca/pairing.py``'s measured-Rg feedback. :type rg3_override: float, optional .. py:function:: calculate_cluster_properties(coords, radii, df, kf, densities = None) Calculate aggregate properties: total mass, Rg, center of mass, Rmax. :param coords: Nx3 array of particle coordinates. :type coords: np.ndarray :param radii: N array of particle radii. :type radii: np.ndarray :param df: Fractal dimension used for Rg calculation. :type df: float :param kf: Fractal prefactor used for Rg calculation. :type kf: float :param densities: Per-particle densities; ``None`` means uniform. Affects the total mass and the center of mass (and hence r_max), which in turn feed the Gamma equation. :type densities: np.ndarray, optional :returns: A tuple containing: - total_mass (float): Sum of individual particle masses. - rg (float | None): Radius of gyration calculated via `calculate_rg`, or None if calculation failed. - cm (np.ndarray | None): 3D center of mass coordinates, or None if calculation failed. - r_max (float): Maximum distance from the center of mass to any particle center in the aggregate. Returns (0.0, 0.0, np.zeros(3), 0.0) for empty inputs (N=0). :rtype: tuple[float, float | None, np.ndarray | None, float] .. py:function:: compute_empirical_rg(coords, radii) Compute empirical Rg directly from particle coordinates (mass-weighted). Unlike ``calculate_rg`` which uses the fractal scaling law Rg = a*(N/kf)^(1/Df), this function measures Rg from the actual spatial distribution of particles. Treats each particle as a point mass. See :func:`compute_empirical_rg_polydisperse` for the form that also carries each particle's own gyration radius (paper Eq. 4) - required wherever the result has to be consistent with the Gamma equation. :param coords: Nx3 array of particle center coordinates. :type coords: np.ndarray :param radii: N array of particle radii. Mass is proportional to r^3. :type radii: np.ndarray :returns: Empirical (mass-weighted) radius of gyration. :rtype: float .. py:function:: compute_pair_correlation_dimensions(coords, radii, n_bins = 50) Estimate fractal dimension from pair-correlation (mass-radius) scaling. Computes the cumulative mass M(r) as a function of radial distance from the centre of mass. For a fractal aggregate, M(r) ~ r^Df, so a log-log fit gives the empirical Df. The fit uses raw (un-normalised) cumulative mass because normalised mass fractions are in [0,1] whose logs are non-positive, breaking the log-linear regression. :param coords: Nx3 array of particle center coordinates. :type coords: np.ndarray :param radii: N array of particle radii. :type radii: np.ndarray :param n_bins: Number of radial bins (default 50). :type n_bins: int :returns: r_bins : np.ndarray — bin edge radii (n_bins+1,) r_centers : np.ndarray — bin centre radii (n_bins,) M_r : np.ndarray — cumulative normalised mass fraction within each radius (n_bins+1,) empirical_Df : float — slope of log(M) vs log(r) fit fit_r_squared : float — R^2 of the linear fit empirical_kf : float — estimated kf from the fit :rtype: dict with keys .. py:function:: validate_fractal_structure(coords, radii, target_df, target_kf, rg_rtol = 0.05) Validate that generated aggregate matches target fractal parameters. Compares theoretical Rg (from scaling law) vs empirical Rg (from coordinates), and estimates the actual fractal dimension from mass-radius scaling. :param coords: Nx3 array of particle center coordinates. :type coords: np.ndarray :param radii: N array of particle radii. :type radii: np.ndarray :param target_df: Target fractal dimension. :type target_df: float :param target_kf: Target fractal prefactor. :type target_kf: float :param rg_rtol: Relative tolerance for Rg agreement (default 5%). :type rg_rtol: float :returns: N : int — number of particles theoretical_rg : float — Rg from scaling law Rg = a*(N/kf)^(1/Df) empirical_rg : float — Rg measured from coordinates rg_error_pct : float — (empirical - theoretical)/theoretical * 100 rg_ok : bool — ``rg_error_pct`` < rg_rtol * 100 empirical_Df : float — Df estimated from mass-radius scaling target_Df : float — target fractal dimension df_error : float — empirical_Df - target_Df fit_r_squared : float — goodness of fit for Df estimation empirical_kf : float — estimated fractal prefactor target_kf : float — target fractal prefactor :rtype: dict with keys