Skip to content

Conversation

zmx27
Copy link
Contributor

@zmx27 zmx27 commented Jul 30, 2025

Addresses #100

@zmx27
Copy link
Contributor Author

zmx27 commented Jul 30, 2025

@sbillinge here are my initial edits to update our code to use the new sasmodel/sasdata api. These changes introduced some warnings that I don't quite understand. Now that the tests in test_characteristicfunctions.py are no longer being skipped, I also added the new updated api there, but it introduced a new error that one of the functions that we were testing is no longer implemented. Here are the pytest results:

======================================== test session starts =========================================
platform darwin -- Python 3.13.5, pytest-8.4.1, pluggy-1.6.0
rootdir: /Users/zhimingxu/BillingeGroup/diffpy.srfit
configfile: pyproject.toml
plugins: env-1.1.5, cov-6.2.1
collected 111 items

tests/test_builder.py ......                                                                   [  5%]
tests/test_characteristicfunctions.py FFFF                                                     [  9%]
tests/test_constraint.py .                                                                     [  9%]
tests/test_contribution.py ..........                                                          [ 18%]
tests/test_diffpyparset.py sss                                                                 [ 21%]
tests/test_equation.py ..                                                                      [ 23%]
tests/test_fitrecipe.py ....                                                                   [ 27%]
tests/test_fitresults.py ...                                                                   [ 29%]
tests/test_literals.py .........                                                               [ 37%]
tests/test_objcrystparset.py sssssssssss                                                       [ 47%]
tests/test_parameter.py ...                                                                    [ 50%]
tests/test_parameterset.py .                                                                   [ 51%]
tests/test_pdf.py ..ssssss                                                                     [ 58%]
tests/test_profile.py ......                                                                   [ 63%]
tests/test_profilegenerator.py ...                                                             [ 66%]
tests/test_recipeorganizer.py ...............                                                  [ 80%]
tests/test_restraint.py .                                                                      [ 81%]
tests/test_sas.py ...                                                                          [ 83%]
tests/test_sgconstraints.py sss                                                                [ 86%]
tests/test_tagmanager.py ....                                                                  [ 90%]
tests/test_version.py .                                                                        [ 90%]
tests/test_visitors.py ....                                                                    [ 94%]
tests/test_weakrefcallable.py ......                                                           [100%]

