From 99d1a6fbfbf7efe4b719d59abf4eb0489ce95d13 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Fri, 16 Oct 2026 10:47:04 -0400 Subject: [PATCH 1/4] ci: install diffpy.srfit 3.4.0rc0 in CI The SAS port needs srfit 3.4.0, but conda-forge only has 3.3.1. Install the rc from PyPI in the workflows' run step until 3.4.0 is on conda-forge. Co-Authored-By: Claude Opus 5.5 --- .../matrix-and-codecov-on-merge-to-main.yml | 3 +++ .github/workflows/tests-on-pr.yml | 3 +++ news/install-srfit-rc.rst | 23 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 news/install-srfit-rc.rst diff --git a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml index 894dd04..6b41ef9 100644 --- a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml +++ b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml @@ -17,5 +17,8 @@ jobs: project: diffpy.cmisas c_extension: false headless: false + run: | + set -Eeuo pipefail + python -m pip install --no-deps diffpy.srfit==3.4.0rc0 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests-on-pr.yml b/.github/workflows/tests-on-pr.yml index f9877ea..c1e0641 100644 --- a/.github/workflows/tests-on-pr.yml +++ b/.github/workflows/tests-on-pr.yml @@ -11,5 +11,8 @@ jobs: project: diffpy.cmisas c_extension: false headless: false + run: | + set -Eeuo pipefail + python -m pip install --no-deps diffpy.srfit==3.4.0rc0 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/news/install-srfit-rc.rst b/news/install-srfit-rc.rst new file mode 100644 index 0000000..26aef03 --- /dev/null +++ b/news/install-srfit-rc.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: Install the diffpy.srfit 3.4.0 rc in CI until 3.4.0 is on conda-forge. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 48745b89b0d82f5d9ba1804487dddee266e9af1e Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Fri, 16 Oct 2026 10:53:53 -0400 Subject: [PATCH 2/4] chore: copy sas subpackage and tests from diffpy.srfit 3.4.0 Verbatim copy of srfit 3.4.0-rc.0 (507aa4e5) sas/ into diffpy.cmisas, with imports pointed at diffpy.cmisas. SASCF moves from srfit's pdf/characteristicfunctions.py into diffpy.cmisas.characteristicfunctions, which holds only SASCF. - Tests: copy test_sas.py, the 4 SASCF tests (keeping their known-broken skip) and their testdata, and add the datafile fixture. The has_sas skips are removed; test_generator and testGenerator2 are strict xfails because sasmodels' load_standard_models imports SasView, which is not installable on Python 3.12+. - Requirements: numpy, scipy, diffpy.srfit, sasdata; sasmodels for tests. - Remove the template functions.py and its test; add LICENSE_DANSE.rst. Co-Authored-By: Claude Opus 5.5 --- LICENSE_DANSE.rst | 34 ++++ news/{install-srfit-rc.rst => port-sas.rst} | 2 +- requirements/conda.txt | 3 + requirements/pip.txt | 3 + requirements/tests.txt | 1 + src/diffpy/cmisas/__init__.py | 16 +- src/diffpy/cmisas/characteristicfunctions.py | 144 +++++++++++++++ src/diffpy/cmisas/functions.py | 31 ---- src/diffpy/cmisas/prcalculator.py | 157 ++++++++++++++++ src/diffpy/cmisas/sasgenerator.py | 76 ++++++++ src/diffpy/cmisas/sasimport.py | 71 ++++++++ src/diffpy/cmisas/sasparameter.py | 84 +++++++++ src/diffpy/cmisas/sasparser.py | 147 +++++++++++++++ src/diffpy/cmisas/sasprofile.py | 123 +++++++++++++ tests/__init__.py | 0 tests/conftest.py | 12 ++ tests/test_characteristicfunctions.py | 166 +++++++++++++++++ tests/test_functions.py | 40 ----- tests/test_sas.py | 179 +++++++++++++++++++ tests/testdata/sas_ascii_test_1.txt | 24 +++ tests/testdata/sas_ellipsoid_testdata.txt | 136 ++++++++++++++ 21 files changed, 1376 insertions(+), 73 deletions(-) create mode 100644 LICENSE_DANSE.rst rename news/{install-srfit-rc.rst => port-sas.rst} (58%) create mode 100644 src/diffpy/cmisas/characteristicfunctions.py delete mode 100644 src/diffpy/cmisas/functions.py create mode 100644 src/diffpy/cmisas/prcalculator.py create mode 100644 src/diffpy/cmisas/sasgenerator.py create mode 100644 src/diffpy/cmisas/sasimport.py create mode 100644 src/diffpy/cmisas/sasparameter.py create mode 100644 src/diffpy/cmisas/sasparser.py create mode 100644 src/diffpy/cmisas/sasprofile.py create mode 100644 tests/__init__.py create mode 100644 tests/test_characteristicfunctions.py delete mode 100644 tests/test_functions.py create mode 100644 tests/test_sas.py create mode 100644 tests/testdata/sas_ascii_test_1.txt create mode 100644 tests/testdata/sas_ellipsoid_testdata.txt diff --git a/LICENSE_DANSE.rst b/LICENSE_DANSE.rst new file mode 100644 index 0000000..c92ca2d --- /dev/null +++ b/LICENSE_DANSE.rst @@ -0,0 +1,34 @@ +This program is part of the DiffPy and DANSE open-source projects at Columbia +University and is available subject to the conditions and terms laid out below. + +Copyright (c) 2008-2011, The Trustees of Columbia University in +the City of New York. All rights reserved. + +For more information please visit the diffpy web-page at + http://www.diffpy.org +or email Prof. Simon Billinge at sb2896@columbia.edu. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the names of COLUMBIA UNIVERSITY, MICHIGAN STATE UNIVERSITY nor the + names of their contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/news/install-srfit-rc.rst b/news/port-sas.rst similarity index 58% rename from news/install-srfit-rc.rst rename to news/port-sas.rst index 26aef03..89a601b 100644 --- a/news/install-srfit-rc.rst +++ b/news/port-sas.rst @@ -1,6 +1,6 @@ **Added:** -* No news added: Install the diffpy.srfit 3.4.0 rc in CI until 3.4.0 is on conda-forge. +* No news added: Port the SAS code from diffpy.srfit 3.4.0 and install the srfit rc in CI; cmisas is unreleased. **Changed:** diff --git a/requirements/conda.txt b/requirements/conda.txt index 24ce15a..2eaf7d4 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1 +1,4 @@ numpy +scipy +diffpy.srfit +sasdata diff --git a/requirements/pip.txt b/requirements/pip.txt index 24ce15a..2eaf7d4 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1 +1,4 @@ numpy +scipy +diffpy.srfit +sasdata diff --git a/requirements/tests.txt b/requirements/tests.txt index a727786..04205aa 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -4,3 +4,4 @@ codecov coverage pytest-cov pytest-env +sasmodels diff --git a/src/diffpy/cmisas/__init__.py b/src/diffpy/cmisas/__init__.py index a29c490..a13640c 100644 --- a/src/diffpy/cmisas/__init__.py +++ b/src/diffpy/cmisas/__init__.py @@ -4,7 +4,8 @@ # (c) 2025 Simon Billinge. # All rights reserved. # -# File coded by: Caden Myers, Simon Billinge, and members of the Billinge group. +# File coded by: Caden Myers, Simon Billinge, and members of the Billinge +# group. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.cmisas/graphs/contributors @@ -14,6 +15,19 @@ ############################################################################## """The code of the SAS pack of the diffpy.cmi package.""" +__all__ = [ + "SASGenerator", + "SASParser", + "SASProfile", + "PrCalculator", + "CFCalculator", +] + +from diffpy.cmisas.prcalculator import CFCalculator, PrCalculator +from diffpy.cmisas.sasgenerator import SASGenerator +from diffpy.cmisas.sasparser import SASParser +from diffpy.cmisas.sasprofile import SASProfile + # package version from diffpy.cmisas.version import __version__ # noqa diff --git a/src/diffpy/cmisas/characteristicfunctions.py b/src/diffpy/cmisas/characteristicfunctions.py new file mode 100644 index 0000000..50e0a6f --- /dev/null +++ b/src/diffpy/cmisas/characteristicfunctions.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2010 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Characteristic function calculator backed by sasmodels.""" + +__all__ = ["SASCF"] + +import numpy +from numpy import ceil, log2, pi +from numpy.fft import fftfreq, ifft + +from diffpy.srfit.fitbase.calculator import Calculator + + +class SASCF(Calculator): + """Calculator class for characteristic functions from sas-models. + + This class wraps a sas.models.BaseModel to calculate I(Q) related to + nanoparticle shape. This I(Q) is inverted to f(r) according to: + f(r) = 1 / (4 pi r) * SINFT(I(Q)), + where "SINFT" represents the sine Fourier transform. + + Attributes + ---------- + _model + BaseModel object this adapts. + Managed Parameters + These depend on the parameters of the BaseModel object held by _model. + They are created from the 'params' attribute of the BaseModel. If a + dispersion is set for the BaseModel, the dispersion "width" will be + accessible under "_width", where is the name a + parameter adjusted by dispersion. + """ + + def __init__(self, name, model): + """Initialize the generator. + + Parameters + ---------- + name : str + The name for the SASCF. + model : BaseModel + The sas.models.BaseModel object this adapts. + """ + Calculator.__init__(self, name) + + self._model = model + + from diffpy.cmisas.sasparameter import SASParameter + + # Wrap normal parameters + for parname in model.params: + par = SASParameter(parname, model) + self.add_parameter(par) + + # Wrap dispersion parameters + for parname in model.dispersion: + name = parname + "_width" + parname += ".width" + par = SASParameter(name, model, parname) + self.add_parameter(par) + + return + + def __call__(self, r): + """Calculate the characteristic function from the transform of + the BaseModel.""" + # Determine q-values. + # We want very fine r-spacing so we can properly normalize f(r). This + # equates to having a large qmax so that the Fourier transform is + # finely spaced. We also want the calculation to be fast, so we pick + # qmax such that the number of q-points is a power of 2. This allows us + # to use the fft. + # + # The initial dr is somewhat arbitrary, but using dr = 0.01 allows for + # the f(r) calculated from a particle of diameter 50, over r = + # arange(1, 60, 0.1) to agree with the spherical_particle with Rw < + # 1e-4%. + # + # We also have to make a q-spacing small enough to compute out to at + # least the size of the signal. + raise NotImplementedError( + "As of release 3.2.0, SAS characteristic functions are not working" + + " but we hope to have them working again in a future release." + ) + + dr = min(0.01, r[1] - r[0]) + ed = 2 * self._model.calculate_ER() + + # Check for nans. If we find any, then return zeros. + if numpy.isnan(ed).any(): + y = numpy.zeros_like(r) + return y + + rmax = max(ed, 2 * r[-1]) + dq = pi / rmax + qmax = pi / dr + numpoints = int(2 ** (ceil(log2(qmax / dq)))) + qmax = dq * numpoints + + # Calculate F(q) = q * I(q) from model + q = fftfreq(int(qmax / dq)) * qmax + fq = q * self._model.evalDistribution(q) + + # Calculate g(r) and the effective r-points + rp = fftfreq(numpoints) * 2 * pi / dq + # Note sine transform = imaginary part of ifft + gr = ifft(fq).imag + + # Calculate full-fr for normalization + assert rp[0] == 0.0 + frp = numpy.zeros_like(gr) + frp[1:] = gr[1:] / rp[1:] + + # Inerpolate onto requested grid, do not use data after jump in rp + assert numpoints % 2 == 0 + nhalf = numpoints / 2 + fr = numpy.interp(r, rp[:nhalf], gr[:nhalf]) + vmask = r != 0 + fr[vmask] /= r[vmask] + + # Normalize. We approximate fr[0] by using the fact that f(r) is linear + # at low r. By definition, fr[0] should equal 1. + fr0 = 2 * frp[2] - frp[1] + fr /= fr0 + + # Fix potential divide-by-zero issue, fr is 1 at r == 0 + fr[~vmask] = 1 + + return fr + + +# End of file diff --git a/src/diffpy/cmisas/functions.py b/src/diffpy/cmisas/functions.py deleted file mode 100644 index e7e2c8e..0000000 --- a/src/diffpy/cmisas/functions.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np - - -def dot_product(a, b): - """Compute the dot product of two vectors of any size. - - Ensure that the inputs, a and b, are of the same size. - The supported types are "array_like" objects, which can - be converted to a NumPy array. Examples include lists and tuples. - - Parameters - ---------- - a : array_like - The first input vector. - b : array_like - The second input vector. - - Returns - ------- - float - The dot product of the two vectors. - - Examples - -------- - Compute the dot product of two lists: - >>> a = [1, 2, 3] - >>> b = [4, 5, 6] - >>> dot_product(a, b) - 32.0 - """ - return float(np.dot(a, b)) diff --git a/src/diffpy/cmisas/prcalculator.py b/src/diffpy/cmisas/prcalculator.py new file mode 100644 index 0000000..2c2e058 --- /dev/null +++ b/src/diffpy/cmisas/prcalculator.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2008 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Nanoparticle form factor P(r) calculator. + +The PrCalculator class wraps a sas.pr.invertor.Invertor object as a +Calculator. This is not wrapped as a ProfileGenerator because it will be +used to share information between SAS I(Q) to PDF G(r), but it does not +use the same profile as the PDF, which is where the calculator will be +applied. +""" + +__all__ = ["PrCalculator", "CFCalculator"] + +from functools import partial + +import numpy + +from diffpy.srfit.fitbase import Calculator + +Invertor = None + + +class PrCalculator(Calculator): + """A class for calculating P(r) from data. + + This is provided so that SAS data can be used to calculate a nanoparticle + form factor (characteristic function) for nanoparticle PDF refinements. For + a crystal-like nanoparticle: + Gnano(r) = f(r)Gcrystal(r), + where f(r) is the nanoparticle form factor. + This is obtained from P(r) as + P(r) = 4 pi r**2 f(r). + + Attributes + ---------- + _invertor + sas.pr.invertor.Invertor object. This object is internal, + but can be configured by the user after initialization. + Note that the 'x', 'y' and 'err' attributes get overwritten + every time the invertor is used. + + Managed Parameters + ------------------ + scale + The scale factor (default 1). + q + The q-values of the I(q) signal + iq + The I(q) signal + diq + The uncertainty in I(q) + """ + + def __init__(self, name): + """Initialize the generator. + + Parameters + ---------- + name + A name for the PrCalculator + """ + Calculator.__init__(self, name) + + # delayed import of Invertor + global Invertor + if Invertor is None: + from diffpy.cmisas.sasimport import sasimport + + Invertor = sasimport("sas.pr.invertor").Invertor + + self._invertor = Invertor() + + self._new_parameter("scale", 1) + self._new_parameter("q", None) + self._new_parameter("iq", None) + self._new_parameter("diq", None) + return + + def __call__(self, r): + """Calculate P(r) from the data or calculated signal.""" + q = self.q.value + iq = self.iq.value + diq = self.diq.value + if diq is None: + diq = numpy.ones_like(q) + self._invertor.d_max = max(r) + 5.0 + # Assume profile doesn't include 0. It's up to the user to make this + # happen. + self._invertor.x = q + self._invertor.y = iq + self._invertor.err = diq + c, c_cov = self._invertor.invert_optimize() + _inverted_w_c = partial(self._inverted, c=c) + pr = map(_inverted_w_c, r) + + pr = numpy.array(pr) + return self.scale.value * pr + + def _inverted(self, x, c): + self._invertor.pr(c, x) + + +# End class PrCalculator + + +class CFCalculator(PrCalculator): + """A class for calculating the characteristic function (CF) from + data. + + This calculator produces + f(r) = P(r) / 4 pi r**2 + which is the nanoparticle form factor scaled by density. + + Attributes + ---------- + _invertor + sas.pr.invertor.Invertor object. This object is internal, + but can be configured by the user after initialization. + Note that the 'x', 'y' and 'err' attributes get overwritten + every time the invertor is used. + + Managed Parameters + ------------------ + scale + The scale factor (default 1). + q + The q-values of the I(q) signal + iq + The I(q) signal + diq + The uncertainty in I(q) + """ + + def __call__(self, r): + """Calculate P(r) from the data or calculated signal.""" + fr = PrCalculator.__call__(self, r) + fr /= 4 * numpy.pi * r**2 + if r[0] == 0: + # Assume the scale makes fr properly normalized. We don't have much + # other choice. + fr[0] = 1 + return fr + + +# End class CFCalculator diff --git a/src/diffpy/cmisas/sasgenerator.py b/src/diffpy/cmisas/sasgenerator.py new file mode 100644 index 0000000..337f83d --- /dev/null +++ b/src/diffpy/cmisas/sasgenerator.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2008 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""SAS profile generator. + +The SASGenerator class wraps a sas.models.BaseModel object as a +ProfileGenerator. +""" + +__all__ = ["SASGenerator"] + +from diffpy.srfit.fitbase import ProfileGenerator +from diffpy.cmisas.sasparameter import SASParameter + + +class SASGenerator(ProfileGenerator): + """A class for calculating I(Q) from a scattering type. + + Attributes + ---------- + _model + BaseModel object this adapts. + + Managed Parameters + These depend on the parameters of the BaseModel object held by _model. + They are created from the 'params' attribute of the BaseModel. If a + dispersion is set for the BaseModel, the dispersion "width" will be + accessible under "_width", where is the name a + parameter adjusted by dispersion. + """ + + def __init__(self, name, model): + """Initialize the generator. + + Parameters + ---------- + name + A name for the SASGenerator + model + SASModel object this adapts. + """ + ProfileGenerator.__init__(self, name) + + self._model = model + + # Wrap normal parameters + for parname in model.params: + par = SASParameter(parname, model) + self.add_parameter(par) + + # Wrap dispersion parameters + for parname in model.dispersion: + name = parname + "_width" + parname += ".width" + par = SASParameter(name, model, parname) + self.add_parameter(par) + + return + + def __call__(self, q): + """Calculate I(Q) for the BaseModel.""" + return self._model.evalDistribution(q) + + +# End class SASGenerator diff --git a/src/diffpy/cmisas/sasimport.py b/src/diffpy/cmisas/sasimport.py new file mode 100644 index 0000000..523c960 --- /dev/null +++ b/src/diffpy/cmisas/sasimport.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit Complex Modeling Initiative +# (c) 2015 Brookhaven Science Associates, +# Brookhaven National Laboratory. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Universal import functions for volatile SasView/SansViews API-s.""" + +# FIXME: remove this line when black and docformatter agree. + + +def sasimport(modname): + """Import specified module from the SasView sas package. + + Parameters + ---------- + modname + absolute module name contained in the sas package. + + When specified import does not work directly, try older API-s and raise + DeprecationWarning. Raise ImportError if nothing works. + """ + if not modname.startswith("sas."): + emsg = 'Module name must start with "sas."' + raise ValueError(emsg) + mobj = None + # standard import + try: + exec("import %s as mobj" % modname) + except ImportError: + pass + else: + return mobj + # revert to the old sans namespace, sas --> sans + modsans = "sans" + modname[3:] + import warnings + + wfmt = ( + "Using obsolete package %r instead of %r. Please install " + "SasView 3.1 or the srfit-sasview package from Anaconda." + ) + wmsg = wfmt % (modsans, modname) + try: + exec("import %s as mobj" % modsans) + warnings.warn(wmsg, DeprecationWarning) + except ImportError: + pass + else: + return mobj + # finally check the oldest DataLoader API for sas.dataloader + if modname.startswith("sas.dataloader"): + modloader = "DataLoader" + modname[14:] + wmsg = wfmt % (modloader, modname) + try: + exec("import %s as mobj" % modloader) + warnings.warn(wmsg, DeprecationWarning) + except ImportError: + pass + else: + return mobj + # Obsolete API-s failed here. Import again and let it raise ImportError. + exec("import %s as mobj" % modname) + raise AssertionError("The import above was supposed to fail.") diff --git a/src/diffpy/cmisas/sasparameter.py b/src/diffpy/cmisas/sasparameter.py new file mode 100644 index 0000000..631c015 --- /dev/null +++ b/src/diffpy/cmisas/sasparameter.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2008 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""SAS profile generator. + +The SASGenerator class wraps a sas.models.BaseModel object as a +ProfileGenerator. +""" + +__all__ = ["SASParameter"] + +from diffpy.srfit.fitbase.parameter import Parameter + + +class SASParameter(Parameter): + """Class adapting a sasmodel parameter to srfit Parameter. + + Attributes + ---------- + name + A name for this Parameter. + const + A flag indicating whether this is considered a constant. + _value + The value of the Parameter. Modified with 'set_value'. + value + Property for 'get_value' and 'set_value'. + constrained + A flag indicating if the Parameter is constrained + (default False). + bounds + A 2-list defining the bounds on the Parameter. This can be + used by some optimizers when the Parameter is varied. These + bounds are unrelated to restraints on a Parameter. + _model + The BaseModel to which the underlying parameter belongs. + _parname + The name of the underlying BaseModel parameter. + """ + + def __init__(self, name, model, parname=None): + """Create the Parameter. + + Parameters + ---------- + name + Name of the Parameter + model + The BaseModel to which the underlying parameter belongs + parname + Name of parameter used by the model. If this is None + (default), then name is used. + """ + self._parname = parname or name + val = model.getParam(self._parname) + self._model = model + Parameter.__init__(self, name, val) + return + + def get_value(self): + """Get the value of the Parameter.""" + value = self._model.getParam(self._parname) + return value + + def set_value(self, value): + """Set the value of the Parameter.""" + if value != self.get_value(): + self._model.setParam(self._parname, value) + self.notify() + return self + + +# End of class SASParameter diff --git a/src/diffpy/cmisas/sasparser.py b/src/diffpy/cmisas/sasparser.py new file mode 100644 index 0000000..ab6b4d4 --- /dev/null +++ b/src/diffpy/cmisas/sasparser.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2008 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""This module contains parsers for SAS data. + +SASParser uses the sas DataLoader class to load data. +""" + +__all__ = ["SASParser"] + + +from diffpy.srfit.exceptions import ParseError +from diffpy.srfit.fitbase.profileparser import ProfileParser + + +class SASParser(ProfileParser): + """Class for parsing a sas profile. + + This uses a sas DataLoader to load the data. The DataInfo object it returns + is held in the metadata under the name "datainfo". + + Attributes + ---------- + _format + Name of the data format that this parses (string, default + ""). The format string is a unique identifier for the data + format handled by the parser. + _banks + The data from each bank. Each bank contains a + (x, y, dx, dy) tuple: + + x + A numpy array containing the independent + variable read from the file. + y + A numpy array containing the profile + from the file. + dx + A numpy array containing the uncertainty in x + read from the file. This is 0 if the + uncertainty cannot be read. + dy + A numpy array containing the uncertainty read + from the file. This is 0 if the uncertainty + cannot be read. + _x + Independent variable from the chosen bank + _y + Profile from the chosen bank + _dx + Uncertainty in independent variable from the chosen bank + _dy + Uncertainty in profile from the chosen bank + _meta + A dictionary containing metadata read from the file. + + + General Metadata + ---------------- + filename + The name of the file from which data was parsed. This key + will not exist if data was not read from file. + nbanks + The number of banks parsed. + bank + The chosen bank number. + + + Metadata + -------- + datainfo + The DataInfo object used to do the data parsing. + + + These may appear in the metadata dictionary + """ + + _format = "SAS" + + def parse_file(self, filename, column_format=None, **kwargs): + """Parse a file and set the _x, _y, _dx, _dy and _meta + variables. + + The sas DataLoader reads the data and the metadata together, so + this overrides parse_file itself rather than the + `_parse_metadata` and `_parse_data` hooks. + + This wipes out the currently loaded data and selected bank number. + + Parameters + ---------- + filename + The name of the file to parse + column_format + Unused. Accepted so that the signature matches + `ProfileParser.parse_file`. + kwargs + Unused. Accepted so that the signature matches + `ProfileParser.parse_file`. + + Raises + ---------- + IOError + if the file cannot be read + ParseError + if the file cannot be parsed + """ + import sasdata.dataloader.loader as sas_dataloader + + Loader = sas_dataloader.Loader + loader = Loader() + + try: + data = loader.load(str(filename)) + except RuntimeError as e: + raise ParseError(e) + except ValueError as e: + raise ParseError(e) + + self._banks = [] + self._meta = {} + self._meta["filename"] = filename + self._meta["datainfo"] = data + + for data_obj in data: + self._banks.append( + [data_obj.x, data_obj.y, data_obj.dx, data_obj.dy] + ) + # FIXME: Revisit when we refactor the SAS characteristic functions. + # Why is a list imported but only the first element is taken? + # Is this desired behavior? + self.select_bank(0) + return + + +# End of class SASParser diff --git a/src/diffpy/cmisas/sasprofile.py b/src/diffpy/cmisas/sasprofile.py new file mode 100644 index 0000000..5e8bd7d --- /dev/null +++ b/src/diffpy/cmisas/sasprofile.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2008 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Chris Farrow +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Class for adapting a sas DataInfo objects to the Profile +interface.""" + +__all__ = ["SASProfile"] + +from numpy import ones_like + +from diffpy.srfit.fitbase.profile import Profile + + +class SASProfile(Profile): + """Observed and calculated profile container for SAS data. + + This wraps a sas DataInfo object as a Profile object. Use this when you + want to use and manipulate a DataProfile before using it with SrFit. + Otherwise, use the SASParser class and load the data into a base Profile + object. + + Attributes + ---------- + _xobs + A numpy array of the observed independent variable (default + None) + xobs + Read-only property of _xobs. + _yobs + A numpy array of the observed signal (default None) + yobs + Read-only property of _yobs. + _dyobs + A numpy array of the uncertainty of the observed signal + (default None, optional). + dyobs + Read-only property of _dyobs. + x + A numpy array of the calculated independent variable (default + None, property for xpar accessors). + y + The profile over the calculation range (default None, property + for ypar accessors). + dy + The uncertainty in the profile over the calculation range + (default None, property for dypar accessors). + ycalc + A numpy array of the calculated signal (default None). + xpar + A ProfileParameter that stores x (named "x"). + ypar + A ProfileParameter that stores y (named "y"). + dypar + A ProfileParameter that stores dy (named "dy"). + meta + A dictionary of metadata. This is only set if provided by a + parser. + + Attributes + ---------- + _datainfo + The DataInfo object this wraps. + """ + + def __init__(self, datainfo): + """Initialize the attributes. + + Parameters + ---------- + datainfo + The DataInfo object this wraps. + """ + self._datainfo = datainfo[0] + Profile.__init__(self) + + self._xobs = self._datainfo.x + self._yobs = self._datainfo.y + if self._datainfo.dy is None or 0 == len(self._datainfo.dy): + self._dyobs = ones_like(self.xobs) + else: + self._dyobs = self._datainfo.dy + return + + def set_observed_profile(self, xobs, yobs, dyobs=None): + """Set the observed profile. + + This is overloaded to change the value within the datainfo object. + + Parameters + ---------- + xobs + Numpy array of the independent variable. + yobs + Numpy array of the observed signal. + dyobs + Numpy array of the uncertainty in the observed signal. If + ``dyobs`` is ``None`` (default), it stays ``None`` to indicate + no uncertainty was observed. + + Raises + ------ + ValueError + If ``len(yobs) != len(xobs)``. + ValueError + If ``dyobs is not None`` and ``len(dyobs) != len(xobs)``. + """ + Profile.set_observed_profile(self, xobs, yobs, dyobs) + # Copy the arrays to the _datainfo attribute. + self._datainfo.x = self._xobs + self._datainfo.y = self._yobs + self._datainfo.dy = self._dyobs + return diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index e3b6313..8b363f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import importlib.resources import json from pathlib import Path @@ -17,3 +18,14 @@ def user_filesystem(tmp_path): json.dump(home_config_data, f) yield tmp_path + + +@pytest.fixture(scope="session") +def datafile(): + """Fixture to load a test data file from the testdata package + directory.""" + + def _datafile(filename): + return importlib.resources.files("tests.testdata").joinpath(filename) + + return _datafile diff --git a/tests/test_characteristicfunctions.py b/tests/test_characteristicfunctions.py new file mode 100644 index 0000000..aa62663 --- /dev/null +++ b/tests/test_characteristicfunctions.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2010 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Tests for the SAS characteristic function calculator.""" + +import numpy +import pytest +from sasmodels.sasview_model import find_model, load_standard_models + +import diffpy.cmisas.characteristicfunctions as cf + + +def testSphere(): + pytest.skip( + "sas characteristic functions not currently working, " + + "remove skip when our code is refactored to use the " + + "latest sasview API" + ) + + load_standard_models() + radius = 25 + # Calculate sphere cf from SphereModel + SphereModel = find_model("sphere") + model = SphereModel() + model.setParam("radius", radius) + ff = cf.SASCF("sphere", model) + r = numpy.arange(1, 60, 0.1, dtype=float) + fr1 = ff(r) + + # Calculate sphere cf analytically + fr2 = cf.spherical_particle(r, 2 * radius) + diff = fr1 - fr2 + res = numpy.dot(diff, diff) + res /= numpy.dot(fr2, fr2) + assert res == pytest.approx(0, abs=1e-4) + return + + +def testSpheroid(): + pytest.skip( + "sas characteristic functions not currently working, " + + "remove skip when our code is refactored to use the " + + "latest sasview API" + ) + + load_standard_models() + prad = 20.9 + erad = 33.114 + # Calculate cf from EllipsoidModel + EllipsoidModel = find_model("ellipsoid") + model = EllipsoidModel() + model.setParam("radius_polar", prad) + model.setParam("radius_equatorial", erad) + ff = cf.SASCF("spheroid", model) + r = numpy.arange(0, 100, 1 / numpy.pi, dtype=float) + fr1 = ff(r) + + # Calculate cf analytically + fr2 = cf.spheroidal_particle(r, erad, prad) + diff = fr1 - fr2 + res = numpy.dot(diff, diff) + res /= numpy.dot(fr2, fr2) + assert res == pytest.approx(0, abs=1e-4) + return + + +def testShell(): + pytest.skip( + "sas characteristic functions not currently working, " + + "remove skip when our code is refactored to use the " + + "latest sasview API" + ) + + load_standard_models() + radius = 19.2 + thickness = 7.8 + # Calculate cf from VesicleModel + VesicleModel = find_model("vesicle") + model = VesicleModel() + model.setParam("radius", radius) + model.setParam("thickness", thickness) + ff = cf.SASCF("vesicle", model) + r = numpy.arange(0, 99.45, 0.1, dtype=float) + fr1 = ff(r) + + # Calculate sphere cf analytically + fr2 = cf.shell_particle(r, radius, thickness) + diff = fr1 - fr2 + res = numpy.dot(diff, diff) + res /= numpy.dot(fr2, fr2) + assert res == pytest.approx(0, abs=1e-4) + return + + +def testCylinder(): + pytest.skip( + "sas characteristic functions not currently working, " + + "remove skip when our code is refactored to use the " + + "latest sasview API" + ) + load_standard_models() + # Make sure cylinder works over different r-ranges. + radius = 100 + length = 30 + + CylinderModel = find_model("cylinder") + model = CylinderModel() + model.setParam("radius", radius) + model.setParam("length", length) + + ff = cf.SASCF("cylinder", model) + + r1 = numpy.arange(0, 10, 0.1, dtype=float) + r2 = numpy.arange(0, 50, 0.1, dtype=float) + r3 = numpy.arange(0, 100, 0.1, dtype=float) + r4 = numpy.arange(0, 500, 0.1, dtype=float) + + fr1 = ff(r1) + fr2 = ff(r2) + fr3 = ff(r3) + fr4 = ff(r4) + + d = fr1 - numpy.interp(r1, r2, fr2) + res12 = numpy.dot(d, d) + res12 /= numpy.dot(fr1, fr1) + assert res12 == pytest.approx(0, abs=1e-4) + + d = fr1 - numpy.interp(r1, r3, fr3) + res13 = numpy.dot(d, d) + res13 /= numpy.dot(fr1, fr1) + assert res13 == pytest.approx(0, abs=1e-4) + + d = fr1 - numpy.interp(r1, r4, fr4) + res14 = numpy.dot(d, d) + res14 /= numpy.dot(fr1, fr1) + assert res14 == pytest.approx(0, abs=1e-4) + + d = fr2 - numpy.interp(r2, r3, fr3) + res23 = numpy.dot(d, d) + res23 /= numpy.dot(fr2, fr2) + assert res23 == pytest.approx(0, abs=1e-4) + + d = fr2 - numpy.interp(r2, r4, fr4) + res24 = numpy.dot(d, d) + res24 /= numpy.dot(fr2, fr2) + assert res24 == pytest.approx(0, abs=1e-4) + + d = fr3 - numpy.interp(r3, r4, fr4) + res34 = numpy.dot(d, d) + res34 /= numpy.dot(fr3, fr3) + assert res34 == pytest.approx(0, abs=1e-4) + return + + +# End of class TestSASCF diff --git a/tests/test_functions.py b/tests/test_functions.py deleted file mode 100644 index 4fe9df0..0000000 --- a/tests/test_functions.py +++ /dev/null @@ -1,40 +0,0 @@ -import numpy as np -import pytest - -from diffpy.cmisas import functions # noqa - - -def test_dot_product_2D_list(): - a = [1, 2] - b = [3, 4] - expected = 11.0 - actual = functions.dot_product(a, b) - assert actual == expected - - -def test_dot_product_3D_list(): - a = [1, 2, 3] - b = [4, 5, 6] - expected = 32.0 - actual = functions.dot_product(a, b) - assert actual == expected - - -@pytest.mark.parametrize( - "a, b, expected", - [ - # Test whether the dot product function works with 2D and 3D vectors - # C1: lists, expect correct float output - ([1, 2], [3, 4], 11.0), - ([1, 2, 3], [4, 5, 6], 32.0), - # C2: tuples, expect correct float output - ((1, 2), (3, 4), 11.0), - ((1, 2, 3), (4, 5, 6), 32.0), - # C3: numpy arrays, expect correct float output - (np.array([1, 2]), np.array([3, 4]), 11.0), - (np.array([1, 2, 3]), np.array([4, 5, 6]), 32.0), - ], -) -def test_dot_product(a, b, expected): - actual = functions.dot_product(a, b) - assert actual == expected diff --git a/tests/test_sas.py b/tests/test_sas.py new file mode 100644 index 0000000..b883d7f --- /dev/null +++ b/tests/test_sas.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srfit by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2010 The Trustees of Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Tests for sas package.""" + +import numpy +import pytest + +from diffpy.cmisas import SASGenerator, SASParser, SASProfile + +# ---------------------------------------------------------------------------- +# FIXME: adjust sensitivity of the pytest.approx statements when ready to test +# with sasview installed. + + +def testParser(datafile): + + data = datafile("sas_ascii_test_1.txt") + parser = SASParser() + parser.parse_file(data) + x, y, dx, dy = parser.get_data() + testx = numpy.array( + [ + 0.002618, + 0.007854, + 0.01309, + 0.01832, + 0.02356, + 0.02879, + 0.03402, + 0.03925, + 0.04448, + 0.0497, + ] + ) + diff = testx - x + res = numpy.dot(diff, diff) + assert 0 == pytest.approx(res) + + testy = numpy.array( + [ + 0.02198, + 0.02201, + 0.02695, + 0.02645, + 0.03024, + 0.3927, + 7.305, + 17.43, + 13.43, + 8.346, + ] + ) + diff = testy - y + res = numpy.dot(diff, diff) + assert 0 == pytest.approx(res) + + testdy = numpy.array( + [ + 0.002704, + 0.001643, + 0.002452, + 0.001769, + 0.001531, + 0.1697, + 1.006, + 0.5351, + 0.3677, + 0.191, + ] + ) + diff = testdy - dy + res = numpy.dot(diff, diff) + assert 0 == pytest.approx(res) + + testdx = numpy.array( + [ + 0.0004091, + 0.005587, + 0.005598, + 0.005624, + 0.005707, + 0.005975, + 0.006264, + 0.006344, + 0.006424, + 0.006516, + ] + ) + diff = testdx - dx + res = numpy.dot(diff, diff) + assert 0 == pytest.approx(res) + return + + +# End of class TestSASParser + + +@pytest.mark.xfail( + raises=ModuleNotFoundError, + strict=True, + reason="sasmodels' load_standard_models imports SasView (sas), " + "which is not installable on Python 3.12+", +) +def test_generator(): + from sasmodels.sasview_model import find_model, load_standard_models + + load_standard_models() + SphereModel = find_model("sphere") + model = SphereModel() + gen = SASGenerator("sphere", model) + for pname in model.params: + defval = model.getParam(pname) + par = gen.get(pname) + assert defval == par.get_value() + # Test setting values + par.set_value(1.0) + assert 1.0 == par.get_value() + assert 1.0 == model.getParam(pname) + par.set_value(defval) + assert defval == par.get_value() + assert defval == model.getParam(pname) + + r = numpy.arange(1, 10, 0.1, dtype=float) + y = gen(r) + refy = model.evalDistribution(r) + diff = y - refy + res = numpy.dot(diff, diff) + assert 0 == pytest.approx(res) + return + + +@pytest.mark.xfail( + raises=ModuleNotFoundError, + strict=True, + reason="sasmodels' load_standard_models imports SasView (sas), " + "which is not installable on Python 3.12+", +) +def testGenerator2(datafile): + from sasmodels.sasview_model import find_model, load_standard_models + + load_standard_models() + EllipsoidModel = find_model("ellipsoid") + model = EllipsoidModel() + gen = SASGenerator("ellipsoid", model) + + # Load the data using SAS tools + import sasdata.dataloader.loader as sas_dataloader + + Loader = sas_dataloader.Loader + loader = Loader() + data = datafile("sas_ellipsoid_testdata.txt") + datainfo = loader.load(str(data)) + profile = SASProfile(datainfo) + + gen.set_profile(profile) + gen.scale.value = 1.0 + gen.radius_polar.value = 20 + gen.radius_equatorial.value = 400 + gen.background.value = 0.01 + + y = gen(profile.xobs) + diff = profile.yobs - y + res = numpy.dot(diff, diff) + # FIXME: go back to default tolerance when we figure out why + # the models are not identical + assert 0 == pytest.approx(res, abs=1e-3) + return diff --git a/tests/testdata/sas_ascii_test_1.txt b/tests/testdata/sas_ascii_test_1.txt new file mode 100644 index 0000000..bd7a34d --- /dev/null +++ b/tests/testdata/sas_ascii_test_1.txt @@ -0,0 +1,24 @@ +FILE: jan08002.SA2_BH _R556 CREATED: Mon, Jan 21, 2008 1:34:58 PM +LABEL: scatt 1m sam1 +MON CNT LAMBDA DET ANG DET DIST TRANS THICK AVE STEP +1e+08 6 25 1 0.5667 0.2C 1 +BCENT(X,Y) A1(mm) A2(mm) A1A2DIST(m) DL/L BSTOP(mm) DET_TYP +114.58 64.22 50 12.7 3.914 0.124 50.8 ORNL +SAM: jan08002.SA2_BH _R556 +BGD: JAN08008.SA2_BH_R562, +EMP: JAN08007.SA2_BH_R561, +DIV: PLEX_04JAN2008_NG7.DIV, +MASK: DEFAULT.MASK, +ABS Parameters (3-6): TSTAND=1;DSTAND=1;IZERO=2.547e+05;XSECT=1 +Average Choices: AVTYPE=Circular;SAVE=Yes;NAME=Auto;PLOT=Yes; +The 6 columns are | Q (1/A) | I(Q) (1/cm) | std. dev. I(Q) (1/cm) | sigmaQ | meanQ | ShadowFactor| + 0.002618 0.02198 0.002704 0.0004091 0.008082 1e-10 + 0.007854 0.02201 0.001643 0.005587 0.0275 1.564e-12 + 0.01309 0.02695 0.002452 0.005598 0.02762 2.525e-07 + 0.01832 0.02645 0.001769 0.005624 0.02787 0.001046 + 0.02356 0.03024 0.001531 0.005707 0.02847 0.1293 + 0.02879 0.3927 0.1697 0.005975 0.03031 0.7933 + 0.03402 7.305 1.006 0.006264 0.03457 0.9972 + 0.03925 17.43 0.5351 0.006344 0.0397 1 + 0.04448 13.43 0.3677 0.006424 0.04488 1 + 0.0497 8.346 0.191 0.006516 0.05006 1 diff --git a/tests/testdata/sas_ellipsoid_testdata.txt b/tests/testdata/sas_ellipsoid_testdata.txt new file mode 100644 index 0000000..b982308 --- /dev/null +++ b/tests/testdata/sas_ellipsoid_testdata.txt @@ -0,0 +1,136 @@ +pythonclass = EllipsoidModel +scale = 1 +radius_a = 20 +radius_b = 400 +contrast = 3e-6 +background = 0.01 + +DATA +0.001 11808.8528968632 +0.00105251266011778 11781.7147338649 +0.00110778289970821 11751.726514487 +0.00116595552660488 11718.5978599118 +0.00122718295288593 11682.01083219 +0.00129162559419317 11641.6177848645 +0.00135945229002046 11597.0391584392 +0.00143084074607265 11547.8612513417 +0.00150597799985384 11493.6340074247 +0.00158506091070502 11433.8688737723 +0.00166829667557485 11368.0367979944 +0.00175590337187494 11295.5664527249 +0.00184811052884188 11215.8427971077 +0.00194515972890305 11128.2061110769 +0.00204730524062173 11031.951668581 +0.00215481468487986 10926.3302508532 +0.00226796973604376 10810.5497404861 +0.00238706685995004 10683.7780812871 +0.00251241809064502 10545.1479370896 +0.00264435184791283 10393.7634337298 +0.00278321379773411 10228.7094202917 +0.00292936775792964 10049.0637353733 +0.00308319665136179 9853.91300696029 +0.00324510350919104 9642.37254402161 +0.00341551252681621 9413.61088528121 +0.00359487017526494 9166.879543996 +0.00378364637094618 8901.54841182498 +0.00398233570682956 8617.14714114096 +0.00419145874827721 8313.41259076583 +0.0044115633969232 7990.34206915047 +0.00464322632617388 7648.25161351986 +0.00488705449209019 7287.83787614165 +0.0051436867236104 6910.24132742693 +0.0054137953962797 6517.10741990347 +0.00569808819387176 6110.64109931719 +0.00599730996251769 5693.64864736002 +0.00631224466220038 5269.55939642207 +0.00664371742072679 4842.41854557534 +0.00699259669556001 4416.8413977311 +0.00735979654917468 3997.91920819247 +0.00774627904389752 3591.06797139074 +0.00815305676250721 3201.81444399825 +0.00858119546119774 2835.51908599765 +0.00903181686185589 2497.04384279986 +0.00950610159096858 2190.38388293401 +0.0100052922728602 1918.29597467291 +0.010530696785364 1681.97052793915 +0.0110836916864573 1480.80647627844 +0.0116657258208385 1312.35366329635 +0.0122783241158954 1172.4806119466 +0.0129230915770094 1055.80087419096 +0.0136017174926639 956.345199427954 +0.0143159798603742 868.401686020703 +0.0150677500450351 787.373295795928 +0.0158589976818897 710.444579006174 +0.0166917958369675 636.839691442685 +0.0175683264385096 567.524137445761 +0.0184908859936133 504.366315112346 +0.0194618916050726 449.001613576719 +0.0204838873041789 401.841966414852 +0.0215595507160742 361.713843354515 +0.0226917000751195 326.376840678275 +0.0238833016086589 293.695004896232 +0.0251374773085249 262.744038847928 +0.0264575131106459 234.039847131565 +0.027846867504187 208.639158819275 +0.0293091805927793 186.837277235271 +0.0308482836315787 167.70997478874 +0.0324682090651407 150.021720507628 +0.0341732010924115 133.469048132347 +0.035967726786514 118.660929907549 +0.0378564877984635 105.851355256071 +0.0398444326754772 94.3663066816175 +0.0419367698261504 83.6578396413674 +0.0441389811664687 74.0303846471908 +0.0464568364824087 65.6488966587392 +0.0488964085467568 58.0126389704112 +0.0514640890297529 51.0164068972012 +0.0541666052452436 44.9003014032841 +0.0570110377762212 39.3721336546038 +0.0600048390259259 34.3529627586149 +0.0631558527431166 29.958497372189 +0.0664723345726646 25.9550620560306 +0.0699629736853145 22.4261198409402 +0.0736369155432808 19.2679866604944 +0.0775037858613269 16.4682212334822 +0.0815737158261042 13.9888621639924 +0.085857368639825 11.805997960264 +0.0903659674578153 9.88077806606173 +0.0951113247931421 8.21127352905187 +0.100105873465356 6.74958811344309 +0.105362699174436 5.49602638898874 +0.110895574785276 4.42590111015658 +0.116718996412541 3.51634335501236 +0.122848221400442 2.7564115317264 +0.129299308296917 2.13145505928053 +0.136089158926978 1.62491960968472 +0.143235562675425 1.22192025374086 +0.150757243094979 0.908755828531692 +0.158673906961919 0.672118368226188 +0.167006295907771 0.499015180776053 +0.175776240762306 0.376993265291224 +0.185006718750238 0.294475487249178 +0.194721913691476 0.240999006101438 +0.20494727936264 0.206976804274189 +0.215709606185875 0.184084804995347 +0.227037091419654 0.166799187053838 +0.238959413035505 0.150583285805888 +0.251507807474183 0.133330958053468 +0.264715151485044 0.113698351753489 +0.278616048263005 0.0866638841974435 +0.2932469181088 0.0612447728443078 +0.308646093850034 0.07146333544507 +0.324853921273063 0.0642081900887752 +0.341912864828804 0.0396251834721476 +0.359867618889456 0.0210274910924806 +0.378765224847594 0.0199010559646776 +0.398655194364451 0.0279754666458064 +0.4195896390903 0.0319472487548983 +0.441623407196792 0.0268350754071031 +0.464814227078974 0.0181555013515734 +0.489222858603482 0.0134732506169512 +0.514913252299177 0.0143447123658664 +0.541952716907305 0.0163992637354073 +0.570412095730168 0.0156477366167964 +0.600365952240318 0.012898427036931 +0.631892765436603 0.0114950061180364 +0.665075135458861 0.0126814563174163 From d31ceec5c170753e4d6f433629705b2b2a4f49b4 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Fri, 16 Oct 2026 13:07:16 -0400 Subject: [PATCH 3/4] build: finish cmisas config for the SAS port - pyproject.toml, cookiecutter.json: support Python 3.12-3.14, matching diffpy.srfit (sasmodels and sasdata are noarch). - pyproject.toml: point the diffpy-cmisas entry point at cmisas_app:main (app.py does not exist), and fail tests when cmisas calls its own deprecated names, matching srfit. - src/diffpy/__init__.py: add extend_path so an editable cmisas does not hide other diffpy packages. - docs: replace the template API page with the cmisas modules, and use m2r2 instead of the unmaintained m2r. Co-Authored-By: Claude Opus 5.5 --- cookiecutter.json | 4 +- .../api/diffpy.cmisas.example_package.rst | 31 --------- docs/source/api/diffpy.cmisas.rst | 66 +++++++++++++++---- docs/source/conf.py | 2 +- pyproject.toml | 12 +++- requirements/docs.txt | 2 +- src/diffpy/__init__.py | 4 ++ 7 files changed, 69 insertions(+), 52 deletions(-) delete mode 100644 docs/source/api/diffpy.cmisas.example_package.rst diff --git a/cookiecutter.json b/cookiecutter.json index 17213b6..2dae4ef 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -11,8 +11,8 @@ "package_dir_name": "diffpy.cmisas", "project_short_description": "The code of the SAS pack of the diffpy.cmi package.", "project_keywords": "Small angle scattering, PDF, x-ray neutron", - "minimum_supported_python_version": "3.11", - "maximum_supported_python_version": "3.13", + "minimum_supported_python_version": "3.12", + "maximum_supported_python_version": "3.14", "project_needs_c_code_compiled": "No", "project_has_gui_tests": "No" } diff --git a/docs/source/api/diffpy.cmisas.example_package.rst b/docs/source/api/diffpy.cmisas.example_package.rst deleted file mode 100644 index 3c960d0..0000000 --- a/docs/source/api/diffpy.cmisas.example_package.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _example_package documentation: - -|title| -======= - -.. |title| replace:: diffpy.cmisas.example_package package - -.. automodule:: diffpy.cmisas.example_package - :members: - :undoc-members: - :show-inheritance: - -|foo| ------ - -.. |foo| replace:: diffpy.cmisas.example_package.foo module - -.. automodule:: diffpy.cmisas.example_package.foo - :members: - :undoc-members: - :show-inheritance: - -|bar| ------ - -.. |bar| replace:: diffpy.cmisas.example_package.bar module - -.. automodule:: diffpy.cmisas.example_package.foo - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/diffpy.cmisas.rst b/docs/source/api/diffpy.cmisas.rst index 17c0a94..53b0d00 100644 --- a/docs/source/api/diffpy.cmisas.rst +++ b/docs/source/api/diffpy.cmisas.rst @@ -1,30 +1,68 @@ :tocdepth: -1 -|title| -======= - -.. |title| replace:: diffpy.cmisas package +diffpy.cmisas package +===================== .. automodule:: diffpy.cmisas :members: :undoc-members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - diffpy.cmisas.example_package - Submodules ---------- -|module| --------- +diffpy.cmisas.sasgenerator module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.sasgenerator + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmisas.sasparser module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.sasparser + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmisas.sasprofile module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.sasprofile + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmisas.sasparameter module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.sasparameter + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmisas.prcalculator module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.prcalculator + :members: + :undoc-members: + :show-inheritance: + +diffpy.cmisas.characteristicfunctions module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: diffpy.cmisas.characteristicfunctions + :members: + :undoc-members: + :show-inheritance: -.. |module| replace:: diffpy.cmisas.example_submodule module +diffpy.cmisas.sasimport module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: diffpy.cmisas.example_submodule +.. automodule:: diffpy.cmisas.sasimport :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index e5302eb..6bbe7cf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ "sphinx.ext.intersphinx", "sphinx_rtd_theme", "sphinx_copybutton", - "m2r", + "m2r2", ] # Add any paths that contain templates here, relative to this directory. diff --git a/pyproject.toml b/pyproject.toml index 84be46a..4643ff9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ maintainers = [ description = "The code of the SAS pack of the diffpy.cmi package." keywords = ['Small angle scattering', 'PDF', 'x-ray neutron'] readme = "README.rst" -requires-python = ">=3.11, <3.14" +requires-python = ">=3.12, <3.15" classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', @@ -25,9 +25,9 @@ classifiers = [ 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: Unix', - 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Topic :: Scientific/Engineering :: Physics', 'Topic :: Scientific/Engineering :: Chemistry', ] @@ -49,11 +49,17 @@ exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) [project.scripts] -diffpy-cmisas = "diffpy.cmisas.app:main" +diffpy-cmisas = "diffpy.cmisas.cmisas_app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} +[tool.pytest.ini_options] +# Fail when cmisas source calls one of its own deprecated names. +filterwarnings = [ + "error:'diffpy\\.cmisas\\.:DeprecationWarning:diffpy\\.cmisas", +] + [tool.codespell] exclude-file = ".codespell/ignore_lines.txt" ignore-words = ".codespell/ignore_words.txt" diff --git a/requirements/docs.txt b/requirements/docs.txt index 5f34c6e..1de813f 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,4 +2,4 @@ sphinx sphinx_rtd_theme sphinx-copybutton doctr -m2r +m2r2 diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index dc6e20d..1b825d2 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -12,3 +12,7 @@ # See LICENSE.rst for license information. # ############################################################################## + +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) From 0045077f89a1b09741044c49d580d23e64575a96 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Fri, 16 Oct 2026 13:18:36 -0400 Subject: [PATCH 4/4] build: support Python 3.11-3.13 for now Set requires-python, the classifiers and cookiecutter.json to 3.11-3.13, matching the other diffpy.cmi* packages. Co-Authored-By: Claude Opus 5.5 --- cookiecutter.json | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cookiecutter.json b/cookiecutter.json index 2dae4ef..17213b6 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -11,8 +11,8 @@ "package_dir_name": "diffpy.cmisas", "project_short_description": "The code of the SAS pack of the diffpy.cmi package.", "project_keywords": "Small angle scattering, PDF, x-ray neutron", - "minimum_supported_python_version": "3.12", - "maximum_supported_python_version": "3.14", + "minimum_supported_python_version": "3.11", + "maximum_supported_python_version": "3.13", "project_needs_c_code_compiled": "No", "project_has_gui_tests": "No" } diff --git a/pyproject.toml b/pyproject.toml index 4643ff9..9c943e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ maintainers = [ description = "The code of the SAS pack of the diffpy.cmi package." keywords = ['Small angle scattering', 'PDF', 'x-ray neutron'] readme = "README.rst" -requires-python = ">=3.12, <3.15" +requires-python = ">=3.11, <3.14" classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', @@ -25,9 +25,9 @@ classifiers = [ 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: Unix', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: 3.14', 'Topic :: Scientific/Engineering :: Physics', 'Topic :: Scientific/Engineering :: Chemistry', ]