From 72b08259dd99b33745f9cd0fe5a3f05339265cc6 Mon Sep 17 00:00:00 2001 From: saal Date: Tue, 23 Jul 2024 13:35:08 -0700 Subject: [PATCH 1/5] Updated structure to match MSCA packaging --- .github/workflows/build.yml | 25 ++++++++ .github/workflows/deploy.yml | 59 +++++++++++++++++++ .gitignore | 11 +--- .pre-commit-config.yaml | 17 ++++++ .vscode/settings.json | 10 ++++ CODE_OF_CONDUCT.md | 111 +++++++++++++++++++++++++++++++++++ LICENSE | 4 +- README.md | 8 +++ pyproject.toml | 32 ++++++++++ ruff.toml | 8 +++ setup.py | 49 ---------------- 11 files changed, 273 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 pyproject.toml create mode 100644 ruff.toml delete mode 100644 setup.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a16e764 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: build + +on: + push: + branches: + - "*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: python -m pip install .[test] --upgrade pip + - name: Test with pytest + run: pytest --cov=./ --cov-report=xml + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: ihmeuw-msca/SFMA diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b86a85d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,59 @@ +name: deploy + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + pypi: + if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: python -m pip install build . --upgrade pip + - name: Build package distribution + run: python -m build --sdist --wheel --outdir dist/ . + - name: Publish package distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip_existing: true + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + + docs: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: python -m pip install .[docs] --upgrade pip + - name: Generate API docs & Build sphinx documentation + run: | + git fetch --tags + cd docs + python build.py + cd .. + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './docs/pages' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index adf9ba8..9fe17bc 100644 --- a/.gitignore +++ b/.gitignore @@ -126,13 +126,4 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ - -# pycharm -.idea/ - -# vscode -.vscode/ - -# mac store -.DS_Store/ +.pyre/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9db7ff8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.2 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 + hooks: + - id: mypy + files: ^src diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5136375 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + }, + "editor.rulers": [80], + }, +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a4e452b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,111 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, +or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official e-mail address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at +[INSERT CONTACT METHOD]. 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. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org + +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/LICENSE b/LICENSE index aba3053..eb87c47 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2021, IHME Math Sciences +Copyright (c) 2019-2024, IHME Math Sciences All rights reserved. Redistribution and use in source and binary forms, with or without @@ -22,4 +22,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md index 04fd122..c0d3a26 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +[![license](https://img.shields.io/pypi/l/SFMA)](https://github.com/ihmeuw-msca/SFMA/blob/main/LICENSE) +[![version](https://img.shields.io/pypi/v/SFMA)](https://pypi.org/project/SFMA) +[![build](https://img.shields.io/github/actions/workflow/status/ihmeuw-msca/SFMA/build.yml?branch=main)](https://github.com/ihmeuw-msca/SFMA/actions) +[![docs](https://img.shields.io/badge/docs-here-green)](https://ihmeuw-msca.github.io/SFMA) +[![codecov](https://img.shields.io/codecov/c/github/ihmeuw-msca/SFMA)](https://codecov.io/gh/ihmeuw-msca/SFMA) +[![codacy](https://img.shields.io/codacy/grade/ae72a07785f5469eac234d1f6bdf555f)](https://app.codacy.com/gh/ihmeuw-msca/SFMA/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) + + # SFMA Stochastic Frontier Meta-Analysis diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c22e4bb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "SFMA" +version = "0.1.0" +description = "Python package template" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "BSD-2-Clause" } +authors = [ + { name = "IHME Math Sciences", email = "ihme.math.sciences@gmail.com" }, +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", +] +dependencies = [ + "anml", + "matplotlib", + "msca", + "xspline==0.0.7", +] + +[project.optional-dependencies] +test = ["pytest", "pytest-mock"] +docs = ["sphinx", "sphinx-autodoc-typehints", "furo"] + +[project.urls] +github = "https://github.com/ihmeuw-msca/SFMA" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..ecd7188 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,8 @@ +line-length = 80 +src = ["src"] + +[format] +docstring-code-format = true + +[lint.pydocstyle] +convention = "numpy" \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 0120b3b..0000000 --- a/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -from pathlib import Path - -from setuptools import find_packages, setup - -if __name__ == "__main__": - base_dir = Path(__file__).parent - src_dir = base_dir/"src"/"sfma" - - about = {} - with (src_dir / "__about__.py").open() as f: - exec(f.read(), about) - - with (base_dir/"README.md").open() as f: - long_description = f.read() - - install_requirements = [ - "anml", - "matplotlib", - "msca", - ] - - test_requirements = [ - "pytest", - "pytest-mock", - ] - - doc_requirements = [] - - setup( - name=about["__title__"], - version=about["__version__"], - description=about["__summary__"], - long_description=long_description, - license=about["__license__"], - url=about["__uri__"], - author=about["__author__"], - author_email=about["__email__"], - package_dir={"": "src"}, - packages=find_packages(where="src"), - include_package_data=True, - install_requires=install_requirements, - tests_require=test_requirements, - extras_require={ - "docs": doc_requirements, - "test": test_requirements, - "dev": doc_requirements + test_requirements - }, - zip_safe=False, - ) From c2a7277ab0cfb9e65db9ebed4350e65d328084a9 Mon Sep 17 00:00:00 2001 From: saal Date: Tue, 23 Jul 2024 13:43:06 -0700 Subject: [PATCH 2/5] Ruff formatted --- README.md | 7 +- notebooks/helpers.py | 24 ++++-- src/sfma/__about__.py | 10 ++- src/sfma/data.py | 2 - src/sfma/model.py | 188 +++++++++++++++++++++++------------------- tests/test_model.py | 41 +++++---- 6 files changed, 155 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index c0d3a26..2c7732c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -[![license](https://img.shields.io/pypi/l/SFMA)](https://github.com/ihmeuw-msca/SFMA/blob/main/LICENSE) -[![version](https://img.shields.io/pypi/v/SFMA)](https://pypi.org/project/SFMA) -[![build](https://img.shields.io/github/actions/workflow/status/ihmeuw-msca/SFMA/build.yml?branch=main)](https://github.com/ihmeuw-msca/SFMA/actions) +[![PyPI](https://img.shields.io/pypi/l/SFMA)](https://pypi.org/project/SFMA/) +![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ihmeuw-msca/SFMA/build.yml) +[![GitHub](https://img.shields.io/github/license/ihmeuw-msca/SFMA)](./LICENSE) [![docs](https://img.shields.io/badge/docs-here-green)](https://ihmeuw-msca.github.io/SFMA) [![codecov](https://img.shields.io/codecov/c/github/ihmeuw-msca/SFMA)](https://codecov.io/gh/ihmeuw-msca/SFMA) [![codacy](https://img.shields.io/codacy/grade/ae72a07785f5469eac234d1f6bdf555f)](https://app.codacy.com/gh/ihmeuw-msca/SFMA/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) diff --git a/notebooks/helpers.py b/notebooks/helpers.py index e25ce9f..7d03741 100644 --- a/notebooks/helpers.py +++ b/notebooks/helpers.py @@ -5,8 +5,16 @@ class Simulator: - def __init__(self, nu: int, gamma: int, sigma_min: float, sigma_max: float, - x: callable, func: callable, ineff_dist: str = 'half-normal'): + def __init__( + self, + nu: int, + gamma: int, + sigma_min: float, + sigma_max: float, + x: callable, + func: callable, + ineff_dist: str = "half-normal", + ): """ Simulation class for stochastic frontier meta-analysis. @@ -31,17 +39,21 @@ def __init__(self, nu: int, gamma: int, sigma_min: float, sigma_max: float, self.x = x self.func = func - if ineff_dist == 'half-normal': + if ineff_dist == "half-normal": self.rvs = halfnorm.rvs - elif ineff_dist == 'exponential': + elif ineff_dist == "exponential": self.rvs = expon.rvs else: - raise RuntimeError("Inefficiency distribution must be half-normal or exponential") + raise RuntimeError( + "Inefficiency distribution must be half-normal or exponential" + ) def simulate(self, n: int = 1, seed=None, **kwargs): if seed is not None: np.random.seed(seed) - sigma = stats.uniform.rvs(loc=self.sigma_min, scale=self.sigma_max, size=n) + sigma = stats.uniform.rvs( + loc=self.sigma_min, scale=self.sigma_max, size=n + ) epsilon = stats.norm.rvs(loc=0, scale=sigma, size=n) us = stats.norm.rvs(loc=0, scale=self.gamma, size=n) diff --git a/src/sfma/__about__.py b/src/sfma/__about__.py index 5656795..71ae65e 100644 --- a/src/sfma/__about__.py +++ b/src/sfma/__about__.py @@ -1,8 +1,14 @@ import datetime __all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", ] __title__ = "sfma" diff --git a/src/sfma/data.py b/src/sfma/data.py index a28e038..5eed744 100644 --- a/src/sfma/data.py +++ b/src/sfma/data.py @@ -5,14 +5,12 @@ class NonNegative(Validator): - def __call__(self, key: str, value: NDArray): if (value < 0).any(): raise ValueError(f"Column '{key}' contains negative numbers.") class Data(DataPrototype): - def __init__(self, obs: str, obs_se: str): obs = Component(obs, [NoNans()]) obs_se = Component(obs_se, [NoNans(), NonNegative()], default_value=1.0) diff --git a/src/sfma/model.py b/src/sfma/model.py index 9c6d183..b33088a 100644 --- a/src/sfma/model.py +++ b/src/sfma/model.py @@ -1,6 +1,7 @@ """ Model class with all information to fit and predict the frontier. """ + from operator import attrgetter from typing import Dict, List, Optional @@ -38,12 +39,14 @@ class SFMAModel: data = property(attrgetter("_data")) variables = property(attrgetter("_variables")) - def __init__(self, - data: Data, - variables: List[Variable], - include_ie: bool = True, - include_re: bool = False, - df: Optional[DataFrame] = None): + def __init__( + self, + data: Data, + variables: List[Variable], + include_ie: bool = True, + include_re: bool = False, + df: Optional[DataFrame] = None, + ): self.data = data self.parameter = Parameter(variables) self.include_ie = include_ie @@ -66,8 +69,9 @@ def __init__(self, @data.setter def data(self, data: Data): if not isinstance(data, Data): - raise TypeError(f"{type(self).__name__}.data must be instance of " - "Data.") + raise TypeError( + f"{type(self).__name__}.data must be instance of " "Data." + ) self._data = data def get_beta_dict(self) -> Dict[str, NDArray]: @@ -100,17 +104,24 @@ def attach(self, df: DataFrame): cmat = cmat / scale[:, np.newaxis] cvec = cvec / scale - self.cmat = asmatrix(np.vstack([ - -cmat[~np.isneginf(cvec[0])], cmat[~np.isposinf(cvec[1])] - ])) - self.cvec = np.hstack([ - -cvec[0][~np.isneginf(cvec[0])], cvec[1][~np.isposinf(cvec[1])] - ]) - - def _objective(self, - beta: Optional[NDArray] = None, - eta: Optional[float] = None, - gamma: Optional[float] = None) -> NDArray: + self.cmat = asmatrix( + np.vstack( + [-cmat[~np.isneginf(cvec[0])], cmat[~np.isposinf(cvec[1])]] + ) + ) + self.cvec = np.hstack( + [ + -cvec[0][~np.isneginf(cvec[0])], + cvec[1][~np.isposinf(cvec[1])], + ] + ) + + def _objective( + self, + beta: Optional[NDArray] = None, + eta: Optional[float] = None, + gamma: Optional[float] = None, + ) -> NDArray: """Objective function for each data point. Parameters @@ -134,9 +145,9 @@ def _objective(self, r = self.data.obs.value - self.mat.dot(beta) t = self.data.obs_se.value**2 + gamma v = t + eta - z = np.sqrt(eta)/np.sqrt(2*v*t) + z = np.sqrt(eta) / np.sqrt(2 * v * t) - return 0.5*(r**2/v) + 0.5*np.log(2*np.pi*v) - logerfc(z*r) + return 0.5 * (r**2 / v) + 0.5 * np.log(2 * np.pi * v) - logerfc(z * r) def objective_beta(self, beta: NDArray) -> float: """Objective value with respect to beta. @@ -151,8 +162,9 @@ def objective_beta(self, beta: NDArray) -> float: float Objective value. """ - return self.weights.dot(self._objective(beta=beta)) + \ - self.parameter.prior_objective(beta) + return self.weights.dot( + self._objective(beta=beta) + ) + self.parameter.prior_objective(beta) def gradient_beta(self, beta: NDArray) -> NDArray: """Gradient vector with respect to beta. @@ -170,13 +182,14 @@ def gradient_beta(self, beta: NDArray) -> NDArray: r = self.data.obs.value - self.mat.dot(beta) t = self.data.obs_se.value**2 + self.gamma v = t + self.eta - z = np.sqrt(self.eta)/np.sqrt(2*v*t) + z = np.sqrt(self.eta) / np.sqrt(2 * v * t) - dlr = -r/v - dzr = logerfc(z*r, order=1) + dlr = -r / v + dzr = logerfc(z * r, order=1) - return self.mat.T.dot(self.weights*(dlr + dzr*z)) + \ - self.parameter.prior_gradient(beta) + return self.mat.T.dot( + self.weights * (dlr + dzr * z) + ) + self.parameter.prior_gradient(beta) def hessian_beta(self, beta: NDArray) -> NDArray: """Hessian matrix with respect to beta. @@ -196,13 +209,14 @@ def hessian_beta(self, beta: NDArray) -> NDArray: r = self.data.obs.value - self.mat.dot(beta) t = self.data.obs_se.value**2 + self.gamma v = t + self.eta - z = np.sqrt(self.eta)/np.sqrt(2*v*t) + z = np.sqrt(self.eta) / np.sqrt(2 * v * t) - d2lr = 1/v - d2zr = logerfc(z*r, order=2) + d2lr = 1 / v + d2zr = logerfc(z * r, order=2) - return (self.mat.T*(w*(d2lr - d2zr*z**2))).dot(self.mat) + \ - self.parameter.prior_hessian(beta) + return (self.mat.T * (w * (d2lr - d2zr * z**2))).dot( + self.mat + ) + self.parameter.prior_hessian(beta) def objective_eta(self, eta: float) -> float: """Objective value with respect to eta. @@ -235,13 +249,13 @@ def gradient_eta(self, eta: float) -> float: r = self.data.obs.value - self.mat.dot(self.beta) t = self.data.obs_se.value**2 + self.gamma v = t + eta - z = np.sqrt(eta)/np.sqrt(2*v*t) + z = np.sqrt(eta) / np.sqrt(2 * v * t) - dzr = logerfc(z*r, order=1) - dlv = 0.5*(-r**2/v**2 + 1/v) - dze = 0.5*z*(1/eta - 1/v) + dzr = logerfc(z * r, order=1) + dlv = 0.5 * (-(r**2) / v**2 + 1 / v) + dze = 0.5 * z * (1 / eta - 1 / v) - return self.weights.dot(dlv - dzr*r*dze) + return self.weights.dot(dlv - dzr * r * dze) def objective_gamma(self, gamma: float) -> float: """Objective value with respect to gamma. @@ -274,17 +288,15 @@ def gradient_gamma(self, gamma: float) -> float: r = self.data.obs.value - self.mat.dot(self.beta) t = self.data.obs_se.value**2 + gamma v = t + self.eta - z = np.sqrt(self.eta)/np.sqrt(2*v*t) + z = np.sqrt(self.eta) / np.sqrt(2 * v * t) - dzr = logerfc(z*r, order=1) - dlv = 0.5*(-r**2/v**2 + 1/v) - dzg = 0.5*z*(-1/t - 1/v) + dzr = logerfc(z * r, order=1) + dlv = 0.5 * (-(r**2) / v**2 + 1 / v) + dzg = 0.5 * z * (-1 / t - 1 / v) - return self.weights.dot(dlv - dzr*r*dzg) + return self.weights.dot(dlv - dzr * r * dzg) - def _fit_beta(self, - beta0: Optional[NDArray] = None, - **options): + def _fit_beta(self, beta0: Optional[NDArray] = None, **options): """Partially minimize beta. Parameters @@ -310,7 +322,7 @@ def _fit_beta(self, self.gradient_beta, self.hessian_beta, self.cmat, - self.cvec + self.cvec, ) result = solver.minimize(beta0, **options) self.beta = result.x @@ -319,25 +331,25 @@ def _fit_eta(self, **options): """Paratial minimize eta.""" options = {"method": "bounded", "bounds": [0.0, 1.0], **options} if self.include_ie: - result = minimize_scalar(self.objective_eta, - **options) + result = minimize_scalar(self.objective_eta, **options) self.eta = result.x def _fit_gamma(self, **options): """Partial minimize gamma""" options = {"method": "bounded", "bounds": [0.0, 1.0], **options} if self.include_re: - result = minimize_scalar(self.objective_gamma, - **options) + result = minimize_scalar(self.objective_gamma, **options) self.gamma = result.x - def _fit(self, - max_iter: int = 20, - tol: float = 1e-2, - verbose: bool = False, - beta_options: Optional[Dict] = None, - eta_options: Optional[Dict] = None, - gamma_options: Optional[Dict] = None): + def _fit( + self, + max_iter: int = 20, + tol: float = 1e-2, + verbose: bool = False, + beta_options: Optional[Dict] = None, + eta_options: Optional[Dict] = None, + gamma_options: Optional[Dict] = None, + ): """Model fitting function. Parameters @@ -364,9 +376,11 @@ def _fit(self, x = np.hstack([self.beta, self.eta, self.gamma]) if verbose: - print(f"{counter=:3d}, obj={self.objective_beta(self.beta):.2e}, " - f"eta={self.eta:.2e}, gamma={self.gamma:.2e}, " - f"error={error:.2e}") + print( + f"{counter=:3d}, obj={self.objective_beta(self.beta):.2e}, " + f"eta={self.eta:.2e}, gamma={self.gamma:.2e}, " + f"error={error:.2e}" + ) while error >= tol and counter < max_iter: counter += 1 @@ -380,18 +394,22 @@ def _fit(self, x = x_new if verbose: - print(f"{counter=:3d}, " - f"obj={self.objective_beta(self.beta):.2e}, " - f"eta={self.eta:.2e}, gamma={self.gamma:.2e} " - f"error={error:.2e}") - - def fit(self, - outlier_pct: float = 0.0, - trim_max_iter: int = 5, - trim_step_size: float = 1.0, - trim_tol: float = 1e-5, - trim_verbose: bool = False, - **options): + print( + f"{counter=:3d}, " + f"obj={self.objective_beta(self.beta):.2e}, " + f"eta={self.eta:.2e}, gamma={self.gamma:.2e} " + f"error={error:.2e}" + ) + + def fit( + self, + outlier_pct: float = 0.0, + trim_max_iter: int = 5, + trim_step_size: float = 1.0, + trim_tol: float = 1e-5, + trim_verbose: bool = False, + **options, + ): """Model fitting function. Parameters @@ -412,7 +430,7 @@ def fit(self, inlier_pct = 1 - outlier_pct if 0.0 < outlier_pct < 1.0: num_obs = self.data.obs.value.size - num_inliers = int(num_obs*inlier_pct) + num_inliers = int(num_obs * inlier_pct) self.weights = np.full(num_obs, inlier_pct) w = self.weights.copy() @@ -420,25 +438,29 @@ def fit(self, trim_counter = 0 if trim_verbose: - print(f"{trim_counter=:3d}, " - f"obj={self.objective_beta(self.beta):.2e}, " - f"{trim_error=:.2e}") + print( + f"{trim_counter=:3d}, " + f"obj={self.objective_beta(self.beta):.2e}, " + f"{trim_error=:.2e}" + ) while trim_error >= trim_tol and trim_counter < trim_max_iter: trim_counter += 1 trim_grad = self._objective() self.weights = proj_capped_simplex( - w - trim_step_size*trim_grad, num_inliers + w - trim_step_size * trim_grad, num_inliers ) trim_error = np.linalg.norm(w - self.weights) w = self.weights.copy() if trim_verbose: - print(f"{trim_counter=:3d}, " - f"obj={self.objective_beta(self.beta):.2e}, " - f"{trim_error=:.2e}") + print( + f"{trim_counter=:3d}, " + f"obj={self.objective_beta(self.beta):.2e}, " + f"{trim_error=:.2e}" + ) self._fit(**options) else: @@ -455,8 +477,8 @@ def get_inefficiency(self) -> NDArray: """ r = self.data.obs.value - self.mat.dot(self.beta) return np.maximum( - 0.0, -self.eta * r / (self.data.obs_se.value**2 + - self.eta + self.gamma) + 0.0, + -self.eta * r / (self.data.obs_se.value**2 + self.eta + self.gamma), ) def predict(self, df: pd.DataFrame) -> NDArray: diff --git a/tests/test_model.py b/tests/test_model.py index 843dd15..3f16f74 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,6 +1,7 @@ """ Test the model module """ + import numpy as np import pandas as pd import pytest @@ -11,32 +12,34 @@ def ad_jacobian(fun, x, out_shape=(), eps=1e-10): c = x + 0j if np.isscalar(x): - g = fun(x + eps*1j).imag/eps + g = fun(x + eps * 1j).imag / eps else: g = np.zeros((*out_shape, *x.shape)) if len(out_shape) == 0: for i in np.ndindex(x.shape): - c[i] += eps*1j - g[i] = fun(c).imag/eps - c[i] -= eps*1j + c[i] += eps * 1j + g[i] = fun(c).imag / eps + c[i] -= eps * 1j else: for j in np.ndindex(out_shape): for i in np.ndindex(x.shape): - c[i] += eps*1j - g[j][i] = fun(c)[j].imag/eps - c[i] -= eps*1j + c[i] += eps * 1j + g[j][i] = fun(c)[j].imag / eps + c[i] -= eps * 1j return g @pytest.fixture def df(): np.random.seed(123) - return pd.DataFrame({ - "obs": np.random.randn(10), - "obs_se": 0.2 + np.random.rand(10), - "var": np.random.randn(10), - "intercept": 1.0, - }) + return pd.DataFrame( + { + "obs": np.random.randn(10), + "obs_se": 0.2 + np.random.rand(10), + "var": np.random.randn(10), + "intercept": 1.0, + } + ) @pytest.fixture @@ -56,10 +59,7 @@ def uprior(): @pytest.fixture def variables(gprior, uprior): - return [ - Variable("intercept"), - Variable("var", priors=[gprior, uprior]) - ] + return [Variable("intercept"), Variable("var", priors=[gprior, uprior])] def test_mat(data, variables, df): @@ -70,8 +70,7 @@ def test_mat(data, variables, df): def test_gprior(data, variables, df): model = SFMAModel(data, variables, df=df) linear_gvec = model.parameter.prior_dict["direct"]["GaussianPrior"].params - assert np.allclose(linear_gvec, - np.array([[0.0, 0.0], [np.inf, 1.0]])) + assert np.allclose(linear_gvec, np.array([[0.0, 0.0], [np.inf, 1.0]])) def test_uprior(data, variables, df): @@ -79,7 +78,7 @@ def test_uprior(data, variables, df): assert np.allclose(model.cvec, np.array([0.0, 1.0])) -@pytest.mark.parametrize("beta", [np.arange(2)*1.0, np.ones(2)]) +@pytest.mark.parametrize("beta", [np.arange(2) * 1.0, np.ones(2)]) def test_gradient_beta(data, variables, df, beta): model = SFMAModel(data, variables, True, True, df=df) my_gradient = model.gradient_beta(beta) @@ -87,7 +86,7 @@ def test_gradient_beta(data, variables, df, beta): assert np.allclose(my_gradient, tr_gradient) -@pytest.mark.parametrize("beta", [np.arange(2)*1.0, np.ones(2)]) +@pytest.mark.parametrize("beta", [np.arange(2) * 1.0, np.ones(2)]) def test_hessian_beta(data, variables, df, beta): model = SFMAModel(data, variables, True, True, df=df) my_hess = model.hessian_beta(beta) From 9914ca087ef39887d74986c812427f9d8bf65900 Mon Sep 17 00:00:00 2001 From: saal Date: Tue, 23 Jul 2024 13:49:40 -0700 Subject: [PATCH 3/5] Added numpy <2 for reticulate 1.36 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c22e4bb..863ee2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "matplotlib", "msca", "xspline==0.0.7", + "numpy<2.0.0" ] [project.optional-dependencies] From f16a27bd3dbbc3992ab964cb407140d2b645a7ca Mon Sep 17 00:00:00 2001 From: saal Date: Fri, 11 Oct 2024 11:16:20 -0700 Subject: [PATCH 4/5] Updated deploy --- .github/workflows/deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b86a85d..91b4063 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,7 +10,6 @@ permissions: jobs: pypi: - if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 116b380c471fed30ec5a2cc6e943857c34311b77 Mon Sep 17 00:00:00 2001 From: saal Date: Fri, 11 Oct 2024 11:40:02 -0700 Subject: [PATCH 5/5] Updated anml to match pre-sxpline update --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 863ee2a..b798ce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ "Natural Language :: English", ] dependencies = [ - "anml", + "anml==0.2.1", "matplotlib", "msca", "xspline==0.0.7",