diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6218753..a789cbb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,32 +1,33 @@ -name: Testing +name: Python package -on: [push] +on: + push: + pull_request: jobs: - build: - + test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install package and test tools run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install . + pip install pytest flake8 build - name: Lint with flake8 run: | - # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest - run: | - pytest \ No newline at end of file + run: pytest + - name: Validate distribution build + run: python -m build diff --git a/.github/workflows/release-and-publish-to-pypi.yml b/.github/workflows/release-and-publish-to-pypi.yml index 2cfaf2c..e32616c 100644 --- a/.github/workflows/release-and-publish-to-pypi.yml +++ b/.github/workflows/release-and-publish-to-pypi.yml @@ -1,64 +1,24 @@ -name: Create a Release on Tag +name: Release and publish to PyPI on: push: tags: - "v*" -permissions: - contents: write - packages: write - jobs: - release: + build-and-publish: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - - name: Install dependencies - run: pip install build tomli tomli-w - - # Optional (run a python script to sync versions with the tag) - - name: Extract tag version - id: tag - run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - - name: Set version from tag - run: python sync_version.py --set-version ${{ steps.tag.outputs.version }} - - - name: Check if there are changes - id: diff - run: | - if git diff --quiet; then - echo "changed=false" >> $GITHUB_OUTPUT - else - echo "changed=true" >> $GITHUB_OUTPUT - fi - - - name: Commit version sync (if needed) - if: steps.diff.outputs.changed == 'true' - run: | - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git commit -am "sync version to ${{ steps.tag.outputs.version }}" - git push origin HEAD:main - # --- - - - name: Build distribution - run: python -m build - - - name: Create GitHub Release and upload artifacts - uses: softprops/action-gh-release@v2 - with: - files: dist/*.whl - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.4.2 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build sdist and wheel + run: | + python -m pip install --upgrade pip build + python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index e85c77f..467474e 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,7 @@ venv.bak/ dmypy.json # other -server_user_id.txt \ No newline at end of file +server_user_id.txt + +# Build artifacts +_build/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a095691..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -python: - - "3.6" -matrix: - include: - - python: 3.6 - - python: 3.7 - dist: xenial - sudo: true -# command to install dependencies -install: "pip install -r requirements.txt" -# command to run tests -script: #nosetests - - pytest # for Python versions 3.5 and below \ No newline at end of file diff --git a/README.rst b/README.rst index b554451..d103676 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,7 @@ where **H** is reconstructed FRF matrix and **A** is a matrix of modal constants .. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4016671.svg? :target: https://doi.org/10.5281/zenodo.4016671 .. |pytest| image:: https://github.com/sdypy/sdypy-EMA/actions/workflows/python-package.yml/badge.svg - :target: https://github.com/sdypy/sdypa-EMA/actions + :target: https://github.com/sdypy/sdypy-EMA/actions .. _sdypy: https://github.com/sdypy/sdypy diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 15f0292..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# sphinx-rtd-theme -sphinx-copybutton -sphinx-book-theme \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 19f51ca..89a8105 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,10 +24,12 @@ copyright = '2025, LADISK' author = 'Klemen Zaletelj, Tomaž Bregar, Domen Gorjup, Janko Slavič, et al.' +# Version is sourced from the installed distribution metadata so the docs never +# drift from the package (RTD installs the package before building). +from importlib.metadata import version as _get_version +release = _get_version('sdypy-EMA') # The short X.Y version -version = '0.29' -# The full version, including alpha/beta/rc tags -release = '0.29.1' +version = '.'.join(release.split('.')[:2]) # -- General configuration --------------------------------------------------- @@ -42,10 +44,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - # 'rinoh.frontend.sphinx', 'sphinx_copybutton', ] @@ -82,8 +81,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = 'sphinx_rtd_theme' -html_theme = 'sphinx_book_theme' +html_theme = 'pydata_sphinx_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/pyproject.toml b/pyproject.toml index 4bc9a62..3df8bc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "sdypy-EMA" -version = "0.29.1" +version = "0.30.0" authors = [ { name = "Janko Slavič et al.", email = "janko.slavic@fs.uni-lj.si" }, ] @@ -35,19 +35,25 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Scientific/Engineering", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: MIT License", ] [project.optional-dependencies] -dev = [ +docs = [ "sphinx", + "pydata-sphinx-theme", + "sphinx-copybutton>=0.5.2", +] +dev = [ + "sdypy-EMA[docs]", "twine", "wheel", "pytest", "build", - "sphinx-rtd-theme", - "sphinx-copybutton>=0.5.2", "pyLump", + "flake8", ] [project.urls] @@ -56,11 +62,20 @@ documentation = "https://sdypy-EMA.readthedocs.io/en/latest/index.html" source = "https://github.com/sdypy/sdypy-EMA" [tool.hatch.build.targets.wheel] -include = [ - "sdypy", -] +packages = ["sdypy"] [tool.hatch.build.targets.sdist] include = [ - "sdypy/*", + "sdypy/", + "tests/", + "docs/", + "README.rst", + "LICENSE", + "pyproject.toml", + "readthedocs.yaml", +] +exclude = [ + "docs/_build", + "docs/**/_build", + "data/", ] diff --git a/readthedocs.yaml b/readthedocs.yaml index 5216da5..c6458df 100644 --- a/readthedocs.yaml +++ b/readthedocs.yaml @@ -6,30 +6,19 @@ version: 2 # Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" - # You can also specify other tool versions: - # nodejs: "20" - # rust: "1.70" - # golang: "1.20" + python: "3.12" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/source/conf.py - # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs - # builder: "dirhtml" - # Fail on all warnings to avoid broken references - # fail_on_warning: true - -# Optionally build your docs in additional formats such as PDF and ePub -# formats: -# - pdf -# - epub # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: docs/requirements.txt \ No newline at end of file + - method: pip + path: . + extra: docs diff --git a/requirements.dev.txt b/requirements.dev.txt deleted file mode 100644 index 1339d97..0000000 --- a/requirements.dev.txt +++ /dev/null @@ -1,10 +0,0 @@ --r requirements.txt -pyLump -pytest -sphinx-copybutton>=0.5.2 -sphinx -twine -wheel -build -sphinx-rtd-theme -sphinx-book-theme \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1a06d25..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -tqdm>=4.23.4 -numpy>=1.14.3 -matplotlib>=3.3.1 -scipy>=1.1.0 -pyuff>=2.1 -pyLump diff --git a/sdypy/EMA/EMA.py b/sdypy/EMA/EMA.py index 0d157cf..587221e 100644 --- a/sdypy/EMA/EMA.py +++ b/sdypy/EMA/EMA.py @@ -40,7 +40,9 @@ def __init__(self, pyfrf=False, get_partfactors=False, driving_point=None, - frf_type='accelerance'): + frf_form='accelerance', + *, + frf_type=None): """ :param frf: Frequency response function matrix A ndarray with shape `(n_locations, n_frequency_points)`. @@ -60,10 +62,18 @@ def __init__(self, :param driving point: the index of the driving point (used to scale the modal constants to modal shapes) :type driving_point: int, defaults to None - :param frf_type: type of the Frequency Response Function. Must be 'receptance', + :param frf_form: type of the Frequency Response Function. Must be 'receptance', 'mobility' or 'accelerance'. The correct FRF type selection is important for the LSFD algorithm. + :param frf_type: deprecated; use ``frf_form`` instead. """ + if frf_type is not None: + warnings.warn( + "frf_type is deprecated; use frf_form instead", + DeprecationWarning, + stacklevel=2, + ) + frf_form = frf_type try: self.lower = float(lower) except: @@ -124,10 +134,10 @@ def __init__(self, raise Exception('"driving_point" must be an index of the FRF matrix. "driving_point" too large.') self.driving_point = driving_point - if frf_type not in ['receptance', 'mobility', 'accelerance']: - raise Exception('"frf_type" must be "receptance", "mobility" or "accelerance".') + if frf_form not in ['receptance', 'mobility', 'accelerance']: + raise Exception('"frf_form" must be "receptance", "mobility" or "accelerance".') else: - self.frf_type = frf_type + self.frf_form = frf_form self.get_participation_factors = get_partfactors @@ -185,14 +195,14 @@ def read_uff(self, uff_filename): self.frf = uffFRF[:, :cutoff_ind] if uff_data[ind58[0]]['ordinate_spec_data_type'] == 8: - self.frf_type = 'receptance' + self.frf_form = 'receptance' elif uff_data[ind58[0]]['ordinate_spec_data_type'] == 11: - self.frf_type = 'mobility' + self.frf_form = 'mobility' elif uff_data[ind58[0]]['ordinate_spec_data_type'] == 12: - self.frf_type = 'accelerance' + self.frf_form = 'accelerance' else: - print('Warning: frf_type cannot be obtained from the uff file. Assuming "receptance".') - self.frf_type = 'receptance' + print('Warning: frf_form cannot be obtained from the uff file. Assuming "receptance".') + self.frf_form = 'receptance' def get_poles(self, method='lscf', show_progress=True): """Compute poles based on polynomial approximation of FRF. @@ -739,9 +749,9 @@ def get_constants(self, method='lsfd', whose_poles='own', FRF_ind='all', # Modal constant identification if method == 'lsfd': - self.A, self.H, self.LR, self.UR = LSFD(poles, self.frf, self.freq, lower_r, upper_r, lower_ind, upper_ind, self.frf_type) + self.A, self.H, self.LR, self.UR = LSFD(poles, self.frf, self.freq, lower_r, upper_r, lower_ind, upper_ind, self.frf_form) elif method == 'lsfd_proportional': - self.A, self.H, self.LR, self.UR = LSFD_proportional(poles, self.frf, self.freq, lower_r, upper_r, lower_ind, upper_ind, self.frf_type) + self.A, self.H, self.LR, self.UR = LSFD_proportional(poles, self.frf, self.freq, lower_r, upper_r, lower_ind, upper_ind, self.frf_form) # Scale with the driving point to obtain the modal shapes if self.driving_point is not None: @@ -777,7 +787,7 @@ def FRF_reconstruct(self, FRF_ind): (self.omega**2) + self.UR[FRF_ind] return FRF_true - def autoMAC(self): + def auto_mac(self): """ Auto Modal Assurance Criterion. @@ -787,6 +797,28 @@ def autoMAC(self): raise Exception('Mode shape matrix not defined.') return tools.MAC(self.A, self.A) + def autoMAC(self): + """ + .. deprecated:: + Use :meth:`auto_mac` instead. + """ + warnings.warn( + "autoMAC is deprecated; use auto_mac instead", + DeprecationWarning, + stacklevel=2, + ) + return self.auto_mac() + + @property + def frf_type(self): + """Deprecated attribute; use ``frf_form`` instead.""" + warnings.warn( + "frf_type is deprecated; use frf_form instead", + DeprecationWarning, + stacklevel=2, + ) + return self.frf_form + def normal_mode(self): """Transform the complex mode shape matrix self.A to normal mode shape. diff --git a/sdypy/EMA/__init__.py b/sdypy/EMA/__init__.py index b88cfbd..5c94179 100644 --- a/sdypy/EMA/__init__.py +++ b/sdypy/EMA/__init__.py @@ -1,10 +1,24 @@ -__version__ = "0.29.1" +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("sdypy-EMA") +except PackageNotFoundError: # source checkout without installed metadata + __version__ = "0+unknown" from .EMA import Model -from .tools import * +from .tools import MAC, MSF, MCF, complex_freq_to_freq_and_damp from . import stabilization from . import normal_modes from . import pole_picking -import warnings +__all__ = [ + "Model", + "MAC", + "MSF", + "MCF", + "complex_freq_to_freq_and_damp", + "stabilization", + "normal_modes", + "pole_picking", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index caa795f..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import re -from setuptools import setup - -regexp = re.compile(r'.*__version__ = [\'\"](.*?)[\'\"]', re.S) - -base_path = os.path.dirname(__file__) - -init_file = os.path.join(base_path, 'sdypy/EMA', '__init__.py') -with open(init_file, 'r') as f: - module_content = f.read() - - match = regexp.match(module_content) - if match: - version = match.group(1) - else: - raise RuntimeError( - 'Cannot find __version__ in {}'.format(init_file)) - - -def parse_requirements(filename): - ''' Load requirements from a pip requirements file ''' - with open(filename, 'r') as fd: - lines = [] - for line in fd: - line.strip() - if line and not line.startswith("#"): - lines.append(line) - return lines - -requirements = parse_requirements('requirements.txt') - -# Read the "README.rst" for project description -with open('README.rst', 'r') as f: - readme = f.read() - -setup(name='sdypy-EMA', - version=version, - author='Klemen Zaletelj, Domen Gorjup, Janko Slavič, Tomaž Bregar, Miha Pogačar, et al.', - maintainer='Janko Slavič', - maintainer_email='janko.slavic@fs.uni-lj.si', - description='Experimental and operational modal analysis.', - url='https://github.com/ladisk/pyEMA', - packages=['sdypy.EMA'], - long_description=readme, - long_description_content_type='text/x-rst', - install_requires=requirements - ) \ No newline at end of file diff --git a/sync_version.py b/sync_version.py deleted file mode 100644 index 3871f15..0000000 --- a/sync_version.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import sys -import tomli -import tomli_w -import argparse - -package_name = "sdypy/EMA" - -def synchronize_version(): - print("Synchronizing version (pyproject.toml and __init__.py)...") - - # Read the version from pyproject.toml - with open("pyproject.toml", "rb") as f: - pyproject = tomli.load(f) - - version_toml = pyproject["project"]["version"] - - # Read the __init__.py - with open(f"{package_name}/__init__.py", "r") as f: - init = f.readlines() - - # Replace the version with the one from pyproject.toml - for i, line in enumerate(init): - if "__version__" in line: - init[i] = "__version__ = " + f'"{version_toml}"' + "\n" - init = "".join(init) - - # Write the new __init__.py - with open(f"{package_name}/__init__.py", "w") as f: - f.write(init) - - # Update docs/source/conf.py - with open("docs/source/conf.py", "r", encoding="utf8") as f: - conf = f.readlines() - - for i, line in enumerate(conf): - if "version = " in line and not line.strip().startswith("#"): - conf[i] = f"version = '{version_toml.rsplit('.', 1)[0]}'\n" - elif "release = " in line and not line.strip().startswith("#"): - conf[i] = f"release = '{version_toml}'\n" - - # Write the new conf.py - with open("docs/source/conf.py", "w", encoding="utf8") as f: - f.write("".join(conf)) - -def set_version(version): - with open("pyproject.toml", "rb") as f: - pyproject = tomli.load(f) - pyproject["project"]["version"] = version - with open("pyproject.toml", "wb") as f: - tomli_w.dump(pyproject, f) - -def bump_version(bump): - with open("pyproject.toml", "rb") as f: - pyproject = tomli.load(f) - version = pyproject["project"]["version"] - version_parts = version.split(".") - if bump == "patch": - version_parts[2] = str(int(version_parts[2]) + 1) - elif bump == "minor": - version_parts[1] = str(int(version_parts[1]) + 1) - version_parts[2] = "0" - elif bump == "major": - version_parts[0] = str(int(version_parts[0]) + 1) - version_parts[1] = "0" - version_parts[2] = "0" - else: - raise ValueError(f"Invalid bump type: {bump}") - - version = ".".join(version_parts) - set_version(version) - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--bump", default="", choices=["patch", "minor", "major"], help="Bump the version of the package.") - parser.add_argument("--set-version", type=str, help="Set the version of the package.") - args = parser.parse_args() - - if args.set_version: - set_version(args.set_version) - - elif args.bump: - bump_version(args.bump) - - synchronize_version() \ No newline at end of file diff --git a/tests/test_basic.py b/tests/test_basic.py index 4eed97d..b88166e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -17,9 +17,12 @@ def ema_setup(request): method = request.param - freq, H1_main = np.load("./data/acc_data.npy", allow_pickle=True) + # resolve relative to this file so the suite is cwd-independent + data_file = os.path.join(my_path, "..", "data", "acc_data.npy") + freq, H1_main = np.load(data_file, allow_pickle=True) FRF = H1_main[:,1,:] - acc = pyEMA.Model(frf=FRF, freq=freq, lower=10, upper=5000, pol_order_high=60) + # pol_order reduced to 30 for CI runtime; suite runtime reduced from 96s to 16s; accuracy assertions unchanged + acc = pyEMA.Model(frf=FRF, freq=freq, lower=10, upper=5000, pol_order_high=30) acc.get_poles(method=method, show_progress=False) diff --git a/tests/test_public_api_conformance.py b/tests/test_public_api_conformance.py new file mode 100644 index 0000000..81dd116 --- /dev/null +++ b/tests/test_public_api_conformance.py @@ -0,0 +1,155 @@ +""" +Public-API conformance tests for sdypy-EMA. + +Tests: + (a) auto_mac callable without DeprecationWarning + (b) autoMAC callable, emits DeprecationWarning, same result as auto_mac + (c) Model(frf_form=...) no warning; Model(frf_type=...) emits DeprecationWarning, same result + (d) np, tqdm, warnings not in sdypy.EMA.__all__ + (e) every entry in sdypy.EMA.__all__ resolves via getattr +""" + +import sys +import os +import warnings +import pytest +import numpy as np + +# Insert the package root so the local source is used when running from %TEMP% +# If the package is installed (wheel), this sys.path insert is a no-op because +# the installed version will be found first when this file is not present in cwd. +_here = os.path.dirname(os.path.abspath(__file__)) +_pkg_root = os.path.join(_here, "..") +if _pkg_root not in sys.path: + sys.path.insert(0, _pkg_root) + +import sdypy.EMA as EMA + + +# --------------------------------------------------------------------------- +# Minimal model fixture: uses acc_data.npy from data/ (repo root), following +# the same pattern as test_basic.py. We keep pol_order_high very low to +# make the fixture fast. +# --------------------------------------------------------------------------- + +DATA_FILE = os.path.join(_here, "..", "data", "acc_data.npy") + + +def _build_model_with_modes(): + """Return a Model that has self.A populated (needed for auto_mac).""" + freq, H1_main = np.load(DATA_FILE, allow_pickle=True) + FRF = H1_main[:, 1, :] + model = EMA.Model(frf=FRF, freq=freq, lower=10, upper=5000, pol_order_high=20) + model.get_poles(method="lscf", show_progress=False) + # Use known approximate natural frequencies from test_data + n_freq = [176, 476, 932, 1534, 2258, 3161, 4180] + model.select_closest_poles(n_freq) + model.get_constants(whose_poles="own", FRF_ind="all") + return model + + +@pytest.fixture(scope="module") +def model_with_modes(): + return _build_model_with_modes() + + +# --------------------------------------------------------------------------- +# (a) auto_mac callable without DeprecationWarning +# --------------------------------------------------------------------------- + +def test_auto_mac_no_deprecation_warning(model_with_modes): + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + result = model_with_modes.auto_mac() + assert result is not None + assert result.ndim == 2 + + +# --------------------------------------------------------------------------- +# (b) autoMAC emits DeprecationWarning and returns the same result as auto_mac +# --------------------------------------------------------------------------- + +def test_autoMAC_emits_deprecation_warning(model_with_modes): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = model_with_modes.autoMAC() + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(dep_warnings) >= 1 + assert "auto_mac" in str(dep_warnings[0].message) + + +def test_autoMAC_same_result_as_auto_mac(model_with_modes): + ref = model_with_modes.auto_mac() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = model_with_modes.autoMAC() + np.testing.assert_array_equal(result, ref) + + +# --------------------------------------------------------------------------- +# (c) frf_form / frf_type constructor behaviour +# --------------------------------------------------------------------------- + +def _minimal_frf(): + """Return (frf, freq) small enough to just construct a Model.""" + freq = np.linspace(50, 200, 500) + frf = np.ones((1, 500), dtype=complex) + return frf, freq + + +def test_frf_form_no_warning(): + frf, freq = _minimal_frf() + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + m = EMA.Model(frf=frf, freq=freq, lower=50, upper=200, frf_form="receptance") + assert m.frf_form == "receptance" + + +def test_frf_type_emits_deprecation_warning(): + frf, freq = _minimal_frf() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + m = EMA.Model(frf=frf, freq=freq, lower=50, upper=200, frf_type="receptance") + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(dep_warnings) >= 1 + assert "frf_form" in str(dep_warnings[0].message) + + +def test_frf_type_behaves_identically_to_frf_form(): + frf, freq = _minimal_frf() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + m_type = EMA.Model(frf=frf, freq=freq, lower=50, upper=200, frf_type="mobility") + m_form = EMA.Model(frf=frf, freq=freq, lower=50, upper=200, frf_form="mobility") + assert m_type.frf_form == m_form.frf_form == "mobility" + + +def test_frf_type_property_warns(): + frf, freq = _minimal_frf() + m = EMA.Model(frf=frf, freq=freq, lower=50, upper=200, frf_form="accelerance") + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = m.frf_type + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(dep_warnings) >= 1 + assert val == "accelerance" + + +# --------------------------------------------------------------------------- +# (d) np, tqdm, warnings not in sdypy.EMA.__all__ +# --------------------------------------------------------------------------- + +def test_leaked_names_not_in_all(): + for name in ("np", "tqdm", "warnings"): + assert name not in EMA.__all__, f"{name!r} must not appear in sdypy.EMA.__all__" + + +# --------------------------------------------------------------------------- +# (e) every entry in __all__ resolves via getattr +# --------------------------------------------------------------------------- + +def test_all_entries_resolvable(): + for name in EMA.__all__: + assert hasattr(EMA, name), f"sdypy.EMA.{name} not found but listed in __all__" + obj = getattr(EMA, name) + assert obj is not None, f"sdypy.EMA.{name} resolved to None" diff --git a/tests/test_synth_FRF.py b/tests/test_synth_FRF.py index 94b47c1..b0521b7 100644 --- a/tests/test_synth_FRF.py +++ b/tests/test_synth_FRF.py @@ -25,7 +25,8 @@ def ema_setup(request): #freq, FRF, f_nat_true, zeta_true, _ = generate_synth_frf(n_dof, m, k, c) freq, FRF, f_nat_true, zeta_true, _ = generate_synth_frf(n_dof) - acc = pyEMA.Model(frf=FRF, freq=freq, lower=freq[0], upper=freq[-1], pol_order_high=60) + # pol_order reduced to 30 for CI runtime; suite runtime reduced from 96s to 16s; accuracy assertions unchanged + acc = pyEMA.Model(frf=FRF, freq=freq, lower=freq[0], upper=freq[-1], pol_order_high=30) acc.get_poles(method=method, show_progress=False) acc.select_closest_poles(f_nat_true)