Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/check_changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Changelog updated

on:
pull_request:
types: [opened, reopened, labeled, synchronize]

jobs:
check_log:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get changes
run: |
set +e # Don't immediately fail when bash sees exit 1
git diff --exit-code "origin/${GITHUB_BASE_REF}" -- CHANGELOG.rst
if [ $? -eq 1 ]; then
exit 0
fi
# git-diff did not return 1; either no changes or another error encountered
exit 1
46 changes: 45 additions & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ on:
pull_request:
types: [opened, reopened, labeled, synchronize]
workflow_dispatch:
inputs:
push_coverage_badge:
description: 'Push coverage badge (even if not running on master)'
required: true
type: boolean
default: false

jobs:
lint:
Expand Down Expand Up @@ -97,4 +103,42 @@ jobs:
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
with:
junit_files: artifacts/**/junit_report*.xml
files: artifacts/**/junit_report*.xml

publish-coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
name: 'Coverage XML (ubuntu-latest)'

- name: Get Coverage
uses: orgoro/coverage@v3.2
continue-on-error: true
with:
coverageFile: 'artifacts/coverage-combined.xml'
token: ${{ secrets.GITHUB_TOKEN }}

publish-coverage-badge:
needs: test
runs-on: ubuntu-latest
if: ${{(github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'workflow_dispatch' && inputs.push_coverage_badge)}}
steps:
- uses: actions/checkout@v6
with:
ref: badges
fetch-depth: 0

- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: .
name: 'Coverage Badge (ubuntu-latest)'

- uses: EndBug/add-and-commit@v9
with:
add: 'badge_shieldsio_linecoverage_lightgrey.svg'
message: 'Update coverage badge'
39 changes: 31 additions & 8 deletions .github/workflows/test_checkout_one_os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,39 @@ jobs:
name: Unit test results ${{ inputs.os }}
path: tests_and_analysis/test/reports/junit_report*.xml

- name: Publish Codacy coverage
uses: codacy/codacy-coverage-reporter-action@v1
- name: Merge coverage reports
if: inputs.coverage
shell: bash -l {0}
run: |
dotnet tool install -g dotnet-coverage
dotnet coverage merge --reports "tests_and_analysis/test/reports/coverage*.xml" -f cobertura -o "tests_and_analysis/test/reports/coverage-combined.xml"

- name: Create coverage outputs
if: inputs.coverage
shell: bash -l {0}
run: |
dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.5.1
reportgenerator -reports:tests_and_analysis/test/reports/coverage-combined.xml -targetdir:tests_and_analysis/test/reports/html -reporttypes:Html_Dark
reportgenerator -reports:tests_and_analysis/test/reports/coverage-combined.xml -targetdir:tests_and_analysis/test/reports -reporttypes:Badges

- name: Upload coverage XML
if: inputs.coverage
uses: actions/upload-artifact@v4
with:
project-token: ${{ secrets.codacy_project_token }}
coverage-reports: tests_and_analysis/test/reports/coverage*.xml
name: Coverage XML (${{ inputs.os }})
path: tests_and_analysis/test/reports/coverage-combined.xml

- uses: codecov/codecov-action@v4
- name: Upload coverage HTML
if: inputs.coverage
uses: actions/upload-artifact@v4
with:
name: Coverage HTML (${{ inputs.os }})
path: tests_and_analysis/test/reports/html
include-hidden-files: true

- name: Upload coverage Badge
if: inputs.coverage
uses: actions/upload-artifact@v4
with:
files: tests_and_analysis/test/reports/coverage*.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
name: Coverage Badge (${{ inputs.os }})
path: tests_and_analysis/test/reports/badge_shieldsio_linecoverage_lightgrey.svg
43 changes: 41 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
`Unreleased <https://github.com/pace-neutrons/Euphonic/compare/v1.6.0...HEAD>`_
-------------------------------------------------------------------------------

- API changes

- Public functions in ``euphonic.powder`` now use mandatory keyword arguments
- This will break code that depends on these arguments being in a specific order

- The various Spectrum classes have an ``assert_regular_bins``
method. It is now forbidden to use positional arguments and in the
2D cases ``bin_ax`` has a default value of "y". This makes the API
safer and more formally correct.

- Unused argument ``use_brille`` is removed from
``euphonic.cli.brille_convergence.check_brille_settings``.

