diff --git a/.codecov.yml b/.codecov.yml index 4af5eb24..1cdcf2e9 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,6 +5,9 @@ coverage: target: auto # use the coverage from the base commit, fail if coverage is lower threshold: 0% # allow the coverage to drop by + ignore: + - "tests/test_speed.py" # ignore performance testing in test coverage. + comment: layout: " diff, flags, files" behavior: default diff --git a/.github/ISSUE_TEMPLATE/release_checklist.md b/.github/ISSUE_TEMPLATE/release_checklist.md index 6107962c..56c5fca3 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.md +++ b/.github/ISSUE_TEMPLATE/release_checklist.md @@ -11,12 +11,13 @@ assignees: "" - [ ] All PRs/issues attached to the release are merged. - [ ] All the badges on the README are passing. - [ ] License information is verified as correct. If you are unsure, please comment below. -- [ ] Locally rendered documentation contains all appropriate pages, including API references (check no modules are - missing), tutorials, and other human-written text is up-to-date with any changes in the code. +- [ ] Locally rendered documentation contains all appropriate pages, tutorials, and other human-written text is up-to-date with any changes in the code. +- [ ] All API references are included. To check this, run `conda install scikit-package` and then `package build api-doc`. Review any edits made by rerendering the docs locally. - [ ] Installation instructions in the README, documentation, and the website are updated. - [ ] Successfully run any tutorial examples or do functional testing with the latest Python version. - [ ] Grammar and writing quality are checked (no typos). - [ ] Install `pip install build twine`, run `python -m build` and `twine check dist/*` to ensure that the package can be built and is correctly formatted for PyPI release. +- [ ] Dispatch matrix testing to test the release on all Python versions and systems. If you do not have permission to run this workflow, tag the maintainer and say `@maintainer, please dispatch matrix testing workflow`. Please tag the maintainer (e.g., @username) in the comment here when you are ready for the PyPI/GitHub release. Include any additional comments necessary, such as version information and details about the pre-release here: @@ -34,7 +35,7 @@ Please let the maintainer know that all checks are done and the package is ready - [ ] Ensure that the full release has appeared on PyPI successfully. -- [ ] New package dependencies listed in `conda.txt` and `test.txt` are added to `meta.yaml` in the feedstock. +- [ ] New package dependencies listed in `conda.txt` and `tests.txt` are added to `meta.yaml` in the feedstock. - [ ] Close any open issues on the feedstock. Reach out to the maintainer if you have questions. - [ ] Tag the maintainer for conda-forge release. diff --git a/.github/workflows/build-and-publish-docs-on-dispatch.yml b/.github/workflows/build-and-publish-docs-on-dispatch.yml new file mode 100644 index 00000000..3d7394d0 --- /dev/null +++ b/.github/workflows/build-and-publish-docs-on-dispatch.yml @@ -0,0 +1,18 @@ +name: Build and Publish Docs on Dispatch + +on: + workflow_dispatch: + +jobs: + get-python-version: + uses: scikit-package/release-scripts/.github/workflows/_get-python-version-latest.yml@v0 + with: + python_version: 0 + + docs: + uses: scikit-package/release-scripts/.github/workflows/_release-docs.yml@v0 + with: + project: diffpy.srfit + c_extension: false + headless: false + python_version: ${{ fromJSON(needs.get-python-version.outputs.latest_python_version) }} diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index 4d66f1b5..c5e92258 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -1,4 +1,4 @@ -name: Release (GitHub/PyPI) and Deploy Docs +name: Build Wheel, Release on GitHub/PyPI, and Deploy Docs on: workflow_dispatch: @@ -7,12 +7,12 @@ on: - "*" # Trigger on all tags initially, but tag and release privilege are verified in _build-wheel-release-upload.yml jobs: - release: + build-release: uses: scikit-package/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.srfit c_extension: false - maintainer_GITHUB_username: sbillinge + maintainer_GITHUB_username: cadenmyers13, sbillinge secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml b/.github/workflows/matrix-and-codecov.yml similarity index 84% rename from .github/workflows/matrix-and-codecov-on-merge-to-main.yml rename to .github/workflows/matrix-and-codecov.yml index 2bd09ede..b9a73c69 100644 --- a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml +++ b/.github/workflows/matrix-and-codecov.yml @@ -1,6 +1,9 @@ -name: CI +name: Matrix and Codecov on: + # push: + # branches: + # - main release: types: - prereleased diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e4a84d1..d118bebc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: submodules: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -21,46 +21,47 @@ repos: - id: check-toml - id: check-added-large-files - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 26.3.1 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 8.0.1 hooks: - id: isort args: ["--profile", "black"] - repo: https://github.com/kynan/nbstripout - rev: 0.7.1 + rev: 0.9.1 hooks: - id: nbstripout - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch name: Prevent Commit to Main Branch args: ["--branch", "main"] stages: [pre-commit] - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.2 hooks: - id: codespell additional_dependencies: - tomli # prettier - multi formatter for .json, .yml, and .md files - repo: https://github.com/pre-commit/mirrors-prettier - rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8 + rev: v4.0.0-alpha.8 hooks: - id: prettier additional_dependencies: - "prettier@^3.2.4" # docformatter - PEP 257 compliant docstring formatter - - repo: https://github.com/s-weigand/docformatter - rev: 5757c5190d95e5449f102ace83df92e7d3b06c6c + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 hooks: - id: docformatter + language_version: python3.13 additional_dependencies: [tomli] args: [--in-place, --config, ./pyproject.toml] diff --git a/AUTHORS.rst b/AUTHORS.rst index b2335043..68e2f2d8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,7 +1,7 @@ Authors ======= -Christopher Farrow, Pavol Juhas, Simon J. L. Billinge, and members of the Billinge Group +Christopher Farrow, Pavol Juhas, Caden Myers, Simon J. L. Billinge, and members of the Billinge Group Contributors ------------ diff --git a/CODE-OF-CONDUCT.rst b/CODE-OF-CONDUCT.rst index e8199ca5..9352c6ec 100644 --- a/CODE-OF-CONDUCT.rst +++ b/CODE-OF-CONDUCT.rst @@ -67,7 +67,7 @@ Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -sb2896@columbia.edu. All complaints will be reviewed and investigated promptly and fairly. +cjm2304@columbia.edu and sbillinge@ucsb.edu. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. diff --git a/LICENSE.rst b/LICENSE.rst index acbe6078..6d222385 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -9,7 +9,7 @@ OPEN SOURCE LICENSE AGREEMENT - Copyright (c) 2008-2012, The Trustees of Columbia University in the City of New York - Copyright (c) 2014-2019, Brookhaven Science Associates, Brookhaven National Laboratory - Copyright (c) 2020-2025, The Trustees of Columbia University in the City of New York - +- Copyright (c) 2026-present, The DiffPy Team. The "DiffPy-CMI" is distributed subject to the following license conditions: diff --git a/MANIFEST.in b/MANIFEST.in index b31fb162..c1ccccac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,4 +11,3 @@ global-exclude __pycache__ # Exclude Python cache directories. global-exclude .git* # Exclude git files and directories. global-exclude .idea # Exclude PyCharm project settings. exclude .codecov.yml -exclude .coveragerc diff --git a/README.rst b/README.rst index 2d4040ec..be8a91e4 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,7 @@ :target: https://anaconda.org/conda-forge/diffpy.srfit .. |PR| image:: https://img.shields.io/badge/PR-Welcome-29ab47ff + :target: https://github.com/diffpy/diffpy.srfit/pulls .. |PyPI| image:: https://img.shields.io/pypi/v/diffpy.srfit :target: https://pypi.org/project/diffpy.srfit/ @@ -38,7 +39,7 @@ diffpy.srfit ============ -Configurable code for solving atomic structures. +Generalized code base for modeling problems. The diffpy.srfit package provides the framework for building a global optimizer on the fly from components such as function calculators (that calculate @@ -62,6 +63,23 @@ obtain the total cost function. Additionally, diffpy.srfit is designed to be extensible, allowing the user to integrate external calculators to perform co-refinements with other techniques. + +SrFit has tools for coherently combining known information about a +material to derive other properties, in particular material structure. +SrFit allows the customization and creation of structure +representations, profile calculators, constraints, restraints and file +input parsers. The customized pieces can be glued together within SrFit +to optimize a structure, or other physically relevant information from +one or more experimental profiles. Other known information about the +system of interest can be included with arbitrarily complex constraints +and restraints. In this way, the end user creates a customized fitting +application that suits the problem to the available information. + +The subpackages herein define various pieces of the SrFit framework. +Developers are encouraged to work through the examples described in the +documentation to learn how to use and customize the various parts of +SrFit. + For more information about the diffpy.srfit library, please consult our `online documentation `_. Citation @@ -94,10 +112,6 @@ The following creates and activates a new environment named ``diffpy.srfit_env`` conda create -n diffpy.srfit_env diffpy.srfit conda activate diffpy.srfit_env -To confirm that the installation was successful, type :: - - python -c "import diffpy.srfit; print(diffpy.srfit.__version__)" - The output should print the latest version displayed on the badges above. This will install the minimal `diffpy.srfit` installation. It will often be used @@ -136,6 +150,19 @@ and run the following :: pip install . +This package also provides command-line utilities. To check the software has been installed correctly, type :: + + diffpy.srfit --version + +You can also type the following command to verify the installation. :: + + python -c "import diffpy.srfit; print(diffpy.srfit.__version__)" + + +To view the basic usage and available commands, type :: + + diffpy.srfit -h + Getting Started --------------- @@ -167,12 +194,12 @@ trying to commit again. Improvements and fixes are always appreciated. -Before contributing, please read our `Code of Conduct `_. +Before contributing, please read our `Code of Conduct `_. Contact ------- -For more information on diffpy.srfit please visit the project `web-page `_ or email Simon Billinge at sb2896@columbia.edu. +For more information on diffpy.srfit please visit the project `web-page `_ or email the maintainers ``Caden Myers (cjm2304@columbia.edu) and Simon J. L. Billinge (sbillinge@ucsb.edu)``. Acknowledgements ---------------- diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 00000000..46da0e2e --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,20 @@ +{ + "author_names": "Christopher Farrow, Pavol Juhas, Caden Myers, Simon J. L. Billinge", + "author_emails": "farrowch@gmail.com, pavol.juhas@gmail.com, cjm2304@columbia.edu, sbillinge@ucsb.edu", + "maintainer_names": "Caden Myers, Simon J. L. Billinge", + "maintainer_emails": "cjm2304@columbia.edu, sbillinge@ucsb.edu", + "maintainer_github_usernames": "cadenmyers13, sbillinge", + "contributors": "Christopher Farrow, Pavol Juhas, Caden Myers, Simon J. L. Billinge, and members of the DiffPy community.", + "license_holders": "The DiffPy Team", + "project_name": "diffpy.srfit", + "github_username_or_orgname": "diffpy", + "github_repo_name": "diffpy.srfit", + "conda_pypi_package_dist_name": "diffpy.srfit", + "package_dir_name": "diffpy.srfit", + "project_short_description": "Generalized code base for modeling problems.", + "project_keywords": "regression, modeling, fitting, diffraction, PDF", + "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/docs/examples/coreshellnp.py b/docs/examples/coreshellnp.py index 5b8c6eb2..8560221d 100644 --- a/docs/examples/coreshellnp.py +++ b/docs/examples/coreshellnp.py @@ -29,8 +29,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator # Example Code @@ -42,10 +43,10 @@ def makeRecipe(stru1, stru2, datname): profile = Profile() # Load data and add it to the profile - parser = PDFParser() + parser = ProfileParser() parser.parseFile(datname) - profile.loadParsedData(parser) - profile.setCalculationRange(xmin=1.5, xmax=45, dx=0.1) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmin=1.5, xmax=45, dx=0.1) # The ProfileGenerator # In order to fit the core and shell phases simultaneously, we must use two @@ -66,8 +67,8 @@ def makeRecipe(stru1, stru2, datname): # The FitContribution # Add both generators and the profile to the FitContribution. contribution = FitContribution("cdszns") - contribution.addProfileGenerator(generator_cds) - contribution.addProfileGenerator(generator_zns) + contribution.add_profile_generator(generator_cds) + contribution.add_profile_generator(generator_zns) contribution.set_profile(profile, xname="r") # Set up the characteristic functions. We use a spherical CF for the core @@ -76,56 +77,60 @@ def makeRecipe(stru1, stru2, datname): # very little to the PDF. from diffpy.srfit.pdf.characteristicfunctions import shellCF, sphericalCF - contribution.registerFunction(sphericalCF, name="f_CdS") - contribution.registerFunction(shellCF, name="f_ZnS") + contribution.register_function(sphericalCF, name="f_CdS") + contribution.register_function(shellCF, name="f_ZnS") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. - contribution.setEquation("scale * (f_CdS * G_CdS + f_ZnS * G_ZnS)") + contribution.set_equation("scale * (f_CdS * G_CdS + f_ZnS * G_ZnS)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Vary the inner radius and thickness of the shell. Constrain the core # diameter to twice the shell radius. - recipe.addVar(contribution.radius, 15) - recipe.addVar(contribution.thickness, 11) - recipe.constrain(contribution.psize, "2 * radius") + recipe.add_variable(contribution.radius, 15) + recipe.add_variable(contribution.thickness, 11) + recipe.add_constraint(contribution.psize, "2 * radius") # Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. - recipe.newVar("scale_CdS", 0.7) - recipe.constrain(generator_cds.scale, "scale_CdS") - recipe.constrain(generator_zns.scale, "1 - scale_CdS") + recipe.create_new_variable("scale_CdS", 0.7) + recipe.add_constraint(generator_cds.scale, "scale_CdS") + recipe.add_constraint(generator_zns.scale, "1 - scale_CdS") # We also want the resolution factor to be the same on each. # Vary the global scale as well. - recipe.addVar(contribution.scale, 0.3) + recipe.add_variable(contribution.scale, 0.3) # Now we can configure the structural parameters. We tag the different # structural variables so we can easily turn them on and off in the # subsequent refinement. phase_cds = generator_cds.phase for par in phase_cds.sgpars.latpars: - recipe.addVar(par, name=par.name + "_cds", tag="lat") + recipe.add_variable(par, name=par.name + "_cds", tag="lat") for par in phase_cds.sgpars.adppars: - recipe.addVar(par, 1, name=par.name + "_cds", tag="adp") - recipe.addVar(phase_cds.sgpars.xyzpars.z_1, name="z_1_cds", tag="xyz") + recipe.add_variable(par, 1, name=par.name + "_cds", tag="adp") + recipe.add_variable( + phase_cds.sgpars.xyzpars.z_1, name="z_1_cds", tag="xyz" + ) # Since we know these have stacking disorder, constrain the B33 adps for # each atom type. - recipe.constrain("B33_1_cds", "B33_0_cds") - recipe.addVar(generator_cds.delta2, name="delta2_cds", value=5) + recipe.add_constraint("B33_1_cds", "B33_0_cds") + recipe.add_variable(generator_cds.delta2, name="delta2_cds", value=5) phase_zns = generator_zns.phase for par in phase_zns.sgpars.latpars: - recipe.addVar(par, name=par.name + "_zns", tag="lat") + recipe.add_variable(par, name=par.name + "_zns", tag="lat") for par in phase_zns.sgpars.adppars: - recipe.addVar(par, 1, name=par.name + "_zns", tag="adp") - recipe.addVar(phase_zns.sgpars.xyzpars.z_1, name="z_1_zns", tag="xyz") - recipe.constrain("B33_1_zns", "B33_0_zns") - recipe.addVar(generator_zns.delta2, name="delta2_zns", value=2.5) + recipe.add_variable(par, 1, name=par.name + "_zns", tag="adp") + recipe.add_variable( + phase_zns.sgpars.xyzpars.z_1, name="z_1_zns", tag="xyz" + ) + recipe.add_constraint("B33_1_zns", "B33_0_zns") + recipe.add_variable(generator_zns.delta2, name="delta2_zns", value=2.5) # Give the recipe away so it can be used! return recipe @@ -169,7 +174,7 @@ def main(): recipe = makeRecipe(stru1, stru2, data) from diffpy.srfit.fitbase.fithook import PlotFitHook - recipe.pushFitHook(PlotFitHook()) + recipe.push_fit_hook(PlotFitHook()) recipe.fithooks[0].verbose = 3 # Optimize - we do this in steps to help convergence @@ -202,7 +207,7 @@ def main(): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/crystalpdf.py b/docs/examples/crystalpdf.py index 835eb977..cdd3472e 100644 --- a/docs/examples/crystalpdf.py +++ b/docs/examples/crystalpdf.py @@ -32,8 +32,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator from diffpy.structure import Structure ###### @@ -48,21 +49,22 @@ def makeRecipe(ciffile, datname): profile = Profile() # Load data and add it to the Profile. Unlike in other examples, we use a - # class (PDFParser) to help us load the data. This class will read the data - # and relevant metadata from a two- to four-column data file generated + # class (ProfileParser) to help us load the data. This class will read the + # data and relevant metadata from a two- to four-column data file generated # with PDFGetX2 or PDFGetN. The metadata will be passed to the PDFGenerator # when they are associated in the FitContribution, which saves some # configuration steps. - parser = PDFParser() + parser = ProfileParser() parser.parseFile(datname) - profile.loadParsedData(parser) - profile.setCalculationRange(xmax=20) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmax=20) # The ProfileGenerator # The PDFGenerator is for configuring and calculating a PDF profile. Here, # we want to refine a Structure object from diffpy.structure. We tell the # PDFGenerator that with the 'setStructure' method. All other configuration - # options will be inferred from the metadata that is read by the PDFParser. + # options will be inferred from the metadata that is read by the + # ProfileParser. # In particular, this will set the scattering type (x-ray or neutron), the # Qmax value, as well as initial values for the non-structural Parameters. generator = PDFGenerator("G") @@ -74,12 +76,12 @@ def makeRecipe(ciffile, datname): # Here we associate the Profile and ProfileGenerator, as has been done # before. contribution = FitContribution("nickel") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="r") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables @@ -113,17 +115,17 @@ def makeRecipe(ciffile, datname): # the xyzpars, latpars, and adppars members of the SpaceGroupParameters # object. for par in sgpars.latpars: - recipe.addVar(par) + recipe.add_variable(par) for par in sgpars.adppars: - recipe.addVar(par, 0.005) + recipe.add_variable(par, 0.005) # We now select non-structural parameters to refine. # This controls the scaling of the PDF. - recipe.addVar(generator.scale, 1) + recipe.add_variable(generator.scale, 1) # This is a peak-damping resolution term. - recipe.addVar(generator.qdamp, 0.01) + recipe.add_variable(generator.qdamp, 0.01) # This is a vibrational correlation term that sharpens peaks at low-r. - recipe.addVar(generator.delta2, 5) + recipe.add_variable(generator.delta2, 5) # Give the recipe away so it can be used! return recipe @@ -168,7 +170,7 @@ def plotResults(recipe): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/crystalpdfall.py b/docs/examples/crystalpdfall.py index 8926952a..37a57c78 100644 --- a/docs/examples/crystalpdfall.py +++ b/docs/examples/crystalpdfall.py @@ -27,8 +27,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator ###### # Example Code @@ -37,17 +38,17 @@ def makeProfile(datafile): """Make an place data within a Profile.""" profile = Profile() - parser = PDFParser() + parser = ProfileParser() parser.parseFile(datafile) - profile.loadParsedData(parser) - profile.setCalculationRange(xmax=20) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmax=20) return profile def makeContribution(name, generator, profile): """Make a FitContribution and add a generator and profile.""" contribution = FitContribution(name) - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="r") return contribution @@ -93,44 +94,44 @@ def makeRecipe( xcontribution_sini = makeContribution( "xsini", xgenerator_sini_ni, xprofile_sini ) - xcontribution_sini.addProfileGenerator(xgenerator_sini_si) - xcontribution_sini.setEquation("scale * (xG_sini_ni + xG_sini_si)") + xcontribution_sini.add_profile_generator(xgenerator_sini_si) + xcontribution_sini.set_equation("scale * (xG_sini_ni + xG_sini_si)") # As explained in another example, we want to minimize using Rw^2. - xcontribution_ni.setResidualEquation("resv") - xcontribution_si.setResidualEquation("resv") - ncontribution_ni.setResidualEquation("resv") - xcontribution_sini.setResidualEquation("resv") + xcontribution_ni.set_residual_equation("resv") + xcontribution_si.set_residual_equation("resv") + ncontribution_ni.set_residual_equation("resv") + xcontribution_sini.set_residual_equation("resv") # Make the FitRecipe and add the FitContributions. recipe = FitRecipe() - recipe.addContribution(xcontribution_ni) - recipe.addContribution(xcontribution_si) - recipe.addContribution(ncontribution_ni) - recipe.addContribution(xcontribution_sini) + recipe.add_contribution(xcontribution_ni) + recipe.add_contribution(xcontribution_si) + recipe.add_contribution(ncontribution_ni) + recipe.add_contribution(xcontribution_sini) # Now we vary and constrain Parameters as before. for par in phase_ni.sgpars: - recipe.addVar(par, name=par.name + "_ni") - delta2_ni = recipe.newVar("delta2_ni", 2.5) - recipe.constrain(xgenerator_ni.delta2, delta2_ni) - recipe.constrain(ngenerator_ni.delta2, delta2_ni) - recipe.constrain(xgenerator_sini_ni.delta2, delta2_ni) + recipe.add_variable(par, name=par.name + "_ni") + delta2_ni = recipe.create_new_variable("delta2_ni", 2.5) + recipe.add_constraint(xgenerator_ni.delta2, delta2_ni) + recipe.add_constraint(ngenerator_ni.delta2, delta2_ni) + recipe.add_constraint(xgenerator_sini_ni.delta2, delta2_ni) for par in phase_si.sgpars: - recipe.addVar(par, name=par.name + "_si") - delta2_si = recipe.newVar("delta2_si", 2.5) - recipe.constrain(xgenerator_si.delta2, delta2_si) - recipe.constrain(xgenerator_sini_si.delta2, delta2_si) + recipe.add_variable(par, name=par.name + "_si") + delta2_si = recipe.create_new_variable("delta2_si", 2.5) + recipe.add_constraint(xgenerator_si.delta2, delta2_si) + recipe.add_constraint(xgenerator_sini_si.delta2, delta2_si) # Now the experimental parameters - recipe.addVar(xgenerator_ni.scale, name="xscale_ni") - recipe.addVar(xgenerator_si.scale, name="xscale_si") - recipe.addVar(ngenerator_ni.scale, name="nscale_ni") - recipe.addVar(xcontribution_sini.scale, 1.0, "xscale_sini") - recipe.newVar("pscale_sini_ni", 0.8) - recipe.constrain(xgenerator_sini_ni.scale, "pscale_sini_ni") - recipe.constrain(xgenerator_sini_si.scale, "1 - pscale_sini_ni") + recipe.add_variable(xgenerator_ni.scale, name="xscale_ni") + recipe.add_variable(xgenerator_si.scale, name="xscale_si") + recipe.add_variable(ngenerator_ni.scale, name="nscale_ni") + recipe.add_variable(xcontribution_sini.scale, 1.0, "xscale_sini") + recipe.create_new_variable("pscale_sini_ni", 0.8) + recipe.add_constraint(xgenerator_sini_ni.scale, "pscale_sini_ni") + recipe.add_constraint(xgenerator_sini_si.scale, "1 - pscale_sini_ni") # The qdamp parameters are too correlated to vary so we fix them based on # previous measurements. @@ -232,7 +233,7 @@ def plotResults(recipe): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/crystalpdfobjcryst.py b/docs/examples/crystalpdfobjcryst.py index d28af6a3..43a98e4d 100644 --- a/docs/examples/crystalpdfobjcryst.py +++ b/docs/examples/crystalpdfobjcryst.py @@ -28,8 +28,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator ###### # Example Code @@ -42,14 +43,14 @@ def makeRecipe(ciffile, datname): # This will be used to store the observed and calculated PDF profile. profile = Profile() - # Load data and add it to the Profile. As before we use a PDFParser. The - # metadata is still passed to the PDFGenerator later on. The interaction - # between the PDFGenerator and the metadata does not depend on type of - # structure being refined. - parser = PDFParser() + # Load data and add it to the Profile. As before we use a ProfileParser. + # The metadata is still passed to the PDFGenerator later on. + # The interaction between the PDFGenerator and the metadata does not + # depend on type of structure being refined. + parser = ProfileParser() parser.parseFile(datname) - profile.loadParsedData(parser) - profile.setCalculationRange(xmax=20) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmax=20) # The ProfileGenerator # This time we use the CreateCrystalFromCIF method of pyobjcryst.crystal to @@ -62,12 +63,12 @@ def makeRecipe(ciffile, datname): # The FitContribution contribution = FitContribution("nickel") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="r") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables @@ -90,18 +91,18 @@ def makeRecipe(ciffile, datname): # As before, we have one free lattice parameter ('a'). We can simplify # things by iterating through all the sgpars. for par in phase.sgpars: - recipe.addVar(par) + recipe.add_variable(par) # set the initial thermal factor to a non-zero value assert hasattr(recipe, "B11_0") recipe.B11_0 = 0.1 # We now select non-structural parameters to refine. # This controls the scaling of the PDF. - recipe.addVar(generator.scale, 1) + recipe.add_variable(generator.scale, 1) # This is a peak-damping resolution term. - recipe.addVar(generator.qdamp, 0.01) + recipe.add_variable(generator.qdamp, 0.01) # This is a vibrational correlation term that sharpens peaks at low-r. - recipe.addVar(generator.delta2, 5) + recipe.add_variable(generator.delta2, 5) # Give the recipe away so it can be used! return recipe @@ -121,7 +122,7 @@ def makeRecipe(ciffile, datname): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/crystalpdftwodata.py b/docs/examples/crystalpdftwodata.py index b13da27f..03458fa8 100644 --- a/docs/examples/crystalpdftwodata.py +++ b/docs/examples/crystalpdftwodata.py @@ -29,8 +29,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator ###### # Example Code @@ -46,15 +47,15 @@ def makeRecipe(ciffile, xdatname, ndatname): nprofile = Profile() # Load data and add it to the proper Profile. - parser = PDFParser() + parser = ProfileParser() parser.parseFile(xdatname) - xprofile.loadParsedData(parser) - xprofile.setCalculationRange(xmax=20) + xprofile.load_parsed_data(parser) + xprofile.set_calculation_range(xmax=20) - parser = PDFParser() + parser = ProfileParser() parser.parseFile(ndatname) - nprofile.loadParsedData(parser) - nprofile.setCalculationRange(xmax=20) + nprofile.load_parsed_data(parser) + nprofile.set_calculation_range(xmax=20) # The ProfileGenerators # We need one of these for the x-ray data. @@ -84,11 +85,11 @@ def makeRecipe(ciffile, xdatname, ndatname): # The FitContributions # We associate the x-ray PDFGenerator and Profile in one FitContribution... xcontribution = FitContribution("xnickel") - xcontribution.addProfileGenerator(xgenerator) + xcontribution.add_profile_generator(xgenerator) xcontribution.set_profile(xprofile, xname="r") # and the neutron objects in another. ncontribution = FitContribution("nnickel") - ncontribution.addProfileGenerator(ngenerator) + ncontribution.add_profile_generator(ngenerator) ncontribution.set_profile(nprofile, xname="r") # This example is different than the previous ones in that we are composing @@ -105,30 +106,30 @@ def makeRecipe(ciffile, xdatname, ndatname): # The contribution's residual can be either chi^2, Rw^2, or custom crafted. # In this case, we should minimize Rw^2 of each contribution so that each # one can contribute roughly equally to the fit. - xcontribution.setResidualEquation("resv") - ncontribution.setResidualEquation("resv") + xcontribution.set_residual_equation("resv") + ncontribution.set_residual_equation("resv") # Make the FitRecipe and add the FitContributions. recipe = FitRecipe() - recipe.addContribution(xcontribution) - recipe.addContribution(ncontribution) + recipe.add_contribution(xcontribution) + recipe.add_contribution(ncontribution) # Now we vary and constrain Parameters as before. - recipe.addVar(xgenerator.scale, 1, "xscale") - recipe.addVar(ngenerator.scale, 1, "nscale") - recipe.addVar(xgenerator.qdamp, 0.01, "xqdamp") - recipe.addVar(ngenerator.qdamp, 0.01, "nqdamp") + recipe.add_variable(xgenerator.scale, 1, "xscale") + recipe.add_variable(ngenerator.scale, 1, "nscale") + recipe.add_variable(xgenerator.qdamp, 0.01, "xqdamp") + recipe.add_variable(ngenerator.qdamp, 0.01, "nqdamp") # delta2 is a non-structual material property. Thus, we constrain together # delta2 Parameter from each PDFGenerator. - delta2 = recipe.newVar("delta2", 2) - recipe.constrain(xgenerator.delta2, delta2) - recipe.constrain(ngenerator.delta2, delta2) + delta2 = recipe.create_new_variable("delta2", 2) + recipe.add_constraint(xgenerator.delta2, delta2) + recipe.add_constraint(ngenerator.delta2, delta2) # We only need to constrain phase properties once since there is a single # ObjCrystCrystalParSet for the Crystal. phase = xgenerator.phase for par in phase.sgpars: - recipe.addVar(par) + recipe.add_variable(par) recipe.B11_0 = 0.1 # Give the recipe away so it can be used! @@ -188,7 +189,7 @@ def plotResults(recipe): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/crystalpdftwophase.py b/docs/examples/crystalpdftwophase.py index 95e659b7..c2dbcf78 100644 --- a/docs/examples/crystalpdftwophase.py +++ b/docs/examples/crystalpdftwophase.py @@ -29,8 +29,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator ###### # Example Code @@ -43,10 +44,10 @@ def makeRecipe(niciffile, siciffile, datname): profile = Profile() # Load data and add it to the profile - parser = PDFParser() + parser = ProfileParser() parser.parseFile(datname) - profile.loadParsedData(parser) - profile.setCalculationRange(xmax=20) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmax=20) # The ProfileGenerator # In order to fit two phases simultaneously, we must use two PDFGenerators. @@ -72,33 +73,33 @@ def makeRecipe(niciffile, siciffile, datname): # Add both generators to the FitContribution. Add the Profile. This will # send the metadata to the generators. contribution = FitContribution("nisi") - contribution.addProfileGenerator(generator_ni) - contribution.addProfileGenerator(generator_si) + contribution.add_profile_generator(generator_ni) + contribution.add_profile_generator(generator_si) contribution.set_profile(profile, xname="r") # Write the fitting equation. We want to sum the PDFs from each phase and # multiply it by a scaling factor. We also want a certain phase scaling # relationship between the PDFs which we will enforce with constraints in # the FitRecipe. - contribution.setEquation("scale * (G_ni + G_si)") + contribution.set_equation("scale * (G_ni + G_si)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. - recipe.newVar("scale_ni", 0.1) - recipe.constrain(generator_ni.scale, "scale_ni") - recipe.constrain(generator_si.scale, "1 - scale_ni") + recipe.create_new_variable("scale_ni", 0.1) + recipe.add_constraint(generator_ni.scale, "scale_ni") + recipe.add_constraint(generator_si.scale, "1 - scale_ni") # We also want the resolution factor to be the same on each. - recipe.newVar("qdamp", 0.03) - recipe.constrain(generator_ni.qdamp, "qdamp") - recipe.constrain(generator_si.qdamp, "qdamp") + recipe.create_new_variable("qdamp", 0.03) + recipe.add_constraint(generator_ni.qdamp, "qdamp") + recipe.add_constraint(generator_si.qdamp, "qdamp") # Vary the global scale as well. - recipe.addVar(contribution.scale, 1) + recipe.add_variable(contribution.scale, 1) # Now we can configure the structural parameters. Since we're using # ObjCrystCrystalParSets, the space group constraints are automatically @@ -107,13 +108,13 @@ def makeRecipe(niciffile, siciffile, datname): # First the nickel parameters phase_ni = generator_ni.phase for par in phase_ni.sgpars: - recipe.addVar(par, name=par.name + "_ni") - recipe.addVar(generator_ni.delta2, name="delta2_ni") + recipe.add_variable(par, name=par.name + "_ni") + recipe.add_variable(generator_ni.delta2, name="delta2_ni") # Next the silicon parameters phase_si = generator_si.phase for par in phase_si.sgpars: - recipe.addVar(par, name=par.name + "_si") - recipe.addVar(generator_si.delta2, name="delta2_si") + recipe.add_variable(par, name=par.name + "_si") + recipe.add_variable(generator_si.delta2, name="delta2_si") # We have prior information from the earlier examples so we'll use it here # in the form of restraints. @@ -123,18 +124,30 @@ def makeRecipe(niciffile, siciffile, datname): # derived has no uncertainty. Thus, we will tell the recipe to scale the # residual, which means that it will be weighted as much as the average # data point during the fit. - recipe.restrain("a_ni", lb=3.527, ub=3.527, scaled=True) + recipe.add_soft_bounds( + "a_ni", lower_bound=3.527, upper_bound=3.527, scaled=True + ) # Now we do the same with the delta2 and Biso parameters (remember that # Biso = 8*pi**2*Uiso) - recipe.restrain("delta2_ni", lb=2.22, ub=2.22, scaled=True) - recipe.restrain("Biso_0_ni", lb=0.454, ub=0.454, scaled=True) + recipe.add_soft_bounds( + "delta2_ni", lower_bound=2.22, upper_bound=2.22, scaled=True + ) + recipe.add_soft_bounds( + "Biso_0_ni", lower_bound=0.454, upper_bound=0.454, scaled=True + ) # # We can do the same with the silicon values. We haven't done a thorough # job of measuring the uncertainties in the results, so we'll scale these # as well. - recipe.restrain("a_si", lb=5.430, ub=5.430, scaled=True) - recipe.restrain("delta2_si", lb=3.54, ub=3.54, scaled=True) - recipe.restrain("Biso_0_si", lb=0.645, ub=0.645, scaled=True) + recipe.add_soft_bounds( + "a_si", lower_bound=5.430, upper_bound=5.430, scaled=True + ) + recipe.add_soft_bounds( + "delta2_si", lower_bound=3.54, upper_bound=3.54, scaled=True + ) + recipe.add_soft_bounds( + "Biso_0_si", lower_bound=0.645, upper_bound=0.645, scaled=True + ) # Give the recipe away so it can be used! return recipe @@ -179,7 +192,7 @@ def plotResults(recipe): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/debyemodel.py b/docs/examples/debyemodel.py index 6abbb04c..7813be4e 100644 --- a/docs/examples/debyemodel.py +++ b/docs/examples/debyemodel.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ######################################################################## -"""Example of fitting the Debye model to experimental Debye-Waller factors. +"""Example of fitting the Debye model to experimental Debye-Waller +factors. In this example, we build a fit recipe that uses an external function that can simulate a atomic displacement parameters using the Debye model. This serves as @@ -93,7 +94,7 @@ def makeRecipe(): # data into the profile. xydy = numpy.array(data.split(), dtype=float).reshape(-1, 3) x, y, dy = xydy.T - profile.setObservedProfile(x, y, dy) + profile.set_observed_profile(x, y, dy) # The FitContribution # The FitContribution associates the profile with the Debye function. @@ -105,14 +106,14 @@ def makeRecipe(): contribution.set_profile(profile, xname="T") # We now need to create the fitting equation. We tell the FitContribution - # to use the 'debye' function defined below. The 'registerFunction' method + # to use the 'debye' function defined below. The 'register_function' method # will let us do this. Since we haven't told it otherwise, - # 'registerFunction' will extract the name of the function ('debye') and + # 'register_function' will extract the name of the function ('debye') and # the names of the arguments ('T', 'm', 'thetaD'). These arguments will # become Parameters of the FitContribution. Since we named the x-variable # 'T' above, the 'T' in the 'debye' equation will refer to this x-variable # whenever it is used. - contribution.registerFunction(debye) + contribution.register_function(debye) # Now we can create the fitting equation. We want to extend the 'debye' # equation by adding a vertical offset. We could wrap 'debye' in a new @@ -126,7 +127,7 @@ def makeRecipe(): # the debye equation to be positive, so we specify the input as abs(thetaD) # in the equation below. Furthermore, we know 'm', the mass of lead, so we # can specify that as well. - contribution.setEquation("debye(T, 207.2, abs(thetaD)) + offset") + contribution.set_equation("debye(T, 207.2, abs(thetaD)) + offset") # The FitRecipe # The FitRecipe lets us define what we want to fit. It is where we can @@ -134,14 +135,14 @@ def makeRecipe(): # to fit simultaneously, the contribution from each could be added to the # recipe. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Specify which Parameters we want to refine. # Vary the offset - recipe.addVar(contribution.offset, 0) + recipe.add_variable(contribution.offset, 0) # We also vary the Debye temperature. - recipe.addVar(contribution.thetaD, 100) + recipe.add_variable(contribution.thetaD, 100) # We would like to 'suggest' that the offset should remain positive. This # is somethine that we know about the system that might help the refinement @@ -151,7 +152,7 @@ def makeRecipe(): # breaking the restraint by the point-average chi^2 value so that the # restraint is roughly as significant as any other data point throughout # the fit. - recipe.restrain(recipe.offset, lb=0, scaled=True) + recipe.add_soft_bounds(recipe.offset, lower_bound=0, scaled=True) # We're done setting up the recipe. We can now do other things with it. return recipe @@ -193,7 +194,7 @@ def main(): res = FitResults(recipe) # Print the results - res.printResults() + res.print_results() # Plot the results plotResults(recipe) @@ -207,7 +208,8 @@ def main(): def debye(T, m, thetaD): - """A wrapped version of 'adps' that can handle an array of T-values.""" + """A wrapped version of 'adps' that can handle an array of + T-values.""" y = numpy.array([adps(m, thetaD, x) for x in T]) return y diff --git a/docs/examples/debyemodelII.py b/docs/examples/debyemodelII.py index 9872f71d..a79df5f6 100644 --- a/docs/examples/debyemodelII.py +++ b/docs/examples/debyemodelII.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ######################################################################## -"""Example of fitting the Debye recipe to experimental Debye-Waller factors. +"""Example of fitting the Debye recipe to experimental Debye-Waller +factors. This is an extension of example in debyemodel.py. The recipe we create will simultaneously fit the low and high temperature parts of the experimental data @@ -71,25 +72,25 @@ def makeRecipeII(): # Now create a fresh FitRecipe to work with and add to it the two # FitContributions. recipe = FitRecipe() - recipe.addContribution(lowT) - recipe.addContribution(highT) + recipe.add_contribution(lowT) + recipe.add_contribution(highT) # Change the fit ranges of the Profiles embedded within the # FitContributions. We want to fit one of the contributions at low # temperature, and one at high. - lowT.profile.setCalculationRange(0, 150) - highT.profile.setCalculationRange(400, 500) + lowT.profile.set_calculation_range(0, 150) + highT.profile.set_calculation_range(400, 500) # Vary the offset from each FitContribution separately, while keeping the # Debye temperatures the same. We give each offset variable a different # name in the recipe so it retains its identity. - recipe.addVar(recipe.lowT.offset, name="lowToffset") - recipe.addVar(recipe.highT.offset, name="highToffset") + recipe.add_variable(recipe.lowT.offset, name="lowToffset") + recipe.add_variable(recipe.highT.offset, name="highToffset") # We create a new Variable and use the recipe's "constrain" method to # associate the Debye temperature parameters with that variable. - recipe.newVar("thetaD", 100) - recipe.constrain(recipe.lowT.thetaD, "thetaD") - recipe.constrain(recipe.highT.thetaD, "thetaD") + recipe.create_new_variable("thetaD", 100) + recipe.add_constraint(recipe.lowT.thetaD, "thetaD") + recipe.add_constraint(recipe.highT.thetaD, "thetaD") return recipe @@ -98,22 +99,22 @@ def plotResults(recipe): # The variable values are returned in the order in which the variables were # added to the FitRecipe. - lowToffset, highToffset, thetaD = recipe.getValues() + lowToffset, highToffset, thetaD = recipe.get_values() # We want to extend the fitting range to its full extent so we can get a # nice full plot. - recipe.lowT.profile.setCalculationRange(xmin="obs", xmax="obs") - recipe.highT.profile.setCalculationRange(xmin="obs", xmax="obs") + recipe.lowT.profile.set_calculation_range(xmin="obs", xmax="obs") + recipe.highT.profile.set_calculation_range(xmin="obs", xmax="obs") T = recipe.lowT.profile.x U = recipe.lowT.profile.y - # We can use a FitContribution's 'evaluateEquation' method to evaluate + # We can use a FitContribution's 'evaluate_equation' method to evaluate # expressions involving the Parameters and other aspects of the # FitContribution. Here we evaluate the fitting equation, which is always # accessed using the name "eq". We access it this way (rather than through # the Profile's ycalc attribute) because we changed the calculation range # above, and we therefore need to recalculate the profile. - lowU = recipe.lowT.evaluateEquation("eq") - highU = recipe.highT.evaluateEquation("eq") + lowU = recipe.lowT.evaluate_equation("eq") + highU = recipe.highT.evaluate_equation("eq") # Now we can plot this. import pylab @@ -146,7 +147,7 @@ def main(): res = FitResults(recipe) # Print the results - res.printResults() + res.print_results() # Plot the results plotResults(recipe) diff --git a/docs/examples/ellipsoidsas.py b/docs/examples/ellipsoidsas.py index 5c0826ec..08586701 100644 --- a/docs/examples/ellipsoidsas.py +++ b/docs/examples/ellipsoidsas.py @@ -39,7 +39,7 @@ def makeRecipe(datname): # properly and pass the metadata along. parser = SASParser() parser.parseFile(datname) - profile.loadParsedData(parser) + profile.load_parsed_data(parser) # The ProfileGenerator # The SASGenerator is for configuring and calculating a SAS profile. We use @@ -57,18 +57,18 @@ def makeRecipe(datname): # Here we associate the Profile and ProfileGenerator, as has been done # before. contribution = FitContribution("ellipsoid") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="q") # We want to fit the log of the signal to the log of the data so that the # higher-Q information remains significant. There are no I(Q) uncertainty # values with the data, so we do not need to worry about the effect this # will have on the estimated parameter uncertainties. - contribution.setResidualEquation("log(eq) - log(y)") + contribution.set_residual_equation("log(eq) - log(y)") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables # The SASGenerator uses the parameters from the params and dispersion @@ -78,10 +78,10 @@ def makeRecipe(datname): # SASGenerator these are named like "radius_width". # # We want to fit the scale factor, radii and background factors. - recipe.addVar(generator.scale, 1) - recipe.addVar(generator.radius_a, 50) - recipe.addVar(generator.radius_b, 500) - recipe.addVar(generator.background, 0) + recipe.add_variable(generator.scale, 1) + recipe.add_variable(generator.radius_a, 50) + recipe.add_variable(generator.radius_b, 500) + recipe.add_variable(generator.background, 0) # Give the recipe away so it can be used! return recipe @@ -122,7 +122,7 @@ def plotResults(recipe): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/gaussiangenerator.py b/docs/examples/gaussiangenerator.py index f806e4a2..69c1507c 100644 --- a/docs/examples/gaussiangenerator.py +++ b/docs/examples/gaussiangenerator.py @@ -33,8 +33,9 @@ Extensions -- Remove the amplitude from GaussianGenerator and instead use the 'setEquation' - method of the FitContribution to account for it. Note that the +- Remove the amplitude from GaussianGenerator and instead use the + 'set_equation' method of the + FitContribution to account for it. Note that the GaussianGenerator will be accessible by its name, "g". """ @@ -144,13 +145,13 @@ def makeRecipe(): # attribute of the FitContribution by its name ("g"). Note that this will # set the fitting equation to "g", which calls the GaussianGenerator. contribution = FitContribution("g1") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile) # The FitRecipe # Now we create the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the @@ -160,9 +161,9 @@ def makeRecipe(): # that the Parameters belong to the GaussianGenerator, not the # FitContribution as in gaussianrecipe.py. We initialize parameters as in # gaussianrecipe.py so we can expect the same output. - recipe.addVar(generator.A, 1) - recipe.addVar(generator.x0, 5) - recipe.addVar(generator.sigma, name="sig") + recipe.add_variable(generator.A, 1) + recipe.add_variable(generator.x0, 5) + recipe.add_variable(generator.sigma, name="sig") recipe.sig.value = 1 # Give the recipe away so it can be used! diff --git a/docs/examples/gaussianrecipe.py b/docs/examples/gaussianrecipe.py index 4641f9ec..d0745660 100644 --- a/docs/examples/gaussianrecipe.py +++ b/docs/examples/gaussianrecipe.py @@ -73,7 +73,7 @@ def main(): res = FitResults(recipe) # Print the results. - res.printResults() + res.print_results() # Plot the results. plotResults(recipe) @@ -124,7 +124,7 @@ def makeRecipe(): # contribution by name. Since we told the contribution that our # independent variable is named "x", this value will be substituted into # the fitting equation whenever it is called. - contribution.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") + contribution.set_equation("A * exp(-0.5*(x-x0)**2/sigma**2)") # To demonstrate how these parameters are used, we will give "A" an initial # value. Note that Parameters are not numbers, but are containers for @@ -140,7 +140,7 @@ def makeRecipe(): # Here we tell the FitRecipe to use our FitContribution. When the FitRecipe # calculates its residual function, it will call on the FitContribution to # do part of the work. - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Specify which Parameters we want to vary in the fit. This will add # Variables to the FitRecipe that directly modify the Parameters of the @@ -149,13 +149,13 @@ def makeRecipe(): # Here we create a Variable for the 'A' Parameter from our fit equation. # The resulting Variable will be named 'A' as well, but it will be accessed # via the FitRecipe. - recipe.addVar(contribution.A) + recipe.add_variable(contribution.A) # Here we create the Variable for 'x0' and give it an initial value of 5. - recipe.addVar(contribution.x0, 5) + recipe.add_variable(contribution.x0, 5) # Here we create a Variable named 'sig', which is tied to the 'sigma' # Parameter of our FitContribution. We give it an initial value through the # FitRecipe instance. - recipe.addVar(contribution.sigma, name="sig") + recipe.add_variable(contribution.sigma, name="sig") recipe.sig.value = 1 return recipe @@ -172,11 +172,11 @@ def scipyOptimize(recipe): # We're going to use the least-squares (Levenberg-Marquardt) optimizer from # scipy. We simply have to give it the function to minimize # (recipe.residual) and the starting values of the Variables - # (recipe.getValues()). + # (recipe.get_values()). from scipy.optimize.minpack import leastsq print("Fit using scipy's LM optimizer") - leastsq(recipe.residual, recipe.getValues()) + leastsq(recipe.residual, recipe.get_values()) return diff --git a/docs/examples/interface.py b/docs/examples/interface.py index 4611ee6b..03bb498a 100644 --- a/docs/examples/interface.py +++ b/docs/examples/interface.py @@ -38,7 +38,7 @@ def main(): # "<<" - Inject a parameter value c = FitContribution("g1") c.set_profile(p) - c.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") + c.set_equation("A * exp(-0.5*(x-x0)**2/sigma**2)") c.A << 0.5 c.x0 << 5 c.sigma << 1 @@ -46,7 +46,7 @@ def main(): # FitRecipe operations # "|=" - Union of necessary components. # "+=" - Add Parameter or create a new one. Each tuple is a set of - # arguments for either setVar or addVar. + # arguments for either setVar or add_variable. # "*=" - Constrain a parameter. Think of "*" as a push-pin holding one # parameter's value to that of another. # "%=" - Restrain a parameter or equation. Think of "%" as a rope @@ -63,7 +63,7 @@ def main(): res = FitResults(r) # Print the results. - res.printResults() + res.print_results() # Plot the results. from gaussianrecipe import plotResults diff --git a/docs/examples/npintensity.py b/docs/examples/npintensity.py index c193922e..eaf17012 100644 --- a/docs/examples/npintensity.py +++ b/docs/examples/npintensity.py @@ -32,9 +32,9 @@ Extensions -- The IntensityGenerator class uses the 'addParameterSet' method to associate +- The IntensityGenerator class uses the 'add_parameter_set' method to associate the structure adapter (DiffpyStructureParSet) with the generator. Most SrFit - classes have an 'addParameterSet' class and can store ParameterSet objects. + classes have an 'add_parameter_set' class and can store ParameterSet objects. Grab the phase object from the IntensityGenerator and try to add it to other objects used in the fit recipe. Create variables from the moved Parameters rather than from the 'phase' that lives in the IntensityGenerator and see if @@ -153,7 +153,7 @@ def setStructure(self, strufile): parset = DiffpyStructureParSet("phase", stru) # Put this ParameterSet in the ProfileGenerator. - self.addParameterSet(parset) + self.add_parameter_set(parset) return @@ -203,7 +203,7 @@ def makeRecipe(strufile, datname): # the FitContribution to name the x-variable of the profile "q", so we can # use it in equations with this name. contribution = FitContribution("bucky") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="q") # Now we're ready to define the fitting equation for the FitContribution. @@ -231,7 +231,7 @@ def makeRecipe(strufile, datname): # This creates a callable equation named "bkgd" within the FitContribution, # and turns the polynomial coefficients into Parameters. - contribution.registerStringFunction(bkgdstr, "bkgd") + contribution.register_string_function(bkgdstr, "bkgd") # We will create the broadening function that we need by creating a python # function and registering it with the FitContribution. @@ -247,7 +247,7 @@ def gaussian(q, q0, width): # This registers the python function and extracts the name and creates # Parameters from the arguments. - contribution.registerFunction(gaussian) + contribution.register_function(gaussian) # Center the Gaussian so it is not truncated. contribution.q0.value = x[len(x) // 2] @@ -256,27 +256,27 @@ def gaussian(q, q0, width): # convolve the signal with the Gaussian to broaden it. Recall that we don't # need to supply arguments to the registered functions unless we want to # make changes to their input values. - contribution.setEquation("scale * convolve(I, gaussian) + bkgd") + contribution.set_equation("scale * convolve(I, gaussian) + bkgd") # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Specify which parameters we want to refine. - recipe.addVar(contribution.b0, 0) - recipe.addVar(contribution.b1, 0) - recipe.addVar(contribution.b2, 0) - recipe.addVar(contribution.b3, 0) - recipe.addVar(contribution.b4, 0) - recipe.addVar(contribution.b5, 0) - recipe.addVar(contribution.b6, 0) - recipe.addVar(contribution.b7, 0) - recipe.addVar(contribution.b8, 0) - recipe.addVar(contribution.b9, 0) + recipe.add_variable(contribution.b0, 0) + recipe.add_variable(contribution.b1, 0) + recipe.add_variable(contribution.b2, 0) + recipe.add_variable(contribution.b3, 0) + recipe.add_variable(contribution.b4, 0) + recipe.add_variable(contribution.b5, 0) + recipe.add_variable(contribution.b6, 0) + recipe.add_variable(contribution.b7, 0) + recipe.add_variable(contribution.b8, 0) + recipe.add_variable(contribution.b9, 0) # We also want to adjust the scale and the convolution width - recipe.addVar(contribution.scale, 1) - recipe.addVar(contribution.width, 0.1) + recipe.add_variable(contribution.scale, 1) + recipe.add_variable(contribution.width, 0.1) # We can also refine structural parameters. Here we extract the # DiffpyStructureParSet from the intensity generator and use the parameters @@ -289,16 +289,16 @@ def gaussian(q, q0, width): # constrained to a Variable by name. This has the same effect. lattice = phase.getLattice() a = lattice.a - recipe.addVar(a) - recipe.constrain(lattice.b, a) - recipe.constrain(lattice.c, a) + recipe.add_variable(a) + recipe.add_constraint(lattice.b, a) + recipe.add_constraint(lattice.c, a) # We want to refine the thermal parameters as well. We will add a new # Variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the following constraints. - Uiso = recipe.newVar("Uiso") + Uiso = recipe.create_new_variable("Uiso") for atom in phase.getScatterers(): - recipe.constrain(atom.Uiso, Uiso) + recipe.add_constraint(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe @@ -326,7 +326,7 @@ def main(): rescount = recipe.fithooks[0].count calcount = recipe.bucky.I.count footer = "iofq called %i%% of the time" % int(100.0 * calcount / rescount) - res.printResults(footer=footer) + res.print_results(footer=footer) # Plot! plotResults(recipe) @@ -342,7 +342,7 @@ def plotResults(recipe): Imeas = recipe.bucky.profile.y Icalc = recipe.bucky.profile.ycalc - bkgd = recipe.bucky.evaluateEquation("bkgd") + bkgd = recipe.bucky.evaluate_equation("bkgd") diff = Imeas - Icalc import pylab diff --git a/docs/examples/npintensityII.py b/docs/examples/npintensityII.py index 552eba96..5a6415a6 100644 --- a/docs/examples/npintensityII.py +++ b/docs/examples/npintensityII.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ######################################################################## -"""Example of extracting information from multiple data sets simultaneously. +"""Example of extracting information from multiple data sets +simultaneously. This example builds on npintensitygenerator.py, and uses IntensityGenerator from that example to build a recipe that simultaneously refines two data sets @@ -83,15 +84,15 @@ def makeRecipe(strufile, datname1, datname2): generator1 = IntensityGenerator("I") generator1.setStructure(strufile) generator2 = IntensityGenerator("I") - generator2.addParameterSet(generator1.phase) + generator2.add_parameter_set(generator1.phase) # The FitContributions # Create the FitContributions. contribution1 = FitContribution("bucky1") - contribution1.addProfileGenerator(generator1) + contribution1.add_profile_generator(generator1) contribution1.set_profile(profile1, xname="q") contribution2 = FitContribution("bucky2") - contribution2.addProfileGenerator(generator2) + contribution2.add_profile_generator(generator2) contribution2.set_profile(profile2, xname="q") # Now we're ready to define the fitting equation for each FitContribution. @@ -99,12 +100,13 @@ def makeRecipe(strufile, datname1, datname2): # the same form and use the same Parameter names. By default, Parameters # in different contributions are different Parameters even if they have the # same names. FitContributions are isolated namespaces than only share - # information if you tell them to by using addParameter or addParameterSet. + # information if you tell them to by using addParameter or + # add_parameter_set. bkgdstr = "b0 + b1*q + b2*q**2 + b3*q**3 + b4*q**4 + b5*q**5 + b6*q**6 +\ b7*q**7 +b8*q**8 + b9*q**9" - contribution1.registerStringFunction(bkgdstr, "bkgd") - contribution2.registerStringFunction(bkgdstr, "bkgd") + contribution1.register_string_function(bkgdstr, "bkgd") + contribution2.register_string_function(bkgdstr, "bkgd") # We will create the broadening function by registering a python function. pi = numpy.pi @@ -117,69 +119,69 @@ def gaussian(q, q0, width): * exp(-0.5 * ((q - q0) / width) ** 2) ) - contribution1.registerFunction(gaussian) - contribution2.registerFunction(gaussian) + contribution1.register_function(gaussian) + contribution2.register_function(gaussian) # Center the gaussian contribution1.q0.value = x[len(x) // 2] contribution2.q0.value = x[len(x) // 2] # Now we can incorporate the scale and bkgd into our calculation. We also # convolve the signal with the gaussian to broaden it. - contribution1.setEquation("scale * convolve(I, gaussian) + bkgd") - contribution2.setEquation("scale * convolve(I, gaussian) + bkgd") + contribution1.set_equation("scale * convolve(I, gaussian) + bkgd") + contribution2.set_equation("scale * convolve(I, gaussian) + bkgd") # Make a FitRecipe and associate the FitContributions. recipe = FitRecipe() - recipe.addContribution(contribution1) - recipe.addContribution(contribution2) + recipe.add_contribution(contribution1) + recipe.add_contribution(contribution2) # Specify which Parameters we want to refine. We want to refine the # background that we just defined in the FitContributions. We have to do # this separately for each FitContribution. We tag the variables so it is # easy to retrieve the background variables. - recipe.addVar(contribution1.b0, 0, name="b1_0", tag="bcoeffs1") - recipe.addVar(contribution1.b1, 0, name="b1_1", tag="bcoeffs1") - recipe.addVar(contribution1.b2, 0, name="b1_2", tag="bcoeffs1") - recipe.addVar(contribution1.b3, 0, name="b1_3", tag="bcoeffs1") - recipe.addVar(contribution1.b4, 0, name="b1_4", tag="bcoeffs1") - recipe.addVar(contribution1.b5, 0, name="b1_5", tag="bcoeffs1") - recipe.addVar(contribution1.b6, 0, name="b1_6", tag="bcoeffs1") - recipe.addVar(contribution1.b7, 0, name="b1_7", tag="bcoeffs1") - recipe.addVar(contribution1.b8, 0, name="b1_8", tag="bcoeffs1") - recipe.addVar(contribution1.b9, 0, name="b1_9", tag="bcoeffs1") - recipe.addVar(contribution2.b0, 0, name="b2_0", tag="bcoeffs2") - recipe.addVar(contribution2.b1, 0, name="b2_1", tag="bcoeffs2") - recipe.addVar(contribution2.b2, 0, name="b2_2", tag="bcoeffs2") - recipe.addVar(contribution2.b3, 0, name="b2_3", tag="bcoeffs2") - recipe.addVar(contribution2.b4, 0, name="b2_4", tag="bcoeffs2") - recipe.addVar(contribution2.b5, 0, name="b2_5", tag="bcoeffs2") - recipe.addVar(contribution2.b6, 0, name="b2_6", tag="bcoeffs2") - recipe.addVar(contribution2.b7, 0, name="b2_7", tag="bcoeffs2") - recipe.addVar(contribution2.b8, 0, name="b2_8", tag="bcoeffs2") - recipe.addVar(contribution2.b9, 0, name="b2_9", tag="bcoeffs2") + recipe.add_variable(contribution1.b0, 0, name="b1_0", tag="bcoeffs1") + recipe.add_variable(contribution1.b1, 0, name="b1_1", tag="bcoeffs1") + recipe.add_variable(contribution1.b2, 0, name="b1_2", tag="bcoeffs1") + recipe.add_variable(contribution1.b3, 0, name="b1_3", tag="bcoeffs1") + recipe.add_variable(contribution1.b4, 0, name="b1_4", tag="bcoeffs1") + recipe.add_variable(contribution1.b5, 0, name="b1_5", tag="bcoeffs1") + recipe.add_variable(contribution1.b6, 0, name="b1_6", tag="bcoeffs1") + recipe.add_variable(contribution1.b7, 0, name="b1_7", tag="bcoeffs1") + recipe.add_variable(contribution1.b8, 0, name="b1_8", tag="bcoeffs1") + recipe.add_variable(contribution1.b9, 0, name="b1_9", tag="bcoeffs1") + recipe.add_variable(contribution2.b0, 0, name="b2_0", tag="bcoeffs2") + recipe.add_variable(contribution2.b1, 0, name="b2_1", tag="bcoeffs2") + recipe.add_variable(contribution2.b2, 0, name="b2_2", tag="bcoeffs2") + recipe.add_variable(contribution2.b3, 0, name="b2_3", tag="bcoeffs2") + recipe.add_variable(contribution2.b4, 0, name="b2_4", tag="bcoeffs2") + recipe.add_variable(contribution2.b5, 0, name="b2_5", tag="bcoeffs2") + recipe.add_variable(contribution2.b6, 0, name="b2_6", tag="bcoeffs2") + recipe.add_variable(contribution2.b7, 0, name="b2_7", tag="bcoeffs2") + recipe.add_variable(contribution2.b8, 0, name="b2_8", tag="bcoeffs2") + recipe.add_variable(contribution2.b9, 0, name="b2_9", tag="bcoeffs2") # We also want to adjust the scale and the convolution width - recipe.addVar(contribution1.scale, 1, name="scale1") - recipe.addVar(contribution1.width, 0.1, name="width1") - recipe.addVar(contribution2.scale, 1, name="scale2") - recipe.addVar(contribution2.width, 0.1, name="width2") + recipe.add_variable(contribution1.scale, 1, name="scale1") + recipe.add_variable(contribution1.width, 0.1, name="width1") + recipe.add_variable(contribution2.scale, 1, name="scale2") + recipe.add_variable(contribution2.width, 0.1, name="width2") # We can also refine structural parameters. We only have to do this once, # since each generator holds the same DiffpyStructureParSet. phase = generator1.phase lattice = phase.getLattice() - a = recipe.addVar(lattice.a) + a = recipe.add_variable(lattice.a) # We want to allow for isotropic expansion, so we'll make constraints for # that. - recipe.constrain(lattice.b, a) - recipe.constrain(lattice.c, a) + recipe.add_constraint(lattice.b, a) + recipe.add_constraint(lattice.c, a) # We want to refine the thermal parameters as well. We will add a new # variable that we call "Uiso" and constrain the atomic Uiso values to # this. Note that we don't give Uiso an initial value. The initial value # will be inferred from the subsequent constraints. - Uiso = recipe.newVar("Uiso") + Uiso = recipe.create_new_variable("Uiso") for atom in phase.getScatterers(): - recipe.constrain(atom.Uiso, Uiso) + recipe.add_constraint(atom.Uiso, Uiso) # Give the recipe away so it can be used! return recipe @@ -194,11 +196,11 @@ def plotResults(recipe): # Plot this for fun. I1 = recipe.bucky1.profile.y Icalc1 = recipe.bucky1.profile.ycalc - bkgd1 = recipe.bucky1.evaluateEquation("bkgd") + bkgd1 = recipe.bucky1.evaluate_equation("bkgd") diff1 = I1 - Icalc1 I2 = recipe.bucky2.profile.y Icalc2 = recipe.bucky2.profile.ycalc - bkgd2 = recipe.bucky2.evaluateEquation("bkgd") + bkgd2 = recipe.bucky2.evaluate_equation("bkgd") diff2 = I2 - Icalc2 offset = 1.2 * max(I2) * numpy.ones_like(I2) I1 += offset @@ -248,22 +250,22 @@ def main(): # fit. recipe.fix("all") recipe.free("bcoeffs1") - recipe.setWeight(recipe.bucky2, 0) + recipe.set_weight(recipe.bucky2, 0) scipyOptimize(recipe) # Now do the same for the second background recipe.fix("all") recipe.free("bcoeffs1") - recipe.setWeight(recipe.bucky2, 1) - recipe.setWeight(recipe.bucky1, 0) + recipe.set_weight(recipe.bucky2, 1) + recipe.set_weight(recipe.bucky1, 0) scipyOptimize(recipe) # Now refine everything with the structure parameters included recipe.free("all") - recipe.setWeight(recipe.bucky1, 1) + recipe.set_weight(recipe.bucky1, 1) scipyOptimize(recipe) # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/nppdfcrystal.py b/docs/examples/nppdfcrystal.py index 49ed32e6..a568e8b9 100644 --- a/docs/examples/nppdfcrystal.py +++ b/docs/examples/nppdfcrystal.py @@ -32,8 +32,9 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator def makeRecipe(ciffile, grdata): @@ -42,10 +43,10 @@ def makeRecipe(ciffile, grdata): # Set up a PDF fit as has been done in other examples. pdfprofile = Profile() - pdfparser = PDFParser() + pdfparser = ProfileParser() pdfparser.parseFile(grdata) - pdfprofile.loadParsedData(pdfparser) - pdfprofile.setCalculationRange(xmin=0.1, xmax=20) + pdfprofile.load_parsed_data(pdfparser) + pdfprofile.set_calculation_range(xmin=0.1, xmax=20) pdfcontribution = FitContribution("pdf") pdfcontribution.set_profile(pdfprofile, xname="r") @@ -54,28 +55,28 @@ def makeRecipe(ciffile, grdata): pdfgenerator.setQmax(30.0) stru = loadCrystal(ciffile) pdfgenerator.setStructure(stru) - pdfcontribution.addProfileGenerator(pdfgenerator) + pdfcontribution.add_profile_generator(pdfgenerator) # Register the nanoparticle shape factor. from diffpy.srfit.pdf.characteristicfunctions import sphericalCF - pdfcontribution.registerFunction(sphericalCF, name="f") + pdfcontribution.register_function(sphericalCF, name="f") # Now we set up the fitting equation. - pdfcontribution.setEquation("f * G") + pdfcontribution.set_equation("f * G") # Now make the recipe. Make sure we fit the characteristic function shape # parameters, in this case 'psize', which is the diameter of the particle. recipe = FitRecipe() - recipe.addContribution(pdfcontribution) + recipe.add_contribution(pdfcontribution) phase = pdfgenerator.phase for par in phase.sgpars: - recipe.addVar(par) + recipe.add_variable(par) - recipe.addVar(pdfcontribution.psize, 20) - recipe.addVar(pdfgenerator.scale, 1) - recipe.addVar(pdfgenerator.delta2, 0) + recipe.add_variable(pdfcontribution.psize, 20) + recipe.add_variable(pdfgenerator.scale, 1) + recipe.add_variable(pdfgenerator.delta2, 0) recipe.B11_0 = 0.1 return recipe @@ -91,10 +92,10 @@ def plotResults(recipe): diffzero = -0.8 * max(g) * numpy.ones_like(g) diff = g - gcalc + diffzero - gcryst = recipe.pdf.evaluateEquation("G") + gcryst = recipe.pdf.evaluate_equation("G") gcryst /= recipe.scale.value - fr = recipe.pdf.evaluateEquation("f") + fr = recipe.pdf.evaluate_equation("f") fr *= max(g) / fr[0] import pylab @@ -122,7 +123,7 @@ def plotResults(recipe): scipyOptimize(recipe) res = FitResults(recipe) - res.printResults() + res.print_results() plotResults(recipe) diff --git a/docs/examples/nppdfobjcryst.py b/docs/examples/nppdfobjcryst.py index 70c0bf83..03511d19 100644 --- a/docs/examples/nppdfobjcryst.py +++ b/docs/examples/nppdfobjcryst.py @@ -40,7 +40,7 @@ def makeRecipe(molecule, datname): # Load data and add it to the profile profile.loadtxt(datname) - profile.setCalculationRange(xmin=1.2, xmax=8) + profile.set_calculation_range(xmin=1.2, xmax=8) # The ProfileGenerator # Create a DebyePDFGenerator named "G". @@ -52,12 +52,12 @@ def makeRecipe(molecule, datname): # The FitContribution contribution = FitContribution("bucky") - contribution.addProfileGenerator(generator) + contribution.add_profile_generator(generator) contribution.set_profile(profile, xname="r") # Make a FitRecipe. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Specify which parameters we want to refine. We'll be using the # MoleculeParSet within the generator, so let's get a handle to it. See the @@ -66,14 +66,14 @@ def makeRecipe(molecule, datname): c60 = generator.phase # First, the isotropic thermal displacement factor. - Biso = recipe.newVar("Biso") + Biso = recipe.create_new_variable("Biso") for atom in c60.getScatterers(): # We have defined a 'center' atom that is a dummy, which means that it # has no scattering power. It is only used as a reference point for # our bond length. We don't want to constrain it. if not atom.isDummy(): - recipe.constrain(atom.Biso, Biso) + recipe.add_constraint(atom.Biso, Biso) # We need to let the molecule expand. If we were modeling it as a crystal, # we could let the unit cell expand. For instruction purposes, we use a @@ -88,7 +88,7 @@ def makeRecipe(molecule, datname): # that we don't give it an initial value. Since the variable is being # directly constrained to further below, its initial value will be inferred # from the constraint. - radius = recipe.newVar("radius") + radius = recipe.create_new_variable("radius") for i, atom in enumerate(c60.getScatterers()): if atom.isDummy(): @@ -97,12 +97,12 @@ def makeRecipe(molecule, datname): # This creates a Parameter that moves the second atom according to the # bond length. Note that each Parameter needs a unique name. par = c60.addBondLengthParameter("rad%i" % i, center, atom) - recipe.constrain(par, radius) + recipe.add_constraint(par, radius) # Add the correlation term, scale. The scale is too short to effectively # determine qdamp. - recipe.addVar(generator.delta2, 2) - recipe.addVar(generator.scale, 1.3e4) + recipe.add_variable(generator.delta2, 2) + recipe.add_variable(generator.scale, 1.3e4) # Give the recipe away so it can be used! return recipe @@ -143,11 +143,11 @@ def main(): # Optimize from scipy.optimize import leastsq - leastsq(recipe.residual, recipe.getValues()) + leastsq(recipe.residual, recipe.get_values()) # Print results res = FitResults(recipe) - res.printResults() + res.print_results() # Plot results plotResults(recipe) diff --git a/docs/examples/nppdfsas.py b/docs/examples/nppdfsas.py index 665e2377..2aac74b4 100644 --- a/docs/examples/nppdfsas.py +++ b/docs/examples/nppdfsas.py @@ -31,14 +31,16 @@ FitRecipe, FitResults, Profile, + ProfileParser, ) -from diffpy.srfit.pdf import PDFGenerator, PDFParser +from diffpy.srfit.pdf import PDFGenerator from diffpy.srfit.pdf.characteristicfunctions import SASCF from diffpy.srfit.sas import SASGenerator, SASParser def makeRecipe(ciffile, grdata, iqdata): - """Make complex-modeling recipe where I(q) and G(r) are fit simultaneously. + """Make complex-modeling recipe where I(q) and G(r) are fit + simultaneously. The fit I(q) is fed into the calculation of G(r), which provides feedback for the fit parameters of both. @@ -46,10 +48,10 @@ def makeRecipe(ciffile, grdata, iqdata): # Create a PDF contribution as before pdfprofile = Profile() - pdfparser = PDFParser() + pdfparser = ProfileParser() pdfparser.parseFile(grdata) - pdfprofile.loadParsedData(pdfparser) - pdfprofile.setCalculationRange(xmin=0.1, xmax=20) + pdfprofile.load_parsed_data(pdfparser) + pdfprofile.set_calculation_range(xmin=0.1, xmax=20) pdfcontribution = FitContribution("pdf") pdfcontribution.set_profile(pdfprofile, xname="r") @@ -58,15 +60,15 @@ def makeRecipe(ciffile, grdata, iqdata): pdfgenerator.setQmax(30.0) stru = loadCrystal(ciffile) pdfgenerator.setStructure(stru) - pdfcontribution.addProfileGenerator(pdfgenerator) - pdfcontribution.setResidualEquation("resv") + pdfcontribution.add_profile_generator(pdfgenerator) + pdfcontribution.set_residual_equation("resv") # Create a SAS contribution as well. We assume the nanoparticle is roughly # elliptical. sasprofile = Profile() sasparser = SASParser() sasparser.parseFile(iqdata) - sasprofile.loadParsedData(sasparser) + sasprofile.load_parsed_data(sasparser) if all(sasprofile.dy == 0): sasprofile.dy[:] = 1 @@ -77,8 +79,8 @@ def makeRecipe(ciffile, grdata, iqdata): model = EllipsoidModel() sasgenerator = SASGenerator("generator", model) - sascontribution.addProfileGenerator(sasgenerator) - sascontribution.setResidualEquation("resv") + sascontribution.add_profile_generator(sasgenerator) + sascontribution.set_residual_equation("resv") # Now we set up a characteristic function calculator that depends on the # sas model. @@ -86,34 +88,34 @@ def makeRecipe(ciffile, grdata, iqdata): # Register the calculator with the pdf contribution and define the fitting # equation. - pdfcontribution.registerCalculator(cfcalculator) + pdfcontribution.register_calculator(cfcalculator) # The PDF for a nanoscale crystalline is approximated by # Gnano = f * Gcryst - pdfcontribution.setEquation("f * G") + pdfcontribution.set_equation("f * G") # Moving on recipe = FitRecipe() - recipe.addContribution(pdfcontribution) - recipe.addContribution(sascontribution) + recipe.add_contribution(pdfcontribution) + recipe.add_contribution(sascontribution) # PDF phase = pdfgenerator.phase for par in phase.sgpars: - recipe.addVar(par) + recipe.add_variable(par) - recipe.addVar(pdfgenerator.scale, 1) - recipe.addVar(pdfgenerator.delta2, 0) + recipe.add_variable(pdfgenerator.scale, 1) + recipe.add_variable(pdfgenerator.delta2, 0) # SAS - recipe.addVar(sasgenerator.scale, 1, name="iqscale") - recipe.addVar(sasgenerator.radius_a, 10) - recipe.addVar(sasgenerator.radius_b, 10) + recipe.add_variable(sasgenerator.scale, 1, name="iqscale") + recipe.add_variable(sasgenerator.radius_a, 10) + recipe.add_variable(sasgenerator.radius_b, 10) # Even though the cfcalculator and sasgenerator depend on the same sas # model, we must still constrain the cfcalculator Parameters so that it is # informed of changes in the refined parameters. - recipe.constrain(cfcalculator.radius_a, "radius_a") - recipe.constrain(cfcalculator.radius_b, "radius_b") + recipe.add_constraint(cfcalculator.radius_a, "radius_a") + recipe.add_constraint(cfcalculator.radius_b, "radius_b") return recipe @@ -122,23 +124,23 @@ def fitRecipe(recipe): """We refine in stages to help the refinement converge.""" # Tune SAS. - recipe.setWeight(recipe.pdf, 0) + recipe.set_weight(recipe.pdf, 0) recipe.fix("all") recipe.free("radius_a", "radius_b", iqscale=1e8) - recipe.constrain("radius_b", "radius_a") + recipe.add_constraint("radius_b", "radius_a") scipyOptimize(recipe) - recipe.unconstrain("radius_b") + recipe.remove_constraint("radius_b") # Tune PDF - recipe.setWeight(recipe.pdf, 1) - recipe.setWeight(recipe.sas, 0) + recipe.set_weight(recipe.pdf, 1) + recipe.set_weight(recipe.sas, 0) recipe.fix("all") recipe.free("a", "Biso_0", "scale", "delta2") scipyOptimize(recipe) # Tune all - recipe.setWeight(recipe.pdf, 1) - recipe.setWeight(recipe.sas, 1) + recipe.set_weight(recipe.pdf, 1) + recipe.set_weight(recipe.sas, 1) recipe.free("all") scipyOptimize(recipe) @@ -155,10 +157,10 @@ def plotResults(recipe): diffzero = -0.8 * max(g) * numpy.ones_like(g) diff = g - gcalc + diffzero - gcryst = recipe.pdf.evaluateEquation("G") + gcryst = recipe.pdf.evaluate_equation("G") gcryst /= recipe.scale.value - fr = recipe.pdf.evaluateEquation("f") + fr = recipe.pdf.evaluate_equation("f") fr *= max(g) / fr[0] import pylab @@ -188,7 +190,7 @@ def plotResults(recipe): fitRecipe(recipe) res = FitResults(recipe) - res.printResults() + res.print_results() plotResults(recipe) diff --git a/docs/examples/simplepdf.py b/docs/examples/simplepdf.py index 027a6d76..a81bd83d 100644 --- a/docs/examples/simplepdf.py +++ b/docs/examples/simplepdf.py @@ -35,7 +35,7 @@ def makeRecipe(ciffile, datname): # Work directly with a custom PDFContribution to load the data contribution = PDFContribution("nickel") contribution.loadData(datname) - contribution.setCalculationRange(xmin=1, xmax=20, dx=0.1) + contribution.set_calculation_range(xmin=1, xmax=20, dx=0.1) # and the phase stru = Structure() @@ -44,7 +44,7 @@ def makeRecipe(ciffile, datname): # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables phase = contribution.nickel.phase @@ -54,13 +54,13 @@ def makeRecipe(ciffile, datname): sgpars = constrainAsSpaceGroup(phase, "Fm-3m") for par in sgpars.latpars: - recipe.addVar(par) + recipe.add_variable(par) for par in sgpars.adppars: - recipe.addVar(par, 0.005) + recipe.add_variable(par, 0.005) - recipe.addVar(contribution.scale, 1) - recipe.addVar(contribution.qdamp, 0.03, fixed=True) - recipe.addVar(contribution.nickel.delta2, 5) + recipe.add_variable(contribution.scale, 1) + recipe.add_variable(contribution.qdamp, 0.03, fixed=True) + recipe.add_variable(contribution.nickel.delta2, 5) # Give the recipe away so it can be used! return recipe @@ -82,8 +82,8 @@ def makeRecipe(ciffile, datname): # Generate, print and save the FitResults res = FitResults(recipe) - res.printResults() - res.saveResults("nickel_example.res") + res.print_results() + res.save_results("nickel_example.res") # Plot! plotResults(recipe) diff --git a/docs/examples/simplepdftwophase.py b/docs/examples/simplepdftwophase.py index a8017c23..0b7ee046 100644 --- a/docs/examples/simplepdftwophase.py +++ b/docs/examples/simplepdftwophase.py @@ -31,7 +31,7 @@ def makeRecipe(niciffile, siciffile, datname): # Load data and add it to the profile contribution = PDFContribution("nisi") contribution.loadData(datname) - contribution.setCalculationRange(xmax=20) + contribution.set_calculation_range(xmax=20) stru = loadCrystal(niciffile) contribution.addStructure("ni", stru) @@ -41,20 +41,20 @@ def makeRecipe(niciffile, siciffile, datname): # Make the FitRecipe and add the FitContribution. recipe = FitRecipe() - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Configure the fit variables # Start by configuring the scale factor and resolution factors. # We want the sum of the phase scale factors to be 1. - recipe.newVar("scale_ni", 0.1) - recipe.constrain(contribution.ni.scale, "scale_ni") - recipe.constrain(contribution.si.scale, "1 - scale_ni") + recipe.create_new_variable("scale_ni", 0.1) + recipe.add_constraint(contribution.ni.scale, "scale_ni") + recipe.add_constraint(contribution.si.scale, "1 - scale_ni") # We also want the resolution factor to be the same on each. This is done # for free by the PDFContribution. We simply need to add it to the recipe. - recipe.addVar(contribution.qdamp, 0.03) + recipe.add_variable(contribution.qdamp, 0.03) # Vary the global scale as well. - recipe.addVar(contribution.scale, 1) + recipe.add_variable(contribution.scale, 1) # Now we can configure the structural parameters. Since we're using # ObjCrystCrystalParSets, the space group constraints are automatically @@ -66,13 +66,13 @@ def makeRecipe(niciffile, siciffile, datname): # above. phase_ni = contribution.ni.phase for par in phase_ni.sgpars: - recipe.addVar(par, name=par.name + "_ni") - recipe.addVar(contribution.ni.delta2, name="delta2_ni") + recipe.add_variable(par, name=par.name + "_ni") + recipe.add_variable(contribution.ni.delta2, name="delta2_ni") # Next the silicon parameters phase_si = contribution.si.phase for par in phase_si.sgpars: - recipe.addVar(par, name=par.name + "_si") - recipe.addVar(contribution.si.delta2, name="delta2_si") + recipe.add_variable(par, name=par.name + "_si") + recipe.add_variable(contribution.si.delta2, name="delta2_si") # We have prior information from the earlier examples so we'll use it here # in the form of restraints. @@ -82,18 +82,30 @@ def makeRecipe(niciffile, siciffile, datname): # derived has no uncertainty. Thus, we will tell the recipe to scale the # residual, which means that it will be weighted as much as the average # data point during the fit. - recipe.restrain("a_ni", lb=3.527, ub=3.527, scaled=True) + recipe.add_soft_bounds( + "a_ni", lower_bound=3.527, upper_bound=3.527, scaled=True + ) # Now we do the same with the delta2 and Biso parameters (remember that # Biso = 8*pi**2*Uiso) - recipe.restrain("delta2_ni", lb=2.22, ub=2.22, scaled=True) - recipe.restrain("Biso_0_ni", lb=0.454, ub=0.454, scaled=True) + recipe.add_soft_bounds( + "delta2_ni", lower_bound=2.22, upper_bound=2.22, scaled=True + ) + recipe.add_soft_bounds( + "Biso_0_ni", lower_bound=0.454, upper_bound=0.454, scaled=True + ) # # We can do the same with the silicon values. We haven't done a thorough # job of measuring the uncertainties in the results, so we'll scale these # as well. - recipe.restrain("a_si", lb=5.430, ub=5.430, scaled=True) - recipe.restrain("delta2_si", lb=3.54, ub=3.54, scaled=True) - recipe.restrain("Biso_0_si", lb=0.645, ub=0.645, scaled=True) + recipe.add_soft_bounds( + "a_si", lower_bound=5.430, upper_bound=5.430, scaled=True + ) + recipe.add_soft_bounds( + "delta2_si", lower_bound=3.54, upper_bound=3.54, scaled=True + ) + recipe.add_soft_bounds( + "Biso_0_si", lower_bound=0.645, upper_bound=0.645, scaled=True + ) # Give the recipe away so it can be used! return recipe @@ -114,7 +126,7 @@ def makeRecipe(niciffile, siciffile, datname): # Generate and print the FitResults res = FitResults(recipe) - res.printResults() + res.print_results() # Plot! plotResults(recipe) diff --git a/docs/examples/simplerecipe.py b/docs/examples/simplerecipe.py index 0ec297a5..cb7d7b69 100644 --- a/docs/examples/simplerecipe.py +++ b/docs/examples/simplerecipe.py @@ -38,7 +38,7 @@ def main(): # Set the equation. The variable "x" is taken from the data that was just # loaded. The other variables, "A", "x0" and "sigma" are turned into # attributes with an initial value of 0. - recipe.setEquation("A * exp(-0.5*(x-x0)**2/sigma**2)") + recipe.set_equation("A * exp(-0.5*(x-x0)**2/sigma**2)") # We can give them other values here. recipe.A = 1 @@ -51,7 +51,7 @@ def main(): leastsq(recipe.residual, recipe.values) # Print the results - recipe.printResults() + recipe.print_results() return diff --git a/docs/examples/threedoublepeaks.py b/docs/examples/threedoublepeaks.py index 60f16f64..ecf01bf4 100644 --- a/docs/examples/threedoublepeaks.py +++ b/docs/examples/threedoublepeaks.py @@ -30,7 +30,8 @@ def makeRecipe(): - """Make a FitRecipe for fitting three double-gaussian curves to data. + """Make a FitRecipe for fitting three double-gaussian curves to + data. The separation and amplitude ratio of the double peaks follows a specific relationship. The peaks are broadend according to their @@ -58,7 +59,7 @@ def makeRecipe(): def gaussian(t, mu, sig): return 1 / (2 * pi * sig**2) ** 0.5 * exp(-0.5 * ((t - mu) / sig) ** 2) - contribution.registerFunction(gaussian, name="peakshape") + contribution.register_function(gaussian, name="peakshape") def delta(t, mu): """Calculate a delta-function. @@ -69,15 +70,15 @@ def delta(t, mu): sig = t[1] - t[0] return gaussian(t, mu, sig) - contribution.registerFunction(delta) + contribution.register_function(delta) # Here is another one bkgdstr = "b0 + b1*t + b2*t**2 + b3*t**3 + b4*t**4 + b5*t**5 + b6*t**6" - contribution.registerStringFunction(bkgdstr, "bkgd") + contribution.register_string_function(bkgdstr, "bkgd") # Now define our fitting equation. We will hardcode the peak ratios. - contribution.setEquation( + contribution.set_equation( "A1 * ( convolve( delta(t, mu11), peakshape(t, c, sig11) ) \ + 0.23*convolve( delta(t, mu12), peakshape(t, c, sig12) ) ) + \ A2 * ( convolve( delta(t, mu21), peakshape(t, c, sig21) ) \ @@ -98,17 +99,17 @@ def delta(t, mu): # Here we tell the FitRecipe to use our FitContribution. When the FitRecipe # calculates its residual function, it will call on the FitContribution to # do part of the work. - recipe.addContribution(contribution) + recipe.add_contribution(contribution) # Vary the amplitudes for each double peak - recipe.addVar(contribution.A1, 100) - recipe.addVar(contribution.A2, 100) - recipe.addVar(contribution.A3, 100) + recipe.add_variable(contribution.A1, 100) + recipe.add_variable(contribution.A2, 100) + recipe.add_variable(contribution.A3, 100) # Vary the position of the first of the double peaks - recipe.addVar(contribution.mu11, 13.0) - recipe.addVar(contribution.mu21, 24.0) - recipe.addVar(contribution.mu31, 33.0) + recipe.add_variable(contribution.mu11, 13.0) + recipe.add_variable(contribution.mu21, 24.0) + recipe.add_variable(contribution.mu31, 33.0) # Constrain the position of the second double peak from numpy import arcsin, sin @@ -119,52 +120,52 @@ def peakloc(mu): l2 = 1.0 return 180 / pi * arcsin(pi / 180 * l2 * sin(mu) / l1) - recipe.registerFunction(peakloc) - recipe.constrain(contribution.mu12, "peakloc(mu11)") - recipe.constrain(contribution.mu22, "peakloc(mu21)") - recipe.constrain(contribution.mu32, "peakloc(mu31)") + recipe.register_function(peakloc) + recipe.add_constraint(contribution.mu12, "peakloc(mu11)") + recipe.add_constraint(contribution.mu22, "peakloc(mu21)") + recipe.add_constraint(contribution.mu32, "peakloc(mu31)") # Vary the width of the peaks. We know the functional form of the peak # broadening. - recipe.newVar("sig0", 0.001) - recipe.newVar("dsig", 4) + recipe.create_new_variable("sig0", 0.001) + recipe.create_new_variable("dsig", 4) def sig(sig0, dsig, mu): """Calculate the peak broadening with respect to position.""" return sig0 * (1 - dsig * mu**2) - recipe.registerFunction(sig) + recipe.register_function(sig) recipe.fix("mu") # Now constrain the peak widths to this recipe.sig0.value = 0.001 recipe.dsig.value = 4.0 - recipe.constrain(contribution.sig11, "sig(sig0, dsig, mu11)") - recipe.constrain( + recipe.add_constraint(contribution.sig11, "sig(sig0, dsig, mu11)") + recipe.add_constraint( contribution.sig12, "sig(sig0, dsig, mu12)", ns={"mu12": contribution.mu12}, ) - recipe.constrain(contribution.sig21, "sig(sig0, dsig, mu21)") - recipe.constrain( + recipe.add_constraint(contribution.sig21, "sig(sig0, dsig, mu21)") + recipe.add_constraint( contribution.sig22, "sig(sig0, dsig, mu22)", ns={"mu22": contribution.mu22}, ) - recipe.constrain(contribution.sig31, "sig(sig0, dsig, mu31)") - recipe.constrain( + recipe.add_constraint(contribution.sig31, "sig(sig0, dsig, mu31)") + recipe.add_constraint( contribution.sig32, "sig(sig0, dsig, mu32)", ns={"mu32": contribution.mu32}, ) # Also the background - recipe.addVar(contribution.b0, 0, tag="bkgd") - recipe.addVar(contribution.b1, 0, tag="bkgd") - recipe.addVar(contribution.b2, 0, tag="bkgd") - recipe.addVar(contribution.b3, 0, tag="bkgd") - recipe.addVar(contribution.b4, 0, tag="bkgd") - recipe.addVar(contribution.b5, 0, tag="bkgd") - recipe.addVar(contribution.b6, 0, tag="bkgd") + recipe.add_variable(contribution.b0, 0, tag="bkgd") + recipe.add_variable(contribution.b1, 0, tag="bkgd") + recipe.add_variable(contribution.b2, 0, tag="bkgd") + recipe.add_variable(contribution.b3, 0, tag="bkgd") + recipe.add_variable(contribution.b4, 0, tag="bkgd") + recipe.add_variable(contribution.b5, 0, tag="bkgd") + recipe.add_variable(contribution.b6, 0, tag="bkgd") return recipe @@ -179,11 +180,11 @@ def scipyOptimize(recipe): # We're going to use the least-squares (Levenberg-Marquardt) optimizer from # scipy. We simply have to give it the function to minimize # (recipe.residual) and the starting values of the Variables - # (recipe.getValues()). + # (recipe.get_values()). from scipy.optimize.minpack import leastsq print("Fit using scipy's LM optimizer") - leastsq(recipe.residual, recipe.getValues()) + leastsq(recipe.residual, recipe.get_values()) return @@ -251,7 +252,7 @@ def steerFit(recipe): res = FitResults(recipe) # Print the results - res.printResults() + res.print_results() # Plot the results plotResults(recipe) diff --git a/docs/source/api/diffpy.srfit.rst b/docs/source/api/diffpy.srfit.rst index 8f840d0c..1e2925bf 100644 --- a/docs/source/api/diffpy.srfit.rst +++ b/docs/source/api/diffpy.srfit.rst @@ -1,7 +1,9 @@ :tocdepth: -1 -diffpy.srfit package -==================== +|title| +======= + +.. |title| replace:: diffpy.srfit package .. automodule:: diffpy.srfit :members: @@ -25,8 +27,10 @@ Subpackages Submodules ---------- -diffpy.srfit.exceptions module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +|module| +-------- + +.. |module| replace:: diffpy.srfit.exceptions module .. automodule:: diffpy.srfit.exceptions :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 6f33a1b7..99dc93f6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,9 +32,7 @@ sys.path.insert(0, str(Path("../../src").resolve())) # abbreviations -ab_authors = ( - "Christopher Farrow, Pavol Juhas, and members of the Billinge Group" -) +ab_authors = "Christopher Farrow, Pavol Juhas, Caden Myers, Simon J. L. Billinge, and members of the Billinge Group." # -- General configuration ------------------------------------------------ @@ -86,7 +84,7 @@ # General information about the project. project = "diffpy.srfit" -copyright = "%Y, The Trustees of Columbia University in the City of New York" +copyright = "%Y, The DiffPy Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -317,7 +315,7 @@ "diffpy.srfit Documentation", ab_authors, "diffpy.srfit", - "Configurable code for solving atomic structures.", + "Generalized code base for modeling problems.", "Miscellaneous", ), ] diff --git a/docs/source/extending.rst b/docs/source/extending.rst index 6e3563de..164ef187 100644 --- a/docs/source/extending.rst +++ b/docs/source/extending.rst @@ -108,7 +108,7 @@ can be adapted as:: setter = SimpleAtom.set, attr = "x") Thus, when ``xpar.getValue()`` is called, it in turn calls -``SimpleAtom.get(atom, "x")``. ``xpar.setValue(value)`` calls +``SimpleAtom.get(atom, "x")``. ``xpar.set_value(value)`` calls ``SimpleAtom.set(atom, "x", value)``. If the attributes of an object cannot be accessed in one of these three ways, @@ -185,7 +185,7 @@ customized Profile is the ``SASProfile`` class in the The ``__init__`` method sets the ``xobs``, ``yobs`` and ``dyobs`` attributes of the ``SASProfile`` to the associated arrays within the ``_datainfo`` attribute. -The ``setObservedProfile`` method is overloaded to modify the ``_datainfo`` +The ``set_observed_profile`` method is overloaded to modify the ``_datainfo`` arrays when their corresponding attributes are modified. This keeps the arrays in sync. @@ -263,5 +263,5 @@ overload. * .. automethod:: FitHook.postcall To use a custom ``FitHook``, assign an instance to a ``FitRecipe`` using the -``pushFitHook`` method. All ``FitHook`` instances held by a ``FitRecipe`` will +``push_fit_hook`` method. All ``FitHook`` instances held by a ``FitRecipe`` will be used in sequence during a call to ``FitRecipe.residual``. diff --git a/docs/source/index.rst b/docs/source/index.rst index ea21479d..b40360f6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,14 +1,20 @@ -.. _developers-guide-index: +####### +|title| +####### -#################################################### -diffpy.srfit documentation -#################################################### +.. |title| replace:: diffpy.srfit documentation -diffpy.srfit - configurable code for solving atomic structures. +``diffpy.srfit`` - Generalized code base for modeling problems. -| Software version |release|. +| Software version |release| | Last updated |today|. +=============== +Getting started +=============== + +Welcome to the ``diffpy.srfit`` documentation! + The diffpy.srfit package provides the framework for building a global optimizer on the fly from components such as function calculators (that calculate different data spectra), regression algorithms and structure models. The @@ -31,9 +37,9 @@ obtain the total cost function. Additionally, diffpy.srfit is designed to be extensible, allowing the user to integrate external calculators to perform co-refinements with other techniques. -======================================== +======= Authors -======================================== +======= diffpy.srfit is developed by members of the Billinge Group at Columbia University and at Brookhaven National Laboratory including @@ -41,17 +47,23 @@ Christopher L. Farrow, Pavol Juhás, Simon J.L. Billinge. The source code in *observable.py* was derived from the 1.0 version of the Caltech "Pyre" project. - -For a detailed list of contributors see +``diffpy.srfit`` is developed by Christopher Farrow, Pavol Juhas, Caden Myers, Simon J. L. Billinge, and members of the DiffPy community. +This project is maintained by Caden Myers and Simon J. L. Billinge. For a detailed list of contributors see https://github.com/diffpy/diffpy.srfit/graphs/contributors. -====================================== +============ Installation -====================================== +============ -See the `README `_ +See the `README `_ file included with the distribution. +================ +Acknowledgements +================ + +``diffpy.srfit`` is built and maintained with `scikit-package `_. + ====================================== Where next? ====================================== @@ -62,10 +74,9 @@ Where next? examples.rst extending.rst -====================================== +================= Table of contents -====================================== - +================= .. toctree:: :titlesonly: @@ -75,9 +86,9 @@ Table of contents .. faq.rst -====================================== +======= Indices -====================================== +======= * :ref:`genindex` * :ref:`modindex` diff --git a/docs/source/license.rst b/docs/source/license.rst index 7d78e94f..748b63bf 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -1,3 +1,5 @@ +:tocdepth: -1 + .. index:: license License @@ -12,10 +14,11 @@ OPEN SOURCE LICENSE AGREEMENT Lawrence Berkeley National Laboratory | Copyright (c) 2014, Australian Synchrotron Research Program Inc., ("ASRP") | Copyright (c) 2006-2007, Board of Trustees of Michigan State University -| Copyright (c) 2008-2012, The Trustees of Columbia University in - the City of New York | Copyright (c) 2014-2019, Brookhaven Science Associates, Brookhaven National Laboratory +| Copyright (c) 2008-2025, The Trustees of Columbia University in + the City of New York +| Copyright (c) 2026-present, The DiffPy Team. The "DiffPy-CMI" is distributed subject to the following license conditions: diff --git a/news/addcontrib-dep.rst b/news/addcontrib-dep.rst new file mode 100644 index 00000000..8a8cf420 --- /dev/null +++ b/news/addcontrib-dep.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``add_contribution`` in replace of ``addContribution``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``addContribution`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/addparameterset-dep.rst b/news/addparameterset-dep.rst new file mode 100644 index 00000000..f69da3c2 --- /dev/null +++ b/news/addparameterset-dep.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``add_parameter_set`` method to replace deprecated ``FitRecipe.addParameterSet``. +* Added ``add_parameter_set`` method to replace deprecated ``ParameterSet.addParameterSet``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``addParameterSet`` method in ``FitRecipe``. +* Deprecate ``addParameterSet`` method in ``ParameterSet``. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/addprofilegenerator-dep.rst b/news/addprofilegenerator-dep.rst new file mode 100644 index 00000000..3a281ff7 --- /dev/null +++ b/news/addprofilegenerator-dep.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``add_profile_generator`` to replace deprecated function. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``addProfileGenerator`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fitcontrib-dep-final.rst b/news/fitcontrib-dep-final.rst new file mode 100644 index 00000000..44a3b787 --- /dev/null +++ b/news/fitcontrib-dep-final.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``get_residual_equation`` method to ``FitContribution``. +* Added ``set_residual_equation`` method to ``FitContribution``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``getResidualEquation`` method of ``FitContribution`` for removal in 4.0.0. +* Deprecated ``setResidualEquation`` method of ``FitContribution`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fitcontrib-dep.rst b/news/fitcontrib-dep.rst new file mode 100644 index 00000000..cedaa94e --- /dev/null +++ b/news/fitcontrib-dep.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``get_equation`` method to ``BaseBuilder``. +* Added ``get_equation`` method to ``FitContribution``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``BaseBuilder.getEquation`` method for removal in 4.0.0. +* Deprecated ``FitContributution.getEquation`` method for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fithook-and-weights-dep.rst b/news/fithook-and-weights-dep.rst new file mode 100644 index 00000000..43157e5a --- /dev/null +++ b/news/fithook-and-weights-dep.rst @@ -0,0 +1,31 @@ +**Added:** + +* Added ``set_weight`` method to ``FitRecipe`` to replace ``setWeight``. +* Added ``get_fit_hooks`` method to ``FitRecipe`` to replace ``getFitHooks``. +* Added ``clear_fit_hooks`` method to ``FitRecipe`` to replace ``clearFitHooks``. +* Added ``pop_fit_hook`` method to ``FitRecipe`` to replace ``popFitHook``. +* Added ``push_fit_hook`` method to ``FitRecipe`` to replace ``pushFitHook``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``setWeight`` in ``FitRecipe`` for removal in 4.0.0. +* Deprecated ``getFitHooks`` in ``FitRecipe`` for removal in 4.0.0. +* Deprecated ``clearFitHooks`` in ``FitRecipe`` for removal in 4.0.0. +* Deprecated ``popFitHook`` in ``FitRecipe`` for removal in 4.0.0. +* Deprecated ``pushFitHook`` in ``FitRecipe`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fitrecipe-dep.rst b/news/fitrecipe-dep.rst new file mode 100644 index 00000000..fbf04526 --- /dev/null +++ b/news/fitrecipe-dep.rst @@ -0,0 +1,32 @@ +**Added:** + +* Added ``create_new_variable`` method to ``FitRecipe``. +* Added ``delete_variable`` method to ``FitRecipe``. +* Added ``add_variable`` method to ``FitRecipe``. +* Added ``scalar_residual`` method to ``FitRecipe``. +* Added ``remove_parameter_set`` method to ``FitRecipe``. +* Added test for ``remove_parameter_set`` method in ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``newVar`` method for removal in 4.0.0. +* Deprecated ``delVar`` method for removal in 4.0.0. +* Deprecated ``addVar`` method for removal in 4.0.0. +* Deprecated ``scalarResidual`` method for removal in 4.0.0. +* Deprecated ``removeParameterSet`` method for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fitrecipe2-dep.rst b/news/fitrecipe2-dep.rst new file mode 100644 index 00000000..0b7d9e3a --- /dev/null +++ b/news/fitrecipe2-dep.rst @@ -0,0 +1,33 @@ +**Added:** + +* Added ``convert_bounds_to_restraints`` method to ``FitRecipe``. +* Added ``get_bounds_pairs`` method to ``FitRecipe``. +* Added ``get_bounds_array`` method to ``FitRecipe``. +* Added ``get_names`` method to ``FitRecipe`` and ``RecipeContainer``. +* Added ``get_values`` method to ``FitRecipe`` and ``RecipeContainer``. +* Added ``is_free`` method to ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``boundsToRestraints`` method for removal in 4.0.0. +* Deprecated ``getBounds`` method for removal in 4.0.0. +* Deprecated ``getBounds2`` method for removal in 4.0.0. +* Deprecated ``getNames`` method for removal in 4.0.0. +* Deprecated ``getValues`` method for removal in 4.0.0. +* Deprecated ``isFree`` method for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fitresults-dep.rst b/news/fitresults-dep.rst new file mode 100644 index 00000000..2ee8a8a3 --- /dev/null +++ b/news/fitresults-dep.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``save_results`` method to ``FitResults``. +* Added ``print_results`` method to ``FitResults``. +* Added ``get_results_string`` method to ``FitResults``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``saveResults`` method for removal in 4.0.0. +* Deprecated ``printResults`` method for removal in 4.0.0. +* Deprecated ``formatResults`` method for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/init-w-recipe.rst b/news/init-w-recipe.rst new file mode 100644 index 00000000..073926e7 --- /dev/null +++ b/news/init-w-recipe.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added initialize_recipe_from_recipe to ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/init-w-results.rst b/news/init-w-results.rst new file mode 100644 index 00000000..ae63a94a --- /dev/null +++ b/news/init-w-results.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``initialize_recipe_with_results`` to ``FitRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/param-dep.rst b/news/param-dep.rst new file mode 100644 index 00000000..e0b8eabe --- /dev/null +++ b/news/param-dep.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``is_constant`` method to ``Parameter``. +* Added ``bound_range`` method to ``Parameter``. +* Added ``bound_window`` method to ``Parameter``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``isConst`` method of ``Parameter``. Use ``is_constant`` instead. +* Deprecated ``boundRange`` method of ``Parameter``. Use ``bound_range`` instead. +* Deprecated ``boundWindow`` method of ``Parameter``. Use ``bound_window`` instead. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/profile-dep.rst b/news/profile-dep.rst new file mode 100644 index 00000000..48424bcd --- /dev/null +++ b/news/profile-dep.rst @@ -0,0 +1,29 @@ +**Added:** + +* Added ``load_parsed_data`` in replace of ``loadParsedData`` in ``Profile`` and ``SimpleRecipe``. +* Added ``set_observed_profile`` in replace of ``setObservedProfile`` in ``Profile`` and ``SimpleRecipe``. +* Added ``set_calculation_range`` in replace of ``setCalculationRange`` in ``Profile`` and ``SimpleRecipe``. +* Added ``set_calculation_points`` in replace of ``setCalculationPoints`` in ``Profile`` and ``SimpleRecipe``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``loadParsedData`` in ``Profile`` and ``SimpleRecipe`` for removal in 4.0.0. +* Deprecated ``setObservedProfile`` in ``Profile`` and ``SimpleRecipe`` for removal in 4.0.0. +* Deprecated ``setCalculationRange`` in ``Profile`` and ``SimpleRecipe`` for removal in 4.0.0. +* Deprecated ``setCalculationPoints`` in ``Profile`` and ``SimpleRecipe`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/profileparser_dep.rst b/news/profileparser_dep.rst new file mode 100644 index 00000000..9f180049 --- /dev/null +++ b/news/profileparser_dep.rst @@ -0,0 +1,29 @@ +**Added:** + +* Add ``parse_file`` method to ``ProfileParser`` to parse a file directly with ``load_data`` from ``diffpy.utils``. +* Add ``get_num_bank`` method to ``ProfileParser`` to replace ``getNumBank``. +* Add ``select_bank`` method to ``ProfileParser`` to replace ``selectBank``. +* Add ``get_format`` method to ``ProfileParser`` to replace ``getFormat``. +* Add ``get_data`` method to ``ProfileParser`` to replace ``getData``. +* Add ``get_meta_data`` method to ``ProfileParser`` to replace ``getMetaData``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecate ``PDFParser``. Use ``ProfileParser`` instead. +* Deprecate ``getNumBank``, ``selectBank``, ``getFormat``, ``getData``, and ``getMetaData`` in ``ProfileParser``. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/recipeorg-dep1.rst b/news/recipeorg-dep1.rst new file mode 100644 index 00000000..aff30369 --- /dev/null +++ b/news/recipeorg-dep1.rst @@ -0,0 +1,37 @@ +**Added:** + +* Added ``iterate_over_parameters`` method. +* Added ``register_calculator`` method. +* Added ``register_function`` method. +* Added ``register_string_function`` method. +* Added ``evaluate_equation`` method. +* Added ``is_constrained`` method. +* Added ``get_constrained_parameters`` method. +* Added ``clear_all_constraints`` method. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``iterPars`` method. Use ``iterate_over_parameters`` instead. +* Deprecated ``registerCalculator`` method. Use ``register_calculator`` instead. +* Deprecated ``registerFunction`` method. Use ``register_function`` instead. +* Deprecated ``registerStringFunction`` method. Use ``register_string_function`` instead. +* Deprecated ``evaluateEquation`` method. Use ``evaluate_equation`` instead. +* Deprecated ``isConstrained`` method. Use ``is_constrained`` instead. +* Deprecated ``getConstrainedPars`` method. Use ``get_constrained_parameters`` instead. +* Deprecated ``clearConstraints`` method. Use ``clear_all_constraints`` instead. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/recipeorg-dep2.rst b/news/recipeorg-dep2.rst new file mode 100644 index 00000000..bcd8a3b0 --- /dev/null +++ b/news/recipeorg-dep2.rst @@ -0,0 +1,35 @@ +**Added:** + +* Added ``add_constraint`` method to ``RecipeOrganizer``. +* Added ``remove_constraint`` method to ``RecipeOrganizer``. +* Added ``add_soft_bounds`` method to ``RecipeOrganizer``. +* Added ``remove_soft_bounds`` method to ``RecipeOrganizer``. +* Added ``register_soft_bounds`` method to ``RecipeOrganizer``. +* Added ``clear_all_soft_bounds`` method to ``RecipeOrganizer``. +* Added ``get_equation_from_string`` method to ``RecipeOrganizer``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``constrain`` method of ``RecipeOrganizer``. Use ``add_constraint`` instead. +* Deprecated ``unconstrain`` method of ``RecipeOrganizer``. Use ``remove_constraint`` instead. +* Deprecated ``restrain`` method of ``RecipeOrganizer``. Use ``add_soft_bounds`` instead. +* Deprecated ``unrestrain`` methods of ``RecipeOrganizer``. Use ``remove_soft_bounds`` instead. +* Deprecated ``addRestraint`` method of ``RecipeOrganizer``. Use ``register_soft_bounds`` instead. +* Deprecate ``clearRestraints`` method of ``RecipeOrganizer``. Use ``clear_all_soft_bounds`` instead. +* Deprecated ``equationFromString`` method of ``RecipeOrganizer``. Use ``get_equation_from_string`` instead. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/res-dict.rst b/news/res-dict.rst new file mode 100644 index 00000000..6ebe11fc --- /dev/null +++ b/news/res-dict.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``FitResults.get_results_dictionary`` in replace of ``resultsDictionary``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``resultsDictionary`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/setequation-dep.rst b/news/setequation-dep.rst new file mode 100644 index 00000000..cb6fe13b --- /dev/null +++ b/news/setequation-dep.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``set_equation`` method to replace ``setEquation``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``setEquation`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/setvalue-dep.rst b/news/setvalue-dep.rst new file mode 100644 index 00000000..83121f06 --- /dev/null +++ b/news/setvalue-dep.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``set_value`` method to ``diffpy.srfit.fitbase.Parameter``. + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``setValue`` method in ``diffpy.srfit.fitbase.Parameter`` for removal in 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/update030.rst b/news/update030.rst new file mode 100644 index 00000000..3ce80500 --- /dev/null +++ b/news/update030.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Update codebase to scikit-package 0.3.0 standards. + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index fbbe0c3e..677cc175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,13 +6,17 @@ build-backend = "setuptools.build_meta" name = "diffpy.srfit" dynamic=['version', 'dependencies'] authors = [ - { name="Simon Billinge", email="sb2896@columbia.edu" }, + {name='Christopher Farrow', email='farrowch@gmail.com'}, + {name='Pavol Juhas', email='pavol.juhas@gmail.com'}, + {name='Caden Myers', email='cjm2304@columbia.edu'}, + {name='Simon J. L. Billinge', email='sbillinge@ucsb.edu'}, ] maintainers = [ - { name="Simon Billinge", email="sb2896@columbia.edu" }, + {name='Caden Myers', email='cjm2304@columbia.edu'}, + {name='Simon J. L. Billinge', email='sbillinge@ucsb.edu'}, ] -description = "Configurable code for solving atomic structures." -keywords = ['regression', 'modelling', 'fitting', 'diffraction', 'PDF'] +description = "Generalized code base for modeling problems." +keywords = ['regression', 'modeling', 'fitting', 'diffraction', 'PDF'] readme = "README.rst" requires-python = ">=3.11, <3.14" classifiers = [ @@ -48,6 +52,9 @@ include = ["*"] # package names should match these glob patterns (["*"] by defa exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) +[project.scripts] +diffpy-srfit = "diffpy.srfit.app:main" + [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} @@ -56,6 +63,11 @@ exclude-file = ".codespell/ignore_lines.txt" ignore-words = ".codespell/ignore_words.txt" skip = "*.cif,*.dat" +[tool.docformatter] +recursive = true +wrap-summaries = 72 +wrap-descriptions = 72 + [tool.black] line-length = 79 include = '\.pyi?$' diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 3254c0a6..22cbdcc6 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -4,8 +4,10 @@ # (c) 2008-2025 The Trustees of Columbia University in the City of New York. # All rights reserved. # -# File coded by: Chris Farrow and Billinge Group members and community -# contributors. +# (c) 2025-present The DiffPy Team. +# +# File coded by: Christopher Farrow, Pavol Juhas, Caden Myers, +# Simon J. L. Billinge, and members of the DiffPy community. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.srfit/graphs/contributors diff --git a/src/diffpy/srfit/__init__.py b/src/diffpy/srfit/__init__.py index 0374fdcf..d8b19522 100644 --- a/src/diffpy/srfit/__init__.py +++ b/src/diffpy/srfit/__init__.py @@ -3,9 +3,10 @@ # # (c) 2008-2025 The Trustees of Columbia University in the City of New York. # All rights reserved. +# (c) 2026-present The DiffPy Team. All rights reserved. # -# File coded by: Christopher Farrow, Pavol Juhas, and members of the -# Billinge Group. +# File coded by: Christopher Farrow, Pavol Juhas, Caden Myers, +# Simon J. L. Billinge, and members of the DiffPy community. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.srfit/graphs/contributors @@ -13,27 +14,10 @@ # See LICENSE.rst for license information. # ############################################################################## -"""Complex modeling framework for structure refinement and solution. - -SrFit is a tool for coherently combining known information about a -material to derive other properties, in particular material structure. -SrFit allows the customization and creation of structure -representations, profile calculators, constraints, restraints and file -input parsers. The customized pieces can be glued together within SrFit -to optimize a structure, or other physically relevant information from -one or more experimental profiles. Other known information about the -system of interest can be included with arbitrarily complex constraints -and restraints. In this way, the end user creates a customized fitting -application that suits the problem to the available information. - -The subpackages herein define various pieces of the SrFit framework. -Developers are encouraged to work through the examples described in the -documentation to learn how to use and customize the various parts of -SrFit. -""" +"""Generalized code base for modeling problems.""" # package version -from diffpy.srfit.version import __version__ +from diffpy.srfit.version import __version__ # noqa # silence the pyflakes syntax checker assert __version__ or True diff --git a/src/diffpy/srfit/equation/builder.py b/src/diffpy/srfit/equation/builder.py index 4485087f..a41f6bda 100644 --- a/src/diffpy/srfit/equation/builder.py +++ b/src/diffpy/srfit/equation/builder.py @@ -53,12 +53,12 @@ > # sin is defined in this module as an OperatorBuilder > sin = getBuilder("sin") > beq = A*sin(a*x) -> eq = beq.getEquation() +> eq = beq.get_equation() The equation builder can also handle scalar constants. Staring with the above setup: > beq2 = A*sin(a*x) + 3 -> eq2 = beq2.getEquation() +> eq2 = beq2.get_equation() Here, we didn't have to wrap '3' in an ArgumentBuilder. Non scalars, constant or otherwise, must be wrapped as ArgumentBuilders in order to be used in this way. @@ -75,6 +75,7 @@ > beq = c*f(a,b) > eq = beq.makeEquation() """ + import inspect import numbers import token @@ -86,6 +87,7 @@ import diffpy.srfit.equation.literals as literals from diffpy.srfit.equation.equationmod import Equation from diffpy.srfit.equation.literals.literal import Literal +from diffpy.utils._deprecator import build_deprecation_message, deprecated __all__ = [ "EquationFactory", @@ -105,6 +107,16 @@ _builders = {} +EquationFactory_base = "diffpy.srfit.equation.builder.EquationFactory" +removal_version = "4.0.0" + +registerFunction_dep_msg = build_deprecation_message( + EquationFactory_base, + "registerFunction", + "register_function", + removal_version, +) + class EquationFactory(object): """A Factory for equations. @@ -172,7 +184,7 @@ def makeEquation( lit = literals.Argument(value=beq, const=True) eq = Equation(name="", root=lit) else: - eq = beq.getEquation() + eq = beq.get_equation() self.equations.add(eq) return eq @@ -207,23 +219,26 @@ def registerOperator(self, name, op): opbuilder = wrapOperator(name, op) return self.registerBuilder(name, opbuilder) - def registerFunction(self, name, func, argnames): + def register_function(self, name, func, argnames): """Register a named function with the factory. This will register a builder for the function. - Attributes + Parameters ---------- - name + name : str The name of the function - func - A callable python object - argnames + func : callable + The callable python object + argnames : list of str The argument names for func. If these names do not correspond to builders, then new constants with value 0 will be created for each name. - Returns the registered builder. + Returns + ------- + registered_builder : OperatorBuilder + The registered builder. """ for n in argnames: if n not in self.builders: @@ -233,11 +248,23 @@ def registerFunction(self, name, func, argnames): builder = self.builders[argname] argliteral = builder.literal opbuilder.literal.addLiteral(argliteral) + registered_builder = self.registerBuilder(name, opbuilder) + return registered_builder - return self.registerBuilder(name, opbuilder) + @deprecated(registerFunction_dep_msg) + def registerFunction(self, name, func, argnames): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_function + instead. + """ + return self.register_function(name, func, argnames) def registerBuilder(self, name, builder): - """Register builder in this module so it can be used in makeEquation. + """Register builder in this module so it can be used in + makeEquation. If an extant builder with the given name is already registered, this will replace all instances of the old builder's literal in @@ -281,7 +308,8 @@ def deRegisterBuilder(self, name): return def wipeout(self, eq): - """Invalidate the specified equation and remove it from the factory. + """Invalidate the specified equation and remove it from the + factory. This will remove the equation from the purview of the factory and also change its formula to return NaN. This ensures that eq @@ -404,6 +432,16 @@ def _get_undefined_args(self, eqstr): # End class EquationFactory +base_basebuilder = "diffpy.srfit.equation.builder.BaseBuilder" +removal_version = "4.0.0" + +getequation_dep_msg = build_deprecation_message( + base_basebuilder, + "getEquation", + "get_equation", + removal_version, +) + class BaseBuilder(object): """Class for building equations. @@ -430,7 +468,7 @@ def __call__(self, *args): ) raise TypeError(m) - def getEquation(self): + def get_equation(self): """Get the equation built by this object. The equation will given the name "_eq_" where "" is @@ -441,12 +479,22 @@ def getEquation(self): eq = Equation(name, self.literal) return eq + @deprecated(getequation_dep_msg) + def getEquation(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.builder.BaseBuilder.get_equation + instead. + """ + return self.get_equation() + def __eval_binary(self, other, OperatorClass, onleft=True): """Evaluate a binary function. Other can be an BaseBuilder or a constant. - Attributes + Parameters ---------- onleft Indicates that the operator was passed on the left side @@ -555,7 +603,8 @@ class ArgumentBuilder(BaseBuilder): """ def __init__(self, value=None, name=None, const=False, arg=None): - """Create an ArgumentBuilder instance, containing a new Argument. + """Create an ArgumentBuilder instance, containing a new + Argument. Parameters ---------- @@ -618,7 +667,7 @@ def __call__(self, *args): This creates a new builder that encapsulates the operation. - Attributes + Parameters ---------- args Arguments of the operation. @@ -682,7 +731,7 @@ def wrapOperator(name, op): def wrapFunction(name, func, nin=2, nout=1): """Wrap a function in an OperatorBuilder instance. - Attributes + Parameters ---------- name The name of the function @@ -711,8 +760,8 @@ def getBuilder(name): def __wrap_numpy_operators(): - """Export all numpy operators as OperatorBuilder instances in the module - namespace.""" + """Export all numpy operators as OperatorBuilder instances in the + module namespace.""" for name in dir(numpy): op = getattr(numpy, name) if isinstance(op, numpy.ufunc): diff --git a/src/diffpy/srfit/equation/equationmod.py b/src/diffpy/srfit/equation/equationmod.py index dd8293ec..5be7ba0c 100644 --- a/src/diffpy/srfit/equation/equationmod.py +++ b/src/diffpy/srfit/equation/equationmod.py @@ -25,8 +25,8 @@ these! > b = Argument(name="b") > add.addLiteral(a) > add.addLiteral(b) > # make an Equation instance and pass the root > eq = Equation(root = add) > eq(a=3, b=4) # returns 7 > eq(a=2) # remembers b=4, returns 6 > -eq.a.setValue(-3) > eq.b.setValue(3) > eq() # uses last assignment of a -and b, returns 0 +eq.a.set_value(-3) > eq.b.set_value(3) > eq() # uses last assignment of +a and b, returns 0 See the class documentation for more information. """ @@ -91,7 +91,7 @@ class Equation(Operator): def __init__(self, name=None, root=None): """Initialize. - Attributes + Parameters ---------- name A name for this Equation. @@ -193,14 +193,14 @@ def __call__(self, *args, **kw): if idx >= len(self.argdict): raise ValueError("Too many arguments") arg = self.args[idx] - arg.setValue(val) + arg.set_value(val) # Process kw for name, val in kw.items(): arg = self.argdict.get(name) if arg is None: raise ValueError("No argument named '%s' here" % name) - arg.setValue(val) + arg.set_value(val) self._value = self.root.getValue() return self._value diff --git a/src/diffpy/srfit/equation/literals/abcs.py b/src/diffpy/srfit/equation/literals/abcs.py index 605bc53e..82c633cc 100644 --- a/src/diffpy/srfit/equation/literals/abcs.py +++ b/src/diffpy/srfit/equation/literals/abcs.py @@ -16,14 +16,10 @@ __all__ = ["LiteralABC", "ArgumentABC", "OperatorABC"] +from abc import ABC, abstractmethod -from abc import ABCMeta, abstractmethod, abstractproperty -import six - - -@six.add_metaclass(ABCMeta) -class LiteralABC(object): +class LiteralABC(ABC): """Abstract Base Class for Literal. See Literal for usage. @@ -31,13 +27,19 @@ class LiteralABC(object): @abstractmethod def identify(self, visitor): + """Identify this literal using a visitor.""" pass @abstractmethod def getValue(self): + """Return the value of the literal.""" pass - name = abstractproperty(None, None) + @property + @abstractmethod + def name(self): + """Name of the literal.""" + pass # End class LiteralABC @@ -50,11 +52,21 @@ class ArgumentABC(LiteralABC): """ @abstractmethod - def setValue(self, value): + def set_value(self, value): + """Set the value of the argument.""" pass - const = abstractproperty(None, None) - value = abstractproperty(None, None) + @property + @abstractmethod + def const(self): + """Whether the argument is constant.""" + pass + + @property + @abstractmethod + def value(self): + """Value of the argument.""" + pass # End class ArgumentABC @@ -68,14 +80,44 @@ class OperatorABC(LiteralABC): @abstractmethod def addLiteral(self, literal): + """Add a literal argument to the operator.""" + pass + + @property + @abstractmethod + def args(self): + """Arguments of the operator.""" + pass + + @property + @abstractmethod + def nin(self): + """Number of input arguments.""" pass - args = abstractproperty(None, None) - nin = abstractproperty(None, None) - nout = abstractproperty(None, None) - operation = abstractproperty(None, None) - symbol = abstractproperty(None, None) - value = abstractproperty(None, None) + @property + @abstractmethod + def nout(self): + """Number of outputs.""" + pass + + @property + @abstractmethod + def operation(self): + """Callable implementing the operator.""" + pass + + @property + @abstractmethod + def symbol(self): + """Symbol representing the operator.""" + pass + + @property + @abstractmethod + def value(self): + """Value produced by the operator.""" + pass # End class OperatorABC diff --git a/src/diffpy/srfit/equation/literals/argument.py b/src/diffpy/srfit/equation/literals/argument.py index 60d639f2..c468d516 100644 --- a/src/diffpy/srfit/equation/literals/argument.py +++ b/src/diffpy/srfit/equation/literals/argument.py @@ -37,9 +37,9 @@ class Argument(Literal, ArgumentABC): A flag indicating whether this is considered a constant. Constants may be given special treatment by the Visitors. _value - The value of the Argument. Modified with 'setValue'. + The value of the Argument. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' and 'set_value'. """ const = None @@ -59,10 +59,10 @@ def getValue(self): """Get the value of this Literal.""" return self._value - def setValue(self, val): + def set_value(self, val): """Set the value of the Literal. - Attributes + Parameters ---------- val The value to assign @@ -77,7 +77,7 @@ def setValue(self, val): return value = property( - lambda self: self.getValue(), lambda self, val: self.setValue(val) + lambda self: self.getValue(), lambda self, val: self.set_value(val) ) diff --git a/src/diffpy/srfit/equation/literals/literal.py b/src/diffpy/srfit/equation/literals/literal.py index ebdc17a9..48c48eaa 100644 --- a/src/diffpy/srfit/equation/literals/literal.py +++ b/src/diffpy/srfit/equation/literals/literal.py @@ -26,7 +26,8 @@ class Literal(Observable, LiteralABC): - """Abstract class for equation pieces, such as operators and arguments. + """Abstract class for equation pieces, such as operators and + arguments. Literal derives from Observable. See diffpy.srfit.util.observable. diff --git a/src/diffpy/srfit/equation/literals/operators.py b/src/diffpy/srfit/equation/literals/operators.py index a7863808..bb830b05 100644 --- a/src/diffpy/srfit/equation/literals/operators.py +++ b/src/diffpy/srfit/equation/literals/operators.py @@ -141,7 +141,8 @@ def _loop_check(self, literal): class UnaryOperator(Operator): - """Abstract class for an unary operator with one input and one result. + """Abstract class for an unary operator with one input and one + result. This base class defines the `nin` and `nout` attributes. The derived concrete operator must provide the remaining abstract attributes @@ -154,7 +155,8 @@ class UnaryOperator(Operator): class BinaryOperator(Operator): - """Abstract class for a binary operator with two inputs and one result. + """Abstract class for a binary operator with two inputs and one + result. This base class defines the `nin` and `nout` attributes. The derived concrete operator must define the remaining abstract attributes @@ -345,7 +347,7 @@ def __init__(self, op): Arguments - Attributes + Parameters ---------- op A numpy ufunc @@ -359,7 +361,8 @@ def __init__(self, op): class ArrayOperator(Operator): - """Operator that will take parameters and turn them into an array.""" + """Operator that will take parameters and turn them into an + array.""" name = "array" symbol = "array" diff --git a/src/diffpy/srfit/equation/visitors/__init__.py b/src/diffpy/srfit/equation/visitors/__init__.py index ef50ec43..82239eb4 100644 --- a/src/diffpy/srfit/equation/visitors/__init__.py +++ b/src/diffpy/srfit/equation/visitors/__init__.py @@ -35,7 +35,7 @@ def getArgs(literal, getconsts=True): """Get the Arguments of a Literal tree. - Attributes + Parameters ---------- getconsts If True (default), then Arguments designated as constant @@ -50,7 +50,7 @@ def getArgs(literal, getconsts=True): def getExpression(literal, eqskip=None): """Get math expression string from the Literal tree object. - Attributes + Parameters ---------- eqskip regular expression pattern for Equation objects that should diff --git a/src/diffpy/srfit/equation/visitors/printer.py b/src/diffpy/srfit/equation/visitors/printer.py index 98cfab6a..4fcb717b 100644 --- a/src/diffpy/srfit/equation/visitors/printer.py +++ b/src/diffpy/srfit/equation/visitors/printer.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Printer visitor for printing the equation represented by a Literal tree. +"""Printer visitor for printing the equation represented by a Literal +tree. The Printer visitor creates a one-line representation of the Literal tree, which is valid as a string equivalent of the equation. @@ -28,8 +29,6 @@ class Printer(Visitor): """Printer for printing a Literal tree. - Attributes: - Attributes ---------- eqskip diff --git a/src/diffpy/srfit/equation/visitors/swapper.py b/src/diffpy/srfit/equation/visitors/swapper.py index a6135750..ddd49ef6 100644 --- a/src/diffpy/srfit/equation/visitors/swapper.py +++ b/src/diffpy/srfit/equation/visitors/swapper.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Swapper for replacing a Literal in an equation with another Literals.""" +"""Swapper for replacing a Literal in an equation with another +Literals.""" __all__ = ["Swapper"] @@ -20,7 +21,8 @@ class Swapper(Visitor): - """Swapper for swapping out one literal for another in a literal tree. + """Swapper for swapping out one literal for another in a literal + tree. Note that this cannot swap out a root node of a literal tree. This case must be tested for explicitly. @@ -36,7 +38,7 @@ class Swapper(Visitor): def __init__(self, oldlit, newlit): """Initialize. - Attributes + Parameters ---------- oldlit The literal to be replaced. diff --git a/src/diffpy/srfit/fitbase/__init__.py b/src/diffpy/srfit/fitbase/__init__.py index bdbcb2d1..ce68fa7d 100644 --- a/src/diffpy/srfit/fitbase/__init__.py +++ b/src/diffpy/srfit/fitbase/__init__.py @@ -39,6 +39,7 @@ "Profile", "ProfileGenerator", "SimpleRecipe", + "ProfileParser", ] from diffpy.srfit.fitbase.calculator import Calculator @@ -48,6 +49,7 @@ from diffpy.srfit.fitbase.fitresults import FitResults, initializeRecipe from diffpy.srfit.fitbase.profile import Profile from diffpy.srfit.fitbase.profilegenerator import ProfileGenerator +from diffpy.srfit.fitbase.profileparser import ProfileParser from diffpy.srfit.fitbase.simplerecipe import SimpleRecipe # End of file diff --git a/src/diffpy/srfit/fitbase/calculator.py b/src/diffpy/srfit/fitbase/calculator.py index 75083b4e..41ebde60 100644 --- a/src/diffpy/srfit/fitbase/calculator.py +++ b/src/diffpy/srfit/fitbase/calculator.py @@ -20,7 +20,7 @@ overloaded to accept external arguments. Calculators are used to wrap registered functions so that the function's Parameters are contained in an object specific to the function. A custom Calculator can be added to -another RecipeOrganizer with the 'registerCalculator' method. +another RecipeOrganizer with the 'register_calculator' method. """ __all__ = ["Calculator"] @@ -77,9 +77,9 @@ class Calculator(Operator, ParameterSet): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ # define abstract attributes from the Operator base. diff --git a/src/diffpy/srfit/fitbase/constraint.py b/src/diffpy/srfit/fitbase/constraint.py index 68171548..dab6002b 100644 --- a/src/diffpy/srfit/fitbase/constraint.py +++ b/src/diffpy/srfit/fitbase/constraint.py @@ -24,6 +24,24 @@ from diffpy.srfit.exceptions import SrFitError from diffpy.srfit.fitbase.validatable import Validatable +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.srfit.fitbase.constraint.Constraint" +removal_version = "4.0.0" + +constrain_deprecation_msg = build_deprecation_message( + base, + "constrain", + "add_constraint", + removal_version, +) + +unconstrain_deprecation_msg = build_deprecation_message( + base, + "unconstrain", + "remove_constraint", + removal_version, +) class Constraint(Validatable): @@ -47,13 +65,23 @@ def __init__(self): self.eq = None return - def constrain(self, par, eq): + def add_constraint(self, par, eq): """Constrain a Parameter according to an Equation. The parameter will be set constant once it is constrained. This will keep it from being constrained multiple times. - Raises a ValueError if par is const. + Parameters + ---------- + par : Parameter + The Parameter to constrain. + eq : Equation + The Equation to use to constrain the Parameter. + + Raises + ------ + ValueError + If par is constant or already constrained. """ if par.const: @@ -69,20 +97,44 @@ def constrain(self, par, eq): self.update() return - def unconstrain(self): - """Clear the constraint.""" + @deprecated(constrain_deprecation_msg) + def constrain(self, par, eq): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.constraint.Constraint.add_constraint + instead. + """ + self.add_constraint(par, eq) + return + + def remove_constraint(self): + """Clear the constraint from a Parameter.""" self.par.constrained = False self.par = None self.eq = None return + @deprecated(unconstrain_deprecation_msg) + def unconstrain(self): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.constraint.Constraint.remove_constraint + instead. + """ + self.remove_constraint() + return + def update(self): """Update the parameter according to the equation.""" # This will be evaluated quickly thanks to the Equation class. val = self.eq() # This will only change the Parameter if val is different from the # currently stored value. - self.par.setValue(val) + self.par.set_value(val) return def _validate(self): @@ -107,7 +159,7 @@ def _validate(self): # Try to get the value of eq. try: val = self.eq() - self.par.setValue(val) + self.par.set_value(val) except TypeError: raise SrFitError("eq cannot be evaluated") finally: diff --git a/src/diffpy/srfit/fitbase/fitcontribution.py b/src/diffpy/srfit/fitbase/fitcontribution.py index 71048092..710f492c 100644 --- a/src/diffpy/srfit/fitbase/fitcontribution.py +++ b/src/diffpy/srfit/fitbase/fitcontribution.py @@ -28,12 +28,20 @@ from diffpy.srfit.fitbase.parameter import ParameterProxy from diffpy.srfit.fitbase.parameterset import ParameterSet from diffpy.srfit.fitbase.profile import Profile -from diffpy.srfit.fitbase.recipeorganizer import equationFromString +from diffpy.srfit.fitbase.recipeorganizer import get_equation_from_string from diffpy.utils._deprecator import build_deprecation_message, deprecated base = "diffpy.srfit.fitbase.FitContribution" removal_version = "4.0.0" +setequation_dep_msg = build_deprecation_message( + base, + "setEquation", + "set_equation", + removal_version, +) + + setprofile_dep_msg = build_deprecation_message( base, "setProfile", @@ -41,6 +49,34 @@ removal_version, ) +addprofilegenerator_dep_msg = build_deprecation_message( + base, + "addProfileGenerator", + "add_profile_generator", + removal_version, +) + +getequation_dep_msg = build_deprecation_message( + base, + "getEquation", + "get_equation", + removal_version, +) + +setresidualequation_dep_msg = build_deprecation_message( + base, + "setResidualEquation", + "set_residual_equation", + removal_version, +) + +getresidualequation_dep_msg = build_deprecation_message( + base, + "getResidualEquation", + "get_residual_equation", + removal_version, +) + class FitContribution(ParameterSet): """FitContribution class. @@ -89,9 +125,9 @@ class FitContribution(ParameterSet): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ def __init__(self, name): @@ -111,7 +147,7 @@ def __init__(self, name): def set_profile(self, profile, xname=None, yname=None, dyname=None): """Assign the Profile for this FitContribution. - Attributes + Parameters ---------- profile A Profile that specifies the calculation points and that @@ -164,7 +200,7 @@ def set_profile(self, profile, xname=None, yname=None, dyname=None): # If we have _eq, but not _reseq, set the residual if self._eq is not None and self._reseq is None: - self.setResidualEquation("chiv") + self.set_residual_equation("chiv") return @@ -179,7 +215,7 @@ def setProfile(self, profile, xname=None, yname=None, dyname=None): profile, xname=xname, yname=yname, dyname=dyname ) - def addProfileGenerator(self, gen, name=None): + def add_profile_generator(self, gen, name=None): """Add a ProfileGenerator to be used by this FitContribution. The ProfileGenerator is given a name so that it can be used as part of @@ -191,7 +227,7 @@ def addProfileGenerator(self, gen, name=None): Calling addProfileGenerator sets the profile equation to call the calculator and if there is not a profile equation already. - Attributes + Parameters ---------- gen A ProfileGenerator instance @@ -219,24 +255,36 @@ def addProfileGenerator(self, gen, name=None): # Make this our equation if we don't have one. This will set the # residual equation if necessary. if self._eq is None: - self.setEquation(name) + self.set_equation(name) return - def setEquation(self, eqstr, ns={}): + @deprecated(addprofilegenerator_dep_msg) + def addProfileGenerator(self, gen, name=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.FitContribution.add_profile_generator + instead. + """ + self.add_profile_generator(gen, name=name) + return + + def set_equation(self, eqstr, ns={}): """Set the profile equation for the FitContribution. This sets the equation that will be used when generating the residual for this FitContribution. The equation will be usable within - setResidualEquation as "eq", and it takes no arguments. + set_residual_equation as "eq", and it takes no arguments. - Attributes + Parameters ---------- eqstr A string representation of the equation. Any Parameter registered by addParameter or setProfile, or function - registered by setCalculator, registerFunction or - registerStringFunction can be can be used in the equation + registered by setCalculator, register_function or + register_string_function can be can be used in the equation by name. Other names will be turned into Parameters of this FitContribution. ns @@ -248,7 +296,9 @@ def setEquation(self, eqstr, ns={}): variable. """ # Build the equation instance. - eq = equationFromString(eqstr, self._eqfactory, buildargs=True, ns=ns) + eq = get_equation_from_string( + eqstr, self._eqfactory, buildargs=True, ns=ns + ) eq.name = "eq" # Register any new Parameters. @@ -262,11 +312,22 @@ def setEquation(self, eqstr, ns={}): # Set the residual if we need to if self.profile is not None and self._reseq is None: - self.setResidualEquation("chiv") + self.set_residual_equation("chiv") return - def getEquation(self): + @deprecated(setequation_dep_msg) + def setEquation(self, eqstr, ns={}): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitContribution.set_equation + instead. + """ + self.set_equation(eqstr, ns=ns) + return + + def get_equation(self): """Get math expression string for the active profile equation. Return normalized math expression or an empty string if profile @@ -279,10 +340,20 @@ def getEquation(self): rv = getExpression(self._eq) return rv - def setResidualEquation(self, eqstr): + @deprecated(getequation_dep_msg) + def getEquation(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitContribution.get_equation + instead. + """ + return self.get_equation() + + def set_residual_equation(self, eqstr): """Set the residual equation for the FitContribution. - Attributes + Parameters ---------- eqstr A string representation of the residual. If eqstr is None @@ -317,13 +388,25 @@ def setResidualEquation(self, eqstr): elif eqstr == "resv": eqstr = resvstr - reseq = equationFromString(eqstr, self._eqfactory) + reseq = get_equation_from_string(eqstr, self._eqfactory) self._eqfactory.wipeout(self._reseq) self._reseq = reseq return - def getResidualEquation(self): + @deprecated(setresidualequation_dep_msg) + def setResidualEquation(self, eqstr): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.FitContribution.set_residual_equation + instead. + """ + self.set_residual_equation(eqstr) + return + + def get_residual_equation(self): """Get math expression string for the active residual equation. Return normalized math formula or an empty string if residual @@ -336,6 +419,18 @@ def getResidualEquation(self): rv = getExpression(self._reseq, eqskip="eq$") return rv + @deprecated(getresidualequation_dep_msg) + def getResidualEquation(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.FitContribution.get_residual_equation + instead. + """ + + return self.get_residual_equation() + def residual(self): """Calculate the residual for this fitcontribution. @@ -347,7 +442,7 @@ def residual(self): chiv = (eq() - self.profile.y) / self.profile.dy The value that is optimized is dot(chiv, chiv). - The residual equation can be changed with the setResidualEquation + The residual equation can be changed with the set_residual_equation method. """ # Assign the calculated profile @@ -357,7 +452,8 @@ def residual(self): return self._reseq() def evaluate(self): - """Evaluate the contribution equation and update profile.ycalc.""" + """Evaluate the contribution equation and update + profile.ycalc.""" yc = self._eq() if self.profile is not None: self.profile.ycalc = yc diff --git a/src/diffpy/srfit/fitbase/fithook.py b/src/diffpy/srfit/fitbase/fithook.py index 14c5811c..43e2c84e 100644 --- a/src/diffpy/srfit/fitbase/fithook.py +++ b/src/diffpy/srfit/fitbase/fithook.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""The FitHook class for inspecting the progress of a FitRecipe refinement. +"""The FitHook class for inspecting the progress of a FitRecipe +refinement. FitHooks are called by a FitRecipe during various times of the residual is evaluation. The default FitHook simply counts the number of times the @@ -37,10 +38,10 @@ class FitHook(object): """Base class for inspecting the progress of a FitRecipe refinement. Can serve as a fithook for the FitRecipe class (see - FitRecipe.pushFitHook method.) The methods in this class are called - during the preparation of the FitRecipe for refinement, and during - the residual call. See the class methods for a description of their - purpose. + FitRecipe.push_fit_hook method.) The methods in this class are + called during the preparation of the FitRecipe for refinement, and + during the residual call. See the class methods for a description of + their purpose. """ def reset(self, recipe): @@ -54,9 +55,10 @@ def reset(self, recipe): return def precall(self, recipe): - """This is called within FitRecipe.residual, before the calculation. + """This is called within FitRecipe.residual, before the + calculation. - Attributes + Parameters ---------- recipe The FitRecipe instance @@ -64,9 +66,10 @@ def precall(self, recipe): return def postcall(self, recipe, chiv): - """This is called within FitRecipe.residual, after the calculation. + """This is called within FitRecipe.residual, after the + calculation. - Attributes + Parameters ---------- recipe The FitRecipe instance @@ -85,8 +88,6 @@ class PrintFitHook(FitHook): This FitHook prints out a running count of the number of times the residual has been called, or other information, based on the verbosity. - Attributes - Attributes ---------- count @@ -121,9 +122,10 @@ def reset(self, recipe): return def precall(self, recipe): - """This is called within FitRecipe.residual, before the calculation. + """This is called within FitRecipe.residual, before the + calculation. - Attributes + Parameters ---------- recipe The FitRecipe instance @@ -134,9 +136,10 @@ def precall(self, recipe): return def postcall(self, recipe, chiv): - """This is called within FitRecipe.residual, after the calculation. + """This is called within FitRecipe.residual, after the + calculation. - Attributes + Parameters ---------- recipe The FitRecipe instance @@ -164,8 +167,8 @@ def postcall(self, recipe, chiv): if self.verbose >= 3: print("Variables") - vnames = recipe.getNames() - vals = recipe.getValues() + vnames = recipe.get_names() + vals = recipe.get_values() # byname = _byname() items = sorted(zip(vnames, vals), key=_byname) for name, val in items: @@ -227,11 +230,12 @@ def reset(self, recipe): return def postcall(self, recipe, chiv): - """This is called within FitRecipe.residual, after the calculation. + """This is called within FitRecipe.residual, after the + calculation. Find data and plot it. - Attributes + Parameters ---------- recipe The FitRecipe instance diff --git a/src/diffpy/srfit/fitbase/fitrecipe.py b/src/diffpy/srfit/fitbase/fitrecipe.py index 4d7be2c9..d4fdf279 100644 --- a/src/diffpy/srfit/fitbase/fitrecipe.py +++ b/src/diffpy/srfit/fitbase/fitrecipe.py @@ -35,90 +35,175 @@ __all__ = ["FitRecipe"] from collections import OrderedDict +from pathlib import Path import matplotlib.pyplot as plt from bg_mpl_stylesheets.styles import all_styles from numpy import array, concatenate, dot, sqrt +import diffpy.srfit.util.inpututils as utils from diffpy.srfit.fitbase.fithook import PrintFitHook from diffpy.srfit.fitbase.parameter import ParameterProxy from diffpy.srfit.fitbase.recipeorganizer import RecipeOrganizer from diffpy.srfit.interface import _fitrecipe_interface from diffpy.srfit.util.tagmanager import TagManager +from diffpy.utils._deprecator import build_deprecation_message, deprecated plt.style.use(all_styles["bg-style"]) +base = "diffpy.srfit.fitbase.FitRecipe" +removal_version = "4.0.0" +addcontrib_dep_msg = build_deprecation_message( + base, "addContribution", "add_contribution", removal_version +) + +pushfithook_dep_msg = build_deprecation_message( + base, "pushFitHook", "push_fit_hook", removal_version +) + +popfithook_dep_msg = build_deprecation_message( + base, "popFitHook", "pop_fit_hook", removal_version +) + +getfithooks_dep_msg = build_deprecation_message( + base, "getFitHooks", "get_fit_hooks", removal_version +) + +clearfithooks_dep_msg = build_deprecation_message( + base, "clearFitHooks", "clear_fit_hooks", removal_version +) + +setweight_dep_msg = build_deprecation_message( + base, "setWeight", "set_weight", removal_version +) + +addparset_dep_msg = build_deprecation_message( + base, "addParameterSet", "add_parameter_set", removal_version +) + +removeParameterSet_dep_msg = build_deprecation_message( + base, "removeParameterSet", "remove_parameter_set", removal_version +) + +scalarResidual_dep_msg = build_deprecation_message( + base, "scalarResidual", "scalar_residual", removal_version +) + +addVar_dep_msg = build_deprecation_message( + base, "addVar", "add_variable", removal_version +) + +delVar_dep_msg = build_deprecation_message( + base, "delVar", "delete_variable", removal_version +) + +newVar_dep_msg = build_deprecation_message( + base, "newVar", "create_new_variable", removal_version +) + +isFree_dep_msg = build_deprecation_message( + base, "isFree", "is_free", removal_version +) + +getValues_dep_msg = build_deprecation_message( + base, "getValues", "get_values", removal_version +) + +getNames_dep_msg = build_deprecation_message( + base, "getNames", "get_names", removal_version +) + +getBounds_dep_msg = build_deprecation_message( + base, "getBounds", "get_bounds_pairs", removal_version +) + +getBounds2_dep_msg = build_deprecation_message( + base, "getBounds2", "get_bounds_array", removal_version +) + +boundsToRestraints_dep_msg = build_deprecation_message( + base, "boundsToRestraints", "convert_bounds_to_restraints", removal_version +) + +constrain_dep_msg = build_deprecation_message( + base, "constrain", "add_constraint", removal_version +) + +unconstrain_dep_msg = build_deprecation_message( + base, "unconstrain", "remove_constraint", removal_version +) + class FitRecipe(_fitrecipe_interface, RecipeOrganizer): """FitRecipe class. Attributes ---------- - name + name : str A name for this FitRecipe. - fithooks - List of FitHook instances that can pass information out - of the system during a refinement. By default, the is + fithooks : list + The list of FitHook instances that can pass information out + of the system during a refinement. By default, this is populated by a PrintFitHook instance. - _constraints - A dictionary of Constraints, indexed by the constrained + _constraints : dict + The dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the 'constrain' method. - _oconstraints - An ordered list of the constraints from this and all + _oconstraints : list + The ordered list of the constraints from this and all sub-components. - _calculators - A managed dictionary of Calculators. - _contributions - A managed OrderedDict of FitContributions. - _parameters - A managed OrderedDict of parameters (in this case the + _calculators : dict + The managed dictionary of Calculators. + _contributions : OrderedDict + The managed OrderedDict of FitContributions. + _parameters : OrderedDict + The managed OrderedDict of parameters (in this case the parameters are varied). - _parsets - A managed dictionary of ParameterSets. - _eqfactory - A diffpy.srfit.equation.builder.EquationFactory + _parsets : dict + The managed dictionary of ParameterSets. + _eqfactory : diffpy.srfit.equation.builder.EquationFactory + The diffpy.srfit.equation.builder.EquationFactory instance that is used to create constraints and - restraints from string - _restraintlist - A list of restraints from this and all sub-components. - _restraints - A set of Restraints. Restraints can be added using the + restraints from strings. + _restraintlist : list + The list of restraints from this and all sub-components. + _restraints : set + The set of Restraints. Restraints can be added using the 'restrain' or 'confine' methods. - _ready - A flag indicating if all attributes are ready for the + _ready : bool + The flag indicating if all attributes are ready for the calculation. - _tagmanager - A TagManager instance for managing tags on Parameters. - _weights - List of weighing factors for each FitContribution. The + _tagmanager : TagManager + The TagManager instance for managing tags on Parameters. + _weights : list + The list of weighing factors for each FitContribution. The weights are multiplied by the residual of the FitContribution when determining the overall residual. - _fixedtag + _fixedtag : str "__fixed", used for tagging variables as fixed. Don't use this tag unless you want issues. Properties ---------- - names - Variable names (read only). See getNames. - values - Variable values (read only). See getValues. - fixednames - Names of the fixed refinable variables (read only). - fixedvalues - Values of the fixed refinable variables (read only). - bounds - Bounds on parameters (read only). See getBounds. - bounds2 - Bounds on parameters (read only). See getBounds2. + names : list + The variable names (read only). See get_names. + values : numpy.ndarray + The variable values (read only). See get_values. + fixednames : list + The names of the fixed refinable variables (read only). + fixedvalues : numpy.ndarray + The values of the fixed refinable variables (read only). + bounds : list of tuple + The bounds on parameters (read only). See get_bounds_pairs. + bounds2 : tuple of numpy.ndarray + The bounds on parameters (read only). See get_bounds_array. """ fixednames = property( lambda self: [ v.name for v in self._parameters.values() - if not (self.isFree(v) or self.isConstrained(v)) + if not (self.is_free(v) or self.is_constrained(v)) ], doc="names of the fixed refinable variables", ) @@ -127,13 +212,13 @@ class FitRecipe(_fitrecipe_interface, RecipeOrganizer): [ v.value for v in self._parameters.values() - if not (self.isFree(v) or self.isConstrained(v)) + if not (self.is_free(v) or self.is_constrained(v)) ] ), doc="values of the fixed refinable variables", ) - bounds = property(lambda self: self.getBounds()) - bounds2 = property(lambda self: self.getBounds2()) + bounds = property(lambda self: self.get_bounds_pairs()) + bounds2 = property(lambda self: self.get_bounds_array()) def __init__(self, name="fit"): """Initialization.""" @@ -184,7 +269,7 @@ def __init__(self, name="fit"): } return - def pushFitHook(self, fithook, index=None): + def push_fit_hook(self, fithook, index=None): """Add a FitHook to be called within the residual method. The hook is an object for reporting updates, or more fundamentally, @@ -192,12 +277,12 @@ def pushFitHook(self, fithook, index=None): diffpy.srfit.fitbase.fithook.FitHook class for the required interface. Added FitHooks will be called sequentially during refinement. - Attributes + Parameters ---------- - fithook - FitHook instance to add to the sequence - index - Index for inserting fithook into the list of fit hooks. If + fithook : diffpy.srfit.fitbase.fithook.FitHook + The FitHook instance to add to the sequence. + index : int or None, optional + The index for inserting fithook into the list of fit hooks. If this is None (default), the fithook is added to the end. """ if index is None: @@ -207,21 +292,33 @@ def pushFitHook(self, fithook, index=None): self._update_configuration() return - def popFitHook(self, fithook=None, index=-1): + @deprecated(pushfithook_dep_msg) + def pushFitHook(self, fithook, index=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.push_fit_hook instead. + """ + self.push_fit_hook(fithook, index) + return + + def pop_fit_hook(self, fithook=None, index=-1): """Remove a FitHook by index or reference. - Attributes + Parameters ---------- - fithook - FitHook instance to remove from the sequence. If this is + fithook : diffpy.srfit.fitbase.fithook.FitHook or None, optional + The FitHook instance to remove from the sequence. If this is None (default), default to index. - index - Index of FitHook instance to remove (default -1). - - - Raises ValueError if fithook is not None, but is not present in the - sequence. - Raises IndexError if the sequence is empty or index is out of range. + index : int, optional + The index of FitHook instance to remove (default -1). + + Raises + ------ + ValueError + If fithook is not None, but is not present in the sequence. + IndexError + If the sequence is empty or index is out of range. """ if fithook is not None: self.fithooks.remove(fithook) @@ -229,78 +326,179 @@ def popFitHook(self, fithook=None, index=-1): self.fithook.remove(index) return - def getFitHooks(self): + @deprecated(popfithook_dep_msg) + def popFitHook(self, fithook=None, index=-1): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.pop_fit_hook instead. + """ + self.pop_fit_hook(fithook, index) + return + + def get_fit_hooks(self): """Get the sequence of FitHook instances.""" return self.fithooks[:] - def clearFitHooks(self): + @deprecated(getfithooks_dep_msg) + def getFitHooks(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.get_fit_hooks instead.""" + return self.get_fit_hooks() + + def clear_fit_hooks(self): """Clear the FitHook sequence.""" del self.fithooks[:] return - def addContribution(self, con, weight=1.0): + @deprecated(clearfithooks_dep_msg) + def clearFitHooks(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.clear_fit_hooks instead.""" + self.clear_fit_hooks() + return + + def add_contribution(self, con, weight=1.0): """Add a FitContribution to the FitRecipe. - Attributes + Parameters ---------- - con + con : FitContribution The FitContribution to be stored. - - - Raises ValueError if the FitContribution has no name - Raises ValueError if the FitContribution has the same name as some - other managed object. + weight : float, optional + The weight of the FitContribution. Default is 1.0. + + Raises + ------ + ValueError + If the FitContribution has no name or if the FitContribution has + the same name as some other managed object. """ self._add_object(con, self._contributions, True) self._weights.append(weight) return - def setWeight(self, con, weight): - """Set the weight of a FitContribution.""" + @deprecated(addcontrib_dep_msg) + def addContribution(self, con, weight=1.0): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.add_contribution + instead. + """ + self.add_contribution(con, weight) + return + + def set_weight(self, con, weight): + """Set the weight of a FitContribution. + + Parameters + ---------- + con : FitContribution + The FitContribution object whose weight is to be set. + weight : float + The weight value to assign to the specified FitContribution. + + Returns + ------- + None + """ idx = list(self._contributions.values()).index(con) self._weights[idx] = weight return - def addParameterSet(self, parset): + @deprecated(setweight_dep_msg) + def setWeight(self, con, weight): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.set_weight instead.""" + self.set_weight(con, weight) + return + + def add_parameter_set(self, parset): """Add a ParameterSet to the hierarchy. - Attributes + Parameters ---------- - parset + parset : ParameterSet The ParameterSet to be stored. - - Raises ValueError if the ParameterSet has no name. - Raises ValueError if the ParameterSet has the same name as some other - managed object. + Raises + ------ + ValueError + If the ParameterSet has no name or if the ParameterSet has the same + name as some other managed object. """ self._add_object(parset, self._parsets, True) return - def removeParameterSet(self, parset): + @deprecated(addparset_dep_msg) + def addParameterSet(self, parset): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.add_parameter_set instead. + """ + self.add_parameter_set(parset) + return + + def remove_parameter_set(self, parset): """Remove a ParameterSet from the hierarchy. - Raises ValueError if parset is not managed by this object. + This method removes the specified ParameterSet object from the internal + hierarchy of managed ParameterSets. If the provided ParameterSet is not + currently managed by this object, a ValueError will be raised. + + Parameters: + ----------- + parset : ParameterSet + The ParameterSet instance to be removed from the hierarchy. + + Raises: + ------- + ValueError + If the provided ParameterSet is not managed by this object. """ self._remove_object(parset, self._parsets) return + @deprecated(removeParameterSet_dep_msg) + def removeParameterSet(self, parset): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.remove_parameter_set instead. + """ + self.remove_parameter_set(parset) + return + def residual(self, p=[]): """Calculate the vector residual to be optimized. + The residual is by default the weighted concatenation of each + FitContribution's residual, plus the value of each restraint. The array + returned, denoted chiv, is such that + dot(chiv, chiv) = chi^2 + restraints. + Parameters ---------- - p + p : list or numpy.ndarray The list of current variable values, provided in the same order as the '_parameters' list. If p is an empty iterable (default), then it is assumed that the parameters have already been updated in some other way, and the explicit update within this function is skipped. - The residual is by default the weighted concatenation of each - FitContribution's residual, plus the value of each restraint. The array - returned, denoted chiv, is such that - dot(chiv, chiv) = chi^2 + restraints. + Return + ------ + chiv : numpy.ndarray + The array of residuals to be optimized. The array is such that + dot(chiv, chiv) = chi^2 + restraints. """ # Prepare, if necessary @@ -336,12 +534,12 @@ def residual(self, p=[]): return chiv - def scalarResidual(self, p=[]): + def scalar_residual(self, p=[]): """Calculate the scalar residual to be optimized. Parameters ---------- - p + p : list or numpy.ndarray The list of current variable values, provided in the same order as the '_parameters' list. If p is an empty iterable (default), then it is assumed that the parameters have already been @@ -356,9 +554,19 @@ def scalarResidual(self, p=[]): chiv = self.residual(p) return dot(chiv, chiv) + @deprecated(scalarResidual_dep_msg) + def scalarResidual(self, p=[]): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.scalar_residual + instead. + """ + return self.scalar_residual(p) + def __call__(self, p=[]): - """Same as scalarResidual method.""" - return self.scalarResidual(p) + """Same as scalar_residual method.""" + return self.scalar_residual(p) def _prepare(self): """Prepare for the residual calculation, if necessary. @@ -369,7 +577,10 @@ def _prepare(self): This updates the local restraints with those of the contributions. - Raises AttributeError if there are variables without a value. + Raises + ------ + AttributeError + If there are variables without a value. """ # Only prepare if the configuration has changed within the recipe @@ -425,7 +636,7 @@ def __verify_parameters(self): # Get all parameters with a value of None badpars = [] - for par in self.iterPars(): + for par in self.iterate_over_parameters(): try: par.getValue() except ValueError: @@ -515,60 +726,57 @@ def cmp(x, y): # Variable manipulation - def addVar( + def add_variable( self, par, value=None, name=None, fixed=False, tag=None, tags=[] ): """Add a variable to be refined. - Attributes + Parameters ---------- - par - A Parameter that will be varied during a fit. - value - An initial value for the variable. If this is None + par : diffpy.srfit.fitbase.Parameter + The Parameter that will be varied during a fit. + value : float or None, optional + The initial value for the variable. If this is None (default), then the current value of par will be used. - name - A name for this variable. If name is None (default), then + name : str or None, optional + The name for this variable. If name is None (default), then the name of the parameter will be used. - fixed + fixed : bool, optional Fix the variable so that it does not vary (default False). - tag - A tag for the variable. This can be used to retrieve, fix + tag : str or None, optional + The tag for the variable. This can be used to retrieve, fix or free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". - tags - A list of tags (default []). Both tag and tags can be + tags : list of str, optional + The list of tags (default []). Both tag and tags can be applied. - Returns ------- - vars + ParameterProxy ParameterProxy (variable) for the passed Parameter. - - Raises ValueError if the name of the variable is already taken by - another managed object. - Raises ValueError if par is constant. - Raises ValueError if par is constrained. + Raises + ------ + ValueError + If the name of the variable is already taken by + another managed object. + ValueError + If par is constant. + ValueError + If par is constrained. """ name = name or par.name - if par.const: raise ValueError("The parameter '%s' is constant" % par) - if par.constrained: raise ValueError("The parameter '%s' is constrained" % par) - var = ParameterProxy(name, par) if value is not None: - var.setValue(value) - + var.set_value(value) self._add_parameter(var) - if fixed: self.fix(var) - # Tag with passed tags and by name self._tagmanager.tag(var, var.name) self._tagmanager.tag(var, "all") @@ -577,33 +785,58 @@ def addVar( self._tagmanager.tag(var, tag) return var - def delVar(self, var): + @deprecated(addVar_dep_msg) + def addVar( + self, par, value=None, name=None, fixed=False, tag=None, tags=[] + ): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.add_variable instead. + """ + return self.add_variable(par, value, name, fixed, tag, tags) + + def delete_variable(self, var): """Remove a variable. Note that constraints and restraints involving the variable are not modified. - Attributes + Parameters ---------- - var + var : ParameterProxy A variable of the FitRecipe. - - Raises ValueError if var is not part of the FitRecipe. + Raises + ------ + ValueError + If var is not part of the FitRecipe. """ self._remove_parameter(var) self._tagmanager.untag(var) return + @deprecated(delVar_dep_msg) + def delVar(self, var): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.delete_variable instead. + """ + self.delete_variable(var) + return + def __delattr__(self, name): if name in self._parameters: - self.delVar(self._parameters[name]) + self.delete_variable(self._parameters[name]) return super(FitRecipe, self).__delattr__(name) return - def newVar(self, name, value=None, fixed=False, tag=None, tags=[]): + def create_new_variable( + self, name, value=None, fixed=False, tag=None, tags=[] + ): """Create a new variable of the fit. This method lets new variables be created that are not tied to a @@ -611,37 +844,37 @@ def newVar(self, name, value=None, fixed=False, tag=None, tags=[]): optimization routine, and therefore should only be created to be used in constraint or restraint equations. - Attributes + Parameters ---------- - name + name : str The name of the variable. The variable will be able to be used by this name in restraint and constraint equations. - value - An initial value for the variable. If this is None + value : float or None, optional + The initial value for the variable. If this is None (default), then the variable will be given the value of the first non-None-valued Parameter constrained to it. If this fails, an error will be thrown when 'residual' is called. - fixed + fixed : bool, optional Fix the variable so that it does not vary (default False). The variable will still be managed by the FitRecipe. - tag - A tag for the variable. This can be used to fix and free + tag : str or None, optional + The tag for the variable. This can be used to fix and free variables by tag (default None). Note that a variable is automatically tagged with its name and "all". - tags - A list of tags (default []). Both tag and tags can be + tags : list of str, optional + The list of tags (default []). Both tag and tags can be applied. - - Returns the new variable (Parameter instance). + Returns + ------- + Parameter + The new variable (Parameter instance). """ # This will fix the Parameter var = self._new_parameter(name, value) - # We may explicitly free it if not fixed: self.free(var) - # Tag with passed tags self._tagmanager.tag(var, *tags) if tag is not None: @@ -649,6 +882,15 @@ def newVar(self, name, value=None, fixed=False, tag=None, tags=[]): return var + @deprecated(newVar_dep_msg) + def newVar(self, name, value=None, fixed=False, tag=None, tags=[]): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.create_new_variable instead. + """ + return self.create_new_variable(name, value, fixed, tag, tags) + def _new_parameter(self, name, value, check=True): """Overloaded to tag variables. @@ -664,7 +906,7 @@ def _new_parameter(self, name, value, check=True): def __get_var_and_check(self, var): """Get the actual variable from var. - Attributes + Parameters ---------- var A variable of the FitRecipe, or the name of a variable. @@ -722,16 +964,51 @@ def __get_vars_from_args(self, *args, **kw): return varargs def fix(self, *args, **kw): - """Fix a parameter by reference, name or tag. + """Fix one or more parameters by reference, name, or tag. - A fixed variable is not refined. Variables are free by default. + This method marks specified parameters as fixed, meaning they will not + be refined during the fitting process. By default, all parameters are + free (not fixed). Parameters can be specified using their references, + names, or tags. Additionally, keyword arguments can be used to assign + specific values to the fixed parameters. - This method accepts string or variable arguments. An argument of - "all" selects all variables. Keyword arguments must be parameter - names, followed by a value to assign to the fixed variable. + Parameters + ---------- + *args : str or Parameter + The positional arguments specifying the parameters to fix. + These can be parameter objects, their names as strings, or + tags. The special string "all" can be used to select all + parameters. + **kw : dict + The keyword arguments where the keys are parameter names and + the values are the values to assign to the corresponding + fixed parameters. + + Raises + ------ + ValueError: + If an unknown parameter, name, or tag is passed, or if a + tag is passed as a keyword argument. + + Example + ------- + + :: + + # Fix a parameter by reference + recipe.fix(param1) + + # Fix a parameter by name + recipe.fix("param2") + + # Fix all parameters + recipe.fix("all") + + # Fix parameters by tag + recipe.fix(tag="group1") - Raises ValueError if an unknown Parameter, name or tag is - passed, or if a tag is passed in a keyword. + # Fix a parameter and assign it a value + recipe.fix(param3=10.0) """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) @@ -747,17 +1024,44 @@ def fix(self, *args, **kw): return def free(self, *args, **kw): - """Free a parameter by reference, name or tag. + """Free one or more parameters by reference, name, or tag. - A free variable is refined. Variables are free by default. - Constrained variables are not free. + This method marks specified parameters as free, allowing them to be + refined during the fitting process. By default, variables + are free unless they are constrained. Constrained variables + cannot be freed. - This method accepts string or variable arguments. An argument of - "all" selects all variables. Keyword arguments must be parameter - names, followed by a value to assign to the fixed variable. + Parameters + ---------- + *args : str or Parameter + The positional arguments specifying the parameters to free. + These can be: + - Parameter objects + - Names of parameters (as strings) + - Tags associated with parameters (as strings) + - The string "all" to select all parameters. + **kw : dict + The keyword arguments specifying parameter names as keys and + their values to assign after freeing. This is useful + for setting the value of a parameter while marking it as free. + + Raises + ------ + ValueError + If an unknown parameter, name, or tag is passed, or if a + tag is passed as a keyword argument. + + Notes + ----- + - Parameters that are already free will remain free. + - Tags associated with fixed parameters will be removed when they + are freed. + - If keyword arguments are provided, the corresponding parameter values + will be updated after freeing. - Raises ValueError if an unknown Parameter, name or tag is - passed, or if a tag is passed in a keyword. + Returns + ------- + None """ # Check the inputs and get the variables from them varargs = self.__get_vars_from_args(*args, **kw) @@ -773,23 +1077,51 @@ def free(self, *args, **kw): return - def isFree(self, var): - """Check if a variable is fixed.""" + def is_free(self, var): + """Determine if a variable is free (not fixed) in the fit + recipe. + + This method checks whether the specified variable does not have the + fixed tag associated with it, indicating that it is free to vary + during the fitting process. + + Parameters + ---------- + var : object + The variable to check. This is typically an instance of a parameter + or variable object used in the fit recipe. + + Returns + ------- + bool + True if the variable is free (not fixed), False otherwise. + """ return not self._tagmanager.hasTags(var, self._fixedtag) - def unconstrain(self, *pars): + @deprecated(isFree_dep_msg) + def isFree(self, var): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.is_free instead. + """ + return self.is_free(var) + + def remove_constraint(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. If the Parameter is also a variable of the recipe, it will be freed as well. - Attributes + Parameters ---------- - *pars - The names of Parameters or Parameters to unconstrain. + *pars : str or Parameter + The names of Parameters or Parameter objects to unconstrain. - - Raises ValueError if the Parameter is not constrained. + Raises + ------ + ValueError + If the Parameter is not constrained. """ update = False for par in pars: @@ -801,7 +1133,7 @@ def unconstrain(self, *pars): raise ValueError("The parameter cannot be found") if par in self._constraints: - self._constraints[par].unconstrain() + self._constraints[par].remove_constraint() del self._constraints[par] update = True @@ -814,7 +1146,18 @@ def unconstrain(self, *pars): return - def constrain(self, par, con, ns={}): + @deprecated(unconstrain_dep_msg) + def unconstrain(self, *pars): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.remove_constraint + instead. + """ + self.remove_constraint(*pars) + return + + def add_constraint(self, par, con, ns={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. @@ -823,26 +1166,29 @@ def constrain(self, par, con, ns={}): and its current value is None. A constrained variable will be set as fixed. - Attributes + Parameters ---------- - par + par : Parameter The Parameter to constrain. - con - A string representation of the constraint equation or a - Parameter to constrain to. A constraint equation must + con : str or Parameter + The string representation of the constraint equation or a + Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in the ns argument, or if they are managed by this object. - ns - A dictionary of Parameters, indexed by name, that are used + ns : dict, optional + The dictionary of Parameters, indexed by name, that are used in the eqstr, but not part of this object (default {}). - - Raises ValueError if ns uses a name that is already used for a - variable. - Raises ValueError if eqstr depends on a Parameter that is not part of - the FitRecipe and that is not defined in ns. - Raises ValueError if par is marked as constant. + Raises + ------ + ValueError + If ns uses a name that is already used for a variable. + ValueError + If eqstr depends on a Parameter that is not part of the FitRecipe + and that is not defined in ns. + ValueError + If par is marked as constant. """ if isinstance(par, str): name = par @@ -864,41 +1210,245 @@ def constrain(self, par, con, ns={}): val = con.getValue() if val is None: val = par.getValue() - con.setValue(val) + con.set_value(val) if par in self._parameters.values(): self.fix(par) - RecipeOrganizer.constrain(self, par, con, ns) + RecipeOrganizer.add_constraint(self, par, con, ns) return - def getValues(self): - """Get the current values of the variables in a list.""" - return array( - [v.value for v in self._parameters.values() if self.isFree(v)] + @deprecated(constrain_dep_msg) + def constrain(self, par, con, ns={}): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.add_constraint + instead. + """ + self.add_constraint(par, con, ns) + return + + def get_values(self): + """Retrieve the current values of all free variables in the fit + recipe. + + This method collects the values of all parameters that are marked as + free (i.e., adjustable during the fitting process) and returns them + as a NumPy array. + + Returns + ------- + + values_array : numpy.ndarray + The array containing the current values of all free + variables in the fit recipe. + """ + values_array = array( + [v.value for v in self._parameters.values() if self.is_free(v)] ) + return values_array + + @deprecated(getValues_dep_msg) + def getValues(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.get_values instead.""" + return self.get_values() + + def get_names(self): + """Retrieve the names of all free variables in the fit recipe. + + This method iterates through the parameters in the fit recipe and + returns a list of names for those variables that are marked as free. + + Returns + ------- + parameter_names :list of str + The list containing the names of free variables. + """ + parameter_names = [ + v.name for v in self._parameters.values() if self.is_free(v) + ] + return parameter_names + @deprecated(getNames_dep_msg) def getNames(self): - """Get the names of the variables in a list.""" - return [v.name for v in self._parameters.values() if self.isFree(v)] + """This function has been deprecated and will be removed in version + 4.0.0. - def getBounds(self): + Please use diffpy.srfit.fitbase.FitRecipe.get_names instead.""" + return self.get_names() + + def get_bounds_pairs(self): """Get the bounds on variables in a list. - Returns a list of (lb, ub) pairs, where lb is the lower bound - and ub is the upper bound. + Returns + ------- + bounds_pair_list : list of tuple of float + The list of ``(lower, upper)`` bounds on the variables, in the same + order as ``get_names`` and ``get_values``. + """ + return [v.bounds for v in self._parameters.values() if self.is_free(v)] + + @deprecated(getBounds_dep_msg) + def getBounds(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.get_bounds_pairs + instead. + """ + return self.get_bounds_pairs() + + def get_bounds_array(self): + """Get the bounds on variables in two numpy arrays. + + Returns + ------- + lower_bounds : numpy.ndarray + The numpy array of lower bounds on the variables, in the same order + as ``get_names`` and ``get_values``. + upper_bounds : numpy.ndarray + The numpy array of upper bounds on the variables, in the same order + as ``get_names`` and ``get_values``. """ - return [v.bounds for v in self._parameters.values() if self.isFree(v)] + bounds = self.get_bounds_pairs() + lower_bounds = array([b[0] for b in bounds]) + upper_bounds = array([b[1] for b in bounds]) + return lower_bounds, upper_bounds + @deprecated(getBounds2_dep_msg) def getBounds2(self): - """Get the bounds on variables in two lists. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns lower- and upper-bound lists of variable bounds. + Please use diffpy.srfit.fitbase.FitRecipe.get_bounds_array instead. """ - bounds = self.getBounds() - lb = array([b[0] for b in bounds]) - ub = array([b[1] for b in bounds]) - return lb, ub + return self.get_bounds_array() + + def initialize_recipe_with_recipe(self, recipe_object): + """Initialize a FitRecipe with another FitRecipe. + + This is used to initialize a FitRecipe with the contribution(s), + parameters, constraints and restraints of another FitRecipe. + If a duplicate contribution, parameter, constraint, or restraint + is added to the FitRecipe you are initializing, the value from the + added object will be used. + + Parameters + ---------- + recipe_object : FitRecipe + The FitRecipe to initialize with. + + Raises + ------ + ValueError + If the object passed is not a FitRecipe. + """ + if not isinstance(recipe_object, FitRecipe): + raise ValueError( + "The input recipe_object must be a FitRecipe, " + f"but got {type(recipe_object)}." + ) + + for contrib_object in recipe_object._contributions.values(): + if contrib_object not in self._contributions.values(): + self.add_contribution(contrib_object) + + for param_name, param_object in recipe_object._parameters.items(): + if param_name not in self._parameters: + self._parameters.update({param_name: param_object}) + + for ( + parameter_object, + constraint_object, + ) in recipe_object._constraints.items(): + if parameter_object not in self._constraints: + self._constraints.update({parameter_object: constraint_object}) + + for restraint in recipe_object._restraints: + if restraint not in self._restraints: + self._restraints.add(restraint) + + def _pretty_print_results_dict(self, params_dict): + """Pretty print a dictionary of parameter names and values.""" + sorted_params = sorted(params_dict.items()) + width = max(len(name) for name, _ in sorted_params) + for name, value in sorted_params: + if isinstance(value, float): + value_str = f"{value:.6g}" + else: + value_str = str(value) + print(f" {name:<{width}} = {value_str}") + + def _set_parameters_from_dict(self, params_dict): + """Set the parameters of the FitRecipe from a dictionary of + parameter names and values.""" + for param_name, param_value in params_dict.items(): + if param_name in self._parameters: + self._parameters[param_name].set_value(param_value) + else: + print( + f"Warning: Parameter '{param_name}' from results " + "not found in FitRecipe and will be ignored." + ) + + def initialize_recipe_with_results(self, results, verbose=True): + """Initialize a FitRecipe with a FitResults object or a results + file. + + Note that at least one FitContribution must already exist in + the FitRecipe. + + Parameters + ---------- + results : FitResults, pathlib.Path, or str + The FitResults object or path to results file to initialize with. + verbose : bool, optional + If True, print warnings for any parameters in the results that are + not in the FitRecipe. Default is True. + + Raises + ------ + ValueError + If the input results is not a FitResults object or a path to a + results file. + """ + if hasattr(results, "get_results_dictionary"): + params_dict = results.get_results_dictionary() + metrics_in_dict = [ + "Residual", + "Contributions", + "Restraints", + "Chi2", + "Reduced Chi2", + "Rw", + ] + for metric in metrics_in_dict: + params_dict.pop(metric, None) + elif isinstance(results, (str, Path)): + params_dict = utils.get_dict_from_results_file(results) + else: + raise ValueError( + "The input results must be a FitResults object or a path to a " + f"results file, but got {type(results)}." + ) + self._set_parameters_from_dict(params_dict) + if verbose: + print() + print("Parameters found in Results:") + print("=" * 30) + self._pretty_print_results_dict(params_dict) + print() + print("Parameters set in FitRecipe:") + print("=" * 30) + set_parameters_dict = { + param.name: param.getValue() + for param in self._parameters.values() + } + self._pretty_print_results_dict(set_parameters_dict) def set_plot_defaults(self, **kwargs): """Set default plotting options for all future plots. @@ -994,8 +1544,8 @@ def set_plot_defaults(self, **kwargs): self.plot_options.update(kwargs) def _set_axes_labels_from_metadata(self, meta, plot_params): - """Set axes labels based on filename suffix in profile metadata if not - already set.""" + """Set axes labels based on filename suffix in profile metadata + if not already set.""" if isinstance(meta, dict): filename = meta.get("filename") if filename: @@ -1008,8 +1558,8 @@ def _set_axes_labels_from_metadata(self, meta, plot_params): return def plot_recipe(self, ax=None, return_fig=False, **kwargs): - """Plot the observed, fit, and difference curves for each contribution - of the fit recipe. + """Plot the observed, fit, and difference curves for each + contribution of the fit recipe. If the recipe has multiple contributions, a separate plot is created for each contribution. @@ -1184,39 +1734,56 @@ def plot_recipe(self, ax=None, return_fig=False, **kwargs): else: return figures, axes_list - def boundsToRestraints(self, sig=1, scaled=False): + def convert_bounds_to_restraints(self, sig=1, scaled=False): """Turn all bounded parameters into restraints. The bounds become limits on the restraint. - Attributes + Parameters ---------- - sig - The uncertainty on the bounds (scalar or iterable, - default 1). - scaled - Scale the restraints, see restrain. + sig : float or iterable of float, optional + The number of standard deviations associated with each bound. + Smaller values produce stronger restraints. If a scalar is given, + the same value is applied to all parameters. If an iterable is + provided, it must match the number of parameters. Default is 1. + + scaled : bool, optional + If True, scale each restraint by the magnitude of the corresponding + parameter, consistent with the behavior of :meth:`restrain`. + Default is False. """ pars = self._parameters.values() if not hasattr(sig, "__iter__"): sig = [sig] * len(pars) for par, x in zip(pars, sig): - self.restrain( + self.add_soft_bounds( par, par.bounds[0], par.bounds[1], sig=x, scaled=scaled ) return + @deprecated(boundsToRestraints_dep_msg) + def boundsToRestraints(self, sig=1, scaled=False): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitRecipe.convert_bounds_to_restraints + instead. + """ + self.convert_bounds_to_restraints(sig, scaled) + return + def _apply_values(self, p): """Apply variable values to the variables.""" if len(p) == 0: return - vargen = (v for v in self._parameters.values() if self.isFree(v)) + vargen = (v for v in self._parameters.values() if self.is_free(v)) for var, pval in zip(vargen, p): - var.setValue(pval) + var.set_value(pval) return def _update_configuration(self): - """Notify RecipeContainers in hierarchy of configuration change.""" + """Notify RecipeContainers in hierarchy of configuration + change.""" self._ready = False return diff --git a/src/diffpy/srfit/fitbase/fitresults.py b/src/diffpy/srfit/fitbase/fitresults.py index 1949526a..18f0423c 100644 --- a/src/diffpy/srfit/fitbase/fitresults.py +++ b/src/diffpy/srfit/fitbase/fitresults.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""The FitResults and ContributionResults classes for storing results of a fit. +"""The FitResults and ContributionResults classes for storing results of +a fit. The FitResults class is used to display the current state of a FitRecipe. It stores the state, and uses it to calculate useful @@ -31,6 +32,47 @@ from diffpy.srfit.util import _DASHEDLINE from diffpy.srfit.util import sortKeyForNumericString as numstr from diffpy.srfit.util.inpututils import inputToString +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +fitresults_base = "diffpy.srfit.fitbase.FitResults" +removal_version = "4.0.0" + +formatResults_dep_msg = build_deprecation_message( + fitresults_base, + "formatResults", + "get_results_string", + removal_version, +) + +printResults_dep_msg = build_deprecation_message( + fitresults_base, + "printResults", + "print_results", + removal_version, +) + +saveResults_dep_msg = build_deprecation_message( + fitresults_base, + "saveResults", + "save_results", + removal_version, +) + +resultsDictionary_dep_msg = build_deprecation_message( + "diffpy.srfit.fitbase", + "resultsDictionary", + "get_results_dictionary", + removal_version, + new_base="diffpy.srfit.fitbase.FitResults", +) + +initializeRecipe_dep_msg = build_deprecation_message( + "diffpy.srfit.fitbase", + "initializeRecipe", + "initialize_recipe_with_results", + removal_version, + new_base="diffpy.srfit.fitbase.FitRecipe", +) class FitResults(object): @@ -38,58 +80,80 @@ class FitResults(object): Attributes ---------- - recipe - The recipe containing the results. - cov - The covariance matrix from the recipe. - conresults - An ordered dictionary of ContributionResults for each - FitContribution, indexed by the FitContribution name. - derivstep - The fractional step size for calculating numeric - derivatives. Default 1e-8. - varnames - Names of the variables in the recipe. - varvals - Values of the variables in the recipe. - varunc - Uncertainties in the variable values. - showfixed - Show fixed variables (default True). - fixednames - Names of the fixed variables of the recipe. - fixedvals - Values of the fixed variables of the recipe. - showcon - Show constraint values in the output (default False). - connames - Names of the constrained parameters. - convals - Values of the constrained parameters. - conunc - Uncertainties in the constraint values. - residual - The scalar residual of the recipe. - penalty - The penalty to residual from the restraints. - chi2 - The chi2 of the recipe. - cumchi2 - The cumulative chi2 of the recipe. - rchi2 - The reduced chi2 of the recipe. - rw - The Rw of the recipe. - cumrw - The cumulative Rw of the recipe. - messages - A list of messages about the results. - precision - The precision of numeric output (default 8). - _dcon - The derivatives of the constraint equations with respect to - the variables. This is used internally. + recipe : FitRecipe + The recipe from which the results were generated. + + cov : numpy.ndarray or None + The covariance matrix of the refined variables. None if unavailable. + + conresults : collections.OrderedDict[str, ContributionResults] + The ordered mapping of FitContribution name → ContributionResults. + + derivstep : float + The fractional step size used for numerical derivatives (default 1e-8). + + varnames : list[str] + The names of refined variables in the recipe. + + varvals : numpy.ndarray + The optimized values of the refined variables. + + varunc : numpy.ndarray or None + The estimated standard uncertainties of the variables. None if invalid. + + showfixed : bool + Show the fixed variables in the formatted output + (default True). + + fixednames : list[str] + The names of variables held fixed during refinement. + + fixedvals : numpy.ndarray + The values of the fixed variables. + + showcon : bool + show the constrained parameters in the formatted output + (default False). + + connames : list[str] + The names of constrained parameters. + + convals : numpy.ndarray + The values of constrained parameters. + + conunc : numpy.ndarray or None + The uncertainties of constrained parameters. None if unavailable. + + residual : float + The scalar residual value of the recipe. + penalty : float + The penalty contribution to the residual from restraints. + + chi2 : float + The chi-squared value of the fit. + + cumchi2 : numpy.ndarray + The cumulative chi-squared as a function of data index. + + rchi2 : float + The reduced chi-squared of the fit. + + rw : float + The weighted R-factor of the fit. + + cumrw : numpy.ndarray + The cumulative weighted R-factor as a function of data index. + + messages : list[str] + The informational or warning messages associated with the results. + + precision : int + The number of digits used when formatting numeric output (default 8). + + _dcon : numpy.ndarray + The jacobian of constraint equations with respect to variables. + Used internally for uncertainty propagation. Each of these attributes, except the recipe, are created or updated when the update method is called. @@ -98,16 +162,16 @@ class FitResults(object): def __init__(self, recipe, update=True, showfixed=True, showcon=False): """Initialize the attributes. - Attributes + Parameters ---------- - recipe - The recipe containing the results - update - Flag indicating whether to do an immediate update (default - True). - showcon + recipe : FitRecipe + The recipe containing the results. + update : bool + The flag indicating whether to do an immediate update + (default True). + showfixed : bool Show fixed variables in the output (default True). - showcon + showcon : bool Show constraint values in the output (default False). """ self.recipe = recipe @@ -139,7 +203,8 @@ def __init__(self, recipe, update=True, showfixed=True, showcon=False): return def update(self): - """Update the results according to the current state of the recipe.""" + """Update the results according to the current state of the + recipe.""" # Note that the order of these operations are chosen to reduce # computation time. @@ -152,8 +217,8 @@ def update(self): recipe._prepare() # Store the variable names and values - self.varnames = recipe.getNames() - self.varvals = recipe.getValues() + self.varnames = recipe.get_names() + self.varvals = recipe.get_values() fixedpars = recipe._tagmanager.union(recipe._fixedtag) fixedpars = [p for p in fixedpars if not p.constrained] self.fixednames = [p.name for p in fixedpars] @@ -277,7 +342,8 @@ def _calculate_jacobian(self): return jac def _calculate_metrics(self): - """Calculate chi2, cumchi2, rchi2, rw and cumrw for the recipe.""" + """Calculate chi2, cumchi2, rchi2, rw and cumrw for the + recipe.""" cumchi2 = numpy.array([], dtype=float) # total weighed denominator for the ratio in the Rw formula yw2tot = 0.0 @@ -324,24 +390,24 @@ def _calculate_constraint_uncertainties(self): self.conunc.append(sig2c**0.5) return - def formatResults(self, header="", footer="", update=False): + def get_results_string(self, header="", footer="", update=False): """Format the results and return them in a string. - This function is called by printResults and saveResults. Overloading + This function is called by print_results and save_results. Overloading the formatting here will change all three functions. - Attributes + Parameters ---------- - header - A header to add to the output (default "") - footer - A footer to add to the output (default "") - update - Flag indicating whether to call update() (default False). + header : str + The header to add to the output (default "") + footer : str + The footer to add to the output (default "") + update : bool + The flag indicating whether to call update() (default False). Returns ------- - out + out : str a string containing the formatted results. """ if update: @@ -516,37 +582,58 @@ def formatResults(self, header="", footer="", update=False): out = "\n".join(lines) + "\n" return out - def printResults(self, header="", footer="", update=False): + @deprecated(formatResults_dep_msg) + def formatResults(self, header="", footer="", update=False): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitResults.get_results_string + instead. + """ + return self.get_results_string(header, footer, update) + + def print_results(self, header="", footer="", update=False): """Format and print the results. Parameters ---------- header - A header to add to the output (default "") + The header to add to the output (default "") footer - A footer to add to the output (default "") + The footer to add to the output (default "") update - Flag indicating whether to call update() (default False). + The flag indicating whether to call update() (default False). """ - print(self.formatResults(header, footer, update).rstrip()) + print(self.get_results_string(header, footer, update).rstrip()) + return + + @deprecated(printResults_dep_msg) + def printResults(self, header="", footer="", update=False): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitResults.print_results + instead. + """ + self.print_results(header, footer, update) return def __str__(self): - return self.formatResults() + return self.get_results_string() - def saveResults(self, filename, header="", footer="", update=False): + def save_results(self, filename, header="", footer="", update=False): """Format and save the results. Parameters ---------------------------------- filename - Name of the save file. + The name of the save file. header - A header to add to the output (default "") + The header to add to the output (default "") footer - A footer to add to the output (default "") + The footer to add to the output (default "") update - Flag indicating whether to call update() (default False). + The flag indicating whether to call update() (default False). """ # Save the time and user from getpass import getuser @@ -556,12 +643,48 @@ def saveResults(self, filename, header="", footer="", update=False): myheader += "produced by " + getuser() + "\n" header = myheader + header - res = self.formatResults(header, footer, update) + res = self.get_results_string(header, footer, update) f = open(filename, "w") f.write(res) f.close() return + @deprecated(saveResults_dep_msg) + def saveResults(self, filename, header="", footer="", update=False): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.FitResults.save_results + instead. + """ + self.save_results(filename, header, footer, update) + return + + def get_results_dictionary(self): + """Get a dictionary of results, with variable names and values, + and overall metrics. + + Returns + ------- + results_dict : dict + The dictionary containing the variable names and values, + and overall metrics, from the FitResults. + """ + parameter_names = self.varnames + parameter_values = self.varvals + results_dict = dict(zip(parameter_names, parameter_values)) + results_dict.update( + { + "Residual": self.residual, + "Contributions": self.residual - self.penalty, + "Restraints": self.penalty, + "Chi2": self.chi2, + "Reduced Chi2": self.rchi2, + "Rw": self.rw, + } + ) + return results_dict + # End class FitResults @@ -573,44 +696,44 @@ class ContributionResults(object): Attributes ---------- - y + y : numpy.ndarray or None The FitContribution's profile over the calculation range (default None). - dy + dy : numpy.ndarray or None The uncertainty in the FitContribution's profile over the calculation range (default None). - x - A numpy array of the calculated independent variable for the + x : numpy.ndarray or None + The numpy array of the calculated independent variable for the FitContribution (default None). - ycalc - A numpy array of the calculated signal for the FitContribution + ycalc : numpy.ndarray or None + The numpy array of the calculated signal for the FitContribution (default None). - residual + residual : float The scalar residual of the FitContribution. - chi2 + chi2 : float The chi2 of the FitContribution. - cumchi2 + cumchi2 : numpy.ndarray The cumulative chi2 of the FitContribution. - rw + rw : float The Rw of the FitContribution. - cumrw + cumrw : numpy.ndarray The cumulative Rw of the FitContribution. - weight + weight : float The weight of the FitContribution in the recipe. - conlocs + conlocs : list The location of the constrained parameters in the FitContribution (see the RecipeContainer._locate_managed_object method). - convals - Values of the constrained parameters. - conunc - Uncertainties in the constraint values. + convals : list + The values of the constrained parameters. + conunc : list + The uncertainties in the constraint values. """ def __init__(self, con, weight, fitres): """Initialize the attributes. - Attributes + Parameters ---------- con The FitContribution @@ -693,13 +816,20 @@ def _calculate_metrics(self): # End class ContributionResults +@deprecated(resultsDictionary_dep_msg) def resultsDictionary(results): - """Get dictionary of results from file. + """**This function has been deprecated and will be** **removed in version + 4.0.0.** + + **Please use** + **diffpy.srfit.fitbase.FitResults.get_results_dictionary instead.** + + Get dictionary of results from file. This reads the results from file and stores them in a dictionary to be returned to the caller. The dictionary may contain non-result entries. - Attributes + Parameters ---------- results An open file-like object, name of a file that contains @@ -720,14 +850,22 @@ def resultsDictionary(results): return mpairs +@deprecated(initializeRecipe_dep_msg) def initializeRecipe(recipe, results): - """Initialize the variables of a recipe from a results file. + """**This function has been deprecated and will be** **removed in + version 4.0.0.** + + **Please use** + **diffpy.srfit.fitbase.FitRecipe.initialize_recipe_with_results** + **instead.** + + Initialize the variables of a recipe from a results file. This reads the results from file and initializes any variables (fixed or free) in the recipe to the results values. Note that the recipe has to be configured, with variables. This does not reconstruct a FitRecipe. - Attributes + Parameters ---------- recipe A configured recipe with variables diff --git a/src/diffpy/srfit/fitbase/parameter.py b/src/diffpy/srfit/fitbase/parameter.py index 90691e55..4fa2c2b6 100644 --- a/src/diffpy/srfit/fitbase/parameter.py +++ b/src/diffpy/srfit/fitbase/parameter.py @@ -34,6 +34,25 @@ from diffpy.srfit.interface import _parameter_interface from diffpy.srfit.util.argbinders import bind2nd from diffpy.srfit.util.nameutils import validateName +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +parameter_base = "diffpy.srfit.fitbase.Parameter" +removal_version = "4.0.0" +setValue_dep_msg = build_deprecation_message( + parameter_base, "setValue", "set_value", removal_version +) + +setConst_dep_msg = build_deprecation_message( + parameter_base, "setConst", "set_constant", removal_version +) + +boundRange_dep_msg = build_deprecation_message( + parameter_base, "boundRange", "bound_range", removal_version +) + +boundWindow_dep_msg = build_deprecation_message( + parameter_base, "boundWindow", "bound_window", removal_version +) class Parameter(_parameter_interface, Argument, Validatable): @@ -46,22 +65,22 @@ class Parameter(_parameter_interface, Argument, Validatable): const A flag indicating whether this is considered a constant. _value - The value of the Parameter. Modified with 'setValue'. + The value of the Parameter. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' 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. See - FitRecipe.getBounds and FitRecipe.boundsToRestraints. + FitRecipe.get_bounds_pairs and FitRecipe.convert_bounds_to_restraints. """ def __init__(self, name, value=None, const=False): """Initialization. - Attributes + Parameters ---------- name The name of this Parameter (must be a valid attribute @@ -81,17 +100,17 @@ def __init__(self, name, value=None, const=False): Argument.__init__(self, name, value, const) return - def setValue(self, val): + def set_value(self, val): """Set the value of the Parameter and the bounds. - Attributes + Parameters ---------- val The value to assign. - lb + lower_bound : float The lower bounds for the bounds list. If this is None (default), then the lower bound will not be alterered. - ub + upper_bound : float The upper bounds for the bounds list. If this is None (default), then the upper bound will not be alterered. @@ -100,20 +119,29 @@ def setValue(self, val): self Returns self so that mutators can be chained. """ - Argument.setValue(self, val) + Argument.set_value(self, val) return self - def setConst(self, const=True, value=None): + @deprecated(setValue_dep_msg) + def setValue(self, val): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.Parameter.set_value instead. + """ + return self.set_value(val) + + def set_constant(self, is_constant=True, value=None): """Toggle the Parameter as constant. - Attributes + Parameters ---------- - const - Flag indicating if the parameter is constant (default + is_constant : bool, optional + The flag indicating if the parameter is constant (default True). - value - An optional value for the parameter (default None). If this - is not None, then the parameter will get a new value, + value : float, optional + The value value for the parameter to be set to (default None). + If this is not None, then the parameter will get a new value, constant or otherwise. Returns @@ -121,19 +149,29 @@ def setConst(self, const=True, value=None): self Returns self so that mutators can be chained. """ - self.const = bool(const) + self.const = bool(is_constant) if value is not None: - self.setValue(value) + self.set_value(value) + return self + + @deprecated(setConst_dep_msg) + def setConst(self, const=True, value=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.Parameter.set_constant instead. + """ + self.set_constant(const, value) return self - def boundRange(self, lb=None, ub=None): + def bound_range(self, lower_bound=None, upper_bound=None): """Set lower and upper bound of the Parameter. - Attributes + Parameters ---------- - lb + lower_bound : float The lower bound for the bounds list. - ub + upper_bound : float The upper bound for the bounds list. Returns @@ -141,24 +179,34 @@ def boundRange(self, lb=None, ub=None): self Returns self so that mutators can be chained. """ - if lb is not None: - self.bounds[0] = lb - if ub is not None: - self.bounds[1] = ub + if lower_bound is not None: + self.bounds[0] = lower_bound + if upper_bound is not None: + self.bounds[1] = upper_bound return self - def boundWindow(self, lr=0, ur=None): + @deprecated(boundRange_dep_msg) + def boundRange(self, lower_bound=None, upper_bound=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.Parameter.bound_range instead. + """ + self.bound_range(lower_bound, upper_bound) + return self + + def bound_window(self, lower_radius=0, upper_radius=None): """Create bounds centered on the current value of the Parameter. - Attributes + Parameters ---------- - lr + lower_radius : float, optional The radius of the lower bound (default 0). The lower bound is - computed as value - lr. - ur + computed as value - lower_radius. + upper_radius : float, optional The radius of the upper bound. The upper bound is computed as - value + ur. If this is None (default), then the value of the - lower radius is used. + value + upper_radius. If this is None (default), then the value + of the lower radius is used. Returns ------- @@ -166,11 +214,21 @@ def boundWindow(self, lr=0, ur=None): Returns self so that mutators can be chained. """ val = self.getValue() - lb = val - lr - if ur is None: - ur = lr - ub = val + ur - self.bounds = [lb, ub] + lower_bound = val - lower_radius + if upper_radius is None: + upper_radius = lower_radius + upper_bound = val + upper_radius + self.bounds = [lower_bound, upper_bound] + return self + + @deprecated(boundWindow_dep_msg) + def boundWindow(self, lr=0, ur=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.fitbase.Parameter.bound_window instead. + """ + self.bound_window(lr, ur) return self def _validate(self): @@ -205,7 +263,7 @@ class ParameterProxy(Parameter): def __init__(self, name, par): """Initialization. - Attributes + Parameters ---------- name The name of this ParameterProxy. @@ -238,8 +296,8 @@ def bounds(self): """List of lower and upper bounds of the proxied Parameter. This can be used by some optimizers when the Parameter is - varied. See FitRecipe.getBounds and - FitRecipe.boundsToRestraints. + varied. See FitRecipe.get_bounds_pairs and + FitRecipe.convert_bounds_to_restraints. """ return self.par.bounds @@ -254,25 +312,25 @@ def _observers(self): # wrap Parameter methods to use the target object ------------------------ - @wraps(Parameter.setValue) - def setValue(self, val): - return self.par.setValue(val) + @wraps(Parameter.set_value) + def set_value(self, val): + return self.par.set_value(val) @wraps(Parameter.getValue) def getValue(self): return self.par.getValue() - @wraps(Parameter.setConst) - def setConst(self, const=True, value=None): - return self.par.setConst(const, value) + @wraps(Parameter.set_constant) + def set_constant(self, const=True, value=None): + return self.par.set_constant(const, value) - @wraps(Parameter.boundRange) - def boundRange(self, lb=None, ub=None): - return self.par.boundRange(lb, ub) + @wraps(Parameter.bound_range) + def bound_range(self, lower_bound=None, upper_bound=None): + return self.par.bound_range(lower_bound, upper_bound) - @wraps(Parameter.boundWindow) - def boundWindow(self, lr=0, ur=None): - return self.par.boundWindow(lr, ur) + @wraps(Parameter.bound_window) + def bound_window(self, lr=0, ur=None): + return self.par.bound_window(lr, ur) def _validate(self): """Validate my state. @@ -293,14 +351,14 @@ def _validate(self): class ParameterAdapter(Parameter): """An adapter for parameter-like objects. - This class wraps an object as a Parameter. The getValue and setValue - methods defer to the data of the wrapped object. + This class wraps an object as a Parameter. The getValue and + set_value methods defer to the data of the wrapped object. """ def __init__(self, name, obj, getter=None, setter=None, attr=None): """Wrap an object as a Parameter. - Attributes + Parameters ---------- name The name of this Parameter. @@ -359,7 +417,7 @@ def getValue(self): """Get the value of the Parameter.""" return self.getter(self.obj) - def setValue(self, value): + def set_value(self, value): """Set the value of the Parameter.""" if value != self.getValue(): self.setter(self.obj, value) diff --git a/src/diffpy/srfit/fitbase/parameterset.py b/src/diffpy/srfit/fitbase/parameterset.py index ce89326f..edaac6f5 100644 --- a/src/diffpy/srfit/fitbase/parameterset.py +++ b/src/diffpy/srfit/fitbase/parameterset.py @@ -25,6 +25,22 @@ from collections import OrderedDict from diffpy.srfit.fitbase.recipeorganizer import RecipeOrganizer +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.srfit.fitbase.parameterset.ParameterSet" +removal_version = "4.0.0" + +addparset_dep_msg = build_deprecation_message( + base, "addParameterSet", "add_parameter_set", removal_version +) + +removeParameterSet_dep_msg = build_deprecation_message( + base, "removeParameterSet", "remove_parameter_set", removal_version +) + +setConst_dep_msg = build_deprecation_message( + base, "setConst", "set_constant", removal_version +) class ParameterSet(RecipeOrganizer): @@ -63,15 +79,15 @@ class ParameterSet(RecipeOrganizer): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ def __init__(self, name): """Initialize. - Attributes + Parameters ---------- name The name of this ParameterSet. @@ -87,10 +103,10 @@ def __init__(self, name): newParameter = RecipeOrganizer._new_parameter removeParameter = RecipeOrganizer._remove_parameter - def addParameterSet(self, parset): + def add_parameter_set(self, parset): """Add a ParameterSet to the hierarchy. - Attributes + Parameters ---------- parset The ParameterSet to be stored. @@ -103,7 +119,19 @@ def addParameterSet(self, parset): self._add_object(parset, self._parsets, True) return - def removeParameterSet(self, parset): + @deprecated(addparset_dep_msg) + def addParameterSet(self, parset): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.parameterset.ParameterSet.add_parameter_set + instead. + """ + self.add_parameter_set(parset) + return + + def remove_parameter_set(self, parset): """Remove a ParameterSet from the hierarchy. Raises ValueError if parset is not managed by this object. @@ -111,18 +139,41 @@ def removeParameterSet(self, parset): self._remove_object(parset, self._parsets) return - def setConst(self, const=True): + @deprecated(removeParameterSet_dep_msg) + def removeParameterSet(self, parset): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.parameterset.ParameterSet.remove_parameter_set + instead. + """ + self.remove_parameter_set(parset) + return + + def set_constant(self, is_constant=True): """Set every parameter within the set to a constant. - Attributes + Parameters ---------- - const - Flag indicating if the parameter is constant (default + is_constant : bool, optional + The flag indicating if the parameter is constant (default True). """ - for par in self.iterPars(): - par.setConst(const) + for par in self.iterate_over_parameters(): + par.set_constant(is_constant) + return + @deprecated(setConst_dep_msg) + def setConst(self, const=True): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.parameterset.ParameterSet.set_constant + instead. + """ + self.set_constant(const) return diff --git a/src/diffpy/srfit/fitbase/profile.py b/src/diffpy/srfit/fitbase/profile.py index 2600fb8f..d66e61f7 100644 --- a/src/diffpy/srfit/fitbase/profile.py +++ b/src/diffpy/srfit/fitbase/profile.py @@ -29,9 +29,40 @@ from diffpy.srfit.fitbase.parameter import Parameter from diffpy.srfit.fitbase.validatable import Validatable from diffpy.srfit.util.observable import Observable +from diffpy.utils._deprecator import build_deprecation_message, deprecated # This is the roundoff tolerance for selecting bounds on arrays. epsilon = 1e-8 +base = "diffpy.srfit.fitbase.profile.Profile" +removal_version = "4.0.0" + +loadParsedData_dep_msg = build_deprecation_message( + base, + "loadParsedData", + "load_parsed_data", + removal_version, +) + +setObservedProfile_dep_msg = build_deprecation_message( + base, + "setObservedProfile", + "set_observed_profile", + removal_version, +) + +setCalculationRange_dep_msg = build_deprecation_message( + base, + "setCalculationRange", + "set_calculation_range", + removal_version, +) + +setCalculationPoints_dep_msg = build_deprecation_message( + base, + "setCalculationPoints", + "set_calculation_points", + removal_version, +) class Profile(Observable, Validatable): @@ -40,8 +71,6 @@ class Profile(Observable, Validatable): Profile is an Observable. The xpar, ypar and dypar attributes are observed by the Profile, which can in turn be observed by some other object. - Attributes - Attributes ---------- _xobs @@ -105,19 +134,19 @@ def __init__(self): # We want x, y, ycalc and dy to stay in-sync with xpar, ypar and dypar x = property( lambda self: self.xpar.getValue(), - lambda self, val: self.xpar.setValue(val), + lambda self, val: self.xpar.set_value(val), ) y = property( lambda self: self.ypar.getValue(), - lambda self, val: self.ypar.setValue(val), + lambda self, val: self.ypar.set_value(val), ) dy = property( lambda self: self.dypar.getValue(), - lambda self, val: self.dypar.setValue(val), + lambda self, val: self.dypar.set_value(val), ) ycalc = property( lambda self: self.ycpar.getValue(), - lambda self, val: self.ycpar.setValue(val), + lambda self, val: self.ycpar.set_value(val), ) # We want xobs, yobs and dyobs to be read-only @@ -125,17 +154,28 @@ def __init__(self): yobs = property(lambda self: self._yobs) dyobs = property(lambda self: self._dyobs) - def loadParsedData(self, parser): + def load_parsed_data(self, parser): """Load parsed data from a ProfileParser. This sets the xobs, yobs, dyobs arrays as well as the metadata. """ - x, y, junk, dy = parser.getData() - self.meta = dict(parser.getMetaData()) - self.setObservedProfile(x, y, dy) + x, y, dx, dy = parser.get_data() + self.meta = dict(parser.get_metadata()) + self.set_observed_profile(x, y, dy) return - def setObservedProfile(self, xobs, yobs, dyobs=None): + @deprecated(loadParsedData_dep_msg) + def loadParsedData(self, parser): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.profile.Profile.load_parsed_data + instead. + """ + self.load_parsed_data(parser) + return + + def set_observed_profile(self, xobs, yobs, dyobs=None): """Set the observed profile. Parameters @@ -167,13 +207,25 @@ def setObservedProfile(self, xobs, yobs, dyobs=None): # Set the default calculation points if self.x is None: - self.setCalculationPoints(self._xobs) + self.set_calculation_points(self._xobs) else: - self.setCalculationPoints(self.x) + self.set_calculation_points(self.x) return - def setCalculationRange(self, xmin=None, xmax=None, dx=None): + @deprecated(setObservedProfile_dep_msg) + def setObservedProfile(self, xobs, yobs, dyobs=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.profile.Profile.set_observed_profile + instead. + """ + self.set_observed_profile(xobs, yobs, dyobs) + return + + def set_calculation_range(self, xmin=None, xmax=None, dx=None): """Set epsilon-inclusive calculation range. Adhere to the observed ``xobs`` points when ``dx`` is the same @@ -274,10 +326,22 @@ def _isobs(a): self.dy = self.dyobs[indices] else: x1 = numpy.arange(lo, hi + epshi, step) - self.setCalculationPoints(x1) + self.set_calculation_points(x1) return - def setCalculationPoints(self, x): + @deprecated(setCalculationRange_dep_msg) + def setCalculationRange(self, xmin=None, xmax=None, dx=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.profile.Profile.set_calculation_range + instead. + """ + self.set_calculation_range(xmin, xmax, dx) + return + + def set_calculation_points(self, x): """Set the calculation points. Parameters @@ -295,7 +359,7 @@ def setCalculationPoints(self, x): x = x[x <= self.xobs[-1] + epsilon] self.x = x if self.yobs is not None: - self.y = rebinArray(self.yobs, self.xobs, self.x) + self.y = _rebin_array(self.yobs, self.xobs, self.x) if self.dyobs is not None: # work around for interpolation issue making some of these non-1 if (self.dyobs == 1).all(): @@ -303,8 +367,24 @@ def setCalculationPoints(self, x): else: # FIXME - This does not follow error propagation rules and it # introduces (more) correlation between the data points. - self.dy = rebinArray(self.dyobs, self.xobs, self.x) + self.dy = _rebin_array(self.dyobs, self.xobs, self.x) + + return + @deprecated(setCalculationPoints_dep_msg) + def setCalculationPoints(self, x): + """Set the calculation points. + + Parameters + ---------- + x + A non-empty numpy array containing the calculation points. If + xobs exists, the bounds of x will be limited to its bounds. + + This will create y and dy on the specified grid if xobs, yobs and + dyobs exist. + """ + self.set_calculation_points(x) return def loadtxt(self, *args, **kw): @@ -314,7 +394,7 @@ def loadtxt(self, *args, **kw): enforced. The first two arrays returned by numpy.loadtxt are assumed to be x and y. If there is a third array, it is assumed to by dy. Any other arrays are ignored. These are passed to - setObservedProfile. + set_observed_profile. Raises ValueError if the call to numpy.loadtxt returns fewer than 2 arrays. @@ -345,7 +425,7 @@ def loadtxt(self, *args, **kw): if len(cols) > 2: dy = cols[2] - self.setObservedProfile(x, y, dy) + self.set_observed_profile(x, y, dy) return x, y, dy def savetxt(self, fname, **kwargs): @@ -418,7 +498,7 @@ def _validate(self): # End class Profile -def rebinArray(A, xold, xnew): +def _rebin_array(A, xold, xnew): """Rebin the an array by interpolating over the new x range. Parameters diff --git a/src/diffpy/srfit/fitbase/profilegenerator.py b/src/diffpy/srfit/fitbase/profilegenerator.py index c6adabbc..e0206084 100644 --- a/src/diffpy/srfit/fitbase/profilegenerator.py +++ b/src/diffpy/srfit/fitbase/profilegenerator.py @@ -110,9 +110,9 @@ class ProfileGenerator(Operator, ParameterSet): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ # define abstract attributes from the Operator base. @@ -156,7 +156,7 @@ def operation(self): def set_profile(self, profile): """Assign the profile. - Attributes + Parameters ---------- profile A Profile that specifies the calculation points and which @@ -171,10 +171,10 @@ def set_profile(self, profile): # Merge the profiles metadata with our own self.meta.update(self.profile.meta) - self.processMetaData() + self._process_metadata() return - def processMetaData(self): + def _process_metadata(self): """Process the metadata. This can be used to configure a ProfileGenerator upon a change diff --git a/src/diffpy/srfit/fitbase/profileparser.py b/src/diffpy/srfit/fitbase/profileparser.py index 32cb9669..7713abc5 100644 --- a/src/diffpy/srfit/fitbase/profileparser.py +++ b/src/diffpy/srfit/fitbase/profileparser.py @@ -22,8 +22,56 @@ See the class documentation for more information. """ +from pathlib import Path + +import numpy as np from diffpy.srfit.exceptions import ParseError +from diffpy.utils._deprecator import build_deprecation_message, deprecated +from diffpy.utils.parsers import load_data + +removal_verison = "4.0.0" +pdfparser_base = "diffpy.srfit.pdf.pdfparser.PDFParser" +new_base = "diffpy.srfit.fitbase.ProfileParser" + + +parseFile_dep_msg = build_deprecation_message( + pdfparser_base, + "parseFile", + "parse_file", + removal_verison, + new_base=new_base, +) + +pp_base = "diffpy.srfit.fitbase.profileparser.ProfileParser" + +getNumBanks_dep_msg = build_deprecation_message( + pp_base, + "getNumBanks", + "get_num_banks", + removal_verison, +) + +selectBank_dep_msg = build_deprecation_message( + pp_base, + "selectBank", + "select_bank", + removal_verison, +) + +getData_dep_msg = build_deprecation_message( + pp_base, + "getData", + "get_data", + removal_verison, +) + +getMetaData_dep_msg = build_deprecation_message( + pp_base, + "getMetaData", + "get_metadata", + removal_verison, +) class ProfileParser(object): @@ -31,48 +79,47 @@ class ProfileParser(object): Attributes ---------- - _format - Name of the data format that this parses (string, default - ""). The format string is a unique identifier for the data + _format : str, optional + The 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 + _banks : list of tuples 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 + x : np.ndarray + The independent variable read from the file. + y : np.ndarray + The dependent variable (profile) read from the file. - dx - A numpy array containing the uncertainty in x - read from the file. This is None if the + dx : np.ndarray + The uncertainties associated with x + read from the file. This is 0 if the + uncertainty cannot be read. + dy : np.ndarray + The uncertainties associated with y + 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 None if the uncertainty - cannot be read. - _x + _x : np.ndarray Independent variable from the chosen bank - _y + _y : np.ndarray Profile from the chosen bank - _dx + _dx : np.ndarray Uncertainty in independent variable from the chosen bank - _dy + _dy : np.ndarray Uncertainty in profile from the chosen bank - _meta + _meta : dict A dictionary containing metadata read from the file. General Metadata ---------------- - filename + filename : str or Path The name of the file from which data was parsed. This key will not exist if data was not read from file. - nbanks + nbanks : int The number of banks parsed. - bank + bank : int The chosen bank number. """ @@ -93,7 +140,8 @@ def getFormat(self): return self._format def parseString(self, patstring): - """Parse a string and set the _x, _y, _dx, _dy and _meta variables. + """Parse a string and set the _x, _y, _dx, _dy and _meta + variables. When _dx or _dy cannot be obtained in the data format it is set to None. @@ -109,8 +157,11 @@ def parseString(self, patstring): """ raise NotImplementedError() + # remove parseString too when this file is removed. + @deprecated(parseFile_dep_msg) def parseFile(self, filename): - """Parse a file and set the _x, _y, _dx, _dy and _meta variables. + """Parse a file and set the _x, _y, _dx, _dy and _meta + variables. This wipes out the currently loaded data and selected bank number. @@ -133,14 +184,149 @@ def parseFile(self, filename): if len(self._banks) < 1: raise ParseError("There are no data in the banks") - self.selectBank(0) + self.select_bank(0) return - def getNumBanks(self): - """Get the number of banks read by the parser.""" + def parse_file(self, filename, column_format=None): + """Parse a data file to extract data and metadata, with + automatic handling of uncertainties. + + - For files with 2 columns: assumes (x, y) and sets dx, dy to 0. + - For files with 3 columns: assumes (x, y, dy) and sets dx to 0. + - For files with 4 columns: assumes (x, y, dx, dy). + - For other cases: `column_format` must be explicitly specified. + + Uncertainty columns (dx, dy) are only considered valid if all values + are positive and not NaN/Inf. Otherwise they are set to 0. + + This wipes out the currently loaded data and selected bank number. + + Parameters + ---------- + filename : str or Path + The name of the file to parse. + column_format : tuple of str, optional + The order in which columns appear in the file. + If None, the format is auto-detected based on the + number of columns. + + Valid labels: `"x"`, `"y"`, `"dx"`, `"dy"` + + Examples: + + - `("x", "y")` + - `("x", "y", "dy")` + - `("x", "y", "dx", "dy")` + - `("x", "dx", "y", "dy")` + + Raises + ------ + ParseError + If parsing fails or ambiguity detected. + """ + # Reset internal state + self._banks = [] + if isinstance(filename, Path): + filename = str(filename) + # Load metadata and numeric data + self._meta, data = self._load_file(filename) + column_format = self._detect_column_format(data, column_format) + # Map columns to x, y, dx, dy + columns = self._map_column_labels_to_data(data, column_format) + # Extract required arrays + x = columns["x"] + y = columns["y"] + x_length = len(x) + y_length = len(y) + dx = self._validate_uncertainty(columns.get("dx"), x_length) + dy = self._validate_uncertainty(columns.get("dy"), y_length) + # Store as single bank + self._banks = [(x, y, dx, dy)] + self._meta["nbanks"] = 1 + self.select_bank(0) + + def _load_file(self, filename): + """Load metadata and numeric data from a file.""" + meta = load_data(filename, headers=True) + meta["filename"] = filename + data = load_data(filename) + if data.size == 0 or (data.ndim == 1): + raise ParseError( + "Data block must have at least two columns (x, y)." + ) + return meta, data + + def _detect_column_format(self, data, column_format): + """Auto-detect or validate column format.""" + num_cols = data.shape[1] + + if column_format is None: + if num_cols == 2: + column_format = ("x", "y") + elif num_cols == 3: + column_format = ("x", "y", "dy") + elif num_cols == 4: + column_format = ("x", "y", "dx", "dy") + else: + raise ParseError( + f"Expected 2 to 4 columns but found {num_cols}." + ) + if len(column_format) != num_cols: + raise ParseError( + f"column_format has {len(column_format)} " + f"labels but file contains {num_cols} columns." + ) + if len(set(column_format)) != len(column_format): + raise ParseError("column_format cannot contain duplicate labels.") + for label in column_format: + if label not in {"x", "y", "dx", "dy"}: + raise ParseError( + f"column_format contains invalid label '{label}'. " + "Valid labels are 'x', 'y', 'dx', and 'dy'." + ) + return column_format + + def _map_column_labels_to_data(self, data, column_format): + """Map numeric data to columns by label.""" + columns = {} + for i, label in enumerate(column_format): + columns[label] = data[:, i] + + if "x" not in columns or "y" not in columns: + raise ParseError( + "Both 'x' and 'y' columns must be present in the data." + ) + + return columns + + @staticmethod + def _validate_uncertainty(data, length): + """Return the uncertainty data if valid, otherwise 0.""" + if data is None or not np.all(np.isfinite(data)) or np.any(data <= 0): + return np.zeros(length) + return data + + def get_num_banks(self): + """Get the number of banks read by the parser. + + Returns + ------- + int + The number of banks read by the parser. + """ return len(self._banks) - def selectBank(self, index): + @deprecated(getNumBanks_dep_msg) + def getNumBanks(self): + """This function is deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.ProfileParser.get_num_banks + instead. + """ + return self.get_num_banks() + + def select_bank(self, index): """Select which bank to use. This method should only be called after the data has been parsed. The @@ -158,7 +344,7 @@ def selectBank(self, index): if index is None: index = self._meta.get("bank", 0) - numbanks = self.getNumBanks() + numbanks = self.get_num_banks() if index > numbanks: raise IndexError("Bank index out of range") @@ -173,7 +359,18 @@ def selectBank(self, index): self._x, self._y, self._dx, self._dy = self._banks[index] return - def getData(self, index=None): + @deprecated(selectBank_dep_msg) + def selectBank(self, index): + """This function is deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.ProfileParser.select_bank + instead. + """ + self.select_bank(index) + return + + def get_data(self, index=None): """Get the data. This method should only be called after the data has been parsed. The @@ -190,12 +387,37 @@ def getData(self, index=None): This returns (x, y, dx, dy) tuple for the bank. dx is 0 if it cannot be determined from the data format. """ - self.selectBank(index) + self.select_bank(index) return self._x, self._y, self._dx, self._dy + @deprecated(getData_dep_msg) + def getData(self, index=None): + """This function is deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.ProfileParser.get_data instead. + """ + return self.get_data(index) + + def get_metadata(self): + """Get the parsed metadata. + + Returns + ------- + dict + A dictionary containing metadata read from the file. + """ + return self._meta + + @deprecated(getMetaData_dep_msg) def getMetaData(self): - """Get the parsed metadata.""" + """This function is deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.fitbase.ProfileParser.get_metadata + instead. + """ return self._meta diff --git a/src/diffpy/srfit/fitbase/recipeorganizer.py b/src/diffpy/srfit/fitbase/recipeorganizer.py index 3b95a216..20d16c49 100644 --- a/src/diffpy/srfit/fitbase/recipeorganizer.py +++ b/src/diffpy/srfit/fitbase/recipeorganizer.py @@ -17,17 +17,16 @@ RecipeContainer is the base class for organizing Parameters, and other RecipeContainers. RecipeOrganizer is an extended RecipeContainer that incorporates equation building, constraints and Restraints. -equationFromString creates an Equation instance from a string. +get_equation_from_string creates an Equation instance from a string. """ -__all__ = ["RecipeContainer", "RecipeOrganizer", "equationFromString"] +__all__ = ["RecipeContainer", "RecipeOrganizer", "get_equation_from_string"] import re from collections import OrderedDict from functools import partial from itertools import chain, groupby -import six from numpy import inf from diffpy.srfit.equation import Equation @@ -42,6 +41,131 @@ from diffpy.srfit.util import sortKeyForNumericString as numstr from diffpy.srfit.util.nameutils import validateName from diffpy.srfit.util.observable import Observable +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +recipecontainer_base = "diffpy.srfit.fitbase.recipeorganizer.RecipeContainer" +removal_version = "4.0.0" + +getValues_deprecation_msg = build_deprecation_message( + recipecontainer_base, + "getValues", + "get_values", + removal_version, +) + +getNames_deprecation_msg = build_deprecation_message( + recipecontainer_base, + "getNames", + "get_names", + removal_version, +) + +iterPars_deprecation_msg = build_deprecation_message( + recipecontainer_base, + "iterPars", + "iterate_over_parameters", + removal_version, +) + +recipeorganizer_base = "diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer" + +registerCalculator_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "registerCalculator", + "register_calculator", + removal_version, +) + +registerFunction_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "registerFunction", + "register_function", + removal_version, +) + +registerStringFunction_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "registerStringFunction", + "register_string_function", + removal_version, +) + +evaluateEquation_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "evaluateEquation", + "evaluate_equation", + removal_version, +) + +isConstrained_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "isConstrained", + "is_constrained", + removal_version, +) + +getConstrainedPars_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "getConstrainedPars", + "get_constrained_parmeters", + removal_version, +) + +clearConstraints_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "clearConstraints", + "clear_all_constraints", + removal_version, +) + +addRestraint_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "addRestraint", + "register_soft_bounds", + removal_version, +) + +constrain_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "constrain", + "add_constraint", + removal_version, +) + +unconstrain_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "unconstrain", + "remove_constraint", + removal_version, +) + +restrain_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "restrain", + "add_soft_bounds", + removal_version, +) + +unrestrain_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "unrestrain", + "remove_soft_bounds", + removal_version, +) + +clearRestraints_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "clearRestraints", + "clear_all_soft_bounds", + removal_version, +) + +equationFromString_deprecation_msg = build_deprecation_message( + recipeorganizer_base, + "equationFromString", + "get_equation_from_string", + removal_version, +) class RecipeContainer(Observable, Configurable, Validatable): @@ -83,13 +207,13 @@ class RecipeContainer(Observable, Configurable, Validatable): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ - names = property(lambda self: self.getNames()) - values = property(lambda self: self.getValues()) + names = property(lambda self: self.get_names()) + values = property(lambda self: self.get_values()) def __init__(self, name): Observable.__init__(self) @@ -116,21 +240,35 @@ def _iter_managed(self): """Get iterator over managed objects.""" return chain(*(d.values() for d in self.__managed)) - def iterPars(self, pattern="", recurse=True): + def iterate_over_parameters(self, pattern="", recurse=True): """Iterate over the Parameters contained in this object. Parameters ---------- - pattern : str - Iterate over parameters with names matching this regular - expression (all parameters by default). - recurse : bool - Recurse into managed objects when True (default). + pattern : str, optional + The regular expression pattern to match parameter + names against. Only parameters with names matching + this pattern will be returned. Default is an empty + string, which matches all parameter names. + recurse : bool, optional + The flag indicating whether to recurse into managed + objects when iterating over parameters. If True + (default), the method will also iterate over + parameters in managed sub-objects. If False, only + top-level parameters will be iterated over. + + Example + ------- + + .. + for param in recipe.iterate_over_parameters(pattern="scale_"): + # print the name and value of parameters containing "scale_" + print(f"{param.name}={param.value}") """ regexp = re.compile(pattern) - for par in list(self._parameters.values()): - if regexp.search(par.name): - yield par + for parameter in list(self._parameters.values()): + if regexp.search(parameter.name): + yield parameter if not recurse: return # Iterate over objects within the managed dictionaries. @@ -138,11 +276,24 @@ def iterPars(self, pattern="", recurse=True): managed.remove(self._parameters) for m in managed: for obj in m.values(): - if hasattr(obj, "iterPars"): - for par in obj.iterPars(pattern=pattern): - yield par + if hasattr(obj, "iterate_over_parameters"): + for parameter in obj.iterate_over_parameters( + pattern=pattern + ): + yield parameter return + @deprecated(iterPars_deprecation_msg) + def iterPars(self, pattern="", recurse=True): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.iterate_over_parameters + instead. + """ + return self.iterate_over_parameters(pattern=pattern, recurse=recurse) + def __iter__(self): """Iterate over top-level parameters.""" return iter(self._parameters.values()) @@ -188,11 +339,11 @@ def __dir__(self): def __setattr__(self, name, value): """Parameter access and object checking.""" if name in self._parameters: - par = self._parameters[name] + parameter = self._parameters[name] if isinstance(value, Parameter): - par.value = value.value + parameter.value = value.value else: - par.value = value + parameter.value = value return m = self.get(name) @@ -229,18 +380,40 @@ def get(self, name, default=None): return default - def getNames(self): + def get_names(self): """Get the names of managed parameters.""" return [p.name for p in self._parameters.values()] - def getValues(self): + @deprecated(getNames_deprecation_msg) + def getNames(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.get_names + instead. + """ + return self.get_names() + + def get_values(self): """Get the values of managed parameters.""" return [p.value for p in self._parameters.values()] + @deprecated(getValues_deprecation_msg) + def getValues(self): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeContainer.get_values + instead. + """ + return self.get_values() + def _add_object(self, obj, d, check=True): """Add an object to a managed dictionary. - Attributes + Parameters ---------- obj The object to be stored. @@ -309,7 +482,7 @@ def _remove_object(self, obj, d): def _locate_managed_object(self, obj): """Find the location a managed object within the hierarchy. - Attributes + Parameters ---------- obj The object to find. @@ -400,9 +573,9 @@ class RecipeOrganizer(_recipeorganizer_interface, RecipeContainer): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. Raises ValueError if the name is not a valid attribute identifier @@ -432,14 +605,14 @@ def _new_parameter(self, name, value, check=True): self._add_parameter(p, check) return p - def _add_parameter(self, par, check=True): + def _add_parameter(self, parameter, check=True): """Store a Parameter. Parameters added in this way are registered with the _eqfactory. - Attributes + Parameters ---------- - par + parameter The Parameter to be stored. check If True (default), a ValueError is raised a Parameter of @@ -452,13 +625,13 @@ def _add_parameter(self, par, check=True): """ # Store the Parameter - RecipeContainer._add_object(self, par, self._parameters, check) + RecipeContainer._add_object(self, parameter, self._parameters, check) # Register the Parameter - self._eqfactory.registerArgument(par.name, par) + self._eqfactory.registerArgument(parameter.name, parameter) return - def _remove_parameter(self, par): + def _remove_parameter(self, parameter): """Remove a parameter. This de-registers the Parameter with the _eqfactory. The @@ -467,14 +640,16 @@ def _remove_parameter(self, par): Note that constraints and restraints involving the Parameter are not modified. - Raises ValueError if par is not part of the RecipeOrganizer. + Raises ValueError if parameter is not part of the + RecipeOrganizer. """ - self._remove_object(par, self._parameters) - self._eqfactory.deRegisterBuilder(par.name) + self._remove_object(parameter, self._parameters) + self._eqfactory.deRegisterBuilder(parameter.name) return - def registerCalculator(self, f, argnames=None): - """Register a Calculator so it can be used within equation strings. + def register_calculator(self, calculator, argnames=None): + """Register a Calculator so it can be used within equation + strings. A Calculator is an elaborate function that can organize Parameters. This creates a function with this class that can be used within string @@ -482,79 +657,101 @@ def registerCalculator(self, f, argnames=None): arguments like a function or without, in which case the values of the Parameters created from argnames will be be used to compute the value. - Attributes + Parameters ---------- - f + calculator : Calculator object The Calculator to register. - argnames - The names of the arguments to f (list or None). + argnames : list or None, optional + The names of the arguments to `calculator` (list or None). If this is None, then the argument names will be extracted from the function. """ - self._eqfactory.registerOperator(f.name, f) - self._add_object(f, self._calculators) + self._eqfactory.registerOperator(calculator.name, calculator) + self._add_object(calculator, self._calculators) # Register arguments of the calculator if argnames is None: - fncode = f.__call__.__func__.__code__ + fncode = calculator.__call__.__func__.__code__ argnames = list(fncode.co_varnames) argnames = argnames[1 : fncode.co_argcount] for pname in argnames: if pname not in self._eqfactory.builders: - par = self._new_parameter(pname, 0) + parameter = self._new_parameter(pname, 0) else: - par = self.get(pname) - f.addLiteral(par) + parameter = self.get(pname) + calculator.addLiteral(parameter) # Now return an equation object - eq = self._eqfactory.makeEquation(f.name) + eq = self._eqfactory.makeEquation(calculator.name) return eq - def registerFunction(self, f, name=None, argnames=None): - """Register a function so it can be used within equation strings. + @deprecated(registerCalculator_deprecation_msg) + def registerCalculator(self, f, argnames=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_calculator + instead. + """ + return self.register_calculator(f, argnames=argnames) + + def register_function(self, function, name=None, argnames=None): + """Register a function so it can be used within equation + strings. This creates a function with this class that can be used within string - equations. The resulting equation does not require the arguments to be + equations. The resulting equation does not require the arguments to be passed in the equation string, as this will be handled automatically. - Attributes + Parameters ---------- - f + function : callable The callable to register. If this is an Equation instance, then all that needs to be provided is a name. - name + name : str or None, optional The name of the function to be used in equations. If this is None (default), the method will try to determine the name of the function automatically. - argnames - The names of the arguments to f (list or None). + argnames : list or None, optional + The names of the arguments to `function` (list or None). If this is None (default), then the argument names will be extracted from the function. - - Note that name and argnames can be extracted from regular python - functions (of type 'function'), bound class methods and callable + Note + ---- + The `name` and `argnames` args can be extracted from regular Python + functions (of type ), bound class methods, and callable classes. + Raises + ------ + TypeError + If name or argnames cannot be automatically extracted. + TypeError + If an automatically extracted name is ''. + ValueError + If function is an Equation object and name is None. - Raises TypeError if name or argnames cannot be automatically - extracted. - Raises TypeError if an automatically extracted name is ''. - Raises ValueError if f is an Equation object and name is None. - - Returns the callable Equation object. + Returns + ------- + equation_object : Equation + The callable Equation object. """ # If the function is an equation, we treat it specially. This is # required so that the objects observed by the root get observed if the # Equation is used within another equation. It is assumed that a plain # function is not observable. - if isinstance(f, Equation): + if isinstance(function, Equation): if name is None: - m = "Equation must be given a name" + m = ( + "The equation must be given a name. " + "Specify a name with the 'name' argument." + ) raise ValueError(m) - self._eqfactory.registerOperator(name, f) - return f + self._eqfactory.registerOperator(name, function) + return function # Introspection code if name is None or argnames is None: @@ -567,15 +764,17 @@ def registerFunction(self, f, name=None, argnames=None): offset = 0 # check regular functions - if inspect.isfunction(f): - fncode = f.__code__ + if inspect.isfunction(function): + fncode = function.__code__ # check class method - elif inspect.ismethod(f): - fncode = f.__func__.__code__ + elif inspect.ismethod(function): + fncode = function.__func__.__code__ offset = 1 # check functor - elif hasattr(f, "__call__") and hasattr(f.__call__, "__func__"): - fncode = f.__call__.__func__.__code__ + elif hasattr(function, "__call__") and hasattr( + function.__call__, "__func__" + ): + fncode = function.__call__.__func__.__code__ offset = 1 else: m = "Cannot extract name or argnames" @@ -603,179 +802,271 @@ def registerFunction(self, f, name=None, argnames=None): # Initialize and register from diffpy.srfit.fitbase.calculator import Calculator - if isinstance(f, Calculator): + if isinstance(function, Calculator): for pname in argnames: - par = self.get(pname) - f.addLiteral(par) - self._eqfactory.registerOperator(name, f) + parameter = self.get(pname) + function.addLiteral(parameter) + self._eqfactory.registerOperator(name, function) else: - self._eqfactory.registerFunction(name, f, argnames) + self._eqfactory.register_function(name, function, argnames) # Now we can create the Equation and return it to the user. - eq = self._eqfactory.makeEquation(name) + equation_object = self._eqfactory.makeEquation(name) - return eq + return equation_object - def registerStringFunction(self, fstr, name, ns={}): + @deprecated(registerFunction_deprecation_msg) + def registerFunction(self, f, name=None, argnames=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_function + instead. + """ + return self.register_function(f, name=name, argnames=argnames) + + def register_string_function(self, function_str, name, func_params={}): """Register a string function. This creates a function with this class that can be used within string - equations. The resulting equation does not require the arguments to be + equations. The resulting equation does not require the arguments to be passed in the function string, as this will be handled automatically. - Attributes + Parameters ---------- - fstr + function_str : str A string equation to register. - name + name : str The name of the function to be used in equations. - ns + func_params : dict, optional A dictionary of Parameters, indexed by name, that are - used in fstr, but not part of the FitRecipe (default - {}). + used in `function_str`, but not part of the FitRecipe (default {}). + Raises + ------ + ValueError + If `func_params` uses a name that is already used for another + managed object. + ValueError + If the function name is the name of another managed object. - Raises ValueError if ns uses a name that is already used for another - managed object. - Raises ValueError if the function name is the name of another managed - object. - - Returns the callable Equation object. + Returns + ------- + equation_object : Equation + The callable Equation object. """ # Build the equation instance. - eq = equationFromString(fstr, self._eqfactory, ns=ns, buildargs=True) + eq = get_equation_from_string( + function_str, self._eqfactory, ns=func_params, buildargs=True + ) eq.name = name # Register any new Parameters. - for par in self._eqfactory.newargs: - self._add_parameter(par) + for parameter in self._eqfactory.newargs: + self._add_parameter(parameter) # Register the equation as a callable function. argnames = eq.argdict.keys() - return self.registerFunction(eq, name, argnames) + equation_object = self.register_function( + eq, name=name, argnames=argnames + ) + return equation_object - def evaluateEquation(self, eqstr, ns={}): + @deprecated(registerStringFunction_deprecation_msg) + def registerStringFunction(self, fstr, name, ns={}): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_string_function + instead. + """ + return self.register_string_function(fstr, name, func_params=ns) + + def evaluate_equation(self, equation_str, func_params={}): """Evaluate a string equation. - Attributes + This method takes a string representation of a mathematical equation + and evaluates it using the current values of the registered Parameters + in the FitRecipe. Additional parameters not part of the FitRecipe can + also be provided via the `func_params` dictionary. + + Parameters ---------- - eqstr - A string equation to evaluate. The equation is evaluated at + equation_str + The string equation to evaluate. The equation is evaluated at the current value of the registered Parameters. - ns - A dictionary of Parameters, indexed by name, that are - used in fstr, but not part of the FitRecipe (default {}). - + func_params : dict, optional + The dictionary of Parameters, indexed by name, that are + used in `equation_str`, but not part of the FitRecipe + (default `{}`). - Raises ValueError if ns uses a name that is already used for a - variable. + Returns + ------- + returned_value : float + The value of the evaluated equation. + + Raises + ------ + ValueError + If `func_params` uses a name that is already used for a + variable. """ - eq = equationFromString(eqstr, self._eqfactory, ns) + eq = get_equation_from_string( + equation_str, self._eqfactory, func_params + ) try: - rv = eq() + returned_value = eq() finally: self._eqfactory.wipeout(eq) - return rv + return returned_value + + @deprecated(evaluateEquation_deprecation_msg) + def evaluateEquation(self, eqstr, ns={}): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.evaluate_equation + instead. + """ + return self.evaluate_equation(eqstr, func_params=ns) - def constrain(self, par, con, ns={}): + def add_constraint(self, parameter, constraint_eq, params={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. - Attributes + Parameters ---------- - par + parameter : str or Parameter The name of a Parameter or a Parameter to constrain. - con + constraint_eq : str or Equation A string representation of the constraint equation or a - Parameter to constrain to. A constraint equation must + Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. - Parameters are known if they are in the ns argument, or if - they are managed by this object. - ns + Parameters are known if they are in the `params` + argument, or if they are managed by this object. + params : dict, optional A dictionary of Parameters, indexed by name, that are used - in the parameter, but not part of this object (default {}). - - - Raises ValueError if ns uses a name that is already used for a - variable. - Raises ValueError if par is a string but not part of this object or in - ns. - Raises ValueError if par is marked as constant. + in `parameter`, but not part of this object (default {}). + + Raises + ------ + ValueError + If `params` uses a name that is already used for a + variable. + ValueError + If `parameter` is a string but not part of this object or + in `params`. + ValueError + If `parameter` is marked as constant. """ - if isinstance(par, six.string_types): - name = par - par = self.get(name) - if par is None: - par = ns.get(name) + if isinstance(parameter, str): + name = parameter + parameter = self.get(name) + if parameter is None: + parameter = params.get(name) - if par is None: + if parameter is None: raise ValueError("The parameter cannot be found") - if par.const: - raise ValueError("The parameter '%s' is constant" % par) + if parameter.const: + raise ValueError("The parameter '%s' is constant" % parameter) - if isinstance(con, six.string_types): - eqstr = con - eq = equationFromString(con, self._eqfactory, ns) + if isinstance(constraint_eq, str): + eqstr = constraint_eq + eq = get_equation_from_string( + constraint_eq, self._eqfactory, params + ) else: - eq = Equation(root=con) - eqstr = con.name + eq = Equation(root=constraint_eq) + eqstr = constraint_eq.name - eq.name = "_constraint_%s" % par.name + eq.name = "_constraint_%s" % parameter.name # Make and store the constraint - con = Constraint() - con.constrain(par, eq) + constraint_eq = Constraint() + constraint_eq.add_constraint(parameter, eq) # Store the equation string so it can be shown later. - con.eqstr = eqstr - self._constraints[par] = con + constraint_eq.eqstr = eqstr + self._constraints[parameter] = constraint_eq # Our configuration changed self._update_configuration() return - def isConstrained(self, par): + @deprecated(constrain_deprecation_msg) + def constrain(self, parameter, constraint_eq, params={}): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.add_constraint + instead. + """ + self.add_constraint(parameter, constraint_eq, params=params) + return + + def is_constrained(self, parameter): """Determine if a Parameter is constrained in this object. - Attributes + Parameters ---------- - par + parameter : str or Parameter The name of a Parameter or a Parameter to check. + + Returns + ------- + bool + True if the Parameter is constrained in this object, False + otherwise. """ - if isinstance(par, six.string_types): - name = par - par = self.get(name) + if isinstance(parameter, str): + name = parameter + parameter = self.get(name) - return par in self._constraints + return parameter in self._constraints - def unconstrain(self, *pars): + @deprecated(isConstrained_deprecation_msg) + def isConstrained(self, parameter): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.is_constrained + instead. + """ + return self.is_constrained(parameter) + + def remove_constraint(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. - Attributes + Parameters ---------- - *pars + *pars : str or Parameter The names of Parameters or Parameters to unconstrain. Raises ValueError if the Parameter is not constrained. """ update = False - for par in pars: - if isinstance(par, six.string_types): - name = par - par = self.get(name) + for parameter in pars: + if isinstance(parameter, str): + name = parameter + parameter = self.get(name) - if par is None: + if parameter is None: raise ValueError("The parameter cannot be found") - if par in self._constraints: - self._constraints[par].unconstrain() - del self._constraints[par] + if parameter in self._constraints: + self._constraints[parameter].remove_constraint() + del self._constraints[parameter] update = True if update: @@ -788,95 +1079,199 @@ def unconstrain(self, *pars): return - def getConstrainedPars(self, recurse=False): + @deprecated(unconstrain_deprecation_msg) + def unconstrain(self, *pars): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.remove_constraint + instead. + """ + self.remove_constraint(*pars) + return + + def get_constrained_parmeters(self, recurse=False): """Get a list of constrained managed Parameters in this object. - Attributes + Parameters ---------- - recurse - Recurse into managed objects and retrieve their constrained - Parameters as well (default False). + recurse : bool, optional + If False (default), only constrained Parameters in + this object are returned. If True, constrained + Parameters in managed sub-objects are also included. + + Return + ------ + constrained_params : list of Parameter + A list of constrained managed Parameters in this object. """ const = self._get_constraints(recurse) - return const.keys() + constrained_params = const.keys() + return constrained_params - def clearConstraints(self, recurse=False): - """Clear all constraints managed by this organizer. + @deprecated(getConstrainedPars_deprecation_msg) + def getConstrainedPars(self, recurse=False): + """Get a list of constrained managed Parameters in this object. - Attributes + Parameters ---------- recurse - Recurse into managed objects and clear all constraints - found there as well. + Recurse into managed objects and retrieve their constrained + Parameters as well (default False). + """ + return self.get_constrained_parmeters(recurse=recurse) + def clear_all_constraints(self, recurse=False): + """Clear all constraints managed by this organizer. This removes constraints that are held in this organizer, no matter where the constrained parameters are from. + + Parameters + ---------- + recurse : bool, optional + If False (default), only constraints in this object + are cleared. If True, constraints in managed + sub-objects are also cleared. """ if self._constraints: - self.unconstrain(*self._constraints) + self.remove_constraint(*self._constraints) if recurse: for m in filter(_has_clear_constraints, self._iter_managed()): - m.clearConstraints(recurse) + m.clear_all_constraints(recurse) return - def restrain(self, res, lb=-inf, ub=inf, sig=1, scaled=False, ns={}): - """Restrain an expression to specified bounds. - - Attributes - ---------- - res - An equation string or Parameter to restrain. - lb - The lower bound on the restraint evaluation (default -inf). - ub - The lower bound on the restraint evaluation (default inf). - sig - The uncertainty on the bounds (default 1). - scaled - A flag indicating if the restraint is scaled (multiplied) - by the unrestrained point-average chi^2 (chi^2/numpoints) - (default False). - ns - A dictionary of Parameters, indexed by name, that are used - in the equation string, but not part of the RecipeOrganizer - (default {}). + @deprecated(clearConstraints_deprecation_msg) + def clearConstraints(self, recurse=False): + """This function has been deprecated and will be removed in + version 4.0.0. + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.clear_all_constraints + instead. + """ + return self.clear_all_constraints(recurse=recurse) + + def add_soft_bounds( + self, + param_or_eq, + lower_bound=-inf, + upper_bound=inf, + sig=1, + scaled=False, + params={}, + ): + """Restrain an expression to specified bounds. - The penalty is calculated as - (max(0, lb - val, val - ub)/sig)**2 - and val is the value of the calculated equation. This is multiplied by - the average chi^2 if scaled is True. + See Notes for how the penalty is calculated. + Parameters + ---------- + param_or_eq : str + The equation or parameter to restrain. + lower_bound : float, optional + The lower bound for the restraint evaluation (default is -inf). + upper_bound : float, optional + The upper bound for the restraint evaluation (default is inf). + sig : float, optional + The uncertainty associated with the bounds (default is 1). + Please see Notes for how this is used in the penalty calculation. + scaled : bool, optional + If True, the restraint penalty is scaled by the unrestrained + point-average chi^2 (chi^2/numpoints) (default is False). + params : dict, optional + The dictionary of Parameters, indexed by name, that are used in + `param_or_eq` (if an equation string is used) but are not part + of the RecipeOrganizer (default is {}). - Raises ValueError if ns uses a name that is already used for a - Parameter. - Raises ValueError if res depends on a Parameter that is not part of - the RecipeOrganizer and that is not defined in ns. + Returns + ------- + Restraint + The created Restraint object, which can be used with the + 'unrestrain' method. + + Notes + ----- + The penalty is calculated as: + + .. + (max(0, lower_bound - val, val - upper_bound) / sig) ** 2 + + where `val` is the value of the evaluated `param_or_eq`. + If `scaled` is True, this penalty is multiplied by + the average chi^2. + + Examples + -------- + Restraining the lattice parameters of an Ni lattice to be + approximately 7.4Å (2x the original lattice param) + can be done with the following code: + .. + recipe.add_soft_bounds( + "a_ni + b_ni", + lower_bound=7.0, + upper_bound=7.5, + sig=0.1, + scaled=True, + params={"b_ni": Parameter("b_ni", 3.473)} + ) - Returns the Restraint object for use with the 'unrestrain' method. + Raises + ------ + ValueError + If `params` contains a name that is already used + for a Parameter. + ValueError + If `param_or_eq` depends on a Parameter that is not part of the + RecipeOrganizer and is not defined in `params`. """ - - if isinstance(res, six.string_types): - eqstr = res - eq = equationFromString(res, self._eqfactory, ns) + if isinstance(param_or_eq, str): + eqstr = param_or_eq + eq = get_equation_from_string(param_or_eq, self._eqfactory, params) else: - eq = Equation(root=res) - eqstr = res.name + eq = Equation(root=param_or_eq) + eqstr = param_or_eq.name # Make and store the restraint - res = Restraint(eq, lb, ub, sig, scaled) - res.eqstr = eqstr - self.addRestraint(res) - return res - - def addRestraint(self, res): + param_or_eq = Restraint(eq, lower_bound, upper_bound, sig, scaled) + param_or_eq.eqstr = eqstr + self.register_soft_bounds(param_or_eq) + return param_or_eq + + @deprecated(restrain_deprecation_msg) + def restrain( + self, + param_or_eq, + lower_bound=-inf, + upper_bound=inf, + sig=1, + scaled=False, + params={}, + ): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.add_soft_bounds + instead. + """ + return self.add_soft_bounds( + param_or_eq, + lower_bound=lower_bound, + upper_bound=upper_bound, + sig=sig, + scaled=scaled, + params=params, + ) + + def register_soft_bounds(self, res): """Add a Restraint instance to the RecipeOrganizer. - Attributes + Parameters ---------- - res + res : Restraint A Restraint instance. """ self._restraints.add(res) @@ -884,14 +1279,26 @@ def addRestraint(self, res): self._update_configuration() return - def unrestrain(self, *ress): + @deprecated(addRestraint_deprecation_msg) + def addRestraint(self, res): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.register_soft_bounds + instead. + """ + self.register_soft_bounds(res) + return + + def remove_soft_bounds(self, *ress): """Remove a Restraint from the RecipeOrganizer. - Attributes + Parameters ---------- - *ress - Restraints returned from the 'restrain' method or added - with the 'addRestraint' method. + *ress : Restraint + The Restraints returned from the 'add_soft_bounds' method or added + with the 'register_soft_bounds' method. """ update = False restuple = tuple(self._restraints) @@ -906,23 +1313,48 @@ def unrestrain(self, *ress): return - def clearRestraints(self, recurse=False): + @deprecated(unrestrain_deprecation_msg) + def unrestrain(self, *ress): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.remove_soft_bounds + instead. + """ + self.remove_soft_bounds(*ress) + return + + def clear_all_soft_bounds(self, recurse=False): """Clear all restraints. - Attributes + Parameters ---------- recurse Recurse into managed objects and clear all restraints found there as well. """ - self.unrestrain(*self._restraints) + self.remove_soft_bounds(*self._restraints) if recurse: for msg in filter(_has_clear_restraints, self._iter_managed()): - msg.clearRestraints(recurse) + msg.clear_all_soft_bounds(recurse) + return + + @deprecated(clearRestraints_deprecation_msg) + def clearRestraints(self, recurse=False): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.RecipeOrganizer.clear_all_soft_bounds + instead. + """ + self.clear_all_soft_bounds(recurse=recurse) return def _get_constraints(self, recurse=True): - """Get the constrained Parameters for this and managed sub-objects.""" + """Get the constrained Parameters for this and managed sub- + objects.""" constraints = {} if recurse: for m in filter(_has_get_constraints, self._iter_managed()): @@ -1007,13 +1439,13 @@ def _format_constraints(self): cdict = self._get_constraints() # Find each constraint and format the equation clines = [] - for par, con in cdict.items(): - loc = self._locate_managed_object(par) + for parameter, con in cdict.items(): + loc = self._locate_managed_object(parameter) if loc: locstr = ".".join(o.name for o in loc[1:]) clines.append("%s <-- %s" % (locstr, con.eqstr)) else: - clines.append("%s <-- %s" % (par.name, con.eqstr)) + clines.append("%s <-- %s" % (parameter.name, con.eqstr)) clines.sort(key=numstr) return clines @@ -1032,12 +1464,15 @@ def _format_restraints(self): rset = self._get_restraints() rlines = [] for res in rset: - line = "%s: lb = %f, ub = %f, sig = %f, scaled = %s" % ( - res.eqstr, - res.lb, - res.ub, - res.sig, - res.scaled, + line = ( + "%s: lower_bound = %f, upper_bound = %f, sig = %f, scaled = %s" + % ( + res.eqstr, + res.lower_bound, + res.upper_bound, + res.sig, + res.scaled, + ) ) rlines.append(line) rlines.sort(key=numstr) @@ -1103,38 +1538,45 @@ def show(self, pattern="", textwidth=78): # End RecipeOrganizer -def equationFromString( +def get_equation_from_string( eqstr, factory, ns={}, buildargs=False, argclass=Parameter, argkw={} ): - """Make an equation from a string. + """Make an Equation object from a string. - Attributes + Parameters ---------- - eqstr + eqstr : str A string representation of the equation. The equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in ns, or already defined in the factory. - factory + factory : EquationFactory An EquationFactory instance. - ns - A dictionary of Parameters indexed by name that are used + ns : dict, optional + The dictionary of Parameters indexed by name that are used in the eqstr but not already defined in the factory (default {}). - buildargs + buildargs : bool, optional A flag indicating whether missing Parameters can be created by the Factory (default False). If False, then the a ValueError will be raised if there are undefined arguments in the eqstr. - argclass + argclass : Parameter class, optional Class to use when creating new Arguments (default Parameter). The class constructor must accept the 'name' key word. - argkw + argkw : dict, optional Key word dictionary to pass to the argclass constructor (default {}). + Returns + ------- + eq : Equation + An Equation instance representing the equation in eqstr. - Raises ValueError if ns uses a name that is already defined in the factory. - Raises ValueError if the equation has undefined parameters. + Raises + ------ + ValueError + If buildargs is False and there are undefined parameters in eqstr + or if ns uses a name that is already defined in the factory. """ defined = set(factory.builders.keys()) @@ -1156,12 +1598,33 @@ def equationFromString( return eq +@deprecated(equationFromString_deprecation_msg) +def equationFromString( + eqstr, factory, ns={}, buildargs=False, argclass=Parameter, argkw={} +): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.recipeorganizer.get_equation_from_string + instead. + """ + return get_equation_from_string( + eqstr, + factory, + ns=ns, + buildargs=buildargs, + argclass=argclass, + argkw=argkw, + ) + + def _has_clear_constraints(msg): - return hasattr(msg, "clearConstraints") + return hasattr(msg, "clear_all_constraints") def _has_clear_restraints(msg): - return hasattr(msg, "clearRestraints") + return hasattr(msg, "clear_all_soft_bounds") def _has_get_restraints(msg): diff --git a/src/diffpy/srfit/fitbase/restraint.py b/src/diffpy/srfit/fitbase/restraint.py index 5630d808..fb0006d2 100644 --- a/src/diffpy/srfit/fitbase/restraint.py +++ b/src/diffpy/srfit/fitbase/restraint.py @@ -33,39 +33,41 @@ class Restraint(Validatable): Attributes ---------- - eq + eq : Equation An equation whose evaluation is compared against the restraint bounds. - lb + lower_bound : float The lower bound on the restraint evaluation (default -inf). - ub + upper_bound : float The lower bound on the restraint evaluation (default inf). - sig + sig : float The uncertainty on the bounds (default 1). - scaled + scaled : bool A flag indicating if the restraint is scaled (multiplied) by the unrestrained point-average chi^2 (chi^2/numpoints) (default False). The penalty is calculated as - (max(0, lb - val, val - ub)/sig)**2 + (max(0, lower_bound - val, val - upper_bound)/sig)**2 and val is the value of the calculated equation. This is multiplied by the average chi^2 if scaled is True. """ - def __init__(self, eq, lb=-inf, ub=inf, sig=1, scaled=False): + def __init__( + self, eq, lower_bound=-inf, upper_bound=inf, sig=1, scaled=False + ): """Restrain an equation to specified bounds. - Attributes + Parameters ---------- eq An equation whose evaluation is compared against the restraint bounds. - lb + lower_bound The lower bound on the restraint evaluation (float, default -inf). - ub + upper_bound The lower bound on the restraint evaluation (float, default inf). sig @@ -76,8 +78,8 @@ def __init__(self, eq, lb=-inf, ub=inf, sig=1, scaled=False): (bool, default False). """ self.eq = eq - self.lb = float(lb) - self.ub = float(ub) + self.lower_bound = float(lower_bound) + self.upper_bound = float(upper_bound) self.sig = float(sig) self.scaled = bool(scaled) return @@ -85,7 +87,7 @@ def __init__(self, eq, lb=-inf, ub=inf, sig=1, scaled=False): def penalty(self, w=1.0): """Calculate the penalty of the restraint. - Attributes + Parameters ---------- w The point-average chi^2 which is optionally used to scale the @@ -97,7 +99,9 @@ def penalty(self, w=1.0): Returns the penalty as a float """ val = self.eq() - penalty = (max(0, self.lb - val, val - self.ub) / self.sig) ** 2 + penalty = ( + max(0, self.lower_bound - val, val - self.upper_bound) / self.sig + ) ** 2 if self.scaled: penalty *= w diff --git a/src/diffpy/srfit/fitbase/simplerecipe.py b/src/diffpy/srfit/fitbase/simplerecipe.py index 81a0094a..5f27889b 100644 --- a/src/diffpy/srfit/fitbase/simplerecipe.py +++ b/src/diffpy/srfit/fitbase/simplerecipe.py @@ -12,12 +12,66 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Simple FitRecipe class that includes a FitContribution and Profile.""" +"""Simple FitRecipe class that includes a FitContribution and +Profile.""" from diffpy.srfit.fitbase.fitcontribution import FitContribution from diffpy.srfit.fitbase.fitrecipe import FitRecipe from diffpy.srfit.fitbase.fitresults import FitResults from diffpy.srfit.fitbase.profile import Profile +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.srfit.fitbase.SimpleRecipe" +removal_version = "4.0.0" + +loadParsedData_dep_msg = build_deprecation_message( + base, + "loadParsedData", + "load_parsed_data", + removal_version, +) + +setObservedProfile_dep_msg = build_deprecation_message( + base, + "setObservedProfile", + "set_observed_profile", + removal_version, +) + +setCalculationRange_dep_msg = build_deprecation_message( + base, + "setCalculationRange", + "set_calculation_range", + removal_version, +) + +setCalculationPoints_dep_msg = build_deprecation_message( + base, + "setCalculationPoints", + "set_calculation_points", + removal_version, +) + +setEquation_dep_msg = build_deprecation_message( + base, + "setEquation", + "set_equation", + removal_version, +) + +printResults_dep_msg = build_deprecation_message( + base, + "printResults", + "print_results", + removal_version, +) + +saveResults_dep_msg = build_deprecation_message( + base, + "saveResults", + "save_results", + removal_version, +) class SimpleRecipe(FitRecipe): @@ -82,9 +136,9 @@ class SimpleRecipe(FitRecipe): Properties ---------- names - Variable names (read only). See getNames. + Variable names (read only). See get_names. values - Variable values (read only). See getValues. + Variable values (read only). See get_values. """ def __init__(self, name="fit", conclass=FitContribution): @@ -94,7 +148,7 @@ def __init__(self, name="fit", conclass=FitContribution): contribution = conclass("contribution") self.profile = Profile() contribution.set_profile(self.profile) - self.addContribution(contribution) + self.add_contribution(contribution) self.results = FitResults(self, update=False) # Adopt all the FitContribution methods @@ -109,14 +163,25 @@ def __init__(self, name="fit", conclass=FitContribution): return # Profile methods - def loadParsedData(self, parser): + def load_parsed_data(self, parser): """Load parsed data from a ProfileParser. This sets the xobs, yobs, dyobs arrays as well as the metadata. """ - return self.profile.loadParsedData(parser) + return self.profile.load_parsed_data(parser) - def setObservedProfile(self, xobs, yobs, dyobs=None): + @deprecated(loadParsedData_dep_msg) + def loadParsedData(self, parser): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.load_parsed_data + instead. + """ + return self.load_parsed_data(parser) + + def set_observed_profile(self, xobs, yobs, dyobs=None): """Set the observed profile. Parameters @@ -134,9 +199,20 @@ def setObservedProfile(self, xobs, yobs, dyobs=None): Raises ValueError if len(yobs) != len(xobs) Raises ValueError if dyobs != None and len(dyobs) != len(xobs) """ - return self.profile.setObservedProfile(xobs, yobs, dyobs) + return self.profile.set_observed_profile(xobs, yobs, dyobs) - def setCalculationRange(self, xmin=None, xmax=None, dx=None): + @deprecated(setObservedProfile_dep_msg) + def setObservedProfile(self, xobs, yobs, dyobs=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.set_observed_profile + instead. + """ + return self.set_observed_profile(xobs, yobs, dyobs) + + def set_calculation_range(self, xmin=None, xmax=None, dx=None): """Set epsilon-inclusive calculation range. Adhere to the observed ``xobs`` points when ``dx`` is the same @@ -168,9 +244,20 @@ def setCalculationRange(self, xmin=None, xmax=None, dx=None): ValueError When xmin > xmax or if dx <= 0. Also if dx > xmax - xmin. """ - return self.profile.setCalculationRange(xmin, xmax, dx) + return self.profile.set_calculation_range(xmin, xmax, dx) - def setCalculationPoints(self, x): + @deprecated(setCalculationRange_dep_msg) + def setCalculationRange(self, xmin=None, xmax=None, dx=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.set_calculation_range + instead. + """ + return self.set_calculation_range(xmin, xmax, dx) + + def set_calculation_points(self, x): """Set the calculation points. Parameters @@ -182,7 +269,18 @@ def setCalculationPoints(self, x): This will create y and dy on the specified grid if xobs, yobs and dyobs exist. """ - return self.profile.setCalculationPoints(x) + return self.profile.set_calculation_points(x) + + @deprecated(setCalculationPoints_dep_msg) + def setCalculationPoints(self, x): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.set_calculation_points + instead. + """ + return self.set_calculation_points(x) def loadtxt(self, *args, **kw): """Use numpy.loadtxt to load data. @@ -191,7 +289,7 @@ def loadtxt(self, *args, **kw): enforced. The first two arrays returned by numpy.loadtxt are assumed to be x and y. If there is a third array, it is assumed to by dy. Any other arrays are ignored. These are passed to - setObservedProfile. + set_observed_profile. Raises ValueError if the call to numpy.loadtxt returns fewer than 2 arrays. @@ -201,14 +299,14 @@ def loadtxt(self, *args, **kw): return self.profile.loadtxt(*args, **kw) # FitContribution - def setEquation(self, eqstr, ns={}): + def set_equation(self, eqstr, ns={}): """Set the profile equation for the FitContribution. This sets the equation that will be used when generating the residual. - The equation will be usable within setResidualEquation as "eq", and it - takes no arguments. + The equation will be usable within set_residual_equation as "eq", and + it takes no arguments. - Attributes + Parameters ---------- eqstr A string representation of the equation. Variables will be @@ -222,7 +320,7 @@ def setEquation(self, eqstr, ns={}): Raises ValueError if ns uses a name that is already used for a variable. """ - self.contribution.setEquation(eqstr, ns={}) + self.contribution.set_equation(eqstr, ns={}) # Extract variables for par in self.contribution: # Skip Profile Parameters @@ -231,7 +329,19 @@ def setEquation(self, eqstr, ns={}): if par.value is None: par.value = 0 if par.name not in self._parameters: - self.addVar(par) + self.add_variable(par) + return + + @deprecated(setEquation_dep_msg) + def setEquation(self, eqstr, ns={}): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.set_equation + instead. + """ + self.set_equation(eqstr, ns) return def __call__(self): @@ -239,21 +349,32 @@ def __call__(self): return self.contribution.evaluate() # FitResults methods - - def printResults(self, header="", footer=""): + def print_results(self, header="", footer=""): """Format and print the results. - Attributes + Parameters ---------- header A header to add to the output (default "") footer A footer to add to the output (default "") """ - self.results.printResults(header, footer, True) + self.results.print_results(header, footer, True) return - def saveResults(self, filename, header="", footer=""): + @deprecated(printResults_dep_msg) + def printResults(self, header="", footer=""): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.print_results + instead. + """ + self.print_results(header, footer) + return + + def save_results(self, filename, header="", footer=""): """Format and save the results. Parameters @@ -265,7 +386,18 @@ def saveResults(self, filename, header="", footer=""): footer A footer to add to the output (default "") """ - self.results.saveResults(filename, header, footer, True) + self.results.save_results(filename, header, footer, True) + + @deprecated(saveResults_dep_msg) + def saveResults(self, filename, header="", footer=""): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.fitbase.SimpleRecipe.save_results + instead. + """ + self.save_results(filename, header, footer) # End class SimpleRecipe diff --git a/src/diffpy/srfit/interface/__init__.py b/src/diffpy/srfit/interface/__init__.py index 3157c1ec..896ae519 100644 --- a/src/diffpy/srfit/interface/__init__.py +++ b/src/diffpy/srfit/interface/__init__.py @@ -19,7 +19,6 @@ for scripting. """ - from diffpy.srfit.interface.interface import ( FitRecipeInterface, ParameterInterface, diff --git a/src/diffpy/srfit/interface/interface.py b/src/diffpy/srfit/interface/interface.py index 63ccb573..308e0471 100644 --- a/src/diffpy/srfit/interface/interface.py +++ b/src/diffpy/srfit/interface/interface.py @@ -36,7 +36,7 @@ class ParameterInterface(object): """Mix-in class for enhancing the Parameter interface.""" def __lshift__(self, v): - """SetValue with << + """set_value with << Think of '<<' as injecting a value @@ -115,11 +115,11 @@ def __ior__(self, args): This accepts a single argument. """ - self.addContribution(args) + self.add_contribution(args) return self def __iadd__(self, args): - """AddVar or newVar with += + """add_variable or create_new_variable with += Think of "+" as addition of a variable. @@ -127,12 +127,12 @@ def __iadd__(self, args): arguments or argument tuples. """ - # Want to detect addVar or newVar + # Want to detect add_variable or create_new_variable def f(*args): if isinstance(args[0], six.string_types): - self.newVar(*args) + self.create_new_variable(*args) else: - self.addVar(*args) + self.add_variable(*args) return _applymanyargs(args, f) diff --git a/src/diffpy/srfit/pdf/basepdfgenerator.py b/src/diffpy/srfit/pdf/basepdfgenerator.py index e91023e8..e8566b84 100644 --- a/src/diffpy/srfit/pdf/basepdfgenerator.py +++ b/src/diffpy/srfit/pdf/basepdfgenerator.py @@ -117,13 +117,13 @@ def _set_calculator(self, calc): self._calc = calc for pname in self.__class__._parnames: self.addParameter(ParameterAdapter(pname, self._calc, attr=pname)) - self.processMetaData() + self._process_metadata() return def parallel(self, ncpu, mapfunc=None): """Run calculation in parallel. - Attributes + Parameters ---------- ncpu Number of parallel processes. Revert to serial mode when 1. @@ -154,9 +154,9 @@ def parallel(self, ncpu, mapfunc=None): self._calc = createParallelCalculator(calc_serial, ncpu, mapfunc) return - def processMetaData(self): + def _process_metadata(self): """Process the metadata once it gets set.""" - ProfileGenerator.processMetaData(self) + ProfileGenerator._process_metadata(self) stype = self.meta.get("stype") if stype is not None: @@ -174,14 +174,14 @@ def processMetaData(self): val = self.meta.get(name) if val is not None: par = self.get(name) - par.setValue(val) + par.set_value(val) return def setScatteringType(self, stype="X"): """Set the scattering type. - Attributes + Parameters ---------- stype "X" for x-ray, "N" for neutron, "E" for electrons, @@ -230,7 +230,7 @@ def setStructure(self, stru, name="phase", periodic=True): See those classes (located in diffpy.srfit.structure) for how they are used. The resulting ParameterSet will be managed by this generator. - Attributes + Parameters ---------- stru diffpy.structure.Structure, pyobjcryst.crystal.Crystal or @@ -260,7 +260,7 @@ def setPhase(self, parset, periodic=True): object (from diffpy or pyobjcryst). The passed ParameterSet will be managed by this generator. - Attributes + Parameters ---------- parset A SrRealParSet that holds the structural information. @@ -277,7 +277,7 @@ def setPhase(self, parset, periodic=True): self.stru = self._phase.stru # Put this ParameterSet in the ProfileGenerator. - self.addParameterSet(parset) + self.add_parameter_set(parset) # Set periodicity self._phase.useSymmetry(periodic) diff --git a/src/diffpy/srfit/pdf/characteristicfunctions.py b/src/diffpy/srfit/pdf/characteristicfunctions.py index 5d2bf15e..19f8364a 100644 --- a/src/diffpy/srfit/pdf/characteristicfunctions.py +++ b/src/diffpy/srfit/pdf/characteristicfunctions.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Form factors (characteristic functions) used in PDF nanoshape fitting. +"""Form factors (characteristic functions) used in PDF nanoshape +fitting. These are used to calculate the attenuation of the PDF due to a finite size. For a crystal-like nanoparticle, one can calculate the PDF via @@ -20,7 +21,7 @@ function and Gcryst(f) is the crystal PDF. These functions are meant to be imported and added to a FitContribution -using the 'registerFunction' method of that class. +using the 'register_function' method of that class. """ __all__ = [ @@ -47,7 +48,7 @@ def sphericalCF(r, psize): """Spherical nanoparticle characteristic function. - Attributes + Parameters ---------- r distance of interaction @@ -72,7 +73,7 @@ def spheroidalCF(r, erad, prad): Spheroid with radii (erad, erad, prad) - Attributes + Parameters ---------- prad polar radius @@ -94,7 +95,7 @@ def spheroidalCF2(r, psize, axrat): Form factor for ellipsoid with radii (psize/2, psize/2, axrat*psize/2) - Attributes + Parameters ---------- r distance of interaction @@ -202,10 +203,10 @@ def spheroidalCF2(r, psize, axrat): def lognormalSphericalCF(r, psize, psig): - """Spherical nanoparticle characteristic function with lognormal size - distribution. + """Spherical nanoparticle characteristic function with lognormal + size distribution. - Attributes + Parameters ---------- r distance of interaction @@ -262,7 +263,7 @@ def lognormalSphericalCF(r, psize, psig): def sheetCF(r, sthick): """Nanosheet characteristic function. - Attributes + Parameters ---------- r distance of interaction @@ -292,7 +293,7 @@ def sheetCF(r, sthick): def shellCF(r, radius, thickness): """Spherical shell characteristic function. - Attributes + Parameters ---------- radius Inner radius @@ -312,7 +313,7 @@ def shellCF(r, radius, thickness): def shellCF2(r, a, delta): """Spherical shell characteristic function. - Attributes + Parameters ---------- a Central radius @@ -377,7 +378,7 @@ class SASCF(Calculator): def __init__(self, name, model): """Initialize the generator. - Attributes + Parameters ---------- name A name for the SASCF @@ -405,8 +406,8 @@ def __init__(self, name, model): return def __call__(self, r): - """Calculate the characteristic function from the transform of the - BaseModel.""" + """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 diff --git a/src/diffpy/srfit/pdf/debyepdfgenerator.py b/src/diffpy/srfit/pdf/debyepdfgenerator.py index 641c577f..ba84faac 100644 --- a/src/diffpy/srfit/pdf/debyepdfgenerator.py +++ b/src/diffpy/srfit/pdf/debyepdfgenerator.py @@ -92,7 +92,7 @@ def setStructure(self, stru, name="phase", periodic=False): See those classes (located in diffpy.srfit.structure) for how they are used. The resulting ParameterSet will be managed by this generator. - Attributes + Parameters ---------- stru diffpy.structure.Structure, pyobjcryst.crystal.Crystal or @@ -116,7 +116,7 @@ def setPhase(self, parset, periodic=False): object (from diffpy or pyobjcryst). The passed ParameterSet will be managed by this generator. - Attributes + Parameters ---------- parset A SrRealParSet that holds the structural information. diff --git a/src/diffpy/srfit/pdf/pdfcontribution.py b/src/diffpy/srfit/pdf/pdfcontribution.py index bcd694ff..73e666ea 100644 --- a/src/diffpy/srfit/pdf/pdfcontribution.py +++ b/src/diffpy/srfit/pdf/pdfcontribution.py @@ -20,7 +20,7 @@ __all__ = ["PDFContribution"] -from diffpy.srfit.fitbase import FitContribution, Profile +from diffpy.srfit.fitbase import FitContribution, Profile, ProfileParser class PDFContribution(FitContribution): @@ -84,7 +84,7 @@ class PDFContribution(FitContribution): def __init__(self, name): """Create the PDFContribution. - Attributes + Parameters ---------- name The name of the contribution. @@ -105,31 +105,19 @@ def __init__(self, name): # Data methods - def loadData(self, data): - """Load the data in various formats. - - This uses the PDFParser to load the data and then passes it to the - built-in profile with loadParsedData. + def loadData(self, datafile): + """Load the data from a datafile. - Attributes + Parameters ---------- - data - An open file-like object, name of a file that contains data - or a string containing the data. + data : str or Path + The path to the data file. """ - # Get the data into a string - from diffpy.srfit.util.inpututils import inputToString - - datstr = inputToString(data) - - # Load data with a PDFParser - from diffpy.srfit.pdf.pdfparser import PDFParser - - parser = PDFParser() - parser.parseString(datstr) + parser = ProfileParser() + parser.parse_file(datafile) # Pass it to the profile - self.profile.loadParsedData(parser) + self.profile.load_parsed_data(parser) return def setCalculationRange(self, xmin=None, xmax=None, dx=None): @@ -164,7 +152,7 @@ def setCalculationRange(self, xmin=None, xmax=None, dx=None): ValueError When xmin > xmax or if dx <= 0. Also if dx > xmax - xmin. """ - return self.profile.setCalculationRange(xmin, xmax, dx) + return self.profile.set_calculation_range(xmin, xmax, dx) def savetxt(self, fname, **kwargs): """Call numpy.savetxt with x, ycalc, y, dy. @@ -180,7 +168,7 @@ def savetxt(self, fname, **kwargs): def addStructure(self, name, stru, periodic=True): """Add a phase that goes into the PDF calculation. - Attributes + Parameters ---------- name A name to give the generator that will manage the PDF @@ -224,7 +212,7 @@ def addStructure(self, name, stru, periodic=True): def addPhase(self, name, parset, periodic=True): """Add a phase that goes into the PDF calculation. - Attributes + Parameters ---------- name A name to give the generator that will manage the PDF @@ -273,22 +261,22 @@ def _setup_generator(self, gen): with setStructure or setPhase. """ # Add the generator to this FitContribution - self.addProfileGenerator(gen) + self.add_profile_generator(gen) # Set the proper equation for the fit, depending on the number of # phases we have. gnames = self._generators.keys() eqstr = " + ".join(gnames) eqstr = "scale * (%s)" % eqstr - self.setEquation(eqstr) + self.set_equation(eqstr) # Update with our metadata gen.meta.update(self._meta) - gen.processMetaData() + gen._process_metadata() # Constrain the shared parameters - self.constrain(gen.qdamp, self.qdamp) - self.constrain(gen.qbroad, self.qbroad) + self.add_constraint(gen.qdamp, self.qdamp) + self.add_constraint(gen.qbroad, self.qbroad) return # Calculation setup methods @@ -307,7 +295,7 @@ def _get_meta_value(self, kwd): def setScatteringType(self, type="X"): """Set the scattering type. - Attributes + Parameters ---------- type "X" for x-ray or "N" for neutron diff --git a/src/diffpy/srfit/pdf/pdfparser.py b/src/diffpy/srfit/pdf/pdfparser.py index 445835f7..bfb10a15 100644 --- a/src/diffpy/srfit/pdf/pdfparser.py +++ b/src/diffpy/srfit/pdf/pdfparser.py @@ -28,13 +28,24 @@ from diffpy.srfit.exceptions import ParseError from diffpy.srfit.fitbase.profileparser import ProfileParser +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +removal_verison = "4.0.0" +base = "diffpy.srfit.pdf.pdfparser.PDFParser" +new_base = "diffpy.srfit.fitbase.ProfileParser" + +parseFile_dep_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version=removal_verison, + new_base=new_base, +) class PDFParser(ProfileParser): """Class for holding a diffraction pattern. - Attributes - Attributes ---------- _format @@ -108,8 +119,14 @@ class PDFParser(ProfileParser): _format = "PDF" + # Marking this function as deprecated because PDFParser.parseFile calls it + # so when people use PDFParser.parseFile, they will get a + # warning that it is deprecated and they should use + # ProfileParser.parse_file instead. + @deprecated(parseFile_dep_msg) def parseString(self, patstring): - """Parse a string and set the _x, _y, _dx, _dy and _meta variables. + """Parse a string and set the _x, _y, _dx, _dy and _meta + variables. When _dx or _dy cannot be obtained in the data format it is set to 0. @@ -122,6 +139,7 @@ def parseString(self, patstring): Raises ParseError if the string cannot be parsed """ + # useful regex patterns: rx = {"f": r"[-+]?(\d+(\.\d*)?|\d*\.\d+)([eE][-+]?\d+)?"} # find where does the data start diff --git a/src/diffpy/srfit/sas/prcalculator.py b/src/diffpy/srfit/sas/prcalculator.py index 17c63de6..00d3c8e8 100644 --- a/src/diffpy/srfit/sas/prcalculator.py +++ b/src/diffpy/srfit/sas/prcalculator.py @@ -66,7 +66,7 @@ class PrCalculator(Calculator): def __init__(self, name): """Initialize the generator. - Attributes + Parameters ---------- name A name for the PrCalculator @@ -116,7 +116,8 @@ def _inverted(self, x, c): class CFCalculator(PrCalculator): - """A class for calculating the characteristic function (CF) from data. + """A class for calculating the characteristic function (CF) from + data. This calculator produces f(r) = P(r) / 4 pi r**2 diff --git a/src/diffpy/srfit/sas/sasgenerator.py b/src/diffpy/srfit/sas/sasgenerator.py index 238ae43f..a986e283 100644 --- a/src/diffpy/srfit/sas/sasgenerator.py +++ b/src/diffpy/srfit/sas/sasgenerator.py @@ -43,7 +43,7 @@ class SASGenerator(ProfileGenerator): def __init__(self, name, model): """Initialize the generator. - Attributes + Parameters ---------- name A name for the SASGenerator diff --git a/src/diffpy/srfit/sas/sasimport.py b/src/diffpy/srfit/sas/sasimport.py index 2c15c4ad..cfd10fa8 100644 --- a/src/diffpy/srfit/sas/sasimport.py +++ b/src/diffpy/srfit/sas/sasimport.py @@ -18,7 +18,7 @@ def sasimport(modname): """Import specified module from the SasView sas package. - Attributes + Parameters ---------- modname absolute module name contained in the sas package. diff --git a/src/diffpy/srfit/sas/sasparameter.py b/src/diffpy/srfit/sas/sasparameter.py index ad3d9895..b74a58f1 100644 --- a/src/diffpy/srfit/sas/sasparameter.py +++ b/src/diffpy/srfit/sas/sasparameter.py @@ -33,9 +33,9 @@ class SASParameter(Parameter): const A flag indicating whether this is considered a constant. _value - The value of the Parameter. Modified with 'setValue'. + The value of the Parameter. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' and 'set_value'. constrained A flag indicating if the Parameter is constrained (default False). @@ -52,7 +52,7 @@ class SASParameter(Parameter): def __init__(self, name, model, parname=None): """Create the Parameter. - Attributes + Parameters ---------- name Name of the Parameter @@ -73,7 +73,7 @@ def getValue(self): value = self._model.getParam(self._parname) return value - def setValue(self, value): + def set_value(self, value): """Set the value of the Parameter.""" if value != self.getValue(): self._model.setParam(self._parname, value) diff --git a/src/diffpy/srfit/sas/sasparser.py b/src/diffpy/srfit/sas/sasparser.py index 1ddd48b3..5503a417 100644 --- a/src/diffpy/srfit/sas/sasparser.py +++ b/src/diffpy/srfit/sas/sasparser.py @@ -30,8 +30,6 @@ class SASParser(ProfileParser): This uses a sas DataLoader to load the data. The DataInfo object it returns is held in the metadata under the name "datainfo". - Attributes - Attributes ---------- _format @@ -91,7 +89,8 @@ class SASParser(ProfileParser): _format = "SAS" def parseFile(self, filename): - """Parse a file and set the _x, _y, _dx, _dy and _meta variables. + """Parse a file and set the _x, _y, _dx, _dy and _meta + variables. This wipes out the currently loaded data and selected bank number. @@ -127,11 +126,12 @@ def parseFile(self, filename): # 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.selectBank(0) + self.select_bank(0) return def parseString(self, patstring): - """Parse a string and set the _x, _y, _dx, _dy and _meta variables. + """Parse a string and set the _x, _y, _dx, _dy and _meta + variables. When _dx or _dy cannot be obtained in the data format it is set to 0. diff --git a/src/diffpy/srfit/sas/sasprofile.py b/src/diffpy/srfit/sas/sasprofile.py index a5ad4225..2ac6ab3b 100644 --- a/src/diffpy/srfit/sas/sasprofile.py +++ b/src/diffpy/srfit/sas/sasprofile.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Class for adapting a sas DataInfo objects to the Profile interface.""" +"""Class for adapting a sas DataInfo objects to the Profile +interface.""" __all__ = ["SASProfile"] @@ -29,8 +30,6 @@ class SASProfile(Profile): Otherwise, use the SASParser class and load the data into a base Profile object. - Attributes - Attributes ---------- _xobs @@ -77,7 +76,7 @@ class SASProfile(Profile): def __init__(self, datainfo): """Initialize the attributes. - Attributes + Parameters ---------- datainfo The DataInfo object this wraps. diff --git a/src/diffpy/srfit/srfit_app.py b/src/diffpy/srfit/srfit_app.py new file mode 100644 index 00000000..65b92262 --- /dev/null +++ b/src/diffpy/srfit/srfit_app.py @@ -0,0 +1,33 @@ +import argparse + +from diffpy.srfit.version import __version__ + + +def main(): + parser = argparse.ArgumentParser( + prog="diffpy.srfit", + description=( + "Generalized code base for modeling problems.\n\n" + "For more information, visit: " + "https://github.com/diffpy/diffpy.srfit/" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", + action="store_true", + help="Show the program's version number and exit", + ) + + args = parser.parse_args() + + if args.version: + print(f"diffpy.srfit {__version__}") + else: + # Default behavior when no arguments are given + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/src/diffpy/srfit/structure/__init__.py b/src/diffpy/srfit/structure/__init__.py index 2a34680e..d09aa5cc 100644 --- a/src/diffpy/srfit/structure/__init__.py +++ b/src/diffpy/srfit/structure/__init__.py @@ -12,9 +12,9 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Modules and classes that adapt structure representations to the ParameterSet -interface and automatic structure constraint generation from space group -information.""" +"""Modules and classes that adapt structure representations to the +ParameterSet interface and automatic structure constraint generation +from space group information.""" from diffpy.srfit.structure.sgconstraints import constrainAsSpaceGroup @@ -25,7 +25,7 @@ def struToParameterSet(name, stru): This returns a ParameterSet adapted for the structure depending on its type. - Attributes + Parameters ---------- stru a structure object known by this module diff --git a/src/diffpy/srfit/structure/bvsrestraint.py b/src/diffpy/srfit/structure/bvsrestraint.py index f8af1ebe..ec156449 100644 --- a/src/diffpy/srfit/structure/bvsrestraint.py +++ b/src/diffpy/srfit/structure/bvsrestraint.py @@ -47,7 +47,7 @@ class BVSRestraint(Restraint): def __init__(self, parset, sig=1, scaled=False): """Initialize the Restraint. - Attributes + Parameters ---------- parset SrRealParSet that creates this BVSRestraint. @@ -69,7 +69,7 @@ def __init__(self, parset, sig=1, scaled=False): def penalty(self, w=1.0): """Calculate the penalty of the restraint. - Attributes + Parameters ---------- w The point-average chi^2 which is optionally used to scale the diff --git a/src/diffpy/srfit/structure/cctbxparset.py b/src/diffpy/srfit/structure/cctbxparset.py index 2bb05c7d..544d1f3f 100644 --- a/src/diffpy/srfit/structure/cctbxparset.py +++ b/src/diffpy/srfit/structure/cctbxparset.py @@ -56,7 +56,7 @@ class CCTBXScattererParSet(ParameterSet): def __init__(self, name, strups, idx): """Initialize. - Attributes + Parameters ---------- name The name of this scatterer. @@ -153,7 +153,7 @@ class CCTBXUnitCellParSet(ParameterSet): def __init__(self, strups): """Initialize. - Attributes + Parameters ---------- strups The CCTBXCrystalParSet that contains the cctbx structure @@ -228,7 +228,7 @@ class CCTBXCrystalParSet(BaseStructureParSet): def __init__(self, name, stru): """Initialize. - Attributes + Parameters ---------- name A name for this @@ -237,7 +237,7 @@ def __init__(self, name, stru): """ ParameterSet.__init__(self, name) self.stru = stru - self.addParameterSet(CCTBXUnitCellParSet(self)) + self.add_parameter_set(CCTBXUnitCellParSet(self)) self.scatterers = [] self._update = False @@ -249,7 +249,7 @@ def __init__(self, name, stru): sname = "%s%i" % (el, i) cdict[el] = i + 1 scatterer = CCTBXScattererParSet(sname, self, i) - self.addParameterSet(scatterer) + self.add_parameter_set(scatterer) self.scatterers.append(scatterer) # Constrain the lattice diff --git a/src/diffpy/srfit/structure/diffpyparset.py b/src/diffpy/srfit/structure/diffpyparset.py index 6ed3beef..452dd6ed 100644 --- a/src/diffpy/srfit/structure/diffpyparset.py +++ b/src/diffpy/srfit/structure/diffpyparset.py @@ -91,7 +91,7 @@ class DiffpyAtomParSet(ParameterSet): def __init__(self, name, atom): """Initialize. - Attributes + Parameters ---------- atom A diffpy.structure.Atom instance @@ -205,7 +205,7 @@ class DiffpyLatticeParSet(ParameterSet): def __init__(self, lattice): """Initialize. - Attributes + Parameters ---------- lattice A diffpy.structure.Lattice instance @@ -275,7 +275,7 @@ class DiffpyStructureParSet(SrRealParSet): def __init__(self, name, stru): """Initialize. - Attributes + Parameters ---------- name A name for the structure @@ -284,7 +284,7 @@ def __init__(self, name, stru): """ SrRealParSet.__init__(self, name) self.stru = stru - self.addParameterSet(DiffpyLatticeParSet(stru.lattice)) + self.add_parameter_set(DiffpyLatticeParSet(stru.lattice)) self.atoms = [] cdict = {} @@ -297,7 +297,7 @@ def __init__(self, name, stru): aname = "%s%i" % (el, i) cdict[el] = i + 1 atom = DiffpyAtomParSet(aname, a) - self.addParameterSet(atom) + self.add_parameter_set(atom) self.atoms.append(atom) return diff --git a/src/diffpy/srfit/structure/objcrystparset.py b/src/diffpy/srfit/structure/objcrystparset.py index 19213b43..25cd50d8 100644 --- a/src/diffpy/srfit/structure/objcrystparset.py +++ b/src/diffpy/srfit/structure/objcrystparset.py @@ -12,7 +12,8 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Wrappers for adapting pyobjcryst.crystal.Crystal to a srfit ParameterSet. +"""Wrappers for adapting pyobjcryst.crystal.Crystal to a srfit +ParameterSet. This will adapt a Crystal or Molecule object from pyobjcryst into the ParameterSet interface. The following classes are adapted. @@ -55,6 +56,37 @@ ) from diffpy.srfit.fitbase.parameterset import ParameterSet from diffpy.srfit.structure.srrealparset import SrRealParSet +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +removal_version = "4.0.0" +bl_base = "diffpy.srfit.structure.objcrystparset.ObjCrystBondLengthParameter" + +bl_setConst_dep_msg = build_deprecation_message( + bl_base, + "setConst", + "set_constant", + removal_version, +) + +ba_base = "diffpy.srfit.structure.objcrystparset.ObjCrystBondAngleParameter" + +ba_setConst_dep_msg = build_deprecation_message( + ba_base, + "setConst", + "set_constant", + removal_version, +) + +da_base = ( + "diffpy.srfit.structure.objcrystparset.ObjCrystDihedralAngleParameter" +) + +da_setConst_dep_msg = build_deprecation_message( + da_base, + "setConst", + "set_constant", + removal_version, +) class ObjCrystScattererParSet(ParameterSet): @@ -82,7 +114,7 @@ class ObjCrystScattererParSet(ParameterSet): def __init__(self, name, scat, parent): """Initialize. - Attributes + Parameters ---------- name The name of the scatterer @@ -144,7 +176,7 @@ class ObjCrystAtomParSet(ObjCrystScattererParSet): def __init__(self, name, atom, parent): """Initialize. - Attributes + Parameters ---------- name The name of the scatterer @@ -220,7 +252,7 @@ class ObjCrystMoleculeParSet(ObjCrystScattererParSet): def __init__(self, name, molecule, parent=None): """Initialize. - Attributes + Parameters ---------- name The name of the scatterer @@ -252,7 +284,7 @@ def __init__(self, name, molecule, parent=None): atom = ObjCrystMolAtomParSet(name, a, self) atom.molecule = self - self.addParameterSet(atom) + self.add_parameter_set(atom) self.atoms.append(atom) anames.append(name) @@ -335,7 +367,8 @@ def wrapRestraints(self): return def wrapStretchModeParameters(self): - """Wrap the stretch modes implicit to the Molecule as Parameters. + """Wrap the stretch modes implicit to the Molecule as + Parameters. This will wrap StretchModeBondLengths and StretchModeBondAngles of the Molecule as Parameters. Note that this requires that the MolBondAtoms @@ -400,7 +433,7 @@ def restrainBondLength( This creates an instance of ObjCrystBondLengthRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the bond @@ -438,7 +471,7 @@ def restrainBondLengthParameter( This creates an instance of ObjCrystBondLengthRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- par A ObjCrystBondLengthParameter (see addBondLengthParameter) @@ -471,7 +504,7 @@ def restrainBondAngle( This creates an instance of ObjCrystBondAngleRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the bond angle @@ -512,7 +545,7 @@ def restrainBondAngleParameter( This creates an instance of ObjCrystBondAngleRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- par A ObjCrystBondAngleParameter (see addBondAngleParameter) @@ -545,7 +578,7 @@ def restrainDihedralAngle( This creates an instance of ObjCrystDihedralAngleRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the angle @@ -587,7 +620,7 @@ def restrainDihedralAngleParameter( This creates an instance of ObjCrystDihedralAngleRestraint and adds it to the ObjCrystMoleculeParSet. - Attributes + Parameters ---------- par A ObjCrystDihedralAngleParameter (see @@ -628,7 +661,7 @@ def addBondLengthParameter( This creates a ObjCrystBondLengthParameter to the ObjCrystMoleculeParSet that can be adjusted during the fit. - Attributes + Parameters ---------- name The name of the ObjCrystBondLengthParameter @@ -663,7 +696,7 @@ def addBondAngleParameter( This creates a ObjCrystBondAngleParameter to the ObjCrystMoleculeParSet that can be adjusted during the fit. - Attributes + Parameters ---------- name The name of the ObjCrystBondAngleParameter @@ -703,7 +736,7 @@ def addDihedralAngleParameter( This creates a ObjCrystDihedralAngleParameter to the ObjCrystMoleculeParSet that can be adjusted during the fit. - Attributes + Parameters ---------- name The name of the ObjCrystDihedralAngleParameter. @@ -776,7 +809,7 @@ class ObjCrystMolAtomParSet(ObjCrystScattererParSet): def __init__(self, name, scat, parent): """Initialize. - Attributes + Parameters ---------- name The name of the scatterer @@ -848,7 +881,7 @@ class ObjCrystMoleculeRestraint(object): def __init__(self, res, scaled=False): """Create a Restraint-like from a pyobjcryst Molecule restraint. - Attributes + Parameters ---------- res The pyobjcryst Molecule restraint. @@ -864,7 +897,7 @@ def __init__(self, res, scaled=False): def penalty(self, w=1.0): """Calculate the penalty of the restraint. - Attributes + Parameters ---------- w The point-average chi^2 which is optionally used to scale the @@ -905,7 +938,7 @@ class ObjCrystBondLengthRestraint(ObjCrystMoleculeRestraint): def __init__(self, atom1, atom2, length, sigma, delta, scaled=False): """Create a bond length restraint. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the bond @@ -977,7 +1010,7 @@ class ObjCrystBondAngleRestraint(ObjCrystMoleculeRestraint): def __init__(self, atom1, atom2, atom3, angle, sigma, delta, scaled=False): """Create a bond angle restraint. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the bond angle @@ -1059,7 +1092,7 @@ def __init__( ): """Create a dihedral angle restraint. - Attributes + Parameters ---------- atom1 First atom (ObjCrystMolAtomParSet) in the angle @@ -1135,7 +1168,7 @@ class StretchModeParameter(Parameter): def __init__(self, name, value=None, const=False): """Initialization. - Attributes + Parameters ---------- name The name of this Parameter (must be a valid attribute @@ -1151,7 +1184,7 @@ def __init__(self, name, value=None, const=False): Parameter.__init__(self, name, value, const) self.keepcenter = True - def setValue(self, val): + def set_value(self, val): """Change the value of the Parameter.""" curval = self.getValue() val = float(val) @@ -1164,7 +1197,7 @@ def setValue(self, val): self.mode.Stretch(delta, self.keepcenter) # Let Parameter take care of the general details - Parameter.setValue(self, val) + Parameter.set_value(self, val) return self @@ -1266,9 +1299,9 @@ class ObjCrystBondLengthParameter(StretchModeParameter): const A flag indicating whether this is considered a constant. _value - The value of the Parameter. Modified with 'setValue'. + The value of the Parameter. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' and 'set_value'. constraint A callable that calculates the value of this Parameter. If this is None (None), the the Parameter is responsible for its @@ -1281,7 +1314,7 @@ class ObjCrystBondLengthParameter(StretchModeParameter): def __init__(self, name, atom1, atom2, value=None, const=False, mode=None): """Create a ObjCrystBondLengthParameter. - Attributes + Parameters ---------- name The name of the ObjCrystBondLengthParameter @@ -1325,31 +1358,49 @@ def __init__(self, name, atom1, atom2, value=None, const=False, mode=None): if value is None: value = GetBondLength(atom1.scat, atom2.scat) StretchModeParameter.__init__(self, name, value, const) - self.setConst(const) + self.set_constant(const) return - def setConst(self, const=True, value=None): + def set_constant(self, is_constant=True, value=None): """Toggle the Parameter as constant. - This sets the underlying ObjCrystMolAtomParSet positions const as well. + This sets the underlying ObjCrystMolAtomParSet positions + constant as well. - Attributes + Parameters ---------- - const + is_constant Flag indicating if the Parameter is constant (default True). value An optional value for the Parameter (default None). If this is not None, then the Parameter will get a new value, constant or otherwise. + + Return + ------ + self + Returns self so that mutators can be chained. """ - StretchModeParameter.setConst(self, const, value) + StretchModeParameter.set_constant(self, is_constant, value) for a in [self.atom1, self.atom2]: - a.x.setConst(const) - a.y.setConst(const) - a.z.setConst(const) + a.x.set_constant(is_constant) + a.y.set_constant(is_constant) + a.z.set_constant(is_constant) + return self + + @deprecated(bl_setConst_dep_msg) + def setConst(self, const=True, value=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use + diffpy.srfit.structure.objcryst.ObjCrystBondLengthParameter.set_constant + instead. + """ + self.set_constant(const, value) return self def getValue(self): @@ -1361,7 +1412,7 @@ def getValue(self): """ if self._value is None: val = GetBondLength(self.atom1.scat, self.atom2.scat) - Parameter.setValue(self, val) + Parameter.set_value(self, val) return self._value @@ -1404,9 +1455,9 @@ class ObjCrystBondAngleParameter(StretchModeParameter): const A flag indicating whether this is considered a constant. _value - The value of the Parameter. Modified with 'setValue'. + The value of the Parameter. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' and 'set_value'. constraint A callable that calculates the value of this Parameter. If this is None (None), the the Parameter is responsible for its @@ -1421,7 +1472,7 @@ def __init__( ): """Create a ObjCrystBondAngleParameter. - Attributes + Parameters ---------- name The name of the ObjCrystBondAngleParameter. @@ -1470,30 +1521,48 @@ def __init__( if value is None: value = GetBondAngle(atom1.scat, atom2.scat, atom3.scat) StretchModeParameter.__init__(self, name, value, const) - self.setConst(const) + self.set_constant(const) return - def setConst(self, const=True, value=None): + def set_constant(self, is_constant=True, value=None): """Toggle the Parameter as constant. - This sets the underlying ObjCrystMolAtomParSet positions const as well. + This sets the underlying ObjCrystMolAtomParSet positions + constant as well. - Attributes + Parameters ---------- - const + is_constant Flag indicating if the Parameter is constant (default True). value An optional value for the Parameter (default None). If this is not None, then the Parameter will get a new value, constant or otherwise. + + Return + ------ + self + Returns self so that mutators can be chained. """ - StretchModeParameter.setConst(self, const, value) + StretchModeParameter.set_constant(self, is_constant, value) for a in [self.atom1, self.atom2, self.atom3]: - a.x.setConst(const) - a.y.setConst(const) - a.z.setConst(const) + a.x.set_constant(is_constant) + a.y.set_constant(is_constant) + a.z.set_constant(is_constant) + return self + + @deprecated(ba_setConst_dep_msg) + def setConst(self, const=True, value=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.structure.objcryst.ObjCrystBondAngleParameter.set_constant + instead. + """ + self.set_constant(const, value) return self def getValue(self): @@ -1507,7 +1576,7 @@ def getValue(self): val = GetBondAngle( self.atom1.scat, self.atom2.scat, self.atom3.scat ) - Parameter.setValue(self, val) + Parameter.set_value(self, val) return self._value @@ -1516,7 +1585,8 @@ def getValue(self): class ObjCrystDihedralAngleParameter(StretchModeParameter): - """Class for abstracting a dihedral angle in a Molecule to a Parameter. + """Class for abstracting a dihedral angle in a Molecule to a + Parameter. This wraps up a pyobjcryst.molecule.StretchModeTorsion object so that the angle defined by four MolAtoms ([a1-a2].[a3-a4]) in a Molecule can be used @@ -1554,9 +1624,9 @@ class ObjCrystDihedralAngleParameter(StretchModeParameter): const A flag indicating whether this is considered a constant. _value - The value of the Parameter. Modified with 'setValue'. + The value of the Parameter. Modified with 'set_value'. value - Property for 'getValue' and 'setValue'. + Property for 'getValue' and 'set_value'. constraint A callable that calculates the value of this Parameter. If this is None (None), the the Parameter is responsible for its @@ -1579,7 +1649,7 @@ def __init__( ): """Create a ObjCrystDihedralAngleParameter. - Attributes + Parameters ---------- name The name of the ObjCrystDihedralAngleParameter @@ -1633,30 +1703,47 @@ def __init__( atom1.scat, atom2.scat, atom3.scat, atom4.scat ) StretchModeParameter.__init__(self, name, value, const) - self.setConst(const) + self.set_constant(const) return - def setConst(self, const=True, value=None): + def set_constant(self, is_constant=True, value=None): """Toggle the Parameter as constant. This sets the underlying ObjCrystMolAtomParSet positions const as well. - Attributes + Parameters ---------- - const + is_constant Flag indicating if the Parameter is constant (default True). value An optional value for the Parameter (default None). If this is not None, then the Parameter will get a new value, constant or otherwise. + + Return + ------ + self + Returns self so that mutators can be chained. """ - StretchModeParameter.setConst(self, const, value) + StretchModeParameter.set_constant(self, is_constant, value) for a in [self.atom1, self.atom2, self.atom3, self.atom4]: - a.x.setConst(const) - a.y.setConst(const) - a.z.setConst(const) + a.x.set_constant(is_constant) + a.y.set_constant(is_constant) + a.z.set_constant(is_constant) + return self + + @deprecated(da_setConst_dep_msg) + def setConst(self, const=True, value=None): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.structure.objcryst.ObjCrystDihedralAngleParameter.set_constant + instead. + """ + self.set_constant(const, value) return self def getValue(self): @@ -1673,7 +1760,7 @@ def getValue(self): self.atom3.scat, self.atom4.scat, ) - Parameter.setValue(self, val) + Parameter.set_value(self, val) return self._value @@ -1719,7 +1806,7 @@ class ObjCrystCrystalParSet(SrRealParSet): def __init__(self, name, cryst): """Initialize. - Attributes + Parameters ---------- name A name for this ParameterSet @@ -1760,7 +1847,7 @@ def __init__(self, name, cryst): else: raise TypeError("Unrecognized scatterer '%s'" % cname) - self.addParameterSet(parset) + self.add_parameter_set(parset) self.scatterers.append(parset) snames.append(name) @@ -1794,7 +1881,7 @@ def _constrain_space_group(self): def _create_space_group(sgobjcryst): """Create a diffpy.structure SpaceGroup object from pyobjcryst. - Attributes + Parameters ---------- sgobjcryst A pyobjcryst.spacegroup.SpaceGroup instance. diff --git a/src/diffpy/srfit/structure/sgconstraints.py b/src/diffpy/srfit/structure/sgconstraints.py index c9dbc5b9..893c90a0 100644 --- a/src/diffpy/srfit/structure/sgconstraints.py +++ b/src/diffpy/srfit/structure/sgconstraints.py @@ -14,7 +14,6 @@ ############################################################################## """Code to set space group constraints for a crystal structure.""" - import re import numpy @@ -190,7 +189,7 @@ def __init__(self, name="sgpars"): def addParameter(self, par, check=True): """Store a Parameter. - Attributes + Parameters ---------- par The Parameter to be stored. @@ -390,9 +389,9 @@ def _clear_constraints(self): for scatterer in scatterers: for par in [scatterer.x, scatterer.y, scatterer.z]: - if scatterer.isConstrained(par): - scatterer.unconstrain(par) - par.setConst(False) + if scatterer.is_constrained(par): + scatterer.remove_constraint(par) + par.set_constant(False) # Clear the lattice if self.constrainlat: @@ -407,9 +406,9 @@ def _clear_constraints(self): lattice.gamma, ] for par in latpars: - if lattice.isConstrained(par): - lattice.unconstrain(par) - par.setConst(False) + if lattice.is_constrained(par): + lattice.remove_constraint(par) + par.set_constant(False) # Clear ADPs if self.constrainadps: @@ -417,16 +416,16 @@ def _clear_constraints(self): if isosymbol: par = scatterer.get(isosymbol) if par is not None: - if scatterer.isConstrained(par): - scatterer.unconstrain(par) - par.setConst(False) + if scatterer.is_constrained(par): + scatterer.remove_constraint(par) + par.set_constant(False) for pname in adpsymbols: par = scatterer.get(pname) if par is not None: - if scatterer.isConstrained(par): - scatterer.unconstrain(par) - par.setConst(False) + if scatterer.is_constrained(par): + scatterer.remove_constraint(par) + par.set_constant(False) return @@ -470,7 +469,7 @@ def _constrain_lattice(self): def _constrain_xyzs(self, positions): """Constrain the positions. - Attributes + Parameters ---------- positions The coordinates of the scatterers. @@ -513,7 +512,7 @@ def _constrain_xyzs(self, positions): def _constrain_adps(self, positions): """Constrain the ADPs. - Attributes + Parameters ---------- positions The coordinates of the scatterers. @@ -596,7 +595,9 @@ def _constrain_adps(self, positions): continue isoidx.append(j) scatterer = scatterers[j] - scatterer.constrain(isosymbol, isoname, ns=self._parameters) + scatterer.add_constraint( + isosymbol, isoname, params=self._parameters + ) fadp = g.UFormulas(adpnames) @@ -616,7 +617,7 @@ def _constrain_adps(self, positions): def __add_par(self, parname, par): """Constrain a parameter via proxy with a specified name. - Attributes + Parameters ---------- par Parameter to constrain @@ -650,14 +651,14 @@ def _constrain_monoclinic(lattice): if lattice.angunits == "rad": afactor = deg2rad ang90 = 90.0 * afactor - lattice.alpha.setConst(True, ang90) + lattice.alpha.set_constant(True, ang90) beta = lattice.beta.getValue() gamma = lattice.gamma.getValue() if ang90 != beta and ang90 == gamma: - lattice.gamma.setConst(True, ang90) + lattice.gamma.set_constant(True, ang90) else: - lattice.beta.setConst(True, ang90) + lattice.beta.set_constant(True, ang90) return @@ -670,9 +671,9 @@ def _constrain_orthorhombic(lattice): if lattice.angunits == "rad": afactor = deg2rad ang90 = 90.0 * afactor - lattice.alpha.setConst(True, ang90) - lattice.beta.setConst(True, ang90) - lattice.gamma.setConst(True, ang90) + lattice.alpha.set_constant(True, ang90) + lattice.beta.set_constant(True, ang90) + lattice.gamma.set_constant(True, ang90) return @@ -686,10 +687,10 @@ def _constrain_tetragonal(lattice): if lattice.angunits == "rad": afactor = deg2rad ang90 = 90.0 * afactor - lattice.alpha.setConst(True, ang90) - lattice.beta.setConst(True, ang90) - lattice.gamma.setConst(True, ang90) - lattice.constrain(lattice.b, lattice.a) + lattice.alpha.set_constant(True, ang90) + lattice.beta.set_constant(True, ang90) + lattice.gamma.set_constant(True, ang90) + lattice.add_constraint(lattice.b, lattice.a) return @@ -706,15 +707,15 @@ def _constrain_trigonal(lattice): ang90 = 90.0 * afactor ang120 = 120.0 * afactor if lattice.gamma.getValue() == ang120: - lattice.constrain(lattice.b, lattice.a) - lattice.alpha.setConst(True, ang90) - lattice.beta.setConst(True, ang90) - lattice.gamma.setConst(True, ang120) + lattice.add_constraint(lattice.b, lattice.a) + lattice.alpha.set_constant(True, ang90) + lattice.beta.set_constant(True, ang90) + lattice.gamma.set_constant(True, ang120) else: - lattice.constrain(lattice.b, lattice.a) - lattice.constrain(lattice.c, lattice.a) - lattice.constrain(lattice.beta, lattice.alpha) - lattice.constrain(lattice.gamma, lattice.alpha) + lattice.add_constraint(lattice.b, lattice.a) + lattice.add_constraint(lattice.c, lattice.a) + lattice.add_constraint(lattice.beta, lattice.alpha) + lattice.add_constraint(lattice.gamma, lattice.alpha) return @@ -729,10 +730,10 @@ def _constrain_hexagonal(lattice): afactor = deg2rad ang90 = 90.0 * afactor ang120 = 120.0 * afactor - lattice.constrain(lattice.b, lattice.a) - lattice.alpha.setConst(True, ang90) - lattice.beta.setConst(True, ang90) - lattice.gamma.setConst(True, ang120) + lattice.add_constraint(lattice.b, lattice.a) + lattice.alpha.set_constant(True, ang90) + lattice.beta.set_constant(True, ang90) + lattice.gamma.set_constant(True, ang120) return @@ -746,11 +747,11 @@ def _constrain_cubic(lattice): if lattice.angunits == "rad": afactor = deg2rad ang90 = 90.0 * afactor - lattice.constrain(lattice.b, lattice.a) - lattice.constrain(lattice.c, lattice.a) - lattice.alpha.setConst(True, ang90) - lattice.beta.setConst(True, ang90) - lattice.gamma.setConst(True, ang90) + lattice.add_constraint(lattice.b, lattice.a) + lattice.add_constraint(lattice.c, lattice.a) + lattice.alpha.set_constant(True, ang90) + lattice.beta.set_constant(True, ang90) + lattice.gamma.set_constant(True, ang90) return @@ -770,7 +771,7 @@ def _constrain_cubic(lattice): def _makeconstraint(parname, formula, scatterer, idx, ns={}): """Constrain a parameter according to a formula. - Attributes + Parameters ---------- parname Name of parameter @@ -803,18 +804,19 @@ def _makeconstraint(parname, formula, scatterer, idx, ns={}): # Check to see if it is a constant fval = _get_float(formula) if fval is not None: - par.setConst() + par.set_constant() return # If we got here, then we have a constraint equation # Fix any division issues formula = formula.replace("/", "*1.0/") - scatterer.constrain(par, formula, ns=ns) + scatterer.add_constraint(par, formula, params=ns) return def _get_float(formula): - """Get a float from a formula string, or None if this is not possible.""" + """Get a float from a formula string, or None if this is not + possible.""" try: return eval(formula) except NameError: diff --git a/src/diffpy/srfit/structure/srrealparset.py b/src/diffpy/srfit/structure/srrealparset.py index 5a5e5028..4ed31ffc 100644 --- a/src/diffpy/srfit/structure/srrealparset.py +++ b/src/diffpy/srfit/structure/srrealparset.py @@ -52,7 +52,7 @@ def restrainBVS(self, sig=1, scaled=False): this is also scaled by the current point-averaged chi^2 value so the restraint is roughly equally weighted in the fit. - Attributes + Parameters ---------- sig The uncertainty on the BVS (default 1). diff --git a/src/diffpy/srfit/util/__init__.py b/src/diffpy/srfit/util/__init__.py index 023b4256..c8c9b830 100644 --- a/src/diffpy/srfit/util/__init__.py +++ b/src/diffpy/srfit/util/__init__.py @@ -18,8 +18,8 @@ def sortKeyForNumericString(s): - """Compute key for sorting strings according to their integer numeric - value. + """Compute key for sorting strings according to their integer + numeric value. Each string gets split to string and integer segments to create keys for comparison. Signs, decimal points and exponents are ignored. diff --git a/src/diffpy/srfit/util/argbinders.py b/src/diffpy/srfit/util/argbinders.py index 28cd5144..71db89b4 100644 --- a/src/diffpy/srfit/util/argbinders.py +++ b/src/diffpy/srfit/util/argbinders.py @@ -16,7 +16,8 @@ class bind2nd(object): - """Freeze second argument of a callable object to a given constant.""" + """Freeze second argument of a callable object to a given + constant.""" def __init__(self, func, arg1): """Freeze the second argument of function func to arg1.""" diff --git a/src/diffpy/srfit/util/inpututils.py b/src/diffpy/srfit/util/inpututils.py index b5dfa2ac..1e0d83fa 100644 --- a/src/diffpy/srfit/util/inpututils.py +++ b/src/diffpy/srfit/util/inpututils.py @@ -17,6 +17,7 @@ __all__ = ["inputToString"] import os.path +from pathlib import Path def inputToString(input): @@ -25,7 +26,7 @@ def inputToString(input): This is useful when you want a method to accept a string, open file object or file name. - Attributes + Parameters ---------- input An open file-like object, name of a file @@ -51,4 +52,44 @@ def inputToString(input): return inptstr +def get_dict_from_results_file( + results_filepath: Path | str, +) -> dict[str, float]: + """Get a dictionary of parameter names and values from a results + file. + + The file should have lines in the format: + "parameter_name value +/- uncertainty". Lines that do not match this + format will be ignored. + + Parameters + ---------- + results_filepath : pathlib.Path or str + The path to the results file. + + Returns + ------- + parsed_results_dict : dict + The dictionary where keys are parameter names and values are the + corresponding parameter values as floats. + """ + with open(results_filepath, "r") as f: + results_string = f.read() + parsed_results_dict = {} + for raw_line in results_string.splitlines(): + line = raw_line.strip() + # skip blank lines and lines that are just dashes + if not line or set(line) == {"-"}: + continue + line_items = line.split() + if len(line_items) < 2: + continue + if len(line_items) >= 4 and line_items[2] == "+/-": + try: + parsed_results_dict[line_items[0]] = float(line_items[1]) + except ValueError: + pass + return parsed_results_dict + + # End of file diff --git a/src/diffpy/srfit/util/tagmanager.py b/src/diffpy/srfit/util/tagmanager.py index 9e981d64..37c58a3d 100644 --- a/src/diffpy/srfit/util/tagmanager.py +++ b/src/diffpy/srfit/util/tagmanager.py @@ -53,7 +53,7 @@ def tag(self, obj, *tags): Tags are stored as strings. - Attributes + Parameters ---------- obj Any hashable object to be untagged. @@ -71,7 +71,7 @@ def tag(self, obj, *tags): def untag(self, obj, *tags): """Remove tags from an object. - Attributes + Parameters ---------- obj Any hashable object to be untagged. diff --git a/src/diffpy/srfit/util/weakrefcallable.py b/src/diffpy/srfit/util/weakrefcallable.py index 287d2e47..1cbb87aa 100644 --- a/src/diffpy/srfit/util/weakrefcallable.py +++ b/src/diffpy/srfit/util/weakrefcallable.py @@ -14,7 +14,6 @@ ############################################################################## """Picklable storage of callable objects using weak references.""" - import types import weakref @@ -69,7 +68,8 @@ def __init__(self, f, fallback=None): return def __call__(self, *args, **kwargs): - """Call the wrapped method if the weak-referenced object is alive. + """Call the wrapped method if the weak-referenced object is + alive. If that object does not exist and the fallback function is defined, call the fallback function instead. @@ -122,8 +122,9 @@ def __getstate__(self): return state def __setstate__(self, state): - """Restore the weak reference in this wrapper upon unpickling.""" - (self._class, nm, self.fallback, mobj) = state + """Restore the weak reference in this wrapper upon + unpickling.""" + self._class, nm, self.fallback, mobj = state self.function = getattr(self._class, nm) if mobj is None: # use a fake weak reference that mimics deallocated object. diff --git a/src/diffpy/srfit/version.py b/src/diffpy/srfit/version.py index f0833049..c62be11e 100644 --- a/src/diffpy/srfit/version.py +++ b/src/diffpy/srfit/version.py @@ -3,9 +3,10 @@ # # (c) 2008-2025 The Trustees of Columbia University in the City of New York. # All rights reserved. +# (c) 2026-present The DiffPy Team. All rights reserved. # -# File coded by: Christopher Farrow, Pavol Juhas, and members of the -# Billinge Group. +# File coded by: Christopher Farrow, Pavol Juhas, Caden Myers, +# Simon J. L. Billinge, and members of the DiffPy community. # # See GitHub contributions for a more detailed list of contributors. # https://github.com/diffpy/diffpy.srfit/graphs/contributors @@ -19,8 +20,9 @@ # __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"] # obtain version information -from importlib.metadata import version +from importlib.metadata import PackageNotFoundError, version -__version__ = version("diffpy.srfit") - -# End of file +try: + __version__ = version("diffpy.srfit") +except PackageNotFoundError: + __version__ = "unknown" diff --git a/tests/conftest.py b/tests/conftest.py index f251746f..23b92f6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,7 +90,8 @@ def pyobjcryst_available(): @pytest.fixture(scope="session") def datafile(): - """Fixture to load a test data file from the testdata package directory.""" + """Fixture to load a test data file from the testdata package + directory.""" def _datafile(filename): return importlib.resources.files("tests.testdata").joinpath(filename) @@ -146,50 +147,79 @@ def _capturestdout(f, *args, **kwargs): return _capturestdout -@pytest.fixture(scope="session") -def build_recipe_one_contribution(): - "helper to build a simple recipe" - profile = Profile() - x = linspace(0, pi, 10) +@pytest.fixture(scope="function") +def build_recipes_one_contribution(): + """Helper to build a simple recipe. + + Two identical recipes are returned if needed. + """ + x = linspace(0, pi, 11) y = sin(x) - profile.setObservedProfile(x, y) - contribution = FitContribution("c1") - contribution.set_profile(profile) - contribution.setEquation("A*sin(k*x + c)") - recipe = FitRecipe() - recipe.addContribution(contribution) - recipe.addVar(contribution.A, 1) - recipe.addVar(contribution.k, 1) - recipe.addVar(contribution.c, 1) - return recipe + profile1 = Profile() + profile1.set_observed_profile(x, y) + contribution1 = FitContribution("c1") + contribution1.set_profile(profile1) + contribution1.set_equation("amplitude*sin(wave_number*x + phase_shift)") + recipe1 = FitRecipe() + recipe1.add_contribution(contribution1) + recipe1.add_variable(contribution1.amplitude, 4) + recipe1.add_variable(contribution1.wave_number, 3) + recipe1.add_variable(contribution1.phase_shift, 2) -@pytest.fixture(scope="session") + profile2 = Profile() + profile2.set_observed_profile(x, y) + contribution2 = FitContribution("c2") + contribution2.set_profile(profile2) + contribution2.set_equation("amplitude*sin(wave_number*x + phase_shift)") + recipe2 = FitRecipe() + recipe2.add_contribution(contribution2) + recipe2.add_variable(contribution2.amplitude, 4) + recipe2.add_variable(contribution2.wave_number, 3) + recipe2.add_variable(contribution2.phase_shift, 2) + return recipe1, recipe2 + + +@pytest.fixture(scope="function") def build_recipe_two_contributions(): - "helper to build a recipe with two contributions" + """Helper to build a recipe with two physically related + contributions.""" profile1 = Profile() - x = linspace(0, pi, 10) - y1 = sin(x) - profile1.setObservedProfile(x, y1) + x = linspace(0, pi, 51) + y1 = sin(x) # amplitude=1, freq=1 + profile1.set_observed_profile(x, y1) + contribution1 = FitContribution("c1") contribution1.set_profile(profile1) - contribution1.setEquation("A*sin(k*x + c)") + contribution1.set_equation("A*sin(k*x + c)") profile2 = Profile() - y2 = 0.5 * sin(2 * x) - profile2.setObservedProfile(x, y2) + y2 = 0.5 * sin(2 * x) # amplitude=0.5, freq=2 + profile2.set_observed_profile(x, y2) + contribution2 = FitContribution("c2") contribution2.set_profile(profile2) - contribution2.setEquation("B*sin(m*x + d)") + contribution2.set_equation("B*sin(m*x + d)") + recipe = FitRecipe() - recipe.addContribution(contribution1) - recipe.addContribution(contribution2) - recipe.addVar(contribution1.A, 1) - recipe.addVar(contribution1.k, 1) - recipe.addVar(contribution1.c, 1) - recipe.addVar(contribution2.B, 0.5) - recipe.addVar(contribution2.m, 2) - recipe.addVar(contribution2.d, 0) + recipe.add_contribution(contribution1) + recipe.add_contribution(contribution2) + + # Add variables with reasonable initial guesses + recipe.add_variable(contribution1.A, 0.8) + recipe.add_variable(contribution1.k, 1.0) + recipe.add_variable(contribution1.c, 0.1) + + recipe.add_variable(contribution2.B, 0.4) + recipe.add_variable(contribution2.m, 2.0) + recipe.add_variable(contribution2.d, 0.1) + + # ---- Meaningful constraints ---- + recipe.add_constraint(contribution2.m, "2*k") + recipe.add_constraint(contribution2.d, contribution1.c) + recipe.add_constraint(contribution2.B, "0.5*A") + recipe.add_soft_bounds(contribution1.A, 0.5, 1.5) + recipe.add_soft_bounds(contribution1.k, 0.8, 1.2) return recipe @@ -210,4 +240,124 @@ def temp_data_files(tmp_path): cgr_file = tmp_path / "cgr_file.cgr" cgr_file.write_text("1.0 2.0\n" "1.1 2.1\n" "1.2 2.2\n") + + results_file = tmp_path / "fit_results.res" + results_file.write_text(""" +Results written: Wed Feb 25 15:14:58 2026 +produced by cadenmyers + +Some quantities invalid due to missing profile uncertainty +Overall (Chi2 and Reduced Chi2 invalid) +------------------------------------------------------------------------------ +Residual 0.00000000 +Contributions 0.00000000 +Restraints 0.00000000 +Chi2 0.00000000 +Reduced Chi2 0.00000000 +Rw 0.00000000 + +Variables (Uncertainties invalid) +------------------------------------------------------------------------------ +amplitude 1.00000000e+00 +/- 4.82804000e-01 +phase_shift -1.61291146e-18 +/- 1.00000000e+00 +wave_number 1.00000000e+00 +/- 2.17496687e-01 + +Variable Correlations greater than 25% (Correlations invalid) +------------------------------------------------------------------------------ +No correlations greater than 25% +""") + yield tmp_path + + +@pytest.fixture +def parser_datafiles(tmp_path): + """Create temporary data files with different column layouts and + yield the directory.""" + + METADATA_HEADER = r"""# xPDFsuite Configuration # +[PDF] +wavelength = 0.1 +dataformat = QA +inputfile = input.iq +backgroundfile = backgroundfile.iq +mode = xray +bgscale = 1.0 +composition = TiSe2 +outputtype = gr +qmaxinst = 25.0 +qmin = 0.1 +qmax = 25.0 +rmax = 140.0 +rmin = 0.0 +rstep = 0.01 +rpoly = 0.7 + +[Misc] +inputdir = /my/data/dir +savedir = /my/save/dir +backgroundfilefull = /my/data/dir/backgroundfile.iq + +#### start data +#S 1 +""" + + # Four-column standard + (tmp_path / "four_col.gr").write_text( + METADATA_HEADER + + r"""#L r($\AA$) G($\AA^{-2}$) dr($\AA$) dG($\AA^{-2}$) +1.0 2.0 0.1 0.2 +1.1 2.1 0.3 0.4 +1.2 2.2 0.5 0.6""" + ) + + # Three-column (x, y, dy) + (tmp_path / "three_col.dat").write_text( + METADATA_HEADER + r"""#L r($\AA$) G($\AA^{-2}$) dG($\AA^{-2}$) +1.0 2.0 0.2 +1.1 2.1 0.4 +1.2 2.2 0.6""" + ) + + # Two-column (x, y) + (tmp_path / "two_col.txt").write_text( + METADATA_HEADER + r"""#L r($\AA$) G($\AA^{-2}$) +1.0 2.0 +1.1 2.1 +1.2 2.2""" + ) + + # Four-column reordered (x, dx, y, dy) + (tmp_path / "four_col_reordered.txt").write_text( + METADATA_HEADER + + r"""#L r($\AA$) dr($\AA$) G($\AA^{-2}$) dG($\AA^{-2}$) +1.0 0.1 2.0 0.2 +1.1 0.3 2.1 0.4 +1.2 0.5 2.2 0.6""" + ) + + # Four-column with NaN/Inf + (tmp_path / "four_col_nan_inf.gr").write_text( + METADATA_HEADER + + r"""#L r($\AA$) G($\AA^{-2}$) dr($\AA$) dG($\AA^{-2}$) +1.0 2.0 nan inf +1.1 2.1 inf 1 +1.2 2.2 nan nan""" + ) + + # One-column + (tmp_path / "one_col.gr").write_text(METADATA_HEADER + r"""#L r($\AA$) +1.0 +1.1 +1.2""") + + # Five-column (extra column) + (tmp_path / "five_col.gr").write_text( + METADATA_HEADER + + r"""#L r($\AA$) G($\AA^{-2}$) dr($\AA$) dG($\AA^{-2}$) extra +1.0 2.0 0.1 0.2 9.9 +1.1 2.1 0.3 0.4 9.8 +1.2 2.2 0.5 0.6 9.7""" + ) + + # Yield the directory yield tmp_path diff --git a/tests/debug.py b/tests/debug.py index 8ebfc4ad..64d5c83f 100644 --- a/tests/debug.py +++ b/tests/debug.py @@ -19,7 +19,6 @@ Exceptions raised by failed tests or other errors are not caught. """ - if __name__ == "__main__": import sys diff --git a/tests/run.py b/tests/run.py index a375045e..9ca3054a 100644 --- a/tests/run.py +++ b/tests/run.py @@ -17,7 +17,6 @@ python -m diffpy.srfit.tests.run """ - if __name__ == "__main__": import sys diff --git a/tests/test_builder.py b/tests/test_builder.py index 4045dd6d..cf1c3681 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -97,7 +97,7 @@ def g2(v1): factory.registerArgument("v2", v2) factory.registerArgument("v3", v3) factory.registerArgument("v4", v4) - b = factory.registerFunction("g", g1, ["v1", "v2", "v3", "v4"]) + b = factory.register_function("g", g1, ["v1", "v2", "v3", "v4"]) # Now associate args with the wrapped function op = b.literal @@ -121,7 +121,7 @@ def g2(v1): assert round(abs(24 - eq1()), 7) == 0 # Now swap out the function - b = factory.registerFunction("g", g2, ["v1"]) + b = factory.register_function("g", g2, ["v1"]) op = b.literal assert op.operation == g2 assert v1 in op.args @@ -153,10 +153,10 @@ def testParseEquation(noObserversInGlobalBuilders): x = numpy.pi B = 4.0 C = 2.0 - eq.A.setValue(A) - eq.x.setValue(x) - eq.B.setValue(B) - eq.C.setValue(C) + eq.A.set_value(A) + eq.x.set_value(x) + eq.B.set_value(B) + eq.C.set_value(C) assert numpy.array_equal(eq(), f_equation(A, x, B, C)) # Make sure that the arguments of eq are listed in the order in which @@ -167,8 +167,8 @@ def testParseEquation(noObserversInGlobalBuilders): eq = factory.makeEquation("sqrt(e**(-0.5*(x/sigma)**2))") x = numpy.arange(0, 1, 0.05) sigma = 0.1 - eq.x.setValue(x) - eq.sigma.setValue(sigma) + eq.x.set_value(x) + eq.sigma.set_value(sigma) assert numpy.allclose(eq(), gaussian_test(x, sigma)) assert eq.args == [eq.x, eq.sigma] @@ -181,7 +181,7 @@ def testParseEquation(noObserversInGlobalBuilders): assert eq.args == [eq.sigma] # Equation with user-defined functions - factory.registerFunction("myfunc", eq, ["sigma"]) + factory.register_function("myfunc", eq, ["sigma"]) eq2 = factory.makeEquation("c*myfunc(sigma)") assert numpy.allclose(eq2(c=2, sigma=sigma), 2 * gaussian_test(x, sigma)) assert "sigma" in eq2.argdict @@ -214,7 +214,7 @@ def testBuildEquation(noObserversInGlobalBuilders): x = numpy.arange(0, numpy.pi, 0.1) beq = A * sin(a * x) - eq = beq.getEquation() + eq = beq.get_equation() assert "a" in eq.argdict assert "A" in eq.argdict @@ -235,7 +235,7 @@ def _f(a, b): b = builder.ArgumentBuilder(name="b", value=1) beq = sin(f(a, b)) - eq = beq.getEquation() + eq = beq.get_equation() assert eq() == numpy.sin(_f(2, 1)) # complex function @@ -245,23 +245,23 @@ def _f(a, b): x = builder.ArgumentBuilder(name="x", value=_x, const=True) sigma = builder.ArgumentBuilder(name="sigma", value=0.1) beq = sqrt(e ** (-0.5 * (x / sigma) ** 2)) - eq = beq.getEquation() + eq = beq.get_equation() assert numpy.allclose(eq(), numpy.sqrt(numpy.exp(-0.5 * (_x / 0.1) ** 2))) # Equation with Equation A = builder.ArgumentBuilder(name="A", value=2) B = builder.ArgumentBuilder(name="B", value=4) beq = A + B - eq = beq.getEquation() + eq = beq.get_equation() E = builder.wrapOperator("eq", eq) - eq2 = (2 * E).getEquation() + eq2 = (2 * E).get_equation() # Make sure these evaluate to the same thing assert eq.args == [A.literal, B.literal] assert 2 * eq() == eq2() # Pass new arguments to the equation C = builder.ArgumentBuilder(name="C", value=5) D = builder.ArgumentBuilder(name="D", value=6) - eq3 = (E(C, D) + 1).getEquation() + eq3 = (E(C, D) + 1).get_equation() assert 12 == eq3() # Pass old and new arguments to the equation # If things work right, A has been given the value of C in the last diff --git a/tests/test_constraint.py b/tests/test_constraint.py index 5c41689f..f173de14 100644 --- a/tests/test_constraint.py +++ b/tests/test_constraint.py @@ -19,11 +19,50 @@ from diffpy.srfit.equation.builder import EquationFactory from diffpy.srfit.fitbase.constraint import Constraint from diffpy.srfit.fitbase.parameter import Parameter -from diffpy.srfit.fitbase.recipeorganizer import equationFromString +from diffpy.srfit.fitbase.recipeorganizer import get_equation_from_string class TestConstraint(unittest.TestCase): + def test_constrain_parameter(self): + """Test the Constraint class.""" + + p1 = Parameter("p1", 1) + p2 = Parameter("p2", 2) + + factory = EquationFactory() + + factory.registerArgument("p1", p1) + factory.registerArgument("p2", p2) + + c = Constraint() + # Constrain p1 = 2*p2 + eq = get_equation_from_string("2*p2", factory) + c.add_constraint(p1, eq) + + self.assertTrue(p1.constrained) + self.assertFalse(p2.constrained) + + eq2 = get_equation_from_string("2*p2+1", factory) + c2 = Constraint() + self.assertRaises(ValueError, c2.constrain, p1, eq2) + p2.set_constant() + eq3 = get_equation_from_string("p1", factory) + self.assertRaises(ValueError, c2.constrain, p2, eq3) + + p2.set_value(2.5) + c.update() + self.assertEqual(5.0, p1.getValue()) + + p2.set_value(8.1) + self.assertEqual(5.0, p1.getValue()) + c.update() + self.assertEqual(16.2, p1.getValue()) + return + + +class TestConstraint_deprecated(unittest.TestCase): + def testConstraint(self): """Test the Constraint class.""" @@ -37,24 +76,24 @@ def testConstraint(self): c = Constraint() # Constrain p1 = 2*p2 - eq = equationFromString("2*p2", factory) + eq = get_equation_from_string("2*p2", factory) c.constrain(p1, eq) self.assertTrue(p1.constrained) self.assertFalse(p2.constrained) - eq2 = equationFromString("2*p2+1", factory) + eq2 = get_equation_from_string("2*p2+1", factory) c2 = Constraint() self.assertRaises(ValueError, c2.constrain, p1, eq2) p2.setConst() - eq3 = equationFromString("p1", factory) + eq3 = get_equation_from_string("p1", factory) self.assertRaises(ValueError, c2.constrain, p2, eq3) - p2.setValue(2.5) + p2.set_value(2.5) c.update() self.assertEqual(5.0, p1.getValue()) - p2.setValue(8.1) + p2.set_value(8.1) self.assertEqual(5.0, p1.getValue()) c.update() self.assertEqual(16.2, p1.getValue()) diff --git a/tests/test_contribution.py b/tests/test_contribution.py index 1332de33..56975905 100644 --- a/tests/test_contribution.py +++ b/tests/test_contribution.py @@ -13,6 +13,7 @@ # ############################################################################## """Tests for refinableobj module.""" + import unittest import numpy as np @@ -50,7 +51,7 @@ def testset_profile(self): self.assertRaises(TypeError, fc1.setProfile, "invalid") # check if residual equation is set up when possible fc2 = FitContribution("test2") - fc2.setEquation("A * x") + fc2.set_equation("A * x") fc2.set_profile(profile) self.assertFalse(fc2._reseq is None) return @@ -67,15 +68,28 @@ def testAddProfileGenerator(self): self.assertTrue(fc._eq is not None) return + def test_add_profile_generator(self): + fc = self.fitcontribution + gen = self.gen + fc.add_profile_generator(gen, "gen") + + xobs = arange(0, 10, 0.5) + self.assertTrue(array_equal(xobs, gen(xobs))) + + self.assertTrue(gen.profile is None) + self.assertTrue(fc._eq is not None) + return + def testInteraction(self): - """Test the interaction between the profile and profile generator.""" + """Test the interaction between the profile and profile + generator.""" fc = self.fitcontribution profile = self.profile gen = self.gen # Add the calculator and profile fc.set_profile(profile) - fc.addProfileGenerator(gen, "I") + fc.add_profile_generator(gen, "I") # Check attributes are created self.assertTrue(fc.profile is profile) @@ -89,7 +103,7 @@ def testInteraction(self): # create some data xobs = arange(0, 10, 0.5) yobs = xobs - profile.setObservedProfile(xobs, yobs) + profile.set_observed_profile(xobs, yobs) # Make sure this is where it's supposed to be self.assertTrue(gen.profile.xobs is xobs) @@ -111,16 +125,16 @@ def testReplacements(self): xobs = arange(0, 10, 0.5) yobs = xobs profile = self.profile - profile.setObservedProfile(xobs, yobs) + profile.set_observed_profile(xobs, yobs) xobs2 = arange(0, 10, 0.8) yobs2 = 0.5 * xobs2 profile2 = Profile() - profile2.setObservedProfile(xobs2, yobs2) + profile2.set_observed_profile(xobs2, yobs2) gen = self.gen # Validate equations fc.set_profile(profile) - fc.addProfileGenerator(gen, "I") + fc.add_profile_generator(gen, "I") self.assertTrue(array_equal(gen.value, xobs)) self.assertTrue(array_equal(fc._eq(), xobs)) self.assertAlmostEqual(0, sum(fc._reseq())) @@ -143,40 +157,53 @@ def testReplacements(self): self.assertEqual(len(xobs2), len(fc.residual())) return + def test_get_residual_equation(self): + """Check getting the current formula for residual equation.""" + fc = self.fitcontribution + self.assertEqual("", fc.get_residual_equation()) + fc.set_profile(self.profile) + fc.set_equation("A * x + B") + self.assertEqual("((eq - y) / dy)", fc.get_residual_equation()) + fc.set_residual_equation("2 * (eq - y)") + self.assertEqual("(2 * (eq - y))", fc.get_residual_equation()) + return + def test_getResidualEquation(self): """Check getting the current formula for residual equation.""" fc = self.fitcontribution - self.assertEqual("", fc.getResidualEquation()) + self.assertEqual("", fc.get_residual_equation()) fc.set_profile(self.profile) - fc.setEquation("A * x + B") + fc.set_equation("A * x + B") self.assertEqual("((eq - y) / dy)", fc.getResidualEquation()) fc.setResidualEquation("2 * (eq - y)") - self.assertEqual("(2 * (eq - y))", fc.getResidualEquation()) + self.assertEqual("(2 * (eq - y))", fc.get_residual_equation()) return def test_releaseOldEquations(self): - """Ensure EquationFactory does not hold to obsolete Equations.""" + """Ensure EquationFactory does not hold to obsolete + Equations.""" fc = self.fitcontribution self.assertEqual(0, len(fc._eqfactory.equations)) for i in range(5): - fc.setEquation("A * x + B") + fc.set_equation("A * x + B") self.assertEqual(1, len(fc._eqfactory.equations)) fc.set_profile(self.profile) for i in range(5): - fc.setResidualEquation("chiv") + fc.set_residual_equation("chiv") self.assertEqual(2, len(fc._eqfactory.equations)) return - def test_registerFunction(self): - """Ensure registered function works after second setEquation call.""" + def test_register_function(self): + """Ensure registered function works after second set_equation + call.""" fc = self.fitcontribution - fc.registerFunction(_fsquare, name="fsquare") - fc.setEquation("fsquare") - fc.x.setValue(5) + fc.register_function(_fsquare, name="fsquare") + fc.set_equation("fsquare") + fc.x.set_value(5) self.assertEqual(25, fc.evaluate()) fc.x << 6 self.assertEqual(36, fc.evaluate()) - fc.setEquation("fsquare + 5") + fc.set_equation("fsquare + 5") self.assertEqual(41, fc.evaluate()) fc.x << -1 self.assertEqual(6, fc.evaluate()) @@ -196,7 +223,7 @@ def testResidual(noObserversInGlobalBuilders): # Add the calculator and profile fc.set_profile(profile) assert fc.profile is profile - fc.addProfileGenerator(gen, "I") + fc.add_profile_generator(gen, "I") assert fc._eq._value is None assert fc._reseq._value is None assert 1 == len(fc._generators) @@ -205,7 +232,7 @@ def testResidual(noObserversInGlobalBuilders): # Let's create some data) xobs = arange(0, 10, 0.5) yobs = xobs - profile.setObservedProfile(xobs, yobs) + profile.set_observed_profile(xobs, yobs) # Check our fitting equation. assert np.array_equal(fc._eq(), gen(xobs)) @@ -215,7 +242,7 @@ def testResidual(noObserversInGlobalBuilders): assert dot(chiv, chiv) == pytest.approx(0) # Now change the equation - fc.setEquation("2*I") + fc.set_equation("2*I") assert fc._eq._value is None assert fc._reseq._value is None chiv = fc.residual() @@ -224,20 +251,20 @@ def testResidual(noObserversInGlobalBuilders): # Try to add a parameter c = Parameter("c", 2) fc._add_parameter(c) - fc.setEquation("c*I") + fc.set_equation("c*I") assert fc._eq._value is None assert fc._reseq._value is None chiv = fc.residual() assert dot(chiv, chiv) == pytest.approx(dot(yobs, yobs)) # Try something more complex - c.setValue(3) - fc.setEquation("c**2*sin(I)") + c.set_value(3) + fc.set_equation("c**2*sin(I)") assert fc._eq._value is None assert fc._reseq._value is None xobs = arange(0, 10, 0.5) yobs = 9 * sin(xobs) - profile.setObservedProfile(xobs, yobs) + profile.set_observed_profile(xobs, yobs) assert fc._eq._value is None assert fc._reseq._value is None @@ -245,27 +272,27 @@ def testResidual(noObserversInGlobalBuilders): assert dot(chiv, chiv) == pytest.approx(0) # Choose a new residual. - fc.setEquation("2*I") - fc.setResidualEquation("resv") + fc.set_equation("2*I") + fc.set_residual_equation("resv") chiv = fc.residual() assert dot(chiv, chiv) == pytest.approx( sum((2 * xobs - yobs) ** 2) / sum(yobs**2) ) # Make a custom residual. - fc.setResidualEquation("abs(eq-y)**0.5") + fc.set_residual_equation("abs(eq-y)**0.5") chiv = fc.residual() assert dot(chiv, chiv) == pytest.approx(sum(abs(2 * xobs - yobs))) # Test configuration checks fc1 = FitContribution("test1") with pytest.raises(SrFitError): - fc1.setResidualEquation("chiv") + fc1.set_residual_equation("chiv") fc1.set_profile(profile) with pytest.raises(SrFitError): - fc1.setResidualEquation("chiv") - fc1.setEquation("A * x") - fc1.setResidualEquation("chiv") + fc1.set_residual_equation("chiv") + fc1.set_equation("A * x") + fc1.set_residual_equation("chiv") assert noObserversInGlobalBuilders return @@ -274,7 +301,21 @@ def test_setEquation(noObserversInGlobalBuilders): """Check replacement of removed parameters.""" fc = FitContribution("test") fc.setEquation("x + 5") - fc.x.setValue(2) + fc.x.set_value(2) + assert 7 == fc.evaluate() + fc.removeParameter(fc.x) + x = arange(0, 10, 0.5) + fc.newParameter("x", x) + assert np.array_equal(5 + x, fc.evaluate()) + assert noObserversInGlobalBuilders + return + + +def test_set_equation(noObserversInGlobalBuilders): + """Check replacement of removed parameters.""" + fc = FitContribution("test") + fc.set_equation("x + 5") + fc.x.set_value(2) assert 7 == fc.evaluate() fc.removeParameter(fc.x) x = arange(0, 10, 0.5) @@ -287,12 +328,22 @@ def test_setEquation(noObserversInGlobalBuilders): def test_getEquation(noObserversInGlobalBuilders): """Check getting the current profile simulation formula.""" fc = FitContribution("test") - assert "" == fc.getEquation() - fc.setEquation("A * sin(x + 5)") + assert "" == fc.get_equation() + fc.set_equation("A * sin(x + 5)") assert "(A * sin((x + 5)))" == fc.getEquation() assert noObserversInGlobalBuilders return +def test_get_equation(noObserversInGlobalBuilders): + """Check getting the current profile simulation formula.""" + fc = FitContribution("test") + assert "" == fc.get_equation() + fc.set_equation("A * sin(x + 5)") + assert "(A * sin((x + 5)))" == fc.get_equation() + assert noObserversInGlobalBuilders + return + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_diffpyparset.py b/tests/test_diffpyparset.py index 8463d80d..2e523d35 100644 --- a/tests/test_diffpyparset.py +++ b/tests/test_diffpyparset.py @@ -90,12 +90,12 @@ def _testLattice(): _testLattice() # Now change values from the srfit DiffpyStructureParSet - s.Cu0.x.setValue(0.456) - s.Cu0.U22.setValue(0.441) - s.Cu0.B13.setValue(0.550) + s.Cu0.x.set_value(0.456) + s.Cu0.U22.set_value(0.441) + s.Cu0.B13.set_value(0.550) d = dsstru.lattice.dist(a1.xyz, a2.xyz) - s.lattice.b.setValue(4.6) - s.lattice.alpha.setValue(91.3) + s.lattice.b.set_value(4.6) + s.lattice.alpha.set_value(91.3) _testAtoms() _testLattice() # Make sure the distance changed diff --git a/tests/test_equation.py b/tests/test_equation.py index ff44a3d8..c4bc7cd8 100644 --- a/tests/test_equation.py +++ b/tests/test_equation.py @@ -46,11 +46,11 @@ def testSimpleFunction(make_args, noObserversInGlobalBuilders): # Set the values of the variables. # The equation should evaluate to 2.5*(1+3)*(4-2) = 20 - v1.setValue(1) - v2.setValue(2) - v3.setValue(3) - v4.setValue(4) - c.setValue(2.5) + v1.set_value(1) + v2.set_value(2) + v3.set_value(3) + v4.set_value(4) + c.set_value(2.5) # Make an equation and test eq = Equation("eq", mult2) @@ -135,11 +135,11 @@ def testEmbeddedEquation(make_args, noObserversInGlobalBuilders): # Set the values of the variables. # The equation should evaluate to 2.5*(1+3)*(4-2) = 20 - v1.setValue(1) - v2.setValue(2) - v3.setValue(3) - v4.setValue(4) - c.setValue(2.5) + v1.set_value(1) + v2.set_value(2) + v3.set_value(3) + v4.set_value(4) + c.set_value(2.5) # Make an equation and test root = Equation("root", mult2) diff --git a/tests/test_fitrecipe.py b/tests/test_fitrecipe.py index 53fe7d72..b697f531 100644 --- a/tests/test_fitrecipe.py +++ b/tests/test_fitrecipe.py @@ -18,10 +18,12 @@ import matplotlib import matplotlib.pyplot as plt +import numpy as np import pytest from numpy import array_equal, dot, linspace, pi, sin from scipy.optimize import leastsq +from diffpy.srfit.fitbase import FitResults, ProfileParser from diffpy.srfit.fitbase.fitcontribution import FitContribution from diffpy.srfit.fitbase.fitrecipe import FitRecipe from diffpy.srfit.fitbase.parameter import Parameter @@ -41,58 +43,58 @@ def setUp(self): self.profile = Profile() x = linspace(0, pi, 10) y = sin(x) - self.profile.setObservedProfile(x, y) + self.profile.set_observed_profile(x, y) # Set up the FitContribution self.fitcontribution = FitContribution("cont") self.fitcontribution.set_profile(self.profile) - self.fitcontribution.setEquation("A*sin(k*x + c)") - self.fitcontribution.A.setValue(1) - self.fitcontribution.k.setValue(1) - self.fitcontribution.c.setValue(0) + self.fitcontribution.set_equation("A*sin(k*x + c)") + self.fitcontribution.A.set_value(1) + self.fitcontribution.k.set_value(1) + self.fitcontribution.c.set_value(0) - self.recipe.addContribution(self.fitcontribution) + self.recipe.add_contribution(self.fitcontribution) return - def testFixFree(self): + def test_fix_free(self): recipe = self.recipe con = self.fitcontribution - recipe.addVar(con.A, 2, tag="tagA") - recipe.addVar(con.k, 1, tag="tagk") - recipe.addVar(con.c, 0) - recipe.newVar("B", 0) + recipe.add_variable(con.A, 2, tag="tagA") + recipe.add_variable(con.k, 1, tag="tagk") + recipe.add_variable(con.c, 0) + recipe.create_new_variable("B", 0) - self.assertTrue(recipe.isFree(recipe.A)) + self.assertTrue(recipe.is_free(recipe.A)) recipe.fix("tagA") - self.assertFalse(recipe.isFree(recipe.A)) + self.assertFalse(recipe.is_free(recipe.A)) recipe.free("tagA") - self.assertTrue(recipe.isFree(recipe.A)) + self.assertTrue(recipe.is_free(recipe.A)) recipe.fix("A") - self.assertFalse(recipe.isFree(recipe.A)) + self.assertFalse(recipe.is_free(recipe.A)) recipe.free("A") - self.assertTrue(recipe.isFree(recipe.A)) + self.assertTrue(recipe.is_free(recipe.A)) recipe.fix(recipe.A) - self.assertFalse(recipe.isFree(recipe.A)) + self.assertFalse(recipe.is_free(recipe.A)) recipe.free(recipe.A) - self.assertTrue(recipe.isFree(recipe.A)) + self.assertTrue(recipe.is_free(recipe.A)) recipe.fix(recipe.A) - self.assertFalse(recipe.isFree(recipe.A)) + self.assertFalse(recipe.is_free(recipe.A)) recipe.free("all") - self.assertTrue(recipe.isFree(recipe.A)) - self.assertTrue(recipe.isFree(recipe.k)) - self.assertTrue(recipe.isFree(recipe.c)) - self.assertTrue(recipe.isFree(recipe.B)) + self.assertTrue(recipe.is_free(recipe.A)) + self.assertTrue(recipe.is_free(recipe.k)) + self.assertTrue(recipe.is_free(recipe.c)) + self.assertTrue(recipe.is_free(recipe.B)) recipe.fix(recipe.A, "tagk", c=3) - self.assertFalse(recipe.isFree(recipe.A)) - self.assertFalse(recipe.isFree(recipe.k)) - self.assertFalse(recipe.isFree(recipe.c)) - self.assertTrue(recipe.isFree(recipe.B)) + self.assertFalse(recipe.is_free(recipe.A)) + self.assertFalse(recipe.is_free(recipe.k)) + self.assertFalse(recipe.is_free(recipe.c)) + self.assertTrue(recipe.is_free(recipe.B)) self.assertEqual(3, recipe.c.value) recipe.fix("all") - self.assertFalse(recipe.isFree(recipe.A)) - self.assertFalse(recipe.isFree(recipe.k)) - self.assertFalse(recipe.isFree(recipe.c)) + self.assertFalse(recipe.is_free(recipe.A)) + self.assertFalse(recipe.is_free(recipe.k)) + self.assertFalse(recipe.is_free(recipe.c)) self.assertFalse(recipe.isFree(recipe.B)) self.assertRaises(ValueError, recipe.free, "junk") @@ -100,39 +102,87 @@ def testFixFree(self): self.assertRaises(ValueError, recipe.fix, "junk") return + def test_variables(self): + """Test to see if variables are added and removed properly.""" + recipe = self.recipe + con = self.fitcontribution + + recipe.add_variable(con.A, 2) + recipe.add_variable(con.k, 1) + recipe.add_variable(con.c, 0) + recipe.create_new_variable("B", 0) + + names = recipe.get_names() + self.assertEqual(names, ["A", "k", "c", "B"]) + values = recipe.get_values() + self.assertTrue((values == [2, 1, 0, 0]).all()) + + # Constrain a parameter to the B-variable to give it a value + p = Parameter("Bpar", -1) + recipe.add_constraint(recipe.B, p) + values = recipe.get_values() + self.assertTrue((values == [2, 1, 0]).all()) + recipe.delete_variable(recipe.B) + + recipe.fix(recipe.k) + + names = recipe.get_names() + self.assertEqual(names, ["A", "c"]) + values = recipe.get_values() + self.assertTrue((values == [2, 0]).all()) + + recipe.fix("all") + names = recipe.get_names() + self.assertEqual(names, []) + values = recipe.get_values() + self.assertTrue((values == []).all()) + + recipe.free("all") + names = recipe.get_names() + self.assertEqual(3, len(names)) + self.assertTrue("A" in names) + self.assertTrue("k" in names) + self.assertTrue("c" in names) + values = recipe.get_values() + self.assertEqual(3, len(values)) + self.assertTrue(0 in values) + self.assertTrue(1 in values) + self.assertTrue(2 in values) + return + def testVars(self): """Test to see if variables are added and removed properly.""" recipe = self.recipe con = self.fitcontribution - recipe.addVar(con.A, 2) - recipe.addVar(con.k, 1) + recipe.add_variable(con.A, 2) + recipe.add_variable(con.k, 1) recipe.addVar(con.c, 0) recipe.newVar("B", 0) - names = recipe.getNames() + names = recipe.get_names() self.assertEqual(names, ["A", "k", "c", "B"]) - values = recipe.getValues() + values = recipe.get_values() self.assertTrue((values == [2, 1, 0, 0]).all()) # Constrain a parameter to the B-variable to give it a value p = Parameter("Bpar", -1) recipe.constrain(recipe.B, p) - values = recipe.getValues() + values = recipe.get_values() self.assertTrue((values == [2, 1, 0]).all()) recipe.delVar(recipe.B) recipe.fix(recipe.k) - names = recipe.getNames() + names = recipe.get_names() self.assertEqual(names, ["A", "c"]) - values = recipe.getValues() + values = recipe.get_values() self.assertTrue((values == [2, 0]).all()) recipe.fix("all") - names = recipe.getNames() + names = recipe.get_names() self.assertEqual(names, []) - values = recipe.getValues() + values = recipe.get_values() self.assertTrue((values == []).all()) recipe.free("all") @@ -158,18 +208,18 @@ def testResidual(self): # Change the c value to 1 so that the equation evaluates as sin(x+1) x = self.profile.x y = sin(x + 1) - self.recipe.cont.c.setValue(1) + self.recipe.cont.c.set_value(1) res = self.recipe.residual() self.assertTrue(array_equal(y - self.profile.y, res)) # Try some constraints # Make c = 2*A, A = Avar - var = self.recipe.newVar("Avar") - self.recipe.constrain( + var = self.recipe.create_new_variable("Avar") + self.recipe.add_constraint( self.fitcontribution.c, "2*A", {"A": self.fitcontribution.A} ) self.assertEqual(2, self.fitcontribution.c.value) - self.recipe.constrain(self.fitcontribution.A, var) + self.recipe.add_constraint(self.fitcontribution.A, var) self.assertEqual(1, var.getValue()) self.assertEqual(self.recipe.cont.A.getValue(), var.getValue()) # c is constrained to a constrained parameter. @@ -182,7 +232,7 @@ def testResidual(self): # Now try some restraints. We want c to be exactly zero. It should give # a penalty of (c-0)**2, which is 4 in this case - r1 = self.recipe.restrain(self.fitcontribution.c, 0, 0, 1) + r1 = self.recipe.add_soft_bounds(self.fitcontribution.c, 0, 0, 1) self.recipe._ready = False res = self.recipe.residual() chi2 = 4 + dot(y - self.profile.y, y - self.profile.y) @@ -190,22 +240,22 @@ def testResidual(self): # Clear the constraint and restore the value of c to 0. This should # give us chi2 = 0 again. - self.recipe.unconstrain(self.fitcontribution.c) - self.fitcontribution.c.setValue(0) + self.recipe.remove_constraint(self.fitcontribution.c) + self.fitcontribution.c.set_value(0) res = self.recipe.residual([self.recipe.cont.A.getValue()]) chi2 = 0 self.assertAlmostEqual(chi2, dot(res, res)) # Remove the restraint and variable - self.recipe.unrestrain(r1) - self.recipe.delVar(self.recipe.Avar) + self.recipe.remove_soft_bounds(r1) + self.recipe.delete_variable(self.recipe.Avar) self.recipe._ready = False res = self.recipe.residual() chi2 = 0 self.assertAlmostEqual(chi2, dot(res, res)) # Add constraints at the fitcontribution level. - self.fitcontribution.constrain(self.fitcontribution.c, "2*A") + self.fitcontribution.add_constraint(self.fitcontribution.c, "2*A") # This should evaluate to sin(x+2) x = self.profile.x y = sin(x + 2) @@ -213,7 +263,9 @@ def testResidual(self): self.assertTrue(array_equal(y - self.profile.y, res)) # Add a restraint at the fitcontribution level. - r1 = self.fitcontribution.restrain(self.fitcontribution.c, 0, 0, 1) + r1 = self.fitcontribution.add_soft_bounds( + self.fitcontribution.c, 0, 0, 1 + ) self.recipe._ready = False # The chi2 is the same as above, plus 4 res = self.recipe.residual() @@ -223,22 +275,22 @@ def testResidual(self): self.assertAlmostEqual(chi2, dot(res, res)) # Remove those - self.fitcontribution.unrestrain(r1) + self.fitcontribution.remove_soft_bounds(r1) self.recipe._ready = False self.fitcontribution.unconstrain(self.fitcontribution.c) - self.fitcontribution.c.setValue(0) + self.fitcontribution.c.set_value(0) res = self.recipe.residual() chi2 = 0 self.assertAlmostEqual(chi2, dot(res, res)) # Now try to use the observed profile inside of the equation # Set the equation equal to the data - self.fitcontribution.setEquation("y") + self.fitcontribution.set_equation("y") res = self.recipe.residual() self.assertAlmostEqual(0, dot(res, res)) # Now add the uncertainty. This should give dy/dy = 1 for the residual - self.fitcontribution.setEquation("y+dy") + self.fitcontribution.set_equation("y+dy") res = self.recipe.residual() self.assertAlmostEqual(len(res), dot(res, res)) @@ -249,6 +301,46 @@ def testResidual(self): # ---------------------------------------------------------------------------- +def test_boundsToRestraints(): + recipe = FitRecipe("recipe") + + # create a bounded variable + recipe.create_new_variable("var1", 1) + expected_lower_bound = -1 + expected_upper_bound = 1 + expected_sigma = 2 + recipe.var1.bounds = (expected_lower_bound, expected_upper_bound) + + # apply restraints from bounds + recipe.boundsToRestraints(sig=expected_sigma, scaled=True) + restraints = list(recipe._restraints) + assert len(restraints) == 1 + r = restraints[0] + actual_lower_bound = r.lower_bound + actual_upper_bound = r.upper_bound + actual_sigma = r.sig + assert actual_lower_bound == expected_lower_bound + assert actual_upper_bound == expected_upper_bound + assert actual_sigma == expected_sigma + assert r.scaled is True + + +def test_convert_bounds_to_restraints(): + recipe = FitRecipe("recipe") + # create a bounded variable + recipe.create_new_variable("var1", 1) + recipe.var1.bounds = (-1, 1) + # apply restraints from bounds + recipe.convert_bounds_to_restraints(sig=2, scaled=True) + restraints = list(recipe._restraints) + assert len(restraints) == 1 + r = restraints[0] + assert r.lower_bound == -1 + assert r.upper_bound == 1 + assert r.sig == 2 + assert r.scaled is True + + def testPrintFitHook(capturestdout): "check output from default PrintFitHook." recipe = FitRecipe("recipe") @@ -258,29 +350,29 @@ def testPrintFitHook(capturestdout): profile = Profile() x = linspace(0, pi, 10) y = sin(x) - profile.setObservedProfile(x, y) + profile.set_observed_profile(x, y) # Set up the FitContribution fitcontribution = FitContribution("cont") fitcontribution.set_profile(profile) - fitcontribution.setEquation("A*sin(k*x + c)") - fitcontribution.A.setValue(1) - fitcontribution.k.setValue(1) - fitcontribution.c.setValue(0) + fitcontribution.set_equation("A*sin(k*x + c)") + fitcontribution.A.set_value(1) + fitcontribution.k.set_value(1) + fitcontribution.c.set_value(0) recipe.addContribution(fitcontribution) - recipe.addVar(fitcontribution.c) - recipe.restrain("c", lb=5) + recipe.add_variable(fitcontribution.c) + recipe.add_soft_bounds("c", lower_bound=5) (pfh,) = recipe.getFitHooks() - out = capturestdout(recipe.scalarResidual) + out = capturestdout(recipe.scalar_residual) assert "" == out pfh.verbose = 1 - out = capturestdout(recipe.scalarResidual) + out = capturestdout(recipe.scalar_residual) assert out.strip().isdigit() assert "\nRestraints:" not in out pfh.verbose = 2 - out = capturestdout(recipe.scalarResidual) + out = capturestdout(recipe.scalar_residual) assert "\nResidual:" in out assert "\nRestraints:" in out assert "\nVariables" not in out @@ -291,6 +383,82 @@ def testPrintFitHook(capturestdout): return +def test_add_and_remove_ParameterSet(): + # add a parset + recipe = FitRecipe("recipe") + parameter_to_add = Parameter("added_param", 1) + recipe.addParameterSet(parameter_to_add) + # check that the parameter is added + assert recipe.added_param == parameter_to_add + assert recipe.added_param.value == 1 + # remove the added parameter + recipe.removeParameterSet(parameter_to_add) + # check that the parameter is removed + assert not hasattr(recipe, "added_param") + + +def test_add_and_remove_parameter_set(): + recipe = FitRecipe("recipe") + parameter_to_add = Parameter("added_param", 1) + # add a parset + recipe.add_parameter_set(parameter_to_add) + # check that the parameter is added + assert recipe.added_param == parameter_to_add + assert recipe.added_param.value == 1 + # remove the added parameter + recipe.remove_parameter_set(parameter_to_add) + # check that the parameter is removed + assert not hasattr(recipe, "added_param") + + +def test_add_contribution(capturestdout): + """Duplicated test of PrintFitHooks except addContribution method + has changed to the new add_contribution method. This is because + addContribution is deprecated. + + Remove this test after addContribution is removed and update + testPrintFitHook to use add_contribution instead of addContribution. + """ + recipe = FitRecipe("recipe") + recipe.fithooks[0].verbose = 0 + + # Set up the Profile + profile = Profile() + x = linspace(0, pi, 10) + y = sin(x) + profile.set_observed_profile(x, y) + + # Set up the FitContribution + fitcontribution = FitContribution("cont") + fitcontribution.set_profile(profile) + fitcontribution.set_equation("A*sin(k*x + c)") + fitcontribution.A.set_value(1) + fitcontribution.k.set_value(1) + fitcontribution.c.set_value(0) + + recipe.add_contribution(fitcontribution) + + recipe.add_variable(fitcontribution.c) + recipe.add_soft_bounds("c", lower_bound=5) + (pfh,) = recipe.get_fit_hooks() + out = capturestdout(recipe.scalar_residual) + assert "" == out + pfh.verbose = 1 + out = capturestdout(recipe.scalar_residual) + assert out.strip().isdigit() + assert "\nRestraints:" not in out + pfh.verbose = 2 + out = capturestdout(recipe.scalar_residual) + assert "\nResidual:" in out + assert "\nRestraints:" in out + assert "\nVariables" not in out + pfh.verbose = 3 + out = capturestdout(recipe.scalar_residual) + assert "\nVariables" in out + assert "c = " in out + return + + def optimize_recipe(recipe): recipe.fithooks[0].verbose = 0 residuals = recipe.residual @@ -298,6 +466,174 @@ def optimize_recipe(recipe): leastsq(residuals, values) +def test_initialize_recipe_from_recipe(build_recipes_one_contribution): + # Case: User initializes a FitRecipe from a previously optimized fit + # expected: recipe is initialized with everything: + # contributions, profiles (contained in contributions), + # variables, restraints, and constraints + recipe1, _ = build_recipes_one_contribution + optimize_recipe(recipe1) + expected_parameters_dict = recipe1._parameters + expected_constraints_dict = recipe1._constraints + expected_restraints_set = recipe1._restraints + expected_contributions_dict = recipe1._contributions + expected_profiles_list = [] + for con_name, contribution in expected_contributions_dict.items(): + expected_profile = contribution.profile + expected_profiles_list.append(expected_profile) + + recipe2 = FitRecipe() + assert recipe1 != recipe2 + recipe2.initialize_recipe_with_recipe(recipe1) + actual_parameters_dict = recipe2._parameters + actual_constraints_dict = recipe2._constraints + actual_restraints_set = recipe2._restraints + actual_contributions_dict = recipe2._contributions + actual_profiles_list = [] + for con_name, contribution in actual_contributions_dict.items(): + actual_profile = contribution.profile + actual_profiles_list.append(actual_profile) + + assert expected_parameters_dict == actual_parameters_dict + assert expected_constraints_dict == actual_constraints_dict + assert expected_restraints_set == actual_restraints_set + assert expected_contributions_dict == actual_contributions_dict + assert expected_profiles_list == actual_profiles_list + + # Check to see if the refined values and variable names are + # the same in the results objects for each recipe + results1 = FitResults(recipe1) + # round to account for small numerical differences + expected_values = np.round(results1.varvals, 7) + expected_names = results1.varnames + + optimize_recipe(recipe2) + results2 = FitResults(recipe2) + # round to account for small numerical differences + actual_values = np.round(results2.varvals, 7) + actual_names = results2.varnames + + assert sorted(expected_names) == sorted(actual_names) + assert sorted(list(expected_values)) == sorted(list(actual_values)) + + +def test_initialize_recipe_from_recipe_bad(build_recipe_two_contributions): + # Case: User tries to initialize a FitRecipe from a non recipe object + # expected: raised ValueError with message + recipe_bad = 12345 # not a FitRecipe object + recipe2 = FitRecipe() + msg = ( + "The input recipe_object must be a FitRecipe, " + "but got ." + ) + with pytest.raises(ValueError, match=msg): + recipe2.initialize_recipe_with_recipe(recipe_bad) + + +def test_initialize_recipe_from_results_object(build_recipes_one_contribution): + # Case: User initializes a FitRecipe from a FitResults object + # expected: recipe is initialized with variables from previous fit + + # create unique recipe1 + recipe1, recipe2 = build_recipes_one_contribution + optimize_recipe(recipe1) + results1 = FitResults(recipe1) + expected_values = np.round(results1.varvals, 5) + expected_names = results1.varnames + + assert recipe1 != recipe2 + # create a new var that should be include in the initialized recipe + recipe2.create_new_variable("extra_var", 5) + actual_values_before_init = [val for val in recipe2.get_values()] + actual_names_before_init = sorted(recipe2.get_names()) + # the three variables + the extra_var + expected_names_before_init = sorted( + [ + "amplitude", + "extra_var", + "phase_shift", + "wave_number", + ] + ) + expected_values_before_init = [4, 3, 2, 5] + assert actual_values_before_init == expected_values_before_init + assert actual_names_before_init == expected_names_before_init + recipe2.initialize_recipe_with_results(results1) + optimize_recipe(recipe2) + results2 = FitResults(recipe2) + actual_values = np.round(results2.varvals, 5) + actual_names = results2.varnames + + # add the new variable name to expected names + expected_names = expected_names + ["extra_var"] + # add the value of the new variable to expected values + expected_values = list(expected_values) + [5] + assert sorted(expected_names) == sorted(actual_names) + assert sorted(expected_values) == sorted(list(actual_values)) + + +def test_initialize_recipe_from_results_file( + build_recipes_one_contribution, temp_data_files +): + # Case: User initializes a FitRecipe from a FitResults file + # expected: recipe is initialized with variables from previous fit + results_file = temp_data_files / "fit_results.res" + expected_names = ["amplitude", "phase_shift", "wave_number"] + expected_values = [1, 1, 0] + + recipe, _ = build_recipes_one_contribution + recipe.initialize_recipe_with_results(results_file) + results = FitResults(recipe) + actual_values = np.round(results.varvals, 5) + actual_names = results.varnames + + assert sorted(expected_names) == sorted(actual_names) + assert list(expected_values) == list(actual_values) + + +def test_initialize_recipe_from_results_file_bad( + build_recipes_one_contribution, +): + # Case: User tries to initialize a recipe with something that + # isn't a path, str, or FitResults object + # Expected: raised ValueError with message + recipe, _ = build_recipes_one_contribution + bad_input = 12345 # not a valid input type + msg = ( + "The input results must be a FitResults object or a path to a " + "results file, but got ." + ) + with pytest.raises(ValueError, match=msg): + recipe.initialize_recipe_with_results(bad_input) + + +def test_initialize_recipe_from_results_file_wrong( + build_recipe_two_contributions, temp_data_files, capsys +): + # Case: User tries to initialize a FitRecipe from a results file + # that does not match params in the recipe + # expected: Warning message is printed and things proceed as + # usual with the variables in the recipe + + results_file_from_single_contrib = temp_data_files / "fit_results.res" + recipe = build_recipe_two_contributions + recipe.initialize_recipe_with_results(results_file_from_single_contrib) + captured = capsys.readouterr() + actual_print_msg = captured.out # .strip() + + results_file_param_names = ["amplitude", "phase_shift", "wave_number"] + expected_print_messages = [] + for param_name in results_file_param_names: + msg = ( + f"Warning: Parameter '{param_name}' from results not found " + "in FitRecipe and will be ignored." + ) + expected_print_messages.append(msg) + + for expected_print_msg in expected_print_messages: + assert expected_print_msg in actual_print_msg + + def get_labels_and_linecount(ax): """Helper to get line labels and count from a matplotlib Axes.""" labels = [ @@ -319,22 +655,43 @@ def build_recipe_from_datafile(datafile): """Helper to build a FitRecipe from a datafile using PDFParser and PDFGenerator.""" profile = Profile() + parser = ProfileParser() + parser.parse_file(str(datafile)) + profile.load_parsed_data(parser) + + contribution = FitContribution("c") + contribution.set_profile(profile) + contribution.set_equation("m*x + b") + recipe = FitRecipe() + recipe.add_contribution(contribution) + recipe.add_variable(contribution.m, 1) + recipe.add_variable(contribution.b, 0) + return recipe + + +def build_recipe_from_datafile_deprecated(datafile): + """Duplicate of build_recipe_from_datafile to use deprecated + loadParsedData method. + + Remove in version 4.0.0. + """ + profile = Profile() parser = PDFParser() parser.parseFile(str(datafile)) profile.loadParsedData(parser) contribution = FitContribution("c") contribution.set_profile(profile) - contribution.setEquation("m*x + b") + contribution.set_equation("m*x + b") recipe = FitRecipe() - recipe.addContribution(contribution) - recipe.addVar(contribution.m, 1) - recipe.addVar(contribution.b, 0) + recipe.add_contribution(contribution) + recipe.add_variable(contribution.m, 1) + recipe.add_variable(contribution.b, 0) return recipe -def test_plot_recipe_bad_display(build_recipe_one_contribution): - recipe = build_recipe_one_contribution +def test_plot_recipe_bad_display(build_recipes_one_contribution): + recipe, _ = build_recipes_one_contribution # Case: All plots are disabled # expected: raised ValueError with message plt.close("all") @@ -360,11 +717,11 @@ def test_plot_recipe_no_contribution(): recipe.plot_recipe() -def test_plot_recipe_before_refinement(capsys, build_recipe_one_contribution): +def test_plot_recipe_before_refinement(capsys, build_recipes_one_contribution): # Case: User tries to plot recipe before refinement # expected: Data plotted without fit line or difference curve # and warning message printed - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution plt.close("all") before = set(plt.get_fignums()) # include fit_label="nothing" to make sure fit line is not plotted @@ -389,10 +746,10 @@ def test_plot_recipe_before_refinement(capsys, build_recipe_one_contribution): assert actual == expected -def test_plot_recipe_after_refinement(build_recipe_one_contribution): +def test_plot_recipe_after_refinement(build_recipes_one_contribution): # Case: User refines recipe and then plots # expected: Plot generates with no problem - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") before = set(plt.get_fignums()) @@ -426,10 +783,10 @@ def test_plot_recipe_two_contributions(build_recipe_two_contributions): assert len(new_figs) == 2 -def test_plot_recipe_on_existing_plot(build_recipe_one_contribution): +def test_plot_recipe_on_existing_plot(build_recipes_one_contribution): # Case: User passes axes to plot_recipe to plot on existing figure # expected: User modifications are present in the final figure - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") fig, ax = plt.subplots() @@ -446,10 +803,10 @@ def test_plot_recipe_on_existing_plot(build_recipe_one_contribution): assert actual_title == expected_title -def test_plot_recipe_add_new_data(build_recipe_one_contribution): +def test_plot_recipe_add_new_data(build_recipes_one_contribution): # Case: User wants to add data to figure generated by plot_recipe # Expected: New data is added to existing figure (check with labels) - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") before = set(plt.get_fignums()) @@ -490,10 +847,10 @@ def test_plot_recipe_add_new_data_two_figs(build_recipe_two_contributions): assert len(new_figs) == 2 -def test_plot_recipe_set_title(build_recipe_one_contribution): +def test_plot_recipe_set_title(build_recipes_one_contribution): # Case: User sets title via plot_recipe # Expected: Title is set correctly - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") expected_title = "Custom Recipe Title" @@ -504,10 +861,10 @@ def test_plot_recipe_set_title(build_recipe_one_contribution): assert actual_title == expected_title -def test_plot_recipe_set_defaults(build_recipe_one_contribution): +def test_plot_recipe_set_defaults(build_recipes_one_contribution): # Case: user sets default plot options with set_plot_defaults # Expected: plot_recipe uses the default options for all calls - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") # set new defaults @@ -532,10 +889,10 @@ def test_plot_recipe_set_defaults(build_recipe_one_contribution): assert actual_labels == expected_labels -def test_plot_recipe_set_defaults_bad(capsys, build_recipe_one_contribution): +def test_plot_recipe_set_defaults_bad(capsys, build_recipes_one_contribution): # Case: user tries to set kwargs that are not valid plot_recipe options # Expected: Plot is shown and warning is printed - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") recipe.set_plot_defaults( @@ -599,7 +956,24 @@ def test_plot_recipe_labels_from_gr_file_overwrite(temp_data_files): assert actual_ylabel == expected_ylabel -def test_plot_recipe_reset_all_defaults(build_recipe_one_contribution): +def test_plot_recipe_labels_from_gr_file_overwrite_deprecated(temp_data_files): + "Remove this test with version 4.0.0." + gr_file = temp_data_files / "gr_file.gr" + recipe = build_recipe_from_datafile_deprecated(gr_file) + optimize_recipe(recipe) + plt.close("all") + fig, ax = recipe.plot_recipe( + return_fig=True, show=False, xlabel="My X", ylabel="My Y" + ) + actual_xlabel = ax.get_xlabel() + actual_ylabel = ax.get_ylabel() + expected_xlabel = "My X" + expected_ylabel = "My Y" + assert actual_xlabel == expected_xlabel + assert actual_ylabel == expected_ylabel + + +def test_plot_recipe_reset_all_defaults(build_recipes_one_contribution): expected_defaults = { "show_observed": True, "show_fit": True, @@ -628,7 +1002,7 @@ def test_plot_recipe_reset_all_defaults(build_recipe_one_contribution): "show": True, } - recipe = build_recipe_one_contribution + recipe, _ = build_recipes_one_contribution optimize_recipe(recipe) plt.close("all") diff --git a/tests/test_fitresults.py b/tests/test_fitresults.py index 5702b03f..d9be7317 100644 --- a/tests/test_fitresults.py +++ b/tests/test_fitresults.py @@ -16,17 +16,201 @@ import unittest +import numpy as np import pytest +from scipy.optimize import leastsq from diffpy.srfit.fitbase.fitrecipe import FitRecipe -from diffpy.srfit.fitbase.fitresults import initializeRecipe +from diffpy.srfit.fitbase.fitresults import ( + FitResults, + initializeRecipe, + resultsDictionary, +) + +# The fit results from the recipe fixture in conftest.py +expected_fitresults = """\ +My Custom header +Some quantities invalid due to missing profile uncertainty +Overall (Chi2 and Reduced Chi2 invalid) +------------------------------------------------------------------------------ +Residual 0.00000000 +Contributions 0.00000000 +Restraints 0.00000000 +Chi2 0.00000000 +Reduced Chi2 0.00000000 +Rw 0.00000010 + +Variables (Uncertainties invalid) +------------------------------------------------------------------------------ +""" +expected_refined_variables = ["amplitude", "wave_number", "phase_shift"] + + +def optimize_recipe(recipe): + recipe.fithooks[0].verbose = 0 + residuals = recipe.residual + values = recipe.values + leastsq(residuals, values) + + +def test_formatResults(build_recipes_one_contribution): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + actual_results_string = results.formatResults(header="My Custom header") + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results_string.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results_string.strip() + + +def test_get_results_string(build_recipes_one_contribution): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + actual_results_string = results.get_results_string( + header="My Custom header" + ) + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results_string.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results_string.strip() + + +def test_printResults(build_recipes_one_contribution, capsys): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + results.printResults(header="My Custom header") + actual_results = capsys.readouterr().out + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results.strip() + + +def test_print_results(build_recipes_one_contribution, capsys): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + results.print_results(header="My Custom header") + actual_results = capsys.readouterr().out + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results.strip() + + +def test_saveResults(build_recipes_one_contribution, tmp_path): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + actual_results_file = tmp_path / "fit_results.txt" + results.saveResults(actual_results_file, header="My Custom header") + assert actual_results_file.exists() + with open(actual_results_file, "r") as res_file: + actual_results = res_file.read() + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results.strip() + + +def test_save_results(build_recipes_one_contribution, tmp_path): + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + actual_results_file = tmp_path / "fit_results.txt" + results.save_results(actual_results_file, header="My Custom header") + assert actual_results_file.exists() + with open(actual_results_file, "r") as res_file: + actual_results = res_file.read() + # Because slight variations in refinement, just check + # that the header of the results are the same. + assert expected_fitresults.strip() in actual_results.strip() + # check if the refined variables are in the results + for expected_var in expected_refined_variables: + assert expected_var in actual_results.strip() + + +def test_get_results_dictionary(build_recipes_one_contribution): + # Case: user gets results dictionary after optimization + # expected: results dictionary contains expected keys and values + recipe, _ = build_recipes_one_contribution + optimize_recipe(recipe) + results = FitResults(recipe) + actual_results_dict = results.get_results_dictionary() + expected_results_dict = { + "amplitude": 1.000000000060171, + "wave_number": 1.00000000012548, + "phase_shift": -1.6129114631049646e-18, + "Residual": 3.3284672708760557e-19, + "Contributions": 3.3284672708760557e-19, + "Restraints": 0, + "Chi2": 3.3284672708760557e-19, + "Reduced Chi2": 4.7549532441086507e-20, + "Rw": 2.7196679825449506e-10, + } + actual_values = np.round(np.array(list(actual_results_dict.values())), 5) + actual_keys = set(actual_results_dict.keys()) + expected_values = np.round( + np.array(list(expected_results_dict.values())), 5 + ) + expected_keys = set(expected_results_dict.keys()) + assert expected_keys == actual_keys + assert list(expected_values == list(actual_values)) + + +def test_resultsDictionary(temp_data_files): + # Case: user gets results dictionary from a results file + # expected: results dictionary contains expected keys and values + actual_results_dict = resultsDictionary( + temp_data_files / "fit_results.res" + ) + # bad behavior: values are stored as strings + expected_results_dict = { + "than": "25", # bad behavior: shouldn't be here + "wave_number": "1.00000000e+00", + "phase_shift": "-1.61291146e-18", + "amplitude": "1.00000000e+00", + "Rw": "0.00000000", + "Chi2": "0.00000000", + "Restraints": "0.00000000", + "Contributions": "0.00000000", + "Residual": "0.00000000", + "Feb": "25", # bad behavior: shouldn't be here + } + # convert values to float for comparison (with rounding) + for key in expected_results_dict: + expected_results_dict[key] = float(expected_results_dict[key]) + for key in actual_results_dict: + actual_results_dict[key] = float(actual_results_dict[key]) + + actual_keys = set(actual_results_dict.keys()) + actual_values = np.round(np.array(list(actual_results_dict.values())), 5) + expected_keys = set(expected_results_dict.keys()) + expected_values = np.round( + np.array(list(expected_results_dict.values())), 5 + ) + assert expected_keys == actual_keys + assert list(expected_values == list(actual_values)) def testInitializeFromFileName(datafile): recipe = FitRecipe("recipe") - recipe.newVar("A", 0) - recipe.newVar("sig", 0) - recipe.newVar("x0", 0) + recipe.create_new_variable("A", 0) + recipe.create_new_variable("sig", 0) + recipe.create_new_variable("x0", 0) filename = datafile("results.res") Aval = 5.77619823e-01 sigval = -9.22758690e-01 @@ -44,9 +228,9 @@ def testInitializeFromFileName(datafile): def testInitializeFromFileObj(datafile): recipe = FitRecipe("recipe") - recipe.newVar("A", 0) - recipe.newVar("sig", 0) - recipe.newVar("x0", 0) + recipe.create_new_variable("A", 0) + recipe.create_new_variable("sig", 0) + recipe.create_new_variable("x0", 0) filename = datafile("results.res") Aval = 5.77619823e-01 sigval = -9.22758690e-01 @@ -67,9 +251,9 @@ def testInitializeFromFileObj(datafile): def testInitializeFromString(datafile): recipe = FitRecipe("recipe") - recipe.newVar("A", 0) - recipe.newVar("sig", 0) - recipe.newVar("x0", 0) + recipe.create_new_variable("A", 0) + recipe.create_new_variable("sig", 0) + recipe.create_new_variable("x0", 0) filename = datafile("results.res") Aval = 5.77619823e-01 sigval = -9.22758690e-01 diff --git a/tests/test_literals.py b/tests/test_literals.py index 8155d64a..970a2702 100644 --- a/tests/test_literals.py +++ b/tests/test_literals.py @@ -49,10 +49,10 @@ def testValue(self): self.assertEqual(None, a.getValue()) # Test setting value - a.setValue(3.14) + a.set_value(3.14) self.assertAlmostEqual(3.14, a._value) - a.setValue(3.14) + a.set_value(3.14) self.assertAlmostEqual(3.14, a.value) self.assertAlmostEqual(3.14, a.getValue()) return @@ -100,7 +100,7 @@ def testValue(self): self.assertAlmostEqual(0, op.value) # Test update from the nodes - a.setValue(4) + a.set_value(4) self.assertTrue(op._value is None) self.assertAlmostEqual(4, op.value) self.assertAlmostEqual(4, op.getValue()) @@ -129,11 +129,11 @@ def testAddLiteral(self): op.addLiteral(b) self.assertAlmostEqual(0, op.value) - a.setValue(1) - b.setValue(2) + a.set_value(1) + b.set_value(2) self.assertAlmostEqual(3, op.value) - a.setValue(None) + a.set_value(None) # Test for self-references # Try to add self diff --git a/tests/test_objcrystparset.py b/tests/test_objcrystparset.py index 5150ae54..f905a9eb 100644 --- a/tests/test_objcrystparset.py +++ b/tests/test_objcrystparset.py @@ -223,11 +223,11 @@ def _testMolecule(): _testMolecule() # Now change values from the srfit StructureParSet - cryst.c60.C44.x.setValue(1.1) - cryst.c60.C44.occ.setValue(1.1) - cryst.c60.C44.Biso.setValue(1.1) - cryst.c60.q3.setValue(1.1) - cryst.a.setValue(1.1) + cryst.c60.C44.x.set_value(1.1) + cryst.c60.C44.occ.set_value(1.1) + cryst.c60.C44.Biso.set_value(1.1) + cryst.c60.q3.set_value(1.1) + cryst.a.set_value(1.1) _testCrystal() _testMolecule() @@ -411,7 +411,7 @@ def testExplicitBondLengthParameter(self): # Change the value scale = 1.05 - p1.setValue(scale * d0) + p1.set_value(scale * d0) # Verify that it has changed. assert scale * d0 == pytest.approx(p1.getValue(), abs=1e-6) @@ -483,7 +483,7 @@ def testExplicitBondAngleParameter(self): # Change the value scale = 1.05 - p1.setValue(scale * angle0) + p1.set_value(scale * angle0) # Verify that it has changed. assert scale * angle0 == pytest.approx(p1.getValue(), abs=1e-6) @@ -564,7 +564,7 @@ def testExplicitDihedralAngleParameter(self): # Change the value scale = 1.05 - p1.setValue(scale * angle0) + p1.set_value(scale * angle0) # Verify that it has changed. assert scale * angle0 == pytest.approx(p1.getValue(), abs=1e-6) @@ -650,7 +650,8 @@ def sgsEquivalent(self, sg1, sg2): # FIXME: only about 50% of the spacegroups pass the assertion # test disabled even if cctbx is installed def xtestCreateSpaceGroup(self): - """Check all sgtbx space groups for proper conversion to SpaceGroup.""" + """Check all sgtbx space groups for proper conversion to + SpaceGroup.""" try: from cctbx import sgtbx diff --git a/tests/test_parameter.py b/tests/test_parameter.py index 04ecd4c7..a56d3ddf 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -16,6 +16,9 @@ import unittest +import numpy as np +import pytest + from diffpy.srfit.fitbase.parameter import ( Parameter, ParameterAdapter, @@ -29,7 +32,7 @@ def testSetValue(self): """Test initialization.""" par_l = Parameter("l") - par_l.setValue(3.14) + par_l.set_value(3.14) self.assertAlmostEqual(3.14, par_l.getValue()) # Try array @@ -47,7 +50,7 @@ def testSetValue(self): self.assertTrue(par_l.value is y) # Back to scalar - par_l.setValue(1.01) + par_l.set_value(1.01) self.assertAlmostEqual(1.01, par_l.getValue()) self.assertAlmostEqual(1.01, par_l.value) return @@ -89,18 +92,18 @@ def testWrapper(self): # Try Accessor adaptation la = ParameterAdapter( - "l", par_l, getter=Parameter.getValue, setter=Parameter.setValue + "l", par_l, getter=Parameter.getValue, setter=Parameter.set_value ) self.assertEqual(par_l.name, la.name) self.assertEqual(par_l.getValue(), la.getValue()) # Change the parameter - par_l.setValue(2.3) + par_l.set_value(2.3) self.assertEqual(par_l.getValue(), la.getValue()) # Change the adapter - la.setValue(3.2) + la.set_value(3.2) self.assertEqual(par_l.getValue(), la.getValue()) # Try Attribute adaptation @@ -111,15 +114,105 @@ def testWrapper(self): self.assertEqual(par_l.getValue(), la.getValue()) # Change the parameter - par_l.setValue(2.3) + par_l.set_value(2.3) self.assertEqual(par_l.getValue(), la.getValue()) # Change the adapter - la.setValue(3.2) + la.set_value(3.2) self.assertEqual(par_l.getValue(), la.getValue()) return +@pytest.mark.parametrize( + "lower, upper, expected", + [ + # User sets both lower and upper bounds explicitly. + (1, 10, [1, 10]), + # User sets only a lower bound. + (2, None, [2, np.inf]), + # User sets only an upper bound. + (None, 8, [-np.inf, 8]), + # User overwrites existing bounds. + (2, 6, [2, 6]), + ], +) +def test_bound_range(lower, upper, expected): + p = Parameter("a", value=5) + # If testing overwrite, pre-set bounds to see overwrite effect + if expected == [2, 6]: + p.bound_range(0, 10) + p.bound_range(lower_bound=lower, upper_bound=upper) + actual = p.bounds + assert actual == expected + + +@pytest.mark.parametrize( + "lower, upper, expected", + [ + # User sets both lower and upper bounds explicitly. + (1, 10, [1, 10]), + # User sets only a lower bound. + (2, None, [2, np.inf]), + # User sets only an upper bound. + (None, 8, [-np.inf, 8]), + # User overwrites existing bounds. + (2, 6, [2, 6]), + ], +) +def test_boundRange(lower, upper, expected): + p = Parameter("a", value=5) + # If testing overwrite, pre-set bounds to see overwrite effect + if expected == [2, 6]: + p.boundRange(0, 10) + p.boundRange(lower_bound=lower, upper_bound=upper) + actual = p.bounds + assert actual == expected + + +@pytest.mark.parametrize( + "value, lower_radius, upper_radius, expected", + [ + # Symmetric radius (upper_radius None, uses lower_radius) + (10, 2, None, [8, 12]), + # Asymmetric radius + (10, 3, 5, [7, 15]), + # Zero radius + (4, 0, None, [4, 4]), + # Current value updated before bounding + (20, 2, None, [18, 22]), + ], +) +def test_bound_window(value, lower_radius, upper_radius, expected): + p = Parameter("a", value=5) + if value != 5: + p.set_value(value) + p.bound_window(lower_radius=lower_radius, upper_radius=upper_radius) + actual = p.bounds + assert actual == expected + + +@pytest.mark.parametrize( + "value, lower_radius, upper_radius, expected", + [ + # Symmetric radius (upper_radius None, uses lower_radius) + (10, 2, None, [8, 12]), + # Asymmetric radius + (10, 3, 5, [7, 15]), + # Zero radius + (4, 0, None, [4, 4]), + # Current value updated before bounding + (20, 2, None, [18, 22]), + ], +) +def test_boundWindow(value, lower_radius, upper_radius, expected): + p = Parameter("a", value=5) + if value != 5: + p.set_value(value) + p.boundWindow(lr=lower_radius, ur=upper_radius) + actual = p.bounds + assert actual == expected + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_parameterset.py b/tests/test_parameterset.py index 51ffa14c..1a954c8d 100644 --- a/tests/test_parameterset.py +++ b/tests/test_parameterset.py @@ -27,14 +27,35 @@ def setUp(self): return def testAddParameterSet(self): - """Test the addParameterSet method.""" + """Test the deprecated addParameterSet method. + + Remove this test after the addParameterSet is removed in version + 4.0.0. + """ parset2 = ParameterSet("parset2") p1 = Parameter("parset2", 1) self.parset.addParameterSet(parset2) self.assertTrue(self.parset.parset2 is parset2) - self.assertRaises(ValueError, self.parset.addParameterSet, p1) + self.assertRaises(ValueError, self.parset.add_parameter_set, p1) + + p1.name = "p1" + parset2.addParameter(p1) + + self.assertTrue(self.parset.parset2.p1 is p1) + + return + + def test_add_parameter_set(self): + """Test the add_parameter_set method.""" + parset2 = ParameterSet("parset2") + p1 = Parameter("parset2", 1) + + self.parset.add_parameter_set(parset2) + self.assertTrue(self.parset.parset2 is parset2) + + self.assertRaises(ValueError, self.parset.add_parameter_set, p1) p1.name = "p1" parset2.addParameter(p1) diff --git a/tests/test_pdf.py b/tests/test_pdf.py index b46fa2a6..908d5eb1 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -23,6 +23,7 @@ import pytest from diffpy.srfit.exceptions import SrFitError +from diffpy.srfit.fitbase import ProfileParser from diffpy.srfit.pdf import PDFContribution, PDFGenerator, PDFParser # ---------------------------------------------------------------------------- @@ -46,7 +47,7 @@ def testParser1(datafile): assert meta.get("scale") is None assert meta.get("doping") is None - x, y, dx, dy = parser.getData() + x, y, dx, dy = parser.get_data() assert dx is None assert dy is None @@ -78,12 +79,12 @@ def testParser1(datafile): def testParser2(datafile): data = datafile("si-q27r60-xray.gr") - parser = PDFParser() - parser.parseFile(data) + parser = ProfileParser() + parser.parse_file(data) meta = parser._meta - assert data == meta["filename"] + assert str(data) == meta["filename"] assert 1 == meta["nbanks"] assert "X" == meta["stype"] assert 27 == meta["qmax"] @@ -94,7 +95,7 @@ def testParser2(datafile): assert meta.get("scale") is None assert meta.get("doping") is None - x, y, dx, dy = parser.getData() + x, y, dx, dy = parser.get_data() testx = numpy.linspace(0.01, 60, 5999, endpoint=False) diff = testx - x res = numpy.dot(diff, diff) @@ -136,7 +137,7 @@ def testParser2(datafile): res = numpy.dot(diff, diff) assert 0 == pytest.approx(res) - assert dx is None + assert dx.tolist() == [0] * len(dx) return @@ -173,9 +174,9 @@ def testGenerator( defval = calc._getDoubleAttr(pname) assert defval == par.getValue() # Test setting values - par.setValue(1.0) + par.set_value(1.0) assert 1.0 == par.getValue() - par.setValue(defval) + par.set_value(defval) assert defval == par.getValue() r = numpy.arange(0, 10, 0.1) @@ -308,7 +309,9 @@ def test_pickling( pc2 = pickle.loads(pickle.dumps(pc)) res0 = pc.residual() assert numpy.array_equal(res0, pc2.residual()) - for p in chain(pc.iterPars("Uiso"), pc2.iterPars("Uiso")): + for p in chain( + pc.iterate_over_parameters("Uiso"), pc2.iterate_over_parameters("Uiso") + ): p.value = 0.004 res1 = pc.residual() assert not numpy.allclose(res0, res1) diff --git a/tests/test_profile.py b/tests/test_profile.py index 29da2162..b69f6f7b 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -22,6 +22,7 @@ from numpy import allclose, arange, array, array_equal, ones_like from diffpy.srfit.exceptions import SrFitError +from diffpy.srfit.fitbase import ProfileParser from diffpy.srfit.fitbase.profile import Profile @@ -43,8 +44,44 @@ def testInit(self): self.assertEqual(profile.meta, {}) return + def test_set_observed_profile(self): + """Test the set_observed_profile method.""" + # Make a profile with defined dy + + x = arange(0, 10, 0.1) + y = x + dy = x + + prof = self.profile + prof.set_observed_profile(x, y, dy) + + self.assertTrue(array_equal(x, prof.xobs)) + self.assertTrue(array_equal(y, prof.yobs)) + self.assertTrue(array_equal(dy, prof.dyobs)) + + # Make a profile with undefined dy + x = arange(0, 10, 0.1) + y = x + dy = None + + self.profile.set_observed_profile(x, y, dy) + + self.assertTrue(array_equal(x, prof.xobs)) + self.assertTrue(array_equal(y, prof.yobs)) + self.assertTrue(array_equal(ones_like(prof.xobs), prof.dyobs)) + + # Get the ranged profile to make sure its the same + self.assertTrue(array_equal(x, prof.x)) + self.assertTrue(array_equal(y, prof.y)) + self.assertTrue(array_equal(ones_like(prof.xobs), prof.dy)) + + return + def testSetObservedProfile(self): - """Test the setObservedProfile method.""" + """Test the deprecated setObservedProfile method. + + Remove this test when setObservedProfile is removed in 4.0.0. + """ # Make a profile with defined dy x = arange(0, 10, 0.1) @@ -52,7 +89,7 @@ def testSetObservedProfile(self): dy = x prof = self.profile - prof.setObservedProfile(x, y, dy) + prof.set_observed_profile(x, y, dy) self.assertTrue(array_equal(x, prof.xobs)) self.assertTrue(array_equal(y, prof.yobs)) @@ -76,90 +113,134 @@ def testSetObservedProfile(self): return - def testSetCalculationRange(self): - """Test the setCalculationRange method.""" + def test_set_calculation_range(self): + """Test the set_calculation_range method.""" x = arange(2, 9.6, 0.5) y = array(x) dy = array(x) prof = self.profile # Check call before data arrays are present - self.assertRaises(AttributeError, prof.setCalculationRange) - self.assertRaises(AttributeError, prof.setCalculationRange, 0) - self.assertRaises(AttributeError, prof.setCalculationRange, 0, 5) - self.assertRaises(AttributeError, prof.setCalculationRange, 0, 5, 0.2) + self.assertRaises(AttributeError, prof.set_calculation_range) + self.assertRaises(AttributeError, prof.set_calculation_range, 0) + self.assertRaises(AttributeError, prof.set_calculation_range, 0, 5) + self.assertRaises( + AttributeError, prof.set_calculation_range, 0, 5, 0.2 + ) # assign data - prof.setObservedProfile(x, y, dy) + prof.set_observed_profile(x, y, dy) # Test normal execution w/o arguments self.assertTrue(array_equal(x, prof.x)) self.assertTrue(array_equal(y, prof.y)) self.assertTrue(array_equal(dy, prof.dy)) - prof.setCalculationRange() + prof.set_calculation_range() self.assertTrue(array_equal(x, prof.x)) self.assertTrue(array_equal(y, prof.y)) self.assertTrue(array_equal(dy, prof.dy)) # Test a lower bound < xmin - prof.setCalculationRange(xmin=0) + prof.set_calculation_range(xmin=0) self.assertTrue(array_equal(x, prof.x)) self.assertTrue(array_equal(y, prof.y)) self.assertTrue(array_equal(dy, prof.dy)) # Test an upper bound > xmax - prof.setCalculationRange(xmax=100) + prof.set_calculation_range(xmax=100) self.assertTrue(array_equal(x, prof.x)) self.assertTrue(array_equal(y, prof.y)) self.assertTrue(array_equal(dy, prof.dy)) # Test xmin > xmax self.assertRaises( - ValueError, prof.setCalculationRange, xmin=10, xmax=3 + ValueError, prof.set_calculation_range, xmin=10, xmax=3 ) # Test xmax - xmin < dx self.assertRaises( - ValueError, prof.setCalculationRange, xmin=3, xmax=3.9, dx=1.0 + ValueError, prof.set_calculation_range, xmin=3, xmax=3.9, dx=1.0 ) # Test dx <= 0 - self.assertRaises(ValueError, prof.setCalculationRange, dx=0) - self.assertRaises(ValueError, prof.setCalculationRange, dx=-0.000001) + self.assertRaises(ValueError, prof.set_calculation_range, dx=0) + self.assertRaises(ValueError, prof.set_calculation_range, dx=-0.000001) # using string other than 'obs' - self.assertRaises(ValueError, prof.setCalculationRange, xmin="oobs") - self.assertRaises(ValueError, prof.setCalculationRange, xmax="oobs") - self.assertRaises(ValueError, prof.setCalculationRange, dx="oobs") + self.assertRaises(ValueError, prof.set_calculation_range, xmin="oobs") + self.assertRaises(ValueError, prof.set_calculation_range, xmax="oobs") + self.assertRaises(ValueError, prof.set_calculation_range, dx="oobs") # This should be alright - prof.setCalculationRange(3, 5) - prof.setCalculationRange(xmin="obs", xmax=7, dx=0.001) + prof.set_calculation_range(3, 5) + prof.set_calculation_range(xmin="obs", xmax=7, dx=0.001) self.assertEqual(5001, len(prof.x)) self.assertEqual(len(prof.x), len(prof.y)) self.assertEqual(len(prof.x), len(prof.dy)) # Test an internal bound - prof.setCalculationRange(4, 7, dx="obs") + prof.set_calculation_range(4, 7, dx="obs") self.assertTrue(array_equal(prof.x, arange(4, 7.1, 0.5))) self.assertTrue(array_equal(prof.y, arange(4, 7.1, 0.5))) self.assertTrue(array_equal(prof.y, arange(4, 7.1, 0.5))) # test setting only one of the bounds - prof.setCalculationRange(xmin=3) + prof.set_calculation_range(xmin=3) self.assertTrue(array_equal(prof.x, arange(3, 7.1, 0.5))) self.assertTrue(array_equal(prof.x, prof.y)) self.assertTrue(array_equal(prof.x, prof.dy)) - prof.setCalculationRange(xmax=5.1) + prof.set_calculation_range(xmax=5.1) self.assertTrue(array_equal(prof.x, arange(3, 5.1, 0.5))) self.assertTrue(array_equal(prof.x, prof.y)) self.assertTrue(array_equal(prof.x, prof.dy)) - prof.setCalculationRange(dx=1) + prof.set_calculation_range(dx=1) self.assertTrue(array_equal(prof.x, arange(3, 5.1))) self.assertTrue(array_equal(prof.x, prof.y)) self.assertTrue(array_equal(prof.x, prof.dy)) # Test a new grid - prof.setCalculationRange(4.2, 7, 0.3) + prof.set_calculation_range(4.2, 7, 0.3) self.assertTrue(array_equal(prof.x, arange(4.2, 6.901, 0.3))) self.assertTrue(allclose(prof.x, prof.y)) self.assertTrue(allclose(prof.x, prof.dy)) - prof.setCalculationRange(xmin=4.2, xmax=6.001) + prof.set_calculation_range(xmin=4.2, xmax=6.001) self.assertTrue(array_equal(prof.x, arange(4.2, 6.001, 0.3))) # resample on a clipped grid - prof.setCalculationRange(dx=0.5) + prof.set_calculation_range(dx=0.5) self.assertTrue(array_equal(prof.x, arange(4.5, 6.1, 0.5))) return + def testSetCalculationRange(self): + """Test the deprecated setCalculationRange method. + + Remove this test when setCalculationRange is removed in 4.0.0. + """ + x = arange(2, 9.6, 0.5) + y = array(x) + dy = array(x) + prof = self.profile + prof.set_observed_profile(x, y, dy) + # Test normal execution w/o arguments + self.assertTrue(array_equal(x, prof.x)) + self.assertTrue(array_equal(y, prof.y)) + self.assertTrue(array_equal(dy, prof.dy)) + prof.setCalculationRange() + self.assertTrue(array_equal(x, prof.x)) + self.assertTrue(array_equal(y, prof.y)) + self.assertTrue(array_equal(dy, prof.dy)) + return + + def test_set_calculation_points(self): + """Test the set_calculation_points method.""" + prof = self.profile + + x = arange(2, 10.5, 0.5) + y = array(x) + dy = array(x) + + # Test without data + xcalc = arange(3, 12.2, 0.2) + prof.set_calculation_points(xcalc) + self.assertTrue(array_equal(xcalc, prof.x)) + + # Add the data. This should change the bounds of the calculation array. + prof.set_observed_profile(x, y, dy) + self.assertTrue(array_equal(arange(3, 10.1, 0.2), prof.x)) + + return + def testSetCalculationPoints(self): - """Test the setCalculationPoints method.""" + """Test the deprecated setCalculationPoints method. + + Remove this test when setCalculationPoints is removed in 4.0.0. + """ prof = self.profile x = arange(2, 10.5, 0.5) @@ -172,7 +253,7 @@ def testSetCalculationPoints(self): self.assertTrue(array_equal(xcalc, prof.x)) # Add the data. This should change the bounds of the calculation array. - prof.setObservedProfile(x, y, dy) + prof.set_observed_profile(x, y, dy) self.assertTrue(array_equal(arange(3, 10.1, 0.2), prof.x)) return @@ -183,7 +264,7 @@ def test_savetxt(self): self.assertRaises(SrFitError, prof.savetxt, "foo") xobs = arange(-2, 3.01, 0.25) yobs = xobs**2 - prof.setObservedProfile(xobs, yobs) + prof.set_observed_profile(xobs, yobs) prof.ycalc = yobs.copy() fp = io.BytesIO() prof.savetxt(fp) @@ -227,5 +308,20 @@ def _test(p): return +def test_load_parsed_data(parser_datafiles): + """Test the load_parsed_data method.""" + prof = Profile() + parser = ProfileParser() + datafile = parser_datafiles / "four_col.gr" + parser.parse_file(datafile) + prof.load_parsed_data(parser) + expected_xobs = [1.0, 1.1, 1.2] + expected_yobs = [2.0, 2.1, 2.2] + expected_dyobs = [0.2, 0.4, 0.6] + assert prof.xobs.tolist() == expected_xobs + assert prof.yobs.tolist() == expected_yobs + assert prof.dyobs.tolist() == expected_dyobs + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_profilegenerator.py b/tests/test_profilegenerator.py index 14612a93..e4ea0fa3 100644 --- a/tests/test_profilegenerator.py +++ b/tests/test_profilegenerator.py @@ -29,7 +29,7 @@ def setUp(self): self.gen = ProfileGenerator("test") self.profile = Profile() x = arange(0, 10, 0.1) - self.profile.setCalculationPoints(x) + self.profile.set_calculation_points(x) self.gen.set_profile(self.profile) return @@ -48,14 +48,15 @@ def testOperation(self): return def testUpdate(self): - """Update and change the profile to make sure generator is flushed.""" + """Update and change the profile to make sure generator is + flushed.""" gen = self.gen prof = self.profile # Make sure attributes get updated with a change in the calculation # points. x = arange(0, 9, 0.1) - prof.setCalculationPoints(x) + prof.set_calculation_points(x) self.assertTrue(gen._value is None) val = gen.value self.assertTrue(array_equal(x, val)) @@ -68,7 +69,7 @@ def testUpdate(self): # Make sure attributes get updated with a new profile. x = arange(0, 8, 0.1) prof = Profile() - prof.setCalculationPoints(x) + prof.set_calculation_points(x) gen.set_profile(prof) self.assertTrue(gen._value is None) self.assertTrue(array_equal(x, gen.value)) diff --git a/tests/test_profileparser.py b/tests/test_profileparser.py new file mode 100644 index 00000000..e1676b15 --- /dev/null +++ b/tests/test_profileparser.py @@ -0,0 +1,200 @@ +import re +from pathlib import Path + +import pytest + +from diffpy.srfit.exceptions import ParseError +from diffpy.srfit.fitbase.profileparser import ProfileParser + +# UC1: User loads file with all x, y, dx, dy columns in that format +# expected: x, y, dx, dy, and metadata are all read correctly +# UC2: User loads file with x, y, dy columns in that format (dx is missing) +# expected: x, y, dy, and metadata are all read correctly +# UC3: User loads file with x, y columns in that format (dx and dy are missing) +# expected: x, y, and metadata are all read correctly +# UC4: User loads file with x, dx, y, dy columns in that format and specifies +# column_format +# expected: x, y, dx, dy, and metadata are all read correctly +# UC5: User loads file with dy and dx values containing NaN and inf values +# expected: x, y, and metadata are all read correctly and dx and dy are set to +# 0 for all values + +# UC6: User loads file with only one column +# expected: ParseError is raised +# UC7: User loads file with 5 columns +# expected: ParseError is raised +# UC8: User loads file with x, y, and dy but specifies column_format with 4 +# columns +# expected: ParseError is raised +# UC9: User loads file with x, y, dx, and dy but specifies column_format with 5 +# columns +# expected: ParseError is raised +# UC10: User loads file with x, y, dx, and dy but specifies column_format with +# 3 columns +# expected: ParseError is raised +# UC11: User loads file with x, y, dx, and dy but specifies column_format with +# duplicate values +# expected: ParseError is raised + +EXPECTED_META = { + "wavelength": 0.1, + "dataformat": "QA", + "inputfile": "input.iq", + "backgroundfile": "backgroundfile.iq", + "mode": "xray", + "bgscale": 1.0, + "composition": "TiSe2", + "outputtype": "gr", + "qmaxinst": 25.0, + "qmin": 0.1, + "qmax": 25.0, + "rmax": 140.0, + "rmin": 0.0, + "rstep": 0.01, + "rpoly": 0.7, + "inputdir": "/my/data/dir", + "savedir": "/my/save/dir", + "backgroundfilefull": "/my/data/dir/backgroundfile.iq", + "nbanks": 1, + "bank": 0, +} + + +@pytest.mark.parametrize( + "input_file, column_order, expected_x, " + "expected_y, expected_dx, expected_dy", + [ + # UC1: 4-column file (x, y, dx, dy) — all columns present + # expected: x, y, dx, dy, and metadata are all read correctly + ( + Path("four_col.gr"), + None, + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + [0.1, 0.3, 0.5], + [0.2, 0.4, 0.6], + ), + # UC2: 3-column file (x, y, dy) — dx is missing + # expected: x, y, dy, and metadata are all read correctly + ( + Path("three_col.dat"), + None, + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + [0.0, 0.0, 0.0], + [0.2, 0.4, 0.6], + ), + # UC3: 2-column file (x, y) — dx and dy are missing + # expected: x, y, and metadata are all read correctly + ( + Path("two_col.txt"), + None, + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ), + # UC4: 4-column file in (x, dx, y, dy) order with explicit + # column_format + # expected: x, y, dx, dy, and metadata are all read correctly + ( + Path("four_col_reordered.txt"), + ("x", "dx", "y", "dy"), + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + [0.1, 0.3, 0.5], + [0.2, 0.4, 0.6], + ), + # UC5: 4-column file where dx/dy contain NaN and inf values + # expected: x, y, and metadata are read correctly; dx and dy + # are set to 0 + ( + Path("four_col_nan_inf.gr"), + None, + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ), + ], +) +def test_parse_file( + parser_datafiles, + input_file, + column_order, + expected_x, + expected_y, + expected_dx, + expected_dy, +): + parser = ProfileParser() + parser.parse_file(parser_datafiles / input_file, column_order) + actual_x = parser._x.tolist() + actual_y = parser._y.tolist() + actual_dx = parser._dx.tolist() + actual_dy = parser._dy.tolist() + actual_metadata = parser._meta + actual_metadata["filename"] = actual_metadata["filename"].split("/")[-1] + + EXPECTED_META["filename"] = str(input_file).split("/")[-1] + assert actual_x == expected_x + assert actual_y == expected_y + assert actual_dx == expected_dx + assert actual_dy == expected_dy + assert actual_metadata == EXPECTED_META + + +@pytest.mark.parametrize( + "input_file, column_order, msg", + [ + # UC6: Only one column — cannot form x/y pair + # expected: ParseError is raised + ( + "one_col.gr", + None, + "Data block must have at least two columns (x, y).", + ), + # UC7: Five columns — ambiguous, no mapping defined + # expected: ParseError is raised + ("five_col.gr", None, "Expected 2 to 4 columns but found 5."), + # UC8: 3-column file but column_format expects 4 columns + # expected: ParseError is raised + ( + "three_col.dat", + ("x", "y", "dx", "dy"), + "column_format has 4 labels but file contains 3 columns.", + ), + # UC9: 4-column file but column_format expects 5 columns + # expected: ParseError is raised + ( + "four_col.gr", + ("x", "y", "dx", "dy", "extra"), + "column_format has 5 labels but file contains 4 columns.", + ), + # UC10: 4-column file but column_format expects only 3 columns + # expected: ParseError is raised + ( + "four_col.gr", + ("x", "y", "dy"), + "column_format has 3 labels but file contains 4 columns.", + ), + # UC11: column_format contains duplicate column names + # expected: ParseError is raised + ( + "four_col.gr", + ("x", "x", "dx", "dy"), + "column_format cannot contain duplicate labels.", + ), + # UC12: column_format contains invalid column names + ( + "four_col.gr", + ("x", "y", "dx", "invalid"), + "column_format contains invalid label 'invalid'. " + "Valid labels are 'x', 'y', 'dx', and 'dy'.", + ), + ], +) +def test_parse_file_bad(parser_datafiles, input_file, column_order, msg): + parser = ProfileParser() + with pytest.raises(ParseError, match=re.escape(msg)): + parser.parse_file(parser_datafiles / input_file, column_order) diff --git a/tests/test_recipeorganizer.py b/tests/test_recipeorganizer.py index a5db1e1b..08d46dec 100644 --- a/tests/test_recipeorganizer.py +++ b/tests/test_recipeorganizer.py @@ -25,6 +25,7 @@ RecipeContainer, RecipeOrganizer, equationFromString, + get_equation_from_string, ) # ---------------------------------------------------------------------------- @@ -32,8 +33,8 @@ class TestEquationFromString(unittest.TestCase): - def testEquationFromString(self): - """Test the equationFromString method.""" + def test_get_equation_from_string(self): + """Test the get_equation_from_string method.""" p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) @@ -46,7 +47,7 @@ def testEquationFromString(self): factory.registerArgument("p2", p2) # Check usage where all parameters are registered with the factory - eq = equationFromString("p1+p2", factory) + eq = get_equation_from_string("p1+p2", factory) self.assertEqual(2, len(eq.args)) self.assertTrue(p1 in eq.args) @@ -54,7 +55,9 @@ def testEquationFromString(self): self.assertEqual(3, eq()) # Try to use a parameter that is not registered - self.assertRaises(ValueError, equationFromString, "p1+p2+p3", factory) + self.assertRaises( + ValueError, get_equation_from_string, "p1+p2+p3", factory + ) # Pass that argument in the ns dictionary eq = equationFromString("p1+p2+p3", factory, {"p3": p3}) @@ -69,12 +72,16 @@ def testEquationFromString(self): # Pass and use an unregistered parameter self.assertRaises( - ValueError, equationFromString, "p1+p2+p3+p4", factory, {"p3": p3} + ValueError, + get_equation_from_string, + "p1+p2+p3+p4", + factory, + {"p3": p3}, ) # Try to overload a registered parameter self.assertRaises( - ValueError, equationFromString, "p1+p2", factory, {"p2": p4} + ValueError, get_equation_from_string, "p1+p2", factory, {"p2": p4} ) return @@ -259,7 +266,7 @@ def testRemoveParameter(self): self.assertRaises(ValueError, m._remove_parameter, c) return - def testConstrain(self): + def test_constrain_parameter(self): """Test the constrain method.""" p1 = self.m._new_parameter("p1", 1) @@ -268,14 +275,26 @@ def testConstrain(self): self.assertFalse(p1.constrained) self.assertEqual(0, len(self.m._constraints)) - self.m.constrain(p1, "2*p2") + self.m.add_constraint(p1, "2*p2") + + actual_constrained_params = self.m.getConstrainedPars() + actual_constrained_params = [p.name for p in actual_constrained_params] + expected_constrained_params = [p1.name] + + assert actual_constrained_params == expected_constrained_params + + actual_constrained_params = self.m.get_constrained_parmeters() + actual_constrained_params = [p.name for p in actual_constrained_params] + expected_constrained_params = [p1.name] + + assert actual_constrained_params == expected_constrained_params self.assertTrue(p1.constrained) self.assertTrue(p1 in self.m._constraints) self.assertEqual(1, len(self.m._constraints)) - self.assertTrue(self.m.isConstrained(p1)) + self.assertTrue(self.m.is_constrained(p1)) - p2.setValue(10) + p2.set_value(10) self.m._constraints[p1].update() self.assertEqual(20, p1.getValue()) @@ -284,19 +303,39 @@ def testConstrain(self): self.assertRaises(ValueError, self.m.constrain, p1, "2*p2", {"p2": p3}) # Remove the constraint - self.m.unconstrain(p1) + self.m.remove_constraint(p1) self.assertFalse(p1.constrained) self.assertEqual(0, len(self.m._constraints)) self.assertFalse(self.m.isConstrained(p1)) # Try an straight constraint - self.m.constrain(p1, p2) - p2.setValue(7) + self.m.add_constraint(p1, p2) + p2.set_value(7) self.m._constraints[p1].update() self.assertEqual(7, p1.getValue()) + + self.m.clear_all_constraints() + actual_constrained_params = self.m.get_constrained_parmeters() + actual_constrained_params = [p.name for p in actual_constrained_params] + expected_constrained_params = [] + assert actual_constrained_params == expected_constrained_params + + # add constraint back and test the old function name `clearConstraints` + self.m.add_constraint(p1, p2) + actual_constrained_params = self.m.get_constrained_parmeters() + actual_constrained_params = [p.name for p in actual_constrained_params] + expected_constrained_params = [p1.name] + assert actual_constrained_params == expected_constrained_params + + self.m.clearConstraints() + actual_constrained_params = self.m.get_constrained_parmeters() + actual_constrained_params = [p.name for p in actual_constrained_params] + expected_constrained_params = [] + assert actual_constrained_params == expected_constrained_params + return - def testRestrain(self): + def test_add_restraint(self): """Test the restrain method.""" p1 = Parameter("p1", 1) @@ -306,21 +345,23 @@ def testRestrain(self): self.m._eqfactory.registerArgument("p2", p2) self.assertEqual(0, len(self.m._restraints)) - r = self.m.restrain("p1+p2", ub=10) + r = self.m.add_soft_bounds("p1+p2", upper_bound=10) self.assertEqual(1, len(self.m._restraints)) - p2.setValue(10) + p2.set_value(10) self.assertEqual(1, r.penalty()) - self.m.unrestrain(r) + self.m.remove_soft_bounds(r) self.assertEqual(0, len(self.m._restraints)) - r = self.m.restrain(p1, ub=10) + r = self.m.restrain(p1, upper_bound=10) self.assertEqual(1, len(self.m._restraints)) - p1.setValue(11) + p1.set_value(11) self.assertEqual(1, r.penalty()) # Check errors on unregistered parameters self.assertRaises(ValueError, self.m.restrain, "2*p3") - self.assertRaises(ValueError, self.m.restrain, "2*p2", ns={"p2": p3}) + self.assertRaises( + ValueError, self.m.restrain, "2*p2", params={"p2": p3} + ) return def testGetConstraints(self): @@ -341,8 +382,8 @@ def testGetConstraints(self): m2._add_parameter(p3) m2._add_parameter(p4) - self.m.constrain(p1, "p2") - m2.constrain(p3, "p4") + self.m.add_constraint(p1, "p2") + m2.add_constraint(p3, "p4") cons = self.m._get_constraints() self.assertTrue(p1 in cons) @@ -368,8 +409,8 @@ def testGetRestraints(self): m2._add_parameter(p3) m2._add_parameter(p4) - r1 = self.m.restrain("p1 + p2") - r2 = m2.restrain("2*p3 + p4") + r1 = self.m.add_soft_bounds("p1 + p2") + r2 = m2.add_soft_bounds("2*p3 + p4") res = self.m._get_restraints() self.assertTrue(r1 in res) @@ -377,6 +418,51 @@ def testGetRestraints(self): self.assertEqual(2, len(res)) return + def test_register_calculator(self): + + class GCalc(Calculator): + + def __init__(self, name): + Calculator.__init__(self, name) + self.newParameter("A", 1.0) + self.newParameter("center", 0.0) + self.newParameter("width", 0.1) + return + + def __call__(self, x): + A = self.A.getValue() + c = self.center.getValue() + w = self.width.getValue() + return A * numpy.exp(-0.5 * ((x - c) / w) ** 2) + + # End class GCalc + + g = GCalc("g") + + self.m.register_calculator(g) + + x = numpy.arange(0.5, 10, 0.5) + self.m.x.set_value(x) + + self.m.g.center.set_value(3.0) + + self.assertTrue( + numpy.array_equal(numpy.exp(-0.5 * ((x - 3.0) / 0.1) ** 2), g(x)) + ) + + self.m.g.center.set_value(5.0) + + self.assertTrue( + numpy.array_equal(numpy.exp(-0.5 * ((x - 5.0) / 0.1) ** 2), g(x)) + ) + + # Use this in another equation + + eq = self.m.register_string_function("g/x - 1", "pdf") + self.assertTrue(numpy.array_equal(g(x) / x - 1, eq())) + + return + def testRegisterCalculator(self): class GCalc(Calculator): @@ -401,15 +487,15 @@ def __call__(self, x): self.m.registerCalculator(g) x = numpy.arange(0.5, 10, 0.5) - self.m.x.setValue(x) + self.m.x.set_value(x) - self.m.g.center.setValue(3.0) + self.m.g.center.set_value(3.0) self.assertTrue( numpy.array_equal(numpy.exp(-0.5 * ((x - 3.0) / 0.1) ** 2), g(x)) ) - self.m.g.center.setValue(5.0) + self.m.g.center.set_value(5.0) self.assertTrue( numpy.array_equal(numpy.exp(-0.5 * ((x - 5.0) / 0.1) ** 2), g(x)) @@ -417,7 +503,7 @@ def __call__(self, x): # Use this in another equation - eq = self.m.registerStringFunction("g/x - 1", "pdf") + eq = self.m.register_string_function("g/x - 1", "pdf") self.assertTrue(numpy.array_equal(g(x) / x - 1, eq())) return @@ -431,27 +517,27 @@ def g1(A, c, w, x): def g2(A): return A + 1 - eq = self.m.registerFunction(g1, "g") + eq = self.m.register_function(g1, "g") for p in eq.args: self.assertTrue(p in self.m._parameters.values()) x = numpy.arange(0.5, 10, 0.5) - self.m.x.setValue(x) - self.m.A.setValue(1.0) - self.m.c.setValue(3.0) - self.m.w.setValue(0.1) + self.m.x.set_value(x) + self.m.A.set_value(1.0) + self.m.c.set_value(3.0) + self.m.w.set_value(0.1) self.assertTrue( numpy.array_equal(numpy.exp(-0.5 * ((x - 3.0) / 0.1) ** 2), eq()) ) # Use this in another equation - eq2 = self.m.registerStringFunction("g/x - 1", "pdf") + eq2 = self.m.register_string_function("g/x - 1", "pdf") self.assertTrue(numpy.array_equal(eq() / x - 1, eq2())) # Make sure we can swap out "g". - self.m.registerFunction(g2, "g") + self.m.register_function(g2, "g") self.assertAlmostEqual(2.0, eq()) # Try a bound method @@ -463,7 +549,7 @@ def __call__(self): return 4.56 t = temp() - eq = self.m.registerFunction(t.eval, "eval") + eq = self.m.register_function(t.eval, "eval") self.assertAlmostEqual(1.23, eq()) # Now the callable @@ -472,12 +558,12 @@ def __call__(self): return - def testRegisterStringFunction(self): + def test_register_string_function(self): """Test registering string functions in various ways.""" # Make an equation. - eq1 = self.m.registerStringFunction("x**2 + 3", "eq1") - eq1.x.setValue(0) + eq1 = self.m.register_string_function("x**2 + 3", "eq1") + eq1.x.set_value(0) for p in eq1.args: self.assertTrue(p in self.m._parameters.values()) @@ -492,9 +578,9 @@ def testRegisterStringFunction(self): # Use eq1 in some equations # x**2 (x**2 + 3 - 3) - eq2 = self.m.registerStringFunction("eq1 - 3", "eq2") + eq2 = self.m.register_string_function("eq1 - 3", "eq2") # y**2 - eq3 = self.m.registerStringFunction("eq1(y) - 3", "eq3") + eq3 = self.m.register_string_function("eq1(y) - 3", "eq3") # Test these equations. self.assertEqual(0, eq2()) @@ -510,6 +596,13 @@ def testRegisterStringFunction(self): return + def test_release_old_equations(self): + """Verify EquationFactory does not hold temporary equations.""" + self.m._new_parameter("x", 12) + self.assertEqual(36, self.m.evaluate_equation("3 * x")) + self.assertEqual(0, len(self.m._eqfactory.equations)) + return + def test_releaseOldEquations(self): """Verify EquationFactory does not hold temporary equations.""" self.m._new_parameter("x", 12) @@ -539,14 +632,14 @@ def capture_show(*args, **kwargs): assert "Constraints" not in lines1 assert "Restraints" not in lines1 organizer._new_parameter("z", 7) - organizer.constrain("y", "3 * z") + organizer.add_constraint("y", "3 * z") out2 = capture_show() lines2 = out2.strip().split("\n") assert 9 == len(lines2) assert "Parameters" in lines2 assert "Constraints" in lines2 assert "Restraints" not in lines2 - organizer.restrain("z", lb=2, ub=3, sig=0.001) + organizer.add_soft_bounds("z", lower_bound=2, upper_bound=3, sig=0.001) out3 = capture_show() lines3 = out3.strip().split("\n") assert 13 == len(lines3) diff --git a/tests/test_restraint.py b/tests/test_restraint.py index 68673679..befae689 100644 --- a/tests/test_restraint.py +++ b/tests/test_restraint.py @@ -18,7 +18,7 @@ from diffpy.srfit.equation.builder import EquationFactory from diffpy.srfit.fitbase.parameter import Parameter -from diffpy.srfit.fitbase.recipeorganizer import equationFromString +from diffpy.srfit.fitbase.recipeorganizer import get_equation_from_string from diffpy.srfit.fitbase.restraint import Restraint @@ -36,24 +36,24 @@ def testRestraint(self): factory.registerArgument("p2", p2) # Restrain 1 < p1 + p2 < 5 - eq = equationFromString("p1 + p2", factory) + eq = get_equation_from_string("p1 + p2", factory) r = Restraint(eq, 1, 5) # This should have no penalty - p1.setValue(1) - p2.setValue(1) + p1.set_value(1) + p2.set_value(1) self.assertEqual(0, r.penalty()) # Make p1 + p2 = 0 # This should have a penalty of 1*(1 - 0)**2 = 1 - p1.setValue(-1) - p2.setValue(1) + p1.set_value(-1) + p2.set_value(1) self.assertEqual(1, r.penalty()) # Make p1 + p2 = 8 # This should have a penalty of 1*(8 - 5)**2 = 9 - p1.setValue(4) - p2.setValue(4) + p1.set_value(4) + p2.set_value(4) self.assertEqual(9, r.penalty()) # Set the chi^2 to get a dynamic penalty @@ -63,8 +63,8 @@ def testRestraint(self): # Make a really large number to check the upper bound import numpy - r.ub = numpy.inf - p1.setValue(1e100) + r.upper_bound = numpy.inf + p1.set_value(1e100) self.assertEqual(0, r.penalty()) return diff --git a/tests/test_sas.py b/tests/test_sas.py index 59cb14c0..33dab04a 100644 --- a/tests/test_sas.py +++ b/tests/test_sas.py @@ -31,7 +31,7 @@ def testParser(sas_available, datafile): data = datafile("sas_ascii_test_1.txt") parser = SASParser() parser.parseFile(data) - x, y, dx, dy = parser.getData() + x, y, dx, dy = parser.get_data() testx = numpy.array( [ 0.002618, @@ -123,10 +123,10 @@ def test_generator(sas_available): par = gen.get(pname) assert defval == par.getValue() # Test setting values - par.setValue(1.0) + par.set_value(1.0) assert 1.0 == par.getValue() assert 1.0 == model.getParam(pname) - par.setValue(defval) + par.set_value(defval) assert defval == par.getValue() assert defval == model.getParam(pname) diff --git a/tests/test_sgconstraints.py b/tests/test_sgconstraints.py index 32d6afe4..211d61be 100644 --- a/tests/test_sgconstraints.py +++ b/tests/test_sgconstraints.py @@ -87,20 +87,20 @@ def test_ObjCryst_constrain_space_group(pyobjcryst_available): # Make sure we can't constrain these with pytest.raises(ValueError): - mn.constrain(mn.x, "y") + mn.add_constraint(mn.x, "y") with pytest.raises(ValueError): - mn.constrain(mn.y, "z") + mn.add_constraint(mn.y, "z") with pytest.raises(ValueError): - mn.constrain(mn.z, "x") + mn.add_constraint(mn.z, "x") # Nor can we make them into variables from diffpy.srfit.fitbase.fitrecipe import FitRecipe f = FitRecipe() with pytest.raises(ValueError): - f.addVar(mn.x) + f.add_variable(mn.x) return @@ -179,7 +179,8 @@ def _alltests(par): def test_constrain_as_space_group_args(pyobjcryst_available, datafile): - """Test the arguments processing of constrainAsSpaceGroup function.""" + """Test the arguments processing of constrainAsSpaceGroup + function.""" if not pyobjcryst_available: pytest.skip("pyobjcrysta package not available") diff --git a/tests/test_speed.py b/tests/test_speed.py index 6bc75014..e859e4e0 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -17,14 +17,23 @@ import random import numpy +import pytest import diffpy.srfit.equation.literals as literals import diffpy.srfit.equation.visitors as visitors +pytestmark = pytest.mark.skip( + reason=( + "This is a performance benchmark test, " + "not a unit test. Comment out this line " + "to run performance testing." + ) +) + x = numpy.arange(0, 20, 0.05) -def makeLazyEquation(make_args): +def make_lazy_equation(make_args): """Make a lazy equation and see how fast it is.""" # Make some variables @@ -52,19 +61,19 @@ def makeLazyEquation(make_args): mult2.addLiteral(pow) mult2.addLiteral(exp) - v2.setValue(x) - v3.setValue(50 * x) - v5.setValue(2.11) - v6.setValue(numpy.e) + v2.set_value(x) + v3.set_value(50 * x) + v5.set_value(2.11) + v6.set_value(numpy.e) evaluator = visitors.Evaluator() def _f(a, b, c, d, e): - v1.setValue(a) - v4.setValue(b) - v5.setValue(c) - v6.setValue(d) - v7.setValue(e) + v1.set_value(a) + v4.set_value(b) + v5.set_value(c) + v6.set_value(d) + v7.set_value(e) mult2.identify(evaluator) evaluator.clicker.click() return evaluator.value @@ -72,7 +81,7 @@ def _f(a, b, c, d, e): return _f -def makeEquation1(): +def make_equation1(): """Make the same equation as the lazy one.""" y = 50 * x @@ -83,7 +92,7 @@ def _f(a, b, c, d, e): return _f -def timeFunction(f, *args, **kw): +def time_function(f, *args, **kw): """Time a function in ms.""" import time @@ -93,9 +102,9 @@ def timeFunction(f, *args, **kw): return (t2 - t1) * 1000 -def speedTest1(): - f1 = makeLazyEquation() - f2 = makeEquation1() +def test_speed1(): + f1 = make_lazy_equation() + f2 = make_equation1() args = [3.1, 8.19973123410, 2.1, numpy.e, numpy.pi] @@ -104,8 +113,8 @@ def speedTest1(): for i in range(len(args)): args[i] = 10 * random.random() print("Changing argument %i" % (i + 1)) - t1 = timeFunction(f1, *args) - t2 = timeFunction(f2, *args) + t1 = time_function(f1, *args) + t2 = time_function(f2, *args) total1 += t1 total2 += t2 print("lazy", t1) @@ -117,7 +126,7 @@ def speedTest1(): print("Ratio (lazy/regular)", total1 / total2) -def speedTest2(mutate=2): +def test_speed2(mutate=2): from diffpy.srfit.equation.builder import EquationFactory @@ -133,18 +142,18 @@ def speedTest2(mutate=2): """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) - eq.qsig.setValue(qsig) - eq.sigma1.setValue(sigma) - eq.sigma2.setValue(sigma) - eq.A0.setValue(1.0) - eq.b1.setValue(0) - eq.b2.setValue(1) - eq.b3.setValue(2.0) - eq.b4.setValue(2.0) - eq.b5.setValue(2.0) - eq.b6.setValue(2.0) - eq.b7.setValue(2.0) - eq.b8.setValue(2.0) + eq.qsig.set_value(qsig) + eq.sigma1.set_value(sigma) + eq.sigma2.set_value(sigma) + eq.A0.set_value(1.0) + eq.b1.set_value(0) + eq.b2.set_value(1) + eq.b3.set_value(2.0) + eq.b4.set_value(2.0) + eq.b5.set_value(2.0) + eq.b6.set_value(2.0) + eq.b7.set_value(2.0) + eq.b8.set_value(2.0) from numpy import exp, polyval @@ -176,8 +185,8 @@ def f(A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8): args[idx] = random.random() # Time the different functions with these arguments - tnpy += timeFunction(f, *args) - teq += timeFunction(eq, *args) + tnpy += time_function(f, *args) + teq += time_function(eq, *args) print( "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) @@ -189,7 +198,7 @@ def f(A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8): return -def speedTest3(mutate=2): +def test_speed3(mutate=2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). @@ -211,18 +220,18 @@ def speedTest3(mutate=2): """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) - eq.qsig.setValue(qsig) - eq.sigma1.setValue(sigma) - eq.sigma2.setValue(sigma) - eq.A0.setValue(1.0) - eq.b1.setValue(0) - eq.b2.setValue(1) - eq.b3.setValue(2.0) - eq.b4.setValue(2.0) - eq.b5.setValue(2.0) - eq.b6.setValue(2.0) - eq.b7.setValue(2.0) - eq.b8.setValue(2.0) + eq.qsig.set_value(qsig) + eq.sigma1.set_value(sigma) + eq.sigma2.set_value(sigma) + eq.A0.set_value(1.0) + eq.b1.set_value(0) + eq.b2.set_value(1) + eq.b3.set_value(2.0) + eq.b4.set_value(2.0) + eq.b5.set_value(2.0) + eq.b6.set_value(2.0) + eq.b7.set_value(2.0) + eq.b8.set_value(2.0) from numpy import polyval from sympy import exp, lambdify, var @@ -265,8 +274,8 @@ def speedTest3(mutate=2): args[idx] = random.random() # Time the different functions with these arguments - teq += timeFunction(eq, *(args[:-1])) - tnpy += timeFunction(f, *args) + teq += time_function(eq, *(args[:-1])) + tnpy += time_function(f, *args) print( "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) @@ -278,7 +287,7 @@ def speedTest3(mutate=2): return -def speedTest4(mutate=2): +def test_speed4(mutate=2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). @@ -310,7 +319,7 @@ def speedTest4(mutate=2): teq = 0 # Randomly change variables numargs = len(eq.args) - choices = range(numargs) + choices = list(range(numargs)) args = [1.0] * (len(eq.args)) args.append(x) @@ -329,8 +338,8 @@ def speedTest4(mutate=2): args[idx] = random.random() # Time the different functions with these arguments - teq += timeFunction(eq, *(args[:-1])) - tnpy += timeFunction(f, *args) + teq += time_function(eq, *(args[:-1])) + tnpy += time_function(f, *args) print( "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) @@ -342,7 +351,7 @@ def speedTest4(mutate=2): return -def weightedTest(mutate=2): +def test_weighted(mutate=2): """Show the benefits of a properly balanced equation tree.""" from diffpy.srfit.equation.builder import EquationFactory @@ -357,14 +366,14 @@ def weightedTest(mutate=2): factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) - eq.b1.setValue(0) - eq.b2.setValue(1) - eq.b3.setValue(2.0) - eq.b4.setValue(2.0) - eq.b5.setValue(2.0) - eq.b6.setValue(2.0) - eq.b7.setValue(2.0) - eq.b8.setValue(2.0) + eq.b1.set_value(0) + eq.b2.set_value(1) + eq.b3.set_value(2.0) + eq.b4.set_value(2.0) + eq.b5.set_value(2.0) + eq.b6.set_value(2.0) + eq.b7.set_value(2.0) + eq.b8.set_value(2.0) # scale = visitors.NodeWeigher() # eq.root.identify(scale) @@ -379,7 +388,7 @@ def f(b1, b2, b3, b4, b5, b6, b7, b8): teq = 0 # Randomly change variables numargs = len(eq.args) - choices = range(numargs) + choices = list(range(numargs)) args = [0.1] * numargs # The call-loop @@ -399,8 +408,8 @@ def f(b1, b2, b3, b4, b5, b6, b7, b8): # print(args) # Time the different functions with these arguments - teq += timeFunction(eq, *args) - tnpy += timeFunction(f, *args) + teq += time_function(eq, *args) + tnpy += time_function(f, *args) print( "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) @@ -412,9 +421,9 @@ def f(b1, b2, b3, b4, b5, b6, b7, b8): return -def profileTest(): +def test_profile(): - from diffpy.srfit.builder import EquationFactory + from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() @@ -426,18 +435,18 @@ def profileTest(): factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) - eq.b1.setValue(0) - eq.b2.setValue(1) - eq.b3.setValue(2.0) - eq.b4.setValue(2.0) - eq.b5.setValue(2.0) - eq.b6.setValue(2.0) - eq.b7.setValue(2.0) - eq.b8.setValue(2.0) + eq.b1.set_value(0) + eq.b2.set_value(1) + eq.b3.set_value(2.0) + eq.b4.set_value(2.0) + eq.b5.set_value(2.0) + eq.b6.set_value(2.0) + eq.b7.set_value(2.0) + eq.b8.set_value(2.0) mutate = 8 numargs = len(eq.args) - choices = range(numargs) + choices = list(range(numargs)) args = [0.1] * numargs # The call-loop diff --git a/tests/test_visitors.py b/tests/test_visitors.py index 546edaaa..9ae8698d 100644 --- a/tests/test_visitors.py +++ b/tests/test_visitors.py @@ -117,10 +117,10 @@ def testSimpleFunction(self): # Set the values of the variables. # The equation should evaluate to (1+3)*(4-2) = 8 - v1.setValue(1) - v2.setValue(2) - v3.setValue(3) - v4.setValue(4) + v1.set_value(1) + v2.set_value(2) + v3.set_value(3) + v4.set_value(4) # now get the args args = visitors.getArgs(mult) @@ -170,11 +170,11 @@ def testSimpleFunction(self): # Set the values of the variables. # The equation should evaluate to (1+3)*(4-2) = 8 - v1.setValue(1) - v2.setValue(2) - v3.setValue(3) - v4.setValue(4) - v5.setValue(5) + v1.set_value(1) + v2.set_value(2) + v3.set_value(3) + v4.set_value(4) + v5.set_value(5) # Evaluate assert 8 == mult.value diff --git a/tests/test_weakrefcallable.py b/tests/test_weakrefcallable.py index 5d44d18b..688bc563 100644 --- a/tests/test_weakrefcallable.py +++ b/tests/test_weakrefcallable.py @@ -14,7 +14,6 @@ ############################################################################## """Unit tests for the weakrefcallable module.""" - import pickle import unittest @@ -29,7 +28,7 @@ class TestWeakBoundMethod(unittest.TestCase): def setUp(self): self.f = FitContribution("f") - self.f.setEquation("7") + self.f.set_equation("7") self.w = weak_ref(self.f._eq._flush, fallback=_fallback_example) return @@ -112,14 +111,14 @@ def test_observable_deregistration(self): """Check if Observable drops dead Observer.""" f = self.f x = f.newParameter("x", 5) - f.setEquation("3 * x") + f.set_equation("3 * x") self.assertEqual(15, f.evaluate()) self.assertEqual(15, f._eq._value) # get one of the observer callables that are associated with f xof = next(iter(x._observers)) self.assertTrue(isinstance(xof, WeakBoundMethod)) # changing value of x should reset f._eq - x.setValue(x.value + 1) + x.set_value(x.value + 1) self.assertTrue(None is f._eq._value) self.assertEqual(18, f.evaluate()) # deallocate f now @@ -127,7 +126,7 @@ def test_observable_deregistration(self): self.assertTrue(xof in x._observers) # since f does not exist anymore, the next notification call # should drop the associated observer. - x.setValue(x.value + 1) + x.set_value(x.value + 1) self.assertEqual(0, len(x._observers)) return diff --git a/tests/testdata/si-q27r60-xray.gr b/tests/testdata/si-q27r60-xray.gr index e25d5795..a0f208ad 100644 --- a/tests/testdata/si-q27r60-xray.gr +++ b/tests/testdata/si-q27r60-xray.gr @@ -9,7 +9,9 @@ sourcedir=C:\Program Files\PDFgetX2\ logfile=.pdfgetx2.log quiet=0 debug=0 autosave_isa=1 savefilenamebase=si325_mesh_300k_nor_4-8 iqfilesurfix=.iq sqfilesurfix=.sq fqfilesurfix=.fq grfilesurfix=.gr - +stype = X +qmax = 27 +temperature = 300 ##### DataFileFormat datatype=1 (0:SPEC, 1:CHI, 2:nxm column, 3:unknown) num_skiplines=3 comment_id=# delimiter=