From 3e2039cfaa9e8509ed16be960eb5fcefea5ed8ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:20:49 +0000 Subject: [PATCH 1/3] Initial plan From f320944b518edaaf7e21dce473664883878751be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:28:21 +0000 Subject: [PATCH 2/3] docs: normalize docstrings and add sphinx pages workflow Co-authored-by: beykyle <22779182+beykyle@users.noreply.github.com> --- .github/workflows/docs.yml | 50 ++++++++++++++++++ README.md | 8 ++- docs/api.rst | 47 +++++++++++++++++ docs/conf.py | 26 ++++++++++ docs/index.rst | 10 ++++ docs/requirements.txt | 1 + src/rxmc/__init__.py | 2 + src/rxmc/adaptive_metropolis.py | 6 ++- src/rxmc/constraint.py | 42 ++++++++------- ...correlated_discrepancy_likelihood_model.py | 5 +- src/rxmc/elastic_diffxs_model.py | 16 +++--- src/rxmc/elastic_diffxs_observation.py | 12 ++++- src/rxmc/evidence.py | 2 + src/rxmc/ias_pn_model.py | 16 +++--- src/rxmc/ias_pn_observation.py | 12 ++++- src/rxmc/likelihood_model.py | 44 +++++++++++----- src/rxmc/metropolis_hastings.py | 36 +++++++------ src/rxmc/observation.py | 10 +++- src/rxmc/observation_from_measurement.py | 4 ++ src/rxmc/param_sampling.py | 14 ++--- src/rxmc/params.py | 42 ++++++++++++--- src/rxmc/physical_model.py | 52 +++++++++++-------- src/rxmc/proposal.py | 40 ++++++++++---- src/rxmc/walker.py | 30 ++++++----- 24 files changed, 398 insertions(+), 129 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..3638871 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,50 @@ +name: docs + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r docs/requirements.txt + python -m pip install -e . + - name: Build HTML docs + run: sphinx-build -b html docs docs/_build/html + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 92e721b..e4bdce0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,13 @@ An example of this code in use is in the development of the [East Lansing Model] Check out the [`examples/` directory](https://github.com/beykyle/rxmc/blob/main/examples/). ## documentation -- TBD +- Build locally with: + ```bash + pip install -r docs/requirements.txt + sphinx-build -b html docs docs/_build/html + ``` +- API docs are generated automatically from docstrings via Sphinx autodoc. +- Documentation is published to GitHub Pages by `.github/workflows/docs.yml`. ## installation ### pypi diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..cffec2d --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,47 @@ +API reference +============= + +.. automodule:: rxmc + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.params + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.observation + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.constraint + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.evidence + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.likelihood_model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.physical_model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.param_sampling + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: rxmc.walker + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..7c85623 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,26 @@ +"""Sphinx configuration for rxmc documentation.""" + +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +project = "rxmc" +copyright = "2026, rxmc contributors" +author = "rxmc contributors" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", +] + +autosummary_generate = True +napoleon_numpy_docstring = True +napoleon_google_docstring = False + +templates_path = ["_templates"] +exclude_patterns = ["_build"] + +html_theme = "alabaster" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..387bfc5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,10 @@ +rxmc documentation +================== + +`rxmc` provides tools for Bayesian calibration of reaction models. + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + api diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..9628c79 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx>=8 diff --git a/src/rxmc/__init__.py b/src/rxmc/__init__.py index 80d2e0b..7fa3c41 100644 --- a/src/rxmc/__init__.py +++ b/src/rxmc/__init__.py @@ -1,3 +1,5 @@ +"""Public package exports for rxmc.""" + from . import physical_model from . import likelihood_model from . import evidence diff --git a/src/rxmc/adaptive_metropolis.py b/src/rxmc/adaptive_metropolis.py index cfb4317..566df7e 100644 --- a/src/rxmc/adaptive_metropolis.py +++ b/src/rxmc/adaptive_metropolis.py @@ -1,3 +1,5 @@ +"""Adaptive Metropolis sampling algorithms.""" + import numpy as np from typing import Callable, Tuple @@ -14,8 +16,8 @@ def adaptive_metropolis( ) -> Tuple[np.ndarray, np.ndarray, int]: """ Adaptive Metropolis algorithm with a sliding window covariance adaptation. - Parameters: - --------- + Parameters + ---------- x0 : np.ndarray Initial point in the parameter space. n_steps : int diff --git a/src/rxmc/constraint.py b/src/rxmc/constraint.py index 5f9fda7..7bcf408 100644 --- a/src/rxmc/constraint.py +++ b/src/rxmc/constraint.py @@ -1,3 +1,5 @@ +"""Constraint composition of observations, models, and likelihoods.""" + import numpy as np from .likelihood_model import LikelihoodModel @@ -28,7 +30,7 @@ def __init__( """ Initialize the Constraint with some Observations, a PhysicalModel, and - Parameters: + Parameters ---------- observations: list[Observation] The observed data that the model will attempt to reproduce. @@ -47,7 +49,7 @@ def model(self, model_params): """ Compute the model output for each observation, given model_params. - Parameters: + Parameters ---------- model_params : tuple The parameters of the physical model @@ -59,7 +61,7 @@ def log_likelihood(self, model_params, likelihood_params=()): Calculate the log probability density function that the model predictions, given the parameters, reproduce the observed data. - Parameters: + Parameters ---------- model_params : tuple The parameters of the physical model @@ -67,8 +69,8 @@ def log_likelihood(self, model_params, likelihood_params=()): Additional parameters for the likelihood model, if any. - Returns: - ------- + Returns + ---------- float The log probability density of the observation given the parameters. @@ -86,7 +88,7 @@ def marginal_log_likelihood(self, ym: list, *likelihood_params): likelihood_params provided, reproduces the observations in the constraints. - Parameters: + Parameters ---------- ym : list The model predictions for the observed data. @@ -94,8 +96,8 @@ def marginal_log_likelihood(self, ym: list, *likelihood_params): Additional parameters for the likelihood model, if any. - Returns: - ------- + Returns + ---------- float The log probability density of the observation given the parameters. @@ -110,15 +112,15 @@ def chi2(self, model_params, likelihood_params=()): Calculate the chi-squared statistic (or Mahalanobis distance) between the model prediction, given the parameters, and the observed data. - Parameters: + Parameters ---------- model_params : tuple The parameters of the physical model likelihood_params : tuple, optional Additional parameters for the likelihood model, if any. - Returns: - ------- + Returns + ---------- float The chi-squared statistic. """ @@ -134,13 +136,13 @@ def predict(self, *model_params): Generate predictions for each observation using the physical model with the provided parameters. - Parameters: + Parameters ---------- *model_params : tuple The parameters of the physical model. - Returns: - ------- + Returns + ---------- list[np.ndarray] The predicted values for each observation. """ @@ -153,7 +155,7 @@ def num_pts_within_interval( Count the number of points within the specified interval for each observation. - Parameters: + Parameters ---------- ylow : list[np.ndarray] Lower bounds of the intervals for each observation. @@ -162,8 +164,8 @@ def num_pts_within_interval( xlim : tuple, optional Limits for the x-axis, if applicable. - Returns: - ------- + Returns + ---------- int The total number of points within the specified intervals across all observations. @@ -181,7 +183,7 @@ def empirical_coverage( specified intervals for each observation, as a fraction of the total number of data points. - Parameters: + Parameters ---------- ylow : list[np.ndarray] Lower bounds of the intervals for each observation. @@ -190,8 +192,8 @@ def empirical_coverage( xlim : tuple, optional Limits for the x-axis, if applicable. - Returns: - ------- + Returns + ---------- float The empirical coverage across all observations. """ diff --git a/src/rxmc/correlated_discrepancy_likelihood_model.py b/src/rxmc/correlated_discrepancy_likelihood_model.py index bca7d75..a7883da 100644 --- a/src/rxmc/correlated_discrepancy_likelihood_model.py +++ b/src/rxmc/correlated_discrepancy_likelihood_model.py @@ -1,3 +1,5 @@ +"""Likelihood models with correlated discrepancy terms.""" + import numpy as np from sklearn.gaussian_process.kernels import Kernel @@ -56,6 +58,8 @@ def _kernel_matrix( return k(X) # (N,N) def covariance(self, observation: Observation, ym: np.ndarray, *kernel_theta): + """Return covariance including observational and discrepancy components.""" + if len(kernel_theta) != self.n_params: raise ValueError( f"Expected {self.n_params} kernel hyperparameters, got {len(kernel_theta)}" @@ -70,4 +74,3 @@ def covariance(self, observation: Observation, ym: np.ndarray, *kernel_theta): cov = cov + self.jitter * np.eye(observation.n_data_pts) return cov - diff --git a/src/rxmc/elastic_diffxs_model.py b/src/rxmc/elastic_diffxs_model.py index c87857d..5804c22 100644 --- a/src/rxmc/elastic_diffxs_model.py +++ b/src/rxmc/elastic_diffxs_model.py @@ -1,3 +1,5 @@ +"""Physical models for elastic differential cross sections.""" + from typing import Callable import jitr @@ -26,7 +28,7 @@ def __init__( """ Initialize the ElasticDifferentialXSModel with the interactions and a function to calculate the subparameters. - Parameters: + Parameters ---------- quantity : str The type of quantity to be calculated (e.g., "dXS/dA", @@ -75,15 +77,15 @@ def evaluate( """ Evaluate the model at the given parameters. - Parameters: + Parameters ---------- observation : ElasticDifferentialXSObservation The observation containing the reaction data and workspace. params : tuple The parameters of the physical model. - Returns: - ------- + Returns + ---------- np.ndarray An array, containing the evaluated differential data on the angular grid corresponding to the @@ -109,15 +111,15 @@ def visualizable_model_prediction( """ Visualize the model at the given parameters. - Parameters: + Parameters ---------- observation : ElasticDifferentialXSObservation The observation containing the reaction data and workspace. params : tuple The parameters of the physical model. - Returns: - ------- + Returns + ---------- np.ndarray An array, containing the evaluated differential data on the angular grid corresponding to the diff --git a/src/rxmc/elastic_diffxs_observation.py b/src/rxmc/elastic_diffxs_observation.py index a744f6f..eefd7ae 100644 --- a/src/rxmc/elastic_diffxs_observation.py +++ b/src/rxmc/elastic_diffxs_observation.py @@ -1,3 +1,5 @@ +"""Observation helpers for elastic differential cross sections.""" + from typing import Type import jitr @@ -50,7 +52,7 @@ def __init__( """ Initialize a ReactionObservation instance. - Parameters: + Parameters ---------- measurements : list[Distribution] List of measurements, each containing x, y, and associated errors. @@ -124,15 +126,23 @@ class `Observation`, but the user can supply any other subclass. self.n_data_pts = self._obs.n_data_pts def covariance(self, y): + """Delegate covariance computation to the wrapped observation.""" + return self._obs.covariance(y) def residual(self, ym): + """Delegate residual computation to the wrapped observation.""" + return self._obs.residual(ym) def num_pts_within_interval(self, interval): + """Delegate interval counting to the wrapped observation.""" + return self._obs.num_pts_within_interval(interval) def calculate_normalization(self, measurement): + """Compute normalization factors and output units for a measurement.""" + # Determine the xs_unit based on self.quantity xs_unit = ureg.barn / ureg.steradian rutherford_unit = ureg.millibarn / ureg.steradian diff --git a/src/rxmc/evidence.py b/src/rxmc/evidence.py index 929cec3..111eb0e 100644 --- a/src/rxmc/evidence.py +++ b/src/rxmc/evidence.py @@ -1,3 +1,5 @@ +"""Aggregate evidence across multiple independent constraints.""" + import numpy as np from .constraint import Constraint diff --git a/src/rxmc/ias_pn_model.py b/src/rxmc/ias_pn_model.py index 4329683..b63f070 100644 --- a/src/rxmc/ias_pn_model.py +++ b/src/rxmc/ias_pn_model.py @@ -1,3 +1,5 @@ +"""Physical models for (p,n) IAS differential cross sections.""" + from typing import Callable import jitr @@ -38,7 +40,7 @@ def __init__( model_name: str = None, ): """ - Parameters: + Parameters ---------- U_p_coulomb : Callable Function to calculate the proton Coulomb potential. @@ -79,7 +81,7 @@ def evaluate( """ Evaluate the model at the given parameters. - Parameters: + Parameters ---------- observation : IsobaricAnalogPNObservation The observation containing the workspace and data. @@ -87,8 +89,8 @@ def evaluate( The parameters to evaluate the model at, in the order defined by the `calculate_params` function. - Returns: - ------- + Returns + ---------- np.ndarray An array, containing the evaluated differential data on the angular grid corresponding to the @@ -127,7 +129,7 @@ def visualizable_model_prediction( """ Visualize the model at the given parameters. - Parameters: + Parameters ---------- observation : IsobaricAnalogPNObservation The observation containing the workspace and data. @@ -135,8 +137,8 @@ def visualizable_model_prediction( The parameters to evaluate the model at, in the order defined by the `calculate_params` function. - Returns: - ------- + Returns + ---------- np.ndarray An array, containing the evaluated differential data on the angular grid corresponding to the diff --git a/src/rxmc/ias_pn_observation.py b/src/rxmc/ias_pn_observation.py index ad88292..a596ba4 100644 --- a/src/rxmc/ias_pn_observation.py +++ b/src/rxmc/ias_pn_observation.py @@ -1,3 +1,5 @@ +"""Observation helpers for (p,n) IAS data.""" + from typing import Type import jitr @@ -46,7 +48,7 @@ def __init__( """ Initialize a Observation instance for the (p,n) IAS reaction. - Parameters: + Parameters ---------- measurement : Distribution The experimental measurement data. @@ -126,12 +128,18 @@ class `Observation`, but the user can supply any other subclass. self.n_data_pts = self._obs.n_data_pts def covariance(self, y): + """Delegate covariance computation to the wrapped observation.""" + return self._obs.covariance(y) def residual(self, ym): + """Delegate residual computation to the wrapped observation.""" + return self._obs.residual(ym) def num_pts_within_interval(self, interval): + """Delegate interval counting to the wrapped observation.""" + return self._obs.num_pts_within_interval(interval) @@ -209,6 +217,8 @@ def set_up_solver( def check_angle_grid(angles_rad: np.ndarray, name: str): + """Validate that ``angles_rad`` is 1D and bounded on ``[0, pi)``.""" + if len(angles_rad.shape) > 1: raise ValueError(f"{name} must be 1D, is {len(angles_rad.shape)}D") if angles_rad[0] < 0 or angles_rad[-1] > np.pi: diff --git a/src/rxmc/likelihood_model.py b/src/rxmc/likelihood_model.py index c3b1e48..8f1a431 100644 --- a/src/rxmc/likelihood_model.py +++ b/src/rxmc/likelihood_model.py @@ -1,3 +1,5 @@ +"""Likelihood model definitions used by rxmc constraints.""" + import numpy as np import scipy as sc @@ -105,7 +107,7 @@ def residual(self, observation: Observation, ym: np.ndarray): Returns the residual between the model prediction ym and observation.y - Parameters: + Parameters ---------- observation : Observation The observation object containing the observed data. @@ -564,7 +566,7 @@ def residual(self, observation: Observation, ym: np.ndarray, log_rho: float): Returns the residual between the renormalized model prediction ym and observation.y - Parameters: + Parameters ---------- observation : Observation The observation object containing the observed data. @@ -839,6 +841,8 @@ def log_likelihood(self, observation: Observation, ym: np.ndarray, nu: float): def scale_covariance( cov: np.ndarray, observation: Observation, scale: float, divide_by_N: bool ) -> np.ndarray: + """Return covariance scaled by ``scale`` and optionally by sample count.""" + if divide_by_N: scale /= observation.n_data_pts return scale * cov @@ -849,13 +853,19 @@ def mahalanobis_distance_sqr_cholesky(y, ym, cov): Calculate the square of the Mahalanobis distance between y and ym, and the log determinant of the covariance matrix. - Parameters: - y (array-like): The observation vector. - ym (array-like): The model prediction vector. - cov (array-like): The covariance matrix. + Parameters + ---------- + y : array-like + The observation vector. + ym : array-like + The model prediction vector. + cov : array-like + The covariance matrix. - Returns: - tuple: Mahalanobis distance and log determinant of the covariance matrix. + Returns + ---------- + tuple[float, float] + The squared Mahalanobis distance and log determinant of ``cov``. """ L = sc.linalg.cholesky(cov, lower=True) z = sc.linalg.solve_triangular(L, y - ym, lower=True) @@ -869,13 +879,19 @@ def log_likelihood(mahalanobis_sqr: float, log_det: float, n: int): r""" Calculate the log likelihood of a multivariate normal distribution. - Parameters: - mahalanobis_sqr (float): The Mahalanobis distance. - log_det (float): The log determinant of the covariance matrix. - n (int): The dimension of the data. + Parameters + ---------- + mahalanobis_sqr : float + The squared Mahalanobis distance. + log_det : float + The log determinant of the covariance matrix. + n : int + The dimension of the data. - Returns: - float: The log likelihood value. + Returns + ---------- + float + The log-likelihood value. """ return -0.5 * (mahalanobis_sqr + log_det + n * np.log(2 * np.pi)) diff --git a/src/rxmc/metropolis_hastings.py b/src/rxmc/metropolis_hastings.py index fe1fde7..7d80ad1 100644 --- a/src/rxmc/metropolis_hastings.py +++ b/src/rxmc/metropolis_hastings.py @@ -1,3 +1,5 @@ +"""Metropolis-Hastings Markov chain Monte Carlo algorithm.""" + from typing import Callable, Tuple import numpy as np @@ -13,24 +15,24 @@ def metropolis_hastings( """ Performs Metropolis-Hastings MCMC sampling. - Parameters: - x0 : np.ndarray - Initial parameter values for the chain. - n_steps : int - Number of steps/samples to generate. - log_posterior : Callable[[np.ndarray], float] - Function to compute the log posterior probability of - the parameters. - rng : np.random.Generator - Random number generator for reproducibility. - propose : Callable[[np.ndarray, np.random.Generator], np.ndarray] - Function to propose new parameter values. - Returns: - tuple: - - numpy.ndarray: The chain of samples generated. - - numpy.ndarray: Log posteriors corresponding to the samples. - - int: The number of accepted proposals. + Parameters + ---------- + x0 : np.ndarray + Initial parameter values for the chain. + n_steps : int + Number of steps/samples to generate. + log_posterior : Callable[[np.ndarray], float] + Function to compute the log posterior probability. + rng : np.random.Generator + Random number generator for reproducibility. + propose : Callable[[np.ndarray, np.random.Generator], np.ndarray] + Function to propose new parameter values. + Returns + ---------- + tuple[np.ndarray, np.ndarray, int] + The generated chain, corresponding log-posterior values, + and count of accepted proposals. """ chain = np.zeros((n_steps, x0.size)) logp_chain = np.zeros((n_steps,)) diff --git a/src/rxmc/observation.py b/src/rxmc/observation.py index 70c4827..2c873ec 100644 --- a/src/rxmc/observation.py +++ b/src/rxmc/observation.py @@ -1,3 +1,5 @@ +"""Observation data containers and covariance helpers.""" + from collections.abc import Iterable import numpy as np @@ -10,7 +12,7 @@ class Observation: and offset of all or some of the data points of the the dependent variable y. - Attributes: + Attributes ---------- x : np.ndarray The independent variable data. @@ -163,6 +165,8 @@ def covariance(self, y): ) def residual(self, ym: np.ndarray): + """Return residuals ``y - ym`` for the observation.""" + assert ym.shape == self.y.shape return self.y - ym @@ -272,8 +276,12 @@ def covariance(self, y): def is_array_like(obj): + """Return ``True`` when ``obj`` is iterable but not a string/bytes.""" + return isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)) def is_scalar_like(obj): + """Return ``True`` when ``obj`` behaves like a scalar numeric value.""" + return np.isscalar(obj) or isinstance(obj, (int, float, complex)) diff --git a/src/rxmc/observation_from_measurement.py b/src/rxmc/observation_from_measurement.py index f77ab93..4fd2907 100644 --- a/src/rxmc/observation_from_measurement.py +++ b/src/rxmc/observation_from_measurement.py @@ -1,3 +1,5 @@ +"""Utilities to convert measurement objects into observations.""" + from typing import Type import jitr @@ -124,6 +126,8 @@ def set_up_observation( def check_angle_grid(angles_rad: np.ndarray, name: str): + """Validate that ``angles_rad`` is 1D and bounded on ``[0, pi)``.""" + if len(angles_rad.shape) > 1: raise ValueError(f"{name} must be 1D, is {len(angles_rad.shape)}D") if angles_rad[0] < 0 or angles_rad[-1] > np.pi: diff --git a/src/rxmc/param_sampling.py b/src/rxmc/param_sampling.py index c63f942..e65704c 100644 --- a/src/rxmc/param_sampling.py +++ b/src/rxmc/param_sampling.py @@ -1,3 +1,5 @@ +"""Sampling wrappers and sampler implementations.""" + from typing import Callable import numpy as np @@ -24,7 +26,7 @@ def __init__( ): """ Initializes the Sampler with the provided parameters. - Parameters: + Parameters ---------- params: list[params.Parameter] List of parameters to sample. @@ -64,7 +66,7 @@ def record_batch( """ Records the batch of samples and acceptance statistics. - Parameters: + Parameters ---------- n_steps: int Number of steps in the batch. @@ -94,7 +96,7 @@ def sample( sampling algorithm, updating the state and recording the chain, log posterior values, and acceptance statistics. - Parameters: + Parameters ---------- n_steps: int Number of steps to sample. @@ -160,7 +162,7 @@ def __init__( proposal: proposal.ProposalDistribution, ): """ - Parameters: + Parameters ---------- params: list[params.Parameter] List of parameters to sample. @@ -208,7 +210,7 @@ def __init__( epsilon_fraction: float = 1e-6, ): """ - Parameters: + Parameters ---------- params: list[params.Parameter] List of parameters to sample. @@ -278,7 +280,7 @@ def __init__( epsilon_fraction: float = 1e-6, ): """ - Parameters: + Parameters ---------- params: list[params.Parameter] List of parameters to sample. diff --git a/src/rxmc/params.py b/src/rxmc/params.py index 579b24c..a93d7bb 100644 --- a/src/rxmc/params.py +++ b/src/rxmc/params.py @@ -1,3 +1,5 @@ +"""Parameter metadata and sample serialization helpers.""" + from collections import OrderedDict from json import load, dumps from pathlib import Path @@ -7,16 +9,26 @@ class Parameter: + """Metadata for one sampled parameter.""" + def __init__( self, name, dtype=float, unit="", latex_name=None, bounds=(-np.inf, np.inf) ): """ - Parameters: - name (str): Name of the parameter - dtype (np.dtype): Data type of the parameter - unit (str): Unit of the parameter - latex_name (str): LaTeX representation of the parameter - bounds (tuple, optional): Bounds for the parameter as a tuple (min, max) + Initialize parameter metadata. + + Parameters + ---------- + name : str + Name of the parameter. + dtype : numpy.dtype, optional + Data type of the parameter. + unit : str, optional + Unit label for the parameter. + latex_name : str, optional + LaTeX representation of the parameter. + bounds : tuple, optional + Bounds for the parameter as ``(min, max)``. """ self.name = name self.dtype = dtype @@ -37,11 +49,15 @@ def __eq__(self, other): def dump_sample_to_json(fpath: Path, sample: OrderedDict): + """Write one sample to a JSON file.""" + with open(fpath, "w") as file: file.write(dumps(dict(sample), indent=4)) def read_sample_from_json(fpath: Path): + """Read one sample from a JSON file.""" + try: with open(fpath, "r") as file: return load(file, object_pairs_hook=OrderedDict) @@ -50,28 +66,42 @@ def read_sample_from_json(fpath: Path): def array_to_list(samples: np.ndarray, params: list): + """Convert an array of samples to ordered-dict samples.""" + return [to_ordered_dict(sample, params) for sample in samples] def list_to_array(samples: list, params_dtype: tuple): + """Convert list-of-mapping samples to a structured NumPy array.""" + return np.array([(sample.values()) for sample in samples], dtype=params_dtype) def to_ordered_dict(sample: np.ndarray, params: list): + """Convert one sample array to an ordered mapping keyed by params.""" + return OrderedDict(zip(params, sample)) def list_to_dataframe(samples: list): + """Convert list-of-mapping samples to a pandas DataFrame.""" + return pd.DataFrame(samples) def dataframe_to_list(samples: pd.DataFrame): + """Convert a pandas DataFrame to ordered-dict records.""" + return samples.to_dict(orient="records", into=OrderedDict) def dump_samples_to_numpy(fpath: Path, samples: list): + """Write samples to disk in NumPy ``.npy`` format.""" + list_to_array(samples).save(fpath) def read_samples_from_numpy(fpath: Path): + """Read samples from a NumPy ``.npy`` file.""" + return array_to_list(np.load(fpath)) diff --git a/src/rxmc/physical_model.py b/src/rxmc/physical_model.py index b75f5b1..b5ba28a 100644 --- a/src/rxmc/physical_model.py +++ b/src/rxmc/physical_model.py @@ -1,3 +1,5 @@ +"""Physical model interfaces and built-in toy models.""" + import numpy as np from .observation import Observation @@ -6,17 +8,19 @@ class PhysicalModel: """ - Represents an arbitrary parameteric model $y_{model}(x;params)$, for - comparison to some experimental measurement $\{x_i, y(x_i)\}$ contained - in an Observation object + Represent a parametric model ``y_model(x; params)``. + + The model is compared against measured data held by an + :class:`~rxmc.observation.Observation`. """ def __init__(self, params: list[Parameter]): """ Initialize the PhysicalModel with a list of parameters. - Parameters: + + Parameters ---------- - params: list[Parameter] + params : list[Parameter] A list of Parameter objects that define the model's parameters. Each Parameter should have a name and a dtype. """ @@ -28,10 +32,12 @@ def evaluate(self, observation: Observation, *params) -> np.ndarray: Evaluate the model at the given parameter values. Should be overridden by subclasses. - Parameters: + Parameters ---------- - observation: Observation object containing x and y data. - params: Parameters for the model, should match the model's parameters. + observation : Observation + Observation object containing x and y data. + *params : tuple + Parameters for the model, in the same order as ``self.params``. """ raise NotImplementedError("Subclasses must implement the evaluate method.") @@ -42,11 +48,9 @@ def __call__(self, observation: Observation, *params) -> np.ndarray: class Polynomial(PhysicalModel): """ - Polynomial model for fitting, of the form: - \[ - y_{model}(x; params) = \sum_{i=0}^{n} a_i x^i - \] - where $params = [a_0, a_1, ..., a_n]$. + Polynomial model for fitting. + + The model form is ``y_model(x; params) = sum(a_i * x**i)``. """ def __init__(self, order: int): @@ -60,18 +64,22 @@ def evaluate(self, observation: Observation, *params) -> np.ndarray: """ Evaluate the polynomial model at the given parameter values. - Parameters: + Parameters ---------- - observation: Observation - Observation object containing x and y data. - params: tuple - coefficients for the polynomial + observation : Observation + Observation object containing x and y data. + *params : tuple + Coefficients for the polynomial. - Returns: - numpy.ndarray: Evaluated polynomial values at observation.x. + Returns + ---------- + numpy.ndarray + Evaluated polynomial values at ``observation.x``. - Raises: - ValueError: If the number of parameters does not match the model order. + Raises + ---------- + ValueError + If the number of parameters does not match the model order. """ if len(params) != self.order + 1: raise ValueError( diff --git a/src/rxmc/proposal.py b/src/rxmc/proposal.py index 7dea0ba..954c6fe 100644 --- a/src/rxmc/proposal.py +++ b/src/rxmc/proposal.py @@ -1,3 +1,5 @@ +"""Proposal distributions for Markov chain samplers.""" + import numpy as np from scipy import stats @@ -15,11 +17,18 @@ def __init__(self): def __call__(self, x, rng): """ Generate a proposed sample based on the current sample `x`. - Parameters: - x (numpy.ndarray): Current sample from the Markov chain. - rng (numpy.random.Generator): Random number generator instance. - Returns: - numpy.ndarray: Proposed sample. + + Parameters + ---------- + x : numpy.ndarray + Current sample from the Markov chain. + rng : numpy.random.Generator + Random number generator instance. + + Returns + ---------- + numpy.ndarray + Proposed sample. """ raise NotImplementedError("This method should be overridden by subclasses") @@ -33,8 +42,11 @@ class NormalProposalDistribution(ProposalDistribution): def __init__(self, cov): """ Initialize the proposal distribution with a covariance matrix. - Parameters: - cov (numpy.ndarray): Covariance matrix for the multivariate normal distribution. + + Parameters + ---------- + cov : numpy.ndarray + Covariance matrix for the multivariate normal distribution. """ self.cov = cov @@ -51,8 +63,11 @@ class HalfNormalProposalDistribution(ProposalDistribution): def __init__(self, scale): """ Initialize the proposal distribution with a scale parameter. - Parameters: - scale (float): Scale parameter for the half-normal distribution. + + Parameters + ---------- + scale : float + Scale parameter for the half-normal distribution. """ self.scale = scale @@ -69,8 +84,11 @@ class LogspaceNormalProposalDistribution(ProposalDistribution): def __init__(self, scale): """ Initialize the proposal distribution with a scale parameter. - Parameters: - scale (float): Scale parameter for the log-normal distribution. + + Parameters + ---------- + scale : float + Scale parameter for the log-normal distribution. """ self.scale = scale diff --git a/src/rxmc/walker.py b/src/rxmc/walker.py index ec44481..33c1684 100644 --- a/src/rxmc/walker.py +++ b/src/rxmc/walker.py @@ -1,3 +1,5 @@ +"""Gibbs-style walk orchestration across model and likelihood samplers.""" + import numpy as np from .evidence import Evidence @@ -23,7 +25,7 @@ def __init__( """ Initialize the Sampler with a list of samplers. - Parameters: + Parameters ---------- model_sampler: Sampler A Sampler object for physical model parameters. @@ -71,7 +73,7 @@ def run_model_batch(self, n_steps, x0, likelihood_params=[], burn=False): Walks the model parameter space for fixed values of the `likelihood_params` - Parameters: + Parameters ---------- n_steps: int The number of steps to run for the model sampling. @@ -86,8 +88,8 @@ def run_model_batch(self, n_steps, x0, likelihood_params=[], burn=False): the acceptance rate, log probabilities, and parameter chain will not be recorded. - Returns: - ------- + Returns + ---------- batch_chain: np.ndarray The parameter chain generated by the model sampling algorithm. logp: np.ndarray @@ -110,7 +112,7 @@ def run_likelihood_batches( Walks each of the likelihood parameter spacers one by one, for a fixed value of `model_params` - Parameters: + Parameters ---------- n_steps: int The number of steps to run for each likelihood model. @@ -123,8 +125,8 @@ def run_likelihood_batches( the acceptance rate, log probabilities, and parameter chain will not be recorded. - Returns: - ------- + Returns + ---------- list[np.ndarray] A list of parameter chains for each likelihood model. logp : list[np.ndarray] @@ -157,9 +159,13 @@ def log_posterior_lm(x): sampler.sample(n_steps, x0, self.rng, log_posterior_lm, burn=burn) def log_likelihood(self, model_params, likelihood_params): + """Return log-likelihood for the provided model and likelihood parameters.""" + return self.evidence.log_likelihood(model_params, likelihood_params) def log_posterior(self, model_params, likelihood_params): + """Return log-posterior for the provided model and likelihood parameters.""" + return self.log_likelihood(model_params, likelihood_params) + self.log_prior( model_params, likelihood_params ) @@ -169,7 +175,7 @@ def log_prior(self, model_params, likelihood_params): Returns the log-prior probability of the model parameters and likelihood parameters. - Parameters: + Parameters ---------- model_params: tuple The parameters of the physical model. @@ -177,8 +183,8 @@ def log_prior(self, model_params, likelihood_params): A list of tuples containing additional parameters for the likelihood model for each constraint. - Returns: - ------- + Returns + ---------- float The log-prior probability. """ @@ -200,8 +206,8 @@ def walk( `likelihood_samplers` with records of the walk and relevant statistics. - Parameters: - ----------- + Parameters + ---------- n_steps : int Total number of active steps for the MCMC chain. batch_size : int From f52936a2aaf5ba09d419673aee8b8a3057ec19e8 Mon Sep 17 00:00:00 2001 From: Kyle Beyer Date: Mon, 2 Mar 2026 20:00:26 -0500 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e4bdce0..df4df19 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Check out the [`examples/` directory](https://github.com/beykyle/rxmc/blob/main/ ## documentation - Build locally with: ```bash + pip install -e . pip install -r docs/requirements.txt sphinx-build -b html docs docs/_build/html ```