- "Adaptive fit" parameter is removed from spectrum broaden()
methods and euphonic-dos; "cubic" parametrisation is removed and
superior "cheby-log" fit always used.

- Features

- Spectrum1DCollection and Spectrum2DCollection can be indexed with
slices where the stop value exceeds the collection
length. (e.g. if a collection of 5 spectra is indexed with [3:10]
it will return a collection with the spectra at indices 3 and 4.)
This is consistent with the behaviour of Python lists and numpy
arrays.

Previously this would raise an IndexError. Technically it is a
**breaking change** as somebody's code could depend on this
IndexError. At this stage it seems an acceptable risk.

- Other changes

- Error messages have been overhauled and now follow a consistent format::

summary

[reason]

fix

`v1.6.0 <https://github.com/pace-neutrons/Euphonic/compare/v1.5.1...v1.6.0>`_
-----------------------------------------------------------------------------

- Requirements

Expand All @@ -14,7 +53,7 @@
- Security

- Bumped *wheel* requirement for docs and testing to 0.46.2. (`CVE-2026-24049 <https://www.cve.org/CVERecord?id=CVE-2026-24049>`_)


- Bug fixes

Expand Down
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Euphonic
========

|PyPI Version| |Conda Version| |Documentation Status| |Tests| |License| |DOI|
|PyPI Version| |Conda Version| |Documentation Status| |Tests| |Coverage| |License| |DOI|

.. |PyPI Version| image:: https://img.shields.io/pypi/v/euphonic
:target: https://pypi.org/project/euphonic/
Expand All @@ -20,6 +20,9 @@ Euphonic
:target: https://github.com/pace-neutrons/Euphonic/actions/workflows/run_tests.yml
:alt: Tests

.. |Coverage| image:: https://raw.githubusercontent.com/pace-neutrons/Euphonic/refs/heads/badges/badge_shieldsio_linecoverage_lightgrey.svg
:target: https://github.com/pace-neutrons/Euphonic/actions/workflows/run_tests.yml :alt: Coverage

.. |License| image:: https://img.shields.io/pypi/l/euphonic
:target: https://github.com/pace-neutrons/Euphonic/blob/master/LICENSE
:alt: License
Expand Down
15 changes: 11 additions & 4 deletions euphonic/brille.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import brille as br
except ModuleNotFoundError as err:
err_msg = textwrap.dedent("""
Cannot import Brille for use with BrilleInterpolator
(maybe Brille is not installed?). To install Euphonic's
optional Brille dependency, try:
Cannot import Brille for use with BrilleInterpolator.

This may be because Brille is not installed.

To install Euphonic's optional Brille dependency, try:

pip install euphonic[brille]
""")
Expand All @@ -25,6 +27,7 @@
QpointPhononModes,
ureg,
)
from euphonic.util import comma_join, format_error
from euphonic.validate import _check_constructor_inputs


Expand Down Expand Up @@ -194,7 +197,11 @@ def from_force_constants(
"""
grid_type_opts = ('trellis', 'mesh', 'nest')
if grid_type not in grid_type_opts:
msg = f'Grid type "{grid_type}" not recognised'
msg = format_error(
f'Grid type "{grid_type}" not recognised.',
fix=('Acceptable grid types are:'
f' {comma_join(grid_type_opts)}.'),
)
raise ValueError(msg)

crystal = force_constants.crystal
Expand Down
54 changes: 16 additions & 38 deletions euphonic/broadening.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
from scipy.stats import norm

from euphonic.ureg import ureg
from euphonic.util import dedent_and_fill
from euphonic.util import format_error

ErrorFit = Literal['cheby-log', 'cubic']
KernelShape = Literal['gauss', 'lorentz']


Expand All @@ -33,7 +32,6 @@ def variable_width_broadening(
width_convention: Literal['fwhm', 'std'] = 'fwhm',
adaptive_error: float = 1e-2,
shape: KernelShape = 'gauss',
fit: ErrorFit = 'cheby-log',
) -> Quantity:
r"""Apply x-dependent Gaussian broadening to 1-D data series

Expand Down Expand Up @@ -70,25 +68,24 @@ def variable_width_broadening(
approximate gaussians.
shape
Select broadening kernel function.
fit
Select parametrisation of kernel width spacing to adaptive_error.
'cheby-log' is recommended: for shape 'gauss', 'cubic' is also
available.
"""

if width_convention.lower() == 'fwhm' and shape == 'gauss':
def sigma_function(x: Quantity) -> Quantity:
return width_function(x) * FWHM_TO_SIGMA
elif width_convention.lower() == 'std' and shape == 'lorentz':
msg = (
'Standard deviation unavailable for Lorentzian '
'function: please use FWHM.'
msg = format_error(
'Standard deviation unavailable.',
fix='For Lorentzian function: please use FWHM.',
)
raise ValueError(msg)
elif width_convention.lower() in ('std', 'fwhm'):
sigma_function = width_function
else:
msg = 'width_convention must be "std" or "fwhm".'
msg = format_error(
f'Invalid width convention: {width_convention}.',
fix='`width_convention` must be "std" or "fwhm".',
)
raise ValueError(msg)

widths = sigma_function(x)
Expand All @@ -107,8 +104,7 @@ def sigma_function(x: Quantity) -> Quantity:
return width_interpolated_broadening(bins, x, widths,
weights.magnitude,
adaptive_error=adaptive_error,
shape=shape,
fit=fit) * weights_unit
shape=shape) * weights_unit


def width_interpolated_broadening(
Expand All @@ -118,7 +114,6 @@ def width_interpolated_broadening(
weights: np.ndarray,
adaptive_error: float,
shape: KernelShape = 'gauss',
fit: ErrorFit = 'cheby-log',
) -> Quantity:
"""
Uses a fast, approximate method to broaden a spectrum
Expand Down Expand Up @@ -147,10 +142,6 @@ def width_interpolated_broadening(
shape
Select kernel shape. Widths will correspond to sigma or gamma
parameters respectively.
fit
Select parametrisation of kernel width spacing to adaptive_error.
'cheby-log' is recommended: for shape 'gauss', 'cubic' is also
available.

Returns
-------
Expand All @@ -164,33 +155,20 @@ def width_interpolated_broadening(
widths.to(bins.units).magnitude,
weights,
adaptive_error,
shape=shape,
fit=fit) / bins.units
shape=shape) / bins.units


def _lorentzian(x: np.ndarray, gamma: np.ndarray) -> np.ndarray:
return gamma / (2 * np.pi * (x**2 + (gamma / 2)**2))


def _get_spacing(error,
shape: KernelShape = 'gauss',
fit: ErrorFit = 'cheby-log'):
shape: KernelShape = 'gauss'):
"""
Determine suitable spacing value for mode_width given accepted error level

Coefficients have been fitted to plots of error vs spacing value
"""

if fit == 'cubic' and shape == 'gauss':
return np.polyval([612.7, -122.7, 15.40, 1.0831], error)

if fit != 'cheby-log':
msg = dedent_and_fill(f"""
Fit "{fit}" is not available for shape "{shape}". The "cheby-log"
fit is recommended for "gauss" and "Lorentz" shapes.'
""")
raise ValueError(msg)

if shape == 'lorentz':
cheby = Chebyshev(
[1.26039672, 0.39900457, 0.20392176, 0.08602507,
Expand All @@ -209,9 +187,9 @@ def _get_spacing(error,

log_error = np.log10(error)
if not safe_domain[0] < log_error < safe_domain[1]:
msg = (
'Target error is out of fit range; value must lie '
f'in range {np.power(10, safe_domain)}.'
msg = format_error(
f'Target error ({error}) is out of fit range.',
fix=f'Value must lie in range {np.power(10, safe_domain)}.',
)
raise ValueError(msg)
return cheby(log_error)
Expand All @@ -224,7 +202,7 @@ def _width_interpolated_broadening(
weights: np.ndarray,
adaptive_error: float,
shape: KernelShape = 'gauss',
fit: ErrorFit = 'cheby-log') -> np.ndarray:
) -> np.ndarray:
"""
Broadens a spectrum using a variable-width kernel, taking the
same arguments as `variable_width` but expects arrays with
Expand All @@ -234,7 +212,7 @@ def _width_interpolated_broadening(
x = np.ravel(x)
widths = np.ravel(widths)
weights = np.ravel(weights)
spacing = _get_spacing(adaptive_error, shape=shape, fit=fit)
spacing = _get_spacing(adaptive_error, shape=shape)

# bins should be regularly spaced, check that this is the case and
# raise a warning if not
Expand Down
Loading
Loading