============================================== FAILURES ==============================================
...
========================================== warnings summary ==========================================
tests/test_sas.py::test_generator
tests/test_sas.py::testGenerator2
  /Users/zhimingxu/miniconda3/envs/srfit-env/lib/python3.13/site-packages/pyopencl/cache.py:496: CompilerWarning: Non-empty compiler output encountered. Set the environment variable PYOPENCL_COMPILER_OUTPUT=1 to see more.
    _create_built_program_from_source_cached(

tests/test_sas.py::test_generator
tests/test_sas.py::testGenerator2
  /Users/zhimingxu/miniconda3/envs/srfit-env/lib/python3.13/site-packages/pyopencl/cache.py:500: CompilerWarning: Non-empty compiler output encountered. Set the environment variable PYOPENCL_COMPILER_OUTPUT=1 to see more.
    prg.build(options_bytes, devices)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
====================================== short test summary info =======================================
FAILED tests/test_characteristicfunctions.py::testSphere - NotImplementedError: ER function is no longer available.
FAILED tests/test_characteristicfunctions.py::testSpheroid - NotImplementedError: ER function is no longer available.
FAILED tests/test_characteristicfunctions.py::testShell - NotImplementedError: ER function is no longer available.
FAILED tests/test_characteristicfunctions.py::testCylinder - NotImplementedError: ER function is no longer available.
======================== 4 failed, 84 passed, 23 skipped, 4 warnings in 1.98s ========================

I'll get to this issue later.

Copy link
Contributor Author

@zmx27 zmx27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see comments

@@ -22,7 +22,6 @@

from diffpy.srfit.exceptions import ParseError
from diffpy.srfit.fitbase.profileparser import ProfileParser
from diffpy.srfit.sas.sasimport import sasimport
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted the import from this file as not used but I haven't deleted sasimport yet because the sas package in sasview still exists, and we still need to import from it.

loader = Loader()

# Convert Path object to string if needed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load seems to expect a string now because it's calling .lower() on filename. In the source code, the traceback leads to the lookup() function in sasdata.data_util.registry, which calls:

path_lower = path.lower()

@@ -118,7 +122,16 @@ def parseFile(self, filename):
self._meta["filename"] = filename
self._meta["datainfo"] = data

self._banks.append([data.x, data.y, data.dx, data.dy])
# Handle case where loader returns a list of data objects
if isinstance(data, list):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loader returns a list now. From source code:

def load(self, file_path_list: Union[List[Union[str, Path]], str, Path],
             format: Optional[Union[List[str], str]] = None
             ) -> List[Union[Data1D, Data2D]]:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we presumably don't need the conditional then. If it returns a list then just treat a list. We don't have to backwards compatible because we are only supporting recent versions of all dependencies.

model = EllipsoidModel()
model.setParam("radius_a", prad)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find any mention of the use of radius_a and radius_b in the documentation for sasview, even ones that dated back to version 4.x (the latest release is version 6.1.0). I can only assume that suitable replacements are radius_polar and radius_equatorial, which are the parameters that the ellipsoid model now uses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EllipsoidModel must have a major and a minor axis, so these presumably refer to this? What are these axes called in EllipsoidModel?

@zmx27
Copy link
Contributor Author

zmx27 commented Jul 31, 2025

I looked into the source code for sas, and it seems like the calculate_ER function is indeed deprecated. Here is the function definition from sas.sascalc.calculator.BaseComponent:

def calculate_ER(self):
        """
        Calculate effective radius
        """
        return NotImplemented

Should I look into ways to somehow calculate the effective radius manually? How else should I modify the source code that the tests are testing? Or should I just deal with this in another PR?

@sbillinge
Copy link
Contributor

I looked into the source code for sas, and it seems like the calculate_ER function is indeed deprecated. Here is the function definition from sas.sascalc.calculator.BaseComponent:

def calculate_ER(self):
        """
        Calculate effective radius
        """
        return NotImplemented

Should I look into ways to somehow calculate the effective radius manually? How else should I modify the source code that the tests are testing? Or should I just deal with this in another PR?

this appears not to be deprecated, but it is not implemented. I will have to look at the test

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good work. Progress is being made. Please can you see my comments?


**Changed:**

* Refactored code utilizing sasmodels to use the new sasview api.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move to fixed. This is a bug fix not a change. Reserve changes for changes in behavior that a user might need to know about.


Loader = sasimport("sas.dataloader.loader").Loader
Loader = ld.Loader
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please could you make ld more readable. maybe sas_dataloader?

loader = Loader()

# Convert Path object to string if needed
if not isinstance(filename, str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just data = loader.load(str(filename)). I don't think we have to wrap this in a conditional.

@@ -118,7 +122,16 @@ def parseFile(self, filename):
self._meta["filename"] = filename
self._meta["datainfo"] = data

self._banks.append([data.x, data.y, data.dx, data.dy])
# Handle case where loader returns a list of data objects
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comment. Just make you code as readable as possible.

self._banks.append([data.x, data.y, data.dx, data.dy])
# Handle case where loader returns a list of data objects
if isinstance(data, list):
# If it's a list, iterate through each data object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove gratuitous comments

@@ -19,8 +19,10 @@
import numpy
import pytest

# Use the updated SasView model API to load models
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove comments

@@ -34,7 +36,7 @@ def testSphere(sas_available):
pytest.skip("sas package not available")
radius = 25
# Calculate sphere cf from SphereModel
SphereModel = sasimport("sas.models.SphereModel").SphereModel
SphereModel = _make_standard_model("sphere")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems a bit odd that we are importing a private function. Are we sure this is the way we are supposed to be using the API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep looking into this

model = EllipsoidModel()
model.setParam("radius_a", prad)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EllipsoidModel must have a major and a minor axis, so these presumably refer to this? What are these axes called in EllipsoidModel?

@zmx27
Copy link
Contributor Author

zmx27 commented Aug 1, 2025

@sbillinge I'm sorry, I couldn't figure out why the tests aren't passing with the default tolerance for test_generator2(). I initially thought it may be because the list item that the load() function returns could have multiple elements, but a IndexError occurs when I changed it to return index 1 of the list, telling me that there is only one item in the list. And how should I proceed regarding the calculate_ER() function? Should I attempt to try to make a function that calculates the effectively radius? Could you please look into these when you have time and give me some guidance? Or should some of this work be done in another PR? Thanks!

@sbillinge
Copy link
Contributor

@zmx27 I had a look at the sas models test and it is hard to figure out why it is off so much. I suggest that we

  1. make an issue to look at this again in a future release. It would require some forensics like plotting the data in .txt file and plotting the generated curve on top of each other to see what is wrong.
  2. leave the test as is in your dedit since it is now passing but add a comment line `# FIXME: go back to default tolerance when we figure out why the models are not identical"

There are a few other issues with the sasview integration (it is not working beyond python 3.11) and I am not sure who is using it, if anyone. So revisiting later seems like the best bet,

I will take a look at the characteristic function test that are failing now.....

@sbillinge
Copy link
Contributor

Should I look into ways to somehow calculate the effective radius manually? How else should I modify the source code that the tests are testing? Or should I just deal with this in another PR?

I don't see where this is being called. It seems that test_sphere and test_spheroid and so on are failing becaue they call something that calls something insde sasview. Sot I feel that it is something inside sasview that is failing. Since spherical and spheroidal models are really common, I imagine that we are trying to call them in a way that has been deprecated. Please could you try and read the sasview/sasmodels documentation and see if there are examples of how to call these models in the latest versions? Then we can try and figure out if we are calling them in the right way. If we find we are not and there is a new way to invoke them or they have a new name, for example, then this could be a quick fix. If we are calling them correctly, this will take a bit longer.

Let's give it one more go before we punt it to a later release, but we can do that if we have to

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zmx27 please see my inline comment.

@zmx27
Copy link
Contributor Author

zmx27 commented Aug 3, 2025

I don't see where this is being called. It seems that test_sphere and test_spheroid and so on are failing becaue they call something that calls something insde sasview. Sot I feel that it is something inside sasview that is failing. Since spherical and spheroidal models are really common, I imagine that we are trying to call them in a way that has been deprecated. Please could you try and read the sasview/sasmodels documentation and see if there are examples of how to call these models in the latest versions? Then we can try and figure out if we are calling them in the right way. If we find we are not and there is a new way to invoke them or they have a new name, for example, then this could be a quick fix. If we are calling them correctly, this will take a bit longer.

Sorry, I don't think I ever gave you a full traceback of the error. Here it is:

____________________________________________ testSpheroid ____________________________________________

sas_available = True

    def testSpheroid(sas_available):
        if not sas_available:
            pytest.skip("sas package not available")
        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)
              ^^^^^

tests/test_characteristicfunctions.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/diffpy/srfit/pdf/characteristicfunctions.py:419: in __call__
    ed = 2 * self._model.calculate_ER()
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <sasmodels.sasview_model.ellipsoid object at 0x10f14e270>, mode = 1

    def calculate_ER(self, mode=1):
        # type: (int) -> float
        """
        Calculate the effective radius for P(q)*S(q)

        *mode* is the R_eff type, which defaults to 1 to match the ER
        calculation for sasview models from version 3.x.

        :return: the value of the effective radius
        """
        # ER and VR are only needed for old multiplication models, based on
        # sas.sascalc.fit.MultiplicationModel.  Fail for now.  If we want to
        # continue supporting them then add some test cases so that the code
        # is exercised.  We can access ER/VR using the kernel Fq function by
        # extending _calculate_Iq so that it calls:
        #    if er_mode > 0:
        #        res = calculator.Fq(call_details, values, cutoff=self.cutoff,
        #                            magnetic=False, radius_effective_mode=mode)
        #        R_eff, form_shell_ratio = res[2], res[4]
        #        return R_eff, form_shell_ratio
        # Then use the following in calculate_ER:
        #    ER, VR = self._calculate_Iq(q=[0.1], er_mode=mode)
        #    return ER
        # Similarly, for calculate_VR:
        #    ER, VR = self._calculate_Iq(q=[0.1], er_mode=1)
        #    return VR
        # Obviously a combined calculate_ER_VR method would be better, but
        # we only need them to support very old models, so ignore the 2x
        # performance hit.
>       raise NotImplementedError("ER function is no longer available.")
E       NotImplementedError: ER function is no longer available.

../../miniconda3/envs/srfit-env/lib/python3.13/site-packages/sasmodels/sasview_model.py:780: NotImplementedError

From what I can tell from the documentation, it seems like the calculate_ER function was only used for a very old model. And it seems like the calculate_ER is the only problematic code. I didn't realize this before, but I noticed that the source code for sasmodels seem to give a suggestion for how we can access the ER without using this function, so maybe I will just follow that and try to implement that tomorrow. Also, sorry for the delayed responses! I had to deal with a family situation today

@sbillinge
Copy link
Contributor

Good, we are mkaing progress. this is tests working exactly as they are supposed to work. Remember, tests test behavior if they are done right. Here a dependency changed and the tests started failing. The reason the dependency changed in this case is not because they don't want to offer the same thing (functions that calculate characteristic functions for spherical particles) but because they want to change how they do that. We don't care how they do that, we just want the characteristic function of a spherical model. So our job is to figure out how they are handling that now (and they may have changed their API, they have apparently) and adapt all our code that uses this to make it work again.

This is a fun project and worth doing, but it could take some time. I suggest that we

  1. move this issue to the next release and
  2. for now skip the tests. We will need to put a permanent skip on the characteristic function tests, not just hte "conditional skip if sas is not available" skip.
  3. As sasview did for us, put some kind of NotImplementedError in the code for the sas characteristic functions that mention that as of release 3.2.0 these are not working but we hope to have them working again in a future release

Then we can safely move this off the current release milestone. Let's leave this PR open as it will be quite hlepful for that work later. but change the release milestone on the issue that this closes.

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see my comments.

@@ -415,6 +415,13 @@ def __call__(self, r):
#
# We also have to make a q-spacing small enough to compute out to at
# least the size of the signal.

# As of release 3.2.0, these are not working but we hope to have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would like this comment to be the NotImplemented error message. No user will see this comment.

I am not sure how it is used, but do we also want this behavior in __init__() constructor? Or is there some reason we may want it instantiated but not called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be any issues with instantiating the class so we don't need this error message in the __init__ constructor. The problematic line that results in a NotImplemented error is when the __call__ function calls

ed = 2 * self._model.calculate_ER()

The calculate_ER() function is not called in the __init__ function.

@@ -32,8 +32,7 @@


def testSphere(sas_available):
if not sas_available:
pytest.skip("sas package not available")
pytest.skip("calculate_ER() not available")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the wrong error message. I think the ER function is not the issue. Just say "sas characteristic functions not currently working. Remove skip when our code is refactored to use the latest sasview API" or sthg like that.

Also, let's keep the original skip but commented out. We will need it again when we implement the new code.

@@ -53,8 +52,7 @@ def testSphere(sas_available):


def testSpheroid(sas_available):
if not sas_available:
pytest.skip("sas package not available")
pytest.skip("calculate_ER() not available")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see above and handle all the models in the same way.

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see my comments and I can merge this.

@@ -415,6 +415,11 @@ def __call__(self, r):
#
# 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, these are not working but we hope to have "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change "these" to "SAS characteristic functions"


**Fixed:**

* Refactored code utilizing sasmodels to use the new sasview api.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's change this to "changed" as "temporarily removed support for SAS characteristic functions until we can migrate to the new sasview api"

@zmx27
Copy link
Contributor Author

zmx27 commented Aug 4, 2025

@sbillinge ready for review

@sbillinge
Copy link
Contributor

Everything looks good. Is there a reason it is marked as a draft? Or is it ready to merge?

@zmx27 zmx27 marked this pull request as ready for review August 4, 2025 21:43
@zmx27
Copy link
Contributor Author

zmx27 commented Aug 4, 2025

@sbillinge sorry, I forgot to mark this PR as ready for review. Also, it seems like CI is failing because sasmodels is not currently listed as a dependency in the requirements folder. Putting the load_standard_models() function in the top of the test files will also cause CI to fail because it internally issues a call to sasview, which is not available to be installed for python > 3.11. I've fixed these issues now, so this is ready for review.

Edit: Turns out I forgot to add sasmodels as a dependency for some reason… I’ll add it when I get back.

@sbillinge
Copy link
Contributor

@sbillinge sorry, I forgot to mark this PR as ready for review. Also, it seems like CI is failing because sasmodels is not currently listed as a dependency in the requirements folder. Putting the load_standard_models() function in the top of the test files will also cause CI to fail because it internally issues a call to sasview, which is not available to be installed for python > 3.11. I've fixed these issues now, so this is ready for review.

Edit: Turns out I forgot to add sasmodels as a dependency for some reason… I’ll add it when I get back.

we don't want to add that as a dependency if nothing is working with sas. Just add a skip if sasmodels is not installed for now.

@zmx27
Copy link
Contributor Author

zmx27 commented Aug 5, 2025

@sbillinge ready for review. I ended up adding each sasmodel import statement inside the test functions because commenting them out would result in unknown variables in load_standard_models and find_model, and pre-commit would fail.

Copy link

codecov bot commented Aug 5, 2025

Codecov Report

❌ Patch coverage is 14.70588% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.60%. Comparing base (b94a3a9) to head (29ef13e).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
tests/test_characteristicfunctions.py 22.22% 14 Missing ⚠️
tests/test_sas.py 0.00% 12 Missing ⚠️
tests/conftest.py 25.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #126      +/-   ##
==========================================
- Coverage   67.05%   66.60%   -0.46%     
==========================================
  Files          25       25              
  Lines        3160     3171      +11     
==========================================
- Hits         2119     2112       -7     
- Misses       1041     1059      +18     
Files with missing lines Coverage Δ
tests/conftest.py 86.66% <25.00%> (-2.10%) ⬇️
tests/test_sas.py 15.78% <0.00%> (-2.27%) ⬇️
tests/test_characteristicfunctions.py 12.26% <22.22%> (-5.22%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sbillinge
Copy link
Contributor

Thanks, that's a good fix. I thought about how to handle this later and we could, later, either require sasview etc. as dependencies, or we could do like matplotlib and have diffpy.srfit-base and diffpy.srfit where the former is the smallest set of inputs for it to work and the latter is the largest. We could also keep it small, but figure out a way to put a line CI run: command which is conda install sasview, sasmodels or sthg, so they are not specified as dependencies but they are tested by CI.

In general we want srfit to be extensible, so if someone comes up with a XANES module or a Raman module we could do joint XANES/PDF refinements etc. and so in the future this problem could get worse and worse and worse, which is why it might be nice to have ways of testing the extensions but not making the basic package more and more bloated.

@sbillinge sbillinge merged commit de32312 into diffpy:main Aug 5, 2025
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants